chore: update document approval workflow and bug fixes

This commit is contained in:
2026-05-25 13:05:20 +08:00
parent d573c02893
commit 39a8e1d4cd
910 changed files with 49994 additions and 1010 deletions

View File

@@ -0,0 +1,72 @@
<?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
{
// Master material catalog
Schema::create('materials', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('sku')->unique()->nullable();
$table->string('category')->nullable(); // cement, steel, lumber, aggregate, etc.
$table->string('unit'); // pcs, kg, cubic_meter, bag, etc.
$table->decimal('unit_cost', 12, 2)->default(0);
$table->text('description')->nullable();
$table->string('status')->default('active'); // active, discontinued
$table->timestamps();
});
// Per-project inventory tracking
Schema::create('project_inventories', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->decimal('on_hand_qty', 12, 2)->default(0);
$table->decimal('allocated_qty', 12, 2)->default(0);
$table->decimal('consumed_qty', 12, 2)->default(0);
$table->decimal('unit_cost', 12, 2)->default(0); // cost at time of booking
$table->timestamps();
$table->unique(['project_id', 'material_id']);
});
// Material requirements per task
Schema::create('material_requirements', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('task_id')->nullable()->constrained('tasks')->nullOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->decimal('required_qty', 12, 2);
$table->decimal('unit_cost_at_booking', 12, 2)->default(0);
$table->string('status')->default('pending'); // pending, allocated, fulfilled
$table->timestamps();
});
// Material deployment tracking (request → dispatch → deliver → consume)
Schema::create('material_deployments', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete();
$table->decimal('quantity', 12, 2);
$table->string('status')->default('requested'); // requested, pending_dispatch, in_transit, delivered, consumed, cancelled
$table->text('notes')->nullable();
$table->timestamp('dispatched_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('consumed_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('material_deployments');
Schema::dropIfExists('material_requirements');
Schema::dropIfExists('project_inventories');
Schema::dropIfExists('materials');
}
};

View File

@@ -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::create('material_groups', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->string('name');
$table->text('description')->nullable();
$table->string('status')->default('active'); // active, archived
$table->timestamps();
});
Schema::create('material_group_items', function (Blueprint $table) {
$table->id();
$table->foreignId('material_group_id')->constrained('material_groups')->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->timestamps();
$table->unique(['material_group_id', 'material_id']);
});
Schema::create('project_material_groups', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_group_id')->constrained('material_groups')->cascadeOnDelete();
$table->timestamps();
$table->unique(['project_id', 'material_group_id']);
});
}
public function down(): void
{
Schema::dropIfExists('project_material_groups');
Schema::dropIfExists('material_group_items');
Schema::dropIfExists('material_groups');
}
};

View File

@@ -0,0 +1,76 @@
<?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('material_requisitions', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->string('document_number')->unique();
$table->string('status')->default('draft'); // draft, submitted, approved, rejected
$table->foreignId('requested_by')->constrained('users')->cascadeOnDelete();
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('approved_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
});
Schema::create('material_requisition_items', function (Blueprint $table) {
$table->id();
$table->foreignId('material_requisition_id')->constrained('material_requisitions')->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->decimal('quantity', 12, 2);
$table->decimal('unit_cost', 12, 2)->default(0);
$table->timestamps();
});
Schema::create('purchase_orders', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->string('document_number')->unique();
$table->string('supplier')->nullable();
$table->string('status')->default('draft'); // draft, submitted, approved, rejected
$table->foreignId('requested_by')->constrained('users')->cascadeOnDelete();
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('approved_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
});
Schema::create('purchase_order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('purchase_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_requisition_item_id')->nullable()
->constrained('material_requisition_items')->nullOnDelete();
$table->decimal('quantity', 12, 2);
$table->decimal('unit_cost', 12, 2)->default(0);
$table->timestamps();
});
// Pivot: 1:many MR → PO relationship
Schema::create('purchase_order_requisitions', function (Blueprint $table) {
$table->id();
$table->foreignId('purchase_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_requisition_id')->constrained('material_requisitions')->cascadeOnDelete();
$table->timestamps();
$table->unique(['purchase_order_id', 'material_requisition_id'], 'po_mr_unique');
});
}
public function down(): void
{
Schema::dropIfExists('purchase_order_requisitions');
Schema::dropIfExists('purchase_order_items');
Schema::dropIfExists('purchase_orders');
Schema::dropIfExists('material_requisition_items');
Schema::dropIfExists('material_requisitions');
}
};

