Files
dashboard-ticketing/database/seeders/TicketingDemoSeeder.php
2026-04-08 20:39:53 -07:00

294 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Dashboard\Ticketing\Database\Seeders;
use Dashboard\Ticketing\Models\PriorityLevel;
use Dashboard\Ticketing\Models\Ticket;
use Dashboard\Ticketing\Models\TicketMessage;
use Dashboard\Ticketing\Models\TicketingAgentAccess;
use Dashboard\Ticketing\Models\TicketingGroup;
use Dashboard\Ticketing\Models\TicketingProject;
use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class TicketingDemoSeeder extends Seeder
{
public function run(): void
{
$admin = DB::table('users')->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' => 'Cant 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.'],
];
}
}