merge upstream
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TicketStatus:string
|
||||
{
|
||||
case DRAFT = 'draft';
|
||||
case OPEN = 'open';
|
||||
case APPROVED = 'approved';
|
||||
case ASSIGNED = 'assigned';
|
||||
case PROGRESS = 'progress';
|
||||
case PENDING = 'pending';
|
||||
case RESOLVED = 'resolved';
|
||||
case CLOSED = 'closed';
|
||||
case CANCELLED = 'cancelled';
|
||||
case REJECTED = 'rejected';
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Ticket;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Resources\Ticket\TicketResource;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Ticket\TicketWorkflowService;
|
||||
|
||||
use App\Http\Requests\Ticket\AssignTicketRequest;
|
||||
use App\Http\Requests\Ticket\ReassignTicketRequest;
|
||||
use App\Http\Requests\Ticket\ApproveTicketRequest;
|
||||
use App\Http\Requests\Ticket\RejectTicketRequest;
|
||||
use App\Http\Requests\Ticket\PendingTicketRequest;
|
||||
use App\Http\Requests\Ticket\ResolveTicketRequest;
|
||||
use App\Http\Requests\Ticket\CloseTicketRequest;
|
||||
use App\Http\Requests\Ticket\CancelTicketRequest;
|
||||
use App\Http\Requests\Ticket\EscalateTicketRequest;
|
||||
use App\Http\Requests\Ticket\StartTicketRequest;
|
||||
|
||||
class TicketWorkflowController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected TicketWorkflowService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* History Workflow
|
||||
*/
|
||||
public function history(Ticket $ticket)
|
||||
{
|
||||
$data = $this->service->history($ticket);
|
||||
|
||||
return $this->success(
|
||||
$data,
|
||||
'Workflow berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign Teknisi
|
||||
*/
|
||||
public function assign(
|
||||
AssignTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->assign(
|
||||
$ticket,
|
||||
$request->technicians,
|
||||
$request->leader_id,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Teknisi berhasil ditugaskan'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reassign Teknisi
|
||||
*/
|
||||
public function reassign(
|
||||
ReassignTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->reassign(
|
||||
$ticket,
|
||||
$request->technicians,
|
||||
$request->leader_id,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Penugasan berhasil diperbarui'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve
|
||||
*/
|
||||
public function approve(
|
||||
ApproveTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->approve(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket berhasil disetujui'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject
|
||||
*/
|
||||
public function reject(
|
||||
RejectTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->reject(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket berhasil ditolak'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Work
|
||||
*/
|
||||
public function start(
|
||||
StartTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->start(
|
||||
$ticket,
|
||||
$request->validated(),
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Pekerjaan dimulai'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending
|
||||
*/
|
||||
public function pending(
|
||||
PendingTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->pending(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket pending'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume
|
||||
*/
|
||||
public function resume(Ticket $ticket)
|
||||
{
|
||||
$ticket = $this->service->resume(
|
||||
$ticket,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket dilanjutkan'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve
|
||||
*/
|
||||
public function resolve(
|
||||
ResolveTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->resolve(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket berhasil diselesaikan'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close
|
||||
*/
|
||||
public function close(
|
||||
CloseTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->close(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket berhasil ditutup'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel
|
||||
*/
|
||||
public function cancel(
|
||||
CancelTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->cancel(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket berhasil dibatalkan'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escalate
|
||||
*/
|
||||
public function escalate(
|
||||
EscalateTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$ticket = $this->service->escalate(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
auth()->id() ?? 1
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
new TicketResource($ticket),
|
||||
'Ticket berhasil dieskalasi'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ApproveTicketRequest 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 [
|
||||
'notes' => 'nullable|string|max:1000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AssignTicketRequest 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 [
|
||||
'technicians' => 'required|array|min:1',
|
||||
'leader_id' => 'required|integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CancelTicketRequest 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 [
|
||||
'reason' => 'required|string|max:2000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CloseTicketRequest 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 [
|
||||
'notes' => 'nullable|string|max:2000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class EscalateTicketRequest 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 [
|
||||
'reason' => 'required|string|max:2000',
|
||||
'level' => 'nullable|in:L1,L2,L3,NOC'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'reason.required' => 'Alasan eskalasi wajib diisi'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PendingTicketRequest 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 [
|
||||
'reason' => 'required|string|max:1000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReassignTicketRequest 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 [
|
||||
'technicians' => 'required|array|min:1',
|
||||
'technicians.*' => 'integer',
|
||||
'leader_id' => 'required|integer'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'technicians.required' => 'Daftar teknisi wajib diisi',
|
||||
'technicians.array' => 'Format teknisi harus array',
|
||||
'leader_id.required' => 'Leader teknisi wajib dipilih'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RejectTicketRequest 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 [
|
||||
'reason' => 'required|string|max:1000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ResolveTicketRequest 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 [
|
||||
'notes' => 'required|string|max:2000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StartTicketRequest 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 [
|
||||
'latitude' => 'nullable|numeric',
|
||||
'longitude' => 'nullable|numeric',
|
||||
'notes' => 'nullable|string|max:1000'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,27 @@ class Ticket extends Model
|
||||
'priority',
|
||||
'status',
|
||||
'created_by',
|
||||
'assigned_at',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'rejected_by',
|
||||
'rejected_at',
|
||||
'rejection_reason',
|
||||
'resolved_at',
|
||||
'closed_at',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function ticketType()
|
||||
{
|
||||
return $this->belongsTo(
|
||||
TicketType::class,
|
||||
'ticket_type_id'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,14 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TicketApproval extends Model
|
||||
{
|
||||
//
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id',
|
||||
'approver_id',
|
||||
'status',
|
||||
'notes',
|
||||
'approved_at',
|
||||
'created_at'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,5 +6,14 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TicketAssignment extends Model
|
||||
{
|
||||
//
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id',
|
||||
'user_id',
|
||||
'is_leader',
|
||||
'assigned_by',
|
||||
'assigned_at',
|
||||
'status'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,10 +12,30 @@ class TicketType extends Model
|
||||
'code',
|
||||
'name',
|
||||
'need_approval',
|
||||
'sla_minutes'
|
||||
'sla_minutes',
|
||||
'require_photo',
|
||||
'require_material',
|
||||
'need_customer',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'need_approval' => 'boolean',
|
||||
'need_approval' => 'boolean',
|
||||
'require_photo' => 'boolean',
|
||||
'require_material' => 'boolean',
|
||||
'need_customer' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function tickets()
|
||||
{
|
||||
return $this->hasMany(
|
||||
Ticket::class,
|
||||
'ticket_type_id'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketLog;
|
||||
use App\Models\TicketApproval;
|
||||
use App\Models\TicketAssignment;
|
||||
use App\Services\Ticket\TicketWorkflowValidator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class TicketWorkflowService
|
||||
{
|
||||
/**
|
||||
* Workflow History
|
||||
*/
|
||||
public function history(Ticket $ticket)
|
||||
{
|
||||
return TicketLog::where('ticket_id', $ticket->id)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* VALIDATION RULE ENGINE
|
||||
* =========================
|
||||
*/
|
||||
|
||||
private function type(Ticket $ticket)
|
||||
{
|
||||
return $ticket->ticketType;
|
||||
}
|
||||
|
||||
private function ensureRequiredAssets(Ticket $ticket, array $payload = [])
|
||||
{
|
||||
$type = $this->type($ticket);
|
||||
|
||||
// PHOTO VALIDATION
|
||||
if ($type?->require_photo) {
|
||||
$photoCount = $payload['photo_count'] ?? 0;
|
||||
|
||||
if ($photoCount <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'photo' => 'Foto wajib diupload untuk ticket ini'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// MATERIAL VALIDATION
|
||||
if ($type?->require_material) {
|
||||
$materialCount = $payload['material_count'] ?? 0;
|
||||
|
||||
if ($materialCount <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'material' => 'Material wajib diinput untuk ticket ini'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureCustomer(Ticket $ticket)
|
||||
{
|
||||
$type = $this->type($ticket);
|
||||
|
||||
if ($type?->need_customer && empty($ticket->customer_id)) {
|
||||
throw ValidationException::withMessages([
|
||||
'customer_id' => 'Customer wajib ada untuk ticket ini'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* ASSIGN / REASSIGN
|
||||
* =========================
|
||||
*/
|
||||
|
||||
public function assign(
|
||||
Ticket $ticket,
|
||||
array $technicians,
|
||||
int $leaderId,
|
||||
int $userId
|
||||
): Ticket {
|
||||
|
||||
TicketWorkflowValidator::allow($ticket, ['approved']);
|
||||
|
||||
DB::transaction(function () use (
|
||||
$ticket,
|
||||
$technicians,
|
||||
$leaderId,
|
||||
$userId
|
||||
) {
|
||||
|
||||
foreach ($technicians as $technicianId) {
|
||||
TicketAssignment::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => $technicianId,
|
||||
'is_leader' => ($technicianId == $leaderId),
|
||||
'assigned_by' => $userId,
|
||||
'assigned_at' => now(),
|
||||
'status' => 'assigned'
|
||||
]);
|
||||
}
|
||||
|
||||
$ticket->update([
|
||||
'status' => 'assigned',
|
||||
'assigned_at' => now()
|
||||
]);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Assign', 'Teknisi ditugaskan');
|
||||
});
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function reassign(
|
||||
Ticket $ticket,
|
||||
array $technicians,
|
||||
int $leaderId,
|
||||
int $userId
|
||||
): Ticket {
|
||||
|
||||
DB::transaction(function () use (
|
||||
$ticket,
|
||||
$technicians,
|
||||
$leaderId,
|
||||
$userId
|
||||
) {
|
||||
|
||||
TicketAssignment::where('ticket_id', $ticket->id)->delete();
|
||||
|
||||
foreach ($technicians as $technicianId) {
|
||||
TicketAssignment::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => $technicianId,
|
||||
'is_leader' => ($technicianId == $leaderId),
|
||||
'assigned_by' => $userId,
|
||||
'assigned_at' => now(),
|
||||
'status' => 'assigned'
|
||||
]);
|
||||
}
|
||||
|
||||
$this->log($ticket->id, $userId, 'Reassign', 'Penugasan diperbarui');
|
||||
});
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* APPROVAL FLOW
|
||||
* =========================
|
||||
*/
|
||||
|
||||
public function approve(
|
||||
Ticket $ticket,
|
||||
?string $notes,
|
||||
int $userId
|
||||
): Ticket {
|
||||
|
||||
TicketWorkflowValidator::allow($ticket, ['open']);
|
||||
|
||||
DB::transaction(function () use ($ticket, $notes, $userId) {
|
||||
|
||||
TicketApproval::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'approver_id' => $userId,
|
||||
'status' => 'approved',
|
||||
'notes' => $notes,
|
||||
'approved_at' => now(),
|
||||
'created_at' => now()
|
||||
]);
|
||||
|
||||
$ticket->update([
|
||||
'status' => 'approved',
|
||||
'approved_by' => $userId,
|
||||
'approved_at' => now()
|
||||
]);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Approved', $notes);
|
||||
});
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function reject(Ticket $ticket, string $reason, int $userId): Ticket
|
||||
{
|
||||
DB::transaction(function () use ($ticket, $reason, $userId) {
|
||||
|
||||
TicketApproval::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'approver_id' => $userId,
|
||||
'status' => 'rejected',
|
||||
'notes' => $reason,
|
||||
'created_at' => now()
|
||||
]);
|
||||
|
||||
$ticket->update([
|
||||
'status' => 'rejected',
|
||||
'rejected_by' => $userId,
|
||||
'rejected_at' => now(),
|
||||
'rejection_reason' => $reason
|
||||
]);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Rejected', $reason);
|
||||
});
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* WORKFLOW START
|
||||
* =========================
|
||||
*/
|
||||
|
||||
public function start(Ticket $ticket, array $location, int $userId): Ticket
|
||||
{
|
||||
TicketWorkflowValidator::allow($ticket, ['assigned']);
|
||||
|
||||
DB::transaction(function () use ($ticket, $location, $userId) {
|
||||
|
||||
$ticket->update([
|
||||
'status' => 'progress'
|
||||
]);
|
||||
|
||||
$this->log(
|
||||
$ticket->id,
|
||||
$userId,
|
||||
'Start Work',
|
||||
'Pekerjaan dimulai'
|
||||
);
|
||||
});
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function pending(Ticket $ticket, string $reason, int $userId): Ticket
|
||||
{
|
||||
$ticket->update(['status' => 'pending']);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Pending', $reason);
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function resume(Ticket $ticket, int $userId): Ticket
|
||||
{
|
||||
$ticket->update(['status' => 'progress']);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Resume', 'Dilanjutkan');
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* RESOLVE (VALIDATION POINT)
|
||||
* =========================
|
||||
*/
|
||||
|
||||
public function resolve(
|
||||
Ticket $ticket,
|
||||
string $notes,
|
||||
int $userId,
|
||||
array $payload = []
|
||||
): Ticket {
|
||||
|
||||
TicketWorkflowValidator::allow($ticket, ['progress']);
|
||||
|
||||
// 🔥 VALIDASI MATERIAL + PHOTO + CUSTOMER
|
||||
$this->ensureRequiredAssets($ticket, $payload);
|
||||
$this->ensureCustomer($ticket);
|
||||
|
||||
DB::transaction(function () use ($ticket, $notes, $userId) {
|
||||
|
||||
$ticket->update([
|
||||
'status' => 'resolved',
|
||||
'resolved_at' => now()
|
||||
]);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Resolved', $notes);
|
||||
});
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function close(Ticket $ticket, ?string $notes, int $userId): Ticket
|
||||
{
|
||||
TicketWorkflowValidator::allow($ticket, ['resolved']);
|
||||
|
||||
$ticket->update([
|
||||
'status' => 'closed',
|
||||
'closed_at' => now()
|
||||
]);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Closed', $notes);
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function cancel(Ticket $ticket, string $reason, int $userId): Ticket
|
||||
{
|
||||
$ticket->update(['status' => 'cancelled']);
|
||||
|
||||
$this->log($ticket->id, $userId, 'Cancelled', $reason);
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
public function escalate(Ticket $ticket, string $reason, int $userId): Ticket
|
||||
{
|
||||
$this->log($ticket->id, $userId, 'Escalated', $reason);
|
||||
|
||||
return $ticket->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* LOG SYSTEM
|
||||
* =========================
|
||||
*/
|
||||
|
||||
private function log(
|
||||
int $ticketId,
|
||||
int $userId,
|
||||
string $activity,
|
||||
?string $notes = null
|
||||
): void {
|
||||
|
||||
TicketLog::create([
|
||||
'ticket_id' => $ticketId,
|
||||
'user_id' => $userId,
|
||||
'activity' => $activity,
|
||||
'notes' => $notes,
|
||||
'created_at'=> now()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\Ticket;
|
||||
use Exception;
|
||||
|
||||
class TicketWorkflowValidator
|
||||
{
|
||||
public static function allow(
|
||||
Ticket $ticket,
|
||||
array $allowedStatuses
|
||||
): void {
|
||||
|
||||
if (! in_array($ticket->status, $allowedStatuses)) {
|
||||
|
||||
throw new Exception(
|
||||
"Ticket status '{$ticket->status}' tidak valid untuk proses ini"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ return new class extends Migration
|
||||
$table->string('name', 100);
|
||||
$table->boolean('need_approval')->default(false);
|
||||
$table->integer('sla_minutes')->nullable();
|
||||
|
||||
// Requirement Ticket
|
||||
$table->boolean('require_photo')->default(false);
|
||||
$table->boolean('require_material')->default(false);
|
||||
$table->boolean('need_customer')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
$this->call([
|
||||
TicketTypeSeeder::class,
|
||||
]);
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\TicketType;
|
||||
|
||||
class TicketTypeSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$types = [
|
||||
|
||||
[
|
||||
'code' => 'GGN',
|
||||
'name' => 'Gangguan Internet',
|
||||
'need_approval' => false,
|
||||
'sla_minutes' => 240,
|
||||
'require_photo' => true,
|
||||
'require_material' => false,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'INS',
|
||||
'name' => 'Instalasi Baru',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 1440,
|
||||
'require_photo' => true,
|
||||
'require_material' => true,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'ONU',
|
||||
'name' => 'Ganti ONU',
|
||||
'need_approval' => false,
|
||||
'sla_minutes' => 240,
|
||||
'require_photo' => true,
|
||||
'require_material' => true,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'CBL',
|
||||
'name' => 'Tarik Kabel',
|
||||
'need_approval' => false,
|
||||
'sla_minutes' => 480,
|
||||
'require_photo' => true,
|
||||
'require_material' => true,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'SVY',
|
||||
'name' => 'Survey Lokasi',
|
||||
'need_approval' => false,
|
||||
'sla_minutes' => 480,
|
||||
'require_photo' => true,
|
||||
'require_material' => false,
|
||||
'need_customer' => false,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'MNT',
|
||||
'name' => 'Maintenance',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 1440,
|
||||
'require_photo' => true,
|
||||
'require_material' => true,
|
||||
'need_customer' => false,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'ADM',
|
||||
'name' => 'Administrasi',
|
||||
'need_approval' => false,
|
||||
'sla_minutes' => null,
|
||||
'require_photo' => false,
|
||||
'require_material' => false,
|
||||
'need_customer' => false,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'REL',
|
||||
'name' => 'Relokasi Pelanggan',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 1440,
|
||||
'require_photo' => true,
|
||||
'require_material' => true,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'UPG',
|
||||
'name' => 'Upgrade Paket',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 240,
|
||||
'require_photo' => false,
|
||||
'require_material' => false,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'DWG',
|
||||
'name' => 'Downgrade Paket',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 240,
|
||||
'require_photo' => false,
|
||||
'require_material' => false,
|
||||
'need_customer' => true,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'BTS',
|
||||
'name' => 'Pekerjaan BTS/POP',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 2880,
|
||||
'require_photo' => true,
|
||||
'require_material' => true,
|
||||
'need_customer' => false,
|
||||
],
|
||||
|
||||
[
|
||||
'code' => 'NOC',
|
||||
'name' => 'Pekerjaan NOC',
|
||||
'need_approval' => true,
|
||||
'sla_minutes' => 1440,
|
||||
'require_photo' => false,
|
||||
'require_material' => false,
|
||||
'need_customer' => false,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
foreach ($types as $type) {
|
||||
|
||||
TicketType::updateOrCreate(
|
||||
['code' => $type['code']],
|
||||
$type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-11
@@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\Ticket\TicketController;
|
||||
use App\Http\Controllers\Ticket\TicketTypeController;
|
||||
use App\Http\Controllers\Ticket\TicketWorkflowController;
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
|
||||
@@ -13,22 +14,46 @@ Route::post('/login', [AuthController::class, 'login']);
|
||||
// Route::get('/', [TicketController::class, 'index']);
|
||||
// Route::post('/', [TicketController::class, 'store']);
|
||||
// });
|
||||
Route::get('/generate-token', function () {
|
||||
|
||||
$user = \App\Models\User::find(1);
|
||||
|
||||
return $user->createToken(
|
||||
'master-token'
|
||||
)->plainTextToken;
|
||||
|
||||
});
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
|
||||
Route::apiResource('tickets', TicketController::class);
|
||||
|
||||
Route::apiResource('ticket-types', TicketTypeController::class);
|
||||
|
||||
Route::prefix('tickets/{ticket}')
|
||||
->controller(TicketWorkflowController::class)
|
||||
->group(function () {
|
||||
|
||||
Route::get('/workflow', 'history');
|
||||
|
||||
Route::post('/assign', 'assign');
|
||||
Route::post('/reassign', 'reassign');
|
||||
|
||||
Route::post('/approve', 'approve');
|
||||
Route::post('/reject', 'reject');
|
||||
|
||||
Route::post('/start', 'start');
|
||||
|
||||
Route::post('/pending', 'pending');
|
||||
Route::post('/resume', 'resume');
|
||||
|
||||
Route::post('/resolve', 'resolve');
|
||||
|
||||
Route::post('/close', 'close');
|
||||
|
||||
Route::post('/cancel', 'cancel');
|
||||
|
||||
Route::post('/escalate', 'escalate');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Route::get('/generate-token', function () {
|
||||
|
||||
// $user = \App\Models\User::find(1);
|
||||
|
||||
// return $user->createToken(
|
||||
// 'master-token'
|
||||
// )->plainTextToken;
|
||||
|
||||
// });
|
||||
|
||||
Reference in New Issue
Block a user