円形プログレスバー、カレンダーを用いたタスク管理アプリの作成

# はじめに 
各カテゴリごとにタスクを分け、それらの進捗度を円形プログレスバーで表示する機能と、カレンダー機能を融合させたタスク管理アプリを作成していきます。

# 開発環境
・windows11
・ruby3.1.6
・rails7.2.2.1

# 基本的なアプリケーション構造
### 保存するカラム
・タスク名
・タスク詳細
・期限
・完了状態
・カテゴリ(今回seedに保存)→今回は大学、サークル、バイト の3つのカテゴリで分けて作成していきます。

# 実装手順
任意のアプリを作成
```terminal:コマンドプロンプト
rails new アプリ名

cd アプリ名
```
# モデルの作成
今回、Categoryモデル/Taskモデルを使用
```terminal:コマンドプロンプト
rails g model Category name:string

rails g model Task title:string description:text due_date:date completed:boolean category:references

rails db:migrate
```

# シードデータの作成(カテゴリの作成)
```Ruby:db/seeds.rb
Category.create([
    { name: '大学' },
    { name: 'サークル' },
    { name: 'バイト' }
])
```
その後コマンドプロンプトで下記を実行
```terminal:コマンドプロンプト
rails db:seed
```

# モデル間のアソシエーションの設定
```Ruby:app/models/category.rb
class Category < ApplicationRecord
    has_many :tasks
end
```
```Ruby:app/models/task.rb
class Task < ApplicationRecord
  belongs_to :category
  validates :title, presence: true
end
```
# コントローラーの作成
```terminal:コマンドプロンプト
rails g controller Tasks index new create edit update destroy
```
# ルーティングの設定
```Ruby:config/routes.rb
Rails.application.routes.draw do
  root 'tasks#index'
  resources :tasks
end
```
# タスク一覧表示とカテゴリ別達成度の計算
```Ruby:app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    @tasks = Task.includes(:category).all
  end

  def new
    @task = Task.new
    @categories = Category.all
  end

  def create
    @task = Task.new(task_params)
    if @task.save
      redirect_to tasks_path, notice: 'タスクが作成されました。'
    else
      @categories = Category.all
      render :new
    end
  end

  def edit
    @task = Task.find(params[:id])
    @categories = Category.all
  end

  def update
    @task = Task.find(params[:id])
    if @task.update(task_params)
      redirect_to tasks_path, notice: 'タスクが更新されました。'
    else
      @categories = Category.all
      render :edit
    end
  end

  def destroy
    @task = Task.find(params[:id])
    @task.destroy
    redirect_to tasks_path, notice: 'タスクが削除されました。'
  end

  private

  def task_params
    params.require(:task).permit(:title, :description, :due_date, :completed, :category_id)
  end
end
```
# ビューの作成
```Ruby:app/views/tasks/index.html.erb
<h1>タスク一覧</h1>

<%= link_to '新規タスク作成', new_task_path %>

<% @tasks.group_by(&:category).each do |category, tasks| %>
    <h2><%= category.name %></h2>
    <ul>
        <% tasks.each do |task| %>
        <li>
            <%= task.title %> - 
            <%= task.completed ? '完了' : '未完了' %> - 
            期限: <%= task.due_date %>
            <%= link_to '編集', edit_task_path(task) %>
            <%= button_to '削除', task_path(task), method: :delete, data: { confirm: '本当に削除しますか？' } %>
        </li>
        <% end %>
    </ul>
<% end %>
```
```Ruby:app/views/tasks/new.html.erb
<h1><%= @task.new_record? ? '新規タスク作成' : 'タスク編集' %></h1>

<%= form_with(model: @task, local: true) do |form| %>
    <div>
        <%= form.label :タスク名 %>
        <%= form.text_field :title %>
    </div>

    <div>
        <%= form.label :詳細 %>
        <%= form.text_area :description %>
    </div>

    <div>
        <%= form.label :締切日 %>
        <%= form.date_field :due_date %>
    </div>

    <div>
        <%= form.label :タスクのカテゴリ %>
        <%= form.collection_select :category_id, @categories, :id, :name %>
    </div>

    <%= form.submit %>
<% end %>

<%= link_to '戻る', tasks_path %>
```
```Ruby:app/views/tasks/edit.html.erb
<h1><%= @task.new_record? ? '新規タスク作成' : 'タスク編集' %></h1>

<%= form_with(model: @task, local: true) do |form| %>
    <div>
        <%= form.label :タスク名 %>
        <%= form.text_field :title %>
    </div>

    <div>
        <%= form.label :詳細 %>
        <%= form.text_area :description %>
    </div>

    <div>
        <%= form.label :締切日 %>
        <%= form.date_field :due_date %>
    </div>

    <div>
        <%= form.label :カテゴリ %>
        <%= form.collection_select :category_id, @categories, :id, :name %>
    </div>

    <div>
        <%= form.label :タスクが完了したらチェック！ %>
        <%= form.check_box :completed %>
    </div>

    <%= form.submit %>
<% end %>

<%= link_to '戻る', tasks_path %>
```

