「保育・教育日数マスタ」の休日データ

This commit is contained in:
2025-08-25 23:25:54 +08:00
parent 86e4a0ece0
commit 2b573daaee
2 changed files with 389 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
#legend {
width: 100%;
top: -24px;
position: absolute;
text-align: right;
font-size: 13px;
}
#legend .box {
display: inline-block;
width: 14px;
height: 14px;
background: #ffe3e6;
margin-right: 4px;
vertical-align: middle;
}
/* カレンダーエリア */
#calendar {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 12px;
box-sizing: border-box;
}
.month {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: auto repeat(6, 1fr);
border: 1px solid #aaa;
border-radius: 4px;
}
.month .title {
grid-column: 1/8;
text-align: center;
font-weight: bold;
padding: 4px 0;
}
.month .cell {
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
}
#calendar:not(.readonly) .month .cell:not(.header):not(:empty) {
cursor: pointer;
}
.month .cell:not(:empty) {
padding: 2px 0;
}
.month .sun {
color: #e74c3c;
}
.month .sat {
color: #2196f3;
}
.month .cell.selected {
background: #ffe3e6;
}
#calendar:not(.readonly) .month .cell:not(.header):not(.selected):not(:empty):hover {
background: #ffe3e677;
}
#calendar:not(.readonly) .month .cell.selected:hover {
background: #ffd3d7;
}

View File

