このシステムは、平日136・土曜日100・日祭日90の各仕事があり、平日、土日祭日を組み合わせて、年間を通して循環するシフト制スケジュールです。136人が365日公平で、整合性の取れる内容としています。この仕様の特徴は土日祭日の休みのルールがない、年間を通しての仕事を月単位の休みの取得や連続勤務の制限等を組み込事によって、規則性が取れた形になっています。

仕様
1.1日に136種類の仕事の対応
2.136種類の交番制
5.平日は136種類,土曜は100種類、日曜・祭日は90種類の対応
6.月単位の一人の休日は、9日を基準1
7.年間136交番が循環するが、136人全員の公平と整合性図る
8.連続勤務は4日を限度
10.通年を通した、日本のカレンダーを基本(うるう年も考慮)
11.136人分の月単位のシフト表を表示
12.シフト表の印刷
仕様ファイル
index.html script.js kouban.css
ご利用について
このコードは、一般公開しており、内容の変更・修正・削除・追加・デザイン変更等は制限がありません。また、コピー&ペーストも許可しますが、動作の保証は致しません。ご利用に当っては自己責任でお願いして、作成者は責任を負う事は致しません。
1.システムの動作確認は、ある程度確認はしていますが、データが多いために全てが整合性を取れているかは、保証しません。

2.システムはストレージ管理(ローカルストレージ・サーバー)はしていません。また、単独の端末環境が推奨されます
※このシステムを仕様変更を一切変えずにご使用された時は、どこか片隅に小さくMOMOPLANと書いて頂けたら嬉しく思います
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="kouban.css">
  <title>シフト管理</title>
</head>
<body>
  <!-- シフト選択フォーム -->
  <form id="shiftForm">
    <div id="buttonContainerWrapper">
      <CENTER><div id="buttonContainerHeader"></div><FONT color="#3333ff" size="+2">シフト者選択</FONT><BR></CENTER>
        <BR>
      <div id="buttonContainer"></div>
    </div>
    <!-- 日付セレクター -->
    <div id="dateSelector" class="date-input-container">
      <div class="date-input-group">
        <label for="monthSelector"><FONT color="#3333ff" >【月選択:】</FONT></label>
        <select id="monthSelector" class="date-select">
          <option value="1">1月</option>
          <option value="2">2月</option>
          <option value="3">3月</option>
          <option value="4">4月</option>
          <option value="5">5月</option>
          <option value="6">6月</option>
          <option value="7">7月</option>
          <option value="8">8月</option>
          <option value="9">9月</option>
          <option value="10">10月</option>
          <option value="11">11月</option>
          <option value="12">12月</option>
        </select>
      </div>
      <div class="date-input-group">
        <label for="yearSelector"><FONT color="#3333ff" >【年選択:】</FONT></label>
        <select id="yearSelector" class="date-select">
          <option value="2025">2025年</option>
          <option value="2026">2026年</option>
          <option value="2027">2027年</option>
          <!-- 必要な年を追加 -->
        </select>
      </div>
    </div>
  </form>

  <!-- シフトメッセージ表示エリア -->
  <div id="staffShiftDisplay">
    <div id="staffShiftDetails"></div>
  </div>

  <!-- カレンダー表示エリア -->
  <div id="calendarContainer"></div>
  
  <!-- 印刷ボタン -->
  <button id="printButton">カレンダー印刷</button>

  <script src="script.js"></script>
</body>
</html>
JS
// 祝日データ
const holidays = [
  "2025-01-01", "2025-01-13", "2025-02-11", "2025-03-20",
  "2025-04-29", "2025-05-03", "2025-05-04", "2025-05-05",
  "2025-07-21", "2025-09-15", "2025-09-23", "2025-10-13",
  "2025-11-03", "2025-11-23"
];

// シフトデータ
const shifts = {
  weekdays: Array.from({ length: 136 }, (_, i) => `平日シフト${i + 1}`),
  saturdays: Array.from({ length: 100 }, (_, i) => `土曜シフト${i + 1}`),
  sundaysAndHolidays: Array.from({ length: 90 }, (_, i) => `日祝シフト${i + 1}`)
};

// 年選択肢を動的生成
const yearSelector = document.getElementById("yearSelector");
const currentYear = new Date().getFullYear();
const futureRange = 10; // 次の10年分を生成

for (let year = currentYear; year < currentYear + futureRange; year++) {
  const option = document.createElement("option");
  option.value = year;
  option.textContent = `${year}年`;
  yearSelector.appendChild(option);
}

