feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12, Vue 3, Inertia.js 2, and Tailwind CSS 4. Story 0.1 (rename folders to declarations in database) is implemented and code-reviewed: migration, rollback, and 6 Pest tests all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BIN
database/database.sqlite
Normal file
BIN
database/database.sqlite
Normal file
Binary file not shown.
37
database/factories/ClientContactFactory.php
Normal file
37
database/factories/ClientContactFactory.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Client;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ClientContact>
|
||||
*/
|
||||
class ClientContactFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => Client::factory(),
|
||||
'full_name' => fake()->name(),
|
||||
'job_title' => fake()->jobTitle(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'phone' => fake()->phoneNumber(),
|
||||
'is_principal' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the contact as principal.
|
||||
*/
|
||||
public function principal(): static
|
||||
{
|
||||
return $this->state(fn () => ['is_principal' => true]);
|
||||
}
|
||||
}
|
||||
63
database/factories/ClientFactory.php
Normal file
63
database/factories/ClientFactory.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ClientStatus;
|
||||
use App\Enums\LegalForm;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Client>
|
||||
*/
|
||||
class ClientFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'workspace_id' => Workspace::factory(),
|
||||
'company_name' => fake()->company(),
|
||||
'legal_form' => fake()->randomElement(LegalForm::getValues()),
|
||||
'ice' => (string) fake()->numerify('##########'),
|
||||
'fiscal_id' => (string) fake()->numerify('##########'),
|
||||
'rc' => (string) fake()->numerify('######'),
|
||||
'cnss' => (string) fake()->numerify('##########'),
|
||||
'patente' => (string) fake()->numerify('########'),
|
||||
'contact_last_name' => fake()->lastName(),
|
||||
'contact_first_name' => fake()->firstName(),
|
||||
'contact_job_title' => fake()->jobTitle(),
|
||||
'contact_email' => fake()->unique()->safeEmail(),
|
||||
'contact_phone' => fake()->phoneNumber(),
|
||||
'internal_responsible_id' => null,
|
||||
'status' => ClientStatus::Active,
|
||||
'internal_notes' => fake()->optional(0.3)->sentence(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the model factory.
|
||||
*/
|
||||
public function configure(): static
|
||||
{
|
||||
return $this->afterCreating(function ($client) {
|
||||
ClientContact::factory()->principal()->create([
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the client with an internal responsible user.
|
||||
*/
|
||||
public function withInternalResponsible(User $user): static
|
||||
{
|
||||
return $this->state(fn () => ['internal_responsible_id' => $user->id]);
|
||||
}
|
||||
}
|
||||
78
database/factories/FolderFactory.php
Normal file
78
database/factories/FolderFactory.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use App\Models\Client;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Folder>
|
||||
*/
|
||||
class FolderFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$workspace = Workspace::factory()->create();
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$year = fake()->numberBetween(2024, 2026);
|
||||
$excludeOldVat = array_filter(FolderType::getValues(), fn ($v) => $v !== 'vat');
|
||||
$type = fake()->randomElement(array_values($excludeOldVat));
|
||||
|
||||
$isVatMonthly = $type === 'vat_monthly';
|
||||
$isVatQuarterly = $type === 'vat_quarterly';
|
||||
|
||||
return [
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'created_by' => null,
|
||||
'title' => 'Déclaration '.$this->typeLabel($type).' - '.$year,
|
||||
'type' => $type,
|
||||
'period_year' => $year,
|
||||
'period_month' => $isVatMonthly ? fake()->numberBetween(1, 12) : null,
|
||||
'period_quarter' => $isVatQuarterly ? fake()->numberBetween(1, 4) : null,
|
||||
'due_date' => fake()->dateTimeBetween('now', '+3 months'),
|
||||
'status' => fake()->randomElement(FolderStatus::getValues()),
|
||||
'priority' => fake()->randomElement(FolderPriority::getValues()),
|
||||
'assigned_to' => null,
|
||||
'validated_at' => null,
|
||||
'closed_at' => null,
|
||||
'notes_internal' => fake()->optional(0.3)->sentence(),
|
||||
'notes_client' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the model factory to use existing workspace and client.
|
||||
*/
|
||||
public function forWorkspace(Workspace $workspace): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => Client::factory()->create(['workspace_id' => $workspace->id])->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function typeLabel(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'vat' => 'TVA',
|
||||
'vat_monthly' => 'TVA mensuelle',
|
||||
'vat_quarterly' => 'TVA trimestrielle',
|
||||
'corporate_tax' => 'IS',
|
||||
'income_tax' => 'IR',
|
||||
'cnss' => 'CNSS',
|
||||
'annual_balance' => 'Bilan',
|
||||
default => 'Autre',
|
||||
};
|
||||
}
|
||||
}
|
||||
81
database/factories/UserFactory.php
Normal file
81
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\UserGroup;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'group' => UserGroup::User,
|
||||
'remember_token' => Str::random(10),
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the user has superadmin privileges.
|
||||
*/
|
||||
public function superadmin(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'group' => UserGroup::Superadmin,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the user has admin privileges.
|
||||
*/
|
||||
public function admin(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'group' => UserGroup::Admin,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has two-factor authentication configured.
|
||||
*/
|
||||
public function withTwoFactor(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'two_factor_secret' => encrypt('secret'),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
28
database/factories/WorkspaceFactory.php
Normal file
28
database/factories/WorkspaceFactory.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Workspace>
|
||||
*/
|
||||
class WorkspaceFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$name = fake()->company();
|
||||
$slug = Str::slug($name);
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
];
|
||||
}
|
||||
}
|
||||
52
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
52
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\UserGroup;
|
||||
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::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('group')->default(UserGroup::User);
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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('users', function (Blueprint $table) {
|
||||
$table->text('two_factor_secret')->after('password')->nullable();
|
||||
$table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable();
|
||||
$table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_confirmed_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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::create('workspaces', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('workspaces');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
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::create('workspace_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('role')->default(WorkspaceUserRole::Member);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['workspace_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('workspace_user');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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::create('clients', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('company_name');
|
||||
$table->string('legal_form');
|
||||
$table->string('ice')->nullable();
|
||||
$table->string('fiscal_id')->nullable();
|
||||
$table->string('rc')->nullable();
|
||||
$table->string('cnss')->nullable();
|
||||
$table->string('patente')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('clients');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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('clients', function (Blueprint $table) {
|
||||
$table->string('contact_last_name')->nullable()->after('patente');
|
||||
$table->string('contact_first_name')->nullable()->after('contact_last_name');
|
||||
$table->string('contact_job_title')->nullable()->after('contact_first_name');
|
||||
$table->string('contact_email')->nullable()->after('contact_job_title');
|
||||
$table->string('contact_phone')->nullable()->after('contact_email');
|
||||
$table->foreignId('internal_responsible_id')->nullable()->after('contact_phone')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->string('status')->nullable()->after('internal_responsible_id');
|
||||
$table->text('internal_notes')->nullable()->after('status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('clients', function (Blueprint $table) {
|
||||
$table->dropForeign(['internal_responsible_id']);
|
||||
$table->dropColumn([
|
||||
'contact_last_name',
|
||||
'contact_first_name',
|
||||
'contact_job_title',
|
||||
'contact_email',
|
||||
'contact_phone',
|
||||
'internal_responsible_id',
|
||||
'status',
|
||||
'internal_notes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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::create('folders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('client_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
$table->string('title');
|
||||
$table->string('type');
|
||||
$table->unsignedSmallInteger('period_year')->nullable();
|
||||
$table->unsignedTinyInteger('period_month')->nullable();
|
||||
$table->unsignedTinyInteger('period_quarter')->nullable();
|
||||
|
||||
$table->date('due_date')->nullable();
|
||||
$table->string('status');
|
||||
$table->string('priority')->nullable();
|
||||
|
||||
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('validated_at')->nullable();
|
||||
$table->timestamp('closed_at')->nullable();
|
||||
|
||||
$table->text('notes_internal')->nullable();
|
||||
$table->text('notes_client')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('folders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateActivityLogTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('log_name')->nullable();
|
||||
$table->text('description');
|
||||
$table->nullableMorphs('subject', 'subject');
|
||||
$table->nullableMorphs('causer', 'causer');
|
||||
$table->json('properties')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index('log_name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddEventColumnToActivityLogTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->string('event')->nullable()->after('subject_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->dropColumn('event');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddBatchUuidColumnToActivityLogTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->uuid('batch_uuid')->nullable()->after('properties');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
|
||||
$table->dropColumn('batch_uuid');
|
||||
});
|
||||
}
|
||||
}
|
||||
32
database/migrations/2026_03_01_143910_create_media_table.php
Normal file
32
database/migrations/2026_03_01_143910_create_media_table.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('media', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->morphs('model');
|
||||
$table->uuid()->nullable()->unique();
|
||||
$table->string('collection_name');
|
||||
$table->string('name');
|
||||
$table->string('file_name');
|
||||
$table->string('mime_type')->nullable();
|
||||
$table->string('disk');
|
||||
$table->string('conversions_disk')->nullable();
|
||||
$table->unsignedBigInteger('size');
|
||||
$table->json('manipulations');
|
||||
$table->json('custom_properties');
|
||||
$table->json('generated_conversions');
|
||||
$table->json('responsive_images');
|
||||
$table->unsignedInteger('order_column')->nullable()->index();
|
||||
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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::create('messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('folder_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('type'); // invite, situation, file_request, confirmation, text
|
||||
$table->text('body');
|
||||
$table->string('sent_by_type'); // user, client
|
||||
$table->unsignedBigInteger('sent_by_id');
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['folder_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('messages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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::create('folder_invitations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('folder_id')->constrained()->cascadeOnDelete();
|
||||
$table->uuid('token')->unique();
|
||||
$table->string('email')->nullable();
|
||||
$table->timestamp('expires_at');
|
||||
$table->timestamp('used_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['token', 'expires_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('folder_invitations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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('folders', function (Blueprint $table) {
|
||||
$table->timestamp('confirmation_requested_at')->nullable()->after('closed_at');
|
||||
$table->unsignedBigInteger('confirmation_media_id')->nullable()->after('confirmation_requested_at');
|
||||
$table->string('confirmed_by_type')->nullable()->after('confirmation_media_id');
|
||||
$table->unsignedBigInteger('confirmed_by_id')->nullable()->after('confirmed_by_type');
|
||||
$table->text('confirmation_signature')->nullable()->after('confirmed_by_id');
|
||||
$table->timestamp('refused_at')->nullable()->after('confirmation_signature');
|
||||
$table->text('refusal_reason')->nullable()->after('refused_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('folders', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'confirmation_requested_at',
|
||||
'confirmation_media_id',
|
||||
'confirmed_by_type',
|
||||
'confirmed_by_id',
|
||||
'confirmation_signature',
|
||||
'refused_at',
|
||||
'refusal_reason',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('client_contacts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('client_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('full_name');
|
||||
$table->string('job_title')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->boolean('is_principal')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['client_id', 'is_principal']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('client_contacts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$clients = DB::table('clients')
|
||||
->where(function ($q) {
|
||||
$q->whereNotNull('contact_first_name')
|
||||
->orWhereNotNull('contact_last_name')
|
||||
->orWhereNotNull('contact_email')
|
||||
->orWhereNotNull('contact_phone')
|
||||
->orWhereNotNull('contact_job_title');
|
||||
})
|
||||
->get();
|
||||
|
||||
$now = now();
|
||||
|
||||
foreach ($clients as $client) {
|
||||
$fullName = trim(implode(' ', array_filter([
|
||||
$client->contact_first_name,
|
||||
$client->contact_last_name,
|
||||
])));
|
||||
|
||||
DB::table('client_contacts')->insert([
|
||||
'client_id' => $client->id,
|
||||
'full_name' => $fullName !== '' ? $fullName : '(sans nom)',
|
||||
'job_title' => $client->contact_job_title,
|
||||
'email' => $client->contact_email,
|
||||
'phone' => $client->contact_phone,
|
||||
'is_principal' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info("Migrated {$clients->count()} client contacts to client_contacts table.");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('client_contacts')->truncate();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Log rows with both period_month AND period_quarter set
|
||||
$ambiguousRows = DB::table('folders')
|
||||
->where('type', 'vat')
|
||||
->whereNotNull('period_month')
|
||||
->whereNotNull('period_quarter')
|
||||
->pluck('id');
|
||||
|
||||
if ($ambiguousRows->isNotEmpty()) {
|
||||
Log::warning('VAT folders with both period_month AND period_quarter set (migrated as vat_monthly, quarter nulled): '.$ambiguousRows->implode(', '));
|
||||
}
|
||||
|
||||
// Monthly: has month, no quarter
|
||||
DB::table('folders')
|
||||
->where('type', 'vat')
|
||||
->whereNotNull('period_month')
|
||||
->whereNull('period_quarter')
|
||||
->update(['type' => 'vat_monthly']);
|
||||
|
||||
// Quarterly: has quarter, no month
|
||||
DB::table('folders')
|
||||
->where('type', 'vat')
|
||||
->whereNotNull('period_quarter')
|
||||
->whereNull('period_month')
|
||||
->update(['type' => 'vat_quarterly']);
|
||||
|
||||
// Both set: treat as monthly, null quarter
|
||||
DB::table('folders')
|
||||
->where('type', 'vat')
|
||||
->whereNotNull('period_month')
|
||||
->whereNotNull('period_quarter')
|
||||
->update(['type' => 'vat_monthly', 'period_quarter' => null]);
|
||||
|
||||
// Neither set: fallback to monthly
|
||||
DB::table('folders')
|
||||
->where('type', 'vat')
|
||||
->whereNull('period_month')
|
||||
->whereNull('period_quarter')
|
||||
->update(['type' => 'vat_monthly']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('folders')
|
||||
->whereIn('type', ['vat_monthly', 'vat_quarterly'])
|
||||
->update(['type' => 'vat']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
$table->morphs('notifiable');
|
||||
$table->text('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('media_downloads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('media_id')->constrained('media')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestamp('downloaded_at');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
$table->unique(['media_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('media_downloads');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
// Depends on: split_vat_folder_type (2026_03_09_000003) which references DB::table('folders').
|
||||
// This migration MUST run after that one — timestamp ordering guarantees this.
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. Drop FK constraints (use column array for SQLite compatibility)
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->dropForeign(['folder_id']);
|
||||
});
|
||||
Schema::table('folder_invitations', function (Blueprint $table) {
|
||||
$table->dropForeign(['folder_id']);
|
||||
});
|
||||
|
||||
// 2. Drop old index (must happen before column rename for cross-driver safety)
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->dropIndex(['folder_id', 'created_at']);
|
||||
});
|
||||
|
||||
// 3. Rename columns
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->renameColumn('folder_id', 'declaration_id');
|
||||
});
|
||||
Schema::table('folder_invitations', function (Blueprint $table) {
|
||||
$table->renameColumn('folder_id', 'declaration_id');
|
||||
});
|
||||
|
||||
// 4. Create new index with renamed column
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->index(['declaration_id', 'created_at']);
|
||||
});
|
||||
|
||||
// 5-6. Rename tables
|
||||
Schema::rename('folders', 'declarations');
|
||||
Schema::rename('folder_invitations', 'declaration_invitations');
|
||||
|
||||
// 7. Re-add FK constraints
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->foreign('declaration_id')->references('id')->on('declarations')->cascadeOnDelete();
|
||||
});
|
||||
Schema::table('declaration_invitations', function (Blueprint $table) {
|
||||
$table->foreign('declaration_id')->references('id')->on('declarations')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// 1. Drop FK constraints (use column array for SQLite compatibility)
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->dropForeign(['declaration_id']);
|
||||
});
|
||||
Schema::table('declaration_invitations', function (Blueprint $table) {
|
||||
$table->dropForeign(['declaration_id']);
|
||||
});
|
||||
|
||||
// 2-3. Rename tables back
|
||||
Schema::rename('declaration_invitations', 'folder_invitations');
|
||||
Schema::rename('declarations', 'folders');
|
||||
|
||||
// 4. Drop new index
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->dropIndex(['declaration_id', 'created_at']);
|
||||
});
|
||||
|
||||
// 5. Rename columns back
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->renameColumn('declaration_id', 'folder_id');
|
||||
});
|
||||
Schema::table('folder_invitations', function (Blueprint $table) {
|
||||
$table->renameColumn('declaration_id', 'folder_id');
|
||||
});
|
||||
|
||||
// 6. Recreate old index
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->index(['folder_id', 'created_at']);
|
||||
});
|
||||
|
||||
// 7. Re-add FK constraints
|
||||
Schema::table('messages', function (Blueprint $table) {
|
||||
$table->foreign('folder_id')->references('id')->on('folders')->cascadeOnDelete();
|
||||
});
|
||||
Schema::table('folder_invitations', function (Blueprint $table) {
|
||||
$table->foreign('folder_id')->references('id')->on('folders')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
200
database/seeders/DatabaseSeeder.php
Normal file
200
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\ClientStatus;
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use App\Enums\LegalForm;
|
||||
use App\Enums\UserGroup;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Folder;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database with realistic Moroccan data.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// --- Users (firm employees) ---
|
||||
$admin = User::factory()->create([
|
||||
'name' => 'Saad El Amrani',
|
||||
'email' => 'saad@amifiduciaire.ma',
|
||||
'password' => bcrypt('password'),
|
||||
'group' => UserGroup::Superadmin,
|
||||
]);
|
||||
|
||||
$fatima = User::factory()->create([
|
||||
'name' => 'Fatima Bennis',
|
||||
'email' => 'fatima@amifiduciaire.ma',
|
||||
'password' => bcrypt('password'),
|
||||
'group' => UserGroup::Admin,
|
||||
]);
|
||||
|
||||
$youssef = User::factory()->create([
|
||||
'name' => 'Youssef Tazi',
|
||||
'email' => 'youssef@amifiduciaire.ma',
|
||||
'password' => bcrypt('password'),
|
||||
'group' => UserGroup::User,
|
||||
]);
|
||||
|
||||
$khadija = User::factory()->create([
|
||||
'name' => 'Khadija Alaoui',
|
||||
'email' => 'khadija@amifiduciaire.ma',
|
||||
'password' => bcrypt('password'),
|
||||
'group' => UserGroup::User,
|
||||
]);
|
||||
|
||||
$omar = User::factory()->create([
|
||||
'name' => 'Omar Benjelloun',
|
||||
'email' => 'omar@amifiduciaire.ma',
|
||||
'password' => bcrypt('password'),
|
||||
'group' => UserGroup::User,
|
||||
]);
|
||||
|
||||
$users = [$admin, $fatima, $youssef, $khadija, $omar];
|
||||
|
||||
// --- Workspaces ---
|
||||
$wsCasa = Workspace::create(['name' => "L'Ami Fiduciaire — Casablanca", 'slug' => 'ami-casa']);
|
||||
$wsRabat = Workspace::create(['name' => "L'Ami Fiduciaire — Rabat", 'slug' => 'ami-rabat']);
|
||||
|
||||
// Attach users to workspaces
|
||||
$wsCasa->users()->attach($admin->id, ['role' => WorkspaceUserRole::Owner]);
|
||||
$wsCasa->users()->attach($fatima->id, ['role' => WorkspaceUserRole::Manager]);
|
||||
$wsCasa->users()->attach($youssef->id, ['role' => WorkspaceUserRole::Member]);
|
||||
$wsCasa->users()->attach($khadija->id, ['role' => WorkspaceUserRole::Member]);
|
||||
|
||||
$wsRabat->users()->attach($admin->id, ['role' => WorkspaceUserRole::Owner]);
|
||||
$wsRabat->users()->attach($omar->id, ['role' => WorkspaceUserRole::Manager]);
|
||||
|
||||
// --- Moroccan clients for Casablanca ---
|
||||
$casaClients = [
|
||||
['company_name' => 'Atlas Import-Export SARL', 'legal_form' => LegalForm::SARL, 'ice' => '001234567000012', 'fiscal_id' => '12345678', 'rc' => '123456', 'cnss' => '1234567890', 'patente' => '12345678', 'contact_last_name' => 'Bennani', 'contact_first_name' => 'Ahmed', 'contact_job_title' => 'Gérant', 'contact_email' => 'a.bennani@atlasimport.ma', 'contact_phone' => '+212 522 123456'],
|
||||
['company_name' => 'Maghreb Textile SA', 'legal_form' => LegalForm::SA, 'ice' => '001234567000028', 'fiscal_id' => '23456789', 'rc' => '234567', 'cnss' => '2345678901', 'patente' => '23456789', 'contact_last_name' => 'Chraibi', 'contact_first_name' => 'Nadia', 'contact_job_title' => 'Directrice Générale', 'contact_email' => 'n.chraibi@maghrebtextile.ma', 'contact_phone' => '+212 522 234567'],
|
||||
['company_name' => 'Dar Al Maalem Construction', 'legal_form' => LegalForm::SARL, 'ice' => '001234567000034', 'fiscal_id' => '34567890', 'rc' => '345678', 'cnss' => '3456789012', 'patente' => '34567890', 'contact_last_name' => 'Fassi Fihri', 'contact_first_name' => 'Karim', 'contact_job_title' => 'Directeur Technique', 'contact_email' => 'k.fassifihri@daralmaalem.ma', 'contact_phone' => '+212 522 345678'],
|
||||
['company_name' => 'Souss Agro-Alimentaire', 'legal_form' => LegalForm::SA, 'ice' => '001234567000040', 'fiscal_id' => '45678901', 'rc' => '456789', 'cnss' => '4567890123', 'patente' => '45678901', 'contact_last_name' => 'Ouazzani', 'contact_first_name' => 'Rachid', 'contact_job_title' => 'PDG', 'contact_email' => 'r.ouazzani@soussagro.ma', 'contact_phone' => '+212 528 456789'],
|
||||
['company_name' => 'Marrakech Digital Solutions', 'legal_form' => LegalForm::SARL, 'ice' => '001234567000056', 'fiscal_id' => '56789012', 'rc' => '567890', 'cnss' => '5678901234', 'patente' => '56789012', 'contact_last_name' => 'El Idrissi', 'contact_first_name' => 'Salma', 'contact_job_title' => 'Gérante', 'contact_email' => 's.elidrissi@mkdigital.ma', 'contact_phone' => '+212 524 567890'],
|
||||
['company_name' => 'Boulangerie Fès El Bali', 'legal_form' => LegalForm::AutoEntrepreneur, 'ice' => '001234567000062', 'fiscal_id' => '67890123', 'rc' => '678901', 'cnss' => '6789012345', 'patente' => '67890123', 'contact_last_name' => 'Amrani', 'contact_first_name' => 'Hassan', 'contact_job_title' => 'Propriétaire', 'contact_email' => 'h.amrani@boulangeriefes.ma', 'contact_phone' => '+212 535 678901'],
|
||||
['company_name' => 'Pharmacie Anfa', 'legal_form' => LegalForm::SEL, 'ice' => '001234567000078', 'fiscal_id' => '78901234', 'rc' => '789012', 'cnss' => '7890123456', 'patente' => '78901234', 'contact_last_name' => 'Berrada', 'contact_first_name' => 'Laila', 'contact_job_title' => 'Pharmacienne', 'contact_email' => 'l.berrada@pharmacieanfa.ma', 'contact_phone' => '+212 522 789012'],
|
||||
['company_name' => 'Transport Rif Express', 'legal_form' => LegalForm::SARL, 'ice' => '001234567000084', 'fiscal_id' => '89012345', 'rc' => '890123', 'cnss' => '8901234567', 'patente' => '89012345', 'contact_last_name' => 'Kettani', 'contact_first_name' => 'Mehdi', 'contact_job_title' => 'Gérant', 'contact_email' => 'm.kettani@rifexpress.ma', 'contact_phone' => '+212 539 890123'],
|
||||
['company_name' => 'Cabinet Juridique El Mansour', 'legal_form' => LegalForm::SNC, 'ice' => '001234567000090', 'fiscal_id' => '90123456', 'rc' => '901234', 'cnss' => '9012345678', 'patente' => '90123456', 'contact_last_name' => 'El Mansouri', 'contact_first_name' => 'Zineb', 'contact_job_title' => 'Avocate Associée', 'contact_email' => 'z.elmansouri@cabinetmansour.ma', 'contact_phone' => '+212 522 901234'],
|
||||
['company_name' => 'Hôtel Riad Zitoun', 'legal_form' => LegalForm::SARL, 'ice' => '001234567000103', 'fiscal_id' => '01234567', 'rc' => '012345', 'cnss' => '0123456789', 'patente' => '01234567', 'contact_last_name' => 'Skalli', 'contact_first_name' => 'Younes', 'contact_job_title' => 'Directeur', 'contact_email' => 'y.skalli@riadzitoun.ma', 'contact_phone' => '+212 524 012345'],
|
||||
];
|
||||
|
||||
$responsibles = [$youssef, $khadija, $fatima];
|
||||
$createdCasaClients = [];
|
||||
|
||||
foreach ($casaClients as $i => $data) {
|
||||
$data['workspace_id'] = $wsCasa->id;
|
||||
$data['internal_responsible_id'] = $responsibles[$i % count($responsibles)]->id;
|
||||
$data['status'] = $i < 8 ? ClientStatus::Active : ClientStatus::Inactive;
|
||||
|
||||
$client = Client::create($data);
|
||||
ClientContact::create([
|
||||
'client_id' => $client->id,
|
||||
'full_name' => $data['contact_first_name'] . ' ' . $data['contact_last_name'],
|
||||
'job_title' => $data['contact_job_title'],
|
||||
'email' => $data['contact_email'],
|
||||
'phone' => $data['contact_phone'],
|
||||
'is_principal' => true,
|
||||
]);
|
||||
$createdCasaClients[] = $client;
|
||||
}
|
||||
|
||||
// --- Moroccan clients for Rabat ---
|
||||
$rabatClients = [
|
||||
['company_name' => 'Bureau d\'Études Oualili', 'legal_form' => LegalForm::SARL, 'ice' => '002345678000011', 'fiscal_id' => '11223344', 'rc' => '112233', 'cnss' => '1122334455', 'patente' => '11223344', 'contact_last_name' => 'Meknassi', 'contact_first_name' => 'Amine', 'contact_job_title' => 'Ingénieur Principal', 'contact_email' => 'a.meknassi@oualili.ma', 'contact_phone' => '+212 537 112233'],
|
||||
['company_name' => 'Imprimerie Chellah', 'legal_form' => LegalForm::EURL, 'ice' => '002345678000027', 'fiscal_id' => '22334455', 'rc' => '223344', 'cnss' => '2233445566', 'patente' => '22334455', 'contact_last_name' => 'Zniber', 'contact_first_name' => 'Samira', 'contact_job_title' => 'Directrice', 'contact_email' => 's.zniber@imprimeriechellah.ma', 'contact_phone' => '+212 537 223344'],
|
||||
['company_name' => 'Café Oudaya SARL', 'legal_form' => LegalForm::SARL, 'ice' => '002345678000033', 'fiscal_id' => '33445566', 'rc' => '334455', 'cnss' => '3344556677', 'patente' => '33445566', 'contact_last_name' => 'Belhaj', 'contact_first_name' => 'Mouad', 'contact_job_title' => 'Gérant', 'contact_email' => 'm.belhaj@cafeoudaya.ma', 'contact_phone' => '+212 537 334455'],
|
||||
['company_name' => 'Clinique Agdal Santé', 'legal_form' => LegalForm::SA, 'ice' => '002345678000049', 'fiscal_id' => '44556677', 'rc' => '445566', 'cnss' => '4455667788', 'patente' => '44556677', 'contact_last_name' => 'Guessous', 'contact_first_name' => 'Hind', 'contact_job_title' => 'Administratrice', 'contact_email' => 'h.guessous@cliniqueagdal.ma', 'contact_phone' => '+212 537 445566'],
|
||||
['company_name' => 'Pâtisserie Yasmina', 'legal_form' => LegalForm::EntrepriseIndividuelle, 'ice' => '002345678000055', 'fiscal_id' => '55667788', 'rc' => '556677', 'cnss' => '5566778899', 'patente' => '55667788', 'contact_last_name' => 'Tahiri', 'contact_first_name' => 'Yasmina', 'contact_job_title' => 'Propriétaire', 'contact_email' => 'y.tahiri@patisserieyasmina.ma', 'contact_phone' => '+212 537 556677'],
|
||||
];
|
||||
|
||||
foreach ($rabatClients as $data) {
|
||||
$data['workspace_id'] = $wsRabat->id;
|
||||
$data['internal_responsible_id'] = $omar->id;
|
||||
$data['status'] = ClientStatus::Active;
|
||||
|
||||
$client = Client::create($data);
|
||||
ClientContact::create([
|
||||
'client_id' => $client->id,
|
||||
'full_name' => $data['contact_first_name'] . ' ' . $data['contact_last_name'],
|
||||
'job_title' => $data['contact_job_title'],
|
||||
'email' => $data['contact_email'],
|
||||
'phone' => $data['contact_phone'],
|
||||
'is_principal' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// --- Folders (dossiers) for Casablanca clients ---
|
||||
$folderTypes = [
|
||||
['type' => FolderType::VatMonthly, 'label' => 'TVA mensuelle'],
|
||||
['type' => FolderType::VatQuarterly, 'label' => 'TVA trimestrielle'],
|
||||
['type' => FolderType::CorporateTax, 'label' => 'IS'],
|
||||
['type' => FolderType::IncomeTax, 'label' => 'IR'],
|
||||
['type' => FolderType::CNSS, 'label' => 'CNSS'],
|
||||
['type' => FolderType::AnnualBalance, 'label' => 'Bilan annuel'],
|
||||
];
|
||||
|
||||
$statuses = [
|
||||
FolderStatus::Draft,
|
||||
FolderStatus::WaitingDocuments,
|
||||
FolderStatus::DocumentsReceived,
|
||||
FolderStatus::Processing,
|
||||
FolderStatus::WaitingClientValidation,
|
||||
FolderStatus::Validated,
|
||||
FolderStatus::Closed,
|
||||
];
|
||||
|
||||
$priorities = [FolderPriority::Low, FolderPriority::Medium, FolderPriority::High];
|
||||
|
||||
$folderIndex = 0;
|
||||
foreach ($createdCasaClients as $client) {
|
||||
// Each client gets 2-4 folders
|
||||
$numFolders = fake()->numberBetween(2, 4);
|
||||
$selectedTypes = fake()->randomElements($folderTypes, $numFolders);
|
||||
|
||||
foreach ($selectedTypes as $ft) {
|
||||
$year = fake()->randomElement([2025, 2026]);
|
||||
$status = $statuses[$folderIndex % count($statuses)];
|
||||
$isVatMonthly = $ft['type'] === FolderType::VatMonthly;
|
||||
$isVatQuarterly = $ft['type'] === FolderType::VatQuarterly;
|
||||
$month = $isVatMonthly ? fake()->numberBetween(1, 3) : null;
|
||||
$quarter = $isVatQuarterly ? fake()->numberBetween(1, 4) : null;
|
||||
|
||||
$periodSuffix = $isVatMonthly ? " — Mois $month" : ($isVatQuarterly ? " — T$quarter" : '');
|
||||
|
||||
Folder::create([
|
||||
'workspace_id' => $wsCasa->id,
|
||||
'client_id' => $client->id,
|
||||
'created_by' => $responsibles[$folderIndex % count($responsibles)]->id,
|
||||
'title' => "Déclaration {$ft['label']} $year{$periodSuffix}",
|
||||
'type' => $ft['type'],
|
||||
'period_year' => $year,
|
||||
'period_month' => $month,
|
||||
'period_quarter' => $quarter,
|
||||
'due_date' => fake()->dateTimeBetween('2026-01-01', '2026-06-30'),
|
||||
'status' => $status,
|
||||
'priority' => $priorities[$folderIndex % count($priorities)],
|
||||
'assigned_to' => $responsibles[$folderIndex % count($responsibles)]->id,
|
||||
'notes_internal' => fake()->optional(0.4)->randomElement([
|
||||
'En attente des relevés bancaires.',
|
||||
'Client à relancer pour les factures manquantes.',
|
||||
'Dossier prioritaire — échéance proche.',
|
||||
'Documents reçus partiellement.',
|
||||
'Vérification en cours avec la DGI.',
|
||||
]),
|
||||
]);
|
||||
|
||||
$folderIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user