@@ -0,0 +1,312 @@
(() => {
'use strict'
let _CALENDAR
// レコード詳細/新規作成/編集画面表示時にカレンダーを初期化
kintone.events.on(
['app.record.detail.show', 'app.record.create.show', 'app.record.edit.show'],
(event) => {
kintone.app.record.setFieldShown('休日の生データ', false);
initCalendarArea(
event.record,
getHolidayMap(event.record),
event.type === 'app.record.detail.show'
)
return event
}
)
// 年度フィールド変更時に各月の休日をクリアしてカレンダーを再描画
kintone.events.on(
[
'app.record.create.change.年度',
'app.record.edit.change.年度',
'app.record.index.edit.change.年度'
],
(event) => {
forEachRawFields((field) => {
event.record[field].value = ''
})
if (event.type !== 'app.record.index.edit.change.年度') {
initCalendarArea(event.record)
}
return event
}
)
// 一覧編集画面では各月の休日フィールドを編集不可にする
kintone.events.on(
['app.record.index.edit.show'],
(event) => {
forEachRawFields((field) => {
event.record[field].disabled = true
})
return event
}
)
// レコード保存時にカレンダーで選択した休日を各月フィールドへ反映
kintone.events.on(
['app.record.create.submit', 'app.record.edit.submit'],
(event) => {
const resultMap = _CALENDAR.getSelectedDatesMap()
forEachRawFields((field, i) => {
event.record[field].value = resultMap[i].join(',')
})
return event
}
)
/* --------------------------------------------------
* 初期化・データ準備
* -------------------------------------------------- */
// カレンダー表示エリアを初期化
function initCalendarArea(record, map = {}, readonly = false) {
const calendarAreaEl = kintone.app.record.getSpaceElement('calendar-area')
calendarAreaEl.innerHTML = ''
const year = record.年度.value
if (isYearString(year)) {
_CALENDAR = createCalendar(Number(year), map, readonly)
calendarAreaEl.appendChild(_CALENDAR.fragment)
}
}
// 既存レコードに保存されている休日を取得
function getHolidayMap(record) {
const res = {}
forEachRawFields((field, i) => {
const str = record[field].value
if (!str) {
return
}
res[i] = new Set(str.split(','))
})
return res
}
/* --------------------------------------------------
* ユーティリティ
* -------------------------------------------------- */
// 各月フィールドをループ
const forEachRawFields = (callback) => {
for (let i = 1; i <= 12; i++) {
callback(`休日_${i}`, i)
}
}
// ゼロパディング
const padZero = (n) => (n < 10 ? '0' + n : n)
// Date → yyyy-MM-dd
const toYmd = (d) => `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}`
// 土日判定
const isWeekend = (d) => d.getDay() === 0 || d.getDay() === 6
// 年度文字列の妥当性チェック
function isYearString(str) {
// 文字列かつ空でない
if (typeof str !== 'string' || str.length === 0) {
return false
}
// 数字のみ
if (!/^\d+$/.test(str)) {
return false
}
// 4桁
if (str.length !== 4) {
return false
}
// 1900-3000 の範囲
const year = parseInt(str, 10)
return year >= 1900 && year <= 3000
}
/* --------------------------------------------------
* カレンダー生成
* -------------------------------------------------- */
const createCalendar = (year, highlightMap, readonly) => {
const fragment = document.createDocumentFragment()
/* ----- 図例 ----- */
const legend = document.createElement('div')
legend.id = 'legend'
legend.innerHTML = '<span class="box"></span>休日'
fragment.appendChild(legend)
/* ----- カレンダーコンテナ ----- */
const calendar = document.createElement('div')
calendar.id = 'calendar'
if (readonly) {
calendar.classList.add('readonly')
}
fragment.appendChild(calendar)
/* ----- 内部状態 ----- */
const selectedSet = new Set()
const months = buildMonths(year)
months.forEach((m) => {
calendar.appendChild(buildMonth(m))
})
/* ---------- 月情報生成 ---------- */
function buildMonths(year) {
return Array.from({ length: 12 }, (_, i) => {
const monthIndex = 3 + i // 0 基準月で 4 月始まり
const date = new Date(year, monthIndex, 1)
return {
displayMonth: date.getMonth() + 1,
year: date.getFullYear(),
month: date.getMonth(),
label: `${date.getFullYear()}${date.getMonth() + 1}`
}
})
}
/* ---------- 月単位カレンダー生成 ---------- */
function buildMonth({ displayMonth, year, month, label }) {
const monthBox = document.createElement('div')
monthBox.className = 'month'
buildTitle(monthBox, label)
buildHeader(monthBox)
const first = new Date(year, month, 1)
buildPadBlocksStart(first, monthBox)
buildDateBlocks(year, month, displayMonth, monthBox)
buildPadBlocksEnd(monthBox)
return monthBox
}
function buildTitle(monthBox, label) {
const title = document.createElement('div')
title.className = 'title'
title.textContent = label
monthBox.appendChild(title)
}
function buildHeader(monthBox) {
['日', '月', '火', '水', '木', '金', '土'].forEach((day, i) => {
const cell = document.createElement('div')
cell.className = 'cell header'
addWeekendClass(cell, i)
cell.textContent = day
monthBox.appendChild(cell)
})
}
function addWeekendClass(cell, week) {
if (week === 0) {
cell.classList.add('sun')
} else if (week === 6) {
cell.classList.add('sat')
}
}
function buildPadBlocksStart(first, monthBox) {
const startDayInWeek = first.getDay()
for (let i = 0; i < startDayInWeek; i++) {
const cell = document.createElement('div')
cell.className = 'cell'
monthBox.appendChild(cell)
}
}
function buildDateBlocks(year, month, displayMonth, monthBox) {
const last = new Date(year, month + 1, 0)
const highlights = highlightMap[displayMonth] || null
for (let day = 1; day <= last.getDate(); day++) {
const dateObj = new Date(year, month, day)
const ymd = toYmd(dateObj)
const cell = document.createElement('div')
cell.className = 'cell'
cell.textContent = day
cell.dataset.date = ymd
addWeekendClass(cell, dateObj.getDay())
// 初期選択判定
let shouldSelect = false
if (highlights === null) {
shouldSelect = isWeekend(dateObj)
} else if (highlights instanceof Set) {
shouldSelect = highlights.has(ymd)
}
if (shouldSelect) {
cell.classList.add('selected')
selectedSet.add(ymd)
}
// 編集可モードのみクリックイベントを追加
if (!readonly) {
cell.addEventListener('click', () => {
cell.classList.toggle('selected')
if (cell.classList.contains('selected')) {
selectedSet.add(ymd)
} else {
selectedSet.delete(ymd)
}
})
}
monthBox.appendChild(cell)
}
}
function buildPadBlocksEnd(monthBox) {
// 7×7 マスに揃えるための埋め草
while (monthBox.children.length < 49) {
const cell = document.createElement('div')
cell.className = 'cell'
monthBox.appendChild(cell)
}
}
/* ---------- 選択済み日付取得 ---------- */
function getSelectedDatesMap() {
const res = {}
months.forEach(({ month }) => {
res[month + 1] = []
})
// 選択済み日付を振り分け
selectedSet.forEach((ymd) => {
const d = new Date(ymd)
const displayMonth = d.getMonth() + 1
res[displayMonth].push(ymd)
})
// 選択が無い月は全土日を自動追加
Object.keys(res).forEach((monthKey) => {
if (res[monthKey].length === 0) {
const monthInfo = months.find((m) => m.displayMonth == monthKey)
if (monthInfo) {
const year = monthInfo.year
const month = monthInfo.month
const lastDay = new Date(year, month + 1, 0).getDate()
for (let day = 1; day <= lastDay; day++) {
const dateObj = new Date(year, month, day)
if (isWeekend(dateObj)) {
res[monthKey].push(toYmd(dateObj))
}
}
}
}
})
return res
}
return {
fragment,
getSelectedDatesMap
}
}
})()