diff --git a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php index 57592a6..5687b45 100644 --- a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php +++ b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php @@ -8,6 +8,7 @@ use App\Http\Requests\StoreInterventionWithAllDataRequest; use App\Http\Requests\UpdateInterventionRequest; use App\Http\Resources\Intervention\InterventionResource; use App\Http\Resources\Intervention\InterventionCollection; +use App\Models\SousTraitant; use App\Repositories\InterventionRepositoryInterface; use App\Repositories\InterventionPractitionerRepositoryInterface; use App\Repositories\ClientRepositoryInterface; @@ -154,6 +155,9 @@ class InterventionController extends Controller // Wrap everything in a database transaction $result = DB::transaction(function () use ($validated) { + $client = null; + $sousTraitant = null; + // Step 1: Handle Deceased (Create or Link) $deceased = null; if (!empty($validated['deceased_id'])) { @@ -164,18 +168,21 @@ class InterventionController extends Controller $deceased = $this->deceasedRepository->create($deceasedData); } - // Step 2: Link existing client or create a new one + // Step 2: Link existing client, existing sous-traitant, or create a new client if (!empty($validated['client_id'])) { $client = $this->clientRepository->find($validated['client_id']); } + elseif (!empty($validated['sous_traitant_id'])) { + $sousTraitant = SousTraitant::findOrFail($validated['sous_traitant_id']); + } else { - $clientData = $validated['client']; + $clientData = $validated['client'] ?? []; $client = $this->clientRepository->create($clientData); } - // Step 3: Create the contact (if provided) + // Step 3: Create the contact only when a client exists $contactId = null; - if (!empty($validated['contact'])) { + if (!empty($validated['contact']) && $client) { $contactData = array_merge($validated['contact'], [ 'client_id' => $client->id ]); @@ -188,7 +195,7 @@ class InterventionController extends Controller $locationId = $validated['location_id'] ?? null; $locationNotes = ''; - if (!$locationId && !empty($locationData)) { + if (!$locationId && !empty($locationData) && $client) { // Create new location for the client $locData = array_merge($locationData, [ 'client_id' => $client->id, @@ -229,7 +236,8 @@ class InterventionController extends Controller // Step 5: Create the intervention $interventionData = array_merge($validated['intervention'], [ 'deceased_id' => $deceased->id, - 'client_id' => $client->id, + 'client_id' => $client?->id, + 'sous_traitant_id' => $sousTraitant?->id, 'location_id' => $locationId, 'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes ]); @@ -288,7 +296,8 @@ class InterventionController extends Controller $totalTtc = $totalHt + $totalTva; $quoteData = [ - 'client_id' => $client->id, + 'client_id' => $client?->id, + 'sous_traitant_id' => $sousTraitant?->id, 'status' => 'brouillon', 'quote_date' => now()->toDateString(), 'currency' => 'EUR', @@ -345,6 +354,7 @@ class InterventionController extends Controller 'intervention' => $intervention, 'deceased' => $deceased, 'client' => $client, + 'sous_traitant' => $sousTraitant, 'contact_id' => $contactId, 'documents_count' => count($documents) ]; @@ -353,7 +363,8 @@ class InterventionController extends Controller Log::info('Intervention with all data created successfully', [ 'intervention_id' => $result['intervention']->id, 'deceased_id' => $result['deceased']->id, - 'client_id' => $result['client']->id, + 'client_id' => $result['client']?->id, + 'sous_traitant_id' => $result['sous_traitant']?->id, 'documents_count' => $result['documents_count'] ]); @@ -363,6 +374,7 @@ class InterventionController extends Controller 'intervention' => new InterventionResource($result['intervention']), 'deceased' => $result['deceased'], 'client' => $result['client'], + 'sous_traitant' => $result['sous_traitant'], 'contact_id' => $result['contact_id'], 'documents_count' => $result['documents_count'] ] @@ -710,4 +722,4 @@ class InterventionController extends Controller return response()->json(['error' => $e->getMessage()], 500); } } -} \ No newline at end of file +} diff --git a/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php b/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php index 4d27545..c27b8b9 100644 --- a/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php @@ -7,24 +7,16 @@ use Illuminate\Validation\Rule; class StoreInterventionRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { - // Add authorization logic if needed return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { return [ - 'client_id' => ['required', 'exists:clients,id'], + 'client_id' => ['nullable', 'exists:clients,id'], + 'sous_traitant_id' => ['nullable', 'exists:sous_traitants,id'], 'deceased_id' => ['nullable', 'exists:deceased,id'], 'order_giver' => ['nullable', 'string', 'max:255'], 'location_id' => ['nullable', 'exists:client_locations,id'], @@ -35,7 +27,7 @@ class StoreInterventionRequest extends FormRequest 'exhumation', 'retrait_pacemaker', 'retrait_bijoux', - 'autre' + 'autre', ])], 'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], 'duration_min' => ['nullable', 'integer', 'min:0'], @@ -44,7 +36,7 @@ class StoreInterventionRequest extends FormRequest 'planifie', 'en_cours', 'termine', - 'annule' + 'annule', ])], 'practitioners' => ['nullable', 'array'], 'practitioners.*' => ['exists:thanatopractitioners,id'], @@ -52,33 +44,44 @@ class StoreInterventionRequest extends FormRequest 'assistant_practitioner_ids' => ['nullable', 'array'], 'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'], 'notes' => ['nullable', 'string'], - 'created_by' => ['nullable', 'exists:users,id'] + 'created_by' => ['nullable', 'exists:users,id'], ]; } - /** - * Get custom error messages for validator errors. - */ + public function withValidator($validator): void + { + $validator->after(function ($validator) { + $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); + + if (! $hasClient && ! $hasSousTraitant) { + $message = 'Un client ou un sous-traitant est obligatoire.'; + $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); + } + }); + } + public function messages(): array { return [ - 'client_id.required' => 'Le client est obligatoire.', - 'client_id.exists' => 'Le client sélectionné est invalide.', - 'deceased_id.exists' => 'Le défunt sélectionné est invalide.', - 'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', - 'location_id.exists' => 'Le lieu sélectionné est invalide.', - 'type.required' => 'Le type d\'intervention est obligatoire.', - 'type.in' => 'Le type d\'intervention est invalide.', - 'scheduled_at.date_format' => 'Le format de la date programmée est invalide.', - 'duration_min.integer' => 'La durée doit être un nombre entier.', - 'duration_min.min' => 'La durée ne peut pas être négative.', - 'status.in' => 'Le statut de l\'intervention est invalide.', - 'practitioners.array' => 'Les praticiens doivent être un tableau.', - 'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.', - 'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.', - 'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.', + 'client_id.exists' => 'Le client selectionne est invalide.', + 'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.', + 'deceased_id.exists' => 'Le defunt selectionne est invalide.', + 'order_giver.max' => 'Le donneur d ordre ne peut pas depasser 255 caracteres.', + 'location_id.exists' => 'Le lieu selectionne est invalide.', + 'type.required' => 'Le type d intervention est obligatoire.', + 'type.in' => 'Le type d intervention est invalide.', + 'scheduled_at.date_format' => 'Le format de la date programmee est invalide.', + 'duration_min.integer' => 'La duree doit etre un nombre entier.', + 'duration_min.min' => 'La duree ne peut pas etre negative.', + 'status.in' => 'Le statut de l intervention est invalide.', + 'practitioners.array' => 'Les praticiens doivent etre un tableau.', + 'practitioners.*.exists' => 'Un des praticiens selectionnes est invalide.', + 'principal_practitioner_id.exists' => 'Le praticien principal selectionne est invalide.', + 'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent etre un tableau.', 'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.', - 'created_by.exists' => 'L\'utilisateur créateur est invalide.' + 'created_by.exists' => 'L utilisateur createur est invalide.', ]; } } diff --git a/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php b/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php index 44628bf..bd5339e 100644 --- a/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php @@ -33,8 +33,9 @@ class StoreInterventionWithAllDataRequest extends FormRequest 'deceased.notes' => ['nullable', 'string'], 'client_id' => ['nullable', 'exists:clients,id'], - 'client' => 'required_without:client_id|array', - 'client.name' => ['required', 'string', 'max:255'], + 'sous_traitant_id' => ['nullable', 'exists:sous_traitants,id'], + 'client' => 'required_without_all:client_id,sous_traitant_id|array', + 'client.name' => ['required_without_all:client_id,sous_traitant_id', 'string', 'max:255'], 'client.vat_number' => ['nullable', 'string', 'max:32'], 'client.siret' => ['nullable', 'string', 'max:20'], 'client.email' => ['nullable', 'email', 'max:191'], @@ -103,6 +104,21 @@ class StoreInterventionWithAllDataRequest extends FormRequest ]; } + public function withValidator($validator): void + { + $validator->after(function ($validator) { + $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); + $hasNewClient = filled($this->input('client.name')); + + if (! $hasClient && ! $hasSousTraitant && ! $hasNewClient) { + $message = 'Un client ou un sous-traitant est obligatoire.'; + $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); + } + }); + } + /** * Get custom error messages for validator errors. */ @@ -117,6 +133,7 @@ class StoreInterventionWithAllDataRequest extends FormRequest 'deceased.place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.', 'client.required' => 'Les informations du client sont obligatoires.', + 'sous_traitant_id.exists' => 'Le sous-traitant sélectionné est invalide.', 'client.name.required' => 'Le nom du client est obligatoire.', 'client.name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.', 'client.vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', diff --git a/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php b/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php index 2f5168d..7170c5a 100644 --- a/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php @@ -6,23 +6,16 @@ use Illuminate\Foundation\Http\FormRequest; class StoreInvoiceRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { return [ 'client_id' => 'nullable|exists:clients,id', + 'sous_traitant_id' => 'nullable|exists:sous_traitants,id', 'group_id' => 'nullable|exists:client_groups,id', 'source_quote_id' => 'nullable|exists:quotes,id', 'status' => 'required|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir', @@ -53,19 +46,20 @@ class StoreInvoiceRequest extends FormRequest { $validator->after(function ($validator) { $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); $hasGroup = filled($this->input('group_id')); - if (! $hasClient && ! $hasGroup) { - $message = 'Un client ou un groupe de clients est obligatoire.'; - + if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) { + $message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.'; $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); $validator->errors()->add('group_id', $message); } - if ($hasClient && $hasGroup) { - $message = 'Selectionnez soit un client, soit un groupe de clients.'; - + if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) { + $message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.'; $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); $validator->errors()->add('group_id', $message); } }); @@ -74,26 +68,27 @@ class StoreInvoiceRequest extends FormRequest public function messages(): array { return [ - 'client_id.exists' => 'Le client sélectionné est invalide.', - 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'client_id.exists' => 'Le client selectionne est invalide.', + 'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.', + 'group_id.exists' => 'Le groupe selectionne est invalide.', 'status.required' => 'Le statut est obligatoire.', - 'status.in' => 'Le statut sélectionné est invalide.', + 'status.in' => 'Le statut selectionne est invalide.', 'invoice_date.required' => 'La date de la facture est obligatoire.', - 'invoice_date.date' => 'La date de la facture n\'est pas valide.', - 'due_date.date' => 'La date d\'échéance n\'est pas valide.', - 'due_date.after_or_equal' => 'La date d\'échéance doit être postérieure ou égale à la date de la facture.', + 'invoice_date.date' => 'La date de la facture n est pas valide.', + 'due_date.date' => 'La date d echeance n est pas valide.', + 'due_date.after_or_equal' => 'La date d echeance doit etre posterieure ou egale a la date de la facture.', 'currency.required' => 'La devise est obligatoire.', - 'currency.size' => 'La devise doit comporter 3 caractères.', + 'currency.size' => 'La devise doit comporter 3 caracteres.', 'total_ht.required' => 'Le total HT est obligatoire.', - 'total_ht.numeric' => 'Le total HT doit être un nombre.', - 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_ht.numeric' => 'Le total HT doit etre un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas etre negatif.', 'total_tva.required' => 'Le total TVA est obligatoire.', - 'total_tva.numeric' => 'Le total TVA doit être un nombre.', - 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_tva.numeric' => 'Le total TVA doit etre un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas etre negatif.', 'total_ttc.required' => 'Le total TTC est obligatoire.', - 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', - 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', - 'lines.required' => 'Veuillez ajouter au moins une ligne à la facture.', + 'total_ttc.numeric' => 'Le total TTC doit etre un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.', + 'lines.required' => 'Veuillez ajouter au moins une ligne a la facture.', ]; } } diff --git a/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php index 83cb3d2..f8ad217 100644 --- a/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php @@ -6,23 +6,16 @@ use Illuminate\Foundation\Http\FormRequest; class StoreQuoteRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { return [ 'client_id' => 'nullable|exists:clients,id', + 'sous_traitant_id' => 'nullable|exists:sous_traitants,id', 'group_id' => 'nullable|exists:client_groups,id', 'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule', 'quote_date' => 'required|date', @@ -50,19 +43,20 @@ class StoreQuoteRequest extends FormRequest { $validator->after(function ($validator) { $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); $hasGroup = filled($this->input('group_id')); - if (! $hasClient && ! $hasGroup) { - $message = 'Un client ou un groupe de clients est obligatoire.'; - + if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) { + $message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.'; $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); $validator->errors()->add('group_id', $message); } - if ($hasClient && $hasGroup) { - $message = 'Sélectionnez soit un client, soit un groupe de clients.'; - + if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) { + $message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.'; $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); $validator->errors()->add('group_id', $message); } }); @@ -71,26 +65,26 @@ class StoreQuoteRequest extends FormRequest public function messages(): array { return [ - 'client_id.nullable' => 'Le client sélectionné est invalide.', - 'client_id.exists' => 'Le client sélectionné est invalide.', - 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'client_id.exists' => 'Le client selectionne est invalide.', + 'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.', + 'group_id.exists' => 'Le groupe selectionne est invalide.', 'status.required' => 'Le statut est obligatoire.', - 'status.in' => 'Le statut sélectionné est invalide.', + 'status.in' => 'Le statut selectionne est invalide.', 'quote_date.required' => 'La date du devis est obligatoire.', - 'quote_date.date' => 'La date du devis n\'est pas valide.', - 'valid_until.date' => 'La date de validité n\'est pas valide.', - 'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.', + 'quote_date.date' => 'La date du devis n est pas valide.', + 'valid_until.date' => 'La date de validite n est pas valide.', + 'valid_until.after_or_equal' => 'La date de validite doit etre posterieure ou egale a la date du devis.', 'currency.required' => 'La devise est obligatoire.', - 'currency.size' => 'La devise doit comporter 3 caractères.', + 'currency.size' => 'La devise doit comporter 3 caracteres.', 'total_ht.required' => 'Le total HT est obligatoire.', - 'total_ht.numeric' => 'Le total HT doit être un nombre.', - 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_ht.numeric' => 'Le total HT doit etre un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas etre negatif.', 'total_tva.required' => 'Le total TVA est obligatoire.', - 'total_tva.numeric' => 'Le total TVA doit être un nombre.', - 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_tva.numeric' => 'Le total TVA doit etre un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas etre negatif.', 'total_ttc.required' => 'Le total TTC est obligatoire.', - 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', - 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + 'total_ttc.numeric' => 'Le total TTC doit etre un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.', ]; } } diff --git a/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php b/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php index 6a3c1c5..7b94359 100644 --- a/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php @@ -7,24 +7,16 @@ use Illuminate\Validation\Rule; class UpdateInterventionRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { - // Add authorization logic if needed return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { return [ - 'client_id' => ['sometimes', 'required', 'exists:clients,id'], + 'client_id' => ['sometimes', 'nullable', 'exists:clients,id'], + 'sous_traitant_id' => ['sometimes', 'nullable', 'exists:sous_traitants,id'], 'deceased_id' => ['nullable', 'exists:deceased,id'], 'order_giver' => ['nullable', 'string', 'max:255'], 'location_id' => ['nullable', 'exists:client_locations,id'], @@ -35,7 +27,7 @@ class UpdateInterventionRequest extends FormRequest 'exhumation', 'retrait_pacemaker', 'retrait_bijoux', - 'autre' + 'autre', ])], 'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], 'duration_min' => ['nullable', 'integer', 'min:0'], @@ -44,7 +36,7 @@ class UpdateInterventionRequest extends FormRequest 'planifie', 'en_cours', 'termine', - 'annule' + 'annule', ])], 'practitioners' => ['nullable', 'array'], 'practitioners.*' => ['exists:thanatopractitioners,id'], @@ -52,33 +44,48 @@ class UpdateInterventionRequest extends FormRequest 'assistant_practitioner_ids' => ['nullable', 'array'], 'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'], 'notes' => ['nullable', 'string'], - 'created_by' => ['nullable', 'exists:users,id'] + 'created_by' => ['nullable', 'exists:users,id'], ]; } - /** - * Get custom error messages for validator errors. - */ + public function withValidator($validator): void + { + $validator->after(function ($validator) { + if (! $this->hasAny(['client_id', 'sous_traitant_id'])) { + return; + } + + $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); + + if (! $hasClient && ! $hasSousTraitant) { + $message = 'Un client ou un sous-traitant est obligatoire.'; + $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); + } + }); + } + public function messages(): array { return [ - 'client_id.required' => 'Le client est obligatoire.', - 'client_id.exists' => 'Le client sélectionné est invalide.', - 'deceased_id.exists' => 'Le défunt sélectionné est invalide.', - 'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', - 'location_id.exists' => 'Le lieu sélectionné est invalide.', - 'type.required' => 'Le type d\'intervention est obligatoire.', - 'type.in' => 'Le type d\'intervention est invalide.', - 'scheduled_at.date_format' => 'Le format de la date programmée est invalide.', - 'duration_min.integer' => 'La durée doit être un nombre entier.', - 'duration_min.min' => 'La durée ne peut pas être négative.', - 'status.in' => 'Le statut de l\'intervention est invalide.', - 'practitioners.array' => 'Les praticiens doivent être un tableau.', - 'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.', - 'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.', - 'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.', + 'client_id.exists' => 'Le client selectionne est invalide.', + 'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.', + 'deceased_id.exists' => 'Le defunt selectionne est invalide.', + 'order_giver.max' => 'Le donneur d ordre ne peut pas depasser 255 caracteres.', + 'location_id.exists' => 'Le lieu selectionne est invalide.', + 'type.required' => 'Le type d intervention est obligatoire.', + 'type.in' => 'Le type d intervention est invalide.', + 'scheduled_at.date_format' => 'Le format de la date programmee est invalide.', + 'duration_min.integer' => 'La duree doit etre un nombre entier.', + 'duration_min.min' => 'La duree ne peut pas etre negative.', + 'status.in' => 'Le statut de l intervention est invalide.', + 'practitioners.array' => 'Les praticiens doivent etre un tableau.', + 'practitioners.*.exists' => 'Un des praticiens selectionnes est invalide.', + 'principal_practitioner_id.exists' => 'Le praticien principal selectionne est invalide.', + 'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent etre un tableau.', 'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.', - 'created_by.exists' => 'L\'utilisateur créateur est invalide.' + 'created_by.exists' => 'L utilisateur createur est invalide.', ]; } } diff --git a/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php b/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php index a996b5a..3f6d5ac 100644 --- a/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php @@ -6,25 +6,18 @@ use Illuminate\Foundation\Http\FormRequest; class UpdateInvoiceRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { $invoiceId = $this->route('invoice'); return [ 'client_id' => 'nullable|exists:clients,id', + 'sous_traitant_id' => 'nullable|exists:sous_traitants,id', 'group_id' => 'nullable|exists:client_groups,id', 'source_quote_id' => 'nullable|exists:quotes,id', 'invoice_number' => 'sometimes|string|max:191|unique:invoices,invoice_number,' . $invoiceId, @@ -43,49 +36,51 @@ class UpdateInvoiceRequest extends FormRequest public function messages(): array { return [ - 'client_id.exists' => 'Le client sélectionné est invalide.', - 'group_id.exists' => 'Le groupe sélectionné est invalide.', + 'client_id.exists' => 'Le client selectionne est invalide.', + 'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.', + 'group_id.exists' => 'Le groupe selectionne est invalide.', 'source_quote_id.exists' => 'Le devis source est invalide.', - 'invoice_number.string' => 'Le numéro de facture doit être une chaîne de caractères.', - 'invoice_number.max' => 'Le numéro de facture ne doit pas dépasser 191 caractères.', - 'invoice_number.unique' => 'Ce numéro de facture existe déjà.', - 'status.in' => 'Le statut sélectionné est invalide.', - 'invoice_date.date' => 'La date de la facture n\'est pas valide.', - 'due_date.date' => 'La date d\'échéance n\'est pas valide.', - 'due_date.after_or_equal' => 'La date d\'échéance doit être postérieure ou égale à la date de la facture.', - 'currency.size' => 'La devise doit comporter 3 caractères.', - 'total_ht.numeric' => 'Le total HT doit être un nombre.', - 'total_ht.min' => 'Le total HT ne peut pas être négatif.', - 'total_tva.numeric' => 'Le total TVA doit être un nombre.', - 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', - 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', - 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', - 'e_invoicing_channel_id.exists' => 'Le canal de facturation électronique est invalide.', - 'e_invoice_status.in' => 'Le statut de facturation électronique est invalide.', + 'invoice_number.string' => 'Le numero de facture doit etre une chaine de caracteres.', + 'invoice_number.max' => 'Le numero de facture ne doit pas depasser 191 caracteres.', + 'invoice_number.unique' => 'Ce numero de facture existe deja.', + 'status.in' => 'Le statut selectionne est invalide.', + 'invoice_date.date' => 'La date de la facture n est pas valide.', + 'due_date.date' => 'La date d echeance n est pas valide.', + 'due_date.after_or_equal' => 'La date d echeance doit etre posterieure ou egale a la date de la facture.', + 'currency.size' => 'La devise doit comporter 3 caracteres.', + 'total_ht.numeric' => 'Le total HT doit etre un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas etre negatif.', + 'total_tva.numeric' => 'Le total TVA doit etre un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas etre negatif.', + 'total_ttc.numeric' => 'Le total TTC doit etre un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.', + 'e_invoicing_channel_id.exists' => 'Le canal de facturation electronique est invalide.', + 'e_invoice_status.in' => 'Le statut de facturation electronique est invalide.', ]; } public function withValidator($validator): void { $validator->after(function ($validator) { - if (! $this->hasAny(['client_id', 'group_id'])) { + if (! $this->hasAny(['client_id', 'sous_traitant_id', 'group_id'])) { return; } $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); $hasGroup = filled($this->input('group_id')); - if (! $hasClient && ! $hasGroup) { - $message = 'Un client ou un groupe de clients est obligatoire.'; - + if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) { + $message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.'; $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); $validator->errors()->add('group_id', $message); } - if ($hasClient && $hasGroup) { - $message = 'Selectionnez soit un client, soit un groupe de clients.'; - + if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) { + $message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.'; $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); $validator->errors()->add('group_id', $message); } }); diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php index 6fdd10c..ce3e49c 100644 --- a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php @@ -6,25 +6,18 @@ use Illuminate\Foundation\Http\FormRequest; class UpdateQuoteRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { $quoteId = $this->route('quote'); - + return [ - 'client_id' => 'sometimes|exists:clients,id', + 'client_id' => 'sometimes|nullable|exists:clients,id', + 'sous_traitant_id' => 'sometimes|nullable|exists:sous_traitants,id', 'group_id' => 'nullable|exists:client_groups,id', 'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $quoteId, 'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule', @@ -37,25 +30,53 @@ class UpdateQuoteRequest extends FormRequest ]; } + public function withValidator($validator): void + { + $validator->after(function ($validator) { + if (! $this->hasAny(['client_id', 'sous_traitant_id', 'group_id'])) { + return; + } + + $hasClient = filled($this->input('client_id')); + $hasSousTraitant = filled($this->input('sous_traitant_id')); + $hasGroup = filled($this->input('group_id')); + + if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) { + $message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.'; + $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); + $validator->errors()->add('group_id', $message); + } + + if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) { + $message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.'; + $validator->errors()->add('client_id', $message); + $validator->errors()->add('sous_traitant_id', $message); + $validator->errors()->add('group_id', $message); + } + }); + } + public function messages(): array { return [ - 'client_id.exists' => 'Le client sélectionné est invalide.', - 'group_id.exists' => 'Le groupe sélectionné est invalide.', - 'reference.string' => 'Le numéro de devis doit être une chaîne de caractères.', - 'reference.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.', - 'reference.unique' => 'Ce numéro de devis existe déjà.', - 'status.in' => 'Le statut sélectionné est invalide.', - 'quote_date.date' => 'La date du devis n\'est pas valide.', - 'valid_until.date' => 'La date de validité n\'est pas valide.', - 'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.', - 'currency.size' => 'La devise doit comporter 3 caractères.', - 'total_ht.numeric' => 'Le total HT doit être un nombre.', - 'total_ht.min' => 'Le total HT ne peut pas être négatif.', - 'total_tva.numeric' => 'Le total TVA doit être un nombre.', - 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', - 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', - 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + 'client_id.exists' => 'Le client selectionne est invalide.', + 'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.', + 'group_id.exists' => 'Le groupe selectionne est invalide.', + 'reference.string' => 'Le numero de devis doit etre une chaine de caracteres.', + 'reference.max' => 'Le numero de devis ne doit pas depasser 191 caracteres.', + 'reference.unique' => 'Ce numero de devis existe deja.', + 'status.in' => 'Le statut selectionne est invalide.', + 'quote_date.date' => 'La date du devis n est pas valide.', + 'valid_until.date' => 'La date de validite n est pas valide.', + 'valid_until.after_or_equal' => 'La date de validite doit etre posterieure ou egale a la date du devis.', + 'currency.size' => 'La devise doit comporter 3 caracteres.', + 'total_ht.numeric' => 'Le total HT doit etre un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas etre negatif.', + 'total_tva.numeric' => 'Le total TVA doit etre un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas etre negatif.', + 'total_ttc.numeric' => 'Le total TTC doit etre un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.', ]; } } diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php index c0f8753..e17449b 100644 --- a/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php @@ -19,6 +19,8 @@ class InterventionResource extends JsonResource { return [ 'id' => $this->id, + 'client_id' => $this->client_id, + 'sous_traitant_id' => $this->sous_traitant_id, 'quote_id' => $this->quote_id, 'invoice_id' => $this->invoice_id, 'quote' => $this->whenLoaded('quote', function () { @@ -27,6 +29,9 @@ class InterventionResource extends JsonResource 'client' => $this->whenLoaded('client', function () { return new ClientResource($this->client); }), + 'sous_traitant' => $this->whenLoaded('sousTraitant', function () { + return new \App\Http\Resources\SousTraitant\SousTraitantResource($this->sousTraitant); + }), 'deceased' => $this->whenLoaded('deceased', function () { return new DeceasedResource($this->deceased); }), diff --git a/thanasoft-back/app/Http/Resources/InvoiceResource.php b/thanasoft-back/app/Http/Resources/InvoiceResource.php index 9c953c8..88d559c 100644 --- a/thanasoft-back/app/Http/Resources/InvoiceResource.php +++ b/thanasoft-back/app/Http/Resources/InvoiceResource.php @@ -17,6 +17,7 @@ class InvoiceResource extends JsonResource return [ 'id' => $this->id, 'client_id' => $this->client_id, + 'sous_traitant_id' => $this->sous_traitant_id, 'group_id' => $this->group_id, 'source_quote_id' => $this->source_quote_id, 'invoice_number' => $this->invoice_number, @@ -32,6 +33,7 @@ class InvoiceResource extends JsonResource 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, 'client' => $this->whenLoaded('client'), + 'sous_traitant' => $this->whenLoaded('sousTraitant'), 'group' => $this->whenLoaded('group'), 'sourceQuote' => $this->whenLoaded('sourceQuote'), 'lines' => InvoiceLineResource::collection($this->whenLoaded('lines')), diff --git a/thanasoft-back/app/Http/Resources/QuoteResource.php b/thanasoft-back/app/Http/Resources/QuoteResource.php index 40202b2..a71d897 100644 --- a/thanasoft-back/app/Http/Resources/QuoteResource.php +++ b/thanasoft-back/app/Http/Resources/QuoteResource.php @@ -17,6 +17,7 @@ class QuoteResource extends JsonResource return [ 'id' => $this->id, 'client_id' => $this->client_id, + 'sous_traitant_id' => $this->sous_traitant_id, 'group_id' => $this->group_id, 'reference' => $this->reference, 'status' => $this->status, @@ -29,6 +30,7 @@ class QuoteResource extends JsonResource 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, 'client' => $this->whenLoaded('client'), + 'sous_traitant' => $this->whenLoaded('sousTraitant'), 'group' => $this->whenLoaded('group'), 'lines' => QuoteLineResource::collection($this->whenLoaded('lines')), 'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')), diff --git a/thanasoft-back/app/Models/Intervention.php b/thanasoft-back/app/Models/Intervention.php index c69bc31..0066137 100644 --- a/thanasoft-back/app/Models/Intervention.php +++ b/thanasoft-back/app/Models/Intervention.php @@ -19,6 +19,7 @@ class Intervention extends Model */ protected $fillable = [ 'client_id', + 'sous_traitant_id', 'deceased_id', 'order_giver', 'location_id', @@ -60,6 +61,11 @@ class Intervention extends Model return $this->belongsTo(Client::class); } + public function sousTraitant(): BelongsTo + { + return $this->belongsTo(SousTraitant::class, 'sous_traitant_id'); + } + /** * Get the deceased associated with the intervention. */ @@ -165,4 +171,4 @@ class Intervention extends Model { return $this->belongsTo(Quote::class); } -} \ No newline at end of file +} diff --git a/thanasoft-back/app/Models/Invoice.php b/thanasoft-back/app/Models/Invoice.php index 0a27cd9..22d700c 100644 --- a/thanasoft-back/app/Models/Invoice.php +++ b/thanasoft-back/app/Models/Invoice.php @@ -11,6 +11,7 @@ class Invoice extends Model protected $fillable = [ 'client_id', + 'sous_traitant_id', 'group_id', 'source_quote_id', 'invoice_number', @@ -60,6 +61,11 @@ class Invoice extends Model return $this->belongsTo(Client::class); } + public function sousTraitant() + { + return $this->belongsTo(SousTraitant::class, 'sous_traitant_id'); + } + public function group() { return $this->belongsTo(ClientGroup::class, 'group_id'); diff --git a/thanasoft-back/app/Models/Quote.php b/thanasoft-back/app/Models/Quote.php index c010ee2..603d648 100644 --- a/thanasoft-back/app/Models/Quote.php +++ b/thanasoft-back/app/Models/Quote.php @@ -11,6 +11,7 @@ class Quote extends Model protected $fillable = [ 'client_id', + 'sous_traitant_id', 'group_id', 'reference', 'status', @@ -54,6 +55,11 @@ class Quote extends Model return $this->belongsTo(Client::class); } + public function sousTraitant() + { + return $this->belongsTo(SousTraitant::class, 'sous_traitant_id'); + } + public function group() { return $this->belongsTo(ClientGroup::class); diff --git a/thanasoft-back/app/Repositories/InterventionRepository.php b/thanasoft-back/app/Repositories/InterventionRepository.php index 64094ff..91ff4f4 100644 --- a/thanasoft-back/app/Repositories/InterventionRepository.php +++ b/thanasoft-back/app/Repositories/InterventionRepository.php @@ -30,6 +30,10 @@ class InterventionRepository implements InterventionRepositoryInterface $query->where('client_id', $filters['client_id']); } + if (!empty($filters['sous_traitant_id'])) { + $query->where('sous_traitant_id', $filters['sous_traitant_id']); + } + if (!empty($filters['deceased_id'])) { $query->where('deceased_id', $filters['deceased_id']); } @@ -59,6 +63,7 @@ class InterventionRepository implements InterventionRepositoryInterface // Eager load related models $query->with([ 'client', + 'sousTraitant', 'deceased', 'location', 'practitioners' @@ -77,6 +82,7 @@ class InterventionRepository implements InterventionRepositoryInterface { return Intervention::with([ 'client', + 'sousTraitant', 'deceased', 'location', 'practitioners', diff --git a/thanasoft-back/app/Repositories/InvoiceRepository.php b/thanasoft-back/app/Repositories/InvoiceRepository.php index 80ed82e..d6c37f9 100644 --- a/thanasoft-back/app/Repositories/InvoiceRepository.php +++ b/thanasoft-back/app/Repositories/InvoiceRepository.php @@ -39,6 +39,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter // Create Invoice $invoiceData = [ 'client_id' => $quote->client_id, + 'sous_traitant_id' => $quote->sous_traitant_id, 'group_id' => $quote->group_id, 'source_quote_id' => $quote->id, 'status' => 'brouillon', // Start as draft @@ -98,7 +99,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter public function all(array $columns = ['*']): \Illuminate\Support\Collection { - return $this->model->with(['client', 'group', 'lines.product'])->get($columns); + return $this->model->with(['client', 'sousTraitant', 'group', 'lines.product'])->get($columns); } public function create(array $data): Invoice @@ -185,7 +186,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter public function find(int|string $id, array $columns = ['*']): ?Invoice { - return $this->model->with(['client', 'group', 'lines.product', 'history.user'])->find($id, $columns); + return $this->model->with(['client', 'sousTraitant', 'group', 'lines.product', 'history.user'])->find($id, $columns); } private function recordHistory(int $invoiceId, ?string $oldStatus, string $newStatus, ?string $comment = null): void diff --git a/thanasoft-back/app/Repositories/QuoteRepository.php b/thanasoft-back/app/Repositories/QuoteRepository.php index 9fec950..4400623 100644 --- a/thanasoft-back/app/Repositories/QuoteRepository.php +++ b/thanasoft-back/app/Repositories/QuoteRepository.php @@ -24,7 +24,7 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface public function all(array $columns = ['*']): \Illuminate\Support\Collection { - return $this->model->with(['client', 'group', 'lines.product'])->get($columns); + return $this->model->with(['client', 'sousTraitant', 'group', 'lines.product'])->get($columns); } public function create(array $data): Quote @@ -115,7 +115,7 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface public function find(int|string $id, array $columns = ['*']): ?Quote { - return $this->model->with(['client', 'group', 'lines.product', 'history.user'])->find($id, $columns); + return $this->model->with(['client', 'sousTraitant', 'group', 'lines.product', 'history.user'])->find($id, $columns); } private function recordHistory(int $quoteId, ?string $oldStatus, string $newStatus, ?string $comment = null): void diff --git a/thanasoft-back/database/migrations/2026_05_22_130000_add_sous_traitant_id_to_interventions_quotes_invoices.php b/thanasoft-back/database/migrations/2026_05_22_130000_add_sous_traitant_id_to_interventions_quotes_invoices.php new file mode 100644 index 0000000..62d202e --- /dev/null +++ b/thanasoft-back/database/migrations/2026_05_22_130000_add_sous_traitant_id_to_interventions_quotes_invoices.php @@ -0,0 +1,57 @@ +dropForeign(['client_id']); + $table->unsignedBigInteger('client_id')->nullable()->change(); + $table->foreign('client_id')->references('id')->on('clients')->nullOnDelete(); + + $table->foreignId('sous_traitant_id') + ->nullable() + ->after('client_id') + ->constrained('sous_traitants') + ->nullOnDelete(); + }); + + Schema::table('quotes', function (Blueprint $table) { + $table->foreignId('sous_traitant_id') + ->nullable() + ->after('client_id') + ->constrained('sous_traitants') + ->nullOnDelete(); + }); + + Schema::table('invoices', function (Blueprint $table) { + $table->foreignId('sous_traitant_id') + ->nullable() + ->after('client_id') + ->constrained('sous_traitants') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->dropConstrainedForeignId('sous_traitant_id'); + }); + + Schema::table('quotes', function (Blueprint $table) { + $table->dropConstrainedForeignId('sous_traitant_id'); + }); + + Schema::table('interventions', function (Blueprint $table) { + $table->dropConstrainedForeignId('sous_traitant_id'); + $table->dropForeign(['client_id']); + $table->unsignedBigInteger('client_id')->nullable(false)->change(); + $table->foreign('client_id')->references('id')->on('clients'); + }); + } +}; diff --git a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue index ba571b5..46bc951 100644 --- a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue +++ b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue @@ -177,7 +177,7 @@ type="text" class="form-control" :class="{ 'is-invalid': hasError('client') }" - placeholder="Nom, email..." + placeholder="Nom du client ou du sous-traitant..." @input="handleClientSearch" @focus="handleClientFocus" /> @@ -196,6 +196,28 @@ > {{ getFieldError("client") }} +
+ + {{ selectedClient.badge_label }} + +
+
+ {{ selectedClient.name }} +
+
+ {{ selectedClient.subtitle }} +
+
+
{ ); }; +const getOrderGiverBadgeClass = (orderGiver) => + orderGiver?.entity_type === "sous_traitant" + ? "bg-gradient-warning" + : "bg-gradient-info"; + const handleClientSearch = () => { showClientResults.value = true; + if ( + selectedClient.value && + clientSearchQuery.value !== selectedClient.value.name + ) { + selectedClient.value = null; + } if (clientSearchTimeout) clearTimeout(clientSearchTimeout); if (clientSearchQuery.value.length < 2) { clientSearchResults.value = recentClients.value; @@ -802,29 +851,21 @@ const handleClientSearch = () => { } clientSearchTimeout = setTimeout(async () => { try { - clientSearchResults.value = await ClientService.searchClients( + clientSearchResults.value = await OrderGiverSearchService.search( clientSearchQuery.value ); } catch (e) { console.error(e); + clientSearchResults.value = []; } }, 300); }; const loadRecentClients = async () => { try { - const response = await ClientService.getAllClients({ - page: 1, - per_page: 5, - }); - const clients = Array.isArray(response?.data) - ? response.data - : Array.isArray(response?.data?.data) - ? response.data.data - : []; - recentClients.value = clients; + recentClients.value = await OrderGiverSearchService.getRecent(); } catch (e) { - console.error("Failed to load recent clients", e); + console.error("Failed to load recent order givers", e); recentClients.value = []; } }; @@ -841,7 +882,7 @@ const handleClientFocus = async () => { const selectClient = (c) => { selectedClient.value = c; - clientSearchQuery.value = c.name + (c.email ? ` (${c.email})` : ""); + clientSearchQuery.value = c.name; clientSearchResults.value = []; showClientResults.value = false; errors.value = errors.value.filter((e) => e.field !== "client"); @@ -1098,29 +1139,36 @@ const handleSubmit = async () => { formData.append("deceased[death_date]", deceasedForm.value.death_date); } if (selectedClient.value) { - formData.append("client_id", selectedClient.value.id); - if (selectedClient.value.name) - formData.append("client[name]", selectedClient.value.name); - if (selectedClient.value.email) - formData.append("client[email]", selectedClient.value.email); - if (selectedClient.value.phone) - formData.append("client[phone]", selectedClient.value.phone); - if (selectedClient.value.billing_address) { - const ba = selectedClient.value.billing_address; - if (ba.line1) - formData.append("client[billing_address_line1]", ba.line1); - if (ba.line2) - formData.append("client[billing_address_line2]", ba.line2); - if (ba.postal_code) - formData.append("client[billing_postal_code]", ba.postal_code); - if (ba.city) formData.append("client[billing_city]", ba.city); - if (ba.country_code) - formData.append("client[billing_country_code]", ba.country_code); + if (selectedClient.value.entity_type === "sous_traitant") { + formData.append("sous_traitant_id", selectedClient.value.id); + } else { + formData.append("client_id", selectedClient.value.id); + if (selectedClient.value.name) + formData.append("client[name]", selectedClient.value.name); + if (selectedClient.value.email) + formData.append("client[email]", selectedClient.value.email); + if (selectedClient.value.phone) + formData.append("client[phone]", selectedClient.value.phone); + if (selectedClient.value.billing_address) { + const ba = selectedClient.value.billing_address; + if (ba.line1) + formData.append("client[billing_address_line1]", ba.line1); + if (ba.line2) + formData.append("client[billing_address_line2]", ba.line2); + if (ba.postal_code) + formData.append("client[billing_postal_code]", ba.postal_code); + if (ba.city) formData.append("client[billing_city]", ba.city); + if (ba.country_code) + formData.append("client[billing_country_code]", ba.country_code); + } + if (selectedClient.value.vat_number) + formData.append( + "client[vat_number]", + selectedClient.value.vat_number + ); + if (selectedClient.value.siret) + formData.append("client[siret]", selectedClient.value.siret); } - if (selectedClient.value.vat_number) - formData.append("client[vat_number]", selectedClient.value.vat_number); - if (selectedClient.value.siret) - formData.append("client[siret]", selectedClient.value.siret); } if (locationForm.value.is_existing && locationForm.value.id) { formData.append("location_id", locationForm.value.id); diff --git a/thanasoft-front/src/components/Organism/CRM/SousTraitantCommandeListPresentation.vue b/thanasoft-front/src/components/Organism/CRM/SousTraitantCommandeListPresentation.vue new file mode 100644 index 0000000..0727944 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/SousTraitantCommandeListPresentation.vue @@ -0,0 +1,98 @@ + + + diff --git a/thanasoft-front/src/components/Organism/CRM/SousTraitantFactureListPresentation.vue b/thanasoft-front/src/components/Organism/CRM/SousTraitantFactureListPresentation.vue new file mode 100644 index 0000000..1a8e441 --- /dev/null +++ b/thanasoft-front/src/components/Organism/CRM/SousTraitantFactureListPresentation.vue @@ -0,0 +1,98 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue b/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue index fe8cb66..0ccbae8 100644 --- a/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue +++ b/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue @@ -28,8 +28,7 @@
Planifier une intervention

- Préparez une fiche claire, liée au client et au défunt, dans le - style du dashboard. + Préparez une fiche claire, liée à un client ou à un sous-traitant.

@@ -44,7 +43,7 @@
- Associer le bon client + Choisir client ou sous-traitant
@@ -79,7 +78,8 @@
- Client + Client ou + sous-traitant Défunt @@ -102,6 +102,8 @@ :client-loading="clientLoading" :search-clients="searchClients" :on-client-select="onClientSelect" + :search-sous-traitants="searchSousTraitants" + :on-sous-traitant-select="onSousTraitantSelect" :search-deceased="searchDeceased" :on-deceased-select="onDeceasedSelect" @create-intervention="handleCreateIntervention" @@ -109,12 +111,13 @@ + diff --git a/thanasoft-front/src/components/molecules/SousTraitants/SousTraitantFactureListControls.vue b/thanasoft-front/src/components/molecules/SousTraitants/SousTraitantFactureListControls.vue new file mode 100644 index 0000000..8bf947f --- /dev/null +++ b/thanasoft-front/src/components/molecules/SousTraitants/SousTraitantFactureListControls.vue @@ -0,0 +1,69 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/CRM/SousTraitantCommandeTable.vue b/thanasoft-front/src/components/molecules/Tables/CRM/SousTraitantCommandeTable.vue new file mode 100644 index 0000000..95a2d68 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/CRM/SousTraitantCommandeTable.vue @@ -0,0 +1,278 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/CRM/SousTraitantFactureTable.vue b/thanasoft-front/src/components/molecules/Tables/CRM/SousTraitantFactureTable.vue new file mode 100644 index 0000000..6f68292 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/CRM/SousTraitantFactureTable.vue @@ -0,0 +1,285 @@ + + + diff --git a/thanasoft-front/src/components/templates/CRM/SousTraitantCommandeTemplate.vue b/thanasoft-front/src/components/templates/CRM/SousTraitantCommandeTemplate.vue new file mode 100644 index 0000000..b4a0bd1 --- /dev/null +++ b/thanasoft-front/src/components/templates/CRM/SousTraitantCommandeTemplate.vue @@ -0,0 +1,10 @@ + diff --git a/thanasoft-front/src/services/index.ts b/thanasoft-front/src/services/index.ts index cceb8b6..6d3ae0d 100644 --- a/thanasoft-front/src/services/index.ts +++ b/thanasoft-front/src/services/index.ts @@ -1,4 +1,5 @@ export * from "./http"; export { default as AuthService } from "./auth"; +export { default as OrderGiverSearchService } from "./orderGiverSearch"; export { default as WebmailService } from "./webmail"; export { default as SousTraitantService } from "./sousTraitant"; diff --git a/thanasoft-front/src/services/invoice.ts b/thanasoft-front/src/services/invoice.ts index 069dda7..00f3f61 100644 --- a/thanasoft-front/src/services/invoice.ts +++ b/thanasoft-front/src/services/invoice.ts @@ -1,10 +1,12 @@ import { request } from "./http"; import { Client } from "./client"; import type { ClientGroup } from "./clientGroup"; +import type { SousTraitant } from "./sousTraitant"; export interface Invoice { id: number; client_id: number | null; + sous_traitant_id: number | null; group_id: number | null; source_quote_id: number | null; invoice_number: string; @@ -28,6 +30,7 @@ export interface Invoice { created_at: string; updated_at: string; client?: Client; + sous_traitant?: SousTraitant; group?: ClientGroup; } diff --git a/thanasoft-front/src/services/orderGiverSearch.ts b/thanasoft-front/src/services/orderGiverSearch.ts new file mode 100644 index 0000000..416213c --- /dev/null +++ b/thanasoft-front/src/services/orderGiverSearch.ts @@ -0,0 +1,98 @@ +import { ClientService } from "./client"; +import SousTraitantService from "./sousTraitant"; + +export type OrderGiverType = "client" | "sous_traitant"; + +export interface OrderGiverSearchResult { + id: number; + entity_type: OrderGiverType; + name: string; + email: string | null; + phone: string | null; + subtitle: string | null; + badge_label: string; + billing_address?: { + line1: string | null; + line2: string | null; + postal_code: string | null; + city: string | null; + country_code: string | null; + full_address?: string; + } | null; + vat_number?: string | null; + siret?: string | null; +} + +const normalizeClient = (client: any): OrderGiverSearchResult => ({ + id: client.id, + entity_type: "client", + name: client.name, + email: client.email || null, + phone: client.phone || null, + subtitle: client.email || client.phone || null, + badge_label: "Client", + billing_address: client.billing_address || null, + vat_number: client.vat_number || null, + siret: client.siret || null, +}); + +const normalizeSousTraitant = (sousTraitant: any): OrderGiverSearchResult => ({ + id: sousTraitant.id, + entity_type: "sous_traitant", + name: sousTraitant.nom_entreprise, + email: sousTraitant.email || null, + phone: sousTraitant.telephone || null, + subtitle: + sousTraitant.contact_principal || + sousTraitant.email || + sousTraitant.telephone || + null, + badge_label: "Sous-traitant", + siret: sousTraitant.siret || null, +}); + +export const OrderGiverSearchService = { + async search(query: string): Promise { + const [clients, sousTraitants] = await Promise.all([ + ClientService.searchClients(query), + SousTraitantService.searchSousTraitants(query), + ]); + + return [ + ...clients.map(normalizeClient), + ...sousTraitants.map(normalizeSousTraitant), + ]; + }, + + async getRecent(): Promise { + const [clientsResponse, sousTraitantsResponse] = await Promise.all([ + ClientService.getAllClients({ + page: 1, + per_page: 5, + }), + SousTraitantService.getAllSousTraitants({ + page: 1, + per_page: 5, + }), + ]); + + const clients = Array.isArray(clientsResponse?.data) + ? clientsResponse.data + : Array.isArray(clientsResponse?.data?.data) + ? clientsResponse.data.data + : []; + + const sousTraitants = Array.isArray(sousTraitantsResponse?.data) + ? sousTraitantsResponse.data + : Array.isArray(sousTraitantsResponse?.data?.data) + ? sousTraitantsResponse.data.data + : []; + + return [ + ...clients.map(normalizeClient), + ...sousTraitants.map(normalizeSousTraitant), + ]; + }, +}; + +export default OrderGiverSearchService; diff --git a/thanasoft-front/src/services/quote.ts b/thanasoft-front/src/services/quote.ts index 1d22718..ee84c0b 100644 --- a/thanasoft-front/src/services/quote.ts +++ b/thanasoft-front/src/services/quote.ts @@ -1,9 +1,11 @@ import { http, request } from "./http"; import { Client } from "./client"; +import type { SousTraitant } from "./sousTraitant"; export interface Quote { id: number; client_id: number | null; + sous_traitant_id: number | null; group_id: number | null; reference: string; status: "brouillon" | "envoye" | "accepte" | "refuse" | "expire" | "annule"; @@ -16,6 +18,7 @@ export interface Quote { created_at: string; updated_at: string; client?: Client; + sous_traitant?: SousTraitant; } export interface QuoteListResponse { diff --git a/thanasoft-front/src/views/pages/Interventions/AddIntervention.vue b/thanasoft-front/src/views/pages/Interventions/AddIntervention.vue index f3efc4a..b7a4a9e 100644 --- a/thanasoft-front/src/views/pages/Interventions/AddIntervention.vue +++ b/thanasoft-front/src/views/pages/Interventions/AddIntervention.vue @@ -9,69 +9,73 @@ :client-loading="clientStore.isLoading" :search-clients="handleSearchClients" :on-client-select="handleClientSelect" + :search-sous-traitants="handleSearchSousTraitants" + :on-sous-traitant-select="handleSousTraitantSelect" :search-deceased="handleSearchDeceased" :on-deceased-select="handleDeceasedSelect" @create-intervention="handleCreateIntervention" /> + diff --git a/thanasoft-front/src/views/pages/SousTraitants/Factures.vue b/thanasoft-front/src/views/pages/SousTraitants/Factures.vue index df82088..6f42514 100644 --- a/thanasoft-front/src/views/pages/SousTraitants/Factures.vue +++ b/thanasoft-front/src/views/pages/SousTraitants/Factures.vue @@ -1,11 +1,7 @@ -