big update all menus
This commit is contained in:
@@ -9,6 +9,11 @@ use App\Http\Requests\Ticket\UpdateTicketRequest;
|
||||
use App\Http\Resources\Ticket\TicketResource;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Ticket\TicketService;
|
||||
use App\Models\Customer;
|
||||
use App\Models\TicketType;
|
||||
use App\Models\User;
|
||||
use App\Models\TicketMaterial;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketController extends ApiController
|
||||
{
|
||||
@@ -19,7 +24,13 @@ class TicketController extends ApiController
|
||||
public function index(IndexTicketRequest $request)
|
||||
{
|
||||
$filters = $request->validated();
|
||||
$tickets = Ticket::query()
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
$sortBy = $filters['sort_by'] ?? 'created_at';
|
||||
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
||||
$tickets = Ticket::query()->with(['ticketType:id,code,name,need_approval,require_photo,require_material','customer:id,customer_code,name','tenant:id,tenant_name','creator:id,name','assignments.user:id,name'])
|
||||
->when(! $request->user()->isMasterAdmin(), fn ($query) => $query->where('tenant_id', $tenantId))
|
||||
->when($request->user()->isMasterAdmin() && $tenantId, fn ($query) => $query->where('tenant_id', $tenantId))
|
||||
->when(! empty($filters['assigned_to_me']), fn ($query) => $query->whereHas('assignments', fn ($assignment) => $assignment->where('user_id', $request->user()->id)))
|
||||
->when(
|
||||
! empty($filters['search']),
|
||||
fn ($query) => $query->where(function ($searchQuery) use ($filters) {
|
||||
@@ -44,7 +55,7 @@ class TicketController extends ApiController
|
||||
! empty($filters['customer_id']),
|
||||
fn ($query) => $query->where('customer_id', $filters['customer_id'])
|
||||
)
|
||||
->latest()
|
||||
->orderBy($sortBy, $sortDirection)
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
|
||||
return $this->successPaginated(
|
||||
@@ -57,7 +68,7 @@ class TicketController extends ApiController
|
||||
public function store(StoreTicketRequest $request)
|
||||
{
|
||||
$ticket = $this->service->create(
|
||||
$request->validated()
|
||||
$request->validated(), $request->user(), $request->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
return $this->created(
|
||||
@@ -66,10 +77,11 @@ class TicketController extends ApiController
|
||||
);
|
||||
}
|
||||
|
||||
public function show(Ticket $ticket)
|
||||
public function show(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request, $ticket);
|
||||
return $this->success(
|
||||
new TicketResource($ticket)
|
||||
new TicketResource($ticket->load(['ticketType','customer','tenant','creator','assignments.user','logs','materials','attachments']))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,6 +89,7 @@ class TicketController extends ApiController
|
||||
UpdateTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTenant($request, $ticket);
|
||||
$ticket = $this->service->update(
|
||||
$ticket,
|
||||
$request->validated()
|
||||
@@ -88,12 +101,72 @@ class TicketController extends ApiController
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Ticket $ticket)
|
||||
public function destroy(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request, $ticket);
|
||||
$this->service->delete($ticket);
|
||||
|
||||
return $this->deleted(
|
||||
'Ticket berhasil dibatalkan'
|
||||
);
|
||||
}
|
||||
|
||||
public function options(Request $request)
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
return $this->success([
|
||||
'ticket_types' => TicketType::orderBy('name')->get(['id','code','name','need_approval','require_photo','require_material']),
|
||||
'customers' => Customer::when(! $request->user()->isMasterAdmin() || $tenantId, fn ($q) => $q->where('tenant_id', $tenantId))->orderBy('name')->limit(500)->get(['id','customer_code','name','tenant_id']),
|
||||
'technicians' => User::whereHas('userTenants', fn ($q) => $q->where('tenant_id', $tenantId))->orderBy('name')->get(['id','name','email']),
|
||||
'installation_setting' => $tenantId
|
||||
? \App\Models\TenantInstallationSetting::firstOrCreate(['tenant_id' => $tenantId], ['activation_mode' => 'list_order'])
|
||||
: null,
|
||||
'package_profiles'=>\App\Models\NasPackageProfile::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']),
|
||||
'nas_mikrotiks'=>\App\Models\NasMikrotik::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']),
|
||||
'billing_profiles'=>\App\Models\BillingProfile::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']),
|
||||
'topology_odps'=>\App\Models\TopologyNode::where('tenant_id',$tenantId)->whereHas('deviceType',fn($q)=>$q->whereRaw('UPPER(code) = ?',['ODP']))->orderBy('name')->get(['id','code','name']),
|
||||
], 'Pilihan ticket berhasil diambil');
|
||||
}
|
||||
|
||||
public function storeMaterial(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request,$ticket);
|
||||
$data=$request->validate(['technician_material_id'=>['nullable','required_without:technician_consumable_id','integer','exists:technician_materials,id'],'technician_consumable_id'=>['nullable','required_without:technician_material_id','integer','exists:technician_consumables,id'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]);
|
||||
abort_if(!empty($data['technician_material_id'])&&!empty($data['technician_consumable_id']),422,'Pilih satu material saja.');
|
||||
$material=\Illuminate\Support\Facades\DB::transaction(function()use($data,$ticket,$request){
|
||||
if(!empty($data['technician_material_id'])){$source=\App\Models\TechnicianMaterial::whereKey($data['technician_material_id'])->where('user_id',$request->user()->id)->where('status','assigned')->lockForUpdate()->firstOrFail();$qty=1;$source->update(['status'=>'used']);$name=$source->material_name;}
|
||||
else{$source=\App\Models\TechnicianConsumable::whereKey($data['technician_consumable_id'])->where('user_id',$request->user()->id)->where('status','assigned')->lockForUpdate()->firstOrFail();$qty=(float)$data['qty'];abort_if((float)$source->qty_remaining<$qty,422,'Jumlah material yang dibawa tidak mencukupi.');$source->update(['qty_used'=>(float)$source->qty_used+$qty,'qty_remaining'=>(float)$source->qty_remaining-$qty]);$name=$source->material_name;}
|
||||
\App\Models\MaterialMovement::create(['barcode_id'=>$source->barcode_id,'user_id'=>$request->user()->id,'from_user_id'=>$request->user()->id,'movement_type'=>'use','ticket_id'=>$ticket->id,'qty'=>$qty,'description'=>$data['notes']??null,'created_by'=>$request->user()->id,'created_at'=>now()]);
|
||||
$warehouseBarcode=\App\Models\ItemBarcode::where('barcode',$source->barcode_id)->first();
|
||||
if($warehouseBarcode)\App\Models\ItemMovement::create(['tenant_id'=>$ticket->tenant_id,'warehouse_item_id'=>$warehouseBarcode->warehouse_item_id,'item_barcode_id'=>$warehouseBarcode->id,'movement_type'=>'used_on_ticket','qty'=>$qty,'reference_type'=>'ticket','reference_id'=>$ticket->id,'from_user_id'=>$request->user()->id,'notes'=>$data['notes']??null,'created_by'=>$request->user()->id]);
|
||||
return $ticket->materials()->create([...$data,'material_name'=>$name,'qty'=>$qty,'used_by'=>$request->user()->id,'created_at'=>now()]);
|
||||
});
|
||||
\App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Material Digunakan','notes'=>$material->material_name.' x '.$material->qty,'created_at'=>now()]);
|
||||
return $this->created($material->load('user:id,name'),'Material ticket berhasil ditambahkan');
|
||||
}
|
||||
|
||||
public function materialOptions(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request,$ticket);$userId=$request->user()->id;
|
||||
return $this->success(['serialized'=>\App\Models\TechnicianMaterial::where('user_id',$userId)->where('status','assigned')->get(['id','barcode_id','material_name','serial_number']),'consumables'=>\App\Models\TechnicianConsumable::where('user_id',$userId)->where('status','assigned')->where('qty_remaining','>',0)->get(['id','barcode_id','material_name','unit','qty_remaining'])],'Material yang dibawa berhasil diambil');
|
||||
}
|
||||
public function installationPorts(Request $request,Ticket $ticket,int $node,\App\Services\Customer\CustomerService $customers){$this->authorizeTenant($request,$ticket);return $this->success($customers->availableOdpPorts($node,$request->user(),$ticket->tenant_id,$ticket->customer_id),'Port ODP berhasil diambil');}
|
||||
|
||||
public function destroyMaterial(Request $request, Ticket $ticket, TicketMaterial $material)
|
||||
{
|
||||
$this->authorizeTenant($request,$ticket);abort_unless((int)$material->ticket_id===(int)$ticket->id,404);
|
||||
abort_if(in_array($ticket->status,['resolved','closed'],true),422,'Material ticket yang sudah selesai tidak dapat diubah.');
|
||||
\Illuminate\Support\Facades\DB::transaction(function()use($request,$ticket,$material){
|
||||
if($material->technician_material_id){$source=\App\Models\TechnicianMaterial::lockForUpdate()->find($material->technician_material_id);if($source)$source->update(['status'=>'assigned']);}
|
||||
else{$source=\App\Models\TechnicianConsumable::lockForUpdate()->find($material->technician_consumable_id);if($source)$source->update(['qty_used'=>max(0,(float)$source->qty_used-(float)$material->qty),'qty_remaining'=>(float)$source->qty_remaining+(float)$material->qty,'status'=>'assigned']);}
|
||||
if(isset($source)&&$source){\App\Models\MaterialMovement::create(['barcode_id'=>$source->barcode_id,'user_id'=>$request->user()->id,'to_user_id'=>$request->user()->id,'movement_type'=>'return','ticket_id'=>$ticket->id,'qty'=>$material->qty,'description'=>'Pembatalan pemakaian material pada ticket','created_by'=>$request->user()->id,'created_at'=>now()]);$warehouseBarcode=\App\Models\ItemBarcode::where('barcode',$source->barcode_id)->first();if($warehouseBarcode)\App\Models\ItemMovement::create(['tenant_id'=>$ticket->tenant_id,'warehouse_item_id'=>$warehouseBarcode->warehouse_item_id,'item_barcode_id'=>$warehouseBarcode->id,'movement_type'=>'ticket_usage_cancelled','qty'=>$material->qty,'reference_type'=>'ticket','reference_id'=>$ticket->id,'to_user_id'=>$request->user()->id,'notes'=>'Material dikembalikan ke saldo teknisi','created_by'=>$request->user()->id]);}
|
||||
$material->delete();
|
||||
});
|
||||
return $this->deleted('Material ticket berhasil dihapus dan dikembalikan ke daftar bawaan user');
|
||||
}
|
||||
|
||||
private function authorizeTenant(Request $request, Ticket $ticket): void
|
||||
{
|
||||
abort_unless($request->user()->isMasterAdmin() || (int) $ticket->tenant_id === (int) $request->attributes->get('tenant_id'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ use App\Http\Requests\Ticket\CloseTicketRequest;
|
||||
use App\Http\Requests\Ticket\CancelTicketRequest;
|
||||
use App\Http\Requests\Ticket\EscalateTicketRequest;
|
||||
use App\Http\Requests\Ticket\StartTicketRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TicketWorkflowController extends ApiController
|
||||
{
|
||||
@@ -29,6 +31,7 @@ class TicketWorkflowController extends ApiController
|
||||
*/
|
||||
public function history(Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
$data = $this->service->history($ticket);
|
||||
|
||||
return $this->success(
|
||||
@@ -44,6 +47,7 @@ class TicketWorkflowController extends ApiController
|
||||
AssignTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->assign(
|
||||
$ticket,
|
||||
$request->technicians,
|
||||
@@ -64,6 +68,7 @@ class TicketWorkflowController extends ApiController
|
||||
ReassignTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->reassign(
|
||||
$ticket,
|
||||
$request->technicians,
|
||||
@@ -84,6 +89,7 @@ class TicketWorkflowController extends ApiController
|
||||
ApproveTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->approve(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
@@ -103,6 +109,7 @@ class TicketWorkflowController extends ApiController
|
||||
RejectTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->reject(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
@@ -122,6 +129,7 @@ class TicketWorkflowController extends ApiController
|
||||
StartTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->start(
|
||||
$ticket,
|
||||
$request->validated(),
|
||||
@@ -141,10 +149,12 @@ class TicketWorkflowController extends ApiController
|
||||
PendingTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->pending(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
auth()->id() ?? 1
|
||||
auth()->id() ?? 1,
|
||||
$request->validated('stored_file_ids')
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
@@ -158,6 +168,7 @@ class TicketWorkflowController extends ApiController
|
||||
*/
|
||||
public function resume(Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->resume(
|
||||
$ticket,
|
||||
auth()->id() ?? 1
|
||||
@@ -176,10 +187,12 @@ class TicketWorkflowController extends ApiController
|
||||
ResolveTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->resolve(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
auth()->id() ?? 1
|
||||
auth()->id() ?? 1,
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
@@ -195,6 +208,7 @@ class TicketWorkflowController extends ApiController
|
||||
CloseTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->close(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
@@ -207,6 +221,42 @@ class TicketWorkflowController extends ApiController
|
||||
);
|
||||
}
|
||||
|
||||
public function verify(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
abort_unless($ticket->status === 'resolved', 422, 'Hanya ticket selesai yang dapat diverifikasi.');
|
||||
$data=$request->validate(['notes'=>['required','string','max:2000']]);
|
||||
$ticket->update(['verified_by'=>$request->user()->id,'verified_at'=>now(),'verification_notes'=>$data['notes']]);
|
||||
\App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Verified','notes'=>$data['notes'],'created_at'=>now()]);
|
||||
return $this->updated(new TicketResource($ticket->fresh()),'Ticket berhasil diverifikasi');
|
||||
}
|
||||
|
||||
public function installationStep(Request $request, Ticket $ticket, \App\Services\Customer\CustomerService $customers)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
abort_unless($ticket->source === 'new_installation' && $ticket->customer_id,422,'Ticket bukan pemasangan baru.');
|
||||
abort_unless(in_array($ticket->status,['progress','pending'],true),422,'Pekerjaan harus dimulai terlebih dahulu.');
|
||||
$step=$request->validate(['step'=>['required',Rule::in(['documentation','service','activation'])]])['step'];
|
||||
$expected=$ticket->installation_step ?: 'documentation'; abort_unless($step===$expected,422,"Tahap yang harus dikerjakan adalah {$expected}.");
|
||||
$customer=$customers->find($ticket->customer_id);
|
||||
if($step==='documentation'){
|
||||
$data=$request->validate(['stored_file_ids'=>['required','array','min:1'],'stored_file_ids.*'=>['integer','exists:stored_files,id'],'topology_id'=>['required','integer','exists:topology_nodes,id'],'topology_port_number'=>['required','integer','min:1']]);
|
||||
$images=collect($data['stored_file_ids'])->map(fn($id)=>['stored_file_id'=>$id,'category'=>'installation','is_cover'=>false])->all();
|
||||
$customers->update($customer,['tenant_id'=>$customer->tenant_id,'topology_id'=>$data['topology_id'],'topology_port_number'=>$data['topology_port_number'],'images'=>$images],$request->user());
|
||||
foreach($data['stored_file_ids'] as $id)\App\Models\TicketAttachment::firstOrCreate(['ticket_id'=>$ticket->id,'stored_file_id'=>$id,'stage'=>'installation_documentation'],['uploaded_by'=>$request->user()->id]);
|
||||
$next='service';
|
||||
}elseif($step==='service'){
|
||||
$data=$request->validate(['package_profile_id'=>['required','integer','exists:nas_package_profiles,id'],'nas_mikrotik_id'=>['required','integer','exists:nas_mikrotiks,id'],'billing_profile_id'=>['required','integer','exists:billing_profiles,id']]);
|
||||
$customers->update($customer,['tenant_id'=>$customer->tenant_id,...$data],$request->user()); $next='activation';
|
||||
}else{
|
||||
$data=$request->validate(['package_profile_id'=>['required','integer'],'nas_mikrotik_id'=>['required','integer'],'billing_profile_id'=>['required','integer'],'external_username'=>['required','string','max:255'],'external_password'=>['required','string','min:4'],'serial_number'=>['nullable','string','max:120'],'mac_address'=>['nullable','regex:/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/']]);
|
||||
$customers->activate($customer,['activation_source'=>'ticket',...$data],$request->user()); $next='completed';
|
||||
}
|
||||
$ticket->update(['installation_step'=>$next]);
|
||||
\App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Tahap Pemasangan Selesai','notes'=>$step.' → '.$next,'created_at'=>now()]);
|
||||
return $this->updated(new TicketResource($ticket->fresh()->load(['customer','attachments'])),'Tahap pemasangan berhasil disimpan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel
|
||||
*/
|
||||
@@ -214,6 +264,7 @@ class TicketWorkflowController extends ApiController
|
||||
CancelTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->cancel(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
@@ -233,6 +284,7 @@ class TicketWorkflowController extends ApiController
|
||||
EscalateTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->escalate(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
@@ -244,4 +296,11 @@ class TicketWorkflowController extends ApiController
|
||||
'Ticket berhasil dieskalasi'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function authorizeTicket(Ticket $ticket): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenantId = request()->attributes->get('tenant_id');
|
||||
abort_unless($user?->isMasterAdmin() || (int) $ticket->tenant_id === (int) $tenantId, 403);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user