Construction — Crew Schedule
A rugged week-view crew board with crews as rows and weekdays as columns, draggable-feeling job blocks showing site, hours and crew size, status badges for scheduled, in-progress and done, a highlighted today column, and an unassigned jobs sidebar that assigns work to crews.
MCP
Code
:root {
--hv: #f5c518;
--steel: #2b3440;
--steel-d: #1b222b;
--bg: #eceae6;
--ink: #1a1d22;
--muted: #666c75;
--orange: #e8642a;
--white: #ffffff;
--line: #d3cfc7;
--line-d: #3a4452;
--green: #2f9e44;
--green-bg: #e4f4e7;
--blue: #2563c9;
--blue-bg: #e3edfb;
--grey: #8a8f97;
--grey-bg: #e6e4df;
--radius: 6px;
--shadow: 0 1px 0 rgba(0, 0, 0, 0.04), 0 6px 16px rgba(27, 34, 43, 0.08);
--font: "Inter", system-ui, -apple-system, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
font-family: var(--font);
background:
repeating-linear-gradient(135deg, rgba(0, 0, 0, 0.015) 0 14px, transparent 14px 28px),
var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
padding: 22px;
line-height: 1.45;
}
.app {
max-width: 1180px;
margin: 0 auto;
}
/* ---------- Topbar ---------- */
.topbar {
background: var(--steel);
color: var(--white);
border-radius: var(--radius);
border-bottom: 4px solid var(--orange);
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
box-shadow: var(--shadow);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.hazard {
width: 42px;
height: 42px;
border-radius: 5px;
flex: none;
background: repeating-linear-gradient(
-45deg,
var(--hv) 0 9px,
var(--steel-d) 9px 18px
);
border: 2px solid var(--steel-d);
}
.brand-txt h1 {
font-size: 1.18rem;
font-weight: 900;
letter-spacing: -0.02em;
text-transform: uppercase;
}
.brand-txt p {
font-size: 0.82rem;
color: #aeb6c0;
font-weight: 500;
margin-top: 2px;
}
.stats {
display: flex;
gap: 10px;
}
.stat {
background: var(--steel-d);
border: 1px solid var(--line-d);
border-radius: 5px;
padding: 8px 16px;
text-align: center;
min-width: 78px;
}
.stat-num {
display: block;
font-size: 1.4rem;
font-weight: 900;
line-height: 1;
color: var(--hv);
}
.stat:nth-child(2) .stat-num {
color: var(--orange);
}
.stat-lbl {
display: block;
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #97a0ab;
margin-top: 5px;
font-weight: 600;
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 280px;
gap: 16px;
margin-top: 16px;
}
/* ---------- Board ---------- */
.board {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: auto;
}
.grid {
display: grid;
grid-template-columns: 132px repeat(6, minmax(118px, 1fr));
min-width: 760px;
}
.grid-head {
background: var(--steel-d);
color: var(--white);
font-weight: 700;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 12px 10px;
border-right: 1px solid var(--line-d);
position: relative;
text-align: center;
}
.grid-head span {
display: block;
font-size: 0.7rem;
color: #9aa3ae;
font-weight: 600;
margin-top: 2px;
}
.grid-head.corner {
text-align: left;
background: var(--steel);
}
.grid-head.is-today {
background: var(--orange);
}
.grid-head.is-today span {
color: #ffe2d2;
}
.grid-head.is-today em {
position: absolute;
top: 6px;
right: 8px;
font-style: normal;
font-size: 0.58rem;
background: var(--hv);
color: var(--steel-d);
padding: 1px 6px;
border-radius: 3px;
font-weight: 800;
letter-spacing: 0.05em;
}
.crew-cell {
background: var(--bg);
border-right: 1px solid var(--line);
border-top: 1px solid var(--line);
padding: 12px 10px;
display: flex;
flex-direction: column;
justify-content: center;
}
.crew-cell .name {
font-weight: 800;
font-size: 0.9rem;
letter-spacing: -0.01em;
}
.crew-cell .lead {
font-size: 0.72rem;
color: var(--muted);
margin-top: 2px;
}
.crew-cell .trade {
display: inline-block;
margin-top: 7px;
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 700;
color: var(--steel);
background: var(--white);
border: 1px solid var(--line);
padding: 2px 7px;
border-radius: 3px;
align-self: flex-start;
}
.day-cell {
border-right: 1px solid var(--line);
border-top: 1px solid var(--line);
padding: 6px;
min-height: 96px;
display: flex;
flex-direction: column;
gap: 6px;
transition: background 0.15s ease;
}
.day-cell.today-col {
background: rgba(245, 197, 24, 0.07);
}
.day-cell.droppable {
background: rgba(46, 158, 68, 0.12);
outline: 2px dashed var(--green);
outline-offset: -3px;
cursor: pointer;
}
/* ---------- Job blocks ---------- */
.job {
background: var(--white);
border: 1px solid var(--line);
border-left: 4px solid var(--steel);
border-radius: 4px;
padding: 7px 8px 8px;
font-size: 0.74rem;
box-shadow: 0 1px 2px rgba(27, 34, 43, 0.07);
}
.job.s-scheduled {
border-left-color: var(--blue);
}
.job.s-progress {
border-left-color: var(--orange);
}
.job.s-done {
border-left-color: var(--green);
opacity: 0.82;
}
.job .site {
font-weight: 800;
letter-spacing: -0.01em;
display: block;
color: var(--ink);
}
.job .meta {
color: var(--muted);
font-size: 0.68rem;
margin-top: 2px;
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
}
.job .badge {
display: inline-block;
margin-top: 6px;
font-size: 0.6rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 2px 7px;
border-radius: 999px;
cursor: pointer;
border: none;
font-family: inherit;
}
.badge.b-scheduled {
background: var(--blue-bg);
color: var(--blue);
}
.badge.b-progress {
background: #fbe5da;
color: var(--orange);
}
.badge.b-done {
background: var(--green-bg);
color: var(--green);
}
.badge:focus-visible {
outline: 2px solid var(--steel);
outline-offset: 2px;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: var(--white);
border: 1px solid var(--line);
border-top: 4px solid var(--hv);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 16px;
align-self: start;
}
.side-head h2 {
font-size: 1rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: -0.01em;
}
.side-head p {
font-size: 0.74rem;
color: var(--muted);
margin-top: 4px;
}
.unassigned {
list-style: none;
padding: 0;
margin: 14px 0 0;
display: flex;
flex-direction: column;
gap: 9px;
}
.u-job {
width: 100%;
text-align: left;
background: var(--bg);
border: 1px solid var(--line);
border-left: 4px solid var(--orange);
border-radius: 5px;
padding: 9px 10px;
cursor: pointer;
font-family: inherit;
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.u-job:hover {
transform: translateX(2px);
box-shadow: var(--shadow);
}
.u-job.armed {
border-color: var(--hv);
background: #fff8db;
box-shadow: 0 0 0 2px var(--hv);
}
.u-job:focus-visible {
outline: 2px solid var(--steel);
outline-offset: 2px;
}
.u-job .site {
font-weight: 800;
font-size: 0.82rem;
display: block;
}
.u-job .meta {
font-size: 0.7rem;
color: var(--muted);
margin-top: 3px;
display: flex;
gap: 10px;
}
.u-empty {
text-align: center;
font-size: 0.78rem;
color: var(--muted);
padding: 18px 8px;
border: 2px dashed var(--line);
border-radius: 6px;
}
.hint {
font-size: 0.7rem;
color: var(--muted);
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed var(--line);
}
.hint.active {
color: var(--orange);
font-weight: 700;
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 12px;
}
.topbar {
padding: 14px;
}
.stats {
width: 100%;
justify-content: space-between;
}
.stat {
flex: 1;
min-width: 0;
padding: 8px 6px;
}
.layout {
grid-template-columns: 1fr;
}
.board {
order: 2;
}
.sidebar {
order: 1;
}
}(function () {
"use strict";
var DAYS = ["mon", "tue", "wed", "thu", "fri", "sat"];
var TODAY = "tue";
var CREWS = [
{ id: "c1", name: "Crew A", lead: "Foreman: Marcus Hale", trade: "Framing" },
{ id: "c2", name: "Crew B", lead: "Foreman: Tariq Osei", trade: "Concrete" },
{ id: "c3", name: "Crew C", lead: "Foreman: Lena Petrov", trade: "Electrical" },
{ id: "c4", name: "Crew D", lead: "Foreman: Diego Ramos", trade: "Roofing" }
];
// status cycle: scheduled -> progress -> done -> scheduled
var STATUS = {
scheduled: { label: "Scheduled", next: "progress" },
progress: { label: "In Progress", next: "done" },
done: { label: "Done", next: "scheduled" }
};
// jobs already placed on the board
var jobs = [
{ id: "j1", crew: "c1", day: "mon", site: "Maple Ridge Lot 12", hours: "07:00–15:30", size: 5, status: "done" },
{ id: "j2", crew: "c1", day: "tue", site: "Maple Ridge Lot 12", hours: "07:00–15:30", size: 5, status: "progress" },
{ id: "j3", crew: "c1", day: "wed", site: "Harbor St. Annex", hours: "08:00–16:00", size: 4, status: "scheduled" },
{ id: "j4", crew: "c2", day: "tue", site: "Civic Center Foundation", hours: "06:30–14:30", size: 6, status: "progress" },
{ id: "j5", crew: "c2", day: "thu", site: "Civic Center Foundation", hours: "06:30–14:30", size: 6, status: "scheduled" },
{ id: "j6", crew: "c3", day: "mon", site: "Birchwood Tower 3F", hours: "08:00–16:30", size: 3, status: "done" },
{ id: "j7", crew: "c3", day: "wed", site: "Birchwood Tower 4F", hours: "08:00–16:30", size: 3, status: "scheduled" },
{ id: "j8", crew: "c3", day: "fri", site: "Old Mill Rewire", hours: "09:00–17:00", size: 2, status: "scheduled" },
{ id: "j9", crew: "c4", day: "tue", site: "Westgate Plaza Roof", hours: "07:00–15:00", size: 4, status: "progress" },
{ id: "j10", crew: "c4", day: "thu", site: "Westgate Plaza Roof", hours: "07:00–15:00", size: 4, status: "scheduled" },
{ id: "j11", crew: "c2", day: "sat", site: "Riverside Pour Cleanup", hours: "07:00–12:00", size: 3, status: "scheduled" }
];
var unassigned = [
{ id: "u1", site: "Sunset Blvd Curbing", hours: "07:30–15:30", size: 4 },
{ id: "u2", site: "Lakeshore Demo Phase 1", hours: "06:00–14:00", size: 6 },
{ id: "u3", site: "Depot Drainage Trench", hours: "08:00–16:00", size: 3 }
];
var seq = 100;
var armed = null; // currently armed unassigned job id
var grid = document.getElementById("grid");
var unassignedEl = document.getElementById("unassigned");
var hintEl = document.getElementById("hint");
function jobBlock(job) {
var st = STATUS[job.status];
var el = document.createElement("div");
el.className = "job s-" + job.status;
el.innerHTML =
'<span class="site"></span>' +
'<span class="meta"><span class="hrs"></span><span class="crew">◆ ' +
job.size +
"</span></span>" +
'<button type="button" class="badge b-' +
job.status +
'">' +
st.label +
"</button>";
el.querySelector(".site").textContent = job.site;
el.querySelector(".hrs").textContent = job.hours;
var badge = el.querySelector(".badge");
badge.setAttribute("aria-label", "Status " + st.label + ", click to advance");
badge.addEventListener("click", function (e) {
e.stopPropagation();
job.status = STATUS[job.status].next;
render();
});
return el;
}
function render() {
// clear everything except the 7 header cells
var heads = grid.querySelectorAll(".grid-head");
grid.innerHTML = "";
heads.forEach(function (h) {
grid.appendChild(h);
});
CREWS.forEach(function (crew) {
var cell = document.createElement("div");
cell.className = "crew-cell";
cell.innerHTML =
'<span class="name"></span><span class="lead"></span><span class="trade"></span>';
cell.querySelector(".name").textContent = crew.name;
cell.querySelector(".lead").textContent = crew.lead;
cell.querySelector(".trade").textContent = crew.trade;
grid.appendChild(cell);
DAYS.forEach(function (day) {
var dc = document.createElement("div");
dc.className = "day-cell";
if (day === TODAY) dc.classList.add("today-col");
dc.dataset.crew = crew.id;
dc.dataset.day = day;
jobs
.filter(function (j) {
return j.crew === crew.id && j.day === day;
})
.forEach(function (j) {
dc.appendChild(jobBlock(j));
});
if (armed) {
dc.classList.add("droppable");
}
dc.addEventListener("click", function () {
if (armed) assign(crew.id, day);
});
grid.appendChild(dc);
});
});
renderUnassigned();
updateStats();
}
function renderUnassigned() {
unassignedEl.innerHTML = "";
if (!unassigned.length) {
var li = document.createElement("li");
li.className = "u-empty";
li.textContent = "All jobs assigned. Crews are loaded for the week.";
unassignedEl.appendChild(li);
return;
}
unassigned.forEach(function (job) {
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "u-job" + (armed === job.id ? " armed" : "");
btn.setAttribute("aria-pressed", armed === job.id ? "true" : "false");
btn.innerHTML =
'<span class="site"></span><span class="meta"><span></span><span>◆ ' +
job.size +
" crew</span></span>";
btn.querySelector(".site").textContent = job.site;
btn.querySelector(".meta span").textContent = job.hours;
btn.addEventListener("click", function () {
armed = armed === job.id ? null : job.id;
render();
updateHint();
});
li.appendChild(btn);
unassignedEl.appendChild(li);
});
}
function assign(crewId, day) {
var idx = unassigned.findIndex(function (j) {
return j.id === armed;
});
if (idx === -1) return;
var job = unassigned.splice(idx, 1)[0];
jobs.push({
id: "j" + ++seq,
crew: crewId,
day: day,
site: job.site,
hours: job.hours,
size: job.size,
status: "scheduled"
});
armed = null;
render();
updateHint();
}
function updateHint() {
if (armed) {
hintEl.textContent = "Job armed — tap any highlighted day cell to assign it.";
hintEl.classList.add("active");
} else {
hintEl.textContent = "Tip: click a job card to arm it.";
hintEl.classList.remove("active");
}
}
function updateStats() {
var sched = jobs.filter(function (j) {
return j.status === "scheduled";
}).length;
var prog = jobs.filter(function (j) {
return j.status === "progress";
}).length;
document.getElementById("statScheduled").textContent = sched;
document.getElementById("statProgress").textContent = prog;
document.getElementById("statUnassigned").textContent = unassigned.length;
}
// Esc disarms
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && armed) {
armed = null;
render();
updateHint();
}
});
render();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crew Schedule — Meridian Construction</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="hazard" aria-hidden="true"></span>
<div class="brand-txt">
<h1>Meridian Construction</h1>
<p>Crew Schedule · Week of Jun 22 – 27, 2026</p>
</div>
</div>
<div class="stats" role="status" aria-live="polite">
<div class="stat">
<span class="stat-num" id="statScheduled">0</span>
<span class="stat-lbl">Scheduled</span>
</div>
<div class="stat">
<span class="stat-num" id="statProgress">0</span>
<span class="stat-lbl">In Progress</span>
</div>
<div class="stat">
<span class="stat-num" id="statUnassigned">0</span>
<span class="stat-lbl">Unassigned</span>
</div>
</div>
</header>
<div class="layout">
<main class="board" aria-label="Weekly crew schedule">
<div class="grid" id="grid">
<div class="grid-head corner">Crew</div>
<div class="grid-head" data-day="mon">Mon <span>22</span></div>
<div class="grid-head is-today" data-day="tue">Tue <span>23</span><em>Today</em></div>
<div class="grid-head" data-day="wed">Wed <span>24</span></div>
<div class="grid-head" data-day="thu">Thu <span>25</span></div>
<div class="grid-head" data-day="fri">Fri <span>26</span></div>
<div class="grid-head" data-day="sat">Sat <span>27</span></div>
<!-- crew rows injected by script.js -->
</div>
</main>
<aside class="sidebar" aria-label="Unassigned jobs">
<div class="side-head">
<h2>Unassigned Jobs</h2>
<p>Pick a job, then tap an open day cell to assign it to a crew.</p>
</div>
<ul class="unassigned" id="unassigned"></ul>
<p class="hint" id="hint">Tip: click a job card to arm it.</p>
</aside>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Crew Schedule
A hard-hat scheduling board for a construction outfit. The main grid lays out crews as rows and the Mon–Sat work week as columns, with job blocks that show the job site, scheduled hours and crew size. Each block carries a status badge — Scheduled, In Progress or Done — and the current weekday gets a hi-vis Today highlight so the foreman can see what’s happening right now.
An Unassigned Jobs sidebar lists work that still needs a crew. Click a job, then click any open day cell to drop it onto a crew, or use the per-crew assign control. Status badges cycle on click, the header tallies live counts of scheduled vs in-progress jobs, and everything runs on vanilla JS with a sturdy steel-and-concrete palette and safety-orange accents.
Illustrative UI only — not for real construction or safety-critical scheduling.