sample about material

This commit is contained in:
Wian Drs
2026-06-23 16:04:37 +07:00
parent b52e67c428
commit faab64e0dd
16 changed files with 848 additions and 0 deletions
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Material;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Material\AssignMaterialRequest;
use App\Http\Requests\Material\TransferMaterialRequest;
use App\Http\Requests\Material\ReturnMaterialRequest;
use App\Http\Resources\Material\MaterialResource;
use App\Services\Material\MaterialService;
class MaterialController extends ApiController
{
protected $service;
public function __construct(MaterialService $service)
{
$this->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'
);
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests\Material;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AssignMaterialRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|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',
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Material;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class ReturnMaterialRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'barcode_id' => 'required|string',
'user_id' => 'required|integer',
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Material;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class TransferMaterialRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'barcode_id' => 'required|string',
'from_user_id' => 'required|integer',
'to_user_id' => 'required|integer',
];
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Http\Resources\Material;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MaterialResource extends JsonResource
{
public function toArray($request)
{
return [
// universal
'barcode_id' => $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);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MaterialMovement extends Model
{
protected $table = 'material_movements';
public $timestamps = false;
protected $fillable = [
'barcode_id',
'user_id',
'from_user_id',
'to_user_id',
'ticket_id',
'movement_type',
'qty',
'description',
'created_by',
'created_at'
];
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TechnicianConsumable extends Model
{
protected $table = 'technician_consumables';
protected $fillable = [
'user_id',
'barcode_id',
'inventory_item_id',
'material_name',
'unit',
'qty_received',
'qty_used',
'qty_remaining',
'status',
'assigned_at',
'assigned_by'
];
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TechnicianMaterial extends Model
{
protected $table = 'technician_materials';
protected $fillable = [
'user_id',
'barcode_id',
'inventory_item_id',
'material_name',
'serial_number',
'status',
'assigned_at',
'assigned_by',
];
}
+232
View File
@@ -0,0 +1,232 @@
<?php
namespace App\Services\Material;
use App\Models\TechnicianMaterial;
use App\Models\TechnicianConsumable;
use App\Models\MaterialMovement;
use Carbon\Carbon;
use DB;
class MaterialService
{
/*
|--------------------------------------------------------------------------
| ASSIGN MATERIAL (AUTO DETECT TYPE)
|--------------------------------------------------------------------------
*/
public function assign($data)
{
return DB::transaction(function () use ($data) {
$type = $data['type'];
// type dari API Gudang: serialized / consumable
/*
|--------------------------------------------------------------------------
| SERIALIZED (ONT, Router, dll)
|--------------------------------------------------------------------------
*/
if ($type === 'serialized') {
$material = TechnicianMaterial::create([
'user_id' => $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,
];
});
}
}
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('technician_materials', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('technician_consumables', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ticket_consumables', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('material_movements', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,71 @@
<?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('ticket_materials', function (Blueprint $table) {
if (!Schema::hasColumn('ticket_materials', 'user_id')) {
$table->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'
]);
});
}
};
@@ -0,0 +1,39 @@
<?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('technician_consumables', function (Blueprint $table) {
// kalau sebelumnya status belum ada
$table->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');
});
}
};
+12
View File
@@ -6,6 +6,7 @@ use App\Http\Controllers\AuthController;
use App\Http\Controllers\Ticket\TicketController; use App\Http\Controllers\Ticket\TicketController;
use App\Http\Controllers\Ticket\TicketTypeController; use App\Http\Controllers\Ticket\TicketTypeController;
use App\Http\Controllers\Ticket\TicketWorkflowController; use App\Http\Controllers\Ticket\TicketWorkflowController;
use App\Http\Controllers\Material\MaterialController;
Route::post('/login', [AuthController::class, 'login']); Route::post('/login', [AuthController::class, 'login']);
@@ -55,5 +56,16 @@ Route::middleware('auth:sanctum')->group(function () {
Route::post('/escalate', 'escalate'); 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']);
});
}); });