View File

@@ -0,0 +1,107 @@
<?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
{
// Warehouse locations (main, yard, staging, etc.)
Schema::create('warehouses', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->string('name');
$table->string('code')->unique();
$table->text('address')->nullable();
$table->string('type')->default('main'); // main, yard, staging
$table->string('status')->default('active'); // active, inactive
$table->timestamps();
});
// Per-material stock per warehouse
Schema::create('warehouse_stock', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->decimal('quantity', 14, 2)->default(0);
$table->decimal('reserved_qty', 14, 2)->default(0);
$table->decimal('unit_cost', 12, 2)->default(0);
$table->timestamps();
$table->unique(['warehouse_id', 'material_id']);
});
// Full movement ledger — every qty change is a row
Schema::create('inventory_movements', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->foreignId('warehouse_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('project_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('task_id')->nullable()->constrained()->nullOnDelete();
$table->string('movement_type'); // purchase_receipt, dispatch, site_receipt, reserve, allocate, consume, return, adjustment
$table->string('direction'); // in, out
$table->decimal('quantity', 14, 2);
$table->decimal('balance_after', 14, 2)->default(0);
$table->string('from_location_type')->nullable(); // warehouse, project, task, external
$table->unsignedBigInteger('from_location_id')->nullable();
$table->string('to_location_type')->nullable();
$table->unsignedBigInteger('to_location_id')->nullable();
$table->nullableMorphs('reference'); // links to PO, MR, etc.
$table->foreignId('performed_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('notes')->nullable();
$table->timestamps();
$table->index(['material_id', 'movement_type']);
$table->index(['warehouse_id', 'created_at']);
$table->index(['project_id', 'created_at']);
});
// PO payment tracking
Schema::table('purchase_orders', function (Blueprint $table) {
$table->string('payment_status')->default('unpaid')->after('status'); // unpaid, partial, paid
$table->timestamp('paid_at')->nullable()->after('approved_at');
$table->string('receipt_path')->nullable()->after('notes');
$table->string('receipt_original_name')->nullable()->after('receipt_path');
});
// Task execution area
if (!Schema::hasColumn('tasks', 'area')) {
Schema::table('tasks', function (Blueprint $table) {
$table->string('area')->nullable()->after('description');
});
}
// Project inventory: floated qty tracking
if (!Schema::hasColumn('project_inventories', 'floated_qty')) {
Schema::table('project_inventories', function (Blueprint $table) {
$table->decimal('floated_qty', 12, 2)->default(0)->after('on_hand_qty');
});
}
}
public function down(): void
{
Schema::table('project_inventories', function (Blueprint $table) {
if (Schema::hasColumn('project_inventories', 'floated_qty')) {
$table->dropColumn('floated_qty');
}
});
Schema::table('tasks', function (Blueprint $table) {
if (Schema::hasColumn('tasks', 'area')) {
$table->dropColumn('area');
}
});
Schema::table('purchase_orders', function (Blueprint $table) {
$table->dropColumn(['payment_status', 'paid_at', 'receipt_path', 'receipt_original_name']);
});
Schema::dropIfExists('inventory_movements');
Schema::dropIfExists('warehouse_stock');
Schema::dropIfExists('warehouses');
}
};

View File

