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,
QuoteRepositoryInterface $quoteRepository,
ProductRepositoryInterface $productRepository,
private readonly \App\Repositories\ClientLocationRepositoryInterface $clientLocationRepository
) {
private \App\Repositories\ClientLocationRepositoryInterface $clientLocationRepository
)
{
$this->interventionRepository = $interventionRepository;
$this->interventionPractitionerRepository = $interventionPractitionerRepository;
$this->clientRepository = $clientRepository;
@ -110,7 +111,8 @@ class InterventionController extends Controller
$interventions = $this->interventionRepository->getAllPaginated($filters, $perPage);
return response()->json(new InterventionCollection($interventions));
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error fetching interventions list: ' . $e->getMessage());
return response()->json([
@ -131,7 +133,8 @@ class InterventionController extends Controller
$intervention = $this->interventionRepository->create($validated);
return response()->json(new InterventionResource($intervention), Response::HTTP_CREATED);
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error creating intervention: ' . $e->getMessage());
return response()->json([
@ -155,7 +158,8 @@ class InterventionController extends Controller
$deceased = null;
if (!empty($validated['deceased_id'])) {
$deceased = $this->deceasedRepository->findById($validated['deceased_id']);
} else {
}
else {
$deceasedData = $validated['deceased'];
$deceased = $this->deceasedRepository->create($deceasedData);
}
@ -163,7 +167,8 @@ class InterventionController extends Controller
// Step 2: Link existing client or create a new one
if (!empty($validated['client_id'])) {
$client = $this->clientRepository->find($validated['client_id']);
} else {
}
else {
$clientData = $validated['client'];
$client = $this->clientRepository->create($clientData);
}
@ -194,11 +199,11 @@ class InterventionController extends Controller
}
if ($locationId) {
// Fetch location to add details to notes if needed, or just rely on relation.
// For now, let's keep the legacy behavior of adding text to notes for quick reference,
// but also link the ID. Use the provided data or fetch?
// If we have an ID, we might not have the text data in $locationData if it came from search.
// So we only append text notes if we have $locationData (Create mode or if frontend sends it).
// Fetch location to add details to notes if needed, or just rely on relation.
// For now, let's keep the legacy behavior of adding text to notes for quick reference,
// but also link the ID. Use the provided data or fetch?
// If we have an ID, we might not have the text data in $locationData if it came from search.
// So we only append text notes if we have $locationData (Create mode or if frontend sends it).
}
if (!empty($locationData)) {
@ -234,7 +239,7 @@ class InterventionController extends Controller
if (!empty($validated['intervention']['principal_practitioner_id'])) {
$this->interventionPractitionerRepository->createAssignment(
$intervention->id,
(int) $validated['intervention']['principal_practitioner_id'],
(int)$validated['intervention']['principal_practitioner_id'],
'principal'
);
}
@ -243,22 +248,22 @@ class InterventionController extends Controller
foreach ($validated['intervention']['assistant_practitioner_ids'] as $assistantId) {
$this->interventionPractitionerRepository->createAssignment(
$intervention->id,
(int) $assistantId,
(int)$assistantId,
'assistant'
);
}
}
if (
empty($validated['intervention']['principal_practitioner_id']) &&
!empty($validated['intervention']['practitioners']) &&
is_array($validated['intervention']['practitioners'])
empty($validated['intervention']['principal_practitioner_id']) &&
!empty($validated['intervention']['practitioners']) &&
is_array($validated['intervention']['practitioners'])
) {
foreach ($validated['intervention']['practitioners'] as $index => $practitionerId) {
$role = $index === 0 ? 'principal' : 'assistant';
$this->interventionPractitionerRepository->createAssignment(
$intervention->id,
(int) $practitionerId,
(int)$practitionerId,
$role
);
}
@ -304,15 +309,21 @@ class InterventionController extends Controller
]
];
$this->quoteRepository->create($quoteData);
Log::info('Quote auto-created for intervention', ['intervention_id' => $intervention->id]);
} else {
$quote = $this->quoteRepository->create($quoteData);
// 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]);
}
} catch (\Exception $e) {
}
catch (\Exception $e) {
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
}
@ -321,21 +332,21 @@ class InterventionController extends Controller
if (!empty($documents)) {
foreach ($documents as $documentData) {
if (isset($documentData['file']) && $documentData['file']->isValid()) {
// Store the file and create intervention attachment
// This is a placeholder - implement actual file upload logic
// $path = $documentData['file']->store('intervention_documents');
// Create intervention attachment record
// Store the file and create intervention attachment
// This is a placeholder - implement actual file upload logic
// $path = $documentData['file']->store('intervention_documents');
// Create intervention attachment record
}
}
}
// Return all created data
return [
'intervention' => $intervention,
'deceased' => $deceased,
'client' => $client,
'contact_id' => $contactId,
'documents_count' => count($documents)
'intervention' => $intervention,
'deceased' => $deceased,
'client' => $client,
'contact_id' => $contactId,
'documents_count' => count($documents)
];
});
@ -357,13 +368,15 @@ class InterventionController extends Controller
]
], Response::HTTP_CREATED);
} catch (\Illuminate\Validation\ValidationException $e) {
}
catch (\Illuminate\Validation\ValidationException $e) {
// Validation errors are handled by the FormRequest
return response()->json([
'message' => 'Données invalides',
'errors' => $e->errors()
], Response::HTTP_UNPROCESSABLE_ENTITY);
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error creating intervention with all data: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString(),
'input' => $request->except(['documents']) // Don't log file data
@ -385,7 +398,8 @@ class InterventionController extends Controller
$intervention = $this->interventionRepository->findById($id);
return response()->json(new InterventionResource($intervention));
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error fetching intervention details: ' . $e->getMessage());
return response()->json([
@ -408,7 +422,8 @@ class InterventionController extends Controller
$updatedIntervention = $this->interventionRepository->update($intervention, $validated);
return response()->json(new InterventionResource($updatedIntervention));
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error updating intervention: ' . $e->getMessage());
return response()->json([
@ -429,7 +444,8 @@ class InterventionController extends Controller
$this->interventionRepository->delete($intervention);
return response()->json(null, Response::HTTP_NO_CONTENT);
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error deleting intervention: ' . $e->getMessage());
return response()->json([
@ -457,7 +473,8 @@ class InterventionController extends Controller
);
return response()->json(new InterventionResource($updatedIntervention));
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error changing intervention status: ' . $e->getMessage());
return response()->json([
@ -485,15 +502,16 @@ class InterventionController extends Controller
return response()->json([
'data' => $interventions->map(function ($intervention) {
return new InterventionResource($intervention);
}),
return new InterventionResource($intervention);
}),
'meta' => [
'total' => $interventions->count(),
'year' => $validated['year'],
'month' => $validated['month'],
]
]);
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error fetching interventions by month: ' . $e->getMessage());
return response()->json([
@ -550,15 +568,16 @@ class InterventionController extends Controller
'message' => 'Assignment(s) créé(s) avec succès.',
'practitioners_count' => $practitioners->count(),
'practitioners' => $practitioners->map(function ($p) {
return [
return [
'id' => $p->id,
'full_name' => $p->full_name ?? ($p->first_name . ' ' . $p->last_name),
'role' => $p->pivot->role ?? 'unknown'
];
})->toArray()
})->toArray()
], Response::HTTP_OK);
} catch (\Exception $e) {
}
catch (\Exception $e) {
return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'assignment.',
@ -620,14 +639,15 @@ class InterventionController extends Controller
'message' => 'Praticien désassigné avec succès.',
'remaining_practitioners_count' => $remainingPractitioners->count(),
'remaining_practitioners' => $remainingPractitioners->map(function ($p) {
return [
return [
'id' => $p->id,
'employee_name' => $p->employee->full_name ?? ($p->employee->first_name . ' ' . $p->employee->last_name),
'role' => $p->pivot->role ?? 'unknown'
];
})->toArray()
})->toArray()
], Response::HTTP_OK);
} else {
}
else {
Log::warning('No practitioner assignment found to delete', [
'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId
@ -638,7 +658,8 @@ class InterventionController extends Controller
], Response::HTTP_NOT_FOUND);
}
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error unassigning practitioner from intervention: ' . $e->getMessage(), [
'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId,
@ -675,17 +696,18 @@ class InterventionController extends Controller
'database_records' => $dbPractitioners,
'eager_loaded_count' => $eagerPractitioners->count(),
'eager_loaded_data' => $eagerPractitioners->map(function ($p) {
return [
return [
'id' => $p->id,
'full_name' => $p->full_name ?? ($p->first_name . ' ' . $p->last_name),
'role' => $p->pivot->role ?? 'unknown'
];
})->toArray()
})->toArray()
]);
} catch (\Exception $e) {
}
catch (\Exception $e) {
Log::error('Error in debug practitioners: ' . $e->getMessage());
return response()->json(['error' => $e->getMessage()], 500);
}
}
}
}

