- New migrations: ticket views, statuses, participants, merge support - New models: TicketView, TicketStatus, TicketParticipant - New seeder: EmailTemplatesSeeder - Console commands for ticketing - Mobile: sidebar min-w-0/overflow-hidden, tab nav overflow-x-auto Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
410 lines
24 KiB
PHP
410 lines
24 KiB
PHP
<?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
|
|
{
|
|
// Use the first super admin, or fall back to any existing user
|
|
$admin = DB::table('users')->where('is_super_admin', 1)->first()
|
|
?? DB::table('users')->first();
|
|
|
|
if (! $admin) {
|
|
$this->command?->warn('TicketingDemoSeeder skipped: no users found in the database.');
|
|
return;
|
|
}
|
|
|
|
// Additional agents (QA test accounts if they exist)
|
|
$micah = DB::table('users')->where('email', 'micah@qa.test')->first();
|
|
$nahum = DB::table('users')->where('email', 'nahum@qa.test')->first();
|
|
|
|
$users = collect([$admin, $micah, $nahum])->filter()->values();
|
|
|
|
// Clear prior demo/QA ticketing data so the demo state stays coherent instead of
|
|
// piling new groups/tickets on top of earlier test scaffolds.
|
|
TicketMessage::query()->delete();
|
|
Ticket::query()->delete();
|
|
TicketingProject::query()->delete();
|
|
PriorityLevel::query()->delete();
|
|
TicketingAgentAccess::query()->delete();
|
|
TicketingGroup::query()->delete();
|
|
|
|
$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',
|
|
]);
|
|
}
|
|
|
|
// Global priorities — only seed once (first group iteration)
|
|
if (PriorityLevel::count() === 0) {
|
|
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(
|
|
['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'],
|
|
['name' => 'Network & Wi-Fi', 'description' => 'Connectivity and access point issues'],
|
|
],
|
|
'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'],
|
|
['name' => 'Grounds', 'description' => 'Exterior and landscaping requests'],
|
|
],
|
|
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'],
|
|
['name' => 'Payroll', 'description' => 'Pay queries and corrections'],
|
|
],
|
|
};
|
|
|
|
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'];
|
|
$blueprints = $this->ticketBlueprints();
|
|
$blueprintCount = count($blueprints);
|
|
$ticketCount = 45;
|
|
|
|
foreach (range(1, $ticketCount) 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 = $blueprints[($i - 1) % $blueprintCount];
|
|
$createdAt = Carbon::now()->subDays(rand(0, 45))->subHours(rand(0, 23))->subMinutes(rand(0, 59));
|
|
$status = $statuses[array_rand($statuses)];
|
|
|
|
$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' => $blueprint['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'],
|
|
['name' => 'Ben EA', 'email' => 'ben.ea@vcs.local'],
|
|
['name' => 'Chloe Office', 'email' => 'chloe.office@vcs.local'],
|
|
['name' => 'Daniel Coach', 'email' => 'daniel.coach@vcs.local'],
|
|
['name' => 'Emma Principal', 'email' => 'emma.principal@vcs.local'],
|
|
['name' => 'Finn Student Services', 'email' => 'finn.services@vcs.local'],
|
|
['name' => 'Grace Library', 'email' => 'grace.library@vcs.local'],
|
|
['name' => 'Hudson Counsellor', 'email' => 'hudson.counsellor@vcs.local'],
|
|
];
|
|
|
|
$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']),
|
|
'is_super_admin' => 0,
|
|
'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, ['pending']) && $agentId) {
|
|
$messages[] = [
|
|
'user_id' => $submitter['id'],
|
|
'author_email' => $submitter['email'],
|
|
'body' => 'Thanks for looking into this. To answer your question — it started about a week ago and it does happen consistently.',
|
|
'is_internal' => false,
|
|
'source' => 'web',
|
|
'created_at' => $createdAt->copy()->addHours(rand(3, 20)),
|
|
];
|
|
}
|
|
|
|
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
|
|
{
|
|
$notes = 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.',
|
|
'Internal: checked MDM — device is enrolled. May need remote wipe if issue persists.',
|
|
'Internal: similar issue came up last month, possibly related to the Chrome policy push from 2 weeks ago.',
|
|
'Internal: this looks like a profile sync error. Tried clearing cache remotely, should be resolved.',
|
|
],
|
|
'FAC' => [
|
|
'Internal: bundle with nearby work orders if possible. Check whether this is part of a recurring room issue before assigning external contractor time.',
|
|
'Internal: parts are on order, ETA 3 business days. Keep submitter updated.',
|
|
'Internal: assigned to weekend crew, no disruption to classes expected.',
|
|
'Internal: this is the third report from this room — worth doing a full inspection.',
|
|
],
|
|
default => [
|
|
'Internal: keep response factual and short. Confirm whether there is a policy or payroll dependency before promising a turnaround.',
|
|
'Internal: escalated to payroll for confirmation. Waiting on their response before replying to the submitter.',
|
|
'Internal: reviewed file, looks like a data entry error on our end. Will correct and follow up.',
|
|
'Internal: HR director has been looped in. Response expected by end of week.',
|
|
],
|
|
};
|
|
|
|
return $notes[array_rand($notes)];
|
|
}
|
|
|
|
private function ticketBlueprints(): array
|
|
{
|
|
return [
|
|
// ── IT ────────────────────────────────────────────────────────────
|
|
['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' => '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' => 'Classroom speakers crackling during assemblies',
|
|
'description' => 'Audio from the wall speakers has a crackling/popping sound whenever we play laptop audio through HDMI.'],
|
|
['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' => 'Staff laptop camera not detected in Meet',
|
|
'description' => 'Google Meet says no camera found even though the MacBook camera works in Photo Booth.'],
|
|
['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' => 'Need permission to edit school calendar',
|
|
'description' => 'I can view but not edit the school-wide Google Calendar for athletics scheduling.'],
|
|
['title' => 'Smartboard touch screen not responding',
|
|
'description' => 'The SMART Board in room 115 powers on and displays correctly but touch input stopped working this morning.'],
|
|
['title' => 'Wi-Fi dropping in the library wing',
|
|
'description' => 'Multiple staff have reported losing Wi-Fi connection in the library every afternoon around 1:30 PM.'],
|
|
['title' => 'Password reset needed for new EA',
|
|
'description' => 'Our new educational assistant started today and cannot log in. It looks like the account was created but the password was never sent.'],
|
|
['title' => 'Laptop won\'t connect to school network',
|
|
'description' => 'My personal MacBook was connecting to the staff network last week but now shows "cannot join" whenever I try.'],
|
|
['title' => 'Grade 8 iPads not syncing with MDM',
|
|
'description' => 'The Grade 8 iPad cart shows 12 devices as out of compliance in our MDM. Apps are not installing.'],
|
|
['title' => 'Classroom Apple TV not showing up',
|
|
'description' => 'The Apple TV in Room 309 disappeared from AirPlay device list on all our Macs. Tried a reboot — still not showing.'],
|
|
|
|
// ── Facilities ────────────────────────────────────────────────────
|
|
['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' => 'Request for additional key fob',
|
|
'description' => 'We need an extra key fob for evening custodial coverage at the elementary entrance.'],
|
|
['title' => 'Broken desk in portable 3',
|
|
'description' => 'One of the student desks has a cracked support and is wobbling badly.'],
|
|
['title' => 'Request setup for parent info night',
|
|
'description' => 'Need 80 chairs, podium, projector, and two microphones in the gym for Thursday evening.'],
|
|
['title' => 'Door closer slamming in west hallway',
|
|
'description' => 'The west hallway fire door slams shut hard enough to shake the frame.'],
|
|
['title' => 'Water fountain on second floor leaking',
|
|
'description' => 'There is a slow but steady leak beneath the second-floor bottle filler station.'],
|
|
['title' => 'Bathroom stall door hinge broken — girls\' washroom',
|
|
'description' => 'The middle stall door in the main floor girls\' washroom has a broken hinge and cannot close properly.'],
|
|
['title' => 'Light bulbs out in staff parking lot',
|
|
'description' => 'Three of the five overhead lights in the north staff parking lot are burnt out. Safety concern after dark.'],
|
|
['title' => 'Ceiling tile collapsed in room 112',
|
|
'description' => 'A ceiling tile in the corner of Room 112 fell overnight. Debris on the floor but no one was hurt.'],
|
|
['title' => 'Exterior gate latch not catching',
|
|
'description' => 'The latch on the playground gate by the K-3 yard is not catching properly. Gate swings open on its own.'],
|
|
['title' => 'Plumbing noise in staff washroom',
|
|
'description' => 'There is a loud knocking/banging noise coming from the pipes in the main floor staff washroom when the toilet is flushed.'],
|
|
['title' => 'Cafeteria exhaust fan broken',
|
|
'description' => 'The exhaust fan in the cafeteria kitchen has stopped working. It is getting smoky during lunch prep.'],
|
|
['title' => 'Window blinds broken in Grade 4 room',
|
|
'description' => 'Two of the three window blinds in Grade 4 classroom cannot be lowered. Afternoon sun is making it impossible to see the board.'],
|
|
['title' => 'Graffiti on east exterior wall',
|
|
'description' => 'There is graffiti on the east-facing exterior wall near the secondary entrance that needs to be cleaned before the parent open house.'],
|
|
|
|
// ── HR ────────────────────────────────────────────────────────────
|
|
['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' => 'Benefits question about dependent coverage',
|
|
'description' => 'I need clarification on whether orthodontics falls under the current dependent coverage plan.'],
|
|
['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' => 'Request ergonomic chair assessment',
|
|
'description' => 'I am having back pain and would like an ergonomic review of my workstation setup.'],
|
|
['title' => 'Update emergency contact information',
|
|
'description' => 'I recently moved and need to update my emergency contact and home address on file.'],
|
|
['title' => 'Parental leave documentation request',
|
|
'description' => 'I am planning parental leave starting in June and need to know what forms to submit and by when.'],
|
|
['title' => 'Professional development funding approval',
|
|
'description' => 'I would like to attend a literacy conference in May and need to know if PD funding is still available for this year.'],
|
|
['title' => 'Request for letter of employment',
|
|
'description' => 'My bank is requesting a letter of employment for a mortgage application. Can HR provide this?'],
|
|
['title' => 'Sick leave balance inquiry',
|
|
'description' => 'I am not sure how many sick days I have remaining this year. The employee portal is not showing my balance.'],
|
|
['title' => 'Contract renewal question for part-time staff',
|
|
'description' => 'My contract ends June 30 and I have not received any communication about renewal. Can someone confirm the status?'],
|
|
['title' => 'Request to change payroll deposit account',
|
|
'description' => 'I recently switched banks and need to update my direct deposit information before the next pay cycle.'],
|
|
['title' => 'Reference letter request for former employee',
|
|
'description' => 'A former staff member has asked us to provide a reference letter. Who should handle this and what is the process?'],
|
|
['title' => 'Vacation carryover from previous school year',
|
|
'description' => 'I had unused vacation days from last year that were supposed to carry over. They are not showing up in my current balance.'],
|
|
['title' => 'Volunteer supervision policy clarification',
|
|
'description' => 'We have a parent volunteer joining our classroom regularly. Do they need a criminal record check and what is our sign-in process?'],
|
|
['title' => 'Request for resignation acknowledgement letter',
|
|
'description' => 'I submitted my resignation two weeks ago and have not received any written acknowledgement from the school.'],
|
|
];
|
|
}
|
|
}
|