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,33 @@
<?php
namespace App\Http\Controllers\Warehouse;
use App\Http\Controllers\ApiController;use App\Models\{ItemBarcode,ItemCode,ItemMovement,ItemStockLog,WarehouseItem};use Illuminate\Http\Request;use Illuminate\Support\Facades\DB;use Illuminate\Support\Str;use Illuminate\Validation\Rule;
class WarehouseBatchController extends ApiController
{
public function index(Request $r){$t=$r->attributes->get('tenant_id');$status=$r->input('status','active');$q=WarehouseItem::with(['warehouse:id,code,name','itemCode','barcodes'])->when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->where('status',$status)->when($r->search,fn($x,$s)=>$x->where(fn($y)=>$y->where('item_number','ilike',"%$s%")->orWhereHas('itemCode',fn($z)=>$z->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%"))))->orderByDesc('id')->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Daftar barang berhasil diambil');}
public function store(Request $r)
{
$t=$r->user()->isMasterAdmin()?($r->input('tenant_id')?:$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');
$d=$r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'warehouse_id'=>['required','integer','exists:warehouses,id'],'item_code_id'=>['required','integer','exists:item_codes,id'],'name'=>['required','string','max:255'],'quantity'=>['required','numeric','gt:0'],'package_count'=>['nullable','integer','min:1'],'received_at'=>['required','date'],'notes'=>['nullable','string']]);
$code=ItemCode::whereKey($d['item_code_id'])->where('tenant_id',$t)->firstOrFail();
abort_unless(\App\Models\Warehouse::whereKey($d['warehouse_id'])->where('tenant_id',$t)->exists(),422,'Gudang bukan milik tenant aktif.');
return DB::transaction(function()use($r,$d,$t,$code){
$base=$code->code.'-'.now()->format('dmy');
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))',[$t.'|'.$base]);
$seq=WarehouseItem::where('tenant_id',$t)->where('item_number','like',$base.'%')->count()+1;
do{$number=$base.'-'.str_pad((string)$seq++,3,'0',STR_PAD_LEFT);}while(WarehouseItem::where('tenant_id',$t)->where('item_number',$number)->exists());
$item=WarehouseItem::create(['tenant_id'=>$t,'warehouse_id'=>$d['warehouse_id'],'item_code_id'=>$code->id,'item_number'=>$number,'status'=>'draft','sku'=>$code->code,'name'=>$d['name'],'tracking_type'=>$code->tracking_type==='serialized'?'serialized':'bulk','unit'=>$code->unit,'minimum_stock'=>$code->minimum_stock,'received_qty'=>$d['quantity'],'received_at'=>$d['received_at'],'notes'=>$d['notes']??null,'is_active'=>false]);
$count=$code->tracking_type==='serialized'?(int)$d['quantity']:max(1,(int)($d['package_count']??1));
abort_if($code->tracking_type==='serialized'&&(float)$d['quantity']!==floor((float)$d['quantity']),422,'Barang serialized harus berjumlah bilangan bulat.');
$remaining=(float)$d['quantity'];
for($i=1;$i<=$count;$i++){$qty=$code->tracking_type==='serialized'?1:($i===$count?$remaining:round((float)$d['quantity']/$count,2));$remaining-=$qty;ItemBarcode::create(['tenant_id'=>$t,'warehouse_item_id'=>$item->id,'package_number'=>$i,'barcode'=>$this->barcode($code->code,$t),'tracking_type'=>$code->tracking_type,'unit'=>$code->unit,'initial_qty'=>$qty,'remaining_qty'=>$qty,'status'=>'draft']);}
return $this->created($item->load(['warehouse','itemCode','barcodes']),'Barang draft dan barcode berhasil dibuat');
});
}
public function show(Request $r,WarehouseItem $item){$this->auth($r,$item);return $this->success($item->load(['warehouse','itemCode','barcodes','movements']),'Detail barang berhasil diambil');}
public function updateSerials(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_unless($item->status==='draft',422,'Serial number hanya dapat diubah pada draft.');$d=$r->validate(['barcodes'=>['required','array','min:1'],'barcodes.*.id'=>['required','integer'],'barcodes.*.serial_number'=>['nullable','string','max:120']]);DB::transaction(function()use($d,$item){foreach($d['barcodes'] as $row){$barcode=$item->barcodes()->findOrFail($row['id']);$serial=$row['serial_number']?:null;if($serial&&ItemBarcode::where('tenant_id',$item->tenant_id)->where('serial_number',$serial)->whereKeyNot($barcode->id)->exists())abort(422,"Serial number {$serial} sudah digunakan.");$barcode->update(['serial_number'=>$serial]);}});return $this->updated($item->fresh()->load('barcodes'),'Serial number berhasil diperbarui');}
public function post(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_unless($item->status==='draft',422,'Hanya barang draft yang dapat diposting.');$item->load(['itemCode','barcodes']);abort_if($item->barcodes->isEmpty(),422,'Barcode belum tersedia.');if($item->itemCode->need_serial_number)abort_if($item->barcodes->contains(fn($b)=>blank($b->serial_number)),422,'Semua serial number wajib diisi sebelum posting.');return DB::transaction(function()use($r,$item){$item->barcodes()->update(['status'=>'available']);$item->update(['status'=>'active','is_active'=>true,'posted_by'=>$r->user()->id,'posted_at'=>now()]);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'movement_type'=>'stock_posted','qty'=>$item->received_qty,'notes'=>'Draft diposting menjadi stok aktif','created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_movement_id'=>$move->id,'qty_before'=>0,'qty_change'=>$item->received_qty,'qty_after'=>$item->received_qty]);return $this->updated($item->fresh()->load(['warehouse','itemCode','barcodes']),'Barang berhasil diposting ke Daftar Barang');});}
public function destroy(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_unless($item->status==='draft',422,'Hanya barang draft yang dapat dihapus.');$item->delete();return $this->deleted('Draft barang dan barcode berhasil dihapus');}
public function barcodeMovements(Request $r,WarehouseItem $item,ItemBarcode $barcode){$this->auth($r,$item);abort_unless((int)$barcode->warehouse_item_id===(int)$item->id,404);return $this->success(ItemMovement::where('item_barcode_id',$barcode->id)->latest()->get(),'Histori barcode berhasil diambil');}
private function barcode(string $code,int $tenant):string{do{$random='';$chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789';for($i=0;$i<6;$i++)$random.=$chars[random_int(0,strlen($chars)-1)];$value=$code.'-'.now()->format('dmy').'-'.$random;}while(ItemBarcode::where('barcode',$value)->exists());return $value;}
private function auth(Request $r,WarehouseItem $i){abort_unless($r->user()->isMasterAdmin()||(int)$i->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Warehouse;
use App\Http\Controllers\ApiController;use App\Models\{WarehouseItem,ItemBarcode,ItemCode,ItemMovement,ItemStockLog};use Illuminate\Http\Request;use Illuminate\Support\Facades\DB;use Illuminate\Validation\Rule;
class WarehouseController extends ApiController
{
public function index(Request $r){$t=$r->attributes->get('tenant_id');$q=WarehouseItem::with(['itemCode','barcodes'])->when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->when($r->search,fn($x,$s)=>$x->whereHas('itemCode',fn($y)=>$y->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%")))->orderBy($r->input('sort_by','id'),$r->input('sort_direction','desc'))->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Daftar barang berhasil diambil');}
public function store(Request $r){$d=$this->validateItem($r);$d['tenant_id']=$r->user()->isMasterAdmin()?($d['tenant_id']??$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');return $this->created(WarehouseItem::create($d),'Barang berhasil dibuat');}
public function update(Request $r,WarehouseItem $item){$this->auth($r,$item);$item->update($this->validateItem($r,$item));return $this->updated($item->fresh(),'Barang berhasil diperbarui');}
public function destroy(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_if($item->barcodes()->where('remaining_qty','>',0)->exists(),422,'Barang yang masih memiliki stok tidak dapat dihapus.');$item->delete();return $this->deleted('Barang berhasil dihapus');}
public function receive(Request $r,WarehouseItem $item){$this->auth($r,$item);$item->loadMissing('itemCode');$type=$item->itemCode?->tracking_type??($item->tracking_type==='serialized'?'serialized':'quantity');$needSerial=(bool)($item->itemCode?->need_serial_number);$d=$r->validate(['barcode'=>['required','string','max:120','unique:item_barcodes,barcode'],'serial_number'=>[$needSerial?'required':'nullable','string','max:120'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string']]);if($type==='serialized')$d['qty']=1;return DB::transaction(function()use($r,$item,$d,$type){$before=(float)$item->barcodes()->sum('remaining_qty');$barcode=ItemBarcode::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'barcode'=>$d['barcode'],'tracking_type'=>$type,'unit'=>$item->itemCode?->unit??$item->unit,'serial_number'=>$d['serial_number']??null,'initial_qty'=>$d['qty'],'remaining_qty'=>$d['qty'],'status'=>'available']);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'movement_type'=>'stock_in','qty'=>$d['qty'],'notes'=>$d['notes']??null,'created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'item_movement_id'=>$move->id,'qty_before'=>$before,'qty_change'=>$d['qty'],'qty_after'=>$before+$d['qty'],'barcode_qty_before'=>0,'barcode_qty_change'=>$d['qty'],'barcode_qty_after'=>$d['qty']]);return $this->created($barcode,'Stok masuk berhasil dicatat');});}
public function options(Request $r){$t=$r->attributes->get('tenant_id');$users=\App\Models\UserTenant::with('user:id,name')->where('tenant_id',$t)->get()->pluck('user')->filter()->unique('id')->values();$codes=ItemCode::where('tenant_id',$t)->where('is_active',true)->orderBy('name')->get();return $this->success(['users'=>$users,'item_codes'=>$codes],'Pilihan gudang berhasil diambil');}
public function issue(Request $r,ItemBarcode $barcode){$item=$barcode->item;$this->auth($r,$item);$d=$r->validate(['user_id'=>['required','integer','exists:users,id'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]);abort_unless(\App\Models\UserTenant::where('tenant_id',$item->tenant_id)->where('user_id',$d['user_id'])->exists(),422,'Penerima bukan user tenant ini.');return DB::transaction(function()use($r,$item,$barcode,$d){$locked=ItemBarcode::whereKey($barcode->id)->lockForUpdate()->firstOrFail();$barcodeBefore=(float)$locked->remaining_qty;$serialized=($locked->tracking_type??$item->tracking_type)==='serialized';$qty=$serialized?1:(float)$d['qty'];abort_if($barcodeBefore<$qty,422,'Stok barcode tidak mencukupi.');$before=(float)$item->barcodes()->sum('remaining_qty');if($serialized){abort_if(\App\Models\TechnicianMaterial::where('barcode_id',$locked->barcode)->exists(),422,'Barcode sudah pernah dikeluarkan.');\App\Models\TechnicianMaterial::create(['user_id'=>$d['user_id'],'barcode_id'=>$locked->barcode,'inventory_item_id'=>$item->id,'material_name'=>$item->name,'serial_number'=>$locked->serial_number,'status'=>'assigned','assigned_at'=>now(),'assigned_by'=>$r->user()->id]);}else{abort_if(\App\Models\TechnicianConsumable::where('barcode_id',$locked->barcode)->exists(),422,'Satu barcode quantity hanya dapat diserahkan kepada satu user sampai dikembalikan.');\App\Models\TechnicianConsumable::create(['user_id'=>$d['user_id'],'barcode_id'=>$locked->barcode,'inventory_item_id'=>$item->id,'material_name'=>$item->name,'unit'=>$locked->unit??$item->unit,'qty_received'=>$qty,'qty_used'=>0,'qty_remaining'=>$qty,'status'=>'assigned','assigned_at'=>now(),'assigned_by'=>$r->user()->id]);}$barcodeAfter=$barcodeBefore-$qty;$locked->update(['remaining_qty'=>$barcodeAfter,'status'=>$barcodeAfter>0?'available':'issued']);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$locked->id,'movement_type'=>'issue_to_technician','qty'=>$qty,'to_user_id'=>$d['user_id'],'notes'=>$d['notes']??null,'created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$locked->id,'item_movement_id'=>$move->id,'qty_before'=>$before,'qty_change'=>-$qty,'qty_after'=>$before-$qty,'barcode_qty_before'=>$barcodeBefore,'barcode_qty_change'=>-$qty,'barcode_qty_after'=>$barcodeAfter]);return $this->created($move,'Barang berhasil dikeluarkan kepada user');});}
public function movements(Request $r){$t=$r->attributes->get('tenant_id');return $this->successPaginated(ItemMovement::when(!$r->user()->isMasterAdmin()||$t,fn($q)=>$q->where('tenant_id',$t))->latest()->paginate(min((int)$r->input('per_page',20),100)),\Illuminate\Http\Resources\Json\JsonResource::class,'Pergerakan barang berhasil diambil');}
public function technicianInventory(Request $r){$t=$r->attributes->get('tenant_id');$userIds=\App\Models\UserTenant::where('tenant_id',$t)->pluck('user_id');$serialized=\App\Models\TechnicianMaterial::with('user:id,name')->whereIn('user_id',$userIds)->where('status','assigned')->get()->map(fn($x)=>['id'=>'s-'.$x->id,'user'=>$x->user?->name,'material_name'=>$x->material_name,'barcode'=>$x->barcode_id,'tracking_type'=>'serialized','qty'=>1,'unit'=>'pcs','status'=>$x->status,'assigned_at'=>$x->assigned_at]);$quantity=\App\Models\TechnicianConsumable::with('user:id,name')->whereIn('user_id',$userIds)->where('status','assigned')->where('qty_remaining','>',0)->get()->map(fn($x)=>['id'=>'q-'.$x->id,'user'=>$x->user?->name,'material_name'=>$x->material_name,'barcode'=>$x->barcode_id,'tracking_type'=>'quantity','qty'=>$x->qty_remaining,'unit'=>$x->unit,'status'=>$x->status,'assigned_at'=>$x->assigned_at]);$all=$serialized->concat($quantity)->sortByDesc('assigned_at')->values();$page=max(1,(int)$r->input('page',1));$per=min(max(1,(int)$r->input('per_page',10)),100);$p=new \Illuminate\Pagination\LengthAwarePaginator($all->forPage($page,$per)->values(),$all->count(),$per,$page,['path'=>$r->url(),'query'=>$r->query()]);return $this->successPaginated($p,\Illuminate\Http\Resources\Json\JsonResource::class,'Peralatan teknisi berhasil diambil');}
public function returnFromTechnician(Request $r){$d=$r->validate(['source_type'=>['required',Rule::in(['serialized','quantity'])],'source_id'=>['required','integer'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]);$t=$r->attributes->get('tenant_id');return DB::transaction(function()use($r,$d,$t){$source=$d['source_type']==='serialized'?\App\Models\TechnicianMaterial::lockForUpdate()->findOrFail($d['source_id']):\App\Models\TechnicianConsumable::lockForUpdate()->findOrFail($d['source_id']);abort_unless(\App\Models\UserTenant::where('tenant_id',$t)->where('user_id',$source->user_id)->exists(),403);$barcode=ItemBarcode::where('barcode',$source->barcode_id)->lockForUpdate()->firstOrFail();$item=$barcode->item;$qty=$d['source_type']==='serialized'?1:(float)$d['qty'];if($d['source_type']==='serialized'){abort_unless($source->status==='assigned',422,'Barang sudah tidak berada pada teknisi.');$source->update(['status'=>'returned']);}else{abort_if((float)$source->qty_remaining<$qty,422,'Jumlah pengembalian melebihi sisa teknisi.');$remaining=(float)$source->qty_remaining-$qty;$source->update(['qty_remaining'=>$remaining,'status'=>$remaining>0?'assigned':'returned']);}$itemBefore=(float)$item->barcodes()->sum('remaining_qty');$barcodeBefore=(float)$barcode->remaining_qty;$barcodeAfter=$barcodeBefore+$qty;$barcode->update(['remaining_qty'=>$barcodeAfter,'status'=>'available']);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'movement_type'=>'return_to_warehouse','qty'=>$qty,'from_user_id'=>$source->user_id,'notes'=>$d['notes']??null,'created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'item_movement_id'=>$move->id,'qty_before'=>$itemBefore,'qty_change'=>$qty,'qty_after'=>$itemBefore+$qty,'barcode_qty_before'=>$barcodeBefore,'barcode_qty_change'=>$qty,'barcode_qty_after'=>$barcodeAfter]);return $this->created($move,'Barang berhasil dikembalikan ke gudang');});}
public function codeIndex(Request $r){$t=$r->attributes->get('tenant_id');$q=ItemCode::when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->when($r->search,fn($x,$s)=>$x->where(fn($y)=>$y->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%")))->orderBy($r->input('sort_by','name'),$r->input('sort_direction','asc'))->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Kode barang berhasil diambil');}
public function codeStore(Request $r){$d=$this->validateCode($r);$d['tenant_id']=$r->user()->isMasterAdmin()?($d['tenant_id']??$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');$d['created_by']=$r->user()->id;return $this->created(ItemCode::create($d),'Kode barang berhasil dibuat');}
public function codeUpdate(Request $r,ItemCode $code){$this->authCode($r,$code);$code->update($this->validateCode($r,$code));return $this->updated($code->fresh(),'Kode barang berhasil diperbarui');}
public function codeDestroy(Request $r,ItemCode $code){$this->authCode($r,$code);abort_if($code->items()->exists(),422,'Kode barang yang sudah digunakan tidak dapat dihapus.');$code->delete();return $this->deleted('Kode barang berhasil dihapus');}
private function validateCode(Request $r,?ItemCode $code=null){$tenant=$r->input('tenant_id')?:$r->attributes->get('tenant_id');return $r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'code'=>['required','string','max:80',Rule::unique('item_codes')->where('tenant_id',$tenant)->ignore($code)],'name'=>['required','string','max:255'],'tracking_type'=>['required',Rule::in(['serialized','quantity'])],'need_serial_number'=>['boolean'],'unit'=>['required',Rule::in(['pcs','meter','lembar','roll','box','dus','set','unit'])],'minimum_stock'=>['nullable','numeric','min:0'],'description'=>['nullable','string'],'is_active'=>['boolean']]);}
private function validateItem(Request $r,?WarehouseItem $i=null){$tenant=$r->user()->isMasterAdmin()?($r->input('tenant_id')?:$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');$d=$r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'item_code_id'=>['required','integer','exists:item_codes,id',Rule::unique('warehouse_items')->where('tenant_id',$tenant)->ignore($i)],'is_active'=>['boolean']]);$code=ItemCode::findOrFail($d['item_code_id']);abort_unless((int)$code->tenant_id===(int)$tenant,422,'Kode barang bukan milik tenant aktif.');return [...$d,'sku'=>$code->code,'name'=>$code->name,'tracking_type'=>$code->tracking_type==='serialized'?'serialized':'bulk','unit'=>$code->unit,'minimum_stock'=>$code->minimum_stock];}
private function authCode(Request $r,ItemCode $code){abort_unless($r->user()->isMasterAdmin()||(int)$code->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
private function auth(Request $r,WarehouseItem $i){abort_unless($r->user()->isMasterAdmin()||(int)$i->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers\Warehouse;
use App\Http\Controllers\ApiController;use App\Models\Warehouse;use Illuminate\Http\Request;use Illuminate\Validation\Rule;
class WarehouseLocationController extends ApiController
{
public function index(Request $r){$t=$r->attributes->get('tenant_id');$q=Warehouse::withCount('items')->when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->when($r->search,fn($x,$s)=>$x->where(fn($y)=>$y->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%")))->orderBy('name')->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Daftar gudang berhasil diambil');}
public function store(Request $r){$d=$this->data($r);$d['tenant_id']=$r->user()->isMasterAdmin()?($d['tenant_id']??$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');$d['created_by']=$r->user()->id;return $this->created(Warehouse::create($d),'Gudang berhasil dibuat');}
public function update(Request $r,Warehouse $warehouse){$this->auth($r,$warehouse);$warehouse->update($this->data($r,$warehouse));return $this->updated($warehouse->fresh(),'Gudang berhasil diperbarui');}
public function destroy(Request $r,Warehouse $warehouse){$this->auth($r,$warehouse);abort_if($warehouse->items()->exists(),422,'Gudang yang sudah memiliki barang tidak dapat dihapus.');$warehouse->delete();return $this->deleted('Gudang berhasil dihapus');}
private function data(Request $r,?Warehouse $w=null){$t=$r->input('tenant_id')?:$r->attributes->get('tenant_id');return $r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'code'=>['required','string','max:50',Rule::unique('warehouses')->where('tenant_id',$t)->ignore($w)],'name'=>['required','string','max:255'],'address'=>['nullable','string'],'is_active'=>['boolean']]);}
private function auth(Request $r,Warehouse $w){abort_unless($r->user()->isMasterAdmin()||(int)$w->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
}