@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('inventory_movements', function (Blueprint $table) {
$table->foreignId('dispatched_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('received_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('returned_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('adjusted_by')->nullable()->constrained('users')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('inventory_movements', function (Blueprint $table) {
$table->dropForeign(['dispatched_by']);
$table->dropForeign(['received_by']);
$table->dropForeign(['returned_by']);
$table->dropForeign(['adjusted_by']);
$table->dropColumn([
'dispatched_by',
'received_by',
'returned_by',
'adjusted_by',
]);
});
}
};

View 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::table('material_requisitions', function (Blueprint $table) {
$table->dropForeign(['project_id']);
$table->dropColumn('project_id');
});
Schema::table('purchase_orders', function (Blueprint $table) {
$table->dropForeign(['project_id']);
$table->dropColumn('project_id');
});
}
public function down(): void
{
Schema::table('material_requisitions', function (Blueprint $table) {
$table->foreignId('project_id')->nullable()->constrained()->cascadeOnDelete();
});
Schema::table('purchase_orders', function (Blueprint $table) {
$table->foreignId('project_id')->nullable()->constrained()->cascadeOnDelete();
});
}
};

View File

@@ -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('inventory_batches', function (Blueprint $table) {
$table->id();
$table->string('ulid', 26)->unique();
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_id')->constrained()->cascadeOnDelete();
$table->foreignId('material_requisition_id')->nullable()->constrained('material_requisitions')->nullOnDelete();
$table->foreignId('purchase_order_id')->nullable()->constrained('purchase_orders')->nullOnDelete();
$table->decimal('quantity', 12, 2)->default(0);
$table->decimal('unit_cost', 12, 2)->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('inventory_batches');
}
};

View 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::table('inventory_movements', function (Blueprint $table) {
$table->foreignId('inventory_batch_id')->nullable()->constrained('inventory_batches')->nullOnDelete();
});
Schema::table('material_deployments', function (Blueprint $table) {
$table->foreignId('material_requisition_id')->nullable()->constrained('material_requisitions')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('inventory_movements', function (Blueprint $table) {
$table->dropForeign(['inventory_batch_id']);
$table->dropColumn('inventory_batch_id');
});
Schema::table('material_deployments', function (Blueprint $table) {
$table->dropForeign(['material_requisition_id']);
$table->dropColumn('material_requisition_id');
});
}
};

View File

@@ -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('warehouses', function (Blueprint $table) {
if (!Schema::hasColumn('warehouses', 'project_id')) {
$table->foreignId('project_id')->nullable()->after('id')->constrained()->nullOnDelete();
}
});
}
public function down(): void
{
Schema::table('warehouses', function (Blueprint $table) {
if (Schema::hasColumn('warehouses', 'project_id')) {
$table->dropForeign(['project_id']);
$table->dropColumn('project_id');
}
});
}
};

View File

@@ -0,0 +1,50 @@
<?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('material_transfers', function (Blueprint $table) {
$table->id();
$table->string('transfer_number')->unique();
$table->foreignId('from_project_id')->constrained('projects');
$table->foreignId('to_project_id')->constrained('projects');
$table->string('status')->default('draft');
$table->foreignId('requested_by')->constrained('users');
$table->foreignId('approved_by')->nullable()->constrained('users');
$table->foreignId('dispatched_by')->nullable()->constrained('users');
$table->foreignId('received_by')->nullable()->constrained('users');
$table->text('notes')->nullable();
$table->timestamps();
});
Schema::create('material_transfer_items', function (Blueprint $table) {
$table->id();
$table->foreignId('material_transfer_id')->constrained('material_transfers')->cascadeOnDelete();
$table->foreignId('material_id')->constrained('materials');
$table->decimal('requested_quantity', 10, 2);
$table->decimal('dispatched_quantity', 10, 2)->default(0);
$table->decimal('received_quantity', 10, 2)->default(0);
$table->decimal('damaged_quantity', 10, 2)->default(0);
$table->decimal('lost_quantity', 10, 2)->default(0);
$table->text('notes')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('material_transfer_items');
Schema::dropIfExists('material_transfers');
}
};

