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 @@ @@ -27,12 +31,19 @@ import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue"; import { defineProps, defineEmits } from "vue"; import { useRouter } from "vue-router"; +import { useClientStore } from "@/stores/clientStore"; +import { storeToRefs } from "pinia"; const router = useRouter(); +const clientStore = useClientStore(); +// Use storeToRefs to keep reactivity for pagination if it was passed as a prop, +// but since we are using the store directly for actions, we can also extract it here if needed. +// However, the common pattern is that the parent view passes the data. +// Let's check where clientData comes from. It comes from props. const emit = defineEmits(["pushDetails"]); -defineProps({ +const props = defineProps({ clientData: { type: Array, default: [], @@ -41,6 +52,11 @@ defineProps({ type: Boolean, default: false, }, + // We need to accept pagination as a prop if it is passed from the view + pagination: { + type: Object, + default: () => ({}) + } }); const goToClient = () => { @@ -56,4 +72,16 @@ const goToDetails = (client) => { const deleteClient = (client) => { emit("deleteClient", client); }; + +const onPageChange = (page) => { + clientStore.fetchClients({ page: page, per_page: props.pagination.per_page }); +}; + +const onPerPageChange = (perPage) => { + clientStore.fetchClients({ page: 1, per_page: perPage }); +}; + +const onSearch = (query) => { + clientStore.fetchClients({ page: 1, per_page: props.pagination.per_page, search: query }); +}; diff --git a/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue b/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue index 6a71645..a8885b5 100644 --- a/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue +++ b/thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue @@ -17,7 +17,7 @@
- +
diff --git a/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue b/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue index efa1d9d..3e58a67 100644 --- a/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue +++ b/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue @@ -24,7 +24,7 @@ + + diff --git a/thanasoft-front/src/components/molecules/client/ClientActivityTab.vue b/thanasoft-front/src/components/molecules/client/ClientActivityTab.vue index fcb0d9f..1b559e4 100644 --- a/thanasoft-front/src/components/molecules/client/ClientActivityTab.vue +++ b/thanasoft-front/src/components/molecules/client/ClientActivityTab.vue @@ -2,7 +2,7 @@
-
Activités du client
+
Activités récentes
@@ -50,264 +50,34 @@
- -
- -
-
- -
-
-
-
-
Appel téléphonique
-

- - Il y a 2 heures • Durée: 15min -

-

- Discussion concernant le nouveau projet. Le client est - satisfait de la proposition. -

-
- -
-
-
- - -
-
- -
-
-
-
-
Email envoyé
-

- - Il y a 4 heures • Sujet: Devis projet X -

-

- Envoi du devis signé et de la documentation technique. -

-
- - devis.pdf - - - specifications.docx - -
-
- -
-
-
- - -
-
- -
-
-
-
-
Nouvelle facture créée
-

- - Il y a 1 jour • Facture #FACT-2024-001 -

-

- Montant: 1 250,00 € • Échéance: 30/01/2024 -

-
- Payée - - Télécharger - -
-
- -
-
-
- - -
-
- -
-
-
-
-
Fichier ajouté
-

- - Il y a 2 jours • Contrat signé -

-

- Le client a uploadé le contrat signé pour le projet en cours. -

-
- - contrat_signé.pdf - - 2.4 MB -
-
- -
-
-
- - -
-
- -
-
-
-
-
Réunion planifiée
-

- - Il y a 3 jours • 15 Janvier 2024, 14:00 -

-

- Révision du projet avec l'équipe technique. -

-
- Teams - - Ajouter au calendrier - -
-
- -
-
+ +
+
+ Chargement...
+

Récupération des activités...

- -
- -

Aucune activité pour ce client

- + +
+ + + + + +
+ +

Aucune activité pour ce filtre

+
@@ -335,9 +105,76 @@