feat(admin): IP pool allocation, billing exports, service attachments
- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea5697cb81
commit
46aa138eec
@@ -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
|
||||
{
|
||||
Schema::create('service_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
|
||||
$table->string('filename');
|
||||
$table->string('path');
|
||||
$table->string('disk', 50)->default('public');
|
||||
$table->string('mime_type', 100)->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->string('uploaded_by', 100)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('service_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$userIds = DB::table('user')->pluck('id', 'username');
|
||||
|
||||
DB::table('log')
|
||||
->where('description', 'like', '% created new Order with Order id: %')
|
||||
->where('username', '!=', 'System')
|
||||
->orderBy('id')
|
||||
->each(function ($log) use ($userIds) {
|
||||
$userId = $userIds[$log->username] ?? null;
|
||||
if (! $userId || ! preg_match('/Order id: (\d+)$/', $log->description, $match)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('orders')
|
||||
->where('id', (int) $match[1])
|
||||
->whereNull('user_id')
|
||||
->update(['user_id' => $userId]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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
|
||||
{
|
||||
if (! Schema::hasColumn('user', 'fullname')) {
|
||||
Schema::table('user', function (Blueprint $table) {
|
||||
$table->string('fullname', 255)->nullable()->after('id');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasColumns('user', ['firstname', 'lastname'])) {
|
||||
DB::table('user')
|
||||
->where(fn ($query) => $query->whereNull('fullname')->orWhere('fullname', ''))
|
||||
->update(['fullname' => DB::raw("TRIM(CONCAT(COALESCE(firstname, ''), ' ', COALESCE(lastname, '')))")]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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('ip_pools', function (Blueprint $table) {
|
||||
$table->string('ip_block')->nullable()->after('ip_address');
|
||||
$table->unsignedBigInteger('module_network_id')->nullable()->change();
|
||||
});
|
||||
|
||||
DB::table('ip_pools')
|
||||
->where('status', 'free')
|
||||
->whereNull('service_id')
|
||||
->whereRaw("UPPER(TRIM(allocation_status)) = 'ALLOCATED'")
|
||||
->update(['status' => 'assigned']);
|
||||
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->dropColumn('allocation_status');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->string('allocation_status')->nullable()->after('revoked_at');
|
||||
$table->dropColumn('ip_block');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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('ip_pools', function (Blueprint $table) {
|
||||
$table->foreignId('customer_id')->nullable()->after('customer_name')->constrained('customers')->nullOnDelete();
|
||||
});
|
||||
|
||||
Schema::create('ip_pool_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ip_pool_id')->constrained('ip_pools')->cascadeOnDelete();
|
||||
$table->string('ip_address', 45);
|
||||
$table->string('action', 30)->index();
|
||||
$table->string('status_from', 20)->nullable();
|
||||
$table->string('status_to', 20)->nullable();
|
||||
$table->foreignId('customer_id')->nullable()->constrained('customers')->nullOnDelete();
|
||||
$table->string('customer_name')->nullable();
|
||||
$table->unsignedBigInteger('service_id')->nullable();
|
||||
$table->string('vm_name')->nullable();
|
||||
$table->text('email_to')->nullable();
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ip_pool_logs');
|
||||
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('customer_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$seeder = new TemplateSeeder();
|
||||
$seeder->seedScenarioIfMissing('ip_allocated');
|
||||
$seeder->seedScenarioIfMissing('ip_revoked');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('template')->whereIn('scenario', ['ip_allocated', 'ip_revoked'])->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
(new TemplateSeeder())->reseedScenario('ip_allocated');
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?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('ip_pool_logs', function (Blueprint $table) {
|
||||
$table->date('effective_date')->nullable()->after('action');
|
||||
$table->string('service_name')->nullable()->after('customer_name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ip_pool_logs', function (Blueprint $table) {
|
||||
$table->dropColumn(['effective_date', 'service_name']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user