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\Requests\UpdateInterventionRequest;
|
||||||
use App\Http\Resources\Intervention\InterventionResource;
|
use App\Http\Resources\Intervention\InterventionResource;
|
||||||
use App\Http\Resources\Intervention\InterventionCollection;
|
use App\Http\Resources\Intervention\InterventionCollection;
|
||||||
|
use App\Models\SousTraitant;
|
||||||
use App\Repositories\InterventionRepositoryInterface;
|
use App\Repositories\InterventionRepositoryInterface;
|
||||||
use App\Repositories\InterventionPractitionerRepositoryInterface;
|
use App\Repositories\InterventionPractitionerRepositoryInterface;
|
||||||
use App\Repositories\ClientRepositoryInterface;
|
use App\Repositories\ClientRepositoryInterface;
|
||||||
@ -154,6 +155,9 @@ class InterventionController extends Controller
|
|||||||
|
|
||||||
// Wrap everything in a database transaction
|
// Wrap everything in a database transaction
|
||||||
$result = DB::transaction(function () use ($validated) {
|
$result = DB::transaction(function () use ($validated) {
|
||||||
|
$client = null;
|
||||||
|
$sousTraitant = null;
|
||||||
|
|
||||||
// Step 1: Handle Deceased (Create or Link)
|
// Step 1: Handle Deceased (Create or Link)
|
||||||
$deceased = null;
|
$deceased = null;
|
||||||
if (!empty($validated['deceased_id'])) {
|
if (!empty($validated['deceased_id'])) {
|
||||||
@ -164,18 +168,21 @@ class InterventionController extends Controller
|
|||||||
$deceased = $this->deceasedRepository->create($deceasedData);
|
$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'])) {
|
if (!empty($validated['client_id'])) {
|
||||||
$client = $this->clientRepository->find($validated['client_id']);
|
$client = $this->clientRepository->find($validated['client_id']);
|
||||||
}
|
}
|
||||||
|
elseif (!empty($validated['sous_traitant_id'])) {
|
||||||
|
$sousTraitant = SousTraitant::findOrFail($validated['sous_traitant_id']);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
$clientData = $validated['client'];
|
$clientData = $validated['client'] ?? [];
|
||||||
$client = $this->clientRepository->create($clientData);
|
$client = $this->clientRepository->create($clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Create the contact (if provided)
|
// Step 3: Create the contact only when a client exists
|
||||||
$contactId = null;
|
$contactId = null;
|
||||||
if (!empty($validated['contact'])) {
|
if (!empty($validated['contact']) && $client) {
|
||||||
$contactData = array_merge($validated['contact'], [
|
$contactData = array_merge($validated['contact'], [
|
||||||
'client_id' => $client->id
|
'client_id' => $client->id
|
||||||
]);
|
]);
|
||||||
@ -188,7 +195,7 @@ class InterventionController extends Controller
|
|||||||
$locationId = $validated['location_id'] ?? null;
|
$locationId = $validated['location_id'] ?? null;
|
||||||
$locationNotes = '';
|
$locationNotes = '';
|
||||||
|
|
||||||
if (!$locationId && !empty($locationData)) {
|
if (!$locationId && !empty($locationData) && $client) {
|
||||||
// Create new location for the client
|
// Create new location for the client
|
||||||
$locData = array_merge($locationData, [
|
$locData = array_merge($locationData, [
|
||||||
'client_id' => $client->id,
|
'client_id' => $client->id,
|
||||||
@ -229,7 +236,8 @@ class InterventionController extends Controller
|
|||||||
// Step 5: Create the intervention
|
// Step 5: Create the intervention
|
||||||
$interventionData = array_merge($validated['intervention'], [
|
$interventionData = array_merge($validated['intervention'], [
|
||||||
'deceased_id' => $deceased->id,
|
'deceased_id' => $deceased->id,
|
||||||
'client_id' => $client->id,
|
'client_id' => $client?->id,
|
||||||
|
'sous_traitant_id' => $sousTraitant?->id,
|
||||||
'location_id' => $locationId,
|
'location_id' => $locationId,
|
||||||
'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes
|
'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes
|
||||||
]);
|
]);
|
||||||
@ -288,7 +296,8 @@ class InterventionController extends Controller
|
|||||||
$totalTtc = $totalHt + $totalTva;
|
$totalTtc = $totalHt + $totalTva;
|
||||||
|
|
||||||
$quoteData = [
|
$quoteData = [
|
||||||
'client_id' => $client->id,
|
'client_id' => $client?->id,
|
||||||
|
'sous_traitant_id' => $sousTraitant?->id,
|
||||||
'status' => 'brouillon',
|
'status' => 'brouillon',
|
||||||
'quote_date' => now()->toDateString(),
|
'quote_date' => now()->toDateString(),
|
||||||
'currency' => 'EUR',
|
'currency' => 'EUR',
|
||||||
@ -345,6 +354,7 @@ class InterventionController extends Controller
|
|||||||
'intervention' => $intervention,
|
'intervention' => $intervention,
|
||||||
'deceased' => $deceased,
|
'deceased' => $deceased,
|
||||||
'client' => $client,
|
'client' => $client,
|
||||||
|
'sous_traitant' => $sousTraitant,
|
||||||
'contact_id' => $contactId,
|
'contact_id' => $contactId,
|
||||||
'documents_count' => count($documents)
|
'documents_count' => count($documents)
|
||||||
];
|
];
|
||||||
@ -353,7 +363,8 @@ class InterventionController extends Controller
|
|||||||
Log::info('Intervention with all data created successfully', [
|
Log::info('Intervention with all data created successfully', [
|
||||||
'intervention_id' => $result['intervention']->id,
|
'intervention_id' => $result['intervention']->id,
|
||||||
'deceased_id' => $result['deceased']->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']
|
'documents_count' => $result['documents_count']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -363,6 +374,7 @@ class InterventionController extends Controller
|
|||||||
'intervention' => new InterventionResource($result['intervention']),
|
'intervention' => new InterventionResource($result['intervention']),
|
||||||
'deceased' => $result['deceased'],
|
'deceased' => $result['deceased'],
|
||||||
'client' => $result['client'],
|
'client' => $result['client'],
|
||||||
|
'sous_traitant' => $result['sous_traitant'],
|
||||||
'contact_id' => $result['contact_id'],
|
'contact_id' => $result['contact_id'],
|
||||||
'documents_count' => $result['documents_count']
|
'documents_count' => $result['documents_count']
|
||||||
]
|
]
|
||||||
|
|||||||
@ -7,24 +7,16 @@ use Illuminate\Validation\Rule;
|
|||||||
|
|
||||||
class StoreInterventionRequest extends FormRequest
|
class StoreInterventionRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
// Add authorization logic if needed
|
|
||||||
return true;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
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'],
|
'deceased_id' => ['nullable', 'exists:deceased,id'],
|
||||||
'order_giver' => ['nullable', 'string', 'max:255'],
|
'order_giver' => ['nullable', 'string', 'max:255'],
|
||||||
'location_id' => ['nullable', 'exists:client_locations,id'],
|
'location_id' => ['nullable', 'exists:client_locations,id'],
|
||||||
@ -35,7 +27,7 @@ class StoreInterventionRequest extends FormRequest
|
|||||||
'exhumation',
|
'exhumation',
|
||||||
'retrait_pacemaker',
|
'retrait_pacemaker',
|
||||||
'retrait_bijoux',
|
'retrait_bijoux',
|
||||||
'autre'
|
'autre',
|
||||||
])],
|
])],
|
||||||
'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
|
'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
|
||||||
'duration_min' => ['nullable', 'integer', 'min:0'],
|
'duration_min' => ['nullable', 'integer', 'min:0'],
|
||||||
@ -44,7 +36,7 @@ class StoreInterventionRequest extends FormRequest
|
|||||||
'planifie',
|
'planifie',
|
||||||
'en_cours',
|
'en_cours',
|
||||||
'termine',
|
'termine',
|
||||||
'annule'
|
'annule',
|
||||||
])],
|
])],
|
||||||
'practitioners' => ['nullable', 'array'],
|
'practitioners' => ['nullable', 'array'],
|
||||||
'practitioners.*' => ['exists:thanatopractitioners,id'],
|
'practitioners.*' => ['exists:thanatopractitioners,id'],
|
||||||
@ -52,33 +44,44 @@ class StoreInterventionRequest extends FormRequest
|
|||||||
'assistant_practitioner_ids' => ['nullable', 'array'],
|
'assistant_practitioner_ids' => ['nullable', 'array'],
|
||||||
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
||||||
'notes' => ['nullable', 'string'],
|
'notes' => ['nullable', 'string'],
|
||||||
'created_by' => ['nullable', 'exists:users,id']
|
'created_by' => ['nullable', 'exists:users,id'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function withValidator($validator): void
|
||||||
* Get custom error messages for validator errors.
|
{
|
||||||
*/
|
$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
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.required' => 'Le client est obligatoire.',
|
'client_id.exists' => 'Le client selectionne est invalide.',
|
||||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.',
|
||||||
'deceased_id.exists' => 'Le défunt sélectionné est invalide.',
|
'deceased_id.exists' => 'Le defunt selectionne est invalide.',
|
||||||
'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
|
'order_giver.max' => 'Le donneur d ordre ne peut pas depasser 255 caracteres.',
|
||||||
'location_id.exists' => 'Le lieu sélectionné est invalide.',
|
'location_id.exists' => 'Le lieu selectionne est invalide.',
|
||||||
'type.required' => 'Le type d\'intervention est obligatoire.',
|
'type.required' => 'Le type d intervention est obligatoire.',
|
||||||
'type.in' => 'Le type d\'intervention est invalide.',
|
'type.in' => 'Le type d intervention est invalide.',
|
||||||
'scheduled_at.date_format' => 'Le format de la date programmée est invalide.',
|
'scheduled_at.date_format' => 'Le format de la date programmee est invalide.',
|
||||||
'duration_min.integer' => 'La durée doit être un nombre entier.',
|
'duration_min.integer' => 'La duree doit etre un nombre entier.',
|
||||||
'duration_min.min' => 'La durée ne peut pas être négative.',
|
'duration_min.min' => 'La duree ne peut pas etre negative.',
|
||||||
'status.in' => 'Le statut de l\'intervention est invalide.',
|
'status.in' => 'Le statut de l intervention est invalide.',
|
||||||
'practitioners.array' => 'Les praticiens doivent être un tableau.',
|
'practitioners.array' => 'Les praticiens doivent etre un tableau.',
|
||||||
'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
|
'practitioners.*.exists' => 'Un des praticiens selectionnes est invalide.',
|
||||||
'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
|
'principal_practitioner_id.exists' => 'Le praticien principal selectionne est invalide.',
|
||||||
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
|
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent etre un tableau.',
|
||||||
'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
|
'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'],
|
'deceased.notes' => ['nullable', 'string'],
|
||||||
|
|
||||||
'client_id' => ['nullable', 'exists:clients,id'],
|
'client_id' => ['nullable', 'exists:clients,id'],
|
||||||
'client' => 'required_without:client_id|array',
|
'sous_traitant_id' => ['nullable', 'exists:sous_traitants,id'],
|
||||||
'client.name' => ['required', 'string', 'max:255'],
|
'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.vat_number' => ['nullable', 'string', 'max:32'],
|
||||||
'client.siret' => ['nullable', 'string', 'max:20'],
|
'client.siret' => ['nullable', 'string', 'max:20'],
|
||||||
'client.email' => ['nullable', 'email', 'max:191'],
|
'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.
|
* 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.',
|
'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.',
|
'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.required' => 'Le nom du client est obligatoire.',
|
||||||
'client.name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.',
|
'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.',
|
'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
|
class StoreInvoiceRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id' => 'nullable|exists:clients,id',
|
'client_id' => 'nullable|exists:clients,id',
|
||||||
|
'sous_traitant_id' => 'nullable|exists:sous_traitants,id',
|
||||||
'group_id' => 'nullable|exists:client_groups,id',
|
'group_id' => 'nullable|exists:client_groups,id',
|
||||||
'source_quote_id' => 'nullable|exists:quotes,id',
|
'source_quote_id' => 'nullable|exists:quotes,id',
|
||||||
'status' => 'required|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir',
|
'status' => 'required|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir',
|
||||||
@ -53,19 +46,20 @@ class StoreInvoiceRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
$validator->after(function ($validator) {
|
$validator->after(function ($validator) {
|
||||||
$hasClient = filled($this->input('client_id'));
|
$hasClient = filled($this->input('client_id'));
|
||||||
|
$hasSousTraitant = filled($this->input('sous_traitant_id'));
|
||||||
$hasGroup = filled($this->input('group_id'));
|
$hasGroup = filled($this->input('group_id'));
|
||||||
|
|
||||||
if (! $hasClient && ! $hasGroup) {
|
if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) {
|
||||||
$message = 'Un client ou un groupe de clients est obligatoire.';
|
$message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.';
|
||||||
|
|
||||||
$validator->errors()->add('client_id', $message);
|
$validator->errors()->add('client_id', $message);
|
||||||
|
$validator->errors()->add('sous_traitant_id', $message);
|
||||||
$validator->errors()->add('group_id', $message);
|
$validator->errors()->add('group_id', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasClient && $hasGroup) {
|
if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) {
|
||||||
$message = 'Selectionnez soit un client, soit un groupe de clients.';
|
$message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.';
|
||||||
|
|
||||||
$validator->errors()->add('client_id', $message);
|
$validator->errors()->add('client_id', $message);
|
||||||
|
$validator->errors()->add('sous_traitant_id', $message);
|
||||||
$validator->errors()->add('group_id', $message);
|
$validator->errors()->add('group_id', $message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -74,26 +68,27 @@ class StoreInvoiceRequest extends FormRequest
|
|||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
'client_id.exists' => 'Le client selectionne est invalide.',
|
||||||
'group_id.exists' => 'Le groupe sélectionné 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.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.required' => 'La date de la facture est obligatoire.',
|
||||||
'invoice_date.date' => 'La date de la facture n\'est pas valide.',
|
'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.date' => 'La date d echeance 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.',
|
'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.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.required' => 'Le total HT est obligatoire.',
|
||||||
'total_ht.numeric' => 'Le total HT doit être un nombre.',
|
'total_ht.numeric' => 'Le total HT doit etre un nombre.',
|
||||||
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
|
'total_ht.min' => 'Le total HT ne peut pas etre negatif.',
|
||||||
'total_tva.required' => 'Le total TVA est obligatoire.',
|
'total_tva.required' => 'Le total TVA est obligatoire.',
|
||||||
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
|
'total_tva.numeric' => 'Le total TVA doit etre un nombre.',
|
||||||
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
|
'total_tva.min' => 'Le total TVA ne peut pas etre negatif.',
|
||||||
'total_ttc.required' => 'Le total TTC est obligatoire.',
|
'total_ttc.required' => 'Le total TTC est obligatoire.',
|
||||||
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
|
'total_ttc.numeric' => 'Le total TTC doit etre un nombre.',
|
||||||
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
|
'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.',
|
||||||
'lines.required' => 'Veuillez ajouter au moins une ligne à la facture.',
|
'lines.required' => 'Veuillez ajouter au moins une ligne a la facture.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,23 +6,16 @@ use Illuminate\Foundation\Http\FormRequest;
|
|||||||
|
|
||||||
class StoreQuoteRequest extends FormRequest
|
class StoreQuoteRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id' => 'nullable|exists:clients,id',
|
'client_id' => 'nullable|exists:clients,id',
|
||||||
|
'sous_traitant_id' => 'nullable|exists:sous_traitants,id',
|
||||||
'group_id' => 'nullable|exists:client_groups,id',
|
'group_id' => 'nullable|exists:client_groups,id',
|
||||||
'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule',
|
'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule',
|
||||||
'quote_date' => 'required|date',
|
'quote_date' => 'required|date',
|
||||||
@ -50,19 +43,20 @@ class StoreQuoteRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
$validator->after(function ($validator) {
|
$validator->after(function ($validator) {
|
||||||
$hasClient = filled($this->input('client_id'));
|
$hasClient = filled($this->input('client_id'));
|
||||||
|
$hasSousTraitant = filled($this->input('sous_traitant_id'));
|
||||||
$hasGroup = filled($this->input('group_id'));
|
$hasGroup = filled($this->input('group_id'));
|
||||||
|
|
||||||
if (! $hasClient && ! $hasGroup) {
|
if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) {
|
||||||
$message = 'Un client ou un groupe de clients est obligatoire.';
|
$message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.';
|
||||||
|
|
||||||
$validator->errors()->add('client_id', $message);
|
$validator->errors()->add('client_id', $message);
|
||||||
|
$validator->errors()->add('sous_traitant_id', $message);
|
||||||
$validator->errors()->add('group_id', $message);
|
$validator->errors()->add('group_id', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasClient && $hasGroup) {
|
if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) {
|
||||||
$message = 'Sélectionnez soit un client, soit un groupe de clients.';
|
$message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.';
|
||||||
|
|
||||||
$validator->errors()->add('client_id', $message);
|
$validator->errors()->add('client_id', $message);
|
||||||
|
$validator->errors()->add('sous_traitant_id', $message);
|
||||||
$validator->errors()->add('group_id', $message);
|
$validator->errors()->add('group_id', $message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -71,26 +65,26 @@ class StoreQuoteRequest extends FormRequest
|
|||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.nullable' => 'Le client sélectionné est invalide.',
|
'client_id.exists' => 'Le client selectionne est invalide.',
|
||||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.',
|
||||||
'group_id.exists' => 'Le groupe sélectionné est invalide.',
|
'group_id.exists' => 'Le groupe selectionne est invalide.',
|
||||||
'status.required' => 'Le statut est obligatoire.',
|
'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.required' => 'La date du devis est obligatoire.',
|
||||||
'quote_date.date' => 'La date du devis n\'est pas valide.',
|
'quote_date.date' => 'La date du devis n est pas valide.',
|
||||||
'valid_until.date' => 'La date de validité n\'est pas valide.',
|
'valid_until.date' => 'La date de validite n est pas valide.',
|
||||||
'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.',
|
'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.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.required' => 'Le total HT est obligatoire.',
|
||||||
'total_ht.numeric' => 'Le total HT doit être un nombre.',
|
'total_ht.numeric' => 'Le total HT doit etre un nombre.',
|
||||||
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
|
'total_ht.min' => 'Le total HT ne peut pas etre negatif.',
|
||||||
'total_tva.required' => 'Le total TVA est obligatoire.',
|
'total_tva.required' => 'Le total TVA est obligatoire.',
|
||||||
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
|
'total_tva.numeric' => 'Le total TVA doit etre un nombre.',
|
||||||
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
|
'total_tva.min' => 'Le total TVA ne peut pas etre negatif.',
|
||||||
'total_ttc.required' => 'Le total TTC est obligatoire.',
|
'total_ttc.required' => 'Le total TTC est obligatoire.',
|
||||||
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
|
'total_ttc.numeric' => 'Le total TTC doit etre un nombre.',
|
||||||
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
|
'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,24 +7,16 @@ use Illuminate\Validation\Rule;
|
|||||||
|
|
||||||
class UpdateInterventionRequest extends FormRequest
|
class UpdateInterventionRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
// Add authorization logic if needed
|
|
||||||
return true;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
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'],
|
'deceased_id' => ['nullable', 'exists:deceased,id'],
|
||||||
'order_giver' => ['nullable', 'string', 'max:255'],
|
'order_giver' => ['nullable', 'string', 'max:255'],
|
||||||
'location_id' => ['nullable', 'exists:client_locations,id'],
|
'location_id' => ['nullable', 'exists:client_locations,id'],
|
||||||
@ -35,7 +27,7 @@ class UpdateInterventionRequest extends FormRequest
|
|||||||
'exhumation',
|
'exhumation',
|
||||||
'retrait_pacemaker',
|
'retrait_pacemaker',
|
||||||
'retrait_bijoux',
|
'retrait_bijoux',
|
||||||
'autre'
|
'autre',
|
||||||
])],
|
])],
|
||||||
'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
|
'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
|
||||||
'duration_min' => ['nullable', 'integer', 'min:0'],
|
'duration_min' => ['nullable', 'integer', 'min:0'],
|
||||||
@ -44,7 +36,7 @@ class UpdateInterventionRequest extends FormRequest
|
|||||||
'planifie',
|
'planifie',
|
||||||
'en_cours',
|
'en_cours',
|
||||||
'termine',
|
'termine',
|
||||||
'annule'
|
'annule',
|
||||||
])],
|
])],
|
||||||
'practitioners' => ['nullable', 'array'],
|
'practitioners' => ['nullable', 'array'],
|
||||||
'practitioners.*' => ['exists:thanatopractitioners,id'],
|
'practitioners.*' => ['exists:thanatopractitioners,id'],
|
||||||
@ -52,33 +44,48 @@ class UpdateInterventionRequest extends FormRequest
|
|||||||
'assistant_practitioner_ids' => ['nullable', 'array'],
|
'assistant_practitioner_ids' => ['nullable', 'array'],
|
||||||
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
||||||
'notes' => ['nullable', 'string'],
|
'notes' => ['nullable', 'string'],
|
||||||
'created_by' => ['nullable', 'exists:users,id']
|
'created_by' => ['nullable', 'exists:users,id'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function withValidator($validator): void
|
||||||
* Get custom error messages for validator errors.
|
{
|
||||||
*/
|
$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
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.required' => 'Le client est obligatoire.',
|
'client_id.exists' => 'Le client selectionne est invalide.',
|
||||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.',
|
||||||
'deceased_id.exists' => 'Le défunt sélectionné est invalide.',
|
'deceased_id.exists' => 'Le defunt selectionne est invalide.',
|
||||||
'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
|
'order_giver.max' => 'Le donneur d ordre ne peut pas depasser 255 caracteres.',
|
||||||
'location_id.exists' => 'Le lieu sélectionné est invalide.',
|
'location_id.exists' => 'Le lieu selectionne est invalide.',
|
||||||
'type.required' => 'Le type d\'intervention est obligatoire.',
|
'type.required' => 'Le type d intervention est obligatoire.',
|
||||||
'type.in' => 'Le type d\'intervention est invalide.',
|
'type.in' => 'Le type d intervention est invalide.',
|
||||||
'scheduled_at.date_format' => 'Le format de la date programmée est invalide.',
|
'scheduled_at.date_format' => 'Le format de la date programmee est invalide.',
|
||||||
'duration_min.integer' => 'La durée doit être un nombre entier.',
|
'duration_min.integer' => 'La duree doit etre un nombre entier.',
|
||||||
'duration_min.min' => 'La durée ne peut pas être négative.',
|
'duration_min.min' => 'La duree ne peut pas etre negative.',
|
||||||
'status.in' => 'Le statut de l\'intervention est invalide.',
|
'status.in' => 'Le statut de l intervention est invalide.',
|
||||||
'practitioners.array' => 'Les praticiens doivent être un tableau.',
|
'practitioners.array' => 'Les praticiens doivent etre un tableau.',
|
||||||
'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
|
'practitioners.*.exists' => 'Un des praticiens selectionnes est invalide.',
|
||||||
'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
|
'principal_practitioner_id.exists' => 'Le praticien principal selectionne est invalide.',
|
||||||
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
|
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent etre un tableau.',
|
||||||
'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
|
'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
|
class UpdateInvoiceRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$invoiceId = $this->route('invoice');
|
$invoiceId = $this->route('invoice');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'client_id' => 'nullable|exists:clients,id',
|
'client_id' => 'nullable|exists:clients,id',
|
||||||
|
'sous_traitant_id' => 'nullable|exists:sous_traitants,id',
|
||||||
'group_id' => 'nullable|exists:client_groups,id',
|
'group_id' => 'nullable|exists:client_groups,id',
|
||||||
'source_quote_id' => 'nullable|exists:quotes,id',
|
'source_quote_id' => 'nullable|exists:quotes,id',
|
||||||
'invoice_number' => 'sometimes|string|max:191|unique:invoices,invoice_number,' . $invoiceId,
|
'invoice_number' => 'sometimes|string|max:191|unique:invoices,invoice_number,' . $invoiceId,
|
||||||
@ -43,49 +36,51 @@ class UpdateInvoiceRequest extends FormRequest
|
|||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
'client_id.exists' => 'Le client selectionne est invalide.',
|
||||||
'group_id.exists' => 'Le groupe sélectionné 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.',
|
'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.string' => 'Le numero de facture doit etre une chaine de caracteres.',
|
||||||
'invoice_number.max' => 'Le numéro de facture ne doit pas dépasser 191 caractères.',
|
'invoice_number.max' => 'Le numero de facture ne doit pas depasser 191 caracteres.',
|
||||||
'invoice_number.unique' => 'Ce numéro de facture existe déjà.',
|
'invoice_number.unique' => 'Ce numero de facture existe deja.',
|
||||||
'status.in' => 'Le statut sélectionné est invalide.',
|
'status.in' => 'Le statut selectionne est invalide.',
|
||||||
'invoice_date.date' => 'La date de la facture n\'est pas valide.',
|
'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.date' => 'La date d echeance 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.',
|
'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 caractères.',
|
'currency.size' => 'La devise doit comporter 3 caracteres.',
|
||||||
'total_ht.numeric' => 'Le total HT doit être un nombre.',
|
'total_ht.numeric' => 'Le total HT doit etre un nombre.',
|
||||||
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
|
'total_ht.min' => 'Le total HT ne peut pas etre negatif.',
|
||||||
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
|
'total_tva.numeric' => 'Le total TVA doit etre un nombre.',
|
||||||
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
|
'total_tva.min' => 'Le total TVA ne peut pas etre negatif.',
|
||||||
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
|
'total_ttc.numeric' => 'Le total TTC doit etre un nombre.',
|
||||||
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
|
'total_ttc.min' => 'Le total TTC ne peut pas etre negatif.',
|
||||||
'e_invoicing_channel_id.exists' => 'Le canal de facturation électronique est invalide.',
|
'e_invoicing_channel_id.exists' => 'Le canal de facturation electronique est invalide.',
|
||||||
'e_invoice_status.in' => 'Le statut de facturation électronique est invalide.',
|
'e_invoice_status.in' => 'Le statut de facturation electronique est invalide.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withValidator($validator): void
|
public function withValidator($validator): void
|
||||||
{
|
{
|
||||||
$validator->after(function ($validator) {
|
$validator->after(function ($validator) {
|
||||||
if (! $this->hasAny(['client_id', 'group_id'])) {
|
if (! $this->hasAny(['client_id', 'sous_traitant_id', 'group_id'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasClient = filled($this->input('client_id'));
|
$hasClient = filled($this->input('client_id'));
|
||||||
|
$hasSousTraitant = filled($this->input('sous_traitant_id'));
|
||||||
$hasGroup = filled($this->input('group_id'));
|
$hasGroup = filled($this->input('group_id'));
|
||||||
|
|
||||||
if (! $hasClient && ! $hasGroup) {
|
if (! $hasClient && ! $hasSousTraitant && ! $hasGroup) {
|
||||||
$message = 'Un client ou un groupe de clients est obligatoire.';
|
$message = 'Un client, un sous-traitant ou un groupe de clients est obligatoire.';
|
||||||
|
|
||||||
$validator->errors()->add('client_id', $message);
|
$validator->errors()->add('client_id', $message);
|
||||||
|
$validator->errors()->add('sous_traitant_id', $message);
|
||||||
$validator->errors()->add('group_id', $message);
|
$validator->errors()->add('group_id', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasClient && $hasGroup) {
|
if ((int) $hasClient + (int) $hasSousTraitant + (int) $hasGroup > 1) {
|
||||||
$message = 'Selectionnez soit un client, soit un groupe de clients.';
|
$message = 'Selectionnez soit un client, soit un sous-traitant, soit un groupe de clients.';
|
||||||
|
|
||||||
$validator->errors()->add('client_id', $message);
|
$validator->errors()->add('client_id', $message);
|
||||||
|
$validator->errors()->add('sous_traitant_id', $message);
|
||||||
$validator->errors()->add('group_id', $message);
|
$validator->errors()->add('group_id', $message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,25 +6,18 @@ use Illuminate\Foundation\Http\FormRequest;
|
|||||||
|
|
||||||
class UpdateQuoteRequest extends FormRequest
|
class UpdateQuoteRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$quoteId = $this->route('quote');
|
$quoteId = $this->route('quote');
|
||||||
|
|
||||||
return [
|
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',
|
'group_id' => 'nullable|exists:client_groups,id',
|
||||||
'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $quoteId,
|
'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $quoteId,
|
||||||
'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule',
|
'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
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
'client_id.exists' => 'Le client selectionne est invalide.',
|
||||||
'group_id.exists' => 'Le groupe sélectionné est invalide.',
|
'sous_traitant_id.exists' => 'Le sous-traitant selectionne est invalide.',
|
||||||
'reference.string' => 'Le numéro de devis doit être une chaîne de caractères.',
|
'group_id.exists' => 'Le groupe selectionne est invalide.',
|
||||||
'reference.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.',
|
'reference.string' => 'Le numero de devis doit etre une chaine de caracteres.',
|
||||||
'reference.unique' => 'Ce numéro de devis existe déjà.',
|
'reference.max' => 'Le numero de devis ne doit pas depasser 191 caracteres.',
|
||||||
'status.in' => 'Le statut sélectionné est invalide.',
|
'reference.unique' => 'Ce numero de devis existe deja.',
|
||||||
'quote_date.date' => 'La date du devis n\'est pas valide.',
|
'status.in' => 'Le statut selectionne est invalide.',
|
||||||
'valid_until.date' => 'La date de validité n\'est pas valide.',
|
'quote_date.date' => 'La date du devis n est pas valide.',
|
||||||
'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.',
|
'valid_until.date' => 'La date de validite n est pas valide.',
|
||||||
'currency.size' => 'La devise doit comporter 3 caractères.',
|
'valid_until.after_or_equal' => 'La date de validite doit etre posterieure ou egale a la date du devis.',
|
||||||
'total_ht.numeric' => 'Le total HT doit être un nombre.',
|
'currency.size' => 'La devise doit comporter 3 caracteres.',
|
||||||
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
|
'total_ht.numeric' => 'Le total HT doit etre un nombre.',
|
||||||
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
|
'total_ht.min' => 'Le total HT ne peut pas etre negatif.',
|
||||||
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
|
'total_tva.numeric' => 'Le total TVA doit etre un nombre.',
|
||||||
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
|
'total_tva.min' => 'Le total TVA ne peut pas etre negatif.',
|
||||||
'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.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ class InterventionResource extends JsonResource
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
|
'client_id' => $this->client_id,
|
||||||
|
'sous_traitant_id' => $this->sous_traitant_id,
|
||||||
'quote_id' => $this->quote_id,
|
'quote_id' => $this->quote_id,
|
||||||
'invoice_id' => $this->invoice_id,
|
'invoice_id' => $this->invoice_id,
|
||||||
'quote' => $this->whenLoaded('quote', function () {
|
'quote' => $this->whenLoaded('quote', function () {
|
||||||
@ -27,6 +29,9 @@ class InterventionResource extends JsonResource
|
|||||||
'client' => $this->whenLoaded('client', function () {
|
'client' => $this->whenLoaded('client', function () {
|
||||||
return new ClientResource($this->client);
|
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 () {
|
'deceased' => $this->whenLoaded('deceased', function () {
|
||||||
return new DeceasedResource($this->deceased);
|
return new DeceasedResource($this->deceased);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class InvoiceResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'client_id' => $this->client_id,
|
'client_id' => $this->client_id,
|
||||||
|
'sous_traitant_id' => $this->sous_traitant_id,
|
||||||
'group_id' => $this->group_id,
|
'group_id' => $this->group_id,
|
||||||
'source_quote_id' => $this->source_quote_id,
|
'source_quote_id' => $this->source_quote_id,
|
||||||
'invoice_number' => $this->invoice_number,
|
'invoice_number' => $this->invoice_number,
|
||||||
@ -32,6 +33,7 @@ class InvoiceResource extends JsonResource
|
|||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
'client' => $this->whenLoaded('client'),
|
'client' => $this->whenLoaded('client'),
|
||||||
|
'sous_traitant' => $this->whenLoaded('sousTraitant'),
|
||||||
'group' => $this->whenLoaded('group'),
|
'group' => $this->whenLoaded('group'),
|
||||||
'sourceQuote' => $this->whenLoaded('sourceQuote'),
|
'sourceQuote' => $this->whenLoaded('sourceQuote'),
|
||||||
'lines' => InvoiceLineResource::collection($this->whenLoaded('lines')),
|
'lines' => InvoiceLineResource::collection($this->whenLoaded('lines')),
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class QuoteResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'client_id' => $this->client_id,
|
'client_id' => $this->client_id,
|
||||||
|
'sous_traitant_id' => $this->sous_traitant_id,
|
||||||
'group_id' => $this->group_id,
|
'group_id' => $this->group_id,
|
||||||
'reference' => $this->reference,
|
'reference' => $this->reference,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
@ -29,6 +30,7 @@ class QuoteResource extends JsonResource
|
|||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
'client' => $this->whenLoaded('client'),
|
'client' => $this->whenLoaded('client'),
|
||||||
|
'sous_traitant' => $this->whenLoaded('sousTraitant'),
|
||||||
'group' => $this->whenLoaded('group'),
|
'group' => $this->whenLoaded('group'),
|
||||||
'lines' => QuoteLineResource::collection($this->whenLoaded('lines')),
|
'lines' => QuoteLineResource::collection($this->whenLoaded('lines')),
|
||||||
'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')),
|
'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')),
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class Intervention extends Model
|
|||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'client_id',
|
'client_id',
|
||||||
|
'sous_traitant_id',
|
||||||
'deceased_id',
|
'deceased_id',
|
||||||
'order_giver',
|
'order_giver',
|
||||||
'location_id',
|
'location_id',
|
||||||
@ -60,6 +61,11 @@ class Intervention extends Model
|
|||||||
return $this->belongsTo(Client::class);
|
return $this->belongsTo(Client::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sousTraitant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SousTraitant::class, 'sous_traitant_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the deceased associated with the intervention.
|
* Get the deceased associated with the intervention.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -11,6 +11,7 @@ class Invoice extends Model
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'client_id',
|
'client_id',
|
||||||
|
'sous_traitant_id',
|
||||||
'group_id',
|
'group_id',
|
||||||
'source_quote_id',
|
'source_quote_id',
|
||||||
'invoice_number',
|
'invoice_number',
|
||||||
@ -60,6 +61,11 @@ class Invoice extends Model
|
|||||||
return $this->belongsTo(Client::class);
|
return $this->belongsTo(Client::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sousTraitant()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SousTraitant::class, 'sous_traitant_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function group()
|
public function group()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(ClientGroup::class, 'group_id');
|
return $this->belongsTo(ClientGroup::class, 'group_id');
|
||||||
|
|||||||
@ -11,6 +11,7 @@ class Quote extends Model
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'client_id',
|
'client_id',
|
||||||
|
'sous_traitant_id',
|
||||||
'group_id',
|
'group_id',
|
||||||
'reference',
|
'reference',
|
||||||
'status',
|
'status',
|
||||||
@ -54,6 +55,11 @@ class Quote extends Model
|
|||||||
return $this->belongsTo(Client::class);
|
return $this->belongsTo(Client::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sousTraitant()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SousTraitant::class, 'sous_traitant_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function group()
|
public function group()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(ClientGroup::class);
|
return $this->belongsTo(ClientGroup::class);
|
||||||
|
|||||||
@ -30,6 +30,10 @@ class InterventionRepository implements InterventionRepositoryInterface
|
|||||||
$query->where('client_id', $filters['client_id']);
|
$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'])) {
|
if (!empty($filters['deceased_id'])) {
|
||||||
$query->where('deceased_id', $filters['deceased_id']);
|
$query->where('deceased_id', $filters['deceased_id']);
|
||||||
}
|
}
|
||||||
@ -59,6 +63,7 @@ class InterventionRepository implements InterventionRepositoryInterface
|
|||||||
// Eager load related models
|
// Eager load related models
|
||||||
$query->with([
|
$query->with([
|
||||||
'client',
|
'client',
|
||||||
|
'sousTraitant',
|
||||||
'deceased',
|
'deceased',
|
||||||
'location',
|
'location',
|
||||||
'practitioners'
|
'practitioners'
|
||||||
@ -77,6 +82,7 @@ class InterventionRepository implements InterventionRepositoryInterface
|
|||||||
{
|
{
|
||||||
return Intervention::with([
|
return Intervention::with([
|
||||||
'client',
|
'client',
|
||||||
|
'sousTraitant',
|
||||||
'deceased',
|
'deceased',
|
||||||
'location',
|
'location',
|
||||||
'practitioners',
|
'practitioners',
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
|
|||||||
// Create Invoice
|
// Create Invoice
|
||||||
$invoiceData = [
|
$invoiceData = [
|
||||||
'client_id' => $quote->client_id,
|
'client_id' => $quote->client_id,
|
||||||
|
'sous_traitant_id' => $quote->sous_traitant_id,
|
||||||
'group_id' => $quote->group_id,
|
'group_id' => $quote->group_id,
|
||||||
'source_quote_id' => $quote->id,
|
'source_quote_id' => $quote->id,
|
||||||
'status' => 'brouillon', // Start as draft
|
'status' => 'brouillon', // Start as draft
|
||||||
@ -98,7 +99,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
|
|||||||
|
|
||||||
public function all(array $columns = ['*']): \Illuminate\Support\Collection
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:class="{ 'is-invalid': hasError('client') }"
|
:class="{ 'is-invalid': hasError('client') }"
|
||||||
placeholder="Nom, email..."
|
placeholder="Nom du client ou du sous-traitant..."
|
||||||
@input="handleClientSearch"
|
@input="handleClientSearch"
|
||||||
@focus="handleClientFocus"
|
@focus="handleClientFocus"
|
||||||
/>
|
/>
|
||||||
@ -196,6 +196,28 @@
|
|||||||
>
|
>
|
||||||
{{ getFieldError("client") }}
|
{{ getFieldError("client") }}
|
||||||
</small>
|
</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
|
<div
|
||||||
v-if="showClientResults"
|
v-if="showClientResults"
|
||||||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
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">
|
<div class="list-group list-group-flush">
|
||||||
<button
|
<button
|
||||||
v-for="c in clientSearchResults"
|
v-for="c in clientSearchResults"
|
||||||
:key="c.id"
|
:key="`${c.entity_type}-${c.id}`"
|
||||||
type="button"
|
type="button"
|
||||||
class="list-group-item list-group-item-action text-start"
|
class="list-group-item list-group-item-action text-start"
|
||||||
@click="selectClient(c)"
|
@click="selectClient(c)"
|
||||||
>
|
>
|
||||||
<div class="fw-semibold text-sm">{{ c.name }}</div>
|
<div
|
||||||
<small class="text-secondary">{{ c.email }}</small>
|
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>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="!clientSearchResults.length"
|
v-if="!clientSearchResults.length"
|
||||||
@ -645,7 +683,7 @@ import { Modal } from "bootstrap";
|
|||||||
import DeceasedService from "@/services/deceased";
|
import DeceasedService from "@/services/deceased";
|
||||||
import ProductService from "@/services/product";
|
import ProductService from "@/services/product";
|
||||||
import ClientLocationService from "@/services/client_location";
|
import ClientLocationService from "@/services/client_location";
|
||||||
import { ClientService } from "@/services/client";
|
import OrderGiverSearchService from "@/services/orderGiverSearch";
|
||||||
import ThanatopractitionerService from "@/services/thanatopractitioner";
|
import ThanatopractitionerService from "@/services/thanatopractitioner";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
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 = () => {
|
const handleClientSearch = () => {
|
||||||
showClientResults.value = true;
|
showClientResults.value = true;
|
||||||
|
if (
|
||||||
|
selectedClient.value &&
|
||||||
|
clientSearchQuery.value !== selectedClient.value.name
|
||||||
|
) {
|
||||||
|
selectedClient.value = null;
|
||||||
|
}
|
||||||
if (clientSearchTimeout) clearTimeout(clientSearchTimeout);
|
if (clientSearchTimeout) clearTimeout(clientSearchTimeout);
|
||||||
if (clientSearchQuery.value.length < 2) {
|
if (clientSearchQuery.value.length < 2) {
|
||||||
clientSearchResults.value = recentClients.value;
|
clientSearchResults.value = recentClients.value;
|
||||||
@ -802,29 +851,21 @@ const handleClientSearch = () => {
|
|||||||
}
|
}
|
||||||
clientSearchTimeout = setTimeout(async () => {
|
clientSearchTimeout = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
clientSearchResults.value = await ClientService.searchClients(
|
clientSearchResults.value = await OrderGiverSearchService.search(
|
||||||
clientSearchQuery.value
|
clientSearchQuery.value
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
clientSearchResults.value = [];
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadRecentClients = async () => {
|
const loadRecentClients = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await ClientService.getAllClients({
|
recentClients.value = await OrderGiverSearchService.getRecent();
|
||||||
page: 1,
|
|
||||||
per_page: 5,
|
|
||||||
});
|
|
||||||
const clients = Array.isArray(response?.data)
|
|
||||||
? response.data
|
|
||||||
: Array.isArray(response?.data?.data)
|
|
||||||
? response.data.data
|
|
||||||
: [];
|
|
||||||
recentClients.value = clients;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load recent clients", e);
|
console.error("Failed to load recent order givers", e);
|
||||||
recentClients.value = [];
|
recentClients.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -841,7 +882,7 @@ const handleClientFocus = async () => {
|
|||||||
|
|
||||||
const selectClient = (c) => {
|
const selectClient = (c) => {
|
||||||
selectedClient.value = c;
|
selectedClient.value = c;
|
||||||
clientSearchQuery.value = c.name + (c.email ? ` (${c.email})` : "");
|
clientSearchQuery.value = c.name;
|
||||||
clientSearchResults.value = [];
|
clientSearchResults.value = [];
|
||||||
showClientResults.value = false;
|
showClientResults.value = false;
|
||||||
errors.value = errors.value.filter((e) => e.field !== "client");
|
errors.value = errors.value.filter((e) => e.field !== "client");
|
||||||
@ -1098,29 +1139,36 @@ const handleSubmit = async () => {
|
|||||||
formData.append("deceased[death_date]", deceasedForm.value.death_date);
|
formData.append("deceased[death_date]", deceasedForm.value.death_date);
|
||||||
}
|
}
|
||||||
if (selectedClient.value) {
|
if (selectedClient.value) {
|
||||||
formData.append("client_id", selectedClient.value.id);
|
if (selectedClient.value.entity_type === "sous_traitant") {
|
||||||
if (selectedClient.value.name)
|
formData.append("sous_traitant_id", selectedClient.value.id);
|
||||||
formData.append("client[name]", selectedClient.value.name);
|
} else {
|
||||||
if (selectedClient.value.email)
|
formData.append("client_id", selectedClient.value.id);
|
||||||
formData.append("client[email]", selectedClient.value.email);
|
if (selectedClient.value.name)
|
||||||
if (selectedClient.value.phone)
|
formData.append("client[name]", selectedClient.value.name);
|
||||||
formData.append("client[phone]", selectedClient.value.phone);
|
if (selectedClient.value.email)
|
||||||
if (selectedClient.value.billing_address) {
|
formData.append("client[email]", selectedClient.value.email);
|
||||||
const ba = selectedClient.value.billing_address;
|
if (selectedClient.value.phone)
|
||||||
if (ba.line1)
|
formData.append("client[phone]", selectedClient.value.phone);
|
||||||
formData.append("client[billing_address_line1]", ba.line1);
|
if (selectedClient.value.billing_address) {
|
||||||
if (ba.line2)
|
const ba = selectedClient.value.billing_address;
|
||||||
formData.append("client[billing_address_line2]", ba.line2);
|
if (ba.line1)
|
||||||
if (ba.postal_code)
|
formData.append("client[billing_address_line1]", ba.line1);
|
||||||
formData.append("client[billing_postal_code]", ba.postal_code);
|
if (ba.line2)
|
||||||
if (ba.city) formData.append("client[billing_city]", ba.city);
|
formData.append("client[billing_address_line2]", ba.line2);
|
||||||
if (ba.country_code)
|
if (ba.postal_code)
|
||||||
formData.append("client[billing_country_code]", ba.country_code);
|
formData.append("client[billing_postal_code]", ba.postal_code);
|
||||||
|
if (ba.city) formData.append("client[billing_city]", ba.city);
|
||||||
|
if (ba.country_code)
|
||||||
|
formData.append("client[billing_country_code]", ba.country_code);
|
||||||
|
}
|
||||||
|
if (selectedClient.value.vat_number)
|
||||||
|
formData.append(
|
||||||
|
"client[vat_number]",
|
||||||
|
selectedClient.value.vat_number
|
||||||
|
);
|
||||||
|
if (selectedClient.value.siret)
|
||||||
|
formData.append("client[siret]", selectedClient.value.siret);
|
||||||
}
|
}
|
||||||
if (selectedClient.value.vat_number)
|
|
||||||
formData.append("client[vat_number]", selectedClient.value.vat_number);
|
|
||||||
if (selectedClient.value.siret)
|
|
||||||
formData.append("client[siret]", selectedClient.value.siret);
|
|
||||||
}
|
}
|
||||||
if (locationForm.value.is_existing && locationForm.value.id) {
|
if (locationForm.value.is_existing && locationForm.value.id) {
|
||||||
formData.append("location_id", locationForm.value.id);
|
formData.append("location_id", locationForm.value.id);
|
||||||
|
|||||||
@ -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>
|
<h5 class="mb-1">Planifier une intervention</h5>
|
||||||
<p class="text-sm text-muted mb-0">
|
<p class="text-sm text-muted mb-0">
|
||||||
Préparez une fiche claire, liée au client et au défunt, dans le
|
Préparez une fiche claire, liée à un client ou à un sous-traitant.
|
||||||
style du dashboard.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,7 +43,7 @@
|
|||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
<div class="check-item">
|
<div class="check-item">
|
||||||
<span class="check-dot bg-gradient-success"></span>
|
<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>
|
||||||
<div class="check-item">
|
<div class="check-item">
|
||||||
<span class="check-dot bg-gradient-info"></span>
|
<span class="check-dot bg-gradient-info"></span>
|
||||||
@ -79,7 +78,8 @@
|
|||||||
|
|
||||||
<div class="hero-pill-group">
|
<div class="hero-pill-group">
|
||||||
<span class="hero-pill">
|
<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>
|
||||||
<span class="hero-pill">
|
<span class="hero-pill">
|
||||||
<i class="fas fa-cross me-2 text-info"></i>Défunt
|
<i class="fas fa-cross me-2 text-info"></i>Défunt
|
||||||
@ -102,6 +102,8 @@
|
|||||||
:client-loading="clientLoading"
|
:client-loading="clientLoading"
|
||||||
:search-clients="searchClients"
|
:search-clients="searchClients"
|
||||||
:on-client-select="onClientSelect"
|
:on-client-select="onClientSelect"
|
||||||
|
:search-sous-traitants="searchSousTraitants"
|
||||||
|
:on-sous-traitant-select="onSousTraitantSelect"
|
||||||
:search-deceased="searchDeceased"
|
:search-deceased="searchDeceased"
|
||||||
:on-deceased-select="onDeceasedSelect"
|
:on-deceased-select="onDeceasedSelect"
|
||||||
@create-intervention="handleCreateIntervention"
|
@create-intervention="handleCreateIntervention"
|
||||||
@ -109,12 +111,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</client-detail-template>
|
</client-detail-template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
||||||
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
|
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
|
||||||
import SoftBadge from "@/components/SoftBadge.vue";
|
import SoftBadge from "@/components/SoftBadge.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
|
||||||
import { RouterLink } from "vue-router";
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
@ -153,6 +156,14 @@ defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
searchSousTraitants: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onSousTraitantSelect: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
searchDeceased: {
|
searchDeceased: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@ -108,25 +108,25 @@
|
|||||||
<h6 class="mb-3">Quote lines</h6>
|
<h6 class="mb-3">Quote lines</h6>
|
||||||
<quote-lines-table :lines="quote.lines" />
|
<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">
|
<ul class="list-group">
|
||||||
<li
|
<li
|
||||||
class="list-group-item border-0 d-flex p-4 mb-2 bg-gray-100 border-radius-lg"
|
class="list-group-item border-0 d-flex p-4 mb-2 bg-gray-100 border-radius-lg"
|
||||||
>
|
>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<h6 class="mb-3 text-sm">
|
<h6 class="mb-3 text-sm">
|
||||||
{{ recipientName }}
|
{{ recipientTypeLabel }} : {{ recipientName }}
|
||||||
</h6>
|
</h6>
|
||||||
<span class="mb-2 text-xs">
|
<span class="mb-2 text-xs">
|
||||||
Email Address:
|
Email Address:
|
||||||
<span class="text-dark ms-2 font-weight-bold">{{
|
<span class="text-dark ms-2 font-weight-bold">{{
|
||||||
quote.client?.email || groupDetailsFallback
|
recipientEmail || groupDetailsFallback
|
||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="mb-2 text-xs">
|
<span class="mb-2 text-xs">
|
||||||
Phone:
|
Phone:
|
||||||
<span class="text-dark ms-2 font-weight-bold">{{
|
<span class="text-dark ms-2 font-weight-bold">{{
|
||||||
quote.client?.phone || groupDetailsFallback
|
recipientPhone || groupDetailsFallback
|
||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs">
|
<span class="text-xs">
|
||||||
@ -191,8 +191,32 @@ const selectedStatus = ref("brouillon");
|
|||||||
|
|
||||||
const recipientName = computed(() => {
|
const recipientName = computed(() => {
|
||||||
if (quote.value?.client?.name) return quote.value.client.name;
|
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;
|
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(() => {
|
const groupDetailsFallback = computed(() => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<!-- Client Selection -->
|
<!-- Client Selection -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Client <span class="text-danger">*</span>
|
Client (Donneur d'ordre) <span class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<modal-search
|
<modal-search
|
||||||
:search-action="searchClients"
|
:search-action="searchClients"
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
Informations principales
|
Informations principales
|
||||||
</h5>
|
</h5>
|
||||||
<p class="mb-0 text-sm text-muted">
|
<p class="mb-0 text-sm text-muted">
|
||||||
Identifiez le client, le défunt concerné et le type
|
Identifiez le client ou le sous-traitant, le défunt concerné
|
||||||
d'intervention.
|
et le type d'intervention.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<soft-badge color="info" variant="gradient" size="sm">
|
<soft-badge color="info" variant="gradient" size="sm">
|
||||||
@ -25,31 +25,58 @@
|
|||||||
<div class="card-body pt-3 p-4">
|
<div class="card-body pt-3 p-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label"
|
<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
|
<search-input
|
||||||
v-model="selectedItem"
|
v-model="selectedIntervenant"
|
||||||
:search-action="props.searchClients"
|
:search-action="currentSearchAction"
|
||||||
:min-chars="0"
|
:min-chars="0"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
item-label="name"
|
:item-label="getIntervenantLabel"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@select="handleSelect"
|
@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>
|
<span>
|
||||||
{{ selectedItem.name }}
|
{{ getIntervenantLabel(selectedIntervenant) }}
|
||||||
<small class="text-muted ms-1">{{
|
<small class="text-muted ms-1">{{
|
||||||
selectedItem.email || "Pas d'email"
|
selectedIntervenant.email || "Pas d'email"
|
||||||
}}</small>
|
}}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="fieldErrors.client_id"
|
v-if="fieldErrors.client_id || fieldErrors.sous_traitant_id"
|
||||||
class="invalid-feedback small-error"
|
class="invalid-feedback small-error"
|
||||||
>
|
>
|
||||||
{{ fieldErrors.client_id }}
|
{{ fieldErrors.client_id || fieldErrors.sous_traitant_id }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -257,10 +284,8 @@
|
|||||||
<div class="summary-panel">
|
<div class="summary-panel">
|
||||||
<p class="summary-title mb-3">Résumé rapide</p>
|
<p class="summary-title mb-3">Résumé rapide</p>
|
||||||
<div class="summary-line">
|
<div class="summary-line">
|
||||||
<span>Client</span>
|
<span>Intervenant</span>
|
||||||
<strong>{{
|
<strong>{{ summaryIntervenant }}</strong>
|
||||||
selectedItem?.name || "Non sélectionné"
|
|
||||||
}}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-line">
|
<div class="summary-line">
|
||||||
<span>Défunt</span>
|
<span>Défunt</span>
|
||||||
@ -319,13 +344,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
import { computed, defineEmits, defineProps, ref, watch } from "vue";
|
||||||
import SoftInput from "@/components/SoftInput.vue";
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import SoftBadge from "@/components/SoftBadge.vue";
|
import SoftBadge from "@/components/SoftBadge.vue";
|
||||||
import SearchInput from "@/components/atoms/input/SearchInput.vue";
|
import SearchInput from "@/components/atoms/input/SearchInput.vue";
|
||||||
|
|
||||||
// Props
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -363,6 +387,14 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
searchSousTraitants: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onSousTraitantSelect: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
searchDeceased: {
|
searchDeceased: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
@ -373,60 +405,17 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
|
||||||
const emit = defineEmits(["createIntervention"]);
|
const emit = defineEmits(["createIntervention"]);
|
||||||
|
|
||||||
// Reactive data
|
|
||||||
const errors = ref([]);
|
const errors = ref([]);
|
||||||
// Search input data
|
const selectedIntervenant = ref(null);
|
||||||
const selectedItem = ref(null);
|
|
||||||
const selectedDeceased = 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 fieldErrors = ref({});
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
|
intervenant_type: "client",
|
||||||
client_id: "",
|
client_id: "",
|
||||||
|
sous_traitant_id: "",
|
||||||
deceased_id: "",
|
deceased_id: "",
|
||||||
type: "",
|
type: "",
|
||||||
scheduled_date: "",
|
scheduled_date: "",
|
||||||
@ -437,7 +426,73 @@ const form = ref({
|
|||||||
notes: "",
|
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(
|
watch(
|
||||||
() => props.validationErrors,
|
() => props.validationErrors,
|
||||||
(newErrors) => {
|
(newErrors) => {
|
||||||
@ -446,7 +501,6 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Watch for success from parent
|
|
||||||
watch(
|
watch(
|
||||||
() => props.success,
|
() => props.success,
|
||||||
(newSuccess) => {
|
(newSuccess) => {
|
||||||
@ -456,20 +510,30 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Watch for client_id changes to update selectedItem
|
|
||||||
watch(
|
watch(
|
||||||
() => form.value.client_id,
|
() => form.value.client_id,
|
||||||
(newClientId) => {
|
(newClientId) => {
|
||||||
if (newClientId && fieldErrors.value.client_id) {
|
if (newClientId && fieldErrors.value.client_id) {
|
||||||
delete fieldErrors.value.client_id;
|
delete fieldErrors.value.client_id;
|
||||||
}
|
}
|
||||||
if (!newClientId) {
|
if (!newClientId && form.value.intervenant_type === "client") {
|
||||||
selectedItem.value = null;
|
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(
|
watch(
|
||||||
() => form.value.deceased_id,
|
() => form.value.deceased_id,
|
||||||
(newDeceasedId) => {
|
(newDeceasedId) => {
|
||||||
@ -492,19 +556,22 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
// Clear errors before submitting
|
|
||||||
fieldErrors.value = {};
|
fieldErrors.value = {};
|
||||||
errors.value = [];
|
errors.value = [];
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
|
|
||||||
if (!form.value.client_id || form.value.client_id === "") {
|
if (form.value.intervenant_type === "client") {
|
||||||
fieldErrors.value.client_id = "Le client est obligatoire";
|
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;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.value.type || form.value.type === "") {
|
if (!form.value.type) {
|
||||||
fieldErrors.value.type = "Le type d'intervention est obligatoire";
|
fieldErrors.value.type = "Le type d'intervention est obligatoire";
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
@ -513,7 +580,6 @@ const submitForm = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up form data: convert empty strings to null
|
|
||||||
const cleanedForm = {};
|
const cleanedForm = {};
|
||||||
const formData = form.value;
|
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) {
|
if (cleanedForm.scheduled_date && cleanedForm.scheduled_time) {
|
||||||
// Format: Y-m-d H:i:s (e.g., "2024-12-15 14:30:00")
|
cleanedForm.scheduled_at = `${cleanedForm.scheduled_date} ${cleanedForm.scheduled_time}: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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert string numbers to integers
|
delete cleanedForm.scheduled_date;
|
||||||
|
delete cleanedForm.scheduled_time;
|
||||||
|
delete cleanedForm.intervenant_type;
|
||||||
|
|
||||||
if (cleanedForm.client_id) {
|
if (cleanedForm.client_id) {
|
||||||
cleanedForm.client_id = parseInt(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) {
|
if (cleanedForm.deceased_id) {
|
||||||
cleanedForm.deceased_id = parseInt(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);
|
console.log("Intervention form data being emitted:", cleanedForm);
|
||||||
|
|
||||||
// Emit the cleaned form data to parent
|
|
||||||
emit("createIntervention", cleanedForm);
|
emit("createIntervention", cleanedForm);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
form.value = {
|
form.value = {
|
||||||
|
intervenant_type: "client",
|
||||||
client_id: "",
|
client_id: "",
|
||||||
|
sous_traitant_id: "",
|
||||||
deceased_id: "",
|
deceased_id: "",
|
||||||
type: "",
|
type: "",
|
||||||
scheduled_date: "",
|
scheduled_date: "",
|
||||||
@ -564,8 +630,7 @@ const resetForm = () => {
|
|||||||
order_giver: "",
|
order_giver: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
};
|
};
|
||||||
// Clear the selected items
|
selectedIntervenant.value = null;
|
||||||
selectedItem.value = null;
|
|
||||||
selectedDeceased.value = null;
|
selectedDeceased.value = null;
|
||||||
clearErrors();
|
clearErrors();
|
||||||
};
|
};
|
||||||
@ -601,6 +666,31 @@ const clearErrors = () => {
|
|||||||
margin-top: 0.25rem;
|
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 {
|
.selection-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -621,20 +711,6 @@ const clearErrors = () => {
|
|||||||
height: 1rem;
|
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-select,
|
||||||
.soft-textarea {
|
.soft-textarea {
|
||||||
background-color: white;
|
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 * from "./http";
|
||||||
export { default as AuthService } from "./auth";
|
export { default as AuthService } from "./auth";
|
||||||
|
export { default as OrderGiverSearchService } from "./orderGiverSearch";
|
||||||
export { default as WebmailService } from "./webmail";
|
export { default as WebmailService } from "./webmail";
|
||||||
export { default as SousTraitantService } from "./sousTraitant";
|
export { default as SousTraitantService } from "./sousTraitant";
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { request } from "./http";
|
import { request } from "./http";
|
||||||
import { Client } from "./client";
|
import { Client } from "./client";
|
||||||
import type { ClientGroup } from "./clientGroup";
|
import type { ClientGroup } from "./clientGroup";
|
||||||
|
import type { SousTraitant } from "./sousTraitant";
|
||||||
|
|
||||||
export interface Invoice {
|
export interface Invoice {
|
||||||
id: number;
|
id: number;
|
||||||
client_id: number | null;
|
client_id: number | null;
|
||||||
|
sous_traitant_id: number | null;
|
||||||
group_id: number | null;
|
group_id: number | null;
|
||||||
source_quote_id: number | null;
|
source_quote_id: number | null;
|
||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
@ -28,6 +30,7 @@ export interface Invoice {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
client?: Client;
|
client?: Client;
|
||||||
|
sous_traitant?: SousTraitant;
|
||||||
group?: ClientGroup;
|
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 { http, request } from "./http";
|
||||||
import { Client } from "./client";
|
import { Client } from "./client";
|
||||||
|
import type { SousTraitant } from "./sousTraitant";
|
||||||
|
|
||||||
export interface Quote {
|
export interface Quote {
|
||||||
id: number;
|
id: number;
|
||||||
client_id: number | null;
|
client_id: number | null;
|
||||||
|
sous_traitant_id: number | null;
|
||||||
group_id: number | null;
|
group_id: number | null;
|
||||||
reference: string;
|
reference: string;
|
||||||
status: "brouillon" | "envoye" | "accepte" | "refuse" | "expire" | "annule";
|
status: "brouillon" | "envoye" | "accepte" | "refuse" | "expire" | "annule";
|
||||||
@ -16,6 +18,7 @@ export interface Quote {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
client?: Client;
|
client?: Client;
|
||||||
|
sous_traitant?: SousTraitant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuoteListResponse {
|
export interface QuoteListResponse {
|
||||||
|
|||||||
@ -9,69 +9,73 @@
|
|||||||
:client-loading="clientStore.isLoading"
|
:client-loading="clientStore.isLoading"
|
||||||
:search-clients="handleSearchClients"
|
:search-clients="handleSearchClients"
|
||||||
:on-client-select="handleClientSelect"
|
:on-client-select="handleClientSelect"
|
||||||
|
:search-sous-traitants="handleSearchSousTraitants"
|
||||||
|
:on-sous-traitant-select="handleSousTraitantSelect"
|
||||||
:search-deceased="handleSearchDeceased"
|
:search-deceased="handleSearchDeceased"
|
||||||
:on-deceased-select="handleDeceasedSelect"
|
:on-deceased-select="handleDeceasedSelect"
|
||||||
@create-intervention="handleCreateIntervention"
|
@create-intervention="handleCreateIntervention"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import AddInterventionPresentation from "@/components/Organism/Interventions/AddInterventionPresentation.vue";
|
import AddInterventionPresentation from "@/components/Organism/Interventions/AddInterventionPresentation.vue";
|
||||||
import { useInterventionStore } from "@/stores/interventionStore";
|
import { useInterventionStore } from "@/stores/interventionStore";
|
||||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
import { useSousTraitantStore } from "@/stores/sousTraitantStore";
|
||||||
import { useNotificationStore } from "@/stores/notification";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const interventionStore = useInterventionStore();
|
const interventionStore = useInterventionStore();
|
||||||
const deceasedStore = useDeceasedStore();
|
const deceasedStore = useDeceasedStore();
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
|
const sousTraitantStore = useSousTraitantStore();
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
const validationErrors = ref({});
|
const validationErrors = ref({});
|
||||||
const showSuccess = ref(false);
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
// Client search handler passed down to form
|
|
||||||
const handleSearchClients = async (query) => {
|
const handleSearchClients = async (query) => {
|
||||||
return await clientStore.searchClients(query);
|
return await clientStore.searchClients(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Client selection handler to pass down to form
|
|
||||||
const handleClientSelect = (client) => {
|
const handleClientSelect = (client) => {
|
||||||
return 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) => {
|
const handleSearchDeceased = async (query) => {
|
||||||
return await deceasedStore.searchDeceased(query);
|
return await deceasedStore.searchDeceased(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deceased selection handler to pass down to form
|
|
||||||
const handleDeceasedSelect = (deceased) => {
|
const handleDeceasedSelect = (deceased) => {
|
||||||
return deceased;
|
return deceased;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateIntervention = async (form) => {
|
const handleCreateIntervention = async (form) => {
|
||||||
try {
|
try {
|
||||||
// Clear previous errors
|
|
||||||
validationErrors.value = {};
|
validationErrors.value = {};
|
||||||
showSuccess.value = false;
|
showSuccess.value = false;
|
||||||
|
|
||||||
// Call the store to create intervention
|
await interventionStore.createIntervention(form);
|
||||||
const intervention = await interventionStore.createIntervention(form);
|
|
||||||
|
|
||||||
// Show success notification
|
|
||||||
notificationStore.created("Intervention");
|
notificationStore.created("Intervention");
|
||||||
showSuccess.value = true;
|
showSuccess.value = true;
|
||||||
|
|
||||||
// Redirect after 2 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push({ name: "Interventions" });
|
router.push({ name: "Interventions" });
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating intervention:", error);
|
console.error("Error creating intervention:", error);
|
||||||
|
|
||||||
// Handle validation errors from Laravel
|
|
||||||
if (error.response && error.response.status === 422) {
|
if (error.response && error.response.status === 422) {
|
||||||
validationErrors.value = error.response.data.errors || {};
|
validationErrors.value = error.response.data.errors || {};
|
||||||
notificationStore.error(
|
notificationStore.error(
|
||||||
@ -79,7 +83,6 @@ const handleCreateIntervention = async (form) => {
|
|||||||
"Veuillez corriger les erreurs dans le formulaire"
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
);
|
);
|
||||||
} else if (error.response && error.response.data) {
|
} else if (error.response && error.response.data) {
|
||||||
// Handle other API errors
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.response.data.message || "Une erreur est survenue";
|
error.response.data.message || "Une erreur est survenue";
|
||||||
notificationStore.error("Erreur", errorMessage);
|
notificationStore.error("Erreur", errorMessage);
|
||||||
@ -89,12 +92,12 @@ const handleCreateIntervention = async (form) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load deceased and client lists for selection
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deceasedStore.fetchDeceased(),
|
deceasedStore.fetchDeceased(),
|
||||||
clientStore.fetchClients(),
|
clientStore.fetchClients(),
|
||||||
|
sousTraitantStore.fetchSousTraitants(),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading data:", error);
|
console.error("Error loading data:", error);
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<sous-traitant-commande-list-presentation />
|
||||||
<h1>Commandes sous-traitants</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import SousTraitantCommandeListPresentation from "@/components/Organism/CRM/SousTraitantCommandeListPresentation.vue";
|
||||||
name: "CommandesSousTraitants",
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<sous-traitant-facture-list-presentation />
|
||||||
<h1>Factures sous-traitants</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import SousTraitantFactureListPresentation from "@/components/Organism/CRM/SousTraitantFactureListPresentation.vue";
|
||||||
name: "FacturesSousTraitants",
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user