勤めている職場で、職員の出勤予定を見られるシステムが欲しいという要望がありました。
タイムカードのシステムを入れ替えたところで、入れ替え以前は誰でも予定を確認できたのが、現行のシステムでは管理者が自分の管理範囲内でしか見られないようになってしまったのです。
本来はその方がいいのかもしれませんが、出勤予定を確認してから電話連絡するというやり方を取っていた職員にとっては不便になってしまいました。
Excelだとデータを壊される可能性が高いですし、PDFだとフィルタがかけられないしどの日付の列かがわかりにくいです。そこでHTML上に表示してJavaScriptで所属ごとに表示するカレンダーを作りました。
プログラムの内容
こちらがサンプルファイルです。JavaScript入りのページと、勤務表からJS用のコードを出力するVBAマクロ入りExcelファイルが入っています。。
こんな感じに表示します。最初は何も表示されていませんが、表示ボタンをクリックすることでスクリプトが実行されます。
リストから所属を選択して実行するとその所属のみに絞り込まれ、「全員」で実行すると全員分表示されます。
この元データはJavaScriptの二次元配列を直接HTMLに入力しています。その方法は使っているシステムによっていろいろですが、今回はExcelの勤務表から出力します。
勤務表のサンプルを作るのに自動作成ツールが役立ちました。所属ごとの勤務表を作成します。
実行ボタンを用意していませんが、「JSコード出力」というマクロを実行します。
マクロのExcelブックと同じディレクトリに「export.txt」が作成されます。その中身は勤務表のデータに従って作成されたJavaScriptの二次元配列です。これをHTMLに入力してやります。
最初の行は1つでOKなので、2つ目以降の所属については2行目からコピペしてください。
プログラミング
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<title>202105勤務カレンダー</title>
<script>
var userAllData = new Array();
var colTitle = new Array();
function showTable() {
var userAllData = [
// ここに出力したデータを入力
['所属','氏名','01(土)','02(日)','03(月)','04(火)','05(水)','06(木)','07(金)','08(土)','09(日)','10(月)','11(火)','12(水)','13(木)','14(金)','15(土)','16(日)','17(月)','18(火)','19(水)','20(木)','21(金)','22(土)','23(日)','24(月)','25(火)','26(水)','27(木)','28(金)','29(土)','30(日)','31(月)'],
['湘北','赤木','遅1','公休','-','公休','遅2','-','-','早2','早1','公休','公休','遅2','-','-','早2','公休','-','早1','公休','公休','-','早1','-','早1','公休','-','遅1','-','公休','公休','遅2'],
['湘北','小暮','-','早2','公休','早1','遅1','公休','遅1','-','-','早2','-','公休','遅2','-','公休','公休','早1','公休','-','早1','-','早2','公休','遅2','遅2','公休','早2','公休','-','遅2','公休'],
['湘北','安田','遅2','-','-','早2','夜','夜','休','休','遅2','-','早2','-','-','早1','-','-','早2','-','早1','遅1','遅1','-','早1','-','夜','夜','休','休','遅1','-','早2'],
['湘北','宮城','公休','-','早1','遅1','-','公休','早2','早1','-','遅2','夜','夜','公休','公休','公休','早2','遅2','-','遅1','公休','早2','公休','夜','夜','公休','公休','公休','早1','遅2','-','遅1'],
['湘北','潮崎','早1','遅1','遅1','-','-','-','早1','遅2','遅1','-','早1','-','遅1','-','夜','夜','休','休','-','遅2','夜','夜','休','休','遅1','-','早1','-','早1','-','-'],
['湘北','角田','夜','夜','休','休','-','早2','-','遅1','-','-','-','早2','-','早2','早1','遅2','遅1','-','夜','夜','休','休','遅2','遅1','-','早2','-','-','早2','遅1','-'],
['湘北','桜木','公休','-','早2','-','公休','早1','-','公休','公休','-','遅2','-','夜','夜','公休','公休','夜','夜','公休','公休','早1','遅1','-','-','早1','遅2','-','早2','公休','公休','早1'],
['湘北','流川','-','公休','夜','夜','公休','公休','公休','-','公休','-','-','遅1','-','遅1','遅1','-','公休','公休','遅2','-','-','-','公休','-','-','公休','-','遅2','夜','夜','公休'],
['湘北','石井','早2','-','-','-','早1','-','-','-','-','早1','遅1','-','早1','遅2','-','早1','-','遅1','-','早2','遅2','-','早2','-','-','早1','-','-','-','早1','-'],
['湘北','佐々岡','-','早1','遅2','-','早2','遅2','遅2','-','早2','遅1','-','-','早2','-','-','遅1','-','早2','早2','-','-','遅2','-','早2','早2','-','遅2','遅1','-','早2','-'],
['湘北','三井','-','遅2','公休','遅2','-','遅1','夜','夜','公休','公休','-','早1','公休','-','遅2','-','公休','遅2','公休','-','公休','-','遅1','公休','-','遅1','夜','夜','公休','公休','-'],
['湘北','桑田','-','-','-','-','-','-','-','-','夜','夜','休','休','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','-','夜'],
['陵南','魚住','公休','-','早','遅1','遅1','遅1','公休','遅2','公休','-','早','早','公休','公休','遅1','-','公休','早','遅1','遅1','遅2','公休','遅1','-','公休','遅2','公休','遅1','公休','遅1','遅1'],
['陵南','池上','遅2','公休','遅1','-','公休','早','早','遅1','遅1','公休','公休','遅1','-','早','遅2','公休','早','遅1','公休','早','公休','遅2','公休','遅2','-','早','遅1','公休','遅1','公休','-'],
['陵南','越野','-','早','夜','夜','公休','公休','遅2','-','-','早','遅2','遅2','夜','夜','公休','公休','遅1','-','遅2','公休','夜','夜','公休','公休','夜','夜','公休','公休','早','遅2','公休'],
['陵南','仙道','夜','夜','公休','公休','早','公休','遅1','-','夜','夜','公休','公休','遅1','遅1','公休','早','遅2','遅2','-','-','早','遅1','夜','夜','公休','公休','夜','夜','公休','公休','早'],
['陵南','植草','遅1','遅1','-','早','遅2','-','夜','夜','休','休','夜','夜','休','休','-','遅2','夜','夜','休','休','-','早','遅2','遅1','遅2','-','早','-','夜','夜','休'],
['陵南','福田','-','遅2','公休','-','夜','夜','公休','公休','早','遅2','-','公休','遅2','遅2','夜','夜','公休','公休','夜','夜','公休','公休','早','公休','早','公休','-','早','遅2','-','夜'],
['陵南','相田','早','-','遅2','遅2','-','遅2','-','早','遅2','遅1','遅1','-','早','-','早','遅1','-','-','早','遅2','遅1','-','-','早','遅1','遅1','遅2','遅2','-','早','遅2'],
['海南','牧','-','遅','公休','遅','-','早','公休','-','公休','早','-','遅','-','公休','早','遅','遅','-','遅','公休','公休','-','遅','公休','早','公休','公休','遅','公休','早','遅'],
['海南','高砂','公休','-','早','-','公休','遅','-','早','-','公休','公休','早','公休','早','-','公休','-','早','公休','遅','遅','公休','早','早','遅','-','遅','公休','公休','-','早'],
['海南','神','早','-','-','早','夜','夜','公休','公休','早','遅','夜','夜','公休','公休','遅','公休','夜','夜','公休','公休','早','遅','-','遅','公休','遅','夜','夜','公休','公休','-'],
['海南','武藤','遅','公休','夜','夜','公休','公休','遅','遅','夜','夜','公休','公休','早','-','公休','早','-','公休','早','-','-','早','夜','夜','公休','公休','早','-','早','公休','夜'],
['海南','清田','-','早','遅','公休','早','-','夜','夜','公休','公休','遅','-','夜','夜','公休','公休','早','遅','夜','夜','公休','公休','-','公休','夜','夜','公休','公休','遅','遅','-'],
['海南','宮益','夜','夜','休','休','遅','-','早','-','遅','-','早','-','遅','遅','夜','夜','休','休','-','早','夜','夜','休','休','-','早','-','早','夜','夜','休'],
['翔陽','藤間','-','早','公休','-','公休','早','公休','公休','-','早','遅','-','遅','-','早','公休','公休','早','遅','-','-','遅','公休','早','遅','遅','公休','早','遅','公休','公休'],
['翔陽','花形','公休','遅','-','早','遅','公休','遅','早','遅','-','公休','早','公休','遅','公休','-','早','公休','公休','-','-','早','早','遅','公休','-','早','公休','公休','遅','遅'],
['翔陽','長谷川','遅','-','早','遅','公休','-','夜','夜','公休','遅','夜','夜','公休','公休','公休','早','公休','-','夜','夜','公休','-','遅','公休','早','早','遅','公休','夜','夜','公休'],
['翔陽','永野','夜','夜','公休','公休','早','遅','-','遅','夜','夜','公休','-','早','公休','遅','公休','遅','-','早','公休','夜','夜','公休','公休','夜','夜','公休','遅','早','早','公休'],
['翔陽','高野','早','公休','遅','-','夜','夜','公休','公休','早','公休','-','-','夜','夜','公休','遅','夜','夜','公休','遅','早','-','夜','夜','公休','公休','公休','-','公休','-','早'],
['翔陽','伊藤','-','公休','夜','夜','公休','公休','早','-','公休','-','早','遅','公休','早','夜','夜','公休','遅','公休','早','遅','-','-','-','公休','-','夜','夜','公休','公休','夜']
// データここまで
];
// 先頭の行を列のタイトルにセット
colTitle = userAllData[0];
// 先頭の行をデータから削除
userAllData.shift();
// 所属の選択
var userData = new Array();
var shozoku = document.getElementById("shozoku").value;
var n = 0;
for(var i=0;i<userAllData.length;i++){
if(shozoku=='全員'){
userData[n] = userAllData[i];
n++;
} else {
if(userAllData[i][0]==shozoku){
userData[n] = userAllData[i];
n++;
}
}
}
// 表の作成
var table = document.getElementById("dataTable");
table.innerHTML = "";
var thead = document.createElement("thead");
var tbody = document.createElement("tbody");
for(i=-1;i<userData.length;i++){
// trの作成
var tr = document.createElement("tr");
if(i!=-1){
if (i%2==0) {
tr.classList.add('evenRow')
} else {
tr.classList.add('oddRow')
}
}
for(j=1;j<userData[0].length;j++){
if (i<0){
// 列タイトル
var th = document.createElement("th");
th.innerHTML = colTitle[j];
// trに追加
tr.appendChild(th);
thead.appendChild(tr);
} else {
if(j==1){
var th = document.createElement("th");
th.innerText = userData[i][j];
// trに追加
tr.appendChild(th);
} else {
// データ
var td = document.createElement("td");
td.innerText = userData[i][j];
// trに追加
tr.appendChild(td);
}
}
}
// tableに追加
if(i<0) {
table.appendChild(thead);
} else {
tbody.appendChild(tr);
}
}
table.appendChild(tbody);
}
</script>
<style>
table {
border-collapse: separate;
border-spacing: 0px;
width: 100%;
}
th, td {
border: 1px solid gray;
white-space: nowrap;
text-align: center;
}
/* ヘッダー行の背景色 */
th {
background-color: #9dd3a8;
}
/* 奇数行の背景色 */
.oddRow th, .oddRow td {
background-color: #ceefe4;
}
/* 偶数行の背景色 */
.evenRow th, .evenRow td {
background-color: #f4f0e6;
}
/* ヘッダー行の固定 */
#dataTable thead th {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 1;
}
/* ヘッダー列の固定 */
#dataTable th:first-child {
position: -webkit-sticky;
position: sticky;
left: 0;
}
/* 左上のセルの固定 */
#dataTable thead th:first-child {
z-index: 2;
}
/* 表示エリア */
#showArea {
overflow: scroll;
width: 100%;
height: 75vh;
}
</style>
</head>
<body>
<p>202105勤務カレンダー</p>
<select id="shozoku" name=”shozoku”>
<option value='全員'>全員</option>
<option value='湘北'>湘北</option>
<option value='陵南'>陵南</option>
<option value='海南'>海南</option>
<option value='翔陽'>翔陽</option>
</select>
<input type="button" value="表示" onclick="showTable();"/>
<hr />
<div id = "showArea">
<table id="dataTable"></table>
</div>
</body>
</html>
「// ここに出力したデータを入力」のコメントの下に二次元配列を入力します。これがそのままuserAllDataの中身になります。
実行時に選択されている所属のみuserDataに格納して、その配列を元にテーブルを作成します。所属のリストはHTMLで。
奇数行と偶数行で色を分けて見やすくするため、oddRowとevenRowというクラス名を付与し、CSSで色を変えています。
ヘッダー行・列を固定する
地味に苦労したのがスクリプトよりも、スクロール時にヘッダー行と列を固定するCSSの方でした。
こちらの記事を参考にしました。stickyっていうプロパティを使います。迷ったのが行と列を固定しても左上のセルだけ動いてしまうところで、左上のセルだけは単独で指定する必要があるんですね。
あと、スクロールした時に罫線が消えてしまったり、そもそもFireFoxでは線が消えてしまうといった場合は、border-collapseプロパティをcollapseで指定していると起きるようで、separeteにしてborder-spacingを0pxにすると綺麗に見えます。
VBA
Sub JSコード出力()
Dim shozoku As String '所属
Dim rowEnd As Integer '最終行
Dim colEnd As Integer '最終列
Dim tableArr() As Variant '二次元配列
Dim txtFileName As String '出力ファイル名
Dim setStr As String '1行分の文字列
'二次元配列を作成
shozoku = Cells(1, 1).Value
Cells(2, 1).Select
rowEnd = Selection.End(xlDown).Row
colEnd = Selection.End(xlToRight).Column
tableArr = Range(Cells(2, 1), Cells(rowEnd, colEnd))
'テキストファイルに出力
txtFileName = ThisWorkbook.Path & "\export.txt"
Open txtFileName For Output As #1
'ヘッダー行
setStr = "['所属','氏名'"
For i = 3 To UBound(tableArr, 2)
'1桁の日付は頭に「0」を付ける
If Len(tableArr(1, i)) = 1 Then
tableArr(1, i) = "0" & tableArr(1, i)
End If
setStr = setStr & ",'" & tableArr(1, i) & "(" & tableArr(2, i) & ")'"
Next
setStr = setStr & "],"
Print #1, setStr
'各職員行を処理
For i = 3 To UBound(tableArr, 1)
setStr = "['" & shozoku & "','" & tableArr(i, 1) & "'"
For j = 3 To UBound(tableArr, 2)
'空白セルは「-」にする
If tableArr(i, j) = "" Then
tableArr(i, j) = "-"
End If
setStr = setStr & ",'" & tableArr(i, j) & "'"
Next j
setStr = setStr & "]"
'最終行以外は「,」を付ける
If i <> UBound(tableArr, 1) Then
setStr = setStr & ","
End If
Print #1, setStr
Next i
Close #1
End Sub
二次元配列の作り方は環境によって異なりますが、今回はExcelの勤務表を1行ずつテキストデータに出力していく方法を取りました。
コメント