// 月の日数を取得
function getDaysInMonth(year, month) {
  return new Date(year, month, 0).getDate();
}

// うるう年を判定
function isLeapYear(year) {
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}

// シフト割り当て
function assignShiftsByMonth(staffId, year, month, totalHolidaysForStaff) {
  const totalDaysInMonth = getDaysInMonth(year, month);
  const shiftsPerMonth = Array(totalDaysInMonth).fill("勤務日");
  let monthlyHolidayCount = 0;
  let consecutiveWorkDays = 0;

  for (let day = 1; day <= totalDaysInMonth; day++) {
    const date = new Date(year, month - 1, day);
    const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
    const isHoliday = holidays.includes(formattedDate);
    const isWeekend = date.getDay() === 0 || date.getDay() === 6;

    if (isHoliday) {
      shiftsPerMonth[day - 1] = shifts.sundaysAndHolidays[(day - 1 + staffId - 1) % shifts.sundaysAndHolidays.length];
      consecutiveWorkDays = 0;
    } else if (monthlyHolidayCount < 9 && totalHolidaysForStaff.remaining > 0 && (consecutiveWorkDays >= 4 || Math.random() < 0.3)) {
      shiftsPerMonth[day - 1] = "休日";
      monthlyHolidayCount++;
      totalHolidaysForStaff.remaining--;
      consecutiveWorkDays = 0;
    } else {
      const shiftType = isWeekend
        ? (date.getDay() === 0
            ? shifts.sundaysAndHolidays[(day - 1 + staffId - 1) % shifts.sundaysAndHolidays.length]
            : shifts.saturdays[(day - 1 + staffId - 1) % shifts.saturdays.length])
        : shifts.weekdays[(day - 1 + staffId - 1) % shifts.weekdays.length];
      shiftsPerMonth[day - 1] = shiftType;
      consecutiveWorkDays++;
    }
  }

  return shiftsPerMonth;
}

// 年間シフト生成
function generateStaffMonthlyShifts(staffId, year) {
  const yearlyShifts = [];
  const totalHolidaysForStaff = { monthlyAllocated: 0, remaining: 108 };

  for (let month = 1; month <= 12; month++) {
    const daysInMonth = getDaysInMonth(year, month);
    const shiftsForMonth = assignShiftsByMonth(staffId, year, month, totalHolidaysForStaff);
    yearlyShifts.push(shiftsForMonth);
  }

  return yearlyShifts;
}

// カレンダー生成
function generateCalendarForMonth(month, staffId) {
  const calendarContainer = document.getElementById("calendarContainer");
  calendarContainer.innerHTML = ""; // 既存のカレンダー内容をクリア

  const year = parseInt(document.getElementById("yearSelector").value); // 選択した年
  const daysInMonth = getDaysInMonth(year, month);

  const totalHolidaysForStaff = { remaining: 108 }; // 年間の休日データ
  const staffShifts = assignShiftsByMonth(staffId, year, month, totalHolidaysForStaff);

  for (let day = 1; day <= daysInMonth; day++) {
    const dayCell = document.createElement("div");
    dayCell.className = "calendar-day";

    if (staffShifts[day - 1].includes("平日シフト")) {
      dayCell.classList.add("weekday-shift");
    } else if (staffShifts[day - 1].includes("土曜シフト")) {
      dayCell.classList.add("saturday-shift");
    } else if (staffShifts[day - 1].includes("日祝シフト")) {
      dayCell.classList.add("sunday-holiday-shift");
    } else if (staffShifts[day - 1].includes("休日")) {
      dayCell.classList.add("holiday");
    }

    dayCell.innerHTML = `<strong>${day}</strong><br>${staffShifts[day - 1]}`;
    calendarContainer.appendChild(dayCell);
  }
}

// 初期表示
const currentMonth = new Date().getMonth() + 1;
generateCalendarForMonth(currentMonth, 1);

// メッセージ表示エリア
const staffShiftDetails = document.getElementById("staffShiftDetails");

// スタッフボタン生成
const buttonContainer = document.getElementById("buttonContainer");

for (let i = 1; i <= 136; i++) {
  const button = document.createElement("button");
  button.textContent = `スタッフ${i}`;
  button.className = "employee-button";
  button.type = "button";
  button.id = `staff-${i}`;
  
  button.addEventListener("click", () => {
    const selectedMonth = parseInt(document.getElementById("monthSelector").value); // 選択した月
    const selectedYear = parseInt(document.getElementById("yearSelector").value);  // 選択した年

    // カレンダーを更新
    generateCalendarForMonth(selectedMonth, i);
    staffShiftDetails.textContent = `スタッフ${i}のシフトです`;
  });

  buttonContainer.appendChild(button);
}

