updates to UI and add archiving option

This commit is contained in:
Simon Pocrnjič
2025-10-05 19:45:49 +02:00
parent fe91c7e4bc
commit bab9d6561f
50 changed files with 3337 additions and 416 deletions
@@ -0,0 +1,43 @@
<?php
namespace Database\Factories;
use App\Models\ArchiveSetting;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<ArchiveSetting>
*/
class ArchiveSettingFactory extends Factory
{
protected $model = ArchiveSetting::class;
public function definition(): array
{
return [
'action_id' => null,
'decision_id' => null,
'segment_id' => null,
'entities' => [
[
'table' => 'documents',
'conditions' => [
'older_than_days' => $this->faker->numberBetween(30, 365),
],
'columns' => ['id', 'deleted_at'],
],
],
'name' => $this->faker->sentence(3),
'description' => $this->faker->optional()->paragraph(),
'enabled' => true,
'strategy' => 'immediate',
'soft' => true,
'options' => [
'batch_size' => $this->faker->numberBetween(50, 500),
],
'created_by' => User::query()->inRandomOrder()->value('id'),
'updated_by' => null,
];
}
}
@@ -0,0 +1,61 @@
<?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('archive_settings', function (Blueprint $table): void {
$table->id();
// Contextual foreign keys (nullable allows broader global rules)
$table->foreignId('action_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('decision_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('segment_id')->nullable()->constrained()->nullOnDelete();
// JSON describing entities (tables/models) impacted
// Example shape: [{"table":"documents","conditions":{"older_than_days":180},"columns":["id","deleted_at"]}]
$table->json('entities');
// Optional descriptive metadata
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->boolean('enabled')->default(true);
// Execution strategy: immediate | scheduled | queued
$table->string('strategy')->default('immediate');
// Whether to perform a soft archive (flag / soft delete) instead of permanent removal
$table->boolean('soft')->default(true);
// Additional arbitrary options (thresholds, flags, custom logic parameters)
$table->json('options')->nullable();
// Auditing foreign keys for who created / last updated the rule
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
// Useful indexes
$table->index(['action_id', 'decision_id', 'segment_id']);
$table->index('enabled');
$table->index('strategy');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('archive_settings');
}
};
@@ -0,0 +1,33 @@
<?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('archive_entities', function (Blueprint $table): void {
$table->id();
$table->string('focus')->unique(); // e.g. contracts, client_cases
$table->json('related'); // JSON array of related table names
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->boolean('enabled')->default(true);
$table->timestamps();
$table->index('enabled');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('archive_entities');
}
};
@@ -0,0 +1,63 @@
<?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
{
// Add active column to bookings if missing
if (Schema::hasTable('bookings') && ! Schema::hasColumn('bookings', 'active')) {
Schema::table('bookings', function (Blueprint $table): void {
$table->unsignedTinyInteger('active')->default(1)->after('description');
$table->index('active');
});
}
// Add active column to activities if missing
if (Schema::hasTable('activities') && ! Schema::hasColumn('activities', 'active')) {
Schema::table('activities', function (Blueprint $table): void {
$table->unsignedTinyInteger('active')->default(1)->after('note');
$table->index('active');
});
}
// Add active column to documents if missing
if (Schema::hasTable('documents') && ! Schema::hasColumn('documents', 'active')) {
Schema::table('documents', function (Blueprint $table): void {
$table->unsignedTinyInteger('active')->default(1)->after('is_public');
$table->index('active');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('bookings') && Schema::hasColumn('bookings', 'active')) {
Schema::table('bookings', function (Blueprint $table): void {
$table->dropIndex(['active']);
$table->dropColumn('active');
});
}
if (Schema::hasTable('activities') && Schema::hasColumn('activities', 'active')) {
Schema::table('activities', function (Blueprint $table): void {
$table->dropIndex(['active']);
$table->dropColumn('active');
});
}
if (Schema::hasTable('documents') && Schema::hasColumn('documents', 'active')) {
Schema::table('documents', function (Blueprint $table): void {
$table->dropIndex(['active']);
$table->dropColumn('active');
});
}
}
};
@@ -0,0 +1,28 @@
<?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
{
if (Schema::hasTable('payments') && ! Schema::hasColumn('payments', 'active')) {
Schema::table('payments', function (Blueprint $table): void {
$table->unsignedTinyInteger('active')->default(1)->after('type_id');
$table->index('active');
});
}
}
public function down(): void
{
if (Schema::hasTable('payments') && Schema::hasColumn('payments', 'active')) {
Schema::table('payments', function (Blueprint $table): void {
$table->dropIndex(['active']);
$table->dropColumn('active');
});
}
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('archive_runs', function (Blueprint $table): void {
$table->id();
$table->foreignId('archive_setting_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('status', 30)->default('running'); // running|success|error
$table->json('counts')->nullable(); // per-table affected counts
$table->json('context')->nullable(); // manual context scope
$table->timestamp('started_at')->nullable();
$table->timestamp('finished_at')->nullable();
$table->unsignedInteger('duration_ms')->nullable();
$table->text('message')->nullable();
$table->timestamps();
$table->index(['archive_setting_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('archive_runs');
}
};
@@ -0,0 +1,28 @@
<?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
{
if (Schema::hasTable('archive_settings') && ! Schema::hasColumn('archive_settings', 'reactivate')) {
Schema::table('archive_settings', function (Blueprint $table): void {
$table->boolean('reactivate')->default(false)->after('soft');
$table->index('reactivate');
});
}
}
public function down(): void
{
if (Schema::hasTable('archive_settings') && Schema::hasColumn('archive_settings', 'reactivate')) {
Schema::table('archive_settings', function (Blueprint $table): void {
$table->dropIndex(['reactivate']);
$table->dropColumn('reactivate');
});
}
}
};
@@ -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
{
public function up(): void
{
Schema::table('import_templates', function (Blueprint $table) {
if (! Schema::hasColumn('import_templates', 'reactivate')) {
$table->boolean('reactivate')->default(false)->after('is_active');
}
});
Schema::table('imports', function (Blueprint $table) {
if (! Schema::hasColumn('imports', 'reactivate')) {
$table->boolean('reactivate')->default(false)->after('status');
}
});
}
public function down(): void
{
Schema::table('import_templates', function (Blueprint $table) {
if (Schema::hasColumn('import_templates', 'reactivate')) {
$table->dropColumn('reactivate');
}
});
Schema::table('imports', function (Blueprint $table) {
if (Schema::hasColumn('imports', 'reactivate')) {
$table->dropColumn('reactivate');
}
});
}
};
@@ -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
{
public function up(): void
{
Schema::table('accounts', function (Blueprint $table) {
// Drop existing unique index if present (contract_id, reference, deleted_at)
try {
$table->dropUnique('accounts_reference_unique');
} catch (\Throwable $e) {
// ignore if it does not exist
}
// Recreate including active flag so archived/inactive accounts can reuse reference
$table->unique(['contract_id', 'reference', 'active', 'deleted_at'], 'accounts_reference_unique');
});
}
public function down(): void
{
Schema::table('accounts', function (Blueprint $table) {
try {
$table->dropUnique('accounts_reference_unique');
} catch (\Throwable $e) {
// ignore
}
// Restore previous definition without active
$table->unique(['contract_id', 'reference', 'deleted_at'], 'accounts_reference_unique');
});
}
};
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Database\Seeders;
use App\Models\ArchiveEntity;
use Illuminate\Database\Seeder;
class ArchiveEntitySeeder extends Seeder
{
/**
* Seed archive focus entities and their selectable related tables.
*/
public function run(): void
{
$entities = [
[
'focus' => 'contracts',
'name' => 'Contracts',
'description' => 'Contracts and their financial / activity related records.',
'related' => [
// Direct related tables
'accounts',
'activities',
'documents', // polymorphic (contract documents only when used as focus)
// Chained relations (dot notation) resolve via contract -> account -> payments/bookings
'account.payments',
'account.bookings',
],
],
[
'focus' => 'client_cases',
'name' => 'Client Cases',
'description' => 'Client cases and subordinate contractual / financial records.',
'related' => [
'contracts', // direct contracts under case
'contracts.account', // via contracts (hasOne account)
'activities', // case level activities (and possibly contract-linked)
'documents', // case level documents
// Chained relations:
'contracts.account.payments', // contracts -> account -> payments
'contracts.account.bookings', // contracts -> account -> bookings
'contracts.documents', // contracts -> documents (polymorphic)
],
],
];
foreach ($entities as $data) {
ArchiveEntity::query()->updateOrCreate(
['focus' => $data['focus']],
[
'name' => $data['name'],
'description' => $data['description'],
'related' => $data['related'],
'enabled' => true,
]
);
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Database\Seeders;
use App\Models\ArchiveSetting;
use Illuminate\Database\Seeder;
class ArchiveSettingSeeder extends Seeder
{
public function run(): void
{
if (ArchiveSetting::query()->count() === 0) {
ArchiveSetting::factory()->count(2)->create();
}
}
}
+1
View File
@@ -31,6 +31,7 @@ public function run(): void
$this->call([
AccountTypeSeeder::class,
PaymentSettingSeeder::class,
ArchiveEntitySeeder::class,
PersonSeeder::class,
SegmentSeeder::class,
ActionSeeder::class,