2025年5月3日土曜日

【無料で簡単】予約管理Webツールの作り方

【無料で簡単】Excel不要!あらゆる業種に使える「予約管理Webツール」の作り方

💭こんな悩み、ありませんか?

  • Excelでの予約管理が煩雑で、入力ミスや更新漏れが起きる
  • Googleフォームや有料予約システムは導入が難しい、または高い
  • 電話や紙での予約管理が非効率で、スタッフ間で情報が共有できない

👨‍⚕️実際の現場からー

クリニック勤務の裏方の友人は、こんな悩みを抱えていました:

  • 「先生がExcelで予約表を作っているけど、セルを探して名前を入力するのが面倒で…」
  • 「電話が鳴るたびにパソコンを開いて、間違えずに書き込むのが大変です」
  • 「Web上で簡単に予約できて、そのままExcelみたいに使えるといいんだけどなぁ…」
  • 「そもそもExcelって入れるのに1万円ぐらいかかっちゃうんだよね…」

「Excelだと不便。そもそも入れると高い。でも高機能なツールは不要」という中小規模の現場には、シンプルで無料な予約管理ツールが求められています。

✅この記事を読むとできること

  • Excel不要!Webブラウザで予約管理ができるシンプルなシステムを自作できます
  • PowerShellとHTMLだけで完結。インターネット接続も不要
  • 整骨院、美容室、学習塾、カフェ、ジムなど、業種問わず活用可能

こんな感じの予約表ができます!

🧑‍🔧ステップ1: PowerShellで予約用CSVを自動生成

以下のPowerShellスクリプトを使って、日付・時間スロット入りのCSVを作成します。


Add-Type -AssemblyName System.Windows.Forms

# フォーム作成
$form = New-Object System.Windows.Forms.Form
$form.Text = "日付選択フォーム"
$form.Width = 300
$form.Height = 220
$form.StartPosition = "CenterScreen"

# 開始日ラベル
$labelStart = New-Object System.Windows.Forms.Label
$labelStart.Text = "開始日:"
$labelStart.Top = 20
$labelStart.Left = 10
$form.Controls.Add($labelStart)

# 開始日ピッカー
$startPicker = New-Object System.Windows.Forms.DateTimePicker
$startPicker.Format = 'Short'
$startPicker.Top = 40
$startPicker.Left = 10
$form.Controls.Add($startPicker)

# 終了日ラベル
$labelEnd = New-Object System.Windows.Forms.Label
$labelEnd.Text = "終了日:"
$labelEnd.Top = 80
$labelEnd.Left = 10
$form.Controls.Add($labelEnd)

# 終了日ピッカー
$endPicker = New-Object System.Windows.Forms.DateTimePicker
$endPicker.Format = 'Short'
$endPicker.Top = 100
$endPicker.Left = 10
$form.Controls.Add($endPicker)

# OKボタン
$okButton = New-Object System.Windows.Forms.Button
$okButton.Text = "OK"
$okButton.Top = 140
$okButton.Left = 100
$okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $okButton
$form.Controls.Add($okButton)

# フォーム表示と戻り値確認
if ($form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
    # 値の取得
    $start = $startPicker.Value.Date
    $end = $endPicker.Value.Date

    # 出力ファイルパス
    $outputPath = "C:\TEST\data.csv"

    # CSVヘッダー
    "予約日,予約時間,予約者1,予約者2,予約者3,予約者4,予約者5" | Out-File -FilePath $outputPath -Encoding UTF8

    # 日付ループ
    while ($start -le $end) {
        for ($hour = 9; $hour -le 21; $hour++) {
            foreach ($min in @(0, 30)) {
                $time = "{0}:{1:00}" -f $hour, $min
                "$($start.ToString('yyyy/M/d')),$time,,,,," | Out-File -Append -FilePath $outputPath -Encoding UTF8
            }
        }
        $start = $start.AddDays(1)
    }

    Write-Host "CSVファイルを作成しました: $outputPath"
} else {
    Write-Host "キャンセルされました。"
}

}

