Feat: Ajout Fron sous-traitant
This commit is contained in:
parent
163d3ff08d
commit
a02a753b77
@ -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']
|
||||
]
|
||||
|
||||
@ -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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.',
|
||||
|
||||
@ -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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}),
|
||||
|
||||
@ -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')),
|
||||
|
||||
@ -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')),
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('interventions', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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") }}
|
||||
</small>
|
||||
<div
|
||||
v-if="selectedClient"
|
||||
class="d-inline-flex align-items-center gap-2 mt-2 px-3 py-2 rounded-3 bg-gray-100"
|
||||
>
|
||||
<span
|
||||
class="badge"
|
||||
:class="getOrderGiverBadgeClass(selectedClient)"
|
||||
>
|
||||
{{ selectedClient.badge_label }}
|
||||
</span>
|
||||
<div class="text-sm">
|
||||
<div class="fw-semibold">
|
||||
{{ selectedClient.name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedClient.subtitle"
|
||||
class="text-secondary"
|
||||
>
|
||||
{{ selectedClient.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showClientResults"
|
||||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
||||
@ -208,13 +230,29 @@
|
||||
<div class="list-group list-group-flush">
|
||||
<button
|
||||
v-for="c in clientSearchResults"
|
||||
:key="c.id"
|
||||
:key="`${c.entity_type}-${c.id}`"
|
||||
type="button"
|
||||
class="list-group-item list-group-item-action text-start"
|
||||
@click="selectClient(c)"
|
||||
>
|
||||
<div class="fw-semibold text-sm">{{ c.name }}</div>
|
||||
<small class="text-secondary">{{ c.email }}</small>
|
||||
<div
|
||||
class="d-flex align-items-start justify-content-between gap-2"
|
||||
>
|
||||
<div>
|
||||
<div class="fw-semibold text-sm">
|
||||
{{ c.name }}
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
{{ c.subtitle || "Aucune information" }}
|
||||
</small>
|
||||
</div>
|
||||
<span
|
||||
class="badge"
|
||||
:class="getOrderGiverBadgeClass(c)"
|
||||
>
|
||||
{{ c.badge_label }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-if="!clientSearchResults.length"
|
||||
@ -645,7 +683,7 @@ import { Modal } from "bootstrap";
|
||||
import DeceasedService from "@/services/deceased";
|
||||
import ProductService from "@/services/product";
|
||||
import ClientLocationService from "@/services/client_location";
|
||||
import { ClientService } from "@/services/client";
|
||||
import OrderGiverSearchService from "@/services/orderGiverSearch";
|
||||
import ThanatopractitionerService from "@/services/thanatopractitioner";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
@ -793,8 +831,19 @@ const clearDeceasedSelection = () => {
|
||||
);
|
||||
};
|
||||
|
||||
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,6 +1139,9 @@ const handleSubmit = async () => {
|
||||
formData.append("deceased[death_date]", deceasedForm.value.death_date);
|
||||
}
|
||||
if (selectedClient.value) {
|
||||
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);
|
||||
@ -1118,10 +1162,14 @@ const handleSubmit = async () => {
|
||||
formData.append("client[billing_country_code]", ba.country_code);
|
||||
}
|
||||
if (selectedClient.value.vat_number)
|
||||
formData.append("client[vat_number]", 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);
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<sous-traitant-commande-template>
|
||||
<template #controls>
|
||||
<sous-traitant-commande-list-controls
|
||||
@filter="handleFilter"
|
||||
@export="handleExport"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<sous-traitant-commande-table
|
||||
:data="filteredQuotes"
|
||||
:loading="loading"
|
||||
@view="handleView"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</template>
|
||||
</sous-traitant-commande-template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useQuoteStore } from "@/stores/quoteStore";
|
||||
import SousTraitantCommandeTemplate from "@/components/templates/CRM/SousTraitantCommandeTemplate.vue";
|
||||
import SousTraitantCommandeListControls from "@/components/molecules/SousTraitants/SousTraitantCommandeListControls.vue";
|
||||
import SousTraitantCommandeTable from "@/components/molecules/Tables/CRM/SousTraitantCommandeTable.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const quoteStore = useQuoteStore();
|
||||
const { quotes, loading } = storeToRefs(quoteStore);
|
||||
|
||||
const activeFilter = ref(null);
|
||||
|
||||
const filteredQuotes = computed(() =>
|
||||
quotes.value.filter((quote) => {
|
||||
const hasSousTraitant = Boolean(quote.sous_traitant_id);
|
||||
const matchesStatus = activeFilter.value
|
||||
? quote.status === activeFilter.value
|
||||
: true;
|
||||
|
||||
return hasSousTraitant && matchesStatus;
|
||||
})
|
||||
);
|
||||
|
||||
const handleFilter = (status) => {
|
||||
activeFilter.value = status;
|
||||
};
|
||||
|
||||
const handleView = (id) => {
|
||||
router.push(`/ventes/devis/${id}`);
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm("Etes-vous sur de vouloir supprimer cette commande ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await quoteStore.deleteQuote(id);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const headers = ["Reference", "Date", "Statut", "Sous-traitant", "Total TTC"];
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
...filteredQuotes.value.map((quote) =>
|
||||
[
|
||||
quote.reference,
|
||||
quote.quote_date,
|
||||
quote.status,
|
||||
quote.sous_traitant?.nom_entreprise || "",
|
||||
quote.total_ttc,
|
||||
].join(",")
|
||||
),
|
||||
].join("\n");
|
||||
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute(
|
||||
"download",
|
||||
`commandes-sous-traitants-${new Date().toISOString().split("T")[0]}.csv`
|
||||
);
|
||||
link.style.visibility = "hidden";
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
quoteStore.fetchQuotes();
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<sous-traitant-commande-template>
|
||||
<template #controls>
|
||||
<sous-traitant-facture-list-controls
|
||||
@filter="handleFilter"
|
||||
@export="handleExport"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<sous-traitant-facture-table
|
||||
:data="filteredInvoices"
|
||||
:loading="loading"
|
||||
@view="handleView"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</template>
|
||||
</sous-traitant-commande-template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useInvoiceStore } from "@/stores/invoiceStore";
|
||||
import SousTraitantCommandeTemplate from "@/components/templates/CRM/SousTraitantCommandeTemplate.vue";
|
||||
import SousTraitantFactureListControls from "@/components/molecules/SousTraitants/SousTraitantFactureListControls.vue";
|
||||
import SousTraitantFactureTable from "@/components/molecules/Tables/CRM/SousTraitantFactureTable.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const invoiceStore = useInvoiceStore();
|
||||
const { invoices, loading } = storeToRefs(invoiceStore);
|
||||
|
||||
const activeFilter = ref(null);
|
||||
|
||||
const filteredInvoices = computed(() =>
|
||||
invoices.value.filter((invoice) => {
|
||||
const hasSousTraitant = Boolean(invoice.sous_traitant_id);
|
||||
const matchesStatus = activeFilter.value
|
||||
? invoice.status === activeFilter.value
|
||||
: true;
|
||||
|
||||
return hasSousTraitant && matchesStatus;
|
||||
})
|
||||
);
|
||||
|
||||
const handleFilter = (status) => {
|
||||
activeFilter.value = status;
|
||||
};
|
||||
|
||||
const handleView = (id) => {
|
||||
router.push(`/ventes/factures/${id}`);
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm("Etes-vous sur de vouloir supprimer cette facture ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoiceStore.deleteInvoice(id);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const headers = ["Numero", "Date", "Statut", "Sous-traitant", "Total TTC"];
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
...filteredInvoices.value.map((invoice) =>
|
||||
[
|
||||
invoice.invoice_number,
|
||||
invoice.invoice_date,
|
||||
invoice.status,
|
||||
invoice.sous_traitant?.nom_entreprise || "",
|
||||
invoice.total_ttc,
|
||||
].join(",")
|
||||
),
|
||||
].join("\n");
|
||||
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute(
|
||||
"download",
|
||||
`factures-sous-traitants-${new Date().toISOString().split("T")[0]}.csv`
|
||||
);
|
||||
link.style.visibility = "hidden";
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
invoiceStore.fetchInvoices();
|
||||
});
|
||||
</script>
|
||||
@ -28,8 +28,7 @@
|
||||
|
||||
<h5 class="mb-1">Planifier une intervention</h5>
|
||||
<p class="text-sm text-muted mb-0">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,7 +43,7 @@
|
||||
<div class="card-body pt-3">
|
||||
<div class="check-item">
|
||||
<span class="check-dot bg-gradient-success"></span>
|
||||
<span class="text-sm">Associer le bon client</span>
|
||||
<span class="text-sm">Choisir client ou sous-traitant</span>
|
||||
</div>
|
||||
<div class="check-item">
|
||||
<span class="check-dot bg-gradient-info"></span>
|
||||
@ -79,7 +78,8 @@
|
||||
|
||||
<div class="hero-pill-group">
|
||||
<span class="hero-pill">
|
||||
<i class="fas fa-user me-2 text-success"></i>Client
|
||||
<i class="fas fa-user-tag me-2 text-success"></i>Client ou
|
||||
sous-traitant
|
||||
</span>
|
||||
<span class="hero-pill">
|
||||
<i class="fas fa-cross me-2 text-info"></i>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 @@
|
||||
</template>
|
||||
</client-detail-template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
||||
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
|
||||
import SoftBadge from "@/components/SoftBadge.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
@ -153,6 +156,14 @@ defineProps({
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
searchSousTraitants: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
onSousTraitantSelect: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
searchDeceased: {
|
||||
type: Function,
|
||||
required: true,
|
||||
|
||||
@ -108,25 +108,25 @@
|
||||
<h6 class="mb-3">Quote lines</h6>
|
||||
<quote-lines-table :lines="quote.lines" />
|
||||
|
||||
<h6 class="mb-3 mt-4">Billing Information</h6>
|
||||
<h6 class="mb-3 mt-4">Informations destinataire</h6>
|
||||
<ul class="list-group">
|
||||
<li
|
||||
class="list-group-item border-0 d-flex p-4 mb-2 bg-gray-100 border-radius-lg"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<h6 class="mb-3 text-sm">
|
||||
{{ recipientName }}
|
||||
{{ recipientTypeLabel }} : {{ recipientName }}
|
||||
</h6>
|
||||
<span class="mb-2 text-xs">
|
||||
Email Address:
|
||||
<span class="text-dark ms-2 font-weight-bold">{{
|
||||
quote.client?.email || groupDetailsFallback
|
||||
recipientEmail || groupDetailsFallback
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="mb-2 text-xs">
|
||||
Phone:
|
||||
<span class="text-dark ms-2 font-weight-bold">{{
|
||||
quote.client?.phone || groupDetailsFallback
|
||||
recipientPhone || groupDetailsFallback
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="text-xs">
|
||||
@ -191,8 +191,32 @@ const selectedStatus = ref("brouillon");
|
||||
|
||||
const recipientName = computed(() => {
|
||||
if (quote.value?.client?.name) return quote.value.client.name;
|
||||
if (quote.value?.sous_traitant?.nom_entreprise) {
|
||||
return quote.value.sous_traitant.nom_entreprise;
|
||||
}
|
||||
if (quote.value?.group?.name) return quote.value.group.name;
|
||||
return "Client inconnu";
|
||||
return "Destinataire inconnu";
|
||||
});
|
||||
|
||||
const recipientTypeLabel = computed(() => {
|
||||
if (quote.value?.client?.name) return "Client";
|
||||
if (quote.value?.sous_traitant?.nom_entreprise) return "Sous-traitant";
|
||||
if (quote.value?.group?.name) return "Groupe";
|
||||
return "Destinataire";
|
||||
});
|
||||
|
||||
const recipientEmail = computed(() => {
|
||||
if (quote.value?.client?.email) return quote.value.client.email;
|
||||
if (quote.value?.sous_traitant?.email) return quote.value.sous_traitant.email;
|
||||
return null;
|
||||
});
|
||||
|
||||
const recipientPhone = computed(() => {
|
||||
if (quote.value?.client?.phone) return quote.value.client.phone;
|
||||
if (quote.value?.sous_traitant?.telephone) {
|
||||
return quote.value.sous_traitant.telephone;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const groupDetailsFallback = computed(() => {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<!-- Client Selection -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Client <span class="text-danger">*</span>
|
||||
Client (Donneur d'ordre) <span class="text-danger">*</span>
|
||||
</label>
|
||||
<modal-search
|
||||
:search-action="searchClients"
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
Informations principales
|
||||
</h5>
|
||||
<p class="mb-0 text-sm text-muted">
|
||||
Identifiez le client, le défunt concerné et le type
|
||||
d'intervention.
|
||||
Identifiez le client ou le sous-traitant, le défunt concerné
|
||||
et le type d'intervention.
|
||||
</p>
|
||||
</div>
|
||||
<soft-badge color="info" variant="gradient" size="sm">
|
||||
@ -25,31 +25,58 @@
|
||||
<div class="card-body pt-3 p-4">
|
||||
<div class="mb-4">
|
||||
<label class="form-label"
|
||||
>Client <span class="text-danger">*</span></label
|
||||
>Intervenant <span class="text-danger">*</span></label
|
||||
>
|
||||
<div class="entity-switch mb-3">
|
||||
<button
|
||||
type="button"
|
||||
class="entity-switch-btn"
|
||||
:class="{ active: form.intervenant_type === 'client' }"
|
||||
@click="setIntervenantType('client')"
|
||||
>
|
||||
<i class="fas fa-user me-2"></i>Client
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="entity-switch-btn"
|
||||
:class="{ active: form.intervenant_type === 'sous_traitant' }"
|
||||
@click="setIntervenantType('sous_traitant')"
|
||||
>
|
||||
<i class="fas fa-people-carry me-2"></i>Sous-traitant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<search-input
|
||||
v-model="selectedItem"
|
||||
:search-action="props.searchClients"
|
||||
v-model="selectedIntervenant"
|
||||
:search-action="currentSearchAction"
|
||||
:min-chars="0"
|
||||
item-key="id"
|
||||
item-label="name"
|
||||
:item-label="getIntervenantLabel"
|
||||
@search="handleSearch"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
<div v-if="selectedItem" class="selection-chip mt-2">
|
||||
<i class="fas fa-user me-2 text-success"></i>
|
||||
|
||||
<div v-if="selectedIntervenant" class="selection-chip mt-2">
|
||||
<i
|
||||
:class="
|
||||
form.intervenant_type === 'client'
|
||||
? 'fas fa-user me-2 text-success'
|
||||
: 'fas fa-people-carry me-2 text-warning'
|
||||
"
|
||||
></i>
|
||||
<span>
|
||||
{{ selectedItem.name }}
|
||||
{{ getIntervenantLabel(selectedIntervenant) }}
|
||||
<small class="text-muted ms-1">{{
|
||||
selectedItem.email || "Pas d'email"
|
||||
selectedIntervenant.email || "Pas d'email"
|
||||
}}</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="fieldErrors.client_id"
|
||||
v-if="fieldErrors.client_id || fieldErrors.sous_traitant_id"
|
||||
class="invalid-feedback small-error"
|
||||
>
|
||||
{{ fieldErrors.client_id }}
|
||||
{{ fieldErrors.client_id || fieldErrors.sous_traitant_id }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -257,10 +284,8 @@
|
||||
<div class="summary-panel">
|
||||
<p class="summary-title mb-3">Résumé rapide</p>
|
||||
<div class="summary-line">
|
||||
<span>Client</span>
|
||||
<strong>{{
|
||||
selectedItem?.name || "Non sélectionné"
|
||||
}}</strong>
|
||||
<span>Intervenant</span>
|
||||
<strong>{{ summaryIntervenant }}</strong>
|
||||
</div>
|
||||
<div class="summary-line">
|
||||
<span>Défunt</span>
|
||||
@ -319,13 +344,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import { computed, defineEmits, defineProps, ref, watch } from "vue";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftBadge from "@/components/SoftBadge.vue";
|
||||
import SearchInput from "@/components/atoms/input/SearchInput.vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
@ -363,6 +387,14 @@ const props = defineProps({
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
searchSousTraitants: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
onSousTraitantSelect: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
searchDeceased: {
|
||||
type: Function,
|
||||
required: true,
|
||||
@ -373,60 +405,17 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["createIntervention"]);
|
||||
|
||||
// Reactive data
|
||||
const errors = ref([]);
|
||||
// Search input data
|
||||
const selectedItem = ref(null);
|
||||
const selectedIntervenant = ref(null);
|
||||
const selectedDeceased = ref(null);
|
||||
|
||||
// Handle client search event
|
||||
const handleSearch = (query) => {
|
||||
console.log("Searching for client:", query);
|
||||
};
|
||||
|
||||
// Handle client select event
|
||||
const handleSelect = (item) => {
|
||||
console.log("Selected client:", item);
|
||||
// Call the parent callback for client selection if provided
|
||||
if (props.onClientSelect) {
|
||||
props.onClientSelect(item);
|
||||
}
|
||||
// Set the client_id in the form
|
||||
if (item && item.id) {
|
||||
form.value.client_id = item.id.toString();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle deceased search event
|
||||
const handleSearchDeceased = (query) => {
|
||||
console.log("Searching for deceased:", query);
|
||||
};
|
||||
|
||||
const getDeceasedFullName = (deceased) => {
|
||||
const parts = [deceased.last_name, deceased.first_name].filter(Boolean);
|
||||
return parts.join(" ").trim();
|
||||
};
|
||||
|
||||
// Handle deceased select event
|
||||
const handleSelectDeceased = (item) => {
|
||||
console.log("Selected deceased:", item);
|
||||
// Call the parent callback for deceased selection if provided
|
||||
if (props.onDeceasedSelect) {
|
||||
props.onDeceasedSelect(item);
|
||||
}
|
||||
// Set the deceased_id in the form
|
||||
if (item && item.id) {
|
||||
form.value.deceased_id = item.id.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const fieldErrors = ref({});
|
||||
|
||||
const form = ref({
|
||||
intervenant_type: "client",
|
||||
client_id: "",
|
||||
sous_traitant_id: "",
|
||||
deceased_id: "",
|
||||
type: "",
|
||||
scheduled_date: "",
|
||||
@ -437,7 +426,73 @@ const form = ref({
|
||||
notes: "",
|
||||
});
|
||||
|
||||
// Watch for validation errors from parent
|
||||
const currentSearchAction = computed(() =>
|
||||
form.value.intervenant_type === "client"
|
||||
? props.searchClients
|
||||
: props.searchSousTraitants
|
||||
);
|
||||
|
||||
const summaryIntervenant = computed(() => {
|
||||
if (!selectedIntervenant.value) {
|
||||
return "Non sélectionné";
|
||||
}
|
||||
|
||||
return getIntervenantLabel(selectedIntervenant.value);
|
||||
});
|
||||
|
||||
const getIntervenantLabel = (item) => {
|
||||
if (!item) return "";
|
||||
return form.value.intervenant_type === "client"
|
||||
? item.name
|
||||
: item.nom_entreprise;
|
||||
};
|
||||
|
||||
const setIntervenantType = (type) => {
|
||||
form.value.intervenant_type = type;
|
||||
form.value.client_id = "";
|
||||
form.value.sous_traitant_id = "";
|
||||
selectedIntervenant.value = null;
|
||||
delete fieldErrors.value.client_id;
|
||||
delete fieldErrors.value.sous_traitant_id;
|
||||
};
|
||||
|
||||
const handleSearch = (query) => {
|
||||
console.log("Searching partner:", query);
|
||||
};
|
||||
|
||||
const handleSelect = (item) => {
|
||||
console.log("Selected partner:", item);
|
||||
selectedIntervenant.value = item;
|
||||
|
||||
if (form.value.intervenant_type === "client") {
|
||||
props.onClientSelect?.(item);
|
||||
form.value.client_id = item?.id ? item.id.toString() : "";
|
||||
form.value.sous_traitant_id = "";
|
||||
} else {
|
||||
props.onSousTraitantSelect?.(item);
|
||||
form.value.sous_traitant_id = item?.id ? item.id.toString() : "";
|
||||
form.value.client_id = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchDeceased = (query) => {
|
||||
console.log("Searching for deceased:", query);
|
||||
};
|
||||
|
||||
const getDeceasedFullName = (deceased) => {
|
||||
const parts = [deceased.last_name, deceased.first_name].filter(Boolean);
|
||||
return parts.join(" ").trim();
|
||||
};
|
||||
|
||||
const handleSelectDeceased = (item) => {
|
||||
console.log("Selected deceased:", item);
|
||||
selectedDeceased.value = item;
|
||||
props.onDeceasedSelect?.(item);
|
||||
if (item?.id) {
|
||||
form.value.deceased_id = item.id.toString();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.validationErrors,
|
||||
(newErrors) => {
|
||||
@ -446,7 +501,6 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Watch for success from parent
|
||||
watch(
|
||||
() => props.success,
|
||||
(newSuccess) => {
|
||||
@ -456,20 +510,30 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// Watch for client_id changes to update selectedItem
|
||||
watch(
|
||||
() => form.value.client_id,
|
||||
(newClientId) => {
|
||||
if (newClientId && fieldErrors.value.client_id) {
|
||||
delete fieldErrors.value.client_id;
|
||||
}
|
||||
if (!newClientId) {
|
||||
selectedItem.value = null;
|
||||
if (!newClientId && form.value.intervenant_type === "client") {
|
||||
selectedIntervenant.value = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => form.value.sous_traitant_id,
|
||||
(newSousTraitantId) => {
|
||||
if (newSousTraitantId && fieldErrors.value.sous_traitant_id) {
|
||||
delete fieldErrors.value.sous_traitant_id;
|
||||
}
|
||||
if (!newSousTraitantId && form.value.intervenant_type === "sous_traitant") {
|
||||
selectedIntervenant.value = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Watch for deceased_id changes to update selectedDeceased
|
||||
watch(
|
||||
() => form.value.deceased_id,
|
||||
(newDeceasedId) => {
|
||||
@ -492,19 +556,22 @@ watch(
|
||||
);
|
||||
|
||||
const submitForm = async () => {
|
||||
// Clear errors before submitting
|
||||
fieldErrors.value = {};
|
||||
errors.value = [];
|
||||
|
||||
// Validate required fields
|
||||
let hasErrors = false;
|
||||
|
||||
if (!form.value.client_id || form.value.client_id === "") {
|
||||
if (form.value.intervenant_type === "client") {
|
||||
if (!form.value.client_id) {
|
||||
fieldErrors.value.client_id = "Le client est obligatoire";
|
||||
hasErrors = true;
|
||||
}
|
||||
} else if (!form.value.sous_traitant_id) {
|
||||
fieldErrors.value.sous_traitant_id = "Le sous-traitant est obligatoire";
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (!form.value.type || form.value.type === "") {
|
||||
if (!form.value.type) {
|
||||
fieldErrors.value.type = "Le type d'intervention est obligatoire";
|
||||
hasErrors = true;
|
||||
}
|
||||
@ -513,7 +580,6 @@ const submitForm = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up form data: convert empty strings to null
|
||||
const cleanedForm = {};
|
||||
const formData = form.value;
|
||||
|
||||
@ -525,20 +591,20 @@ const submitForm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Combine date and time into scheduled_at (backend expects Y-m-d H:i:s format)
|
||||
if (cleanedForm.scheduled_date && cleanedForm.scheduled_time) {
|
||||
// Format: Y-m-d H:i:s (e.g., "2024-12-15 14:30:00")
|
||||
const formattedDate = cleanedForm.scheduled_date;
|
||||
const formattedTime = cleanedForm.scheduled_time;
|
||||
cleanedForm.scheduled_at = `${formattedDate} ${formattedTime}:00`;
|
||||
delete cleanedForm.scheduled_date;
|
||||
delete cleanedForm.scheduled_time;
|
||||
cleanedForm.scheduled_at = `${cleanedForm.scheduled_date} ${cleanedForm.scheduled_time}:00`;
|
||||
}
|
||||
|
||||
// Convert string numbers to integers
|
||||
delete cleanedForm.scheduled_date;
|
||||
delete cleanedForm.scheduled_time;
|
||||
delete cleanedForm.intervenant_type;
|
||||
|
||||
if (cleanedForm.client_id) {
|
||||
cleanedForm.client_id = parseInt(cleanedForm.client_id);
|
||||
}
|
||||
if (cleanedForm.sous_traitant_id) {
|
||||
cleanedForm.sous_traitant_id = parseInt(cleanedForm.sous_traitant_id);
|
||||
}
|
||||
if (cleanedForm.deceased_id) {
|
||||
cleanedForm.deceased_id = parseInt(cleanedForm.deceased_id);
|
||||
}
|
||||
@ -547,14 +613,14 @@ const submitForm = async () => {
|
||||
}
|
||||
|
||||
console.log("Intervention form data being emitted:", cleanedForm);
|
||||
|
||||
// Emit the cleaned form data to parent
|
||||
emit("createIntervention", cleanedForm);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
intervenant_type: "client",
|
||||
client_id: "",
|
||||
sous_traitant_id: "",
|
||||
deceased_id: "",
|
||||
type: "",
|
||||
scheduled_date: "",
|
||||
@ -564,8 +630,7 @@ const resetForm = () => {
|
||||
order_giver: "",
|
||||
notes: "",
|
||||
};
|
||||
// Clear the selected items
|
||||
selectedItem.value = null;
|
||||
selectedIntervenant.value = null;
|
||||
selectedDeceased.value = null;
|
||||
clearErrors();
|
||||
};
|
||||
@ -601,6 +666,31 @@ const clearErrors = () => {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.entity-switch {
|
||||
display: inline-flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem;
|
||||
border-radius: 0.9rem;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid rgba(52, 71, 103, 0.08);
|
||||
}
|
||||
|
||||
.entity-switch-btn {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #67748e;
|
||||
font-weight: 600;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.entity-switch-btn.active {
|
||||
background: white;
|
||||
color: #344767;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.selection-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -621,20 +711,6 @@ const clearErrors = () => {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.soft-select,
|
||||
.soft-textarea {
|
||||
background-color: white;
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="d-sm-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h5 class="mb-1">Commandes sous-traitants</h5>
|
||||
<p class="text-sm text-secondary mb-0">
|
||||
Liste des devis associés à un sous-traitant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="dropdown d-inline">
|
||||
<soft-button
|
||||
id="sousTraitantCommandeFilter"
|
||||
color="dark"
|
||||
variant="outline"
|
||||
class="dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Filtrer
|
||||
</soft-button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-lg-start px-2 py-3"
|
||||
aria-labelledby="sousTraitantCommandeFilter"
|
||||
>
|
||||
<li v-for="option in filterOptions" :key="option.value ?? 'all'">
|
||||
<a
|
||||
class="dropdown-item border-radius-md"
|
||||
:class="{ 'text-danger': option.value === null }"
|
||||
href="javascript:;"
|
||||
@click="$emit('filter', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<soft-button
|
||||
class="btn-icon ms-2 export"
|
||||
color="dark"
|
||||
variant="outline"
|
||||
data-type="csv"
|
||||
@click="$emit('export')"
|
||||
>
|
||||
<span class="btn-inner--icon">
|
||||
<i class="ni ni-archive-2"></i>
|
||||
</span>
|
||||
<span class="btn-inner--text">Export CSV</span>
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from "vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
defineEmits(["filter", "export"]);
|
||||
|
||||
const filterOptions = [
|
||||
{ label: "Statut: Envoye", value: "envoye" },
|
||||
{ label: "Statut: Accepte", value: "accepte" },
|
||||
{ label: "Statut: Brouillon", value: "brouillon" },
|
||||
{ label: "Statut: Refuse", value: "refuse" },
|
||||
{ label: "Retirer Filtres", value: null },
|
||||
];
|
||||
</script>
|
||||
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="d-sm-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h5 class="mb-1">Factures sous-traitants</h5>
|
||||
<p class="text-sm text-secondary mb-0">
|
||||
Liste des factures associées à un sous-traitant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="dropdown d-inline">
|
||||
<soft-button
|
||||
id="sousTraitantFactureFilter"
|
||||
color="dark"
|
||||
variant="outline"
|
||||
class="dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Filtrer
|
||||
</soft-button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-lg-start px-2 py-3"
|
||||
aria-labelledby="sousTraitantFactureFilter"
|
||||
>
|
||||
<li v-for="option in filterOptions" :key="option.value ?? 'all'">
|
||||
<a
|
||||
class="dropdown-item border-radius-md"
|
||||
:class="{ 'text-danger': option.value === null }"
|
||||
href="javascript:;"
|
||||
@click="$emit('filter', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<soft-button
|
||||
class="btn-icon ms-2 export"
|
||||
color="dark"
|
||||
variant="outline"
|
||||
data-type="csv"
|
||||
@click="$emit('export')"
|
||||
>
|
||||
<span class="btn-inner--icon">
|
||||
<i class="ni ni-archive-2"></i>
|
||||
</span>
|
||||
<span class="btn-inner--text">Export CSV</span>
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from "vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
defineEmits(["filter", "export"]);
|
||||
|
||||
const filterOptions = [
|
||||
{ label: "Statut: Brouillon", value: "brouillon" },
|
||||
{ label: "Statut: Emise", value: "emise" },
|
||||
{ label: "Statut: Envoyee", value: "envoyee" },
|
||||
{ label: "Statut: Payee", value: "payee" },
|
||||
{ label: "Statut: Echue", value: "echue" },
|
||||
{ label: "Retirer Filtres", value: null },
|
||||
];
|
||||
</script>
|
||||
@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="card mt-4">
|
||||
<div class="table-responsive">
|
||||
<table id="sous-traitant-commandes-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Reference</th>
|
||||
<th>Date</th>
|
||||
<th>Statut</th>
|
||||
<th>Sous-traitant</th>
|
||||
<th>Produit</th>
|
||||
<th>Total TTC</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="quote in data" :key="quote.id">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox class="me-2" />
|
||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||
{{ quote.reference }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="font-weight-bold">
|
||||
<span class="my-2 text-xs">
|
||||
{{ formatDate(quote.quote_date) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="getStatusColor(quote.status)"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i
|
||||
:class="getStatusIcon(quote.status)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ getStatusLabel(quote.status) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-avatar
|
||||
:img="getRandomAvatar()"
|
||||
class="me-2"
|
||||
size="xs"
|
||||
alt="sous-traitant image"
|
||||
circular
|
||||
/>
|
||||
<span>{{
|
||||
quote.sous_traitant?.nom_entreprise || "Sous-traitant inconnu"
|
||||
}}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="my-2 text-xs">
|
||||
{{ getProductSummary(quote.lines) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="my-2 text-xs">{{
|
||||
formatCurrency(quote.total_ttc)
|
||||
}}</span>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<button
|
||||
class="btn btn-link text-secondary mb-0 px-2"
|
||||
:data-id="quote.id"
|
||||
data-action="view"
|
||||
title="Voir la commande"
|
||||
>
|
||||
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link text-danger mb-0 px-2"
|
||||
:data-id="quote.id"
|
||||
data-action="delete"
|
||||
title="Supprimer la commande"
|
||||
>
|
||||
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
watch,
|
||||
onUnmounted,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
} from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
|
||||
import img1 from "@/assets/img/team-2.jpg";
|
||||
import img2 from "@/assets/img/team-1.jpg";
|
||||
import img3 from "@/assets/img/team-3.jpg";
|
||||
import img4 from "@/assets/img/team-4.jpg";
|
||||
import img5 from "@/assets/img/team-5.jpg";
|
||||
import img6 from "@/assets/img/ivana-squares.jpg";
|
||||
|
||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||
|
||||
const emit = defineEmits(["view", "delete"]);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const dataTableInstance = ref(null);
|
||||
let tableClickHandler = null;
|
||||
|
||||
const getRandomAvatar = () => {
|
||||
const randomIndex = Math.floor(Math.random() * avatarImages.length);
|
||||
return avatarImages[randomIndex];
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "-";
|
||||
|
||||
return new Date(dateString).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const formatCurrency = (value) =>
|
||||
new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value || 0);
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const map = {
|
||||
brouillon: "secondary",
|
||||
envoye: "info",
|
||||
accepte: "success",
|
||||
refuse: "danger",
|
||||
expire: "warning",
|
||||
annule: "danger",
|
||||
};
|
||||
|
||||
return map[status] || "secondary";
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const map = {
|
||||
brouillon: "fas fa-pen",
|
||||
envoye: "fas fa-paper-plane",
|
||||
accepte: "fas fa-check",
|
||||
refuse: "fas fa-times",
|
||||
expire: "fas fa-clock",
|
||||
annule: "fas fa-ban",
|
||||
};
|
||||
|
||||
return map[status] || "fas fa-info";
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
brouillon: "Brouillon",
|
||||
envoye: "Envoye",
|
||||
accepte: "Accepte",
|
||||
refuse: "Refuse",
|
||||
expire: "Expire",
|
||||
annule: "Annule",
|
||||
};
|
||||
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const getProductSummary = (lines) => {
|
||||
if (!lines || lines.length === 0) return "Aucun produit";
|
||||
|
||||
const firstProduct =
|
||||
lines[0].product_name || lines[0].description || "Produit";
|
||||
|
||||
return lines.length > 1
|
||||
? `${firstProduct} +${lines.length - 1} autre(s)`
|
||||
: firstProduct;
|
||||
};
|
||||
|
||||
const initializeDataTable = () => {
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
dataTableInstance.value = null;
|
||||
}
|
||||
|
||||
const dataTableEl = document.getElementById("sous-traitant-commandes-list");
|
||||
if (dataTableEl) {
|
||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||
searchable: true,
|
||||
fixedHeight: false,
|
||||
perPageSelect: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.loading && props.data.length > 0) {
|
||||
initializeDataTable();
|
||||
}
|
||||
|
||||
const table = document.getElementById("sous-traitant-commandes-list");
|
||||
|
||||
if (table) {
|
||||
tableClickHandler = (event) => {
|
||||
const btn = event.target.closest("button");
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.getAttribute("data-id");
|
||||
const action = btn.getAttribute("data-action");
|
||||
|
||||
if (id && action) {
|
||||
if (action === "view") {
|
||||
emit("view", parseInt(id, 10));
|
||||
} else if (action === "delete") {
|
||||
emit("delete", parseInt(id, 10));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
table.addEventListener("click", tableClickHandler);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
initializeDataTable();
|
||||
}, 100);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
}
|
||||
|
||||
const table = document.getElementById("sous-traitant-commandes-list");
|
||||
if (table && tableClickHandler) {
|
||||
table.removeEventListener("click", tableClickHandler);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="card mt-4">
|
||||
<div class="table-responsive">
|
||||
<table id="sous-traitant-factures-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Numero</th>
|
||||
<th>Date</th>
|
||||
<th>Echeance</th>
|
||||
<th>Statut</th>
|
||||
<th>Sous-traitant</th>
|
||||
<th>Total TTC</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="invoice in data" :key="invoice.id">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox class="me-2" />
|
||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||
{{ invoice.invoice_number }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="font-weight-bold">
|
||||
<span class="my-2 text-xs">
|
||||
{{ formatDate(invoice.invoice_date) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="font-weight-bold">
|
||||
<span class="my-2 text-xs" :class="getDueDateClass(invoice)">
|
||||
{{ formatDate(invoice.due_date) || "-" }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="getStatusColor(invoice.status)"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i
|
||||
:class="getStatusIcon(invoice.status)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ getStatusLabel(invoice.status) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-avatar
|
||||
:img="getRandomAvatar()"
|
||||
class="me-2"
|
||||
size="xs"
|
||||
alt="sous-traitant image"
|
||||
circular
|
||||
/>
|
||||
<span>
|
||||
{{
|
||||
invoice.sous_traitant?.nom_entreprise ||
|
||||
"Sous-traitant inconnu"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="my-2 text-xs">{{
|
||||
formatCurrency(invoice.total_ttc)
|
||||
}}</span>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<button
|
||||
class="btn btn-link text-secondary mb-0 px-2"
|
||||
:data-id="invoice.id"
|
||||
data-action="view"
|
||||
title="Voir la facture"
|
||||
>
|
||||
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link text-danger mb-0 px-2"
|
||||
:data-id="invoice.id"
|
||||
data-action="delete"
|
||||
title="Supprimer la facture"
|
||||
>
|
||||
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
watch,
|
||||
onUnmounted,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
} from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
|
||||
import img1 from "@/assets/img/team-2.jpg";
|
||||
import img2 from "@/assets/img/team-1.jpg";
|
||||
import img3 from "@/assets/img/team-3.jpg";
|
||||
import img4 from "@/assets/img/team-4.jpg";
|
||||
import img5 from "@/assets/img/team-5.jpg";
|
||||
import img6 from "@/assets/img/ivana-squares.jpg";
|
||||
|
||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||
|
||||
const emit = defineEmits(["view", "delete"]);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const dataTableInstance = ref(null);
|
||||
let tableClickHandler = null;
|
||||
|
||||
const getRandomAvatar = () => {
|
||||
const randomIndex = Math.floor(Math.random() * avatarImages.length);
|
||||
return avatarImages[randomIndex];
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return null;
|
||||
|
||||
return new Date(dateString).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatCurrency = (value) =>
|
||||
new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value || 0);
|
||||
|
||||
const getDueDateClass = (invoice) => {
|
||||
if (!invoice.due_date) return "";
|
||||
|
||||
const dueDate = new Date(invoice.due_date);
|
||||
const today = new Date();
|
||||
|
||||
return dueDate < today && invoice.status !== "payee" ? "text-danger" : "";
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const map = {
|
||||
brouillon: "secondary",
|
||||
emise: "info",
|
||||
envoyee: "primary",
|
||||
partiellement_payee: "warning",
|
||||
payee: "success",
|
||||
echue: "danger",
|
||||
annulee: "dark",
|
||||
avoir: "info",
|
||||
};
|
||||
|
||||
return map[status] || "secondary";
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const map = {
|
||||
brouillon: "fas fa-pen",
|
||||
emise: "fas fa-file-invoice",
|
||||
envoyee: "fas fa-paper-plane",
|
||||
partiellement_payee: "fas fa-hourglass-half",
|
||||
payee: "fas fa-check",
|
||||
echue: "fas fa-exclamation-circle",
|
||||
annulee: "fas fa-ban",
|
||||
avoir: "fas fa-undo",
|
||||
};
|
||||
|
||||
return map[status] || "fas fa-info";
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
brouillon: "Brouillon",
|
||||
emise: "Emise",
|
||||
envoyee: "Envoyee",
|
||||
partiellement_payee: "Part. Payee",
|
||||
payee: "Payee",
|
||||
echue: "Echue",
|
||||
annulee: "Annulee",
|
||||
avoir: "Avoir",
|
||||
};
|
||||
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const initializeDataTable = () => {
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
dataTableInstance.value = null;
|
||||
}
|
||||
|
||||
const dataTableEl = document.getElementById("sous-traitant-factures-list");
|
||||
if (dataTableEl) {
|
||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||
searchable: true,
|
||||
fixedHeight: false,
|
||||
perPageSelect: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.loading && props.data.length > 0) {
|
||||
initializeDataTable();
|
||||
}
|
||||
|
||||
const table = document.getElementById("sous-traitant-factures-list");
|
||||
if (table) {
|
||||
tableClickHandler = (event) => {
|
||||
const btn = event.target.closest("button");
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.getAttribute("data-id");
|
||||
const action = btn.getAttribute("data-action");
|
||||
|
||||
if (id && action) {
|
||||
if (action === "view") {
|
||||
emit("view", parseInt(id, 10));
|
||||
} else if (action === "delete") {
|
||||
emit("delete", parseInt(id, 10));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
table.addEventListener("click", tableClickHandler);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!props.loading) {
|
||||
setTimeout(() => {
|
||||
initializeDataTable();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
}
|
||||
|
||||
const table = document.getElementById("sous-traitant-factures-list");
|
||||
if (table && tableClickHandler) {
|
||||
table.removeEventListener("click", tableClickHandler);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<slot name="controls" />
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<slot name="table" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
98
thanasoft-front/src/services/orderGiverSearch.ts
Normal file
98
thanasoft-front/src/services/orderGiverSearch.ts
Normal file
@ -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<OrderGiverSearchResult[]> {
|
||||
const [clients, sousTraitants] = await Promise.all([
|
||||
ClientService.searchClients(query),
|
||||
SousTraitantService.searchSousTraitants(query),
|
||||
]);
|
||||
|
||||
return [
|
||||
...clients.map(normalizeClient),
|
||||
...sousTraitants.map(normalizeSousTraitant),
|
||||
];
|
||||
},
|
||||
|
||||
async getRecent(): Promise<OrderGiverSearchResult[]> {
|
||||
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;
|
||||
@ -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 {
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import AddInterventionPresentation from "@/components/Organism/Interventions/AddInterventionPresentation.vue";
|
||||
import { useInterventionStore } from "@/stores/interventionStore";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { useSousTraitantStore } from "@/stores/sousTraitantStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const interventionStore = useInterventionStore();
|
||||
const deceasedStore = useDeceasedStore();
|
||||
const clientStore = useClientStore();
|
||||
const sousTraitantStore = useSousTraitantStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const validationErrors = ref({});
|
||||
const showSuccess = ref(false);
|
||||
|
||||
// Client search handler passed down to form
|
||||
const handleSearchClients = async (query) => {
|
||||
return await clientStore.searchClients(query);
|
||||
};
|
||||
|
||||
// Client selection handler to pass down to form
|
||||
const handleClientSelect = (client) => {
|
||||
return client;
|
||||
};
|
||||
|
||||
// Deceased search handler passed down to form
|
||||
const handleSearchSousTraitants = async (query) => {
|
||||
return await sousTraitantStore.searchSousTraitants(query);
|
||||
};
|
||||
|
||||
const handleSousTraitantSelect = (sousTraitant) => {
|
||||
return sousTraitant;
|
||||
};
|
||||
|
||||
const handleSearchDeceased = async (query) => {
|
||||
return await deceasedStore.searchDeceased(query);
|
||||
};
|
||||
|
||||
// Deceased selection handler to pass down to form
|
||||
const handleDeceasedSelect = (deceased) => {
|
||||
return deceased;
|
||||
};
|
||||
|
||||
const handleCreateIntervention = async (form) => {
|
||||
try {
|
||||
// Clear previous errors
|
||||
validationErrors.value = {};
|
||||
showSuccess.value = false;
|
||||
|
||||
// Call the store to create intervention
|
||||
const intervention = await interventionStore.createIntervention(form);
|
||||
await interventionStore.createIntervention(form);
|
||||
|
||||
// Show success notification
|
||||
notificationStore.created("Intervention");
|
||||
showSuccess.value = true;
|
||||
|
||||
// Redirect after 2 seconds
|
||||
setTimeout(() => {
|
||||
router.push({ name: "Interventions" });
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error creating intervention:", error);
|
||||
|
||||
// Handle validation errors from Laravel
|
||||
if (error.response && error.response.status === 422) {
|
||||
validationErrors.value = error.response.data.errors || {};
|
||||
notificationStore.error(
|
||||
@ -79,7 +83,6 @@ const handleCreateIntervention = async (form) => {
|
||||
"Veuillez corriger les erreurs dans le formulaire"
|
||||
);
|
||||
} else if (error.response && error.response.data) {
|
||||
// Handle other API errors
|
||||
const errorMessage =
|
||||
error.response.data.message || "Une erreur est survenue";
|
||||
notificationStore.error("Erreur", errorMessage);
|
||||
@ -89,12 +92,12 @@ const handleCreateIntervention = async (form) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Load deceased and client lists for selection
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
deceasedStore.fetchDeceased(),
|
||||
clientStore.fetchClients(),
|
||||
sousTraitantStore.fetchSousTraitants(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Commandes sous-traitants</h1>
|
||||
</div>
|
||||
<sous-traitant-commande-list-presentation />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CommandesSousTraitants",
|
||||
};
|
||||
<script setup>
|
||||
import SousTraitantCommandeListPresentation from "@/components/Organism/CRM/SousTraitantCommandeListPresentation.vue";
|
||||
</script>
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Factures sous-traitants</h1>
|
||||
</div>
|
||||
<sous-traitant-facture-list-presentation />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FacturesSousTraitants",
|
||||
};
|
||||
<script setup>
|
||||
import SousTraitantFactureListPresentation from "@/components/Organism/CRM/SousTraitantFactureListPresentation.vue";
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user