time_clock_records_panel.php
10.54 KB
<?php
require_once __DIR__ . '/../lib/feature_modules.php';
wp_feature_ensure_time_clock_schema();
global $pdo;
$date = trim((string)($_GET['date'] ?? date('Y-m-d')));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$date = date('Y-m-d');
}
$rows = [];
$photosByEntry = [];
$totals = ['entries' => 0, 'open' => 0, 'closed' => 0, 'hours' => 0.0, 'photos' => 0];
if (isset($pdo) && $pdo instanceof PDO && wp_db_table_exists('time_clock_entries')) {
$stmt = $pdo->prepare("SELECT * FROM time_clock_entries WHERE DATE(clock_in_at) = ? ORDER BY clock_in_at DESC, id DESC");
$stmt->execute([$date]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
if ($rows && wp_db_table_exists('time_clock_photos')) {
$ids = array_map(static fn($r) => (int)$r['id'], $rows);
$place = implode(',', array_fill(0, count($ids), '?'));
$ps = $pdo->prepare("SELECT * FROM time_clock_photos WHERE clock_entry_id IN ($place) ORDER BY created_at DESC, id DESC");
$ps->execute($ids);
foreach (($ps->fetchAll(PDO::FETCH_ASSOC) ?: []) as $photo) {
$photosByEntry[(int)$photo['clock_entry_id']][] = $photo;
}
}
}
function wp_clock_admin_label(string $key): string {
$map = [
'vehicle_walkaround' => 'Vehicle walk-around',
'keys_docs_checked' => 'Keys / docs / fuel card',
'assigned_van_confirmed' => 'Assigned van / team',
'ppe_equipment_ready' => 'PPE / equipment ready',
'van_parked' => 'Van parked correctly',
'locked_disclock' => 'Locked / disc lock',
'inventory_checked' => 'Inventory checked',
'fuel_card_returned' => 'Fuel card returned',
'fuel_added' => 'Fuel question answered',
'job_area_cleared' => 'Job area cleared',
'equipment_returned' => 'Equipment returned',
'issues_reported' => 'Issues reported',
];
return $map[$key] ?? ucwords(str_replace('_', ' ', trim($key)));
}
function wp_clock_decode_meta(?string $json): array {
if (!$json) return [];
$decoded = json_decode((string)$json, true);
return is_array($decoded) ? $decoded : [];
}
function wp_clock_render_check_cards(array $checks, string $title): void {
if (empty($checks)) return;
echo '<div class="clock-record-block"><strong>' . e($title) . '</strong><div class="clock-record-checks">';
foreach ($checks as $key => $passed) {
echo '<div class="clock-record-check"><span>' . e(wp_clock_admin_label((string)$key)) . '</span><strong>' . (!empty($passed) ? 'Done' : 'Missing') . '</strong></div>';
}
echo '</div></div>';
}
?>
<style>
.clock-record-card{margin-bottom:16px;}
.clock-record-top{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:flex-start;}
.clock-record-badges{display:flex;gap:8px;flex-wrap:wrap;}
.clock-record-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin-top:14px;}
.clock-record-stat{border:1px solid var(--border);border-radius:14px;background:rgba(255,255,255,.025);padding:12px;}
.clock-record-stat span{display:block;color:var(--text-muted);font-size:.82rem;text-transform:uppercase;letter-spacing:.04em;margin-bottom:4px;}
.clock-record-stat strong{font-size:1rem;}
.clock-record-block{margin-top:14px;}
.clock-record-checks{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:10px;}
.clock-record-check{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.02);padding:10px 12px;}
.clock-photo-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px;margin-top:10px;}
.clock-photo-card{border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.02);padding:12px;}
@media(max-width:700px){.clock-record-top{display:grid;}.content-header form{width:100%;}.content-header form .btn{width:100%;}.clock-record-grid{grid-template-columns:1fr;}}
</style>
<div class="content-header">
<div>
<h1 class="content-title">⏱️ <?= e($pageTitle ?? 'Time Clock Records') ?></h1>
<p class="content-subtitle"><?= e($pageSubtitle ?? 'Review clock in/out times, worked hours, role checks, odometer readings and uploaded photos for the selected day.') ?></p>
</div>
<form method="GET" class="d-flex gap-2 align-items-end" style="flex-wrap:wrap;">
<div>
<label class="form-label" for="clockDate">Date</label>
<input class="form-control" id="clockDate" type="date" name="date" value="<?= e($date) ?>">
</div>
<button type="submit" class="btn btn-primary">Load Day</button>
</form>
</div>
<?php if (empty($rows)): ?>
<div class="card"><div class="card-body"><p class="text-muted" style="margin:0;">No time clock entries were found for <?= e(date('d/m/Y', strtotime($date))) ?>.</p></div></div>
<?php else: ?>
<?php foreach ($rows as $entry): ?>
<?php
$totals['entries']++;
$isOpen = empty($entry['clock_out_at']) || strtolower((string)($entry['status'] ?? '')) === 'clocked_in';
if ($isOpen) $totals['open']++; else $totals['closed']++;
$hours = 0.0;
if (!empty($entry['clock_in_at']) && !empty($entry['clock_out_at'])) {
$hours = max(0, (strtotime((string)$entry['clock_out_at']) - strtotime((string)$entry['clock_in_at']))) / 3600;
$totals['hours'] += $hours;
}
$entryPhotos = $photosByEntry[(int)$entry['id']] ?? [];
$totals['photos'] += count($entryPhotos);
$startMeta = wp_clock_decode_meta($entry['start_meta_json'] ?? null);
$endMeta = wp_clock_decode_meta($entry['end_meta_json'] ?? null);
?>
<div class="card clock-record-card">
<div class="card-body">
<div class="clock-record-top">
<div>
<h3 style="margin:0 0 6px 0;"><?= e((string)($entry['display_name'] ?: $entry['username'])) ?></h3>
<div class="text-muted"><?= e(ucwords(str_replace('-', ' ', (string)($entry['role_name'] ?: 'staff')))) ?> · <?= e((string)$entry['username']) ?></div>
</div>
<div class="clock-record-badges">
<span class="badge <?= $isOpen ? 'badge-warning' : 'badge-success' ?>"><?= $isOpen ? 'Open shift' : 'Closed shift' ?></span>
<span class="badge badge-secondary"><?= e(number_format($hours, 2)) ?>h</span>
<span class="badge badge-secondary"><?= count($entryPhotos) ?> photo<?= count($entryPhotos) === 1 ? '' : 's' ?></span>
</div>
</div>
<div class="clock-record-grid">
<div class="clock-record-stat"><span>Clock in</span><strong><?= e(!empty($entry['clock_in_at']) ? date('d/m/Y H:i', strtotime((string)$entry['clock_in_at'])) : '—') ?></strong></div>
<div class="clock-record-stat"><span>Clock out</span><strong><?= e(!empty($entry['clock_out_at']) ? date('d/m/Y H:i', strtotime((string)$entry['clock_out_at'])) : '—') ?></strong></div>
<div class="clock-record-stat"><span>Van / assignment</span><strong><?= e((string)($entry['van_label'] ?? $startMeta['van_label'] ?? '—')) ?></strong></div>
<div class="clock-record-stat"><span>Van reg</span><strong><?= e((string)($entry['van_reg'] ?? $startMeta['van_reg'] ?? '—')) ?></strong></div>
<div class="clock-record-stat"><span>Working with</span><strong><?= e((string)($entry['partner_name'] ?? $startMeta['partner_name'] ?? '—')) ?></strong></div>
<div class="clock-record-stat"><span>Odometer in</span><strong><?= e((string)($entry['mileage_in'] ?? $startMeta['mileage_in'] ?? '—')) ?></strong></div>
<div class="clock-record-stat"><span>Odometer out</span><strong><?= e((string)($entry['mileage_out'] ?? $endMeta['mileage_out'] ?? '—')) ?></strong></div>
<div class="clock-record-stat"><span>Status</span><strong><?= e((string)($entry['status'] ?: '—')) ?></strong></div>
</div>
<?php if (!empty($startMeta['note']) || !empty($entry['notes'])): ?>
<div class="clock-record-block"><strong>Notes</strong><p class="text-muted" style="margin:8px 0 0;"><?= e((string)($entry['notes'] ?: ($startMeta['note'] ?? '—'))) ?></p></div>
<?php endif; ?>
<?php wp_clock_render_check_cards((array)($startMeta['checks'] ?? []), 'Start-of-shift checks'); ?>
<?php wp_clock_render_check_cards((array)($endMeta['checks'] ?? []), 'End-of-shift checks'); ?>
<?php if (!empty($entryPhotos)): ?>
<div class="clock-record-block">
<strong>Uploaded photos</strong>
<div class="clock-photo-grid">
<?php foreach ($entryPhotos as $photo): ?>
<div class="clock-photo-card">
<div><?= e((string)$photo['file_name']) ?></div>
<div class="text-muted"><?= e((string)$photo['phase']) ?> · <?= e(date('d/m H:i', strtotime((string)$photo['created_at']))) ?></div>
<?php if (!empty($photo['onedrive_web_url'])): ?><a class="btn btn-sm btn-secondary" style="margin-top:8px;" target="_blank" rel="noopener" href="<?= e((string)$photo['onedrive_web_url']) ?>">Open photo</a><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<div class="card">
<div class="card-body" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;">
<div><div class="text-muted">Entries</div><div style="font-size:1.5rem;font-weight:800;"><?= (int)$totals['entries'] ?></div></div>
<div><div class="text-muted">Open shifts</div><div style="font-size:1.5rem;font-weight:800;"><?= (int)$totals['open'] ?></div></div>
<div><div class="text-muted">Closed shifts</div><div style="font-size:1.5rem;font-weight:800;"><?= (int)$totals['closed'] ?></div></div>
<div><div class="text-muted">Worked hours</div><div style="font-size:1.5rem;font-weight:800;"><?= e(number_format($totals['hours'], 2)) ?></div></div>
<div><div class="text-muted">Photos</div><div style="font-size:1.5rem;font-weight:800;"><?= (int)$totals['photos'] ?></div></div>
</div>
</div>
<?php endif; ?>