このPowerShellスクリプトを実行してみると、いつからいつまでの予約表を作るか聞かれます!

今回は開始日を2025/4/1、終了日を2025/4/30とします。

なお日付右側のボタンでカレンダーから日付を選ぶことができます。

プログラムを実行すると以下のように空のCSVファイルができます。

💻ステップ2:予約をブラウザ上で管理できるHTMLを作成

先ほどのCSVを読み込み、Web上で予約を閲覧・入力・削除できるページをPowerShellで自動生成します。
下記のコードを実行してください。

※ $folderPath = "C:\TEST" は便宜上の設定です。Webページで更新した後のファイルは download フォルダに保存されるため、そちらを指定しても構いません。

createhtml.ps1


# PowerShellスクリプト: reserve*.csvの最新ファイルからHTMLを生成
# ファイルパス
$folderPath = "C:\TEST"
$htmlPath = "C:\TEST\reservation.html"

# デバッグ: スクリプト開始
Write-Host "スクリプトを開始します..."

# reserve*.csvファイルの検索(最新ファイルを優先)
try {
    $csvFile = Get-ChildItem -Path "$folderPath\reserve*.csv" -File |
               Sort-Object -Property LastWriteTime -Descending |
               Select-Object -First 1
    if (-not $csvFile) {
        Write-Host "エラー: $folderPath に reserve*.csv が見つかりません。CSVファイルを準備してください。"
        Write-Host "サンプルCSVフォーマット:"
        Write-Host "予約日,予約時間,予約者1,予約者2,予約者3,予約者4,予約者5"
        Write-Host "4/1,9:00,山田,田中,,鈴木,,"
        exit
    }
    $csvPath = $csvFile.FullName
    Write-Host "最新CSVファイルを選択しました: $csvPath (更新日時: $($csvFile.LastWriteTime))"
} catch {
    Write-Host "エラー: reserve*.csv の検索に失敗しました。詳細: $_"
    exit
}

# CSV読み込み(UTF-8対応)
try {
    $csvContent = Import-Csv -Path $csvPath -Encoding UTF8
    Write-Host "CSVファイルを正常に読み込みました。レコード数: $($csvContent.Count)"
} catch {
    Write-Host "エラー: CSV読み込みに失敗しました。詳細: $_"
    Write-Host "確認事項:"
    Write-Host "- CSVファイルがUTF-8で保存されているか"
    Write-Host "- ヘッダーが正しいか(予約日,予約時間,予約者1,予約者2,予約者3,予約者4,予約者5)"
    exit
}

# CSVデータをJSONに変換
try {
    $jsonData = $csvContent | ConvertTo-Json -Compress
    Write-Host "CSVデータをJSONに変換しました。"
} catch {
    Write-Host "エラー: JSON変換に失敗しました。詳細: $_"
    exit
}

