From faab64e0dd91e84db01a4661063043015bdd1110 Mon Sep 17 00:00:00 2001 From: Wian Drs Date: Tue, 23 Jun 2026 16:04:37 +0700 Subject: [PATCH] sample about material --- .../Material/MaterialController.php | 70 ++++++ .../Material/AssignMaterialRequest.php | 37 +++ .../Material/ReturnMaterialRequest.php | 30 +++ .../Material/TransferMaterialRequest.php | 31 +++ .../Resources/Material/MaterialResource.php | 65 +++++ app/Models/MaterialMovement.php | 25 ++ app/Models/TechnicianConsumable.php | 24 ++ app/Models/TechnicianMaterial.php | 21 ++ app/Services/Material/MaterialService.php | 232 ++++++++++++++++++ ...2010_create_technician_materials_table.php | 50 ++++ ...32_create_technician_consumables_table.php | 46 ++++ ...142104_create_ticket_consumables_table.php | 44 ++++ ...142120_create_material_movements_table.php | 51 ++++ ...22_143308_alter_ticket_materials_table.php | 71 ++++++ ...954_alter_technician_consumables_table.php | 39 +++ routes/api.php | 12 + 16 files changed, 848 insertions(+) create mode 100644 app/Http/Controllers/Material/MaterialController.php create mode 100644 app/Http/Requests/Material/AssignMaterialRequest.php create mode 100644 app/Http/Requests/Material/ReturnMaterialRequest.php create mode 100644 app/Http/Requests/Material/TransferMaterialRequest.php create mode 100644 app/Http/Resources/Material/MaterialResource.php create mode 100644 app/Models/MaterialMovement.php create mode 100644 app/Models/TechnicianConsumable.php create mode 100644 app/Models/TechnicianMaterial.php create mode 100644 app/Services/Material/MaterialService.php create mode 100644 database/migrations/2026_06_22_142010_create_technician_materials_table.php create mode 100644 database/migrations/2026_06_22_142032_create_technician_consumables_table.php create mode 100644 database/migrations/2026_06_22_142104_create_ticket_consumables_table.php create mode 100644 database/migrations/2026_06_22_142120_create_material_movements_table.php create mode 100644 database/migrations/2026_06_22_143308_alter_ticket_materials_table.php create mode 100644 database/migrations/2026_06_22_160954_alter_technician_consumables_table.php diff --git a/app/Http/Controllers/Material/MaterialController.php b/app/Http/Controllers/Material/MaterialController.php new file mode 100644 index 00000000..930b40cd --- /dev/null +++ b/app/Http/Controllers/Material/MaterialController.php @@ -0,0 +1,70 @@ +service = $service; + } + + public function assign(AssignMaterialRequest $request) + { + $result = $this->service->assign($request->validated()); + + return $this->created( + new MaterialResource($result), + 'Material berhasil di-assign' + ); + } + + public function transfer(TransferMaterialRequest $request) + { + $result = $this->service->transfer($request->validated()); + + return $this->updated( + new MaterialResource($result), + 'Material berhasil di-transfer' + ); + } + + public function return(ReturnMaterialRequest $request) + { + $result = $this->service->return($request->validated()); + + return $this->updated( + new MaterialResource($result), + 'Material berhasil direturn' + ); + } + + public function listByUser($user_id) + { + $data = $this->service->listByUser($user_id); + + return $this->success( + \App\Http\Resources\Material\MaterialResource::collection($data), + 'List material user berhasil diambil' + ); + } + + public function history($barcode_id) + { + $data = $this->service->history($barcode_id); + + return $this->success( + $data, + 'History material berhasil diambil' + ); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Material/AssignMaterialRequest.php b/app/Http/Requests/Material/AssignMaterialRequest.php new file mode 100644 index 00000000..2dace0d6 --- /dev/null +++ b/app/Http/Requests/Material/AssignMaterialRequest.php @@ -0,0 +1,37 @@ +|string> + */ + public function rules(): array + { + return [ + 'user_id' => 'required|integer', + 'barcode_id' => 'required|string', + 'material_name' => 'required|string', + + 'type' => 'required|in:serialized,consumable', + + 'serial_number' => 'nullable|string', + 'qty' => 'nullable|numeric', + 'unit' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Material/ReturnMaterialRequest.php b/app/Http/Requests/Material/ReturnMaterialRequest.php new file mode 100644 index 00000000..895d67c7 --- /dev/null +++ b/app/Http/Requests/Material/ReturnMaterialRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'barcode_id' => 'required|string', + 'user_id' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/Material/TransferMaterialRequest.php b/app/Http/Requests/Material/TransferMaterialRequest.php new file mode 100644 index 00000000..b1063ab7 --- /dev/null +++ b/app/Http/Requests/Material/TransferMaterialRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'barcode_id' => 'required|string', + 'from_user_id' => 'required|integer', + 'to_user_id' => 'required|integer', + ]; + } +} diff --git a/app/Http/Resources/Material/MaterialResource.php b/app/Http/Resources/Material/MaterialResource.php new file mode 100644 index 00000000..f1bea20f --- /dev/null +++ b/app/Http/Resources/Material/MaterialResource.php @@ -0,0 +1,65 @@ + $this->barcode_id, + 'material_name' => $this->material_name, + 'user_id' => $this->user_id, + + // detect type model + 'type' => $this->detectType(), + + // serialized only + 'serial_number' => $this->when( + $this->isSerialized(), + $this->serial_number + ), + + // consumable only + 'qty' => $this->when( + $this->isConsumable(), + [ + 'received' => $this->qty_received ?? null, + 'used' => $this->qty_used ?? null, + 'remaining' => $this->qty_remaining ?? null, + 'unit' => $this->unit ?? null, + ] + ), + + // status (beda logic tapi tetap dipakai keduanya) + 'status' => $this->status ?? null, + + // optional tracking info + 'assigned_at' => $this->assigned_at ?? null, + ]; + } + + private function detectType(): string + { + // kalau punya qty field → consumable + if (isset($this->qty_received) || isset($this->qty_remaining)) { + return 'consumable'; + } + + return 'serialized'; + } + + private function isSerialized(): bool + { + return !isset($this->qty_received); + } + + private function isConsumable(): bool + { + return isset($this->qty_received); + } +} \ No newline at end of file diff --git a/app/Models/MaterialMovement.php b/app/Models/MaterialMovement.php new file mode 100644 index 00000000..43cde904 --- /dev/null +++ b/app/Models/MaterialMovement.php @@ -0,0 +1,25 @@ + $data['user_id'], + 'barcode_id' => $data['barcode_id'], + 'material_name' => $data['material_name'], + 'serial_number' => $data['serial_number'] ?? null, + 'status' => 'assigned', + 'assigned_at' => now(), + 'assigned_by' => auth()->id(), + ]); + + MaterialMovement::create([ + 'barcode_id' => $data['barcode_id'], + 'user_id' => $data['user_id'], + 'to_user_id' => $data['user_id'], + 'movement_type' => 'assign', + 'created_by' => auth()->id(), + 'created_at' => now(), + ]); + + return $material; + } + + /* + |-------------------------------------------------------------------------- + | CONSUMABLE (FO, kabel, dll) + |-------------------------------------------------------------------------- + */ + $material = TechnicianConsumable::create([ + 'user_id' => $data['user_id'], + 'barcode_id' => $data['barcode_id'], + 'material_name' => $data['material_name'], + 'unit' => $data['unit'] ?? 'meter', + 'qty_received' => $data['qty'] ?? 0, + 'qty_remaining' => $data['qty'] ?? 0, + 'qty_used' => 0, + 'status' => 'assigned', + 'assigned_at' => now(), + 'assigned_by' => auth()->id(), + ]); + + MaterialMovement::create([ + 'barcode_id' => $data['barcode_id'], + 'user_id' => $data['user_id'], + 'to_user_id' => $data['user_id'], + 'movement_type' => 'assign', + 'qty' => $data['qty'] ?? 0, + 'created_by' => auth()->id(), + 'created_at' => now(), + ]); + + return $material; + }); + } + + /* + |-------------------------------------------------------------------------- + | TRANSFER MATERIAL + |-------------------------------------------------------------------------- + */ + public function transfer($data) + { + return DB::transaction(function () use ($data) { + + $barcode = $data['barcode_id']; + + // cek serialized dulu + $serialized = TechnicianMaterial::where('barcode_id', $barcode)->first(); + + if ($serialized) { + + $serialized->update([ + 'user_id' => $data['to_user_id'] + ]); + + MaterialMovement::create([ + 'barcode_id' => $barcode, + 'from_user_id' => $data['from_user_id'], + 'to_user_id' => $data['to_user_id'], + 'movement_type' => 'transfer', + 'created_by' => auth()->id(), + 'created_at' => now(), + ]); + + return $serialized; + } + + // kalau bukan serialized → consumable + $consumable = TechnicianConsumable::where('barcode_id', $barcode)->first(); + + $consumable->update([ + 'user_id' => $data['to_user_id'] + ]); + + MaterialMovement::create([ + 'barcode_id' => $barcode, + 'from_user_id' => $data['from_user_id'], + 'to_user_id' => $data['to_user_id'], + 'movement_type' => 'transfer', + 'qty' => $consumable->qty_remaining, + 'created_by' => auth()->id(), + 'created_at' => now(), + ]); + + return $consumable; + }); + } + + /* + |-------------------------------------------------------------------------- + | RETURN MATERIAL + |-------------------------------------------------------------------------- + */ + public function return($data) + { + return DB::transaction(function () use ($data) { + + $barcode = $data['barcode_id']; + + $serialized = TechnicianMaterial::where('barcode_id', $barcode)->first(); + + if ($serialized) { + + $serialized->update([ + 'user_id' => null, + 'status' => 'returned' + ]); + + MaterialMovement::create([ + 'barcode_id' => $barcode, + 'user_id' => $data['user_id'], + 'from_user_id' => $data['user_id'], + 'movement_type' => 'return', + 'created_by' => auth()->id(), + 'created_at' => now(), + ]); + + return $serialized; + } + + $consumable = TechnicianConsumable::where('barcode_id', $barcode)->first(); + + $consumable->update([ + 'user_id' => null, + 'status' => 'returned' + ]); + + MaterialMovement::create([ + 'barcode_id' => $barcode, + 'user_id' => $data['user_id'], + 'from_user_id' => $data['user_id'], + 'movement_type' => 'return', + 'qty' => $consumable->qty_remaining, + 'created_by' => auth()->id(), + 'created_at' => now(), + ]); + + return $consumable; + }); + } + + public function listByUser($userId) + { + $serialized = TechnicianMaterial::where('user_id', $userId) + ->get() + ->map(function ($item) { + $item->type = 'serialized'; + return $item; + }); + + $consumable = TechnicianConsumable::where('user_id', $userId) + ->get() + ->map(function ($item) { + $item->type = 'consumable'; + return $item; + }); + + return $serialized + ->concat($consumable) + ->values(); + } + + public function history($barcodeId) + { + return MaterialMovement::where('barcode_id', $barcodeId) + ->orderBy('created_at', 'desc') + ->get() + ->map(function ($item) { + + return [ + 'barcode_id' => $item->barcode_id, + 'type' => $item->movement_type, + 'from_user_id' => $item->from_user_id, + 'to_user_id' => $item->to_user_id, + 'ticket_id' => $item->ticket_id, + 'qty' => $item->qty, + 'description' => $item->description, + 'created_at' => $item->created_at, + ]; + }); + } +} \ No newline at end of file diff --git a/database/migrations/2026_06_22_142010_create_technician_materials_table.php b/database/migrations/2026_06_22_142010_create_technician_materials_table.php new file mode 100644 index 00000000..ffd0476e --- /dev/null +++ b/database/migrations/2026_06_22_142010_create_technician_materials_table.php @@ -0,0 +1,50 @@ +id(); + + $table->unsignedBigInteger('user_id'); + + $table->string('barcode_id')->unique(); + $table->unsignedBigInteger('inventory_item_id')->nullable(); + + $table->string('material_name'); + $table->string('serial_number')->nullable(); + + $table->enum('status', [ + 'assigned', + 'used', + 'returned', + 'lost', + 'damaged' + ])->default('assigned'); + + $table->dateTime('assigned_at'); + $table->unsignedBigInteger('assigned_by')->nullable(); + + $table->timestamps(); + + $table->index('user_id'); + $table->index('barcode_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('technician_materials'); + } +}; diff --git a/database/migrations/2026_06_22_142032_create_technician_consumables_table.php b/database/migrations/2026_06_22_142032_create_technician_consumables_table.php new file mode 100644 index 00000000..55c4279b --- /dev/null +++ b/database/migrations/2026_06_22_142032_create_technician_consumables_table.php @@ -0,0 +1,46 @@ +id(); + + $table->unsignedBigInteger('user_id'); + + $table->string('barcode_id')->unique(); + $table->unsignedBigInteger('inventory_item_id')->nullable(); + + $table->string('material_name'); + $table->string('unit')->default('meter'); + + $table->decimal('qty_received', 10, 2)->default(0); + $table->decimal('qty_used', 10, 2)->default(0); + $table->decimal('qty_remaining', 10, 2)->default(0); + + $table->dateTime('assigned_at'); + $table->unsignedBigInteger('assigned_by')->nullable(); + + $table->timestamps(); + + $table->index('user_id'); + $table->index('barcode_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('technician_consumables'); + } +}; diff --git a/database/migrations/2026_06_22_142104_create_ticket_consumables_table.php b/database/migrations/2026_06_22_142104_create_ticket_consumables_table.php new file mode 100644 index 00000000..436c6204 --- /dev/null +++ b/database/migrations/2026_06_22_142104_create_ticket_consumables_table.php @@ -0,0 +1,44 @@ +id(); + + $table->unsignedBigInteger('ticket_id'); + $table->unsignedBigInteger('user_id'); + + $table->unsignedBigInteger('technician_consumable_id'); + + $table->string('barcode_id'); + $table->string('material_name'); + + $table->decimal('qty_used', 10, 2); + $table->string('unit')->default('meter'); + + $table->dateTime('used_at'); + + $table->timestamps(); + + $table->index('ticket_id'); + $table->index('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ticket_consumables'); + } +}; diff --git a/database/migrations/2026_06_22_142120_create_material_movements_table.php b/database/migrations/2026_06_22_142120_create_material_movements_table.php new file mode 100644 index 00000000..1fb6cfe6 --- /dev/null +++ b/database/migrations/2026_06_22_142120_create_material_movements_table.php @@ -0,0 +1,51 @@ +id(); + + $table->string('barcode_id'); + + $table->unsignedBigInteger('user_id')->nullable(); + $table->unsignedBigInteger('from_user_id')->nullable(); + $table->unsignedBigInteger('to_user_id')->nullable(); + + $table->unsignedBigInteger('ticket_id')->nullable(); + + $table->enum('movement_type', [ + 'assign', + 'transfer', + 'use', + 'return', + 'lost', + 'damaged' + ]); + + $table->decimal('qty', 10, 2)->nullable(); + + $table->text('description')->nullable(); + + $table->unsignedBigInteger('created_by')->nullable(); + + $table->dateTime('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('material_movements'); + } +}; diff --git a/database/migrations/2026_06_22_143308_alter_ticket_materials_table.php b/database/migrations/2026_06_22_143308_alter_ticket_materials_table.php new file mode 100644 index 00000000..a206b1c4 --- /dev/null +++ b/database/migrations/2026_06_22_143308_alter_ticket_materials_table.php @@ -0,0 +1,71 @@ +unsignedBigInteger('user_id')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'technician_material_id')) { + $table->unsignedBigInteger('technician_material_id')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'barcode_id')) { + $table->string('barcode_id')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'serial_number')) { + $table->string('serial_number')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'used_at')) { + $table->dateTime('used_at')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'ticket_log_id')) { + $table->unsignedBigInteger('ticket_log_id')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'qty')) { + $table->decimal('qty', 12, 2)->default(1); + } + + if (!Schema::hasColumn('ticket_materials', 'notes')) { + $table->text('notes')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'created_at')) { + $table->timestamp('created_at')->nullable(); + } + + if (!Schema::hasColumn('ticket_materials', 'updated_at')) { + $table->timestamp('updated_at')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('ticket_materials', function (Blueprint $table) { + + $table->dropColumn([ + 'user_id', + 'technician_material_id', + 'barcode_id', + 'serial_number', + 'used_at', + 'ticket_log_id', + 'qty', + 'notes' + ]); + }); + } +}; diff --git a/database/migrations/2026_06_22_160954_alter_technician_consumables_table.php b/database/migrations/2026_06_22_160954_alter_technician_consumables_table.php new file mode 100644 index 00000000..0297144c --- /dev/null +++ b/database/migrations/2026_06_22_160954_alter_technician_consumables_table.php @@ -0,0 +1,39 @@ +enum('status', [ + 'assigned', + 'used', + 'returned', + 'lost', + 'damaged' + ])->default('assigned')->after('qty_remaining'); + + $table->index('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('technician_consumables', function (Blueprint $table) { + $table->dropIndex(['status']); + $table->dropColumn('status'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 38bb28ef..96e5ac09 100644 --- a/routes/api.php +++ b/routes/api.php @@ -6,6 +6,7 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\Ticket\TicketController; use App\Http\Controllers\Ticket\TicketTypeController; use App\Http\Controllers\Ticket\TicketWorkflowController; +use App\Http\Controllers\Material\MaterialController; Route::post('/login', [AuthController::class, 'login']); @@ -55,5 +56,16 @@ Route::middleware('auth:sanctum')->group(function () { Route::post('/escalate', 'escalate'); }); + + Route::prefix('materials')->group(function () { + + Route::post('/assign', [MaterialController::class, 'assign']); + Route::post('/transfer', [MaterialController::class, 'transfer']); + Route::post('/return', [MaterialController::class, 'return']); + + Route::get('/user/{user_id}', [MaterialController::class, 'listByUser']); + Route::get('/history/{barcode_id}', [MaterialController::class, 'history']); + + }); });