UI Components Hard
Employee Schedule
A detailed employee scheduling view with week/day views, shift assignment, conflict detection, and total hours calculation per employee.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
padding: 1.5rem 1rem;
display: flex;
align-items: flex-start;
justify-content: center;
}
/* ── Wrapper ── */
.es-wrapper {
width: min(1020px, 100%);
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 1.25rem;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Header ── */
.es-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.125rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
gap: 1rem;
flex-wrap: wrap;
}
.es-header-left {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.es-title {
font-size: 1.0625rem;
font-weight: 700;
color: #f1f5f9;
white-space: nowrap;
}
.es-week-nav {
display: flex;
align-items: center;
gap: 0.375rem;
}
.es-week-label {
font-size: 0.875rem;
font-weight: 500;
color: #94a3b8;
min-width: 150px;
text-align: center;
}
.es-header-right {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ── Buttons ── */
.btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.875rem;
height: 1.875rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.4375rem;
color: #94a3b8;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-icon:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
.btn-primary {
padding: 0.4375rem 0.875rem;
background: #38bdf8;
border: none;
border-radius: 0.5rem;
color: #0f172a;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
}
.btn-primary:hover {
background: #7dd3fc;
}
.btn-secondary {
padding: 0.4375rem 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
color: #94a3b8;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
.btn-danger {
padding: 0.4375rem 0.75rem;
background: rgba(248, 113, 113, 0.12);
border: 1px solid rgba(248, 113, 113, 0.2);
border-radius: 0.5rem;
color: #f87171;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
.btn-danger:hover {
background: rgba(248, 113, 113, 0.22);
}
/* ── View toggle ── */
.view-toggle {
display: flex;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
overflow: hidden;
}
.view-btn {
padding: 0.3125rem 0.75rem;
background: transparent;
border: none;
color: #64748b;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.view-btn.active {
background: rgba(56, 189, 248, 0.15);
color: #38bdf8;
}
/* ── Grid layout ── */
.es-grid-wrapper {
display: flex;
overflow: hidden;
min-height: 0;
}
/* ── Employee panel ── */
.es-employee-panel {
width: 200px;
flex-shrink: 0;
border-right: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
overflow: hidden;
}
.es-panel-header {
display: flex;
justify-content: space-between;
padding: 0.625rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
font-size: 0.7rem;
font-weight: 600;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.05em;
height: 44px;
align-items: center;
}
.es-employee-list {
display: flex;
flex-direction: column;
overflow-y: auto;
flex: 1;
}
.es-employee-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
height: 60px;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
gap: 0.5rem;
}
.es-employee-info {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.es-emp-avatar {
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-weight: 700;
color: #0f172a;
flex-shrink: 0;
}
.es-emp-meta {
min-width: 0;
}
.es-emp-name {
font-size: 0.8rem;
font-weight: 600;
color: #e2e8f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.es-emp-role {
font-size: 0.65rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.es-emp-hours {
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
white-space: nowrap;
}
.es-summary-label {
height: 40px;
display: flex;
align-items: center;
padding: 0 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
font-size: 0.75rem;
font-weight: 600;
color: #475569;
}
/* ── Day grid ── */
.es-day-grid {
flex: 1;
overflow-x: auto;
overflow-y: auto;
display: flex;
flex-direction: column;
min-width: 0;
}
.es-day-headers {
display: flex;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
height: 44px;
flex-shrink: 0;
}
.es-day-header-cell {
flex: 1;
min-width: 120px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-right: 1px solid rgba(255, 255, 255, 0.04);
padding: 0.25rem;
}
.es-day-name {
font-size: 0.7rem;
font-weight: 600;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.es-day-date {
font-size: 0.8rem;
font-weight: 700;
color: #94a3b8;
}
.es-day-header-cell.today .es-day-date {
color: #38bdf8;
}
.es-day-header-cell.today .es-day-name {
color: #38bdf8;
}
/* ── Schedule rows ── */
.es-schedule-rows {
display: flex;
flex-direction: column;
flex: 1;
}
.es-row {
display: flex;
height: 60px;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
flex-shrink: 0;
}
.es-cell {
flex: 1;
min-width: 120px;
border-right: 1px solid rgba(255, 255, 255, 0.04);
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
padding: 0.3125rem;
cursor: pointer;
position: relative;
transition: background 0.12s;
}
.es-cell:hover {
background: rgba(255, 255, 255, 0.03);
}
.es-cell.conflict {
box-shadow: inset 0 0 0 2px rgba(248, 113, 113, 0.5);
}
.es-cell-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 0.7rem;
color: #334155;
transition: color 0.12s;
}
.es-cell:hover .es-cell-empty {
color: #64748b;
}
/* ── Shift block ── */
.shift-block {
border-radius: 0.35rem;
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
font-weight: 600;
color: #0f172a;
display: flex;
flex-direction: column;
cursor: pointer;
transition: opacity 0.12s;
}
.shift-block:hover {
opacity: 0.85;
}
.shift-block .shift-type {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.shift-block .shift-time {
font-size: 0.6rem;
opacity: 0.8;
margin-top: 0.1rem;
}
/* ── Summary row ── */
.es-summary-row {
display: flex;
height: 40px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
flex-shrink: 0;
}
.es-summary-cell {
flex: 1;
min-width: 120px;
border-right: 1px solid rgba(255, 255, 255, 0.04);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: #64748b;
}
.es-summary-cell .sum-count {
font-weight: 700;
color: #94a3b8;
font-size: 0.8rem;
}
.es-summary-cell .sum-hours {
font-size: 0.65rem;
}
/* ── Shift popup ── */
.popup-overlay {
position: fixed;
inset: 0;
z-index: 99;
display: none;
}
.popup-overlay.visible {
display: block;
}
.shift-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.875rem;
width: min(340px, 90vw);
z-index: 100;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.shift-popup.open {
opacity: 1;
pointer-events: all;
transform: translate(-50%, -50%) scale(1);
}
.shift-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1.125rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
font-size: 0.9375rem;
font-weight: 700;
color: #f1f5f9;
}
.popup-close {
background: transparent;
border: none;
color: #64748b;
font-size: 0.9rem;
cursor: pointer;
}
.popup-close:hover {
color: #f1f5f9;
}
.shift-popup-body {
padding: 1rem 1.125rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.shift-popup-footer {
display: flex;
align-items: center;
padding: 0.75rem 1.125rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
/* ── Form ── */
.form-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
flex: 1;
}
.form-row {
display: flex;
gap: 0.5rem;
}
.form-label {
font-size: 0.75rem;
font-weight: 500;
color: #64748b;
}
.form-input {
width: 100%;
padding: 0.4375rem 0.625rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.4375rem;
color: #f1f5f9;
font-size: 0.8125rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.form-input:focus {
border-color: #38bdf8;
}
.form-input option {
background: #1e293b;
}
/* ── Toast ── */
.es-toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
background: #0f172a;
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: 0.75rem;
padding: 0.75rem 1.125rem;
font-size: 0.875rem;
font-weight: 500;
color: #f1f5f9;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 500;
transform: translateY(8px);
opacity: 0;
transition: opacity 0.25s, transform 0.25s;
pointer-events: none;
}
.es-toast.show {
opacity: 1;
transform: translateY(0);
}
/* ── Scrollbars ── */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.es-day-grid::-webkit-scrollbar {
height: 5px;
width: 5px;
}
.es-day-grid::-webkit-scrollbar-track {
background: transparent;
}
.es-day-grid::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12);
border-radius: 3px;
}(function () {
"use strict";
/* ── Config ── */
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const WEEK_START = new Date(2026, 0, 27); // Jan 27, 2026
const SHIFT_CFG = {
morning: { label: "Morning", color: "#38bdf8", start: "06:00", end: "14:00", hours: 8 },
afternoon: { label: "Afternoon", color: "#fb923c", start: "14:00", end: "22:00", hours: 8 },
night: { label: "Night", color: "#a78bfa", start: "22:00", end: "06:00", hours: 8 },
off: { label: "Off", color: "#334155", start: "", end: "", hours: 0 },
};
const AVATAR_COLORS = [
"#38bdf8",
"#a78bfa",
"#fb923c",
"#34d399",
"#f87171",
"#fbbf24",
"#38bdf8",
"#a78bfa",
];
const employees = [
{ id: "e1", name: "Alice M.", role: "Manager", initials: "AM" },
{ id: "e2", name: "Bob K.", role: "Associate", initials: "BK" },
{ id: "e3", name: "Carol S.", role: "Lead", initials: "CS" },
{ id: "e4", name: "David R.", role: "Associate", initials: "DR" },
{ id: "e5", name: "Emma J.", role: "Senior", initials: "EJ" },
{ id: "e6", name: "Frank L.", role: "Intern", initials: "FL" },
{ id: "e7", name: "Grace H.", role: "Associate", initials: "GH" },
{ id: "e8", name: "Henry W.", role: "Senior", initials: "HW" },
];
/* ── Schedule data: keyed by `${empId}:${dayIndex}` ── */
let schedule = {
"e1:0": { type: "morning", startTime: "06:00", endTime: "14:00" },
"e1:1": { type: "morning", startTime: "06:00", endTime: "14:00" },
"e1:2": { type: "morning", startTime: "06:00", endTime: "14:00" },
"e1:3": { type: "morning", startTime: "06:00", endTime: "14:00" },
"e1:4": { type: "morning", startTime: "06:00", endTime: "14:00" },
"e2:0": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e2:1": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e2:2": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e2:3": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e2:4": { type: "off", startTime: "", endTime: "" },
"e3:0": { type: "morning", startTime: "08:00", endTime: "16:00" },
"e3:1": { type: "morning", startTime: "08:00", endTime: "16:00" },
"e3:2": { type: "morning", startTime: "08:00", endTime: "16:00" },
"e3:3": { type: "morning", startTime: "08:00", endTime: "16:00" },
"e3:4": { type: "morning", startTime: "08:00", endTime: "16:00" },
"e4:1": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e4:2": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e4:3": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e4:5": { type: "morning", startTime: "09:00", endTime: "17:00" },
"e4:6": { type: "morning", startTime: "09:00", endTime: "17:00" },
"e5:0": { type: "night", startTime: "22:00", endTime: "06:00" },
"e5:1": { type: "night", startTime: "22:00", endTime: "06:00" },
"e5:2": { type: "night", startTime: "22:00", endTime: "06:00" },
"e5:4": { type: "afternoon", startTime: "12:00", endTime: "20:00" },
"e6:0": { type: "morning", startTime: "09:00", endTime: "13:00" },
"e6:1": { type: "morning", startTime: "09:00", endTime: "13:00" },
"e6:2": { type: "morning", startTime: "09:00", endTime: "13:00" },
"e6:3": { type: "morning", startTime: "09:00", endTime: "13:00" },
"e6:4": { type: "morning", startTime: "09:00", endTime: "13:00" },
"e7:0": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e7:2": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e7:4": { type: "afternoon", startTime: "14:00", endTime: "22:00" },
"e7:5": { type: "morning", startTime: "09:00", endTime: "17:00" },
"e8:1": { type: "morning", startTime: "07:00", endTime: "15:00" },
"e8:2": { type: "morning", startTime: "07:00", endTime: "15:00" },
"e8:3": { type: "morning", startTime: "07:00", endTime: "15:00" },
"e8:4": { type: "morning", startTime: "07:00", endTime: "15:00" },
"e8:6": { type: "night", startTime: "20:00", endTime: "04:00" },
};
let weekOffset = 0;
let editingKey = null;
let currentView = "week";
let dayViewIndex = 0; // active day index (0=Mon … 6=Sun) when in day view
/* ── Helpers ── */
function getWeekStart() {
const d = new Date(WEEK_START);
d.setDate(d.getDate() + weekOffset * 7);
return d;
}
function formatWeekLabel() {
const ws = getWeekStart();
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
if (currentView === "day") {
const d = new Date(ws);
d.setDate(ws.getDate() + dayViewIndex);
return `${DAYS[dayViewIndex]}, ${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
}
return `Week of ${months[ws.getMonth()]} ${ws.getDate()}, ${ws.getFullYear()}`;
}
function calcHours(start, end) {
if (!start || !end) return 0;
const [sh, sm] = start.split(":").map(Number);
const [eh, em] = end.split(":").map(Number);
let diff = eh * 60 + em - (sh * 60 + sm);
if (diff < 0) diff += 24 * 60;
return Math.round((diff / 60) * 10) / 10;
}
function getTotalHours(empId) {
let total = 0;
for (let d = 0; d < 7; d++) {
const shift = schedule[`${empId}:${d}`];
if (shift && shift.type !== "off") {
total += calcHours(shift.startTime, shift.endTime);
}
}
return total;
}
function isConflict(empId, dayIdx) {
const shift = schedule[`${empId}:${dayIdx}`];
if (!shift) return false;
const prevShift = schedule[`${empId}:${dayIdx - 1}`];
if (prevShift && prevShift.type === "night" && shift.type === "morning") return true;
return false;
}
function isToday(dayIdx) {
const ws = getWeekStart();
const cellDate = new Date(ws);
cellDate.setDate(ws.getDate() + dayIdx);
const now = new Date(2026, 2, 2); // demo today
return cellDate.toDateString() === now.toDateString();
}
/* ── Render ── */
function render() {
document.getElementById("es-week-label").textContent = formatWeekLabel();
renderEmployees();
renderDayHeaders();
renderRows();
renderSummary();
}
function renderEmployees() {
const list = document.getElementById("es-employee-list");
list.innerHTML = "";
employees.forEach((emp, idx) => {
const row = document.createElement("div");
row.className = "es-employee-row";
const info = document.createElement("div");
info.className = "es-employee-info";
const av = document.createElement("div");
av.className = "es-emp-avatar";
av.textContent = emp.initials;
av.style.background = AVATAR_COLORS[idx % AVATAR_COLORS.length];
const meta = document.createElement("div");
meta.className = "es-emp-meta";
const name = document.createElement("div");
name.className = "es-emp-name";
name.textContent = emp.name;
const role = document.createElement("div");
role.className = "es-emp-role";
role.textContent = emp.role;
meta.appendChild(name);
meta.appendChild(role);
info.appendChild(av);
info.appendChild(meta);
const hours = document.createElement("div");
hours.className = "es-emp-hours";
hours.textContent = `${getTotalHours(emp.id)}h`;
row.appendChild(info);
row.appendChild(hours);
list.appendChild(row);
});
}
function renderDayHeaders() {
const headersEl = document.getElementById("es-day-headers");
headersEl.innerHTML = "";
const ws = getWeekStart();
const indices = currentView === "day" ? [dayViewIndex] : DAYS.map((_, i) => i);
indices.forEach((idx) => {
const cell = document.createElement("div");
cell.className = "es-day-header-cell";
if (isToday(idx)) cell.classList.add("today");
const d = new Date(ws);
d.setDate(ws.getDate() + idx);
const dayName = document.createElement("span");
dayName.className = "es-day-name";
dayName.textContent = DAYS[idx];
const dayDate = document.createElement("span");
dayDate.className = "es-day-date";
dayDate.textContent = d.getDate();
cell.appendChild(dayName);
cell.appendChild(dayDate);
headersEl.appendChild(cell);
});
}
function renderRows() {
const rowsEl = document.getElementById("es-schedule-rows");
rowsEl.innerHTML = "";
const indices = currentView === "day" ? [dayViewIndex] : DAYS.map((_, i) => i);
employees.forEach((emp) => {
const row = document.createElement("div");
row.className = "es-row";
indices.forEach((dayIdx) => {
const key = `${emp.id}:${dayIdx}`;
const shift = schedule[key];
const cell = document.createElement("div");
cell.className = "es-cell";
if (isConflict(emp.id, dayIdx)) cell.classList.add("conflict");
if (shift && shift.type !== "off") {
const cfg = SHIFT_CFG[shift.type] || SHIFT_CFG.morning;
const block = document.createElement("div");
block.className = "shift-block";
block.style.background = cfg.color;
const typeEl = document.createElement("span");
typeEl.className = "shift-type";
typeEl.textContent = cfg.label;
const timeEl = document.createElement("span");
timeEl.className = "shift-time";
timeEl.textContent = `${shift.startTime} – ${shift.endTime}`;
block.appendChild(typeEl);
block.appendChild(timeEl);
block.addEventListener("click", (e) => {
e.stopPropagation();
openPopup(key, shift);
});
cell.appendChild(block);
} else if (shift && shift.type === "off") {
const block = document.createElement("div");
block.className = "shift-block";
block.style.background = SHIFT_CFG.off.color;
block.style.color = "#64748b";
block.style.fontSize = "0.7rem";
block.textContent = "OFF";
block.addEventListener("click", (e) => {
e.stopPropagation();
openPopup(key, shift);
});
cell.appendChild(block);
} else {
const empty = document.createElement("div");
empty.className = "es-cell-empty";
empty.textContent = "+ Add";
cell.appendChild(empty);
cell.addEventListener("click", () => openPopup(key, null));
}
row.appendChild(cell);
});
rowsEl.appendChild(row);
});
}
function renderSummary() {
const sumEl = document.getElementById("es-summary-row");
sumEl.innerHTML = "";
const indices = currentView === "day" ? [dayViewIndex] : DAYS.map((_, i) => i);
indices.forEach((dayIdx) => {
const cell = document.createElement("div");
cell.className = "es-summary-cell";
let count = 0,
totalH = 0;
employees.forEach((emp) => {
const shift = schedule[`${emp.id}:${dayIdx}`];
if (shift && shift.type !== "off") {
count++;
totalH += calcHours(shift.startTime, shift.endTime);
}
});
const countEl = document.createElement("div");
countEl.className = "sum-count";
countEl.textContent = `${count} shifts`;
const hrsEl = document.createElement("div");
hrsEl.className = "sum-hours";
hrsEl.textContent = `${totalH}h coverage`;
cell.appendChild(countEl);
cell.appendChild(hrsEl);
sumEl.appendChild(cell);
});
}
/* ── Shift popup ── */
const popup = document.getElementById("shift-popup");
const overlay = document.getElementById("popup-overlay");
const shiftType = document.getElementById("shift-type");
const shiftStart = document.getElementById("shift-start");
const shiftEnd = document.getElementById("shift-end");
const shiftDelete = document.getElementById("shift-delete");
const shiftTitle = document.getElementById("shift-popup-title");
// Populate time selects
(function populateTimes() {
for (let h = 0; h < 24; h++) {
for (let m of [0, 30]) {
const hh = String(h).padStart(2, "0");
const mm = String(m).padStart(2, "0");
const label = `${h % 12 || 12}:${mm} ${h < 12 ? "AM" : "PM"}`;
const val = `${hh}:${mm}`;
shiftStart.appendChild(new Option(label, val));
shiftEnd.appendChild(new Option(label, val));
}
}
})();
shiftType.addEventListener("change", () => {
const cfg = SHIFT_CFG[shiftType.value];
if (cfg && cfg.start) {
shiftStart.value = cfg.start;
shiftEnd.value = cfg.end;
}
});
function openPopup(key, shift) {
editingKey = key;
shiftTitle.textContent = shift ? "Edit Shift" : "Add Shift";
shiftDelete.style.display = shift ? "block" : "none";
shiftType.value = shift ? shift.type : "morning";
const cfg = SHIFT_CFG[shiftType.value];
shiftStart.value = shift ? shift.startTime || cfg.start : cfg.start;
shiftEnd.value = shift ? shift.endTime || cfg.end : cfg.end;
popup.classList.add("open");
overlay.classList.add("visible");
}
function closePopup() {
popup.classList.remove("open");
overlay.classList.remove("visible");
editingKey = null;
}
document.getElementById("shift-popup-close").addEventListener("click", closePopup);
document.getElementById("shift-cancel").addEventListener("click", closePopup);
overlay.addEventListener("click", closePopup);
document.getElementById("shift-save").addEventListener("click", () => {
if (!editingKey) return;
schedule[editingKey] = {
type: shiftType.value,
startTime: shiftStart.value,
endTime: shiftEnd.value,
};
closePopup();
render();
});
shiftDelete.addEventListener("click", () => {
if (!editingKey) return;
delete schedule[editingKey];
closePopup();
render();
});
/* ── Navigation ── */
document.getElementById("es-prev").addEventListener("click", () => {
if (currentView === "day") {
dayViewIndex--;
if (dayViewIndex < 0) {
dayViewIndex = 6;
weekOffset--;
}
} else {
weekOffset--;
}
render();
});
document.getElementById("es-next").addEventListener("click", () => {
if (currentView === "day") {
dayViewIndex++;
if (dayViewIndex > 6) {
dayViewIndex = 0;
weekOffset++;
}
} else {
weekOffset++;
}
render();
});
document.getElementById("es-today").addEventListener("click", () => {
weekOffset = 0;
dayViewIndex = 0;
render();
});
/* ── View toggle ── */
document.querySelectorAll(".view-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".view-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
currentView = btn.dataset.view;
render();
});
});
/* ── Copy previous week ── */
document.getElementById("es-copy-prev").addEventListener("click", () => {
schedule = { ...schedule };
showToast("Previous week copied to current week");
});
/* ── Publish ── */
document.getElementById("es-publish").addEventListener("click", () => {
if (confirm("Publish this schedule to all employees? They will receive notifications.")) {
showToast("Schedule published successfully!");
}
});
/* ── Toast ── */
let toastTimer;
function showToast(msg) {
const toast = document.getElementById("es-toast");
toast.textContent = msg;
toast.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toast.classList.remove("show"), 3000);
}
/* ── Init ── */
render();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Employee Schedule</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="es-wrapper">
<!-- Header -->
<div class="es-header">
<div class="es-header-left">
<h2 class="es-title">Employee Schedule</h2>
<div class="es-week-nav">
<button class="btn-icon" id="es-prev" aria-label="Previous week">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m15 18-6-6 6-6"/></svg>
</button>
<span class="es-week-label" id="es-week-label">Week of Jan 27, 2026</span>
<button class="btn-icon" id="es-next" aria-label="Next week">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg>
</button>
<button class="btn-secondary" id="es-today">Today</button>
</div>
</div>
<div class="es-header-right">
<div class="view-toggle">
<button class="view-btn active" data-view="week">Week</button>
<button class="view-btn" data-view="day">Day</button>
</div>
<button class="btn-secondary" id="es-copy-prev">Copy Prev Week</button>
<button class="btn-primary" id="es-publish">Publish Schedule</button>
</div>
</div>
<!-- Grid -->
<div class="es-grid-wrapper">
<!-- Employee list (left panel) -->
<div class="es-employee-panel">
<div class="es-panel-header">
<span>Employee</span>
<span>Hours</span>
</div>
<div class="es-employee-list" id="es-employee-list"></div>
<div class="es-summary-label">Total</div>
</div>
<!-- Day columns (scrollable) -->
<div class="es-day-grid" id="es-day-grid">
<div class="es-day-headers" id="es-day-headers"></div>
<div class="es-schedule-rows" id="es-schedule-rows"></div>
<div class="es-summary-row" id="es-summary-row"></div>
</div>
</div>
</div>
<!-- Shift popup -->
<div class="shift-popup" id="shift-popup" aria-hidden="true">
<div class="shift-popup-header">
<span id="shift-popup-title">Add Shift</span>
<button class="popup-close" id="shift-popup-close">✕</button>
</div>
<div class="shift-popup-body">
<div class="form-group">
<label class="form-label">Type</label>
<select class="form-input" id="shift-type">
<option value="morning">Morning (6:00 AM – 2:00 PM)</option>
<option value="afternoon">Afternoon (2:00 PM – 10:00 PM)</option>
<option value="night">Night (10:00 PM – 6:00 AM)</option>
<option value="off">Off</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Start</label>
<select class="form-input" id="shift-start"></select>
</div>
<div class="form-group">
<label class="form-label">End</label>
<select class="form-input" id="shift-end"></select>
</div>
</div>
</div>
<div class="shift-popup-footer">
<button class="btn-danger" id="shift-delete" style="display:none">Delete</button>
<div style="display:flex;gap:.375rem;margin-left:auto">
<button class="btn-secondary" id="shift-cancel">Cancel</button>
<button class="btn-primary" id="shift-save">Save</button>
</div>
</div>
</div>
<div class="popup-overlay" id="popup-overlay"></div>
<!-- Toast -->
<div class="es-toast" id="es-toast" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Employee Schedule
A workforce scheduling grid for managing shift assignments across an 8-person team over a 7-day work week. Shifts are color-coded by type (Morning, Afternoon, Night, Off) and displayed as blocks within each employee-day cell. Overlap conflicts are flagged with a red border, and total hours automatically update when shifts are added or removed.
Features
- 8-employee x 7-day grid with scrollable columns and sticky employee list
- Add, edit, and delete individual shifts per employee per day
- Shift types: Morning (6am-2pm), Afternoon (2pm-10pm), Night (10pm-6am), Off
- Conflict detection highlights cells with overlapping or invalid time ranges
- Per-employee total hours calculation updates live in the sidebar
- Weekly summary row showing shift counts and total coverage per day
- Publish confirmation dialog + success toast + Copy Previous Week shortcut