#### ここまで無事に実装ができると簡易的なタスク管理アプリが出来上がります。
現状できることはこちらです。確認してみてください。
・新規タスク作成ができる
・タスクを追加すると、indexに ”タスク名 - 未完了 - 期限: 2025-〇-✕  編集 削除” この形式でカテゴリごとにタスクが追加される
・編集を押してタスク編集へ遷移し、”タスクが完了したらチェック！” にチェックを付け、Update Taskを押す。一覧へ戻へ再び遷移、チェックを付けたことで、表示が “未完了→完了” に変化する

無事にできていればここまでのコーディングはクリアです！
次は、ここまでの機能を応用し、それぞれのタスクの進捗度を追加する機能を実装していきます。

# Taskモデルに進捗度を計算するメソッドを追加する
```Ruby:app/models/task.rb
class Task < ApplicationRecord
  belongs_to :category
  validates :title, presence: true
  def progress
    completed ? 100 : 0
  end
end
```
```Ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  #下の記述を追記
  helper_method :calculate_progress

  private

  def calculate_progress(tasks)
    return 0 if tasks.empty?
    (tasks.count(&:completed).to_f / tasks.count * 100).round
  end
end
```
# 進捗度計算機能をビューに追加
```Ruby:index.html.erb
<h1>タスク一覧</h1>

<%= link_to '新規タスク作成', new_task_path %>

<h2>全体の進捗度</h2>
<p><%= number_to_percentage(@overall_progress, precision: 0) %></p>

<% @category_progress.each do |category_name, progress| %>
    <h2><%= category_name %></h2>
    <p>カテゴリー進捗度: <%= number_to_percentage(progress, precision: 0) %></p>
    <ul>
        <% @tasks.select { |task| task.category.name == category_name }.each do |task| %>
            <li>
                <%= task.title %> - 
                進捗度: <%= number_to_percentage(task.progress, precision: 0) %> - 
                <%= task.completed ? '完了' : '未完了' %> - 
                期限: <%= task.due_date %>
                <%= link_to '編集', edit_task_path(task) %>
                <%= button_to '削除', task_path(task), method: :delete, data: { confirm: '本当に削除しますか？' } %>
            </li>
        <% end %>
    </ul>
<% end %>
```
#### これによりタスク管理アプリに全体/各カテゴリ の進捗度が追加されました
・タスクを追加し、編集からチェックボックスにチェックをつけると、indexの進捗度が変化する
・各カテゴリの進捗度が増減することで、全体の進捗度も自動的に更新される

次は、この真直度表示を円形プログレスバーで表示する仕様にするため、デザインを施していきます。
※なお、円形プログレスバーでの表示にはjsを使用しますが、記述がうまくできずhtmlファイルにjsの記述をしています。

# 円形プログレスバーの実装
```Ruby:index.html.erb
<h1>タスク一覧</h1>
<%= link_to '新規タスク作成', new_task_path %>
<div class="pro_all">
    <h2>全体の進捗度</h2>
    <p><%= number_to_percentage(@overall_progress, precision: 0) %></p>
    <svg class="progress-circle" width="100" height="100" data-progress="<%= @overall_progress %>">
        <circle class="background" cx="50" cy="50" r="40"></circle>
        <circle class="progress" cx="50" cy="50" r="40"></circle>
    </svg>
</div>

<div class="pro_contents">
    <% @category_progress.each do |category_name, progress| %>
        <div class="pro_content">
            <h2><%= category_name %></h2>
            <p>カテゴリー進捗度: <%= number_to_percentage(progress, precision: 0) %></p>
            <svg class="progress-circle" width="100" height="100" data-progress="<%= progress %>">
                <circle class="background" cx="50" cy="50" r="40"></circle>
                <circle class="progress" cx="50" cy="50" r="40"></circle>
            </svg>
            <br>
            <%= link_to 'タスクを表示', new_task_path %>
        </div>
    <% end %>
</div>

<script>
    function updateProgressCircles() {
        document.querySelectorAll(".progress-circle").forEach(circle => {
            let progress = circle.dataset.progress;
            let progressCircle = circle.querySelector(".progress");
            let radius = progressCircle.r.baseVal.value;
            let circumference = 2 * Math.PI * radius;
            
            progressCircle.style.strokeDasharray = circumference;
            progressCircle.style.strokeDashoffset = circumference * (1 - progress / 100);
        });
    }

    // Turbo Drive (TurboLinks) に対応
    document.addEventListener("turbo:load", updateProgressCircles);
    document.addEventListener("DOMContentLoaded", updateProgressCircles);
</script>


<style>
.progress-circle {
    transform: rotate(-90deg);
}

circle {
    fill: none;
    stroke-width: 8;
}

.background {
    stroke: #ddd;
}

.progress {
    stroke: #4CAF50;
    stroke-linecap: round;
    transition: stroke-dashoffset 1s ease-in-out;
}
</style>
```
#### これにより円形プログレスバーで%表示が行えるようになりました。
続いては、カレンダー機能を導入し、カレンダーを用いたタスク管理アプリに仕上げていきます。
なお、カレンダー機能は https://qiita.com/isaatsu0131/items/ad1d0a6130fe4fd339d0 こちらを参考に作成していきます。

