diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientController.php b/thanasoft-back/app/Http/Controllers/Api/ClientController.php
index 807165a..4e7986c 100644
--- a/thanasoft-back/app/Http/Controllers/Api/ClientController.php
+++ b/thanasoft-back/app/Http/Controllers/Api/ClientController.php
@@ -207,4 +207,112 @@ class ClientController extends Controller
], 500);
}
}
+ /**
+ * Change client status (active/inactive).
+ */
+ public function changeStatus(Request $request, string $id): ClientResource|JsonResponse
+ {
+ try {
+ $isActive = $request->input('is_active');
+ $updated = $this->clientRepository->update($id, ['is_active' => $isActive]);
+
+ if (!$updated) {
+ return response()->json([
+ 'message' => 'Client non trouvé ou échec de la mise à jour.',
+ ], 404);
+ }
+
+ $client = $this->clientRepository->find($id);
+ return new ClientResource($client);
+ } catch (\Exception $e) {
+ Log::error('Error changing client status: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'client_id' => $id,
+ ]);
+ return response()->json(['message' => 'Erreur serveur'], 500);
+ }
+ }
+
+ /**
+ * Get children clients.
+ */
+ public function getChildren(string $id): AnonymousResourceCollection|JsonResponse
+ {
+ try {
+ $client = $this->clientRepository->find($id);
+ if (!$client) {
+ return response()->json(['message' => 'Client not found'], 404);
+ }
+
+ // Assuming the relationship is defined in the model as 'children'
+ $children = $client->children;
+ return ClientResource::collection($children);
+
+ } catch (\Exception $e) {
+ Log::error('Error fetching children: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'client_id' => $id,
+ ]);
+ return response()->json(['message' => 'Erreur serveur'], 500);
+ }
+ }
+
+ /**
+ * Add a child client.
+ */
+ public function addChild(string $id, string $childId): JsonResponse
+ {
+ try {
+ $parent = $this->clientRepository->find($id);
+ $child = $this->clientRepository->find($childId);
+
+ if (!$parent || !$child) {
+ return response()->json(['message' => 'Parent or Child not found'], 404);
+ }
+
+ // Update child's parent_id
+ $this->clientRepository->update($childId, ['parent_id' => $id]);
+
+ return response()->json(['message' => 'Child added successfully'], 200);
+
+ } catch (\Exception $e) {
+ Log::error('Error adding child: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'parent_id' => $id,
+ 'child_id' => $childId
+ ]);
+ return response()->json(['message' => 'Erreur serveur'], 500);
+ }
+ }
+
+ /**
+ * Remove a child client.
+ */
+ public function removeChild(string $id, string $childId): JsonResponse
+ {
+ try {
+ $child = $this->clientRepository->find($childId);
+
+ if (!$child) {
+ return response()->json(['message' => 'Child not found'], 404);
+ }
+
+ if ($child->parent_id != $id) {
+ return response()->json(['message' => 'Client is not a child of this parent'], 400);
+ }
+
+ // Remove parent_id
+ $this->clientRepository->update($childId, ['parent_id' => null]);
+
+ return response()->json(['message' => 'Child removed successfully'], 200);
+
+ } catch (\Exception $e) {
+ Log::error('Error removing child: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'parent_id' => $id,
+ 'child_id' => $childId
+ ]);
+ return response()->json(['message' => 'Erreur serveur'], 500);
+ }
+ }
}
diff --git a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php
index 1eb5fa7..fb64272 100644
--- a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php
+++ b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php
@@ -13,6 +13,8 @@ use App\Repositories\InterventionPractitionerRepositoryInterface;
use App\Repositories\ClientRepositoryInterface;
use App\Repositories\ContactRepositoryInterface;
use App\Repositories\DeceasedRepositoryInterface;
+use App\Repositories\QuoteRepositoryInterface;
+use App\Repositories\ProductRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -46,6 +48,16 @@ class InterventionController extends Controller
*/
protected $deceasedRepository;
+ /**
+ * @var QuoteRepositoryInterface
+ */
+ protected $quoteRepository;
+
+ /**
+ * @var ProductRepositoryInterface
+ */
+ protected $productRepository;
+
/**
* InterventionController constructor.
*
@@ -54,19 +66,25 @@ class InterventionController extends Controller
* @param ClientRepositoryInterface $clientRepository
* @param ContactRepositoryInterface $contactRepository
* @param DeceasedRepositoryInterface $deceasedRepository
+ * @param QuoteRepositoryInterface $quoteRepository
+ * @param ProductRepositoryInterface $productRepository
*/
public function __construct(
InterventionRepositoryInterface $interventionRepository,
InterventionPractitionerRepositoryInterface $interventionPractitionerRepository,
ClientRepositoryInterface $clientRepository,
ContactRepositoryInterface $contactRepository,
- DeceasedRepositoryInterface $deceasedRepository
+ DeceasedRepositoryInterface $deceasedRepository,
+ QuoteRepositoryInterface $quoteRepository,
+ ProductRepositoryInterface $productRepository
) {
$this->interventionRepository = $interventionRepository;
$this->interventionPractitionerRepository = $interventionPractitionerRepository;
$this->clientRepository = $clientRepository;
$this->contactRepository = $contactRepository;
$this->deceasedRepository = $deceasedRepository;
+ $this->quoteRepository = $quoteRepository;
+ $this->productRepository = $productRepository;
}
/**
@@ -182,6 +200,56 @@ class InterventionController extends Controller
]);
$intervention = $this->interventionRepository->create($interventionData);
+ // Step 5b: Create a Quote for this intervention
+ try {
+ $interventionProduct = $this->productRepository->findInterventionProduct();
+
+ if ($interventionProduct) {
+ // Calculate totals
+ $quantity = 1;
+ // Ideally fetch TVA rate from product, default to 20% if not set
+ // Assuming product has tva_rate relationship or simple logic
+ $tvaRateValue = 20;
+
+ $unitPrice = $interventionProduct->prix_unitaire;
+ $totalHt = $unitPrice * $quantity;
+ $totalTva = $totalHt * ($tvaRateValue / 100);
+ $totalTtc = $totalHt + $totalTva;
+
+ $quoteData = [
+ 'client_id' => $client->id,
+ 'status' => 'brouillon',
+ 'quote_date' => now()->toDateString(),
+ 'currency' => 'EUR',
+ 'valid_until' => now()->addDays(30)->toDateString(),
+ 'total_ht' => $totalHt,
+ 'total_tva' => $totalTva,
+ 'total_ttc' => $totalTtc,
+ 'lines' => [
+ [
+ 'product_id' => $interventionProduct->id,
+ 'description' => 'Intervention: ' . ($intervention->type ?? 'Standard'),
+ 'units_qty' => $quantity,
+ 'unit_price' => $unitPrice,
+ 'discount_pct' => 0,
+ 'total_ht' => $totalHt,
+ // 'tva_rate_id' => ... if needed
+ ]
+ ]
+ ];
+
+ $this->quoteRepository->create($quoteData);
+ Log::info('Quote auto-created for intervention', ['intervention_id' => $intervention->id]);
+ } else {
+ Log::warning('No intervention product found, skipping auto-quote creation', ['intervention_id' => $intervention->id]);
+ }
+
+ } 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
+ }
+
+
// Step 6: Handle document uploads (if any)
$documents = $validated['documents'] ?? [];
if (!empty($documents)) {
diff --git a/thanasoft-back/app/Http/Requests/StoreClientRequest.php b/thanasoft-back/app/Http/Requests/StoreClientRequest.php
index 618388b..31ec8f4 100644
--- a/thanasoft-back/app/Http/Requests/StoreClientRequest.php
+++ b/thanasoft-back/app/Http/Requests/StoreClientRequest.php
@@ -28,7 +28,7 @@ class StoreClientRequest extends FormRequest
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
- 'billing_address_line1' => 'nullable|string|max:255',
+ 'billing_address_line1' => 'required|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
@@ -36,6 +36,8 @@ class StoreClientRequest extends FormRequest
'group_id' => 'nullable|exists:client_groups,id',
'notes' => 'nullable|string',
'is_active' => 'boolean',
+ 'is_parent' => 'boolean|nullable',
+ 'parent_id' => 'nullable|exists:clients,id',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
@@ -56,6 +58,7 @@ class StoreClientRequest extends FormRequest
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
+ 'billing_address_line1.required' => 'L\'adresse facturation est obligatoire.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
diff --git a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php
index f44cead..ff77fc7 100644
--- a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php
+++ b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php
@@ -36,6 +36,8 @@ class UpdateClientRequest extends FormRequest
'group_id' => 'nullable|exists:client_groups,id',
'notes' => 'nullable|string',
'is_active' => 'boolean',
+ 'is_parent' => 'boolean|nullable',
+ 'parent_id' => 'nullable|exists:clients,id',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
diff --git a/thanasoft-back/app/Http/Resources/Client/ClientResource.php b/thanasoft-back/app/Http/Resources/Client/ClientResource.php
index a0b904f..5cb6095 100644
--- a/thanasoft-back/app/Http/Resources/Client/ClientResource.php
+++ b/thanasoft-back/app/Http/Resources/Client/ClientResource.php
@@ -36,6 +36,8 @@ class ClientResource extends JsonResource
'group_id' => $this->group_id,
'notes' => $this->notes,
'is_active' => $this->is_active,
+ 'is_parent' => $this->is_parent,
+ 'parent_id' => $this->parent_id,
// 'default_tva_rate_id' => $this->default_tva_rate_id,
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
@@ -50,6 +52,7 @@ class ClientResource extends JsonResource
// Relations
// 'company' => new CompanyResource($this->whenLoaded('company')),
'group' => new ClientGroupResource($this->whenLoaded('group')),
+ 'parent' => new ClientResource($this->whenLoaded('parent')),
// 'default_tva_rate' => new TvaRateResource($this->whenLoaded('defaultTvaRate')),
'contacts' => ContactResource::collection($this->whenLoaded('contacts')),
'locations' => ClientLocationResource::collection($this->whenLoaded('locations')),
diff --git a/thanasoft-back/app/Models/Client.php b/thanasoft-back/app/Models/Client.php
index 1d01112..36ef240 100644
--- a/thanasoft-back/app/Models/Client.php
+++ b/thanasoft-back/app/Models/Client.php
@@ -21,6 +21,8 @@ class Client extends Model
'group_id',
'notes',
'is_active',
+ 'is_parent',
+ 'parent_id',
// 'default_tva_rate_id',
'client_category_id',
'user_id',
@@ -28,8 +30,19 @@ class Client extends Model
protected $casts = [
'is_active' => 'boolean',
+ 'is_parent' => 'boolean',
];
+ public function parent(): BelongsTo
+ {
+ return $this->belongsTo(Client::class, 'parent_id');
+ }
+
+ public function children()
+ {
+ return $this->hasMany(Client::class, 'parent_id');
+ }
+
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
diff --git a/thanasoft-back/app/Repositories/ProductRepository.php b/thanasoft-back/app/Repositories/ProductRepository.php
index 1a07c12..53fc3b5 100644
--- a/thanasoft-back/app/Repositories/ProductRepository.php
+++ b/thanasoft-back/app/Repositories/ProductRepository.php
@@ -140,4 +140,15 @@ class ProductRepository extends BaseRepository implements ProductRepositoryInter
'total_value' => $totalValue,
];
}
+ /**
+ * Find a default intervention product (where category has intervention=true)
+ */
+ public function findInterventionProduct(): ?Product
+ {
+ return $this->model->newQuery()
+ ->whereHas('category', function ($query) {
+ $query->where('intervention', true);
+ })
+ ->first();
+ }
}
diff --git a/thanasoft-back/app/Repositories/ProductRepositoryInterface.php b/thanasoft-back/app/Repositories/ProductRepositoryInterface.php
index 0f8b152..961e047 100644
--- a/thanasoft-back/app/Repositories/ProductRepositoryInterface.php
+++ b/thanasoft-back/app/Repositories/ProductRepositoryInterface.php
@@ -17,4 +17,6 @@ interface ProductRepositoryInterface extends BaseRepositoryInterface
public function getByCategory(int $categoryId, int $perPage = 15): LengthAwarePaginator;
public function getProductsByFournisseur(int $fournisseurId): LengthAwarePaginator;
+
+ public function findInterventionProduct(): ?\App\Models\Product;
}
diff --git a/thanasoft-back/app/Repositories/QuoteRepository.php b/thanasoft-back/app/Repositories/QuoteRepository.php
index fa23a43..91633b4 100644
--- a/thanasoft-back/app/Repositories/QuoteRepository.php
+++ b/thanasoft-back/app/Repositories/QuoteRepository.php
@@ -18,6 +18,11 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface
}
+ public function all(array $columns = ['*']): \Illuminate\Support\Collection
+ {
+ return $this->model->with(['client', 'lines.product'])->get($columns);
+ }
+
public function create(array $data): Quote
{
return DB::transaction(function () use ($data) {
diff --git a/thanasoft-back/database/migrations/2026_01_08_115945_add_parent_fields_to_clients_table.php b/thanasoft-back/database/migrations/2026_01_08_115945_add_parent_fields_to_clients_table.php
new file mode 100644
index 0000000..bc20a3a
--- /dev/null
+++ b/thanasoft-back/database/migrations/2026_01_08_115945_add_parent_fields_to_clients_table.php
@@ -0,0 +1,30 @@
+boolean('is_parent')->nullable()->default(false);
+ $table->foreignId('parent_id')->nullable()->constrained('clients')->onDelete('set null');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('clients', function (Blueprint $table) {
+ $table->dropForeign(['parent_id']);
+ $table->dropColumn(['is_parent', 'parent_id']);
+ });
+ }
+};
diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php
index 09c7428..7960317 100644
--- a/thanasoft-back/routes/api.php
+++ b/thanasoft-back/routes/api.php
@@ -53,9 +53,16 @@ Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('clients', ClientController::class);
Route::apiResource('client-groups', ClientGroupController::class);
+ Route::apiResource('client-locations', ClientLocationController::class);
Route::apiResource('client-locations', ClientLocationController::class);
Route::get('clients/{clientId}/locations', [ClientLocationController::class, 'getLocationsByClient']);
+ // Client Parent/Child routes
+ Route::get('clients/{id}/children', [ClientController::class, 'getChildren']);
+ Route::post('clients/{id}/children/{childId}', [ClientController::class, 'addChild']);
+ Route::delete('clients/{id}/children/{childId}', [ClientController::class, 'removeChild']);
+ Route::patch('clients/{id}/status', [ClientController::class, 'changeStatus']);
+
// Contact management
Route::apiResource('contacts', ContactController::class);
Route::get('clients/{clientId}/contacts', [ContactController::class, 'getContactsByClient']);
diff --git a/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue
index de43c67..22eb71d 100644
--- a/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue
+++ b/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue
@@ -25,10 +25,12 @@
:client-type="client.type_label || 'Client'"
:contacts-count="contacts.length"
:locations-count="locations.length"
+ :children-count="children ? children.length : 0"
:is-active="client.is_active"
+ :is-parent="client.is_parent"
:active-tab="activeTab"
@edit-avatar="triggerFileInput"
- @change-tab="activeTab = $event"
+ @change-tab="$emit('change-tab', $event)"
/>
@@ -50,7 +52,7 @@
:client-id="client.id"
:contact-is-loading="contactLoading"
:location-is-loading="locationLoading"
- @change-tab="activeTab = $event"
+ @change-tab="$emit('change-tab', $event)"
@updating-client="handleUpdateClient"
@create-contact="handleAddContact"
@updating-contact="handleModifiedContact"
@@ -83,6 +85,11 @@ const props = defineProps({
required: false,
default: () => [],
},
+ children: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
isLoading: {
type: Boolean,
default: false,
@@ -118,7 +125,9 @@ const emit = defineEmits([
"updating-contact",
"add-new-location",
"modify-location",
+ "modify-location",
"remove-location",
+ "change-tab",
]);
const handleAvatarUpload = (event) => {
diff --git a/thanasoft-front/src/components/Organism/CRM/client/AddChildClientModal.vue b/thanasoft-front/src/components/Organism/CRM/client/AddChildClientModal.vue
new file mode 100644
index 0000000..8d6ebc3
--- /dev/null
+++ b/thanasoft-front/src/components/Organism/CRM/client/AddChildClientModal.vue
@@ -0,0 +1,110 @@
+
+
+ Ce client n'est pas défini comme compte parent. Activez l'option ci-dessus pour gérer des sous-comptes.
+ Aucun sous-compte associé.Gestion des sous-comptes
+
+
+ {{ child.name }}
+ {{ child.email }}
+