Initial commit: Import existing Laravel project
This commit is contained in:
390
resources/js/Pages/Commandes/Form.vue
Normal file
390
resources/js/Pages/Commandes/Form.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
order: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Calcul de la date par défaut (+30 jours) au format YYYY-MM-DD
|
||||
const getDefaultDeadline = () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Initialisation du formulaire Inertia
|
||||
const form = useForm({
|
||||
label: props.order?.data?.label || props.order?.label || '',
|
||||
type: props.order?.data?.type || props.order?.type || 'Matériel réseau / serveur',
|
||||
supplier: props.order?.data?.supplier || props.order?.supplier || '',
|
||||
quote_number: props.order?.data?.quote_number || props.order?.quote_number || '',
|
||||
amount_ht: props.order?.data?.amount_ht !== undefined && props.order?.data?.amount_ht !== null
|
||||
? String(props.order.data.amount_ht)
|
||||
: (props.order?.amount_ht !== undefined && props.order?.amount_ht !== null
|
||||
? String(props.order.amount_ht)
|
||||
: ''),
|
||||
requested_by: props.order?.data?.requested_by || props.order?.requested_by || 'Jérémy',
|
||||
prescriber: props.order?.data?.prescriber || props.order?.prescriber || '',
|
||||
delivery_deadline: props.order?.data?.delivery_deadline || props.order?.delivery_deadline || getDefaultDeadline(),
|
||||
notes: props.order?.data?.notes || props.order?.notes || '',
|
||||
exclude_vat: props.order?.data?.exclude_vat || props.order?.exclude_vat || false,
|
||||
quote_file: null,
|
||||
delivery_note_file: null,
|
||||
invoice_file: null,
|
||||
_method: props.isEdit ? 'put' : 'post', // Pour supporter l'upload de fichier en PHP/Laravel lors de la mise à jour
|
||||
});
|
||||
|
||||
// Calcul automatique du montant TTC (HT + 20% TVA) en temps réel
|
||||
const amountTtc = computed(() => {
|
||||
const ht = parseFloat(form.amount_ht);
|
||||
if (isNaN(ht) || ht < 0) return 0;
|
||||
if (form.exclude_vat) return ht.toFixed(2);
|
||||
return (ht * 1.20).toFixed(2);
|
||||
});
|
||||
|
||||
// Récupérer le fichier en cours d'édition si présent
|
||||
const getExistingFile = (type) => {
|
||||
const attachments = props.order?.data?.attachments || props.order?.attachments;
|
||||
if (!attachments) return null;
|
||||
return attachments.find(a => a.file_type === type);
|
||||
};
|
||||
|
||||
// Soumission du formulaire
|
||||
const submit = () => {
|
||||
if (props.isEdit) {
|
||||
const orderId = props.order?.data?.id || props.order?.id;
|
||||
form.post(route('commandes.update', { commande: orderId }), {
|
||||
forceFormData: true,
|
||||
});
|
||||
} else {
|
||||
form.post(route('commandes.store'));
|
||||
}
|
||||
};
|
||||
|
||||
const orderTypes = [
|
||||
'Matériel réseau / serveur',
|
||||
'Licences logicielles',
|
||||
'Consommables / câblage',
|
||||
'Prestations / services',
|
||||
];
|
||||
|
||||
const demandeurs = ['Jérémy', 'Sylvain', 'Kévin'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="isEdit ? `Modifier la Commande ${order?.data?.number || order?.number}` : 'Nouvelle Commande'" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
|
||||
{{ isEdit ? `Modifier la Commande ${order?.data?.number || order?.number}` : 'Nouvelle Demande de Commande' }}
|
||||
</h2>
|
||||
<Link
|
||||
:href="isEdit ? route('commandes.show', { commande: order?.data?.id || order?.id }) : route('commandes.index')"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Retour
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Formulaire -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
||||
<form @submit.prevent="submit" class="p-6 space-y-6">
|
||||
|
||||
<!-- Détails Article -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Informations sur l'article
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
Saisissez la référence ou le libellé principal du matériel ou service demandé.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Libellé article -->
|
||||
<div class="md:col-span-3">
|
||||
<InputLabel for="label" value="Libellé / Référence article" class="font-bold" />
|
||||
<TextInput
|
||||
id="label"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.label"
|
||||
required
|
||||
placeholder="Ex: Serveur NAS Synology 8 baies ou 5x Licences Office 365"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.label" />
|
||||
</div>
|
||||
|
||||
<!-- Type de commande -->
|
||||
<div>
|
||||
<InputLabel for="type" value="Type de commande" class="font-bold" />
|
||||
<select
|
||||
id="type"
|
||||
v-model="form.type"
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
required
|
||||
>
|
||||
<option v-for="type in orderTypes" :key="type" :value="type">
|
||||
{{ type }}
|
||||
</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.type" />
|
||||
</div>
|
||||
|
||||
<!-- Demandeur -->
|
||||
<div>
|
||||
<InputLabel for="requested_by" value="Demandeur" class="font-bold" />
|
||||
<select
|
||||
id="requested_by"
|
||||
v-model="form.requested_by"
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
required
|
||||
>
|
||||
<option v-for="d in demandeurs" :key="d" :value="d">
|
||||
{{ d }}
|
||||
</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.requested_by" />
|
||||
</div>
|
||||
|
||||
<!-- Prescripteur -->
|
||||
<div>
|
||||
<InputLabel for="prescriber" value="Prescripteur / Service à l'origine" class="font-bold" />
|
||||
<TextInput
|
||||
id="prescriber"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.prescriber"
|
||||
required
|
||||
placeholder="Ex: Service RH, Urbanisme..."
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.prescriber" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fournisseur & Devis -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Fournisseur & Devis
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
Références d'acquisition et détails financiers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Fournisseur -->
|
||||
<div>
|
||||
<InputLabel for="supplier" value="Fournisseur" class="font-bold" />
|
||||
<TextInput
|
||||
id="supplier"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.supplier"
|
||||
required
|
||||
placeholder="Ex: LDLC Pro, Dell France..."
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.supplier" />
|
||||
</div>
|
||||
|
||||
<!-- Numéro de devis -->
|
||||
<div>
|
||||
<InputLabel for="quote_number" value="Numéro de devis" class="font-bold" />
|
||||
<TextInput
|
||||
id="quote_number"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.quote_number"
|
||||
required
|
||||
placeholder="Ex: DEV-2026-99182"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.quote_number" />
|
||||
</div>
|
||||
|
||||
<!-- Montant HT -->
|
||||
<div>
|
||||
<InputLabel for="amount_ht" value="Montant HT (€)" class="font-bold" />
|
||||
<TextInput
|
||||
id="amount_ht"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.amount_ht"
|
||||
required
|
||||
placeholder="0.00"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.amount_ht" />
|
||||
|
||||
<div class="mt-3 flex items-center">
|
||||
<input
|
||||
id="exclude_vat"
|
||||
type="checkbox"
|
||||
v-model="form.exclude_vat"
|
||||
class="rounded border-gray-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-gray-700 dark:bg-gray-900"
|
||||
/>
|
||||
<label for="exclude_vat" class="ml-2 text-xs font-semibold text-slate-600 dark:text-slate-400 cursor-pointer select-none">
|
||||
Exonérer de TVA / Non soumis à la TVA
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Montant TTC (calculé) -->
|
||||
<div>
|
||||
<InputLabel for="amount_ttc" :value="form.exclude_vat ? 'Montant TTC (€) - Sans TVA' : 'Montant TTC (€) - TVA 20 % (Calculé)'" class="font-bold text-slate-400" />
|
||||
<input
|
||||
id="amount_ttc"
|
||||
type="text"
|
||||
readonly
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 bg-slate-50 text-slate-500 shadow-sm focus:border-slate-300 focus:ring-0 dark:bg-slate-950 dark:border-slate-850 dark:text-slate-400 cursor-not-allowed font-semibold"
|
||||
:value="new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amountTtc)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date souhaitée de livraison -->
|
||||
<div>
|
||||
<InputLabel for="delivery_deadline" value="Date souhaitée de livraison" class="font-bold" />
|
||||
<TextInput
|
||||
id="delivery_deadline"
|
||||
type="date"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.delivery_deadline"
|
||||
required
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.delivery_deadline" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pièces jointes -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Pièces jointes (PDF, Images, Word, Excel)
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
Joignez les fichiers officiels pour le cycle de vie de la commande.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Devis -->
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="quote_file" value="Devis initial (PDF, Image...)" class="font-bold" />
|
||||
<input
|
||||
id="quote_file"
|
||||
type="file"
|
||||
@input="form.quote_file = $event.target.files[0]"
|
||||
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
|
||||
/>
|
||||
<div v-if="getExistingFile('quote')" class="text-xs mt-1 text-slate-500">
|
||||
Fichier existant :
|
||||
<a :href="getExistingFile('quote').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
|
||||
{{ getExistingFile('quote').file_name }}
|
||||
</a>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.quote_file" />
|
||||
</div>
|
||||
|
||||
<!-- Bon de livraison -->
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="delivery_note_file" value="Bon de livraison" class="font-bold" />
|
||||
<input
|
||||
id="delivery_note_file"
|
||||
type="file"
|
||||
@input="form.delivery_note_file = $event.target.files[0]"
|
||||
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
|
||||
/>
|
||||
<div v-if="getExistingFile('delivery_note')" class="text-xs mt-1 text-slate-500">
|
||||
Fichier existant :
|
||||
<a :href="getExistingFile('delivery_note').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
|
||||
{{ getExistingFile('delivery_note').file_name }}
|
||||
</a>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.delivery_note_file" />
|
||||
</div>
|
||||
|
||||
<!-- Facture -->
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="invoice_file" value="Facture d'achat" class="font-bold" />
|
||||
<input
|
||||
id="invoice_file"
|
||||
type="file"
|
||||
@input="form.invoice_file = $event.target.files[0]"
|
||||
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
|
||||
/>
|
||||
<div v-if="getExistingFile('invoice')" class="text-xs mt-1 text-slate-500">
|
||||
Fichier existant :
|
||||
<a :href="getExistingFile('invoice').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
|
||||
{{ getExistingFile('invoice').file_name }}
|
||||
</a>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.invoice_file" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes libres -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Notes & Commentaires
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="notes" value="Notes libres" class="font-bold" />
|
||||
<textarea
|
||||
id="notes"
|
||||
rows="4"
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
v-model="form.notes"
|
||||
placeholder="Commentaires libres sur la commande, le suivi fournisseur, ou l'utilisation du matériel..."
|
||||
></textarea>
|
||||
<InputError class="mt-2" :message="form.errors.notes" />
|
||||
</div>
|
||||
|
||||
<!-- Barre de boutons -->
|
||||
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-100 dark:border-slate-850">
|
||||
<Link
|
||||
:href="isEdit ? route('commandes.show', { commande: order?.data?.id || order?.id }) : route('commandes.index')"
|
||||
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="inline-flex items-center px-5 py-2.5 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors"
|
||||
>
|
||||
<svg v-if="form.processing" class="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ isEdit ? 'Mettre à jour' : 'Créer la demande' }}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user