【JavaScript】稼働時間の計算とTodoリストの作成をしてみた

# はじめに
既存のソースコードを理解する能力は前よりも身に付いてきた感覚はあるのですが、
基礎知識の理解が不十分で「なんとなく」読んでいる感覚が抜けません。
そこで、今回はJavaScriptを使った実装をしながら、基礎を学んでいきます。

# 実装したこと
簡易的なダッシュボードを作成します。
そこに機能を追加しながら、学んでいきたいと思います。
- ログイン機能
- 現在の稼働時間を表示できる
- Todoの作成、削除ができる

# 現状の画面
Railsでアプリケーションを作成しています。
デザイン面もあまり触れてこなかったので、今回を機に勉強していきます。
![Science_App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/5bfb4294-1cf2-4091-a7bf-bf34d830d1e6.png)


# ログイン機能
Rails8から導入された認証機能を使用します。
```
$ bin/rails generate authentication
```
[詳しくはこちらの記事を参考にしてください](https://techracho.bpsinc.jp/hachi8833/2024_10_21/145343)

# 現在の稼働時間を表示する
現在の稼働時間を計算するのが苦手で、スプレッドシートで計算をしていました。
![Cursor_と_勤怠時間計算_-_Google_スプレッドシート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577266/977bc555-5742-44e6-959e-18798e02fbfe.png)

手入力の部分が多く面倒だったので、ダッシュボードに作成しました。
要件は以下の通りです。
- 現在時刻の表示
- 勤務開始時間の入力
- 勤務開始時間の表示
- 現在の稼働時間を表示する
- 休憩時間のチェックボックス

<details open><summary>Viewのコード</summary>

```html
<div class="bg-gray-600">
  <div class="mx-auto max-w-7xl min-h-screen bg-gray-200">
    <h1 class="p-2 text-2xl font-bold font-serif">Dashboard</h1>
    <p class="p-2 text-gray-600 mb-6">ログイン中のメールアドレス：<%= Current.user.email_address if Current.user %></p>

    <div class="flex justify-center">
      <div class="mx-5 bg-white p-8 rounded-lg shadow-lg w-full max-w-lg h-full">

        <div class="flex flex-row text-center justify-center mb-6 text-2xl font-medium">
          <p class="m-2"><%= Time.current.in_time_zone('Asia/Tokyo').strftime('%Y/%m/%d (%A)') %></p>
          <p class="m-2"><span id="current-time" class="font-mono"><%= Time.current.in_time_zone('Asia/Tokyo').strftime('%H:%M:%S') %></span></p>
        </div>

        <div class="my-5 text-gray-700 font-medium">
          <div class="flex items-center mb-4">
            <label for="time-input" class="block mr-2">Input Starting time：</label>
            <input type="time" id="time-input" class="block w-32 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
            <button id="save-time" class="ml-2 px-4 py-1 bg-blue-300 text-white rounded-md hover:bg-blue-500">Save</button>
          </div>
          <div class="block mb-4">
            <label for="time-display">Starting time：</label>
            <span id="display-time" class="font-mono">--:--</span></p>
          </div>
          <div class="flex items-center">
            <p class=" mr-5">Time tracked：<span id="time-tracked" class="font-mono">--:--</span></p>
            <input type="checkbox" id="break-checkbox" class="mr-2">
            <label for="break-checkbox" class="text-sm font-medium">Include break time</label>
          </div>
        </div>
      </div>

      <div class="mx-5 flex flex-col w-full max-w-md h-full">
        <div class="bg-white p-6 rounded-lg shadow-lg">
          <div class="p-2 bg-blue-50 rounded-lg">
            <label for="hs-trailing-button-add-on" class="sr-only">Label</label>
            <div class="flex rounded-lg">
              <input type="text" id="add-text" name="hs-trailing-button-add-on" class="bg-white py-2.5 sm:py-3 px-4 block w-full border-gray-200 rounded-s-lg sm:text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none">
              <button id="add-button" type="button" class="py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-e-md border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
                Add
              </button>
            </div>
          </div>

          <div class="my-5 p-8 rounded-lg shadow-lg w-full max-w-md">
            <p class="text-center mb-2 text-xl font-semibold">Incomplete</p>
            <ul class="incomplete-lists">
            </ul>
          </div>

          <div class="my-5 p-8 rounded-lg shadow-lg w-full max-w-md">
            <p class="text-center mb-2 text-xl font-semibold">Complete</p>
            <ul class="complete-lists">
            </ul>
          </div>

        </div>
      </div>
    </div>
  </div>
</div>
```
</details>

## 現在の時刻を表示する
現在の時刻を表示します。
```html
  <p class="m-2">
    <span id="current-time" class="font-mono">
      <%= Time.current.in_time_zone('Asia/Tokyo').strftime('%H:%M:%S') %>
    </span>
  </p>
```
このままだと、リロードをしないと時刻が更新されないので、JavaScriptで更新します。
```js
function = updatetime() {
  const now = new Date();
  const hours = String(now.getHours()).padStart(2, '0');
  const minutes = String(now.getMinutes()).padStart(2, '0');
  const seconds = String(now.getSeconds()).padStart(2, '0');
  const currentTime = `${hours}:${minutes}:${seconds}`;
  document.getElementById('current-time').textContent = currentTime;

  setInterval(updateTime, 1000);
  updateTime(); // 初回実行
```

## 勤務開始時間を入力して、出力する
勤務開始時間の入力欄を作成します。
```html
  <div class="flex items-center mb-4">
    <label for="time-input" class="block mr-2">Input Starting time：</label>
    <input type="time" id="time-input" class="block w-32 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
    <button id="save-time" class="ml-2 px-4 py-1 bg-blue-300 text-white rounded-md hover:bg-blue-500">Save</button>
    </div>
```

入力した時間を`display-time`に入れます。
```js
  function saveInputTime() {
    const timeInput = document.getElementById('time-input').value;
    document.getElementById('display-time').textContent = timeInput;
    localStorage.setItem('savedTime', timeInput);
  }
```

入力された時間を表示します。
```html
  <div class="block mb-4">
    <label for="time-input">Starting time：</label>
    <span id="display-time" class="font-mono">--:--</span></p>
  </div>
```

## 現在の稼働時間を表示する
現在の稼働時間を表示します。
```html
<div class="flex items-center">
  <p class=" mr-5">Time tracked：<span id="elapsed-time" class="font-mono">--:--</span></p>
　<input type="checkbox" id="break-checkbox" class="mr-2">
　<label for="break-checkbox" class="text-sm font-medium">Include break time</label>
</div>
```

現在時刻から、勤務開始時間を引いて、現在の稼働時間を出力します。
休憩をとった後はチェックボックスをTrueにして、現在の稼働時間から1時間を引きます。
```js
const savedTime = localStorage.getItem('savedTime');
  if (savedTime) {
    const [startHours, startMinutes] = savedTime.split(':').map(Number);
    const startTime = new Date(now);
    startTime.setHours(startHours, startMinutes, 0, 0);

    let elapsedMilliseconds = now - startTime;
    const breakCheckbox = document.getElementById('break-checkbox');
    if (breakCheckbox.checked) {
      elapsedMilliseconds -= 60 * 60 * 1000; // 1時間を引く
  }

  const elapsedMinutes = Math.floor(elapsedMilliseconds / (1000 * 60));
  const elapsedHours = Math.floor(elapsedMinutes / 60);
  const remainingMinutes = elapsedMinutes % 60;
  document.getElementById('time-tracked').textContent = `${elapsedHours}h ${remainingMinutes}m`;
```

# Todoリストの作成
Todoリストは[じゃけぇさん](https://www.udemy.com/user/huriransuziyakee/)のUdemyを参考に作成しました。

https://www.udemy.com/share/103Fxl3@a2jRNrfNfrdH6QQdtJYfGvjqes_a_DLcethVniX-H11U9RPQ-D8a47xTHldYmnbEGg==/

Todoの入力欄、未完了リスト、完了リストを表示します。

```html
  <div class="mx-5 flex flex-col w-full max-w-md h-full">
    <div class="bg-white p-6 rounded-lg shadow-lg">
      <div class="p-2 bg-blue-50 rounded-lg">
        <label for="hs-trailing-button-add-on" class="sr-only">Label</label>
        <div class="flex rounded-lg">
          <input type="text" id="add-text" name="hs-trailing-button-add-on" class="bg-white py-2.5 sm:py-3 px-4 block w-full border-gray-200 rounded-s-lg sm:text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none">
          <button id="add-button" type="button" class="py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-e-md border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
            Add
          </button>
        </div>
      </div>

      <div class="my-5 p-8 rounded-lg shadow-lg w-full max-w-md">
        <p class="text-center mb-2 text-xl font-semibold">Incomplete</p>
        <ul class="incomplete-lists">
        </ul>
      </div>

      <div class="my-5 p-8 rounded-lg shadow-lg w-full max-w-md">
        <p class="text-center mb-2 text-xl font-semibold">Complete</p>
        <ul class="complete-lists">
        </ul>
      </div>
    </div>
  </div>
```

Addボタンがクリックされた時、`onClickAdd`を呼び出します。

```js
  document.getElementById('add-button').addEventListener('click', onClickAdd);
```

入力されたテキストを読み込んで、テキストを削除後、
`createIncompleteTodo`を呼び出します。

```js
  const onClickAdd = () => {
    const inputText = document.getElementById('add-text').value;
    document.getElementById('add-text').value = '';
    createIncompleteTodo(inputText);
  }
```

`<Li>`タグ、`<p>`タグ、`<div>`タグを生成します。
`<p>`タグには、先ほど読み込んだテキストを入れます。

```js
  const createIncompleteTodo = (todo) => {
    const li = document.createElement('li');
    li.className = 'flex items-center justify-between';

    const p = document.createElement('p');
    p.className = 'flex-grow my-3';
    p.innerText = todo;

    const div = document.createElement('div');
    div.className = 'flex-shrink-0';
```

completeボタンとdeleteボタンを生成します。

```js
  const completeButton = document.createElement('button');
  completeButton.className = 'ml-2 px-4 py-1 bg-blue-300 text-white rounded-md hover:bg-blue-500';
  completeButton.innerText = 'Complete';

  const deleteButton = document.createElement('button');
  deleteButton.className = 'ml-2 px-4 py-1 bg-red-300 text-white rounded-md hover:bg-red-500';
  deleteButton.innerText = 'Delete';
```

以下の階層になるように、`incomplete-lists`に配置します。
```html
  <ul class="incomplete-lists">
    <li class=" flex items-center justify-between">
    <p class="flex-grow my-3">Todo1</p>
    <div class="flex-shrink-0">
      <button class=" ml-2 px-4 py-1 bg-blue-300 text-white rounded-md hover:bg-blue-500">Complete</button>
      <button class="ml-2 px-4 py-1 bg-red-300 text-white rounded-md hover:bg-red-500">Delete</button>
    </div>
    </li>
  </ul>
```
```js
  li.appendChild(p);
  li.appendChild(div);
  div.appendChild(completeButton);
  div.appendChild(deleteButton);
  document.querySelector('.incomplete-lists').appendChild(li);
```

completeボタンがクリックされた時は、以下の処理が実行されます。
- completeボタンを削除し、backボタンを生成する
- backボタンをdivタグ配下に配置し、`<p>`タグと`<div>`タグを含んだ`<Li>`タグをcomplete-listsに配置する

```js
  completeButton.addEventListener('click', () => {
    const moveTarget = completeButton.closest('li');
    completeButton.nextElementSibling.remove();
    completeButton.remove();
    const backButton = document.createElement('button');
    backButton.className = 'ml-2 px-4 py-1 bg-blue-300 text-white rounded-md hover:bg-blue-500';
    backButton.innerText = 'back';
```

backボタンがクリックされた時は、以下の処理が実行されます。
- `<p>`タグのテキストを読み込んで、`createIncompleteTodo`を呼び出す
- `complete-lists`内のbackボタンに一番近い`<li>`タグを削除する

```js
  backButton.addEventListener('click', () => {
    const todoText = backButton.closest('li').querySelector('p').innerText;
    createIncompleteTodo(todoText);
    document.querySelector('.complete-lists').removeChild(backButton.closest('li'));
  }
```

deleteボタンがクリックされた時は、以下の処理が実行されます。
- `incomplete-lists`からdeleteボタンに一番近い`<li>`タグを削除する

```js
  deleteButton.addEventListener('click', () => {
    const deleteTarget = deleteButton.closest('li');
    document.querySelector('.incomplete-lists').removeChild(deleteTarget);
  });
```

# まとめ
今回は、稼働時間の計算とTodoリストを実装しました。
1つひとつの処理を順序立てて細かく把握する時間をあまりとっていなかったので、良い勉強になりました。
今後も色々な機能を追加しながら、JavaScriptの理解を深めていきたいと思います。