Link internvetion and invoice and quote

This commit is contained in:
nyavokevin 2026-03-17 16:30:02 +03:00
parent 8ee7d8f8e9
commit ebd171e9de
8 changed files with 211 additions and 67 deletions

View File

@ -77,8 +77,9 @@ class InterventionController extends Controller
DeceasedRepositoryInterface $deceasedRepository, DeceasedRepositoryInterface $deceasedRepository,
QuoteRepositoryInterface $quoteRepository, QuoteRepositoryInterface $quoteRepository,
ProductRepositoryInterface $productRepository, ProductRepositoryInterface $productRepository,
private readonly \App\Repositories\ClientLocationRepositoryInterface $clientLocationRepository private \App\Repositories\ClientLocationRepositoryInterface $clientLocationRepository
) { )
{
$this->interventionRepository = $interventionRepository; $this->interventionRepository = $interventionRepository;
$this->interventionPractitionerRepository = $interventionPractitionerRepository; $this->interventionPractitionerRepository = $interventionPractitionerRepository;
$this->clientRepository = $clientRepository; $this->clientRepository = $clientRepository;
@ -110,7 +111,8 @@ class InterventionController extends Controller
$interventions = $this->interventionRepository->getAllPaginated($filters, $perPage); $interventions = $this->interventionRepository->getAllPaginated($filters, $perPage);
return response()->json(new InterventionCollection($interventions)); return response()->json(new InterventionCollection($interventions));
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error fetching interventions list: ' . $e->getMessage()); Log::error('Error fetching interventions list: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -131,7 +133,8 @@ class InterventionController extends Controller
$intervention = $this->interventionRepository->create($validated); $intervention = $this->interventionRepository->create($validated);
return response()->json(new InterventionResource($intervention), Response::HTTP_CREATED); return response()->json(new InterventionResource($intervention), Response::HTTP_CREATED);
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error creating intervention: ' . $e->getMessage()); Log::error('Error creating intervention: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -155,7 +158,8 @@ class InterventionController extends Controller
$deceased = null; $deceased = null;
if (!empty($validated['deceased_id'])) { if (!empty($validated['deceased_id'])) {
$deceased = $this->deceasedRepository->findById($validated['deceased_id']); $deceased = $this->deceasedRepository->findById($validated['deceased_id']);
} else { }
else {
$deceasedData = $validated['deceased']; $deceasedData = $validated['deceased'];
$deceased = $this->deceasedRepository->create($deceasedData); $deceased = $this->deceasedRepository->create($deceasedData);
} }
@ -163,7 +167,8 @@ class InterventionController extends Controller
// Step 2: Link existing client or create a new one // Step 2: Link existing client or create a new one
if (!empty($validated['client_id'])) { if (!empty($validated['client_id'])) {
$client = $this->clientRepository->find($validated['client_id']); $client = $this->clientRepository->find($validated['client_id']);
} else { }
else {
$clientData = $validated['client']; $clientData = $validated['client'];
$client = $this->clientRepository->create($clientData); $client = $this->clientRepository->create($clientData);
} }
@ -234,7 +239,7 @@ class InterventionController extends Controller
if (!empty($validated['intervention']['principal_practitioner_id'])) { if (!empty($validated['intervention']['principal_practitioner_id'])) {
$this->interventionPractitionerRepository->createAssignment( $this->interventionPractitionerRepository->createAssignment(
$intervention->id, $intervention->id,
(int) $validated['intervention']['principal_practitioner_id'], (int)$validated['intervention']['principal_practitioner_id'],
'principal' 'principal'
); );
} }
@ -243,7 +248,7 @@ class InterventionController extends Controller
foreach ($validated['intervention']['assistant_practitioner_ids'] as $assistantId) { foreach ($validated['intervention']['assistant_practitioner_ids'] as $assistantId) {
$this->interventionPractitionerRepository->createAssignment( $this->interventionPractitionerRepository->createAssignment(
$intervention->id, $intervention->id,
(int) $assistantId, (int)$assistantId,
'assistant' 'assistant'
); );
} }
@ -258,7 +263,7 @@ class InterventionController extends Controller
$role = $index === 0 ? 'principal' : 'assistant'; $role = $index === 0 ? 'principal' : 'assistant';
$this->interventionPractitionerRepository->createAssignment( $this->interventionPractitionerRepository->createAssignment(
$intervention->id, $intervention->id,
(int) $practitionerId, (int)$practitionerId,
$role $role
); );
} }
@ -304,13 +309,19 @@ class InterventionController extends Controller
] ]
]; ];
$this->quoteRepository->create($quoteData); $quote = $this->quoteRepository->create($quoteData);
Log::info('Quote auto-created for intervention', ['intervention_id' => $intervention->id]);
} else { // Update the intervention with the newly created quote ID
$intervention->update(['quote_id' => $quote->id]);
Log::info('Quote auto-created for intervention', ['intervention_id' => $intervention->id, 'quote_id' => $quote->id]);
}
else {
Log::warning('No intervention product found, skipping auto-quote creation', ['intervention_id' => $intervention->id]); Log::warning('No intervention product found, skipping auto-quote creation', ['intervention_id' => $intervention->id]);
} }
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Failed to auto-create quote for intervention: ' . $e->getMessage()); Log::error('Failed to auto-create quote for intervention: ' . $e->getMessage());
// Silently fail for the quote part to not block intervention creation // Silently fail for the quote part to not block intervention creation
} }
@ -357,13 +368,15 @@ class InterventionController extends Controller
] ]
], Response::HTTP_CREATED); ], Response::HTTP_CREATED);
} catch (\Illuminate\Validation\ValidationException $e) { }
catch (\Illuminate\Validation\ValidationException $e) {
// Validation errors are handled by the FormRequest // Validation errors are handled by the FormRequest
return response()->json([ return response()->json([
'message' => 'Données invalides', 'message' => 'Données invalides',
'errors' => $e->errors() 'errors' => $e->errors()
], Response::HTTP_UNPROCESSABLE_ENTITY); ], Response::HTTP_UNPROCESSABLE_ENTITY);
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error creating intervention with all data: ' . $e->getMessage(), [ Log::error('Error creating intervention with all data: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'input' => $request->except(['documents']) // Don't log file data 'input' => $request->except(['documents']) // Don't log file data
@ -385,7 +398,8 @@ class InterventionController extends Controller
$intervention = $this->interventionRepository->findById($id); $intervention = $this->interventionRepository->findById($id);
return response()->json(new InterventionResource($intervention)); return response()->json(new InterventionResource($intervention));
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error fetching intervention details: ' . $e->getMessage()); Log::error('Error fetching intervention details: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -408,7 +422,8 @@ class InterventionController extends Controller
$updatedIntervention = $this->interventionRepository->update($intervention, $validated); $updatedIntervention = $this->interventionRepository->update($intervention, $validated);
return response()->json(new InterventionResource($updatedIntervention)); return response()->json(new InterventionResource($updatedIntervention));
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error updating intervention: ' . $e->getMessage()); Log::error('Error updating intervention: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -429,7 +444,8 @@ class InterventionController extends Controller
$this->interventionRepository->delete($intervention); $this->interventionRepository->delete($intervention);
return response()->json(null, Response::HTTP_NO_CONTENT); return response()->json(null, Response::HTTP_NO_CONTENT);
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error deleting intervention: ' . $e->getMessage()); Log::error('Error deleting intervention: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -457,7 +473,8 @@ class InterventionController extends Controller
); );
return response()->json(new InterventionResource($updatedIntervention)); return response()->json(new InterventionResource($updatedIntervention));
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error changing intervention status: ' . $e->getMessage()); Log::error('Error changing intervention status: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -493,7 +510,8 @@ class InterventionController extends Controller
'month' => $validated['month'], 'month' => $validated['month'],
] ]
]); ]);
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error fetching interventions by month: ' . $e->getMessage()); Log::error('Error fetching interventions by month: ' . $e->getMessage());
return response()->json([ return response()->json([
@ -558,7 +576,8 @@ class InterventionController extends Controller
})->toArray() })->toArray()
], Response::HTTP_OK); ], Response::HTTP_OK);
} catch (\Exception $e) { }
catch (\Exception $e) {
return response()->json([ return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'assignment.', 'message' => 'Une erreur est survenue lors de la création de l\'assignment.',
@ -627,7 +646,8 @@ class InterventionController extends Controller
]; ];
})->toArray() })->toArray()
], Response::HTTP_OK); ], Response::HTTP_OK);
} else { }
else {
Log::warning('No practitioner assignment found to delete', [ Log::warning('No practitioner assignment found to delete', [
'intervention_id' => $interventionId, 'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId 'practitioner_id' => $practitionerId
@ -638,7 +658,8 @@ class InterventionController extends Controller
], Response::HTTP_NOT_FOUND); ], Response::HTTP_NOT_FOUND);
} }
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error unassigning practitioner from intervention: ' . $e->getMessage(), [ Log::error('Error unassigning practitioner from intervention: ' . $e->getMessage(), [
'intervention_id' => $interventionId, 'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId, 'practitioner_id' => $practitionerId,
@ -683,7 +704,8 @@ class InterventionController extends Controller
})->toArray() })->toArray()
]); ]);
} catch (\Exception $e) { }
catch (\Exception $e) {
Log::error('Error in debug practitioners: ' . $e->getMessage()); Log::error('Error in debug practitioners: ' . $e->getMessage());
return response()->json(['error' => $e->getMessage()], 500); return response()->json(['error' => $e->getMessage()], 500);
} }

