From 990d1dbb1b0d1b7a9aa7bb32fe00ce41e47f8953 Mon Sep 17 00:00:00 2001 From: Joel Wedemire Date: Wed, 8 Apr 2026 20:39:53 -0700 Subject: [PATCH] Add ticketing demo seeder --- database/seeders/TicketingDemoSeeder.php | 293 +++++++++++++++++++++++ src/TicketingServiceProvider.php | 4 + 2 files changed, 297 insertions(+) create mode 100644 database/seeders/TicketingDemoSeeder.php diff --git a/database/seeders/TicketingDemoSeeder.php b/database/seeders/TicketingDemoSeeder.php new file mode 100644 index 0000000..908e0c9 --- /dev/null +++ b/database/seeders/TicketingDemoSeeder.php @@ -0,0 +1,293 @@ +where('email', 'admin@vancouverchristian.org')->first(); + $micah = DB::table('users')->where('email', 'micah@qa.test')->first(); + $nahum = DB::table('users')->where('email', 'nahum@qa.test')->first(); + + if (! $admin) { + $this->command?->warn('TicketingDemoSeeder skipped: admin user not found.'); + return; + } + + $users = collect([$admin, $micah, $nahum])->filter()->values(); + + $groups = [ + ['name' => 'IT Helpdesk', 'prefix' => 'IT', 'email_address' => 'helpdesk@vcs.local', 'color' => '#2563eb'], + ['name' => 'Facilities', 'prefix' => 'FAC', 'email_address' => 'facilities@vcs.local', 'color' => '#059669'], + ['name' => 'HR', 'prefix' => 'HR', 'email_address' => 'hr@vcs.local', 'color' => '#7c3aed'], + ]; + + $groupModels = collect(); + foreach ($groups as $groupData) { + $group = TicketingGroup::firstOrCreate( + ['prefix' => $groupData['prefix']], + $groupData + ); + + foreach ($users as $index => $user) { + TicketingAgentAccess::firstOrCreate([ + 'user_id' => $user->id, + 'group_id' => $group->id, + ], [ + 'role' => $index === 0 ? 'manager' : 'agent', + ]); + } + + foreach ([ + ['name' => 'Low', 'color' => '#94a3b8', 'sort_order' => 1], + ['name' => 'Medium', 'color' => '#3b82f6', 'sort_order' => 2], + ['name' => 'High', 'color' => '#f59e0b', 'sort_order' => 3], + ['name' => 'Urgent', 'color' => '#ef4444', 'sort_order' => 4], + ] as $priorityData) { + PriorityLevel::firstOrCreate([ + 'group_id' => $group->id, + 'name' => $priorityData['name'], + ], [ + 'color' => $priorityData['color'], + 'description' => null, + 'sort_order' => $priorityData['sort_order'], + ]); + } + + $projects = match ($group->prefix) { + 'IT' => [ + ['name' => 'Chromebook Repairs', 'description' => 'Student device triage and repairs'], + ['name' => 'Staff Accounts', 'description' => 'Login, MFA, and permissions issues'], + ['name' => 'Classroom AV', 'description' => 'Projectors, panels, and sound systems'], + ], + 'FAC' => [ + ['name' => 'Work Orders', 'description' => 'General campus maintenance requests'], + ['name' => 'HVAC', 'description' => 'Heating and cooling issues'], + ['name' => 'Events Setup', 'description' => 'Room setup and teardown support'], + ], + default => [ + ['name' => 'Onboarding', 'description' => 'New hire setup and paperwork'], + ['name' => 'Benefits', 'description' => 'Benefits questions and follow-up'], + ['name' => 'Policy Questions', 'description' => 'Staff handbook and policy clarifications'], + ], + }; + + foreach ($projects as $projectData) { + TicketingProject::firstOrCreate([ + 'group_id' => $group->id, + 'name' => $projectData['name'], + ], [ + 'description' => $projectData['description'], + 'status' => 'active', + 'created_by' => $admin->id, + ]); + } + + $groupModels->push($group->fresh(['priorityLevels', 'projects', 'agentAccess'])); + } + + $submitterPool = $this->ensureSubmitterPool(); + $statuses = ['open', 'in_progress', 'pending', 'resolved', 'closed']; + $ticketBlueprints = $this->ticketBlueprints(); + + foreach (range(1, 50) as $i) { + $group = $groupModels[($i - 1) % $groupModels->count()]; + $priority = $group->priorityLevels->random(); + $project = $group->projects->random(); + $submitter = $submitterPool[($i - 1) % count($submitterPool)]; + $assigneeId = $group->agentAccess->random()->user_id; + $blueprint = $ticketBlueprints[($i - 1) % count($ticketBlueprints)]; + $createdAt = Carbon::now()->subDays(rand(0, 35))->subHours(rand(0, 23))->subMinutes(rand(0, 59)); + $status = $statuses[array_rand($statuses)]; + + $title = $blueprint['title']; + if ($i > count($ticketBlueprints)) { + $title .= ' #' . $i; + } + + $ticket = Ticket::updateOrCreate( + ['number' => $group->prefix . '-' . str_pad((string) $i, 4, '0', STR_PAD_LEFT)], + [ + 'group_id' => $group->id, + 'submitter_id' => $submitter['id'], + 'assigned_to' => in_array($status, ['open', 'resolved', 'closed']) && rand(0, 1) === 0 ? null : $assigneeId, + 'project_id' => $project->id, + 'title' => $title, + 'description' => $blueprint['description'], + 'status' => $status, + 'priority_id' => $priority->id, + 'due_date' => rand(0, 1) ? $createdAt->copy()->addDays(rand(2, 14))->toDateString() : null, + 'created_at' => $createdAt, + 'updated_at' => $createdAt, + ] + ); + + if ($ticket->messages()->exists()) { + continue; + } + + $this->seedThread($ticket, $submitter, $group, $status, $createdAt); + } + } + + private function ensureSubmitterPool(): array + { + $defaults = [ + ['name' => 'Ava Teacher', 'email' => 'ava.teacher@vcs.local', 'role' => 'staff'], + ['name' => 'Ben EA', 'email' => 'ben.ea@vcs.local', 'role' => 'staff'], + ['name' => 'Chloe Office', 'email' => 'chloe.office@vcs.local', 'role' => 'staff'], + ['name' => 'Daniel Coach', 'email' => 'daniel.coach@vcs.local', 'role' => 'staff'], + ['name' => 'Emma Principal', 'email' => 'emma.principal@vcs.local', 'role' => 'admin'], + ['name' => 'Finn Student Services', 'email' => 'finn.services@vcs.local', 'role' => 'staff'], + ['name' => 'Grace Library', 'email' => 'grace.library@vcs.local', 'role' => 'staff'], + ['name' => 'Hudson Counsellor', 'email' => 'hudson.counsellor@vcs.local', 'role' => 'staff'], + ]; + + $pool = []; + + foreach ($defaults as $person) { + $userId = DB::table('users')->where('email', $person['email'])->value('id'); + + if (! $userId) { + $userId = DB::table('users')->insertGetId([ + 'name' => $person['name'], + 'email' => $person['email'], + 'google_id' => 'demo-' . Str::slug($person['email']), + 'role' => $person['role'], + 'email_verified_at' => now(), + 'password' => bcrypt(Str::random(32)), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + $pool[] = ['id' => $userId, 'name' => $person['name'], 'email' => $person['email']]; + } + + return $pool; + } + + private function seedThread(Ticket $ticket, array $submitter, TicketingGroup $group, string $status, Carbon $createdAt): void + { + $agents = TicketingAgentAccess::where('group_id', $group->id)->pluck('user_id')->all(); + $agentId = $ticket->assigned_to ?: ($agents[0] ?? null); + + $messages = [ + [ + 'user_id' => $submitter['id'], + 'author_email' => $submitter['email'], + 'body' => $ticket->description, + 'is_internal' => false, + 'source' => rand(0, 3) === 0 ? 'email' : 'web', + 'created_at' => $createdAt, + ], + ]; + + if ($agentId) { + $messages[] = [ + 'user_id' => $agentId, + 'author_email' => null, + 'body' => $this->agentReplyForStatus($status), + 'is_internal' => false, + 'source' => 'web', + 'created_at' => $createdAt->copy()->addHours(rand(1, 8)), + ]; + } + + if (in_array($status, ['in_progress', 'pending', 'resolved', 'closed']) && $agentId) { + $messages[] = [ + 'user_id' => $agentId, + 'author_email' => null, + 'body' => $this->internalNoteForGroup($group->prefix), + 'is_internal' => true, + 'source' => 'web', + 'created_at' => $createdAt->copy()->addHours(rand(2, 16)), + ]; + } + + if (in_array($status, ['resolved', 'closed'])) { + $messages[] = [ + 'user_id' => $submitter['id'], + 'author_email' => $submitter['email'], + 'body' => 'Thanks — that fixed it. Appreciate the quick help.', + 'is_internal' => false, + 'source' => 'web', + 'created_at' => $createdAt->copy()->addHours(rand(10, 36)), + ]; + } + + foreach ($messages as $message) { + TicketMessage::create([ + 'ticket_id' => $ticket->id, + 'user_id' => $message['user_id'], + 'author_email' => $message['author_email'], + 'body' => $message['body'], + 'is_internal' => $message['is_internal'], + 'source' => $message['source'], + 'email_message_id' => $message['source'] === 'email' ? '<' . Str::uuid() . '@vcs.local>' : null, + 'created_at' => $message['created_at'], + 'updated_at' => $message['created_at'], + ]); + } + } + + private function agentReplyForStatus(string $status): string + { + return match ($status) { + 'open' => 'Got it. We have this in the queue and will take a look shortly.', + 'in_progress' => 'We are actively working on this now. I will update you once I have confirmed the fix.', + 'pending' => 'We need one more detail before we can continue. When did you first notice the issue?', + 'resolved' => 'This should now be fixed on our side. Please test when you have a minute.', + 'closed' => 'Closing this out since the issue appears resolved. Reopen any time if it comes back.', + default => 'Thanks, we are on it.', + }; + } + + private function internalNoteForGroup(string $prefix): string + { + return match ($prefix) { + 'IT' => 'Internal: likely permissions/device state issue. If no response by tomorrow, follow up and verify the user can reproduce on a second device.', + 'FAC' => 'Internal: bundle with nearby work orders if possible. Check whether this is part of a recurring room issue before assigning external contractor time.', + 'HR' => 'Internal: keep response factual and short. Confirm whether there is a policy or payroll dependency before promising a turnaround.', + default => 'Internal: triage complete, waiting on next action.', + }; + } + + private function ticketBlueprints(): array + { + return [ + ['title' => 'Projector not turning on in Room 204', 'description' => 'The classroom projector in Room 204 is unresponsive this morning. Power button flashes once, then nothing.'], + ['title' => 'Need access to new staff shared drive', 'description' => 'I was added to the literacy team but still cannot see the shared Google Drive folder for curriculum planning.'], + ['title' => 'Chromebook cart missing three chargers', 'description' => 'We checked the Grade 6 cart and three chargers are missing. Two devices are already below 20%.'], + ['title' => 'Gym thermostat is stuck at 27°C', 'description' => 'The gym feels like a greenhouse. Thermostat shows 27 degrees and the cooling does not seem to be kicking in.'], + ['title' => 'Can’t print to office copier', 'description' => 'Print jobs sit in queue and then disappear. I have tried restarting my laptop and reconnecting to Wi-Fi.'], + ['title' => 'New employee onboarding checklist incomplete', 'description' => 'Our new EA starts Monday and I cannot find confirmation that payroll, email, and building access were submitted.'], + ['title' => 'Classroom speakers crackling during assemblies', 'description' => 'Audio from the wall speakers has a crackling/popping sound whenever we play laptop audio through HDMI.'], + ['title' => 'Request for additional key fob', 'description' => 'We need an extra key fob for evening custodial coverage at the elementary entrance.'], + ['title' => 'Google account keeps asking for password again', 'description' => 'My browser signs me out repeatedly and prompts for my password several times a day on the same Mac.'], + ['title' => 'Benefits question about dependent coverage', 'description' => 'I need clarification on whether orthodontics falls under the current dependent coverage plan.'], + ['title' => 'Broken desk in portable 3', 'description' => 'One of the student desks has a cracked support and is wobbling badly.'], + ['title' => 'Staff laptop camera not detected in Meet', 'description' => 'Google Meet says no camera found even though the MacBook camera works in Photo Booth.'], + ['title' => 'Request setup for parent info night', 'description' => 'Need 80 chairs, podium, projector, and two microphones in the gym for Thursday evening.'], + ['title' => 'Payroll question on missed stipend', 'description' => 'A coaching stipend does not appear on my latest pay statement and I need to know whether it is pending.'], + ['title' => 'Door closer slamming in west hallway', 'description' => 'The west hallway fire door slams shut hard enough to shake the frame.'], + ['title' => 'Need substitute teacher account reactivated', 'description' => 'A returning substitute cannot access school systems and says their previous login no longer works.'], + ['title' => 'Student device filter blocking approved site', 'description' => 'An approved curriculum site is being blocked for students during class and I need it for tomorrow.'], + ['title' => 'Request ergonomic chair assessment', 'description' => 'I am having back pain and would like an ergonomic review of my workstation setup.'], + ['title' => 'Water fountain on second floor leaking', 'description' => 'There is a slow but steady leak beneath the second-floor bottle filler station.'], + ['title' => 'Need permission to edit school calendar', 'description' => 'I can view but not edit the school-wide Google Calendar for athletics scheduling.'], + ]; + } +} diff --git a/src/TicketingServiceProvider.php b/src/TicketingServiceProvider.php index 519b4b5..8b146f2 100644 --- a/src/TicketingServiceProvider.php +++ b/src/TicketingServiceProvider.php @@ -12,5 +12,9 @@ class TicketingServiceProvider extends ServiceProvider { $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); $this->loadRoutesFrom(__DIR__.'/routes/ticketing.php'); + + if ($this->app->runningInConsole()) { + $this->loadSeedersFrom(__DIR__.'/../database/seeders'); + } } }