Initial Ladill Frontdesk release with deploy pipeline.
Visitor management app with SSO, kiosk, badges, reports, and Gitea CI deploy to frontdesk.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Frontdesk\DeviceService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MarkDevicesOfflineCommand extends Command
|
||||
{
|
||||
protected $signature = 'frontdesk:mark-devices-offline {--minutes=10 : Minutes since last heartbeat}';
|
||||
|
||||
protected $description = 'Mark devices without recent heartbeat as offline';
|
||||
|
||||
public function handle(DeviceService $devices): int
|
||||
{
|
||||
$count = $devices->markStaleDevicesOffline((int) $this->option('minutes'));
|
||||
|
||||
$this->info("Marked {$count} device(s) as offline.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Frontdesk\NotificationDispatcher;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MarkExpiredBadgesCommand extends Command
|
||||
{
|
||||
protected $signature = 'frontdesk:mark-expired-badges';
|
||||
|
||||
protected $description = 'Log and alert on checked-in visits with expired badges';
|
||||
|
||||
public function handle(NotificationDispatcher $notifications): int
|
||||
{
|
||||
$visits = Visit::query()
|
||||
->where('status', Visit::STATUS_CHECKED_IN)
|
||||
->whereNotNull('badge_expires_at')
|
||||
->where('badge_expires_at', '<', now())
|
||||
->with(['visitor', 'organization'])
|
||||
->get();
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($visits as $visit) {
|
||||
$alreadyLogged = AuditLog::query()
|
||||
->where('subject_type', Visit::class)
|
||||
->where('subject_id', $visit->id)
|
||||
->where('action', 'badge.expired')
|
||||
->where('created_at', '>=', now()->subDay())
|
||||
->exists();
|
||||
|
||||
if ($alreadyLogged) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AuditLog::record(
|
||||
$visit->owner_ref,
|
||||
'badge.expired',
|
||||
$visit->organization_id,
|
||||
null,
|
||||
Visit::class,
|
||||
$visit->id,
|
||||
[
|
||||
'visitor' => $visit->visitor->full_name,
|
||||
'badge_code' => $visit->badge_code,
|
||||
'expired_at' => $visit->badge_expires_at?->toIso8601String(),
|
||||
],
|
||||
);
|
||||
|
||||
$notifications->badgeExpired($visit);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->info("Recorded {$count} expired badge alert(s).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Frontdesk\VisitLifecycleService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MarkOverdueVisitsCommand extends Command
|
||||
{
|
||||
protected $signature = 'frontdesk:mark-overdue-visits';
|
||||
|
||||
protected $description = 'Mark expected and scheduled visits past their scheduled time as overdue';
|
||||
|
||||
public function handle(VisitLifecycleService $lifecycle): int
|
||||
{
|
||||
$count = $lifecycle->markOverdueVisits();
|
||||
|
||||
$this->info("Marked {$count} visit(s) as overdue.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Services\Comms\EmailService;
|
||||
use App\Services\Frontdesk\ReportService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class SendDailyReportCommand extends Command
|
||||
{
|
||||
protected $signature = 'frontdesk:send-daily-reports';
|
||||
|
||||
protected $description = 'Email daily visitor summary reports to configured org admins';
|
||||
|
||||
public function handle(ReportService $reports, EmailService $email): int
|
||||
{
|
||||
$from = now()->subDay()->startOfDay();
|
||||
$to = now()->subDay()->endOfDay();
|
||||
$sent = 0;
|
||||
|
||||
Organization::query()->each(function (Organization $organization) use ($reports, $email, $from, $to, &$sent) {
|
||||
$recipients = data_get($organization->settings, 'report_daily_recipients', []);
|
||||
if ($recipients === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = $reports->summary($organization->owner_ref, $organization, $from, $to);
|
||||
$subject = "Frontdesk daily summary — {$organization->name} ({$from->toDateString()})";
|
||||
$body = implode("\n", [
|
||||
"Visitors checked in: {$summary['checked_in']}",
|
||||
"Currently unique visitors: {$summary['unique_visitors']}",
|
||||
"Contractors: {$summary['contractors']}",
|
||||
"Deliveries: {$summary['deliveries']}",
|
||||
"Average visit duration (min): {$summary['avg_duration_minutes']}",
|
||||
]);
|
||||
|
||||
foreach ($recipients as $address) {
|
||||
if (is_string($address) && filter_var($address, FILTER_VALIDATE_EMAIL)) {
|
||||
$email->send($address, $subject, $body);
|
||||
$sent++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->info("Sent {$sent} daily report email(s).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user