View File

@ -19,6 +19,11 @@ class InterventionResource extends JsonResource
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'quote_id' => $this->quote_id,
'invoice_id' => $this->invoice_id,
'quote' => $this->whenLoaded('quote', function () {
return new \App\Http\Resources\QuoteResource($this->quote);
}),
'client' => $this->whenLoaded('client', function () { 'client' => $this->whenLoaded('client', function () {
return new ClientResource($this->client); return new ClientResource($this->client);
}), }),

View File

@ -29,7 +29,9 @@ class Intervention extends Model
'status', 'status',
'attachments_count', 'attachments_count',
'notes', 'notes',
'created_by' 'created_by',
'invoice_id',
'quote_id',
]; ];
/** /**
@ -79,7 +81,7 @@ class Intervention extends Model
*/ */
public function practitioners(): BelongsToMany public function practitioners(): BelongsToMany
{ {
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id') return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->withPivot('role', 'assigned_at') ->withPivot('role', 'assigned_at')
->withTimestamps(); ->withTimestamps();
} }
@ -97,7 +99,7 @@ class Intervention extends Model
*/ */
public function principalPractitioner(): BelongsToMany public function principalPractitioner(): BelongsToMany
{ {
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id') return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'principal') ->wherePivot('role', 'principal')
->withPivot('role', 'assigned_at') ->withPivot('role', 'assigned_at')
->withTimestamps(); ->withTimestamps();
@ -108,7 +110,7 @@ class Intervention extends Model
*/ */
public function assistantPractitioners(): BelongsToMany public function assistantPractitioners(): BelongsToMany
{ {
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id') return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'assistant') ->wherePivot('role', 'assistant')
->withPivot('role', 'assigned_at') ->withPivot('role', 'assigned_at')
->withTimestamps(); ->withTimestamps();
@ -119,7 +121,7 @@ class Intervention extends Model
*/ */
public function creator(): BelongsTo public function creator(): BelongsTo
{ {
return $this->belongsTo(User::class, 'created_by'); return $this->belongsTo(User::class , 'created_by');
} }
/** /**
@ -135,7 +137,7 @@ class Intervention extends Model
*/ */
public function fileAttachments() public function fileAttachments()
{ {
return $this->morphMany(FileAttachment::class, 'attachable')->orderBy('sort_order'); return $this->morphMany(FileAttachment::class , 'attachable')->orderBy('sort_order');
} }
/** /**
@ -153,4 +155,14 @@ class Intervention extends Model
{ {
return $this->hasMany(InterventionNotification::class); return $this->hasMany(InterventionNotification::class);
} }
public function invoice(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
} }

View File

@ -70,4 +70,9 @@ class Quote extends Model
->where('document_type', 'quote') ->where('document_type', 'quote')
->orderBy('changed_at', 'desc'); ->orderBy('changed_at', 'desc');
} }
public function interventions()
{
return $this->hasMany(Intervention::class);
}
} }

View File

@ -81,7 +81,8 @@ class InterventionRepository implements InterventionRepositoryInterface
'location', 'location',
'practitioners', 'practitioners',
'attachments', 'attachments',
'notifications' 'notifications',
'quote'
])->findOrFail($id); ])->findOrFail($id);
} }

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('interventions', function (Blueprint $table) {
$table->foreignId('invoice_id')->nullable()->constrained('invoices')->nullOnDelete();
$table->foreignId('quote_id')->nullable()->constrained('quotes')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('interventions', function (Blueprint $table) {
$table->dropForeign(['invoice_id']);
$table->dropColumn('invoice_id');
$table->dropForeign(['quote_id']);
$table->dropColumn('quote_id');
});
}
};

View File

@ -265,6 +265,68 @@
/> />
</div> </div>
<!-- Quote Tab -->
<div v-if="activeTab === 'quote'" class="tab-pane fade show active">
<div class="card">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<h6 class="mb-0">Détails du devis</h6>
</div>
</div>
<div class="card-body">
<div v-if="intervention.quote" class="row mt-4">
<div class="col-md-6 mb-3">
<InfoCard title="Informations" icon="fas fa-file-invoice text-info">
<ul class="list-group list-group-flush">
<li class="list-group-item mx-0 px-0">
<strong class="text-dark">Référence:</strong>
<span class="ms-2">{{ intervention.quote.reference || '-' }}</span>
</li>
<li class="list-group-item mx-0 px-0">
<strong class="text-dark">Date:</strong>
<span class="ms-2">{{ intervention.quote.quote_date || '-' }}</span>
</li>
<li class="list-group-item mx-0 px-0">
<strong class="text-dark">Statut:</strong>
<span class="ms-2 badge badge-sm bg-gradient-success">{{ intervention.quote.status || '-' }}</span>
</li>
</ul>
</InfoCard>
</div>
<div class="col-md-6 mb-3">
<InfoCard title="Montants" icon="fas fa-euro-sign text-success">
<ul class="list-group list-group-flush">
<li class="list-group-item mx-0 px-0 d-flex justify-content-between">
<strong class="text-dark">Total HT:</strong>
<span>{{ intervention.quote.total_ht || '0.00' }} </span>
</li>
<li class="list-group-item mx-0 px-0 d-flex justify-content-between">
<strong class="text-dark">Total TVA:</strong>
<span>{{ intervention.quote.total_tva || '0.00' }} </span>
</li>
<li class="list-group-item mx-0 px-0 d-flex justify-content-between border-0">
<strong class="text-dark">Total TTC:</strong>
<span class="fw-bold">{{ intervention.quote.total_ttc || '0.00' }} </span>
</li>
</ul>
</InfoCard>
</div>
</div>
<div v-else class="text-center py-5">
<div class="avatar avatar-xl mb-3">
<div class="avatar-title bg-gradient-secondary text-white h5 mb-0">
<i class="fas fa-file-invoice"></i>
</div>
</div>
<h6 class="text-sm text-muted">Aucun devis lié</h6>
<p class="text-xs text-muted mb-3">
Il n'y a pas de devis associé à cette intervention.
</p>
</div>
</div>
</div>
</div>
<!-- History Tab --> <!-- History Tab -->
<div v-if="activeTab === 'history'" class="tab-pane fade show active"> <div v-if="activeTab === 'history'" class="tab-pane fade show active">
<div class="card"> <div class="card">

View File

@ -27,6 +27,12 @@
:badge="documentsCount > 0 ? documentsCount : null" :badge="documentsCount > 0 ? documentsCount : null"
@click="$emit('change-tab', 'documents')" @click="$emit('change-tab', 'documents')"
/> />
<TabNavigationItem
icon="fas fa-file-invoice"
label="Devis"
:is-active="activeTab === 'quote'"
@click="$emit('change-tab', 'quote')"
/>
<TabNavigationItem <TabNavigationItem
icon="fas fa-history" icon="fas fa-history"
label="Historique" label="Historique"