View File

@ -19,6 +19,11 @@ class InterventionResource extends JsonResource
{
return [
'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 () {
return new ClientResource($this->client);
}),

View File

@ -29,7 +29,9 @@ class Intervention extends Model
'status',
'attachments_count',
'notes',
'created_by'
'created_by',
'invoice_id',
'quote_id',
];
/**
@ -79,9 +81,9 @@ class Intervention extends Model
*/
public function practitioners(): BelongsToMany
{
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->withPivot('role', 'assigned_at')
->withTimestamps();
return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->withPivot('role', 'assigned_at')
->withTimestamps();
}
/**
@ -97,10 +99,10 @@ class Intervention extends Model
*/
public function principalPractitioner(): BelongsToMany
{
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'principal')
->withPivot('role', 'assigned_at')
->withTimestamps();
return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'principal')
->withPivot('role', 'assigned_at')
->withTimestamps();
}
/**
@ -108,10 +110,10 @@ class Intervention extends Model
*/
public function assistantPractitioners(): BelongsToMany
{
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'assistant')
->withPivot('role', 'assigned_at')
->withTimestamps();
return $this->belongsToMany(Thanatopractitioner::class , 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'assistant')
->withPivot('role', 'assigned_at')
->withTimestamps();
}
/**
@ -119,7 +121,7 @@ class Intervention extends Model
*/
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()
{
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);
}
}
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')
->orderBy('changed_at', 'desc');
}
public function interventions()
{
return $this->hasMany(Intervention::class);
}
}

View File

@ -81,7 +81,8 @@ class InterventionRepository implements InterventionRepositoryInterface
'location',
'practitioners',
'attachments',
'notifications'
'notifications',
'quote'
])->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>
<!-- 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 -->
<div v-if="activeTab === 'history'" class="tab-pane fade show active">
<div class="card">

View File

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