diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientActivityTimelineController.php b/thanasoft-back/app/Http/Controllers/Api/ClientActivityTimelineController.php
new file mode 100644
index 0000000..48fa25c
--- /dev/null
+++ b/thanasoft-back/app/Http/Controllers/Api/ClientActivityTimelineController.php
@@ -0,0 +1,40 @@
+repository = $repository;
+ }
+
+ /**
+ * Get activity timeline for a client
+ */
+ public function index(Request $request, Client $client)
+ {
+ try {
+ $perPage = (int) $request->get('per_page', 10);
+
+ $activities = $this->repository->getByClient($client->id, $perPage);
+
+ return ClientActivityTimelineResource::collection($activities);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Error fetching client timeline: ' . $e->getMessage()
+ ], 500);
+ }
+ }
+}
diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientController.php b/thanasoft-back/app/Http/Controllers/Api/ClientController.php
index 4e7986c..465043c 100644
--- a/thanasoft-back/app/Http/Controllers/Api/ClientController.php
+++ b/thanasoft-back/app/Http/Controllers/Api/ClientController.php
@@ -28,7 +28,7 @@ class ClientController extends Controller
public function index(Request $request): ClientCollection|JsonResponse
{
try {
- $perPage = $request->get('per_page', 15);
+ $perPage = (int) $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'is_active' => $request->get('is_active'),
diff --git a/thanasoft-back/app/Http/Resources/Client/ClientActivityTimelineResource.php b/thanasoft-back/app/Http/Resources/Client/ClientActivityTimelineResource.php
new file mode 100644
index 0000000..3698483
--- /dev/null
+++ b/thanasoft-back/app/Http/Resources/Client/ClientActivityTimelineResource.php
@@ -0,0 +1,66 @@
+
+ */
+ public function toArray(Request $request): array
+ {
+ return [
+ 'id' => $this->id,
+ 'client_id' => $this->client_id,
+ 'actor_type' => $this->actor_type,
+ 'actor_name' => $this->actor ? $this->actor->firstname . ' ' . $this->actor->lastname : 'System',
+ 'event_type' => $this->event_type,
+ 'entity_type' => $this->entity_type,
+ 'entity_id' => $this->entity_id,
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'metadata' => $this->metadata,
+ 'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
+ 'time_ago' => $this->created_at?->diffForHumans(),
+
+ // Helper properties for frontend display
+ 'icon' => $this->getIcon(),
+ 'color' => $this->getColor(),
+ ];
+ }
+
+ protected function getIcon()
+ {
+ // Map event types to icons (using Nucleo icons as requested)
+ return match($this->event_type) {
+ 'call' => 'mobile-button',
+ 'email_sent', 'email_received' => 'email-83',
+ 'invoice_created', 'invoice_sent', 'invoice_paid' => 'money-coins',
+ 'quote_created', 'quote_sent' => 'single-copy-04',
+ 'file_uploaded', 'attachment_sent', 'attachment_received' => 'cloud-upload-96',
+ 'client_created' => 'badge-24',
+ 'meeting', 'intervention_created' => 'laptop',
+ default => 'bell-55'
+ };
+ }
+
+ protected function getColor()
+ {
+ // Map event types to colors
+ return match($this->event_type) {
+ 'client_created' => 'success',
+ 'call' => 'info',
+ 'email_sent' => 'success',
+ 'invoice_paid' => 'success',
+ 'invoice_created' => 'warning',
+ 'quote_created' => 'primary',
+ 'quote_sent' => 'primary',
+ default => 'dark'
+ };
+ }
+}
diff --git a/thanasoft-back/app/Models/Client.php b/thanasoft-back/app/Models/Client.php
index 36ef240..218d6c2 100644
--- a/thanasoft-back/app/Models/Client.php
+++ b/thanasoft-back/app/Models/Client.php
@@ -4,9 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
class Client extends Model
{
+ use HasFactory;
protected $fillable = [
'name',
'vat_number',
diff --git a/thanasoft-back/app/Models/ClientActivityTimeline.php b/thanasoft-back/app/Models/ClientActivityTimeline.php
new file mode 100644
index 0000000..d38fbb3
--- /dev/null
+++ b/thanasoft-back/app/Models/ClientActivityTimeline.php
@@ -0,0 +1,47 @@
+ 'array',
+ 'created_at' => 'datetime',
+ ];
+
+ public function client()
+ {
+ return $this->belongsTo(Client::class);
+ }
+
+ public function actor()
+ {
+ return $this->belongsTo(User::class, 'actor_user_id');
+ }
+
+ // Polymorphic relation to entity if we had models for all of them
+ // public function entity()
+ // {
+ // return $this->morphTo();
+ // }
+}
diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php
index c61700f..6d1c9b4 100644
--- a/thanasoft-back/app/Providers/AppServiceProvider.php
+++ b/thanasoft-back/app/Providers/AppServiceProvider.php
@@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Schema;
class AppServiceProvider extends ServiceProvider
{
@@ -13,7 +14,10 @@ class AppServiceProvider extends ServiceProvider
{
// Repository interface to implementation bindings
$this->app->bind(\App\Repositories\ClientRepositoryInterface::class, function ($app) {
- return new \App\Repositories\ClientRepository($app->make(\App\Models\Client::class));
+ return new \App\Repositories\ClientRepository(
+ $app->make(\App\Models\Client::class),
+ $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class)
+ );
});
$this->app->bind(\App\Repositories\ClientGroupRepositoryInterface::class, function ($app) {
@@ -67,14 +71,30 @@ class AppServiceProvider extends ServiceProvider
$this->app->bind(\App\Repositories\ProductCategoryRepositoryInterface::class, \App\Repositories\ProductCategoryRepository::class);
- $this->app->bind(\App\Repositories\InvoiceRepositoryInterface::class, \App\Repositories\InvoiceRepository::class);
+ $this->app->bind(\App\Repositories\InvoiceRepositoryInterface::class, function ($app) {
+ return new \App\Repositories\InvoiceRepository(
+ $app->make(\App\Models\Invoice::class),
+ $app->make(\App\Repositories\InvoiceLineRepositoryInterface::class),
+ $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class)
+ );
+ });
$this->app->bind(\App\Repositories\InvoiceLineRepositoryInterface::class, \App\Repositories\InvoiceLineRepository::class);
- $this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, \App\Repositories\QuoteRepository::class);
+ $this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, function ($app) {
+ return new \App\Repositories\QuoteRepository(
+ $app->make(\App\Models\Quote::class),
+ $app->make(\App\Repositories\QuoteLineRepositoryInterface::class),
+ $app->make(\App\Repositories\InvoiceRepositoryInterface::class),
+ $app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class)
+ );
+ });
$this->app->bind(\App\Repositories\QuoteLineRepositoryInterface::class, \App\Repositories\QuoteLineRepository::class);
-
+
+ $this->app->bind(\App\Repositories\QuoteLineRepositoryInterface::class, \App\Repositories\QuoteLineRepository::class);
+
+ $this->app->bind(\App\Repositories\ClientActivityTimelineRepositoryInterface::class, \App\Repositories\ClientActivityTimelineRepository::class);
}
@@ -84,6 +104,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
- //
+ Schema::defaultStringLength(191);
}
}
diff --git a/thanasoft-back/app/Repositories/ClientActivityTimelineRepository.php b/thanasoft-back/app/Repositories/ClientActivityTimelineRepository.php
new file mode 100644
index 0000000..5e99b42
--- /dev/null
+++ b/thanasoft-back/app/Repositories/ClientActivityTimelineRepository.php
@@ -0,0 +1,43 @@
+model
+ ->where('client_id', $clientId)
+ ->with('actor') // Load actor relationship
+ ->orderBy('created_at', 'desc')
+ ->paginate($perPage);
+ }
+
+ /**
+ * Log a new activity
+ *
+ * @param array $data
+ * @return ClientActivityTimeline
+ */
+ public function logActivity(array $data)
+ {
+ return $this->create($data);
+ }
+}
diff --git a/thanasoft-back/app/Repositories/ClientActivityTimelineRepositoryInterface.php b/thanasoft-back/app/Repositories/ClientActivityTimelineRepositoryInterface.php
new file mode 100644
index 0000000..0e060f8
--- /dev/null
+++ b/thanasoft-back/app/Repositories/ClientActivityTimelineRepositoryInterface.php
@@ -0,0 +1,27 @@
+timelineRepository->logActivity([
+ 'client_id' => $client->id,
+ 'actor_type' => 'user',
+ 'actor_user_id' => auth()->id(),
+ 'event_type' => 'client_created',
+ 'entity_type' => 'client',
+ 'entity_id' => $client->id,
+ 'title' => 'Nouveau client créé',
+ 'description' => "Le client {$client->name} a été ajouté au système.",
+ 'created_at' => now(),
+ ]);
+ } catch (\Exception $e) {
+ LaravelLog::error("Failed to log client creation activity: " . $e->getMessage());
+ }
+
+ return $client;
+ }
+
/**
* Get paginated clients
diff --git a/thanasoft-back/app/Repositories/InterventionRepository.php b/thanasoft-back/app/Repositories/InterventionRepository.php
index af88573..60a3f17 100644
--- a/thanasoft-back/app/Repositories/InterventionRepository.php
+++ b/thanasoft-back/app/Repositories/InterventionRepository.php
@@ -6,8 +6,14 @@ use App\Models\Intervention;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
class InterventionRepository implements InterventionRepositoryInterface
{
+ public function __construct(
+ protected readonly ClientActivityTimelineRepositoryInterface $timelineRepository
+ ) {}
+
/**
* Get all interventions with optional filtering and pagination
*
@@ -88,7 +94,25 @@ class InterventionRepository implements InterventionRepositoryInterface
public function create(array $data): Intervention
{
return DB::transaction(function () use ($data) {
- return Intervention::create($data);
+ $intervention = Intervention::create($data);
+
+ try {
+ $this->timelineRepository->logActivity([
+ 'client_id' => $intervention->client_id,
+ 'actor_type' => 'user',
+ 'actor_user_id' => auth()->id(),
+ 'event_type' => 'intervention_created',
+ 'entity_type' => 'intervention',
+ 'entity_id' => $intervention->id,
+ 'title' => 'Nouvelle intervention créée',
+ 'description' => "Une intervention de type '{$intervention->type}' a été créée.",
+ 'created_at' => now(),
+ ]);
+ } catch (\Exception $e) {
+ Log::error("Failed to log intervention creation activity: " . $e->getMessage());
+ }
+
+ return $intervention;
});
}
diff --git a/thanasoft-back/app/Repositories/InvoiceRepository.php b/thanasoft-back/app/Repositories/InvoiceRepository.php
index 96e7282..b6c1539 100644
--- a/thanasoft-back/app/Repositories/InvoiceRepository.php
+++ b/thanasoft-back/app/Repositories/InvoiceRepository.php
@@ -9,11 +9,14 @@ use App\Models\Quote;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
+use App\Repositories\ClientActivityTimelineRepositoryInterface;
+
class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInterface
{
public function __construct(
Invoice $model,
- protected InvoiceLineRepositoryInterface $invoiceLineRepository
+ protected InvoiceLineRepositoryInterface $invoiceLineRepository,
+ protected readonly ClientActivityTimelineRepositoryInterface $timelineRepository
) {
parent::__construct($model);
}
@@ -64,6 +67,22 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
// Record history
$this->recordHistory($invoice->id, null, 'brouillon', 'Created from Quote ' . $quote->reference);
+ try {
+ $this->timelineRepository->logActivity([
+ 'client_id' => $invoice->client_id,
+ 'actor_type' => 'user',
+ 'actor_user_id' => auth()->id(),
+ 'event_type' => 'invoice_created',
+ 'entity_type' => 'invoice',
+ 'entity_id' => $invoice->id,
+ 'title' => 'Nouvelle facture créée',
+ 'description' => "Une facture a été créée à partir du devis #{$quote->id}.",
+ 'created_at' => now(),
+ ]);
+ } catch (\Exception $e) {
+ Log::error("Failed to log invoice creation activity: " . $e->getMessage());
+ }
+
return $invoice;
});
}
@@ -93,6 +112,22 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
// Record initial status history
$this->recordHistory($invoice->id, null, $invoice->status, 'Invoice created');
+ try {
+ $this->timelineRepository->logActivity([
+ 'client_id' => $invoice->client_id,
+ 'actor_type' => 'user',
+ 'actor_user_id' => auth()->id(),
+ 'event_type' => 'invoice_created',
+ 'entity_type' => 'invoice',
+ 'entity_id' => $invoice->id,
+ 'title' => 'Nouvelle facture créée',
+ 'description' => "La facture #{$invoice->id} a été créée.",
+ 'created_at' => now(),
+ ]);
+ } catch (\Exception $e) {
+ Log::error("Failed to log manual invoice creation activity: " . $e->getMessage());
+ }
+
return $invoice;
} catch (\Exception $e) {
Log::error('Error creating invoice with lines: ' . $e->getMessage(), [
diff --git a/thanasoft-back/app/Repositories/QuoteRepository.php b/thanasoft-back/app/Repositories/QuoteRepository.php
index eba3c02..ea50c01 100644
--- a/thanasoft-back/app/Repositories/QuoteRepository.php
+++ b/thanasoft-back/app/Repositories/QuoteRepository.php
@@ -8,12 +8,15 @@ use App\Models\Quote;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
+use App\Repositories\ClientActivityTimelineRepositoryInterface;
+
class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface
{
public function __construct(
Quote $model,
protected QuoteLineRepositoryInterface $quoteLineRepository,
- protected InvoiceRepositoryInterface $invoiceRepository
+ protected InvoiceRepositoryInterface $invoiceRepository,
+ protected readonly ClientActivityTimelineRepositoryInterface $timelineRepository
) {
parent::__construct($model);
}
@@ -42,6 +45,22 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface
// Record initial status history
$this->recordHistory($quote->id, null, $quote->status, 'Quote created');
+ try {
+ $this->timelineRepository->logActivity([
+ 'client_id' => $quote->client_id,
+ 'actor_type' => 'user',
+ 'actor_user_id' => auth()->id(),
+ 'event_type' => 'quote_created',
+ 'entity_type' => 'quote',
+ 'entity_id' => $quote->id,
+ 'title' => 'Nouveau devis créé',
+ 'description' => "Le devis #{$quote->id} a été créé.",
+ 'created_at' => now(),
+ ]);
+ } catch (\Exception $e) {
+ Log::error("Failed to log quote creation activity: " . $e->getMessage());
+ }
+
return $quote;
} catch (\Exception $e) {
Log::error('Error creating quote with lines: ' . $e->getMessage(), [
diff --git a/thanasoft-back/database/factories/ClientFactory.php b/thanasoft-back/database/factories/ClientFactory.php
new file mode 100644
index 0000000..d100d39
--- /dev/null
+++ b/thanasoft-back/database/factories/ClientFactory.php
@@ -0,0 +1,31 @@
+ $this->faker->company(),
+ 'vat_number' => 'FR' . $this->faker->numerify('###########'),
+ 'siret' => $this->faker->numerify('##############'),
+ 'email' => $this->faker->unique()->companyEmail(),
+ 'phone' => $this->faker->phoneNumber(),
+ 'billing_address_line1' => $this->faker->streetAddress(),
+ 'billing_city' => $this->faker->city(),
+ 'billing_postal_code' => $this->faker->postcode(),
+ 'billing_country_code' => 'FR',
+ // Assumes categories 1-5 exist from migration
+ 'client_category_id' => $this->faker->numberBetween(1, 5),
+ 'is_active' => true,
+ 'is_parent' => false,
+ 'notes' => $this->faker->optional()->sentence(),
+ ];
+ }
+}
diff --git a/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php b/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php
index 2fe3707..2affb13 100644
--- a/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php
+++ b/thanasoft-back/database/migrations/2025_10_09_065542_create_table_client_categories.php
@@ -15,7 +15,7 @@ return new class extends Migration
Schema::create('client_categories', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
- $table->string('slug', 255)->unique();
+ $table->string('slug', 191)->unique();
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->integer('sort_order')->default(0);
diff --git a/thanasoft-back/database/migrations/2026_01_12_124826_create_client_activity_timelines_table.php b/thanasoft-back/database/migrations/2026_01_12_124826_create_client_activity_timelines_table.php
new file mode 100644
index 0000000..c4d4f6e
--- /dev/null
+++ b/thanasoft-back/database/migrations/2026_01_12_124826_create_client_activity_timelines_table.php
@@ -0,0 +1,78 @@
+id();
+ $table->unsignedBigInteger('client_id');
+ $table->enum('actor_type', ['user', 'system', 'api'])->default('user');
+ $table->unsignedBigInteger('actor_user_id')->nullable();
+
+ $table->enum('event_type', [
+ 'quote_created',
+ 'quote_sent',
+ 'quote_status_changed',
+ 'invoice_created',
+ 'invoice_sent',
+ 'invoice_paid',
+ 'intervention_created',
+ 'intervention_scheduled',
+ 'intervention_completed',
+ 'call',
+ 'email_sent',
+ 'email_received',
+ 'attachment_sent',
+ 'attachment_received',
+ 'client_created',
+ 'client_info_updated',
+ 'note'
+ ]);
+
+ $table->enum('entity_type', [
+ 'quote',
+ 'invoice',
+ 'intervention',
+ 'email',
+ 'file',
+ 'client',
+ 'payment'
+ ])->nullable();
+
+ $table->unsignedBigInteger('entity_id')->nullable();
+ $table->string('title');
+ $table->text('description')->nullable();
+ $table->json('metadata')->nullable();
+ $table->dateTime('created_at')->useCurrent();
+
+ // Indexes
+ $table->index(['entity_type', 'entity_id'], 'idx_cat_entity');
+ $table->index(['event_type'], 'idx_cat_event');
+
+ // Foreign Keys
+ $table->foreign('client_id', 'fk_cat_client')
+ ->references('id')->on('clients')
+ ->onDelete('cascade');
+
+ $table->foreign('actor_user_id', 'fk_cat_user')
+ ->references('id')->on('users')
+ ->onDelete('set null');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('client_activity_timeline');
+ }
+};
diff --git a/thanasoft-back/database/seeders/DatabaseSeeder.php b/thanasoft-back/database/seeders/DatabaseSeeder.php
index fe09534..136cf75 100644
--- a/thanasoft-back/database/seeders/DatabaseSeeder.php
+++ b/thanasoft-back/database/seeders/DatabaseSeeder.php
@@ -16,8 +16,9 @@ class DatabaseSeeder extends Seeder
// User::factory(10)->create();
User::factory()->create([
- 'name' => 'Test User',
- 'email' => 'test@example.com',
+ 'name' => 'Admin User',
+ 'email' => 'admin@admin.com',
+ 'password' => 'password',
]);
$this->call(ProductCategorySeeder::class);
diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php
index 9064fff..174a371 100644
--- a/thanasoft-back/routes/api.php
+++ b/thanasoft-back/routes/api.php
@@ -19,6 +19,7 @@ use App\Http\Controllers\Api\InterventionController;
use App\Http\Controllers\Api\FileController;
use App\Http\Controllers\Api\FileAttachmentController;
use App\Http\Controllers\Api\QuoteController;
+use App\Http\Controllers\Api\ClientActivityTimelineController;
/*
@@ -62,6 +63,7 @@ Route::middleware('auth:sanctum')->group(function () {
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']);
+ Route::get('clients/{client}/timeline', [ClientActivityTimelineController::class, 'index']);
// Contact management
Route::apiResource('contacts', ContactController::class);
diff --git a/thanasoft-back/routes/web.php b/thanasoft-back/routes/web.php
index 46c2d68..999b0f0 100644
--- a/thanasoft-back/routes/web.php
+++ b/thanasoft-back/routes/web.php
@@ -3,5 +3,9 @@
use Illuminate\Support\Facades\Route;
Route::get('/{any}', function () {
- return file_get_contents(public_path('build/index.html'));
+ $path = public_path('build/index.html');
+ if (!file_exists($path)) {
+ return response('Frontend build not found. Please run "npm run build" in the thanasoft-front directory.', 404);
+ }
+ return file_get_contents($path);
})->where('any', '.*');
diff --git a/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue
index 596e018..d7b0ef8 100644
--- a/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue
+++ b/thanasoft-front/src/components/Organism/CRM/ClientPresentation.vue
@@ -13,8 +13,12 @@
- - Il y a 2 heures • Durée: 15min -
-- Discussion concernant le nouveau projet. Le client est - satisfait de la proposition. -
-- - Il y a 4 heures • Sujet: Devis projet X -
-- Envoi du devis signé et de la documentation technique. -
-- - Il y a 1 jour • Facture #FACT-2024-001 -
-- Montant: 1 250,00 € • Échéance: 30/01/2024 -
-- - Il y a 2 jours • Contrat signé -
-- Le client a uploadé le contrat signé pour le projet en cours. -
-- - Il y a 3 jours • 15 Janvier 2024, 14:00 -
-- Révision du projet avec l'équipe technique. -
-Récupération des activités...
Aucune activité pour ce client
- + +Aucune activité pour ce filtre
+