feat(fusion_clock): NFC kiosk — enrollment, manager page, sounds, lock, profile photos
Kiosk work across this session (19.0.3.6.0 -> 19.0.3.10.0): - Program-from-unknown-tap: amber prompt -> Manager PIN -> pick/create employee -> binds the captured UID (no re-tap). Reassign moves a card between employees. - Manager page (gear, when unlocked): search employees + tag status; assign/re-tag, clear tag, archive employee, + new employee. Server-gated by the enroll password. - Screen lock: kiosk starts locked (tap-only); Unlock -> Manager PIN, Lock button; PIN remembered for the session so the gear never re-prompts. - Sounds: pleasant + loud sine chimes (rising in / descending out) + a low "denied" tone for wrong/unknown taps. Gated by fusion_clock.enable_sounds. - Guided profile-photo capture for employees with no picture (clock-in or enroll): live camera + oval face guide -> capture -> preview -> save to hr.employee. - PIN no longer re-renders per digit; centered result card; 12h time; clock-out shows "Worked Xh Ym this shift"; modern clock idle icon; faster animations/result timers; session keep-alive so the kiosk login never expires. - New endpoints: create_employee, clear_tag, delete_employee (archive), verify_pin, save_profile_photo; enroll gains force-reassign. - Docs: fusion_clock is now developed in Claude Code (dropped Cursor references). Spec/plan under fusion_clock/docs/superpowers/. Deployed live on entech (odoo-entech / LXC 111 on pve-worker5), v19.0.3.10.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,7 @@ html:has(#nfc_kiosk_root) {
|
||||
padding: 2rem;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overflow: hidden;
|
||||
background: var(--nfc-bg);
|
||||
@@ -106,18 +107,19 @@ html:has(#nfc_kiosk_root) {
|
||||
top: 1.25rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-height: 52px;
|
||||
max-width: 220px;
|
||||
max-height: 64px;
|
||||
max-width: 260px;
|
||||
object-fit: contain;
|
||||
background: rgba(255, 255, 255, 0.20);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
padding: 0.5rem 0.85rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
padding: 0.6rem 1.1rem;
|
||||
border-radius: 1rem;
|
||||
border: 2px solid hsla(var(--nfc-h), 85%, 72%, 0.95);
|
||||
box-shadow:
|
||||
0 6px 24px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
0 8px 28px rgba(0, 0, 0, 0.4),
|
||||
0 0 30px hsla(var(--nfc-h), 90%, 62%, 0.6),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
box-sizing: content-box;
|
||||
animation: nfc-logo-in 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
@@ -190,6 +192,29 @@ html:has(#nfc_kiosk_root) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.nfc-kiosk__lock {
|
||||
position: absolute;
|
||||
bottom: 1.5rem;
|
||||
right: 4.85rem; // sits just left of the ⚙
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.04);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
color: var(--nfc-text-muted);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
cursor: pointer;
|
||||
font-size: 1.05rem;
|
||||
display: none; // JS shows it only when unlocked
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
// Unlock button sits in the primary bottom-right corner (shown only while locked)
|
||||
#nfc_unlock_btn { right: 1.5rem; }
|
||||
|
||||
.nfc-kiosk__settings {
|
||||
position: absolute;
|
||||
bottom: 1.5rem;
|
||||
@@ -234,7 +259,7 @@ html:has(#nfc_kiosk_root) {
|
||||
// State container — base fade-in for whatever child renders
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
#nfc_state_container > * {
|
||||
animation: nfc-state-in 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
animation: nfc-state-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes nfc-state-in {
|
||||
@@ -251,6 +276,7 @@ html:has(#nfc_kiosk_root) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
margin-top: 7rem; // push icon + prompt down so waves clear the clock/time
|
||||
}
|
||||
|
||||
.nfc-kiosk__icon-svg {
|
||||
@@ -263,6 +289,7 @@ html:has(#nfc_kiosk_root) {
|
||||
.nfc-chip {
|
||||
animation: nfc-chip-pulse 2.5s ease-in-out infinite;
|
||||
transform-origin: center;
|
||||
transform-box: fill-box;
|
||||
}
|
||||
|
||||
.nfc-wave {
|
||||
@@ -340,8 +367,10 @@ html:has(#nfc_kiosk_root) {
|
||||
max-width: 720px;
|
||||
padding: 2.5rem 3rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
text-align: center;
|
||||
gap: 1.25rem;
|
||||
position: relative;
|
||||
|
||||
&--success {
|
||||
@@ -350,7 +379,7 @@ html:has(#nfc_kiosk_root) {
|
||||
0 20px 60px rgba(0,0,0,0.5),
|
||||
0 0 80px rgba(24,169,87,0.35),
|
||||
inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
animation: nfc-success-burst 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
animation: nfc-success-burst 350ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
&--error {
|
||||
@@ -359,7 +388,7 @@ html:has(#nfc_kiosk_root) {
|
||||
0 20px 60px rgba(0,0,0,0.5),
|
||||
0 0 60px rgba(217,55,78,0.3),
|
||||
inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
animation: nfc-shake 350ms ease-in-out, nfc-state-in 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
animation: nfc-shake 350ms ease-in-out, nfc-state-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +435,7 @@ html:has(#nfc_kiosk_root) {
|
||||
flex-shrink: 0;
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
animation: nfc-avatar-in 600ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
animation: nfc-avatar-in 300ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
|
||||
@keyframes nfc-avatar-in {
|
||||
@@ -415,11 +444,11 @@ html:has(#nfc_kiosk_root) {
|
||||
}
|
||||
|
||||
.nfc-kiosk__result-text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
.name { font-size: 2.25rem; font-weight: 600; letter-spacing: -0.02em; }
|
||||
.action { font-size: 1.5rem; margin-top: 0.5rem; opacity: 0.95; font-weight: 400; }
|
||||
.hours { font-size: 1.05rem; opacity: 0.75; margin-top: 0.5rem; }
|
||||
.hours { font-size: 1.35rem; opacity: 0.9; margin-top: 0.6rem; font-weight: 500; }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@@ -477,9 +506,11 @@ html:has(#nfc_kiosk_root) {
|
||||
|
||||
.nfc-kiosk__enroll-panel {
|
||||
@extend %nfc-glass;
|
||||
padding: 2.5rem;
|
||||
padding: 2rem;
|
||||
width: 80vw;
|
||||
max-width: 720px;
|
||||
max-height: 92vh;
|
||||
overflow-y: auto;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
@@ -495,8 +526,8 @@ html:has(#nfc_kiosk_root) {
|
||||
margin: 1rem 0;
|
||||
|
||||
button {
|
||||
font-size: 2rem;
|
||||
padding: 1.5rem 0;
|
||||
font-size: 1.7rem;
|
||||
padding: 1.1rem 0;
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: var(--nfc-text);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
@@ -511,12 +542,12 @@ html:has(#nfc_kiosk_root) {
|
||||
}
|
||||
|
||||
.pin-display {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: 0.5rem;
|
||||
text-align: center;
|
||||
margin: 1rem 0 1.5rem;
|
||||
margin: 0.75rem 0 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-height: 3rem;
|
||||
min-height: 2.6rem;
|
||||
color: hsl(var(--nfc-h), 80%, 70%);
|
||||
}
|
||||
|
||||
@@ -578,6 +609,114 @@ html:has(#nfc_kiosk_root) {
|
||||
}
|
||||
}
|
||||
|
||||
// Amber accent for the "unknown card → program it" prompt + the inline
|
||||
// status line in the new-employee form.
|
||||
.nfc-kiosk__enroll-panel.nfc-kiosk__unknown {
|
||||
border-color: rgba(224, 168, 62, 0.55);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0,0,0,0.5),
|
||||
0 0 60px rgba(224, 168, 62, 0.28),
|
||||
inset 0 1px 0 rgba(255,255,255,0.08);
|
||||
|
||||
.unknown-icon { font-size: 3.5rem; line-height: 1; margin-bottom: 0.5rem; color: #e0a83e; }
|
||||
h2 { color: #e0a83e; }
|
||||
}
|
||||
|
||||
.nfc-kiosk__enroll-panel .enroll-msg {
|
||||
min-height: 1.4rem;
|
||||
margin: 0.25rem 0 0.5rem;
|
||||
color: var(--nfc-error);
|
||||
font-size: 0.95rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Manager page rows (employee + tag status + per-row actions)
|
||||
.nfc-kiosk__enroll-panel .manager-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.7rem 0.4rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
flex-wrap: wrap;
|
||||
|
||||
.m-info { display: flex; align-items: baseline; gap: 0.5rem; flex: 1; min-width: 12rem; }
|
||||
.m-name { font-size: 1.05rem; }
|
||||
.m-dept { color: var(--nfc-text-muted); font-size: 0.8rem; }
|
||||
.m-tag { font-size: 0.78rem; color: var(--nfc-text-muted); white-space: nowrap; }
|
||||
.m-tag--on { color: hsl(var(--nfc-h), 70%, 66%); }
|
||||
.m-actions { display: flex; gap: 0.4rem; flex-shrink: 0; flex-wrap: wrap; }
|
||||
.m-btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: var(--nfc-text);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
cursor: pointer;
|
||||
|
||||
&.m-danger { color: #ff8b9a; border-color: rgba(217,55,78,0.45); }
|
||||
&:active { transform: scale(0.96); }
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Guided profile-photo capture — live camera with an oval face guide
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
.nfc-kiosk__photo-panel {
|
||||
@extend %nfc-glass;
|
||||
padding: 1.5rem;
|
||||
width: 80vw;
|
||||
max-width: 540px;
|
||||
max-height: 92vh;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
|
||||
h2 { font-size: 1.4rem; margin: 0 0 1rem; font-weight: 400; }
|
||||
}
|
||||
.nfc-photo-stage {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 4;
|
||||
max-height: 56vh;
|
||||
margin: 0 auto;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
.nfc-photo-video,
|
||||
.nfc-photo-preview {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.nfc-photo-video { transform: scaleX(-1); } // mirror for a natural selfie view
|
||||
.nfc-photo-guide {
|
||||
position: absolute;
|
||||
top: 48%;
|
||||
left: 50%;
|
||||
width: 60%;
|
||||
height: 66%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 3px dashed rgba(255, 255, 255, 0.92);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); // dim everything outside the oval
|
||||
pointer-events: none;
|
||||
}
|
||||
.nfc-photo-hint {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0.75rem;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.9);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Reduced-motion fallback — respect users who prefer no animation
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user