【備忘録】休業日と営業日の表示を非同期通信で切り替えるRailsアプリの実装

## 内容
休業日と営業日を切り替えて表示するために、以下のコードを実装しました。
備忘録なので説明は省いています（面倒くさいので・・・）

### `app/controllers/reservations_controller.rb`

```ruby
def update_slots
  date = params[:date]
  @date = Date.parse(date)
  @company = current_company
  @time_slots = generate_time_slots(@company)
  @non_business_day = ScheduleService.is_non_business_day?(@company, @date)
  @services = @company.services
  @service_availability = ScheduleService.calculate_availability(@company, @date, @services, @time_slots)
  response_data = {
    services: @services.map { |service|
      {
        name: service.name,
        duration: format_duration(service.duration),
        time_slots: @time_slots.map { |slot| { availability: @service_availability.dig(service.id, slot) || '-' } },
      }
    },
    time_slots: @time_slots,
    non_business_day: @non_business_day
  }
  render json: response_data
end
```

### ビュー (`app/views/schedules/show.html.erb`)

```erb
<div class="schedule-container">
  <% if @non_business_day %>
    <h2><%= "#{@date.strftime("%Y年%m月%d日")}は休業日です" %></h2>
  <% else %>
    <div>
      ○: 空き枠あり、△: 空き枠わずか、- : 要確認、×: 受付不可
    </div>

    <div class="table-responsive">
      <table class="table table-bordered text-center">
        <thead class="thead-light">
          <tr>
            <th class="sticky-left">サービス</th>
            <th class="sticky-duration">所要時間</th>
            <% @time_slots.each do |slot| %>
              <th><%= slot %></th>
            <% end %>
          </tr>
        </thead>
        <tbody>
          <% @services.each do |service| %>
            <tr>
              <th class="service-name sticky-left"><%= service.name %></th>
              <th class="service-duration sticky-duration"><%= format_duration(service.duration) %></th>
              <% @time_slots.each do |slot| %>
                <td>
                  <%= @service_availability[service.id][slot] %>
                </td>
              <% end %>
            </tr>
          <% end %>
        </tbody>
      </table>
    </div>
    <%= paginate @services %>
  <% end %>
</div>
```

### JavaScript (`app/javascript/packs/application.js`)

```javascript
// ある日のスケジュールとかを取得するときの非同期通信用
document.addEventListener('turbo:load', function() {
  const dateField = document.querySelector('#for_customer_index_date_field');
  if (dateField) {
    const companyId = dateField.dataset.companyId;
    const scheduleTitle = document.querySelector('.page-index');
    let companyName = '';

    if (scheduleTitle && scheduleTitle.textContent) {
      const match = scheduleTitle.textContent.match(/^(.*?)の/);
      if (match) {
        companyName = match[1];
      }
    }

    const debouncedFetch = debounce(function(date) {
      fetch(`/reservations/update_slots?date=${date}&company_id=${companyId}`, {
        headers: {
          'Accept': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
          'X-CSRF-Token': document.querySelector("[name='csrf-token']").content
        },
        credentials: 'include'
      })
      .then(response => response.json())
      .then(data => {
        if (data && data.services && data.time_slots !== undefined) {
          updateTable({ ...data, date });
          if (companyName) {
            scheduleTitle.textContent = `${formatDate(date)}のスケジュール`; // 安全に更新
          }
        } else {
          console.error("Invalid data format received:", data);
        }
      })
      .catch(error => console.error('Error fetching data:', error));
    }, 500);

    dateField.addEventListener('input', function() {
      debouncedFetch(this.value);
    });
  }
});

function updateTable(data) {
  const scheduleContainer = document.querySelector('.schedule-container');

  if (data.non_business_day) {
    // 休業日の場合、全ての内容を削除して休業日メッセージを表示
    scheduleContainer.innerHTML = `<h2>${formatDate(data.date)}は休業日です</h2>`;
  } else {
    // 営業日の場合の表示
    scheduleContainer.innerHTML = `
      <div>
        ○: 空き枠あり、△: 空き枠わずか、- : 要確認、×: 受付不可
      </div>
      <div class="table-responsive">
        <table class="table table-bordered text-center">
          <thead class="thead-light">
            <tr>
              <th class="sticky-left">サービス</th>
              <th class="sticky-duration">所要時間</th>
              ${data.time_slots.map(slot => `<th>${slot}</th>`).join('')}
            </tr>
          </thead>
          <tbody>
            ${data.services.map(service => `
              <tr>
                <th class="service-name sticky-left">${service.name}</th>
                <th class="service-duration sticky-duration">${service.duration}</th>
                ${service.time_slots.map(slot => `<td>${slot.availability}</td>`).join('')}
              </tr>
            `).join('')}
          </tbody>
        </table>
      </div>
    `;
  }
}

// 年月日のフォーマット関数
function formatDate(date) {
  const d = new Date(date);
  const year = d.getFullYear();
  const month = ('0' + (d.getMonth() + 1)).slice(-2); // 月を2桁にフォーマット
  const day = ('0' + d.getDate()).slice(-2); // 日を2桁にフォーマット
  return `${year}年${month}月${day}日`;
}

// デバウンス関数
function debounce(func, wait) {
  let timeout;
  return function() {
    const context = this, args = arguments;
    const later = function() {
      timeout = null;
      func.apply(context, args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}
```