Pages Medium
Staff Clock-In Terminal
Touch-friendly staff clock-in/out terminal: employee grid, PIN entry, active shift display, break tracking, and an end-of-shift hours summary.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* =========================================================
Staff Clock-In Terminal — Phase 27 Restaurant Theme
========================================================= */
:root {
--cream: #FAF7F1;
--ink: #2C1A0E;
--forest: #345F40;
--forest-d: #213D29;
--terracotta: #C4622D;
--gold: #D4A853;
--warm-gray: #8A7D72;
--bone: #F0EBE0;
--font-display: 'Playfair Display', Georgia, serif;
--font-body: 'Inter', system-ui, sans-serif;
--radius-card: 16px;
--radius-btn: 12px;
--radius-chip: 16px;
--transition: 0.2s ease;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: var(--forest-d);
font-family: var(--font-body);
color: var(--bone);
-webkit-font-smoothing: antialiased;
}
/* =========================================================
Screen system
========================================================= */
.screen {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
transform: translateY(12px);
}
.screen.active {
opacity: 1;
pointer-events: all;
transform: translateY(0);
}
/* =========================================================
Home screen
========================================================= */
[data-screen="home"] {
background: var(--forest-d);
}
.terminal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 28px 16px;
border-bottom: 1px solid rgba(240, 235, 224, 0.1);
}
.header-brand {
display: flex;
align-items: center;
gap: 10px;
font-family: var(--font-display);
font-size: 1.3rem;
font-weight: 700;
color: var(--gold);
}
.brand-icon {
font-size: 1.4rem;
}
.header-clock {
font-size: 1.8rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--bone);
letter-spacing: 0.02em;
}
.home-welcome {
text-align: center;
padding: 28px 20px 12px;
}
.welcome-heading {
font-family: var(--font-display);
font-size: 2.2rem;
font-weight: 800;
color: var(--cream);
line-height: 1.1;
}
.welcome-sub {
margin-top: 8px;
font-size: 0.95rem;
color: var(--warm-gray);
font-weight: 400;
}
.staff-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 20px 20px 32px;
justify-items: center;
}
@media (min-width: 480px) {
.staff-grid {
grid-template-columns: repeat(3, 1fr);
padding: 20px 32px 40px;
}
}
/* Staff chip */
.staff-chip {
width: 120px;
height: 120px;
background: var(--bone);
border-radius: var(--radius-chip);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: transform var(--transition), border-color var(--transition), box-shadow var(--transition);
position: relative;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.staff-chip:active {
transform: scale(0.95);
}
.staff-chip:hover {
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(212, 168, 83, 0.15);
}
.staff-chip.chip--in {
background: var(--cream);
}
.staff-chip.chip--out {
background: rgba(240, 235, 224, 0.55);
}
.chip-avatar {
width: 52px;
height: 52px;
border-radius: 50%;
background: var(--forest);
color: var(--bone);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.chip--in .chip-avatar {
background: var(--forest);
}
.chip--out .chip-avatar {
background: var(--warm-gray);
}
.chip-name {
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
text-align: center;
line-height: 1.2;
padding: 0 6px;
}
.chip-status-dot {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--bone);
}
.chip-status-dot.dot--in {
background: #3DAA62;
}
.chip-status-dot.dot--out {
background: var(--warm-gray);
}
.chip-break-badge {
position: absolute;
top: 5px;
left: 5px;
background: var(--gold);
color: var(--ink);
font-size: 0.55rem;
font-weight: 700;
padding: 2px 5px;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* =========================================================
PIN Screen
========================================================= */
[data-screen="pin"] {
background: var(--forest-d);
align-items: center;
justify-content: center;
}
.pin-screen-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
width: 100%;
max-width: 340px;
padding: 24px 20px 32px;
}
.pin-staff-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: var(--forest);
border: 3px solid var(--gold);
color: var(--bone);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 14px;
}
.pin-staff-name {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 700;
color: var(--cream);
text-align: center;
}
.pin-staff-role {
font-size: 0.82rem;
color: var(--warm-gray);
font-weight: 500;
margin-top: 4px;
margin-bottom: 20px;
text-align: center;
}
.pin-label {
font-size: 0.9rem;
color: var(--bone);
font-weight: 500;
margin-bottom: 14px;
opacity: 0.8;
}
.pin-dots {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.pin-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid var(--bone);
background: transparent;
transition: background var(--transition);
}
.pin-dot.filled {
background: var(--bone);
}
.pin-error {
font-size: 0.8rem;
color: var(--terracotta);
font-weight: 600;
min-height: 20px;
margin-bottom: 20px;
opacity: 0;
transition: opacity 0.2s;
text-align: center;
}
.pin-error.visible {
opacity: 1;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-6px); }
80% { transform: translateX(6px); }
}
.shake {
animation: shake 0.35s ease;
}
/* Numpad */
.numpad {
display: grid;
grid-template-columns: repeat(3, 72px);
gap: 10px;
margin-bottom: 20px;
}
.numpad-key {
width: 72px;
height: 72px;
border-radius: var(--radius-btn);
background: var(--forest-d);
border: 1.5px solid rgba(240, 235, 224, 0.12);
color: var(--bone);
font-size: 1.5rem;
font-weight: 600;
font-family: var(--font-body);
cursor: pointer;
transition: background var(--transition), transform var(--transition);
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
.numpad-key:hover {
background: rgba(52, 95, 64, 0.6);
}
.numpad-key:active {
transform: scale(0.92);
background: var(--forest);
}
.numpad-key--symbol {
font-size: 1.1rem;
opacity: 0.4;
cursor: default;
}
.numpad-key--back {
font-size: 1.3rem;
}
/* =========================================================
Shared action screen layout
========================================================= */
[data-screen="action-out"],
[data-screen="action-in"],
[data-screen="break"],
[data-screen="confirm"] {
background: var(--forest-d);
align-items: center;
justify-content: center;
}
.action-screen-inner {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 360px;
padding: 32px 24px 40px;
}
.action-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: var(--forest);
color: var(--bone);
font-size: 1.5rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
border: 3px solid rgba(240, 235, 224, 0.15);
}
.action-avatar--active {
border-color: #3DAA62;
box-shadow: 0 0 0 4px rgba(61, 170, 98, 0.15);
}
.action-avatar--break {
border-color: var(--gold);
box-shadow: 0 0 0 4px rgba(212, 168, 83, 0.15);
}
.status-badge {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 4px 12px;
border-radius: 100px;
margin-bottom: 10px;
}
.status-badge--active {
background: rgba(61, 170, 98, 0.18);
color: #5CC87B;
border: 1px solid rgba(61, 170, 98, 0.3);
}
.status-badge--break {
background: rgba(212, 168, 83, 0.18);
color: var(--gold);
border: 1px solid rgba(212, 168, 83, 0.3);
}
.action-greeting {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 700;
color: var(--cream);
text-align: center;
margin-bottom: 6px;
}
.action-time {
font-size: 2.8rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--bone);
letter-spacing: 0.02em;
margin-bottom: 10px;
}
.role-badge {
font-size: 0.78rem;
font-weight: 600;
color: var(--warm-gray);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 20px;
}
.action-hint {
font-size: 0.9rem;
color: var(--warm-gray);
margin-bottom: 24px;
text-align: center;
}
/* Shift info block */
.shift-info {
width: 100%;
background: rgba(52, 95, 64, 0.25);
border: 1px solid rgba(240, 235, 224, 0.1);
border-radius: var(--radius-card);
padding: 16px 20px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.shift-info-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.shift-label {
font-size: 0.82rem;
color: var(--warm-gray);
font-weight: 500;
}
.shift-val {
font-size: 0.95rem;
color: var(--bone);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* Action buttons */
.action-buttons {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 14px;
}
.btn-action {
width: 100%;
height: 64px;
border-radius: var(--radius-btn);
border: none;
font-family: var(--font-body);
font-size: 1.05rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: transform var(--transition), opacity var(--transition);
-webkit-tap-highlight-color: transparent;
}
.btn-action:active {
transform: scale(0.97);
opacity: 0.88;
}
.btn-clockin {
background: var(--bone);
color: var(--forest-d);
}
.btn-clockout {
background: var(--terracotta);
color: var(--cream);
}
.btn-break {
background: var(--gold);
color: var(--ink);
}
.btn-endbreak {
background: var(--bone);
color: var(--forest-d);
margin-bottom: 14px;
}
.btn-done {
background: var(--forest);
color: var(--bone);
}
.btn-icon {
font-size: 1.2rem;
}
.btn-cancel {
width: 100%;
height: 48px;
border-radius: var(--radius-btn);
background: transparent;
border: 1.5px solid rgba(240, 235, 224, 0.2);
color: var(--warm-gray);
font-family: var(--font-body);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: border-color var(--transition), color var(--transition);
margin-top: 4px;
-webkit-tap-highlight-color: transparent;
}
.btn-cancel:hover {
border-color: rgba(240, 235, 224, 0.4);
color: var(--bone);
}
.btn-cancel--light {
border-color: rgba(240, 235, 224, 0.15);
}
/* =========================================================
Break timer
========================================================= */
.break-timer-label {
font-size: 0.82rem;
color: var(--warm-gray);
font-weight: 500;
margin-bottom: 8px;
margin-top: 16px;
}
.break-timer {
font-size: 3.2rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--gold);
letter-spacing: 0.04em;
margin-bottom: 28px;
}
/* =========================================================
Confirmation screen
========================================================= */
.confirm-icon {
width: 72px;
height: 72px;
border-radius: 50%;
background: rgba(61, 170, 98, 0.15);
border: 2px solid rgba(61, 170, 98, 0.4);
color: #5CC87B;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 18px;
}
.confirm-icon.icon--out {
background: rgba(196, 98, 45, 0.15);
border-color: rgba(196, 98, 45, 0.4);
color: var(--terracotta);
}
.confirm-title {
font-family: var(--font-display);
font-size: 1.8rem;
font-weight: 800;
color: var(--cream);
text-align: center;
margin-bottom: 6px;
}
.confirm-sub {
font-size: 0.9rem;
color: var(--warm-gray);
text-align: center;
margin-bottom: 24px;
}
.confirm-summary {
width: 100%;
background: rgba(52, 95, 64, 0.2);
border: 1px solid rgba(240, 235, 224, 0.1);
border-radius: var(--radius-card);
padding: 16px 20px;
margin-bottom: 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.summary-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.summary-label {
font-size: 0.82rem;
color: var(--warm-gray);
font-weight: 500;
}
.summary-val {
font-size: 0.95rem;
color: var(--bone);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* =========================================================
Scrollbar
========================================================= */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(240, 235, 224, 0.15);
border-radius: 2px;
}/* =========================================================
Staff Clock-In Terminal — script.js
Phase 27 Restaurant Theme
========================================================= */
/* ----------------------------------------------------------
Staff data
---------------------------------------------------------- */
const STAFF = [
{ id: 1, name: 'Marco Reyes', role: 'Floor Manager', pin: '1111', status: 'in', clockedAt: '19:02', onBreak: false, breakStart: null },
{ id: 2, name: 'Sofía Medina', role: 'Head Chef', pin: '2222', status: 'in', clockedAt: '15:08', onBreak: false, breakStart: null },
{ id: 3, name: 'Diego Lara', role: 'Sous Chef', pin: '3333', status: 'out', clockedAt: null, onBreak: false, breakStart: null },
{ id: 4, name: 'Camila Torres', role: 'Waitstaff', pin: '4444', status: 'out', clockedAt: null, onBreak: false, breakStart: null },
{ id: 5, name: 'Julián Ortiz', role: 'Bartender', pin: '5555', status: 'in', clockedAt: '18:03', onBreak: true, breakStart: '20:30' },
{ id: 6, name: 'Ana Petit', role: 'Waitstaff', pin: '6666', status: 'out', clockedAt: null, onBreak: false, breakStart: null },
];
/* ----------------------------------------------------------
Live clock — fixed base 20:46, advances with real elapsed time
---------------------------------------------------------- */
const BASE_MINUTES = 20 * 60 + 46; // 20:46 in minutes
const SESSION_START = Date.now();
function getNowMinutes() {
const elapsed = Math.floor((Date.now() - SESSION_START) / 1000); // seconds
return BASE_MINUTES + Math.floor(elapsed / 60);
}
function minutesToHHMM(totalMinutes) {
const h = Math.floor(totalMinutes / 60) % 24;
const m = totalMinutes % 60;
return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
}
function currentTimeStr() {
return minutesToHHMM(getNowMinutes());
}
function updateClock() {
const el = document.getElementById('header-clock');
if (el) el.textContent = currentTimeStr();
}
setInterval(updateClock, 30000);
/* ----------------------------------------------------------
State
---------------------------------------------------------- */
let selectedStaff = null;
let enteredPin = '';
let breakIntervalId = null;
let breakElapsedSeconds = 0;
/* ----------------------------------------------------------
Screen management
---------------------------------------------------------- */
function showScreen(name) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const target = document.querySelector(`[data-screen="${name}"]`);
if (target) target.classList.add('active');
}
/* ----------------------------------------------------------
Helpers
---------------------------------------------------------- */
function initials(name) {
return name.split(' ').map(p => p[0]).join('').slice(0, 2).toUpperCase();
}
function greetingByHour() {
const h = Math.floor(getNowMinutes() / 60) % 24;
if (h < 12) return 'Good morning';
if (h < 18) return 'Good afternoon';
return 'Good evening';
}
/** Parse HH:MM string → total minutes */
function parseTime(hhmm) {
if (!hhmm) return 0;
const [h, m] = hhmm.split(':').map(Number);
return h * 60 + m;
}
/** Compute duration between two HH:MM strings (handles overnight if needed) */
function durationStr(fromHHMM, toHHMM) {
let diff = parseTime(toHHMM) - parseTime(fromHHMM);
if (diff < 0) diff += 24 * 60; // overnight
const h = Math.floor(diff / 60);
const m = diff % 60;
if (h === 0) return `${m}m`;
if (m === 0) return `${h}h`;
return `${h}h ${m}m`;
}
function fmtSeconds(secs) {
const m = Math.floor(secs / 60);
const s = secs % 60;
return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
/* ----------------------------------------------------------
Home screen — staff grid render
---------------------------------------------------------- */
function renderGrid() {
const grid = document.getElementById('staff-grid');
grid.innerHTML = '';
STAFF.forEach(member => {
const chip = document.createElement('button');
chip.className = `staff-chip chip--${member.status}`;
chip.setAttribute('aria-label', `${member.name}, ${member.status === 'in' ? 'clocked in' : 'clocked out'}`);
let breakBadge = '';
if (member.onBreak) {
breakBadge = `<span class="chip-break-badge">Break</span>`;
}
chip.innerHTML = `
${breakBadge}
<div class="chip-avatar">${initials(member.name)}</div>
<div class="chip-name">${member.name.split(' ')[0]}<br>${member.name.split(' ')[1] || ''}</div>
<span class="chip-status-dot dot--${member.status}"></span>
`;
chip.addEventListener('click', () => onStaffSelect(member));
grid.appendChild(chip);
});
}
/* ----------------------------------------------------------
Staff tap → PIN screen
---------------------------------------------------------- */
function onStaffSelect(member) {
selectedStaff = member;
enteredPin = '';
document.getElementById('pin-avatar').textContent = initials(member.name);
document.getElementById('pin-name').textContent = member.name;
document.getElementById('pin-role').textContent = member.role;
clearPinDots();
hidePinError();
showScreen('pin');
}
/* ----------------------------------------------------------
PIN entry
---------------------------------------------------------- */
function clearPinDots() {
document.querySelectorAll('.pin-dot').forEach(d => d.classList.remove('filled'));
}
function updatePinDots() {
const dots = document.querySelectorAll('.pin-dot');
dots.forEach((d, i) => d.classList.toggle('filled', i < enteredPin.length));
}
function showPinError() {
const el = document.getElementById('pin-error');
el.classList.add('visible');
const dots = document.getElementById('pin-dots');
dots.classList.add('shake');
dots.addEventListener('animationend', () => dots.classList.remove('shake'), { once: true });
}
function hidePinError() {
document.getElementById('pin-error').classList.remove('visible');
}
function onDigit(d) {
if (enteredPin.length >= 4) return;
hidePinError();
enteredPin += d;
updatePinDots();
if (enteredPin.length === 4) {
setTimeout(validatePin, 120);
}
}
function onBackspace() {
if (enteredPin.length === 0) return;
enteredPin = enteredPin.slice(0, -1);
updatePinDots();
hidePinError();
}
function validatePin() {
if (!selectedStaff) return;
if (enteredPin === selectedStaff.pin) {
enteredPin = '';
clearPinDots();
navigateToAction(selectedStaff);
} else {
enteredPin = '';
clearPinDots();
showPinError();
}
}
/* ----------------------------------------------------------
Navigate to appropriate action screen
---------------------------------------------------------- */
function navigateToAction(member) {
if (member.status === 'out') {
// Clock-in screen
document.getElementById('action-out-avatar').textContent = initials(member.name);
document.getElementById('action-out-greeting').textContent = `${greetingByHour()}, ${member.name.split(' ')[0]}`;
document.getElementById('action-out-time').textContent = currentTimeStr();
document.getElementById('action-out-role').textContent = member.role;
showScreen('action-out');
} else if (member.onBreak) {
// On break screen
document.getElementById('break-avatar').textContent = initials(member.name);
document.getElementById('break-name').textContent = member.name;
startBreakTimer(member);
showScreen('break');
} else {
// Clocked in screen
document.getElementById('action-in-avatar').textContent = initials(member.name);
document.getElementById('action-in-greeting').textContent = member.name;
document.getElementById('action-in-role').textContent = member.role;
document.getElementById('action-in-clockedat').textContent = member.clockedAt;
document.getElementById('action-in-duration').textContent = durationStr(member.clockedAt, currentTimeStr());
showScreen('action-in');
}
}
/* ----------------------------------------------------------
Clock In
---------------------------------------------------------- */
function clockIn() {
if (!selectedStaff) return;
const now = currentTimeStr();
selectedStaff.status = 'in';
selectedStaff.clockedAt = now;
selectedStaff.onBreak = false;
selectedStaff.breakStart = null;
showConfirmation({
type: 'in',
title: 'Clocked In!',
sub: `Shift started at ${now}`,
rows: [
{ label: 'Staff', val: selectedStaff.name },
{ label: 'Role', val: selectedStaff.role },
{ label: 'Clock-in time', val: now },
]
});
}
/* ----------------------------------------------------------
Clock Out
---------------------------------------------------------- */
function clockOut() {
if (!selectedStaff) return;
const now = currentTimeStr();
const duration = durationStr(selectedStaff.clockedAt, now);
const from = selectedStaff.clockedAt;
selectedStaff.status = 'out';
selectedStaff.clockedAt = null;
selectedStaff.onBreak = false;
selectedStaff.breakStart = null;
showConfirmation({
type: 'out',
title: 'Signed Off',
sub: 'Good work today!',
rows: [
{ label: 'Staff', val: selectedStaff.name },
{ label: 'Clocked in', val: from },
{ label: 'Clocked out', val: now },
{ label: 'Total hours', val: duration },
]
});
}
/* ----------------------------------------------------------
Break management
---------------------------------------------------------- */
function startBreak() {
if (!selectedStaff) return;
const now = currentTimeStr();
selectedStaff.onBreak = true;
selectedStaff.breakStart = now;
document.getElementById('break-avatar').textContent = initials(selectedStaff.name);
document.getElementById('break-name').textContent = selectedStaff.name;
breakElapsedSeconds = 0;
document.getElementById('break-timer').textContent = fmtSeconds(0);
startBreakTimer(selectedStaff);
showScreen('break');
}
function startBreakTimer(member) {
clearInterval(breakIntervalId);
// If member already on break, compute elapsed from breakStart
if (member.breakStart) {
const startMin = parseTime(member.breakStart);
const nowMin = getNowMinutes();
let diff = nowMin - startMin;
if (diff < 0) diff += 24 * 60;
breakElapsedSeconds = diff * 60;
} else {
breakElapsedSeconds = 0;
}
document.getElementById('break-timer').textContent = fmtSeconds(breakElapsedSeconds);
breakIntervalId = setInterval(() => {
breakElapsedSeconds++;
document.getElementById('break-timer').textContent = fmtSeconds(breakElapsedSeconds);
}, 1000);
}
function endBreak() {
clearInterval(breakIntervalId);
if (!selectedStaff) return;
const breakDuration = fmtSeconds(breakElapsedSeconds);
selectedStaff.onBreak = false;
selectedStaff.breakStart = null;
showConfirmation({
type: 'in',
title: 'Break Ended',
sub: 'Welcome back!',
rows: [
{ label: 'Staff', val: selectedStaff.name },
{ label: 'Break duration', val: breakDuration },
{ label: 'Back on shift', val: currentTimeStr() },
]
});
}
/* ----------------------------------------------------------
Confirmation screen
---------------------------------------------------------- */
function showConfirmation({ type, title, sub, rows }) {
const icon = document.getElementById('confirm-icon');
icon.textContent = type === 'in' ? '✓' : '✕';
icon.className = type === 'in' ? 'confirm-icon' : 'confirm-icon icon--out';
document.getElementById('confirm-title').textContent = title;
document.getElementById('confirm-sub').textContent = sub;
const summary = document.getElementById('confirm-summary');
summary.innerHTML = rows.map(r => `
<div class="summary-row">
<span class="summary-label">${r.label}</span>
<span class="summary-val">${r.val}</span>
</div>
`).join('');
showScreen('confirm');
}
/* ----------------------------------------------------------
Event wiring
---------------------------------------------------------- */
function init() {
updateClock();
renderGrid();
// Numpad
document.getElementById('numpad').addEventListener('click', e => {
const key = e.target.closest('.numpad-key');
if (!key) return;
const k = key.dataset.key;
if (k === 'back') onBackspace();
else if (k === '*') { /* no-op */ }
else onDigit(k);
});
// PIN cancel
document.getElementById('btn-cancel').addEventListener('click', () => {
enteredPin = '';
clearPinDots();
hidePinError();
showScreen('home');
});
// Action-out screen
document.getElementById('btn-clockin').addEventListener('click', clockIn);
document.getElementById('btn-action-out-cancel').addEventListener('click', () => showScreen('home'));
// Action-in screen
document.getElementById('btn-clockout').addEventListener('click', clockOut);
document.getElementById('btn-startbreak').addEventListener('click', startBreak);
document.getElementById('btn-action-in-cancel').addEventListener('click', () => showScreen('home'));
// Break screen
document.getElementById('btn-endbreak').addEventListener('click', endBreak);
document.getElementById('btn-break-cancel').addEventListener('click', () => {
clearInterval(breakIntervalId);
showScreen('home');
});
// Confirm / done
document.getElementById('btn-done').addEventListener('click', () => {
renderGrid(); // refresh home grid with updated state
showScreen('home');
selectedStaff = null;
});
}
document.addEventListener('DOMContentLoaded', init);<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Staff Clock-In Terminal</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- SCREEN 1: Home -->
<div data-screen="home" class="screen active">
<header class="terminal-header">
<div class="header-brand">
<span class="brand-icon">🍽️</span>
<span class="brand-name">La Mesa</span>
</div>
<div class="header-clock" id="header-clock">20:46</div>
</header>
<div class="home-welcome">
<h1 class="welcome-heading">Welcome</h1>
<p class="welcome-sub">Tap your name to clock in or out</p>
</div>
<div class="staff-grid" id="staff-grid">
<!-- Staff chips rendered by JS -->
</div>
</div>
<!-- SCREEN 2: PIN Entry -->
<div data-screen="pin" class="screen">
<div class="pin-screen-inner">
<div class="pin-staff-avatar" id="pin-avatar">MR</div>
<div class="pin-staff-name" id="pin-name">Marco Reyes</div>
<div class="pin-staff-role" id="pin-role">Floor Manager</div>
<p class="pin-label">Enter your PIN</p>
<div class="pin-dots" id="pin-dots">
<span class="pin-dot"></span>
<span class="pin-dot"></span>
<span class="pin-dot"></span>
<span class="pin-dot"></span>
</div>
<div class="pin-error" id="pin-error">Incorrect PIN. Try again.</div>
<div class="numpad" id="numpad">
<button class="numpad-key" data-key="1">1</button>
<button class="numpad-key" data-key="2">2</button>
<button class="numpad-key" data-key="3">3</button>
<button class="numpad-key" data-key="4">4</button>
<button class="numpad-key" data-key="5">5</button>
<button class="numpad-key" data-key="6">6</button>
<button class="numpad-key" data-key="7">7</button>
<button class="numpad-key" data-key="8">8</button>
<button class="numpad-key" data-key="9">9</button>
<button class="numpad-key numpad-key--symbol" data-key="*">✱</button>
<button class="numpad-key" data-key="0">0</button>
<button class="numpad-key numpad-key--back" data-key="back">⌫</button>
</div>
<button class="btn-cancel" id="btn-cancel">Cancel</button>
</div>
</div>
<!-- SCREEN 3: Action (clocked-out state — clock in) -->
<div data-screen="action-out" class="screen">
<div class="action-screen-inner">
<div class="action-avatar" id="action-out-avatar">MR</div>
<h2 class="action-greeting" id="action-out-greeting">Good evening, Marco</h2>
<div class="action-time" id="action-out-time">20:46</div>
<div class="role-badge" id="action-out-role">Floor Manager</div>
<p class="action-hint">Ready to start your shift?</p>
<button class="btn-action btn-clockin" id="btn-clockin">
<span>Clock In</span>
<span class="btn-icon">→</span>
</button>
<button class="btn-cancel btn-cancel--light" id="btn-action-out-cancel">Cancel</button>
</div>
</div>
<!-- SCREEN 4: Action (clocked-in state) -->
<div data-screen="action-in" class="screen">
<div class="action-screen-inner">
<div class="action-avatar action-avatar--active" id="action-in-avatar">MR</div>
<div class="status-badge status-badge--active">On Shift</div>
<h2 class="action-greeting" id="action-in-greeting">Marco Reyes</h2>
<div class="role-badge" id="action-in-role">Floor Manager</div>
<div class="shift-info">
<div class="shift-info-row">
<span class="shift-label">Clocked in</span>
<span class="shift-val" id="action-in-clockedat">19:02</span>
</div>
<div class="shift-info-row">
<span class="shift-label">Time on shift</span>
<span class="shift-val" id="action-in-duration">1h 44m</span>
</div>
</div>
<div class="action-buttons">
<button class="btn-action btn-clockout" id="btn-clockout">Clock Out</button>
<button class="btn-action btn-break" id="btn-startbreak">Start Break</button>
</div>
<button class="btn-cancel btn-cancel--light" id="btn-action-in-cancel">Back</button>
</div>
</div>
<!-- SCREEN 5: Break screen -->
<div data-screen="break" class="screen">
<div class="action-screen-inner">
<div class="action-avatar action-avatar--break" id="break-avatar">JO</div>
<div class="status-badge status-badge--break">On Break</div>
<h2 class="action-greeting" id="break-name">Julián Ortiz</h2>
<div class="break-timer-label">Break duration</div>
<div class="break-timer" id="break-timer">00:00</div>
<button class="btn-action btn-endbreak" id="btn-endbreak">End Break</button>
<button class="btn-cancel btn-cancel--light" id="btn-break-cancel">Back</button>
</div>
</div>
<!-- SCREEN 6: Confirmation -->
<div data-screen="confirm" class="screen">
<div class="action-screen-inner">
<div class="confirm-icon" id="confirm-icon">✓</div>
<h2 class="confirm-title" id="confirm-title">Clocked In!</h2>
<p class="confirm-sub" id="confirm-sub">Your shift has started.</p>
<div class="confirm-summary" id="confirm-summary">
<!-- Summary rows injected by JS -->
</div>
<button class="btn-action btn-done" id="btn-done">Done</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Staff Clock-In Terminal
Kiosk-style terminal. Home screen: grid of staff avatar chips (initials + name + status dot: clocked-in green / out gray). Tap a name → PIN entry screen (4-digit keypad). Correct PIN → if clocked out: clock-in confirmation with current time + role + shift summary; if clocked in: options (Clock out / Start break / End break). Break tracking shows break duration. End-of-shift screen shows total hours and a “Sign off” confirmation.