big update all menus

This commit is contained in:
Wian Drs
2026-08-07 09:16:13 +07:00
parent 75342dc7b7
commit e7575ba1c0
101 changed files with 3451 additions and 89 deletions
@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const ROOT_ORDER = [
'users' => 1,
'tenants' => 2,
'ticketing' => 3,
'wilayah' => 4,
'nas' => 5,
'customers' => 6,
'billing' => 7,
'topology' => 8,
'notifications-service' => 9,
'payment-gateway' => 10,
'wallet' => 11,
'voucher-hotspot' => 12,
'role-applications' => 13,
];
public function up(): void
{
foreach (self::ROOT_ORDER as $slug => $sortOrder) {
DB::table('menu_lists')
->whereNull('parent_id')
->where('slug', $slug)
->update(['sort_order' => $sortOrder]);
}
}
public function down(): void
{
foreach ([
'payment-gateway' => 80,
'wallet' => 81,
'voucher-hotspot' => 82,
'role-applications' => 83,
] as $slug => $sortOrder) {
DB::table('menu_lists')
->whereNull('parent_id')
->where('slug', $slug)
->update(['sort_order' => $sortOrder]);
}
}
};
@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const WORKFLOW_ORDER = [
'customers' => 1,
'billing' => 2,
'voucher-hotspot' => 3,
'nas' => 4,
'topology' => 5,
'ticketing' => 6,
'notifications-service' => 7,
'wallet' => 8,
'payment-gateway' => 9,
'users' => 10,
'tenants' => 11,
'role-applications' => 12,
'wilayah' => 13,
];
public function up(): void
{
$this->apply(self::WORKFLOW_ORDER);
}
public function down(): void
{
$this->apply([
'users' => 1,
'tenants' => 2,
'ticketing' => 3,
'wilayah' => 4,
'nas' => 5,
'customers' => 6,
'billing' => 7,
'topology' => 8,
'notifications-service' => 9,
'payment-gateway' => 10,
'wallet' => 11,
'voucher-hotspot' => 12,
'role-applications' => 13,
]);
}
private function apply(array $order): void
{
foreach ($order as $slug => $sortOrder) {
DB::table('menu_lists')
->whereNull('parent_id')
->where('slug', $slug)
->update(['sort_order' => $sortOrder]);
}
}
};
@@ -0,0 +1,54 @@
<?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('nas_mikrotiks', function (Blueprint $table) {
$table->string('connection_status', 20)->default('unchecked')->after('status');
$table->timestamp('last_checked_at')->nullable()->after('connection_status');
$table->timestamp('last_connected_at')->nullable()->after('last_checked_at');
$table->unsignedInteger('last_latency_ms')->nullable()->after('last_connected_at');
$table->unsignedInteger('consecutive_failures')->default(0)->after('last_latency_ms');
$table->string('router_identity')->nullable()->after('consecutive_failures');
$table->string('router_version')->nullable()->after('router_identity');
$table->text('last_connection_error')->nullable()->after('router_version');
$table->index(['connection_type', 'status', 'connection_status'], 'nas_mikrotiks_health_index');
});
Schema::create('customer_nas_accounts', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->unique()->constrained()->cascadeOnDelete();
$table->foreignId('nas_mikrotik_id')->constrained()->restrictOnDelete();
$table->foreignId('nas_package_profile_id')->constrained()->restrictOnDelete();
$table->string('service_type', 20);
$table->string('username');
$table->text('password');
$table->string('remote_id')->nullable();
$table->string('remote_profile')->nullable();
$table->string('status', 20)->default('active');
$table->timestamp('last_synced_at')->nullable();
$table->text('last_sync_error')->nullable();
$table->timestamps();
$table->unique(['nas_mikrotik_id', 'service_type', 'username'], 'customer_nas_account_username_unique');
$table->index(['tenant_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('customer_nas_accounts');
Schema::table('nas_mikrotiks', function (Blueprint $table) {
$table->dropIndex('nas_mikrotiks_health_index');
$table->dropColumn([
'connection_status', 'last_checked_at', 'last_connected_at', 'last_latency_ms',
'consecutive_failures', 'router_identity', 'router_version', 'last_connection_error',
]);
});
}
};
@@ -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::table('nas_mikrotiks', function (Blueprint $table) {
$table->timestamp('next_health_check_at')->nullable()->after('last_checked_at');
$table->json('health_details')->nullable()->after('last_connection_error');
$table->index(['status', 'next_health_check_at'], 'nas_mikrotiks_health_due_index');
});
}
public function down(): void
{
Schema::table('nas_mikrotiks', function (Blueprint $table) {
$table->dropIndex('nas_mikrotiks_health_due_index');
$table->dropColumn(['next_health_check_at', 'health_details']);
});
}
};
@@ -0,0 +1,110 @@
<?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('nas_package_profiles', function (Blueprint $table) {
$table->string('profile_code', 80)->nullable()->after('name');
$table->string('usage_mode', 30)->default('recurring_member')->after('service_type');
$table->string('validity_start', 30)->default('activation')->after('usage_mode');
$table->unsignedInteger('validity_value')->nullable()->after('validity_start');
$table->string('validity_unit', 20)->nullable()->after('validity_value');
$table->unsignedBigInteger('uptime_limit_seconds')->nullable()->after('validity_unit');
$table->unsignedBigInteger('data_limit_bytes')->nullable()->after('uptime_limit_seconds');
$table->unsignedInteger('grace_period_minutes')->default(0)->after('data_limit_bytes');
$table->json('service_parameters')->nullable()->after('grace_period_minutes');
$table->unique(['tenant_id', 'profile_code']);
});
Schema::create('nas_script_policies', function (Blueprint $table) {
$table->id();
$table->string('code', 100)->unique();
$table->string('name');
$table->string('service_type', 20);
$table->string('event', 30);
$table->unsignedInteger('current_version')->default(1);
$table->json('allowed_variables')->nullable();
$table->boolean('enabled')->default(true);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['service_type', 'event', 'name']);
});
Schema::create('nas_script_policy_versions', function (Blueprint $table) {
$table->id();
$table->foreignId('nas_script_policy_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('version');
$table->longText('script_template');
$table->string('checksum', 64);
$table->text('change_notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['nas_script_policy_id', 'version']);
});
Schema::create('nas_package_profile_scripts', function (Blueprint $table) {
$table->id();
$table->foreignId('nas_package_profile_id')->constrained()->cascadeOnDelete();
$table->foreignId('nas_script_policy_id')->constrained()->restrictOnDelete();
$table->foreignId('nas_script_policy_version_id')->nullable()->constrained()->nullOnDelete();
$table->string('event', 30);
$table->boolean('enabled')->default(true);
$table->timestamps();
$table->unique(['nas_package_profile_id', 'event'], 'nas_package_profile_event_unique');
});
Schema::create('nas_package_profile_targets', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('nas_package_profile_id')->constrained()->cascadeOnDelete();
$table->foreignId('nas_mikrotik_id')->constrained()->cascadeOnDelete();
$table->string('backend_type', 30);
$table->string('external_profile_name');
$table->string('remote_id')->nullable();
$table->string('sync_status', 30)->default('not_deployed');
$table->json('desired_snapshot')->nullable();
$table->json('remote_snapshot')->nullable();
$table->string('deployed_checksum', 64)->nullable();
$table->timestamp('last_checked_at')->nullable();
$table->timestamp('last_synced_at')->nullable();
$table->text('last_error')->nullable();
$table->timestamps();
$table->unique(['nas_package_profile_id', 'nas_mikrotik_id'], 'nas_package_profile_target_unique');
$table->index(['tenant_id', 'sync_status']);
});
Schema::table('customer_nas_accounts', function (Blueprint $table) {
$table->foreignId('nas_package_profile_target_id')->nullable()->after('nas_package_profile_id')->constrained()->nullOnDelete();
$table->string('usage_mode', 30)->default('recurring_member')->after('service_type');
$table->timestamp('validity_started_at')->nullable()->after('status');
$table->timestamp('first_login_at')->nullable()->after('validity_started_at');
$table->timestamp('expires_at')->nullable()->after('first_login_at');
$table->unsignedBigInteger('used_uptime_seconds')->default(0)->after('expires_at');
$table->unsignedBigInteger('used_bytes')->default(0)->after('used_uptime_seconds');
$table->timestamp('last_reconciled_at')->nullable()->after('last_synced_at');
$table->json('remote_state')->nullable()->after('last_reconciled_at');
$table->index(['status', 'expires_at']);
});
}
public function down(): void
{
Schema::table('customer_nas_accounts', function (Blueprint $table) {
$table->dropConstrainedForeignId('nas_package_profile_target_id');
$table->dropColumn(['usage_mode', 'validity_started_at', 'first_login_at', 'expires_at', 'used_uptime_seconds', 'used_bytes', 'last_reconciled_at', 'remote_state']);
});
Schema::dropIfExists('nas_package_profile_targets');
Schema::dropIfExists('nas_package_profile_scripts');
Schema::dropIfExists('nas_script_policy_versions');
Schema::dropIfExists('nas_script_policies');
Schema::table('nas_package_profiles', function (Blueprint $table) {
$table->dropUnique(['tenant_id', 'profile_code']);
$table->dropColumn(['profile_code', 'usage_mode', 'validity_start', 'validity_value', 'validity_unit', 'uptime_limit_seconds', 'data_limit_bytes', 'grace_period_minutes', 'service_parameters']);
});
}
};
@@ -0,0 +1,43 @@
<?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('nas_package_profiles', function (Blueprint $table) {
$table->string('usage_mode', 30)->nullable()->default(null)->change();
$table->string('validity_start', 30)->nullable()->default(null)->change();
$table->unsignedInteger('grace_period_minutes')->nullable()->default(null)->change();
$table->boolean('mac_lock_required')->default(false)->after('grace_period_minutes');
$table->string('ip_pool')->nullable()->after('mac_lock_required');
$table->string('subnet_cidr', 64)->nullable()->after('ip_pool');
$table->string('gateway', 64)->nullable()->after('subnet_cidr');
$table->json('dns_servers')->nullable()->after('gateway');
$table->unsignedSmallInteger('vlan_id')->nullable()->after('dns_servers');
});
Schema::table('customer_nas_accounts', function (Blueprint $table) {
$table->string('mac_address', 17)->nullable()->after('username');
$table->string('assigned_ip', 64)->nullable()->after('mac_address');
$table->index(['nas_mikrotik_id', 'mac_address']);
});
}
public function down(): void
{
Schema::table('customer_nas_accounts', function (Blueprint $table) {
$table->dropIndex(['nas_mikrotik_id', 'mac_address']);
$table->dropColumn(['mac_address', 'assigned_ip']);
});
Schema::table('nas_package_profiles', function (Blueprint $table) {
$table->dropColumn(['mac_lock_required', 'ip_pool', 'subnet_cidr', 'gateway', 'dns_servers', 'vlan_id']);
$table->string('usage_mode', 30)->nullable(false)->default('recurring_member')->change();
$table->string('validity_start', 30)->nullable(false)->default('activation')->change();
$table->unsignedInteger('grace_period_minutes')->nullable(false)->default(0)->change();
});
}
};
@@ -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
{
public function up(): void
{
Schema::create('radius_clusters', function (Blueprint $table) {
$table->id();
$table->string('code', 80)->unique();
$table->string('name');
$table->string('region', 80)->nullable();
$table->json('auth_endpoints')->nullable();
$table->json('accounting_endpoints')->nullable();
$table->string('provisioning_api_url')->nullable();
$table->text('provisioning_api_token')->nullable();
$table->string('status', 20)->default('planned');
$table->timestamps();
});
Schema::create('tenant_nas_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->unique()->constrained()->cascadeOnDelete();
$table->string('backend_mode', 30)->default('routeros_api');
$table->string('hybrid_radius_source', 20)->nullable();
$table->foreignId('radius_cluster_id')->nullable()->constrained('radius_clusters')->nullOnDelete();
$table->string('external_radius_host')->nullable();
$table->unsignedSmallInteger('external_auth_port')->default(1812);
$table->unsignedSmallInteger('external_accounting_port')->default(1813);
$table->unsignedSmallInteger('external_coa_port')->default(3799);
$table->text('external_radius_secret')->nullable();
$table->string('external_provisioning_api_url')->nullable();
$table->text('external_provisioning_api_token')->nullable();
$table->boolean('accounting_enabled')->default(true);
$table->boolean('coa_enabled')->default(true);
$table->string('health_status', 20)->default('not_configured');
$table->timestamp('last_checked_at')->nullable();
$table->text('last_error')->nullable();
$table->timestamps();
});
Schema::table('nas_mikrotiks', function (Blueprint $table) {
$table->string('backend_mode_override', 30)->nullable()->after('connection_type');
$table->string('nas_identifier')->nullable()->after('host');
$table->string('nas_ip_address', 64)->nullable()->after('nas_identifier');
$table->unsignedSmallInteger('radius_coa_port')->default(3799)->after('radius_accounting_port');
$table->index(['tenant_id', 'backend_mode_override']);
});
}
public function down(): void
{
Schema::table('nas_mikrotiks', function (Blueprint $table) {
$table->dropIndex(['tenant_id', 'backend_mode_override']);
$table->dropColumn(['backend_mode_override', 'nas_identifier', 'nas_ip_address', 'radius_coa_port']);
});
Schema::dropIfExists('tenant_nas_settings');
Schema::dropIfExists('radius_clusters');
}
};
@@ -0,0 +1,20 @@
<?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('nas_mikrotiks', function (Blueprint $table) {
$table->string('radius_host')->nullable()->after('nas_ip_address');
});
}
public function down(): void
{
Schema::table('nas_mikrotiks', fn (Blueprint $table) => $table->dropColumn('radius_host'));
}
};
@@ -0,0 +1,27 @@
<?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('radius_clusters', function (Blueprint $table) {
$table->json('coa_endpoints')->nullable()->after('accounting_endpoints');
$table->string('healthcheck_url')->nullable()->after('provisioning_api_url');
$table->unsignedSmallInteger('request_timeout')->default(5)->after('healthcheck_url');
$table->unsignedSmallInteger('retry_attempts')->default(2)->after('request_timeout');
$table->text('notes')->nullable()->after('status');
$table->timestamp('last_checked_at')->nullable();
$table->unsignedInteger('last_latency_ms')->nullable();
$table->text('last_error')->nullable();
});
}
public function down(): void
{
Schema::table('radius_clusters', fn (Blueprint $table) => $table->dropColumn(['coa_endpoints','healthcheck_url','request_timeout','retry_attempts','notes','last_checked_at','last_latency_ms','last_error']));
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->string('customer_code')->nullable()->change();
$table->foreignId('sales_user_id')->nullable()->after('user_id')->constrained('users')->nullOnDelete();
$table->foreignId('topology_port_id')->nullable()->after('topology_id')->constrained('topology_ports')->nullOnDelete();
$table->index(['tenant_id','sales_user_id','status']);
});
Schema::table('user_tenants', fn (Blueprint $table) => $table->string('department', 50)->nullable()->after('role')->index());
DB::statement("CREATE UNIQUE INDEX topology_customer_active_port_unique ON topology_customer_connections (odp_port_id) WHERE odp_port_id IS NOT NULL AND status IN ('reserved','active')");
}
public function down(): void
{
DB::statement('DROP INDEX IF EXISTS topology_customer_active_port_unique');
Schema::table('user_tenants', fn (Blueprint $table) => $table->dropColumn('department'));
Schema::table('customers', function (Blueprint $table) {
$table->dropIndex(['tenant_id','sales_user_id','status']);
$table->dropConstrainedForeignId('sales_user_id');
$table->dropConstrainedForeignId('topology_port_id');
$table->string('customer_code')->nullable(false)->change();
});
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
Schema::table('customers', fn (Blueprint $table) => $table->unsignedInteger('topology_port_number')->nullable()->after('topology_id'));
DB::statement('UPDATE customers c SET topology_port_number = p.port_number FROM topology_ports p WHERE c.topology_port_id = p.id');
Schema::table('customers', fn (Blueprint $table) => $table->dropConstrainedForeignId('topology_port_id'));
}
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->foreignId('topology_port_id')->nullable()->after('topology_id')->constrained('topology_ports')->nullOnDelete();
$table->dropColumn('topology_port_number');
});
}
};
@@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::statement("DELETE FROM topology_ports p WHERE p.status = 'available'
AND NOT EXISTS (SELECT 1 FROM topology_customer_connections c WHERE c.odp_port_id = p.id)
AND NOT EXISTS (SELECT 1 FROM topology_links l WHERE l.source_port_id = p.id OR l.target_port_id = p.id)
AND NOT EXISTS (SELECT 1 FROM topology_splitters s WHERE s.input_port_id = p.id)");
}
public function down(): void {}
};
@@ -0,0 +1,21 @@
<?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('customer_installation_workflows', function(Blueprint $t){$t->id();$t->foreignId('tenant_id')->constrained()->cascadeOnDelete();$t->string('name');$t->string('code',80);$t->text('description')->nullable();$t->boolean('is_default')->default(false);$t->string('status',20)->default('draft');$t->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();$t->timestamps();$t->unique(['tenant_id','code']);$t->index(['tenant_id','is_default','status']);});
Schema::create('customer_installation_steps', function(Blueprint $t){$t->id();$t->foreignId('workflow_id')->constrained('customer_installation_workflows')->cascadeOnDelete();$t->string('name');$t->string('code',80);$t->string('step_type',30)->default('custom');$t->unsignedInteger('sort_order');$t->boolean('is_initial')->default(false);$t->boolean('is_final')->default(false);$t->boolean('requires_ticket')->default(false);$t->unsignedBigInteger('ticket_type_id')->nullable();$t->string('ticket_priority',20)->default('medium');$t->boolean('requires_approval')->default(false);$t->timestamps();$t->unique(['workflow_id','code']);$t->unique(['workflow_id','sort_order']);});
Schema::create('customer_installation_step_fields', function(Blueprint $t){$t->id();$t->foreignId('step_id')->constrained('customer_installation_steps')->cascadeOnDelete();$t->string('field_code',100);$t->boolean('is_visible')->default(true);$t->boolean('is_required')->default(false);$t->boolean('is_readonly')->default(false);$t->unsignedInteger('sort_order')->default(0);$t->timestamps();$t->unique(['step_id','field_code']);});
Schema::create('customer_installation_transitions', function(Blueprint $t){$t->id();$t->foreignId('workflow_id')->constrained('customer_installation_workflows')->cascadeOnDelete();$t->foreignId('from_step_id')->constrained('customer_installation_steps')->cascadeOnDelete();$t->foreignId('to_step_id')->constrained('customer_installation_steps')->cascadeOnDelete();$t->string('name');$t->string('code',100);$t->boolean('requires_approval')->default(false);$t->boolean('creates_ticket')->default(false);$t->boolean('closes_ticket')->default(false);$t->timestamps();$t->unique(['workflow_id','code']);});
Schema::create('customer_installation_instances', function(Blueprint $t){$t->id();$t->foreignId('tenant_id')->constrained()->cascadeOnDelete();$t->foreignId('customer_id')->unique()->constrained()->cascadeOnDelete();$t->foreignId('workflow_id')->constrained('customer_installation_workflows')->restrictOnDelete();$t->foreignId('current_step_id')->constrained('customer_installation_steps')->restrictOnDelete();$t->string('status',20)->default('in_progress');$t->timestamp('started_at')->nullable();$t->timestamp('completed_at')->nullable();$t->timestamps();$t->index(['tenant_id','current_step_id','status']);});
Schema::create('customer_installation_histories', function(Blueprint $t){$t->id();$t->foreignId('installation_instance_id')->constrained('customer_installation_instances')->cascadeOnDelete();$t->foreignId('from_step_id')->nullable()->constrained('customer_installation_steps')->nullOnDelete();$t->foreignId('to_step_id')->constrained('customer_installation_steps')->restrictOnDelete();$t->foreignId('transition_id')->nullable()->constrained('customer_installation_transitions')->nullOnDelete();$t->foreignId('performed_by')->nullable()->constrained('users')->nullOnDelete();$t->unsignedBigInteger('ticket_id')->nullable();$t->text('notes')->nullable();$t->json('form_snapshot')->nullable();$t->timestamps();});
Schema::create('menu_group_workflow_permissions', function(Blueprint $t){$t->id();$t->foreignId('menu_group_id')->constrained()->cascadeOnDelete();$t->foreignId('workflow_id')->constrained('customer_installation_workflows')->cascadeOnDelete();$t->foreignId('step_id')->constrained('customer_installation_steps')->cascadeOnDelete();$t->boolean('can_view')->default(false);$t->boolean('can_fill')->default(false);$t->boolean('can_submit')->default(false);$t->boolean('can_approve')->default(false);$t->boolean('can_reject')->default(false);$t->boolean('can_assign')->default(false);$t->timestamps();$t->unique(['menu_group_id','step_id']);});
Schema::table('tickets', function(Blueprint $t){$t->foreignId('installation_instance_id')->nullable()->constrained('customer_installation_instances')->nullOnDelete();$t->foreignId('workflow_step_id')->nullable()->constrained('customer_installation_steps')->nullOnDelete();$t->index(['installation_instance_id','workflow_step_id']);});
}
public function down(): void {Schema::table('tickets',function(Blueprint $t){$t->dropConstrainedForeignId('installation_instance_id');$t->dropConstrainedForeignId('workflow_step_id');});Schema::dropIfExists('menu_group_workflow_permissions');Schema::dropIfExists('customer_installation_histories');Schema::dropIfExists('customer_installation_instances');Schema::dropIfExists('customer_installation_transitions');Schema::dropIfExists('customer_installation_step_fields');Schema::dropIfExists('customer_installation_steps');Schema::dropIfExists('customer_installation_workflows');}
};
@@ -0,0 +1,3 @@
<?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('tenant_installation_settings',function(Blueprint $t){$t->id();$t->foreignId('tenant_id')->unique()->constrained()->cascadeOnDelete();$t->string('activation_mode',30)->default('list_order');$t->boolean('ktp_ocr_enabled')->default(false);$t->string('ocr_provider',30)->nullable();$t->timestamps();});Schema::table('menu_group_menus',fn(Blueprint $t)=>$t->boolean('can_activate')->default(false)->after('can_approve'));Schema::table('tickets',function(Blueprint $t){$t->dropConstrainedForeignId('installation_instance_id');$t->dropConstrainedForeignId('workflow_step_id');});Schema::dropIfExists('menu_group_workflow_permissions');Schema::dropIfExists('customer_installation_histories');Schema::dropIfExists('customer_installation_instances');Schema::dropIfExists('customer_installation_transitions');Schema::dropIfExists('customer_installation_step_fields');Schema::dropIfExists('customer_installation_steps');Schema::dropIfExists('customer_installation_workflows');}public function down():void{Schema::dropIfExists('tenant_installation_settings');Schema::table('menu_group_menus',fn(Blueprint $t)=>$t->dropColumn('can_activate'));}};
@@ -0,0 +1,20 @@
<?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('tenant_installation_settings', function (Blueprint $table) {
$table->text('ocr_api_key')->nullable()->after('ocr_provider');
});
}
public function down(): void
{
Schema::table('tenant_installation_settings', fn (Blueprint $table) => $table->dropColumn('ocr_api_key'));
}
};
@@ -0,0 +1,18 @@
<?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('customer_nas_accounts', fn (Blueprint $table) => $table->string('serial_number', 120)->nullable()->after('mac_address'));
}
public function down(): void
{
Schema::table('customer_nas_accounts', fn (Blueprint $table) => $table->dropColumn('serial_number'));
}
};
@@ -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
{
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropForeign(['customer_id']);
});
Schema::table('invoices', function (Blueprint $table) {
$table->unsignedBigInteger('customer_id')->nullable()->change();
$table->foreign('customer_id')->references('id')->on('customers')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropForeign(['customer_id']);
});
Schema::table('invoices', function (Blueprint $table) {
$table->unsignedBigInteger('customer_id')->nullable(false)->change();
$table->foreign('customer_id')->references('id')->on('customers')->restrictOnDelete();
});
}
};
@@ -0,0 +1,64 @@
<?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('tickets', function (Blueprint $table) {
$table->string('source', 30)->default('manual')->after('customer_id')->index();
$table->string('installation_step', 30)->nullable()->after('source');
$table->foreignId('verified_by')->nullable()->after('resolved_at')->constrained('users')->nullOnDelete();
$table->timestamp('verified_at')->nullable()->after('verified_by');
$table->text('verification_notes')->nullable()->after('verified_at');
});
Schema::create('ticket_attachments', function (Blueprint $table) {
$table->id(); $table->foreignId('ticket_id')->constrained()->cascadeOnDelete();
$table->foreignId('stored_file_id')->constrained('stored_files')->cascadeOnDelete();
$table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete();
$table->string('stage', 30); $table->text('notes')->nullable(); $table->timestamps();
$table->unique(['ticket_id','stored_file_id','stage']);
});
if (! Schema::hasColumn('ticket_materials','technician_consumable_id')) Schema::table('ticket_materials', function (Blueprint $table) {
$table->foreignId('technician_consumable_id')->nullable()->after('technician_material_id')->constrained('technician_consumables')->restrictOnDelete();
});
Schema::create('warehouse_items', function (Blueprint $table) {
$table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('sku'); $table->string('name'); $table->string('category')->nullable();
$table->enum('tracking_type',['serialized','bulk']); $table->string('unit',30)->default('unit');
$table->decimal('minimum_stock',15,2)->default(0); $table->boolean('is_active')->default(true); $table->timestamps();
$table->unique(['tenant_id','sku']);
});
Schema::create('item_barcodes', function (Blueprint $table) {
$table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('warehouse_item_id')->constrained()->cascadeOnDelete(); $table->string('barcode')->unique();
$table->string('serial_number')->nullable(); $table->decimal('initial_qty',15,2)->default(1);
$table->decimal('remaining_qty',15,2)->default(1); $table->string('status',30)->default('available'); $table->timestamps();
});
Schema::create('item_movements', function (Blueprint $table) {
$table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('warehouse_item_id')->constrained()->restrictOnDelete();
$table->foreignId('item_barcode_id')->nullable()->constrained()->restrictOnDelete();
$table->string('movement_type',30); $table->decimal('qty',15,2); $table->string('reference_type')->nullable();
$table->unsignedBigInteger('reference_id')->nullable(); $table->foreignId('from_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('to_user_id')->nullable()->constrained('users')->nullOnDelete(); $table->text('notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps();
});
Schema::create('item_stock_logs', function (Blueprint $table) {
$table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('warehouse_item_id')->constrained()->restrictOnDelete();
$table->foreignId('item_movement_id')->nullable()->constrained()->nullOnDelete();
$table->decimal('qty_before',15,2); $table->decimal('qty_change',15,2); $table->decimal('qty_after',15,2); $table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('item_stock_logs'); Schema::dropIfExists('item_movements'); Schema::dropIfExists('item_barcodes'); Schema::dropIfExists('warehouse_items');
if (Schema::hasColumn('ticket_materials','technician_consumable_id')) Schema::table('ticket_materials', fn(Blueprint $t)=>$t->dropConstrainedForeignId('technician_consumable_id'));
Schema::dropIfExists('ticket_attachments');
Schema::table('tickets', function(Blueprint $t){$t->dropConstrainedForeignId('verified_by');$t->dropColumn(['source','installation_step','verified_at','verification_notes']);});
}
};
@@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('item_codes', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('code', 80);
$table->string('name');
$table->string('category', 100)->nullable();
$table->enum('tracking_type', ['serialized', 'quantity'])->default('serialized');
$table->boolean('need_serial_number')->default(false);
$table->string('unit', 30)->default('pcs');
$table->decimal('minimum_stock', 15, 2)->default(0);
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['tenant_id', 'code']);
});
Schema::table('warehouse_items', function (Blueprint $table) {
$table->foreignId('item_code_id')->nullable()->after('tenant_id')->constrained('item_codes')->restrictOnDelete();
});
Schema::table('item_barcodes', function (Blueprint $table) {
$table->string('tracking_type', 20)->nullable()->after('barcode');
$table->string('unit', 30)->nullable()->after('tracking_type');
});
Schema::table('item_stock_logs', function (Blueprint $table) {
$table->foreignId('item_barcode_id')->nullable()->after('warehouse_item_id')->constrained()->nullOnDelete();
$table->decimal('barcode_qty_before', 15, 2)->nullable()->after('qty_after');
$table->decimal('barcode_qty_change', 15, 2)->nullable()->after('barcode_qty_before');
$table->decimal('barcode_qty_after', 15, 2)->nullable()->after('barcode_qty_change');
});
foreach (DB::table('warehouse_items')->orderBy('id')->get() as $item) {
$codeId = DB::table('item_codes')->insertGetId([
'tenant_id' => $item->tenant_id,
'code' => $item->sku,
'name' => $item->name,
'category' => $item->category,
'tracking_type' => $item->tracking_type === 'serialized' ? 'serialized' : 'quantity',
'need_serial_number' => false,
'unit' => $item->unit,
'minimum_stock' => $item->minimum_stock,
'is_active' => $item->is_active,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at,
]);
DB::table('warehouse_items')->where('id', $item->id)->update(['item_code_id' => $codeId]);
DB::table('item_barcodes')->where('warehouse_item_id', $item->id)->update([
'tracking_type' => $item->tracking_type === 'serialized' ? 'serialized' : 'quantity',
'unit' => $item->unit,
]);
}
}
public function down(): void
{
Schema::table('item_stock_logs', function (Blueprint $table) {
$table->dropConstrainedForeignId('item_barcode_id');
$table->dropColumn(['barcode_qty_before', 'barcode_qty_change', 'barcode_qty_after']);
});
Schema::table('item_barcodes', fn (Blueprint $table) => $table->dropColumn(['tracking_type', 'unit']));
Schema::table('warehouse_items', fn (Blueprint $table) => $table->dropConstrainedForeignId('item_code_id'));
Schema::dropIfExists('item_codes');
}
};
@@ -0,0 +1,20 @@
<?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('warehouse_items', fn (Blueprint $table) => $table->dropColumn('category'));
Schema::table('item_codes', fn (Blueprint $table) => $table->dropColumn('category'));
}
public function down(): void
{
Schema::table('item_codes', fn (Blueprint $table) => $table->string('category', 100)->nullable());
Schema::table('warehouse_items', fn (Blueprint $table) => $table->string('category', 100)->nullable());
}
};
@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('warehouses', function (Blueprint $table) {
$table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('code', 50); $table->string('name'); $table->text('address')->nullable();
$table->boolean('is_active')->default(true); $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps();
$table->unique(['tenant_id','code']);
});
Schema::table('warehouse_items', function (Blueprint $table) {
$table->foreignId('warehouse_id')->nullable()->after('tenant_id')->constrained('warehouses')->restrictOnDelete();
$table->string('item_number',100)->nullable()->after('item_code_id');
$table->string('status',20)->default('draft')->after('item_number')->index();
$table->decimal('received_qty',15,2)->default(0)->after('minimum_stock');
$table->date('received_at')->nullable()->after('received_qty');
$table->foreignId('posted_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('posted_at')->nullable(); $table->text('notes')->nullable();
$table->unique(['tenant_id','item_number']);
});
Schema::table('item_barcodes', function (Blueprint $table) {
$table->unsignedInteger('package_number')->nullable()->after('warehouse_item_id');
$table->unique(['tenant_id','serial_number']);
});
foreach (DB::table('tenants')->select('id')->get() as $tenant) {
$warehouseId=DB::table('warehouses')->insertGetId(['tenant_id'=>$tenant->id,'code'=>'MAIN','name'=>'Gudang Utama','is_active'=>true,'created_at'=>now(),'updated_at'=>now()]);
foreach(DB::table('warehouse_items')->where('tenant_id',$tenant->id)->orderBy('id')->get() as $item){
$date=\Carbon\Carbon::parse($item->created_at)->format('dmy');$base=$item->sku.'-'.$date;$number=$base.'-'.str_pad((string)$item->id,3,'0',STR_PAD_LEFT);
DB::table('warehouse_items')->where('id',$item->id)->update(['warehouse_id'=>$warehouseId,'item_number'=>$number,'status'=>DB::table('item_barcodes')->where('warehouse_item_id',$item->id)->sum('remaining_qty')>0?'active':'depleted','received_qty'=>DB::table('item_barcodes')->where('warehouse_item_id',$item->id)->sum('initial_qty'),'received_at'=>\Carbon\Carbon::parse($item->created_at)->toDateString(),'posted_at'=>$item->created_at]);
}
}
}
public function down(): void
{
Schema::table('item_barcodes',function(Blueprint $t){$t->dropUnique(['tenant_id','serial_number']);$t->dropColumn('package_number');});
Schema::table('warehouse_items',function(Blueprint $t){$t->dropConstrainedForeignId('warehouse_id');$t->dropConstrainedForeignId('posted_by');$t->dropUnique(['tenant_id','item_number']);$t->dropColumn(['item_number','status','received_qty','received_at','posted_at','notes']);});
Schema::dropIfExists('warehouses');
}
};
+26 -14
View File
@@ -13,6 +13,14 @@ class MenuListSeeder extends Seeder
*/
public function run(): void
{
MenuList::where('slug', 'customers-installation-workflows')->delete();
$warehouse = MenuList::updateOrCreate(['slug'=>'warehouse'],['parent_id'=>null,'name'=>'Gudang','url'=>null,'icon'=>'cil-storage','component'=>null,'sort_order'=>8,'is_active'=>true]);
MenuList::updateOrCreate(['slug'=>'warehouse-pending'],['parent_id'=>$warehouse->id,'name'=>'Barang Pending','url'=>'/warehouse/pending','icon'=>'cil-clock','component'=>null,'sort_order'=>1,'is_active'=>true]);
MenuList::updateOrCreate(['slug'=>'warehouse-items'],['parent_id'=>$warehouse->id,'name'=>'Daftar Barang','url'=>'/warehouse/items','icon'=>'cil-list','component'=>null,'sort_order'=>2,'is_active'=>true]);
MenuList::updateOrCreate(['slug'=>'warehouse-depleted'],['parent_id'=>$warehouse->id,'name'=>'Barang Habis','url'=>'/warehouse/depleted','icon'=>'cil-inbox','component'=>null,'sort_order'=>3,'is_active'=>true]);
MenuList::updateOrCreate(['slug'=>'warehouse-technician-inventory'],['parent_id'=>$warehouse->id,'name'=>'Peralatan Teknisi','url'=>'/warehouse/technician-inventory','icon'=>'cil-briefcase','component'=>null,'sort_order'=>4,'is_active'=>true]);
MenuList::updateOrCreate(['slug'=>'warehouse-locations'],['parent_id'=>$warehouse->id,'name'=>'Daftar Gudang','url'=>'/warehouse/locations','icon'=>'cil-building','component'=>null,'sort_order'=>5,'is_active'=>true]);
MenuList::updateOrCreate(['slug'=>'warehouse-codes'],['parent_id'=>$warehouse->id,'name'=>'Kode Barang','url'=>'/warehouse/item-codes','icon'=>'cil-barcode','component'=>null,'sort_order'=>6,'is_active'=>true]);
$users = MenuList::updateOrCreate(
['slug' => 'users'],
[
@@ -21,7 +29,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-user',
'component' => null,
'sort_order' => 1,
'sort_order' => 10,
'is_active' => true,
]
);
@@ -56,14 +64,16 @@ class MenuListSeeder extends Seeder
['slug' => 'tenants'],
[
'parent_id' => null,
'name' => 'Tenant',
'name' => 'Daftar Tenant',
'url' => '/tenants',
'icon' => 'cil-layers',
'component' => null,
'sort_order' => 2,
'sort_order' => 11,
'is_active' => true,
]
);
MenuList::where('slug', 'tenant-installation-settings')->delete();
MenuList::where('slug', 'tenants-list')->update(['is_active' => false]);
$ticketing = MenuList::updateOrCreate(
['slug' => 'ticketing'],
@@ -73,7 +83,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-description',
'component' => null,
'sort_order' => 3,
'sort_order' => 6,
'is_active' => true,
]
);
@@ -164,7 +174,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-location-pin',
'component' => null,
'sort_order' => 4,
'sort_order' => 13,
'is_active' => true,
]
);
@@ -191,7 +201,7 @@ class MenuListSeeder extends Seeder
$accessApplications = MenuList::updateOrCreate(['slug' => 'role-applications'], [
'parent_id' => null, 'name' => 'Permintaan Akses', 'url' => null,
'icon' => 'cil-user-follow', 'component' => null, 'sort_order' => 83,
'icon' => 'cil-user-follow', 'component' => null, 'sort_order' => 12,
]);
foreach ([
['slug' => 'role-applications-tenants', 'name' => 'Pengajuan Tenant', 'url' => '/access-applications/tenants', 'sort_order' => 1],
@@ -227,7 +237,7 @@ class MenuListSeeder extends Seeder
['slug' => 'payment-gateway'],
[
'parent_id' => null, 'name' => 'Payment Gateway', 'url' => null,
'icon' => 'cil-credit-card', 'component' => null, 'sort_order' => 80,
'icon' => 'cil-credit-card', 'component' => null, 'sort_order' => 9,
]
);
foreach ([
@@ -242,14 +252,14 @@ class MenuListSeeder extends Seeder
MenuList::updateOrCreate(['slug' => 'wallet'], [
'parent_id' => null, 'name' => 'Saldo Deposit', 'url' => '/deposit-balance',
'icon' => 'cil-wallet', 'component' => null, 'sort_order' => 81,
'icon' => 'cil-wallet', 'component' => null, 'sort_order' => 8,
]);
$voucherHotspot = MenuList::updateOrCreate(
['slug' => 'voucher-hotspot'],
[
'parent_id' => null, 'name' => 'Voucher Hotspot', 'url' => null,
'icon' => 'cil-barcode', 'component' => null, 'sort_order' => 82,
'icon' => 'cil-barcode', 'component' => null, 'sort_order' => 3,
]
);
foreach ([
@@ -271,7 +281,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-settings',
'component' => null,
'sort_order' => 5,
'sort_order' => 4,
'is_active' => true,
]
);
@@ -281,6 +291,8 @@ class MenuListSeeder extends Seeder
['slug' => 'nas-package-profiles', 'name' => 'Profile Paket', 'url' => '/nas/package-profiles', 'icon' => 'cil-speedometer', 'sort_order' => 2],
['slug' => 'nas-olt', 'name' => 'OLT', 'url' => '/nas/olt', 'icon' => 'cil-layers', 'sort_order' => 3],
['slug' => 'nas-webfig', 'name' => 'Webfig', 'url' => '/nas/webfig', 'icon' => 'cil-laptop', 'sort_order' => 4],
['slug' => 'nas-script-policies', 'name' => 'Script Policy', 'url' => '/nas/script-policies', 'icon' => 'cil-code', 'sort_order' => 5],
['slug' => 'nas-radius-infrastructure', 'name' => 'Infrastruktur RADIUS', 'url' => '/nas/radius-infrastructure', 'icon' => 'cil-storage', 'sort_order' => 6],
] as $item) {
MenuList::updateOrCreate(
['slug' => $item['slug']],
@@ -304,7 +316,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-people',
'component' => null,
'sort_order' => 6,
'sort_order' => 1,
'is_active' => true,
]
);
@@ -338,7 +350,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-description',
'component' => null,
'sort_order' => 7,
'sort_order' => 2,
'is_active' => true,
]
);
@@ -371,7 +383,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-layers',
'component' => null,
'sort_order' => 8,
'sort_order' => 5,
'is_active' => true,
]
);
@@ -405,7 +417,7 @@ class MenuListSeeder extends Seeder
'url' => null,
'icon' => 'cil-bell',
'component' => null,
'sort_order' => 9,
'sort_order' => 7,
'is_active' => true,
]
);