const printButton = document.getElementById("printButton");
if (printButton) {
  printButton.addEventListener("click", () => {
    console.log("印刷ボタンが押されました");
    window.print(); // シフト表の印刷を実行
  });
} else {
  console.error("印刷ボタンが見つかりません");
}
console.log(document.getElementById("staffShiftDetails").textContent);
CSS
body {
  font-family: Arial, sans-serif;
  background-image: url("s-brick008.gif");
}

#calendarContainer {
  display: grid;
  grid-template-columns: repeat(7, 1fr); /* 1週間7日 */
  gap: 5px;
  margin-top: 20px;
}

.calendar-day {
  border: 2px solid #8f53ff;
  padding: 10px;
  text-align: center;
  font-size: 14px;
}

.calendar-day.weekend {
  background-color: #f0f0f0; /* 土日の色付け */
}

.calendar-day.holiday {
  background-color: #ffd700; /* 金色の背景 */
  font-weight: bold; /* 強調表示 */
}

.calendar-day.saturday-shift {
  background-color: #ADD8E6; /* 青色の背景 */
}

.calendar-day.sunday-holiday-shift {
  background-color: #FFB6C1; /* ピンク色の背景 */
}

/* スタッフボタンエリアのスタイル */
#buttonContainerWrapper {
  border: 1px solid #000; /* 罫線 */
  padding: 10px;
  margin-bottom: 20px;
}

#buttonContainerHeader {
  text-align: center;
  font-size: 18px;
  font-weight: bold;
  margin-bottom: 10px;
}

#buttonContainer {
  display: grid;
  grid-template-columns: repeat(8, auto); /* ボタンをグリッドで配置 */
  gap: 5px;
}
#staffShiftDisplay {
margin: 10px auto; /* 中央揃え */
max-width: 200px; /* 表示エリアの幅を固定 */
padding: 10px;
font-weight: bold;
font-size: 12px;
border: 1px solid #ccc;
background-color: #a2f1de;
border-radius: 5px; /* 角を丸める */
font-family: Arial, sans-serif;
}

#staffShiftDetails {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap; /* 改行を維持 */
} 
/* 選択されたスタッフ表示 */
#selectedStaff {
  margin-left: 20px; /* セレクトボックスの右側に余白を追加 */
  font-weight: bold;
  font-size: 16px;
  display: inline-block; /* インライン表示にする */
}
@media print {
#calendarContainer {
border: 1px solid #000; /* 全体の罫線 */
}
.calendar-day {
border: 1px solid #000; /* 各日付セルの罫線 */
page-break-inside: avoid; /* 印刷時にページ内改行を防止 */
}
}

#printButton {
display: block;
margin: 10px auto; /* 中央揃え */
background-color: #4CAF50; /* グリーン */
color: white; /* 白文字 */
font-size: 16px; /* 文字サイズを大きく */
border: none; /* ボーダーなし */
padding: 10px 20px; /* ボタンの余白 */
border-radius: 5px; /* 丸みの角を追加 */
cursor: pointer;
}

#printButton:hover {
background-color: #45a049; /* ホバー時の色変更 */
}

/* 全体のスタイル */
.date-input-container {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 20px; /* 月と年の間のスペース */
  margin: 20px 0;
  font-family: Arial, sans-serif;
}

/* 各入力グループ */
.date-input-group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px; /* ラベルとセレクトボックス間のスペース */
}

/* ラベルとセレクトボックスのスタイル */
.date-input-group label {
  font-weight: bold;
  font-size: 14px;
  color: #333;
}

.date-select {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 5px;
  background-color: #fff;
  cursor: pointer;
  transition: all 0.3s ease;
}

.date-select:hover {
  border-color: #4CAF50; /* ホバー時に緑色のボーダー */
}

.date-select:focus {
  outline: none;
  border-color: #45a049; /* フォーカス時のボーダー */
  box-shadow: 0 0 5px rgba(69, 160, 73, 0.5); /* フォーカス時の影 */
}

#employeeList, #schedule {
  margin-top: 20px;
}
.employee-button {
  display: inline-block;
  margin: 2px;
  padding: 6px 10px;
  background-color: #e0f7fa;
  color: #00796b;
  border: 1px solid #004d40;
  border-radius: 5px;
  cursor: pointer;
}
.employee-button:hover {
  background-color: #b2ebf2;
}

以上です。【デモサイト】