Link internvetion and invoice and quote
This commit is contained in:
parent
8ee7d8f8e9
commit
ebd171e9de
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -70,4 +70,9 @@ class Quote extends Model
|
||||
->where('document_type', 'quote')
|
||||
->orderBy('changed_at', 'desc');
|
||||
}
|
||||
|
||||
public function interventions()
|
||||
{
|
||||
return $this->hasMany(Intervention::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +81,8 @@ class InterventionRepository implements InterventionRepositoryInterface
|
||||
'location',
|
||||
'practitioners',
|
||||
'attachments',
|
||||
'notifications'
|
||||
'notifications',
|
||||
'quote'
|
||||
])->findOrFail($id);
|
||||
}
|
||||
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user