# カレンダー機能を導入するためのGemを追加
```Ruby:gemfile
gem 'simple_calendar', '~> 2.0'
```
その後コマンドプロンプトに
```terminal:コマンドプロンプト
bundle install
```

# コントローラーのindexアクションを修正
```Ruby:tasks.controller
def index
    @tasks = Task.includes(:category).all
    @overall_progress = calculate_progress(@tasks)
    @category_progress = Category.all.map { |category| [category.name, calculate_progress(category.tasks)] }.to_h
end
```

# ビューにカレンダーを追加
かなり長いので、追記する場合はよーく見てね！
```Ruby:index.html.erb
<p id="notice"><%= notice %></p>
<div class="index_h1">
    <h1><span>Task</span>eru!</h1>
</div>

<div class="pro_all">
    <h2>全体の進捗度</h2>
    <p><%= number_to_percentage(@overall_progress, precision: 0) %></p>
    <svg class="progress-circle" width="100" height="100" data-progress="<%= @overall_progress %>">
        <circle class="background" cx="50" cy="50" r="40"></circle>
        <circle class="progress" cx="50" cy="50" r="40"></circle>
    </svg>
</div>

<div class="pro_contents">
    <% @category_progress.each do |category_name, progress| %>
        <div class="pro_content">
            <h2><%= category_name %></h2>
            <p>カテゴリー進捗度: <%= number_to_percentage(progress, precision: 0) %></p>
            <svg class="progress-circle" width="100" height="100" data-progress="<%= progress %>">
                <circle class="background" cx="50" cy="50" r="40"></circle>
                <circle class="progress" cx="50" cy="50" r="40"></circle>
            </svg>
            <br>
            <%= link_to 'タスクを表示', new_task_path %> #こちらは(仮)リンクで現状newに繋がっています
        </div>
    <% end %>
</div>

<script>
    function updateProgressCircles() {
        document.querySelectorAll(".progress-circle").forEach(circle => {
            let progress = circle.dataset.progress;
            let progressCircle = circle.querySelector(".progress");
            let radius = progressCircle.r.baseVal.value;
            let circumference = 2 * Math.PI * radius;
            
            progressCircle.style.strokeDasharray = circumference;
            progressCircle.style.strokeDashoffset = circumference * (1 - progress / 100);
        });
    }

    // Turbo Drive (TurboLinks) に対応
    document.addEventListener("turbo:load", updateProgressCircles);
    document.addEventListener("DOMContentLoaded", updateProgressCircles);
</script>


<style>
.progress-circle {
    transform: rotate(-90deg);
}

circle {
    fill: none;
    stroke-width: 8;
}

.background {
    stroke: #ddd;
}

.progress {
    stroke: #4CAF50;
    stroke-linecap: round;
    transition: stroke-dashoffset 1s ease-in-out;
}
</style>

<div class="index_h1">
    <h1>カレンダー</h1>
    <h2><%= link_to 'タスクを追加', new_task_path %></h2>
</div>


<% require 'ostruct' %>
<%= month_calendar events: @tasks.map { |task| OpenStruct.new(id: task.id, start_time: task.due_date, title: task.title, completed: task.completed) } do |date, tasks| %>
    <%= date.day %>
    <% tasks.each do |task| %>
        <div>
            <%= link_to task.title, edit_task_path(task.id) %> -
            <%= task.completed ? '完了' : '未完了' %>
        </div>
    <% end %>
<% end %>
<br>
<br>
<br>
<br>
```
# カレンダーの見た目を整えるためちょっと手入れ
```terminal:コマンドプロンプト
rails g simple_calendar:views
```
```Ruby:application.scss
/*
 *= require simple_calendar #ここに追記
 *= require_tree .
 *= require_self
 */
```
##### 機能自体は完了ですが、ここでrials s ＆ リロードすると、LoadErrorが発生する場合があるので、こちらで対策します
```terminal:コマンドプロンプト
bundle add sassc

bundle install
```

#### サイトを見てみましょう！実際にサイトを使用してみながら、細かい点は修正してみてね！

※おまけ
軽くcssを施して、円形プログレスバーの配置を整える
```Ruby:css
.progress-bar {
    width: 100%;
    background-color: #f0f0f0;
    border-radius: 10px;
}

.progress {
    width: 50%; /* 進捗率に応じて変更 */
    background-color: #3498db;
    height: 20px;
    border-radius: 10px;
}

h1 span{
    color: red;
}

.index_h1 {
    text-align: center;
}

.pro_all{
    text-align: center;
}

.pro_contents{
    display: flex;
    justify-content: center;
}

.pro_content{
    margin: 0px 40px;
    text-align: center;
}
```
#### 無事に実装できたでしょうか？ぜひいろいろアレンジして自分なりのアプリにしてみてください！！

# おわりに
がんばってみてね～^^