View File

@@ -0,0 +1,106 @@
<?php
namespace Modules\MaterialLogistics\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
use Modules\MasterData\Models\Material;
class ConstructionMaterialsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$materials = [
// Concrete & Cement
['name' => 'Portland Cement Type I', 'category' => 'Concrete & Cement', 'unit' => 'Bag (40kg)', 'unit_cost' => 8.50, 'description' => 'General purpose portland cement.'] ,
['name' => 'Portland Cement Type III', 'category' => 'Concrete & Cement', 'unit' => 'Bag (40kg)', 'unit_cost' => 10.00, 'description' => 'High early strength portland cement.'],
['name' => 'Ready Mix Concrete (20 MPa)', 'category' => 'Concrete & Cement', 'unit' => 'Cubic Meter', 'unit_cost' => 110.00, 'description' => 'Standard strength ready mix concrete.'],
['name' => 'Ready Mix Concrete (30 MPa)', 'category' => 'Concrete & Cement', 'unit' => 'Cubic Meter', 'unit_cost' => 130.00, 'description' => 'High strength ready mix concrete.'],
['name' => 'Concrete Blocks (Hollow) 8x8x16', 'category' => 'Concrete & Cement', 'unit' => 'Piece', 'unit_cost' => 1.50, 'description' => 'Standard 8-inch hollow concrete block.'],
// Aggregates
['name' => 'River Sand', 'category' => 'Aggregates', 'unit' => 'Ton', 'unit_cost' => 20.00, 'description' => 'Washed river sand for mixing.'],
['name' => 'Crushed Stone (Gravel) 3/4"', 'category' => 'Aggregates', 'unit' => 'Ton', 'unit_cost' => 25.00, 'description' => '3/4 inch curshed limestone base.'],
['name' => 'Pea Gravel', 'category' => 'Aggregates', 'unit' => 'Ton', 'unit_cost' => 30.00, 'description' => 'Smooth round pea gravel.'],
['name' => 'Base Gravel (Class 5)', 'category' => 'Aggregates', 'unit' => 'Ton', 'unit_cost' => 18.00, 'description' => 'Class 5 aggregate base course material.'],
// Steel & Rebar
['name' => 'Rebar #4 (1/2" x 20 ft)', 'category' => 'Steel & Metal', 'unit' => 'Piece', 'unit_cost' => 6.20, 'description' => 'Standard #4 reinforcing bar, 20 feet length.'],
['name' => 'Rebar #5 (5/8" x 20 ft)', 'category' => 'Steel & Metal', 'unit' => 'Piece', 'unit_cost' => 9.50, 'description' => 'Standard #5 reinforcing bar.'],
['name' => 'Wire Mesh (6x6 W1.4/W1.4) 150ft Roll', 'category' => 'Steel & Metal', 'unit' => 'Roll', 'unit_cost' => 125.00, 'description' => 'Welded wire mesh for concrete slabs.'],
['name' => 'Structural Steel Beam (W8x10)', 'category' => 'Steel & Metal', 'unit' => 'Foot', 'unit_cost' => 15.00, 'description' => 'W8x10 wide flange steel beam.'],
['name' => 'Tie Wire (Black Annealed) 16 Gauge', 'category' => 'Steel & Metal', 'unit' => 'Roll (3 lb)', 'unit_cost' => 8.00, 'description' => 'Tie wire for rebar.'],
// Timber & Wood
['name' => '2x4 Framing Lumber (8 ft)', 'category' => 'Timber & Wood', 'unit' => 'Piece', 'unit_cost' => 3.50, 'description' => 'Standard SPF 2x4 framing stud.'],
['name' => '2x6 Framing Lumber (10 ft)', 'category' => 'Timber & Wood', 'unit' => 'Piece', 'unit_cost' => 6.00, 'description' => 'Standard 2x6 timber.'],
['name' => 'Plywood (CDX) 15/32" (4x8 ft)', 'category' => 'Timber & Wood', 'unit' => 'Sheet', 'unit_cost' => 22.00, 'description' => 'CDX structural plywood panel.'],
['name' => 'OSB Sheathing 7/16" (4x8 ft)', 'category' => 'Timber & Wood', 'unit' => 'Sheet', 'unit_cost' => 14.50, 'description' => 'Oriented strand board.'],
['name' => 'Treated Timber 4x4 (8 ft)', 'category' => 'Timber & Wood', 'unit' => 'Piece', 'unit_cost' => 11.20, 'description' => 'Pressure treated 4x4 post.'],
// Masonry & Bricks
['name' => 'Red Clay Brick', 'category' => 'Masonry', 'unit' => 'Thousand', 'unit_cost' => 450.00, 'description' => 'Standard solid red clay brick.'],
['name' => 'Mortar Mix (Type S)', 'category' => 'Masonry', 'unit' => 'Bag (80 lb)', 'unit_cost' => 9.00, 'description' => 'High strength masonry mortar.'],
['name' => 'Grout Mix (Non-Shrink)', 'category' => 'Masonry', 'unit' => 'Bag (50 lb)', 'unit_cost' => 14.00, 'description' => 'Structural non-shrink grout.'],
// Roofing
['name' => 'Asphalt Shingles (Architectural)', 'category' => 'Roofing', 'unit' => 'Bundle (33 sq ft)', 'unit_cost' => 35.00, 'description' => 'High quality architectural asphalt shingle.'],
['name' => 'Roofing Felt (15 lb, 400 sq ft)', 'category' => 'Roofing', 'unit' => 'Roll', 'unit_cost' => 20.00, 'description' => 'Asphalt saturated roofing felt base.'],
['name' => 'Corrugated Metal Roof Panel (12 ft)', 'category' => 'Roofing', 'unit' => 'Piece', 'unit_cost' => 28.00, 'description' => 'Galvanized iron corrugated sheet.'],
['name' => 'Roofing Nails (1-1/4", 50 lb)', 'category' => 'Roofing', 'unit' => 'Box', 'unit_cost' => 45.00, 'description' => 'Galvanized roofing nails.'],
// Insulation & Drywall
['name' => 'Fiberglass Insulation (R-13) 15" x 32\'', 'category' => 'Insulation', 'unit' => 'Roll', 'unit_cost' => 22.00, 'description' => 'Unfaced fiberglass insulation batt.'],
['name' => 'Rigid Foam Insulation 1" (4x8 ft)', 'category' => 'Insulation', 'unit' => 'Sheet', 'unit_cost' => 16.50, 'description' => 'Extruded polystyrene (XPS) rigid foam.'],
['name' => 'Drywall (Gypsum Board) 1/2" (4x8 ft)', 'category' => 'Drywall', 'unit' => 'Sheet', 'unit_cost' => 13.00, 'description' => 'Standard interior drywall.'],
['name' => 'Joint Compound (All-Purpose) 4.5 Gal', 'category' => 'Drywall', 'unit' => 'Bucket', 'unit_cost' => 16.00, 'description' => 'Ready-mixed joint compound.'],
['name' => 'Drywall Screws (1-1/4", 5 lb)', 'category' => 'Drywall', 'unit' => 'Box', 'unit_cost' => 18.00, 'description' => 'Coarse thread phosphate drywall screws.'],
// Plumbing & Piping
['name' => 'PVC Pipe SCH 40 (2" x 10 ft)', 'category' => 'Plumbing', 'unit' => 'Piece', 'unit_cost' => 15.00, 'description' => 'Schedule 40 PVC pipe.'],
['name' => 'PVC Pipe SCH 40 (4" x 10 ft)', 'category' => 'Plumbing', 'unit' => 'Piece', 'unit_cost' => 26.00, 'description' => 'Schedule 40 PVC pipe for drainage.'],
['name' => 'Copper Pipe Type L (1/2" x 10 ft)', 'category' => 'Plumbing', 'unit' => 'Piece', 'unit_cost' => 24.00, 'description' => 'Standard soft/hard copper piping.'],
['name' => 'PVC Cement & Primer Pack', 'category' => 'Plumbing', 'unit' => 'Pack', 'unit_cost' => 12.00, 'description' => 'Clear PVC cement and purple primer.'],
// Electrical
['name' => 'NM-B Wire 12/2 (250 ft Roll)', 'category' => 'Electrical', 'unit' => 'Roll', 'unit_cost' => 85.00, 'description' => 'Romex 12/2 residential wire w/ ground.'],
['name' => 'THHN Wire 10 AWG (500 ft Spool)', 'category' => 'Electrical', 'unit' => 'Spool', 'unit_cost' => 115.00, 'description' => 'Stranded copper building wire.'],
['name' => 'Electrical Box (1-Gang, 18 cu in)', 'category' => 'Electrical', 'unit' => 'Piece', 'unit_cost' => 0.85, 'description' => 'Nail-on PVC electrical box.'],
['name' => 'Conduit PVC SCH 40 (3/4" x 10 ft)', 'category' => 'Electrical', 'unit' => 'Piece', 'unit_cost' => 3.50, 'description' => 'Electrical PVC conduit.'],
// Paint & Finishes
['name' => 'Latex Primer (Interior) 5 Gal', 'category' => 'Paint', 'unit' => 'Bucket', 'unit_cost' => 55.00, 'description' => 'White interior latex drywall primer.'],
['name' => 'Interior Flat Paint (White) 5 Gal', 'category' => 'Paint', 'unit' => 'Bucket', 'unit_cost' => 70.00, 'description' => 'Professional grade flat white.'],
['name' => 'Exterior Semi-Gloss Paint 5 Gal', 'category' => 'Paint', 'unit' => 'Bucket', 'unit_cost' => 95.00, 'description' => 'Durable exterior grade semi-gloss paint.'],
// Consumables & Tools
['name' => 'Construction Adhesive (10 oz)', 'category' => 'Consumables', 'unit' => 'Tube', 'unit_cost' => 4.50, 'description' => 'Heavy duty polyurethane adhesive.'],
['name' => 'Painter\'s Tape (1.88" x 60 yds)', 'category' => 'Consumables', 'unit' => 'Roll', 'unit_cost' => 6.00, 'description' => 'Blue masking tape.'],
['name' => 'Work Gloves (Leather Palm)', 'category' => 'PPE', 'unit' => 'Pair', 'unit_cost' => 3.00, 'description' => 'Standard heavy-duty work gloves.'],
['name' => 'Safety Glasses', 'category' => 'PPE', 'unit' => 'Piece', 'unit_cost' => 4.00, 'description' => 'Clear polycarbonate safety glasses.'],
];
foreach ($materials as $materialData) {
$baseSku = strtoupper(substr($materialData['category'], 0, 3)) . '-' . rand(1000, 9999);
// Ensure unique sku
while(Material::where('sku', $baseSku)->exists()) {
$baseSku = strtoupper(substr($materialData['category'], 0, 3)) . '-' . rand(1000, 9999);
}
Material::firstOrCreate(
['name' => $materialData['name']],
[
'sku' => $baseSku,
'category' => $materialData['category'],
'unit' => $materialData['unit'],
'unit_cost' => $materialData['unit_cost'],
'description' => $materialData['description'],
'status' => 'active'
]
);
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Modules\MaterialLogistics\Database\Seeders;
use Illuminate\Database\Seeder;
class MaterialLogisticsDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$this->call([
ConstructionMaterialsSeeder::class,
]);
}
}