feat: full dashboard-ticketing scaffold with data model, controllers, Vue pages

This commit is contained in:
Joel Wedemire
2026-04-08 17:10:30 -07:00
parent 81d0d54f50
commit 391699220f
33 changed files with 1947 additions and 719 deletions

View File

@@ -0,0 +1,63 @@
<?php
namespace Dashboard\Ticketing\Http\Controllers;
use Dashboard\Ticketing\Models\Ticket;
use Dashboard\Ticketing\Models\TicketAttachment;
use Dashboard\Ticketing\Models\TicketingAgentAccess;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class TicketAttachmentController extends Controller
{
private const MAX_SIZE_MB = 20;
private function canAccessTicket(Ticket $ticket): bool
{
$user = Auth::user();
$isAgent = TicketingAgentAccess::where('user_id', $user->id)
->where('group_id', $ticket->group_id)
->exists();
return $isAgent || $ticket->submitter_id === $user->id;
}
public function store(Request $request, Ticket $ticket)
{
if (!$this->canAccessTicket($ticket)) {
abort(403);
}
$request->validate([
'file' => 'required|file|max:' . (self::MAX_SIZE_MB * 1024),
'message_id' => 'nullable|exists:ticket_messages,id',
]);
$file = $request->file('file');
$disk = config('ticketing.storage_disk', env('TICKETING_STORAGE_DISK', 'local'));
$path = $file->store('ticketing/attachments/' . $ticket->id, $disk);
TicketAttachment::create([
'ticket_id' => $ticket->id,
'message_id' => $request->message_id,
'filename' => $file->getClientOriginalName(),
'path' => $path,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
return back()->with('success', 'Attachment uploaded.');
}
public function show(TicketAttachment $attachment)
{
$ticket = $attachment->ticket;
if (!$this->canAccessTicket($ticket)) {
abort(403);
}
$disk = config('ticketing.storage_disk', env('TICKETING_STORAGE_DISK', 'local'));
return Storage::disk($disk)->download($attachment->path, $attachment->filename);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace Dashboard\Ticketing\Http\Controllers;
use Dashboard\Ticketing\Models\Ticket;
use Dashboard\Ticketing\Models\TicketComment;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class TicketCommentController extends Controller
{
public function store(Request $request, Ticket $ticket)
{
$user = auth()->user();
$isAdmin = in_array($user->role ?? '', ['admin', 'super_admin']);
abort_if(! $isAdmin && $ticket->user_id !== $user->id, 403);
$request->validate([
'body' => 'required|string',
'is_internal' => 'boolean',
]);
// Non-admins cannot post internal notes
$isInternal = $isAdmin && $request->boolean('is_internal');
TicketComment::create([
'ticket_id' => $ticket->id,
'user_id' => $user->id,
'body' => $request->body,
'is_internal' => $isInternal,
]);
return redirect()->route('ticketing.show', $ticket)->with('success', 'Comment added.');
}
}

View File

@@ -2,155 +2,250 @@
namespace Dashboard\Ticketing\Http\Controllers;
use Dashboard\Ticketing\Models\PriorityLevel;
use Dashboard\Ticketing\Models\Ticket;
use Dashboard\Ticketing\Models\TicketingAgentAccess;
use Dashboard\Ticketing\Models\TicketingGroup;
use Dashboard\Ticketing\Models\TicketingProject;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class TicketController extends Controller
{
public function index(Request $request)
private function agentGroupIds(): array
{
$user = auth()->user();
$isAdmin = in_array($user->role ?? '', ['admin', 'super_admin']);
return TicketingAgentAccess::where('user_id', Auth::id())
->pluck('group_id')
->toArray();
}
$query = Ticket::with(['submitter:id,name', 'assignee:id,name']);
private function isAgent(int $groupId = null): bool
{
$query = TicketingAgentAccess::where('user_id', Auth::id());
if ($groupId) {
$query->where('group_id', $groupId);
}
return $query->exists();
}
if (! $isAdmin) {
$query->where('user_id', $user->id);
private function isManager(int $groupId): bool
{
return TicketingAgentAccess::where('user_id', Auth::id())
->where('group_id', $groupId)
->where('role', 'manager')
->exists();
}
public function index(Request $request): Response
{
$user = Auth::user();
$agentGroupIds = $this->agentGroupIds();
$isAgent = count($agentGroupIds) > 0;
$query = Ticket::with(['group', 'priority', 'project']);
if ($isAgent) {
$query->whereIn('group_id', $agentGroupIds);
// Filters
if ($request->filled('group_id')) {
$query->where('group_id', $request->group_id);
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('priority_id')) {
$query->where('priority_id', $request->priority_id);
}
if ($request->filter === 'mine') {
$query->where('assigned_to', $user->id);
} elseif ($request->filter === 'unassigned') {
$query->whereNull('assigned_to');
} elseif ($request->filter === 'pending') {
$query->where('status', 'pending');
} elseif (!$request->filled('status')) {
$query->whereNotIn('status', ['resolved', 'closed']);
}
} else {
$query->where('submitter_id', $user->id);
}
if ($search = $request->get('search')) {
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
$tickets = $query->latest()->paginate(30)->withQueryString();
if ($status = $request->get('status')) {
$query->where('status', $status);
}
// Enrich with submitter name
$userIds = $tickets->pluck('submitter_id')->merge($tickets->pluck('assigned_to'))->filter()->unique();
$users = \DB::table('users')->whereIn('id', $userIds)->get(['id', 'name', 'email'])->keyBy('id');
if ($priority = $request->get('priority')) {
$query->where('priority', $priority);
}
$tickets->getCollection()->transform(function ($ticket) use ($users) {
$ticket->submitter = $users[$ticket->submitter_id] ?? null;
$ticket->assignee = $ticket->assigned_to ? ($users[$ticket->assigned_to] ?? null) : null;
return $ticket;
});
if ($category = $request->get('category')) {
$query->where('category', $category);
}
$tickets = $query->latest()->paginate(25)->withQueryString();
$groups = TicketingGroup::when($isAgent, fn($q) => $q->whereIn('id', $agentGroupIds))->get();
$priorities = PriorityLevel::whereNull('group_id')
->orWhenIn('group_id', $agentGroupIds ?? [])
->orderBy('sort_order')
->get();
$projects = TicketingProject::when($isAgent, fn($q) => $q->whereIn('group_id', $agentGroupIds))
->where('status', 'active')
->get();
return Inertia::render('Ticketing/Index', [
'tickets' => $tickets,
'search' => $request->get('search', ''),
'statusFilter' => $request->get('status', ''),
'priorityFilter' => $request->get('priority', ''),
'categoryFilter' => $request->get('category', ''),
'isAdmin' => $isAdmin,
'tickets' => $tickets,
'groups' => $groups,
'priorities' => $priorities,
'projects' => $projects,
'isAgent' => $isAgent,
'filters' => $request->only(['group_id', 'status', 'priority_id', 'filter']),
]);
}
public function create()
public function create(): Response
{
return Inertia::render('Ticketing/Create');
$user = Auth::user();
$agentGroupIds = $this->agentGroupIds();
$isAgent = count($agentGroupIds) > 0;
$groups = TicketingGroup::when($isAgent, fn($q) => $q->whereIn('id', $agentGroupIds))->get();
$priorities = PriorityLevel::orderBy('sort_order')->get();
return Inertia::render('Ticketing/Create', [
'groups' => $groups,
'priorities' => $priorities,
]);
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'category' => 'required|in:IT,Facilities,HR,Other',
'priority' => 'required|in:low,medium,high,urgent',
'group_id' => 'required|exists:ticketing_groups,id',
'priority_id' => 'nullable|exists:ticketing_priority_levels,id',
'due_date' => 'nullable|date',
]);
Ticket::create([
'user_id' => auth()->id(),
'title' => $request->title,
'description' => $request->description,
'category' => $request->category,
'priority' => $request->priority,
'status' => 'open',
$group = TicketingGroup::findOrFail($validated['group_id']);
$number = $group->nextTicketNumber();
$ticket = Ticket::create([
...$validated,
'number' => $number,
'submitter_id' => Auth::id(),
'status' => 'open',
]);
return redirect()->route('ticketing.index')->with('success', 'Ticket submitted.');
return redirect()->route('ticketing.show', $ticket);
}
public function show(Ticket $ticket)
public function show(Ticket $ticket): Response
{
$user = auth()->user();
$isAdmin = in_array($user->role ?? '', ['admin', 'super_admin']);
$user = Auth::user();
$isAgent = $this->isAgent($ticket->group_id);
$isSubmitter = $ticket->submitter_id === $user->id;
abort_if(! $isAdmin && $ticket->user_id !== $user->id, 403);
if (!$isAgent && !$isSubmitter) {
abort(403);
}
$ticket->load([
'submitter:id,name',
'assignee:id,name',
'comments.author:id,name',
]);
$ticket->load(['group', 'priority', 'project', 'messages', 'attachments']);
// Enrich messages with user data
$userIds = $ticket->messages->pluck('user_id')->filter()->unique();
$users = \DB::table('users')->whereIn('id', $userIds)->get(['id', 'name', 'email'])->keyBy('id');
$ticket->messages->each(function ($msg) use ($users) {
$msg->author = $msg->user_id ? ($users[$msg->user_id] ?? null) : null;
});
// Agents for assignment picker
$agents = [];
if ($isAgent) {
$agentIds = TicketingAgentAccess::where('group_id', $ticket->group_id)->pluck('user_id');
$agents = \DB::table('users')->whereIn('id', $agentIds)->get(['id', 'name', 'email']);
}
$priorities = PriorityLevel::where(fn($q) => $q->whereNull('group_id')->orWhere('group_id', $ticket->group_id))
->orderBy('sort_order')->get();
return Inertia::render('Ticketing/Show', [
'ticket' => $ticket,
'isAdmin' => $isAdmin,
'ticket' => $ticket,
'isAgent' => $isAgent,
'isManager' => $isAgent && $this->isManager($ticket->group_id),
'agents' => $agents,
'priorities' => $priorities,
]);
}
public function edit(Ticket $ticket)
public function edit(Ticket $ticket): Response
{
$user = auth()->user();
$isAdmin = in_array($user->role ?? '', ['admin', 'super_admin']);
if (!$this->isAgent($ticket->group_id)) {
abort(403);
}
abort_if(! $isAdmin && $ticket->user_id !== $user->id, 403);
$ticket->load(['group', 'priority', 'project']);
$priorities = PriorityLevel::where(fn($q) => $q->whereNull('group_id')->orWhere('group_id', $ticket->group_id))
->orderBy('sort_order')->get();
$agentIds = TicketingAgentAccess::where('group_id', $ticket->group_id)->pluck('user_id');
$agents = \DB::table('users')->whereIn('id', $agentIds)->get(['id', 'name', 'email']);
$projects = TicketingProject::where('group_id', $ticket->group_id)->get();
return Inertia::render('Ticketing/Edit', [
'ticket' => $ticket,
'isAdmin' => $isAdmin,
'ticket' => $ticket,
'priorities' => $priorities,
'agents' => $agents,
'projects' => $projects,
]);
}
public function update(Request $request, Ticket $ticket)
{
$user = auth()->user();
$isAdmin = in_array($user->role ?? '', ['admin', 'super_admin']);
abort_if(! $isAdmin && $ticket->user_id !== $user->id, 403);
if (!$this->isAgent($ticket->group_id)) {
abort(403);
}
$rules = [
'title' => 'required|string|max:255',
'description' => 'required|string',
'category' => 'required|in:IT,Facilities,HR,Other',
'priority' => 'required|in:low,medium,high,urgent',
'title' => 'sometimes|required|string|max:255',
'description' => 'sometimes|required|string',
'status' => 'sometimes|in:open,in_progress,pending,resolved,closed',
'priority_id' => 'nullable|exists:ticketing_priority_levels,id',
'due_date' => 'nullable|date',
'project_id' => 'nullable|exists:ticketing_projects,id',
];
if ($isAdmin) {
$rules['status'] = 'required|in:open,in_progress,resolved,closed';
// assigned_to only settable by agents
if ($this->isAgent($ticket->group_id)) {
$rules['assigned_to'] = 'nullable|exists:users,id';
}
$request->validate($rules);
$ticket->update($request->validate($rules));
$data = $request->only(['title', 'description', 'category', 'priority']);
if ($isAdmin) {
$data['status'] = $request->status;
$data['assigned_to'] = $request->assigned_to;
}
$ticket->update($data);
return redirect()->route('ticketing.show', $ticket)->with('success', 'Ticket updated.');
return back()->with('success', 'Ticket updated.');
}
public function destroy(Ticket $ticket)
{
$user = auth()->user();
$isAdmin = in_array($user->role ?? '', ['admin', 'super_admin']);
abort_if(! $isAdmin && $ticket->user_id !== $user->id, 403);
if (!$this->isManager($ticket->group_id)) {
abort(403);
}
$ticket->delete();
return redirect()->route('ticketing.index');
}
return redirect()->route('ticketing.index')->with('success', 'Ticket deleted.');
public function myTickets(): Response
{
$tickets = Ticket::where('submitter_id', Auth::id())
->with(['group', 'priority'])
->latest()
->paginate(20);
return Inertia::render('Ticketing/MyTickets', [
'tickets' => $tickets,
]);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Dashboard\Ticketing\Http\Controllers;
use Dashboard\Ticketing\Models\Ticket;
use Dashboard\Ticketing\Models\TicketMessage;
use Dashboard\Ticketing\Models\TicketingAgentAccess;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
class TicketMessageController extends Controller
{
private function isAgent(int $groupId): bool
{
return TicketingAgentAccess::where('user_id', Auth::id())
->where('group_id', $groupId)
->exists();
}
public function store(Request $request, Ticket $ticket)
{
$user = Auth::user();
$isAgent = $this->isAgent($ticket->group_id);
$isSubmitter = $ticket->submitter_id === $user->id;
if (!$isAgent && !$isSubmitter) {
abort(403);
}
$validated = $request->validate([
'body' => 'required|string',
'is_internal' => 'boolean',
]);
// Only agents can post internal notes
if ($validated['is_internal'] ?? false) {
if (!$isAgent) {
abort(403, 'Only agents can post internal notes.');
}
}
$message = TicketMessage::create([
'ticket_id' => $ticket->id,
'user_id' => $user->id,
'body' => $validated['body'],
'is_internal' => $validated['is_internal'] ?? false,
'source' => 'web',
]);
// Submitter reply reopens resolved/closed tickets
if ($isSubmitter && in_array($ticket->status, ['resolved', 'closed'])) {
$ticket->update(['status' => 'open']);
}
// Notify submitter if agent replied (not internal)
if ($isAgent && !($validated['is_internal'] ?? false)) {
$this->notifySubmitter($ticket, $message);
}
return back()->with('success', 'Message sent.');
}
private function notifySubmitter(Ticket $ticket, TicketMessage $message): void
{
// Placeholder for email notification
// In production: Mail::to($submitterEmail)->send(new TicketReplyMail($ticket, $message));
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Dashboard\Ticketing\Http\Controllers;
use Dashboard\Ticketing\Models\PriorityLevel;
use Dashboard\Ticketing\Models\TicketingAgentAccess;
use Dashboard\Ticketing\Models\TicketingGroup;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class TicketingSettingsController extends Controller
{
private function requireAgentAccess(): void
{
$hasAccess = TicketingAgentAccess::where('user_id', Auth::id())->exists();
if (!$hasAccess) {
abort(403);
}
}
private function requireManagerAccess(int $groupId): void
{
$isManager = TicketingAgentAccess::where('user_id', Auth::id())
->where('group_id', $groupId)
->where('role', 'manager')
->exists();
if (!$isManager) {
abort(403);
}
}
public function index(): Response
{
$this->requireAgentAccess();
$userId = Auth::id();
$myGroupIds = TicketingAgentAccess::where('user_id', $userId)->pluck('group_id');
$groups = TicketingGroup::whereIn('id', $myGroupIds)->get();
$agents = TicketingAgentAccess::whereIn('group_id', $myGroupIds)->get();
$agentUserIds = $agents->pluck('user_id')->unique();
$agentUsers = \DB::table('users')->whereIn('id', $agentUserIds)->get(['id', 'name', 'email'])->keyBy('id');
$agents->each(fn($a) => $a->user = $agentUsers[$a->user_id] ?? null);
$priorities = PriorityLevel::where(fn($q) => $q->whereNull('group_id')->orWhereIn('group_id', $myGroupIds))
->orderBy('sort_order')->get();
return Inertia::render('Ticketing/Settings', [
'groups' => $groups,
'agents' => $agents,
'priorities' => $priorities,
'myGroupIds' => $myGroupIds,
]);
}
public function storeGroup(Request $request)
{
$this->requireAgentAccess();
$validated = $request->validate([
'name' => 'required|string|max:100',
'email_address' => 'nullable|email',
'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/',
'prefix' => 'required|string|max:10|alpha_num|unique:ticketing_groups,prefix',
]);
$group = TicketingGroup::create($validated);
// Auto-add creator as manager
TicketingAgentAccess::create([
'user_id' => Auth::id(),
'group_id' => $group->id,
'role' => 'manager',
]);
return back()->with('success', 'Group created.');
}
public function updateGroup(Request $request, TicketingGroup $group)
{
$this->requireManagerAccess($group->id);
$validated = $request->validate([
'name' => 'required|string|max:100',
'email_address' => 'nullable|email',
'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/',
'prefix' => 'required|string|max:10|alpha_num|unique:ticketing_groups,prefix,' . $group->id,
]);
$group->update($validated);
return back()->with('success', 'Group updated.');
}
public function storeAgent(Request $request)
{
$this->requireAgentAccess();
$validated = $request->validate([
'user_id' => 'required|exists:users,id',
'group_id' => 'required|exists:ticketing_groups,id',
'role' => 'required|in:agent,manager',
]);
$this->requireManagerAccess($validated['group_id']);
TicketingAgentAccess::updateOrCreate(
['user_id' => $validated['user_id'], 'group_id' => $validated['group_id']],
['role' => $validated['role']]
);
return back()->with('success', 'Agent added.');
}
public function destroyAgent(TicketingAgentAccess $access)
{
$this->requireManagerAccess($access->group_id);
$access->delete();
return back()->with('success', 'Agent removed.');
}
public function storePriority(Request $request)
{
$this->requireAgentAccess();
$validated = $request->validate([
'name' => 'required|string|max:100',
'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/',
'description' => 'nullable|string',
'sort_order' => 'integer|min:0',
'group_id' => 'nullable|exists:ticketing_groups,id',
]);
PriorityLevel::create($validated);
return back()->with('success', 'Priority level created.');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EmailConnection extends Model
{
protected $table = 'ticketing_email_connections';
protected $fillable = ['group_id', 'type', 'config', 'last_polled_at', 'active'];
protected $casts = [
'config' => 'array',
'active' => 'boolean',
'last_polled_at' => 'datetime',
];
public function group(): BelongsTo
{
return $this->belongsTo(TicketingGroup::class, 'group_id');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class PriorityLevel extends Model
{
protected $table = 'ticketing_priority_levels';
protected $fillable = ['group_id', 'name', 'color', 'description', 'sort_order'];
public function group(): BelongsTo
{
return $this->belongsTo(TicketingGroup::class, 'group_id');
}
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'priority_id');
}
}

View File

@@ -9,27 +9,36 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Ticket extends Model
{
protected $fillable = [
'user_id',
'assigned_to',
'title',
'description',
'category',
'priority',
'status',
'number', 'group_id', 'submitter_id', 'assigned_to', 'project_id',
'title', 'description', 'status', 'priority_id', 'due_date',
];
public function submitter(): BelongsTo
protected $casts = [
'due_date' => 'date',
];
public function group(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
return $this->belongsTo(TicketingGroup::class, 'group_id');
}
public function assignee(): BelongsTo
public function priority(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'assigned_to');
return $this->belongsTo(PriorityLevel::class, 'priority_id');
}
public function comments(): HasMany
public function project(): BelongsTo
{
return $this->hasMany(TicketComment::class);
return $this->belongsTo(TicketingProject::class, 'project_id');
}
public function messages(): HasMany
{
return $this->hasMany(TicketMessage::class, 'ticket_id')->orderBy('created_at');
}
public function attachments(): HasMany
{
return $this->hasMany(TicketAttachment::class, 'ticket_id');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketAttachment extends Model
{
protected $fillable = ['ticket_id', 'message_id', 'filename', 'path', 'mime_type', 'size'];
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
public function message(): BelongsTo
{
return $this->belongsTo(TicketMessage::class, 'message_id');
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketComment extends Model
{
protected $fillable = [
'ticket_id',
'user_id',
'body',
'is_internal',
];
protected $casts = [
'is_internal' => 'boolean',
];
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TicketMessage extends Model
{
protected $fillable = [
'ticket_id', 'user_id', 'author_email', 'body',
'is_internal', 'source', 'email_message_id',
];
protected $casts = [
'is_internal' => 'boolean',
];
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
public function attachments(): HasMany
{
return $this->hasMany(TicketAttachment::class, 'message_id');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketingAgentAccess extends Model
{
protected $table = 'ticketing_agent_access';
protected $fillable = ['user_id', 'group_id', 'role'];
public function group(): BelongsTo
{
return $this->belongsTo(TicketingGroup::class, 'group_id');
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TicketingGroup extends Model
{
protected $fillable = ['name', 'email_address', 'color', 'prefix'];
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'group_id');
}
public function priorityLevels(): HasMany
{
return $this->hasMany(PriorityLevel::class, 'group_id');
}
public function agentAccess(): HasMany
{
return $this->hasMany(TicketingAgentAccess::class, 'group_id');
}
public function projects(): HasMany
{
return $this->hasMany(TicketingProject::class, 'group_id');
}
public function emailConnections(): HasMany
{
return $this->hasMany(EmailConnection::class, 'group_id');
}
public function nextTicketNumber(): string
{
$count = $this->tickets()->count() + 1;
return $this->prefix . '-' . str_pad($count, 4, '0', STR_PAD_LEFT);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Dashboard\Ticketing\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TicketingProject extends Model
{
protected $fillable = ['group_id', 'name', 'description', 'status', 'created_by'];
public function group(): BelongsTo
{
return $this->belongsTo(TicketingGroup::class, 'group_id');
}
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'project_id');
}
}

View File

@@ -6,10 +6,7 @@ use Illuminate\Support\ServiceProvider;
class TicketingServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function register(): void {}
public function boot(): void
{

View File

@@ -1,16 +1,37 @@
<?php
use Dashboard\Ticketing\Http\Controllers\TicketAttachmentController;
use Dashboard\Ticketing\Http\Controllers\TicketController;
use Dashboard\Ticketing\Http\Controllers\TicketCommentController;
use Dashboard\Ticketing\Http\Controllers\TicketMessageController;
use Dashboard\Ticketing\Http\Controllers\TicketingSettingsController;
use Illuminate\Support\Facades\Route;
Route::middleware(['web', 'auth', 'app.access:ticketing'])->prefix('app/ticketing')->name('ticketing.')->group(function () {
// Submitter portal (must come before /{ticket} to avoid conflict)
Route::get('/my-tickets', [TicketController::class, 'myTickets'])->name('my-tickets');
// Settings
Route::get('/settings', [TicketingSettingsController::class, 'index'])->name('settings');
Route::post('/settings/groups', [TicketingSettingsController::class, 'storeGroup'])->name('settings.groups.store');
Route::put('/settings/groups/{group}', [TicketingSettingsController::class, 'updateGroup'])->name('settings.groups.update');
Route::post('/settings/agents', [TicketingSettingsController::class, 'storeAgent'])->name('settings.agents.store');
Route::delete('/settings/agents/{access}', [TicketingSettingsController::class, 'destroyAgent'])->name('settings.agents.destroy');
Route::post('/settings/priorities', [TicketingSettingsController::class, 'storePriority'])->name('settings.priorities.store');
// Ticket routes
Route::get('/', [TicketController::class, 'index'])->name('index');
Route::get('/create', [TicketController::class, 'create'])->name('create');
Route::post('/', [TicketController::class, 'store'])->name('store');
// Attachment show (before /{ticket} wildcard)
Route::get('/attachments/{attachment}', [TicketAttachmentController::class, 'show'])->name('attachments.show');
Route::get('/{ticket}', [TicketController::class, 'show'])->name('show');
Route::get('/{ticket}/edit', [TicketController::class, 'edit'])->name('edit');
Route::put('/{ticket}', [TicketController::class, 'update'])->name('update');
Route::delete('/{ticket}', [TicketController::class, 'destroy'])->name('destroy');
Route::post('/{ticket}/comments', [TicketCommentController::class, 'store'])->name('comments.store');
// Messages & attachments
Route::post('/{ticket}/messages', [TicketMessageController::class, 'store'])->name('messages.store');
Route::post('/{ticket}/attachments', [TicketAttachmentController::class, 'store'])->name('attachments.store');
});