# HTMLテンプレート(変更時にreserve.csvをダウンロード)
$htmlTemplate = @"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>オフライン予約表(CSV管理)</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        button { margin: 5px; }
        .input-group { margin-bottom: 10px; }
        label { margin-right: 10px; }
        .delete-btn { font-size: 12px; padding: 2px 5px; }
        .editable { cursor: pointer; background-color: #f9f9f9; }
        .editable:hover { background-color: #e0e0e0; }
        .alert { color: #d32f2f; font-weight: bold; margin-bottom: 10px; }
    </style>
</head>
<body>
    <h1>予約表(CSV管理)</h1>
    <div class="alert">
        予約の追加、削除、入力時にreserve.csvが自動ダウンロードされます。<br>
        ダウンロード後、C:\TEST\reserve.csvに上書き保存してください。<br>
        最新データを反映するには、PowerShellで .\generate_reservation.ps1 を再実行してください。
    </div>

    <table id="reservationTable">
        <thead>
            <tr>
                <th>予約日</th>
                <th>予約時間</th>
                <th>予約者1</th>
                <th>予約者2</th>
                <th>予約者3</th>
                <th>予約者4</th>
                <th>予約者5</th>
            </tr>
        </thead>
        <tbody id="reservationBody"></tbody>
    </table>

    <script>
        // CSVデータ(PowerShellから埋め込み)
        let reservations = $jsonData;
        console.log("初期データ:", reservations);

        // ページロード時に表を描画
        document.addEventListener("DOMContentLoaded", renderTable);

        function renderTable() {
            console.log("renderTable: 表を描画します");
            const tableBody = document.getElementById("reservationBody");
            tableBody.innerHTML = "";
            reservations.forEach((res, index) => {
                const row = document.createElement("tr");
                const formattedDate = res.予約日.includes("-")
                    ? new Date(res.予約日).toLocaleDateString("ja-JP", { month: "numeric", day: "numeric" })
                    : res.予約日;
                row.innerHTML =
                    "<td>" + formattedDate + "</td>" +
                    "<td>" + res.予約時間 + "</td>" +
                    "<td>" + (res.予約者1 && res.予約者1.trim() ? res.予約者1 + ' <button class="delete-btn" onclick="deletePerson(' + index + ', \'予約者1\')">削除</button>' : '<span class="editable" onclick="editPerson(' + index + ', \'予約者1\')">[入力]</span>') + "</td>" +
                    "<td>" + (res.予約者2 && res.予約者2.trim() ? res.予約者2 + ' <button class="delete-btn" onclick="deletePerson(' + index + ', \'予約者2\')">削除</button>' : '<span class="editable" onclick="editPerson(' + index + ', \'予約者2\')">[入力]</span>') + "</td>" +
                    "<td>" + (res.予約者3 && res.予約者3.trim() ? res.予約者3 + ' <button class="delete-btn" onclick="deletePerson(' + index + ', \'予約者3\')">削除</button>' : '<span class="editable" onclick="editPerson(' + index + ', \'予約者3\')">[入力]</span>') + "</td>" +
                    "<td>" + (res.予約者4 && res.予約者4.trim() ? res.予約者4 + ' <button class="delete-btn" onclick="deletePerson(' + index + ', \'予約者4\')">削除</button>' : '<span class="editable" onclick="editPerson(' + index + ', \'予約者4\')">[入力]</span>') + "</td>" +
                    "<td>" + (res.予約者5 && res.予約者5.trim() ? res.予約者5 + ' <button class="delete-btn" onclick="deletePerson(' + index + ', \'予約者5\')">削除</button>' : '<span class="editable" onclick="editPerson(' + index + ', \'予約者5\')">[入力]</span>') + "</td>";
                tableBody.appendChild(row);
            });
        }

        function addReservation() {
            console.log("addReservation: 予約追加を開始");
            const date = document.getElementById("date").value;
            const time = document.getElementById("time").value;
            const person1 = document.getElementById("person1").value;
            const person2 = document.getElementById("person2").value;
            const person3 = document.getElementById("person3").value;
            const person4 = document.getElementById("person4").value;
            const person5 = document.getElementById("person5").value;

            if (date && time) {
                const formattedDate = new Date(date).toLocaleDateString("ja-JP", { month: "numeric", day: "numeric" });
                reservations.push({
                    予約日: formattedDate,
                    予約時間: time,
                    予約者1: person1,
                    予約者2: person2,
                    予約者3: person3,
                    予約者4: person4,
                    予約者5: person5
                });
                console.log("addReservation: 新しい予約を追加", reservations);
                renderTable();
                saveCSV();
                document.getElementById("date").value = "";
                document.getElementById("time").value = "";
                document.getElementById("person1").value = "";
                document.getElementById("person2").value = "";
                document.getElementById("person3").value = "";
                document.getElementById("person4").value = "";
                document.getElementById("person5").value = "";
            } else {
                alert("予約日と予約時間は必須です");
            }
        }

        function deletePerson(index, personKey) {
            console.log("deletePerson: 削除開始", { index, personKey });
            reservations[index][personKey] = "";
            renderTable();
            saveCSV();
        }

        function editPerson(index, personKey) {
            console.log("editPerson: 入力開始", { index, personKey });
            const name = prompt("予約者名を入力してください");
            if (name && name.trim()) {
                reservations[index][personKey] = name.trim();
                console.log("editPerson: 名前を更新", reservations);
                renderTable();
                saveCSV();
            }
        }

        function saveCSV() {
            console.log("saveCSV: CSVダウンロードを開始");
            try {
                const header = "予約日,予約時間,予約者1,予約者2,予約者3,予約者4,予約者5\n";
                const csvContent = header + reservations.map(res =>
                    res.予約日 + "," + res.予約時間 + "," +
                    (res.予約者1 || "") + "," + (res.予約者2 || "") + "," +
                    (res.予約者3 || "") + "," + (res.予約者4 || "") + "," +
                    (res.予約者5 || "")
                ).join("\n");
                const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
                const link = document.createElement("a");
                const url = URL.createObjectURL(blob);
                link.setAttribute("href", url);
                link.setAttribute("download", "reserve.csv");
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                console.log("reserve.csvをダウンロードしました。C:\\TEST\\reserve.csvに上書き保存してください。");
                console.log("saveCSV: ダウンロード完了");
            } catch (error) {
                console.error("saveCSV: エラー発生", error);
                alert("CSVダウンロードに失敗しました。コンソールを確認してください。");
            }
        }
    </script>
</body>
</html>
"@

# HTMLファイルに保存(UTF-8 BOMなし)
try {
    $htmlTemplate | Out-File -FilePath $htmlPath -Encoding UTF8
    Write-Host "HTMLファイルが正常に生成されました: $htmlPath"
    Write-Host "Microsoft Edgeで $htmlPath を開いて予約表を操作してください。"
} catch {
    Write-Host "エラー: HTMLファイルの生成に失敗しました。詳細: $_"
    exit
}

上記のコードをPowerShellで実行すると、HTMLファイルが生成されます。
ブラウザで開くと以下のような予約表が表示されます(CSVの内容を表形式で表示)。

では、予約を入れてみましょう!

  • 例: 9:00 の予約者1 に 清盛さん を登録

入力をクリックすると予約者の名前を入力するポップが出ますので皐清盛と入力します。

入力が完了すると画面上とCSVに清盛の予約が反映されます。

名前の右にある削除ボタンを押すと予約が削除され画面とCSVに反映されます。

💾ステップ3:CSVを保存・再利用して更新

予約内容を入力・削除したら、ブラウザからCSV形式で保存される仕組みになっています。保存されたCSVをPowerShellスクリプトで読み込めば、常に最新の予約表が再生成されます。

✅まとめ:Excel不要!「自分の現場向け予約管理」を自作しよう

「難しいシステムは要らないけど、紙やExcelの予約管理は限界…」

そんなあなたにぴったりなのが、PowerShellとHTMLで作るシンプルな予約管理システムです。

📌 ここまでの流れをおさらい

  • ① PowerShellでCSV予約表を自動生成
    → カレンダーで期間を選ぶだけ。30分刻みの予約枠が入ったCSVを作成。
  • ② CSVをHTMLに変換し、ブラウザで予約操作が可能に
    → 名前の入力・削除ができ、CSVとして保存されます。
  • ③ 保存したCSVで最新の予約表を再表示
    → データの再利用・更新も簡単!

システム開発の知識がなくても、導入コストゼロ・ネット不要・業種不問で使えるこの仕組み。

小さな現場にこそ、「ちょうどいい」予約管理ツールを、自分の手で作ってみませんか?

2025年4月27日日曜日

【GW2025】電車代が高くて旅行を諦めるのはもったいない!東京近郊をお得に回る2つの神チケット(都区内パス、24時間券) by裏方徹也



こんにちは、裏方徹也です。
今年のゴールデンウィーク、家族や友人とのお出かけを計画している方も多いのではないでしょうか?
でも、こんな悩みありませんか?

💸「行きたいけど交通費が高すぎる…」
・家族4人で出かけたら、電車代だけで5,000円超えた…
・スタンプラリー巡りや観光をしたいけど、途中下車が多くて出費がかさむ
・節約したいけど、安いルートを調べるのが面倒くさい
実は、交通費で損している人がめちゃくちゃ多いんです。

✅この記事を読むとどうなる?
・移動コストを1人当たり500円以上節約できる場合がある
・面倒な乗り換えや精算から解放されて、スムーズに移動できる
・浮いたお金でもう1箇所寄り道したり、美味しいものが食べられる!

今回は、東京近郊をお得に楽しめる2つの鉄板チケットをご紹介します。両方とも、GW中にも使える優秀アイテムです!

🚃【1】都区内パス(JR東日本)
 



▶どんなチケット?
東京都23区内のJR線が1日乗り降り自由になるフリーパス。

▶こんな人におすすめ!
スタンプラリーや美術館めぐりなど、複数のJR駅を使う予定の人
一日かけて都内を広く移動したい人

▶料金:大人760円
▶どれくらいお得?
例)JRで6回移動すると…
通常料金:150円 × 6回 = 900円
→ 都区内パスなら 760円 に!140円お得
さらに、途中下車しても追加料金なし。Suicaでも購入可!

▶便利な理由
・23区内なら、気にせず自由に途中下車できる
・運転見合わせ等となり、振替輸送が使用できる場合、振替輸送の対象となる

⚠注意点:JR東日本の東京23区内の駅しか使用できない。23区外もしくは他社線をまたいで乗車する場合は別途精算が必要
例)東京駅から都区内パスで入場し、横浜で降りた場合、蒲田~横浜までの区間230円を別途支払う必要があります。


🚇【2】東京メトロ24時間券
 



※値段は600円とありますが値上がり前に撮影しました。現在は700円です。
 




▶どんなチケット?
使い始めてから24時間、東京メトロ乗り放題になるチケット。但しICカード版だとその日限り。

▶こんな人におすすめ!
渋谷・表参道・上野・池袋など、都心を中心に回りたい人
翌日にまた移動予定がある人(午後スタートの旅行など)

▶料金:大人700円/IC対応は当日限り

▶お得な使い方例
4回以上乗れば元が取れる!
初乗り180円 × 4回 = 720円
→ 24時間券で700円 に!

▶便利な理由
・例えば4/28の15:00に使い始めたら、翌日4/29の15:00まで有効!
・乗車している最中に有効期限が過ぎた場合は降車した駅まで有効です!
・美術館やカフェ巡りなど、ゆったり旅にピッタリ
・運転見合わせ等となり、振替輸送が使用できる場合、振替輸送の対象となる

⚠注意点1:都営地下鉄では使えません!
⚠注意点2:東京メトロは丸ノ内線、銀座線以外は全て直通運転をしているため、他社線をまたいで利用する場合は別途精算が必要になります。
例)副都心線新宿三丁目から乗車し、東急東横線の代官山駅で降りた場合、東横線の区間渋谷~代官山まで140円別途支払う必要があります。

🤔どっちを選ぶべき?

比較項目    都区内パス    メトロ24時間券
対象エリア    JR 23区内    東京メトロ全線
利用期間    当日限り    24時間有効

迷ったら「目的地がJRでしか行けないのか、メトロでしかいけないのか」で選びましょう!


🎁まとめ:賢く使えばGWの満足度が爆上がり!
交通費は、気づかないうちに旅の満足度を下げる落とし穴。でも、この記事で紹介した2つのチケットを使えば…
出費を減らして、そのぶん観光・食事・お土産に回せる
時間のロスも減り、旅行がもっとスムーズに!
そして何より、「もう一か所行ける」気持ちの余裕が生まれる!
また、使用中に運転見合わせが発生し振り替え輸送ができる場合、この券を使うと対象になります!利用予定の路線が止まってしまった時の万が一の保険という意味でも使えます!


✈さあ、お得に東京を旅しよう!
あなたのGWが、もっと自由で楽しくなることを願って。
裏方徹也でした!