プロジェクト

全般

プロフィール

機能 #195

未完了

チケット詳細画面 - 親課題・子課題一覧表示機能追加

Redmine Admin さんが4日前に追加.

ステータス:
新規
優先度:
高め
担当者:
開始日:
2025-06-04
期日:
進捗率:

0%

予定工数:

説明

機能要望概要

対象画面: チケット詳細画面(例: https://task.call2arm.com/redmine-ui/tickets/192)
設置箇所: 添付ファイル欄の下部
要望内容: 親課題・子課題一覧の表示機能追加

機能仕様

1. 設置場所

位置: 添付ファイル欄(「ファイルを添付」ボタン)の下部
表示条件:

  • 親課題が存在する場合:親課題情報を表示
  • 子課題が存在する場合:子課題一覧を表示
  • 両方ない場合:セクション自体を非表示

2. 親課題表示部分

A. 親課題情報セクション

<div className="parent-ticket-section bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
  <div className="section-header flex items-center gap-2 mb-3">
    <span className="text-blue-600">⬆️</span>
    <h3 className="text-lg font-semibold text-blue-800">親課題</h3>
  </div>
  
  <div className="parent-ticket-info">
    <div className="flex items-start gap-3">
      <div className="ticket-id">
        <span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm font-mono">
          #{parentTicket.id}
        </span>
      </div>
      
      <div className="ticket-details flex-1">
        <div className="ticket-title mb-2">
          <a 
            href={`/redmine-ui/tickets/${parentTicket.id}`}
            className="text-blue-700 hover:text-blue-900 font-medium text-base hover:underline"
          >
            {parentTicket.subject}
          </a>
        </div>
        
        <div className="ticket-meta flex flex-wrap gap-4 text-sm text-gray-600">
          <span className="flex items-center gap-1">
            <span className="w-2 h-2 rounded-full bg-gray-400"></span>
            {parentTicket.status.name}
          </span>
          <span className="flex items-center gap-1">
            👤 {parentTicket.assigned_to?.name || '未割当て'}
          </span>
          <span className="flex items-center gap-1">
            📊 {parentTicket.done_ratio}%
          </span>
          <span className="flex items-center gap-1">
            📅 {formatDate(parentTicket.due_date)}
          </span>
        </div>
      </div>
      
      <div className="ticket-actions">
        <button 
          className="text-blue-600 hover:text-blue-800 p-1"
          title="親課題を表示"
          onClick={() => navigateToTicket(parentTicket.id)}
        >
          🔗
        </button>
      </div>
    </div>
  </div>
</div>

3. 子課題一覧表示部分

A. 子課題セクション

<div className="child-tickets-section bg-green-50 border border-green-200 rounded-lg p-4">
  <div className="section-header flex items-center justify-between mb-4">
    <div className="flex items-center gap-2">
      <span className="text-green-600">⬇️</span>
      <h3 className="text-lg font-semibold text-green-800">
        子課題 ({childTickets.length}件)
      </h3>
    </div>
    
    <div className="header-actions flex items-center gap-2">
      {/* 進捗概要 */}
      <div className="progress-summary text-sm text-gray-600">
        完了: {completedChildTickets}/{childTickets.length}</div>
      
      {/* 新規子課題作成ボタン (チケット#194の機能と連携) */}
      <button 
        className="btn btn-sm btn-success flex items-center gap-1"
        onClick={openChildTicketWizard}
      >
        ➕ 子課題を作成
      </button>
    </div>
  </div>
  
  {/* 子課題一覧テーブル */}
  <div className="child-tickets-list">
    {childTickets.length > 0 ? (
      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead>
            <tr className="border-b border-green-200 bg-green-100">
              <th className="text-left py-2 px-3 font-medium">ID</th>
              <th className="text-left py-2 px-3 font-medium">件名</th>
              <th className="text-left py-2 px-3 font-medium">ステータス</th>
              <th className="text-left py-2 px-3 font-medium">担当者</th>
              <th className="text-left py-2 px-3 font-medium">進捗</th>
              <th className="text-left py-2 px-3 font-medium">期限</th>
              <th className="text-left py-2 px-3 font-medium">操作</th>
            </tr>
          </thead>
          <tbody>
            {childTickets.map((child, index) => (
              <tr 
                key={child.id} 
                className={`border-b border-green-100 hover:bg-green-25 ${
                  child.status.is_closed ? 'opacity-60' : ''
                }`}
              >
                <td className="py-2 px-3">
                  <span className="font-mono text-xs bg-gray-100 px-2 py-1 rounded">
                    #{child.id}
                  </span>
                </td>
                
                <td className="py-2 px-3">
                  <a 
                    href={`/redmine-ui/tickets/${child.id}`}
                    className="text-blue-600 hover:text-blue-800 hover:underline"
                  >
                    {child.subject}
                  </a>
                  {child.status.is_closed && (
                    <span className="ml-2 text-green-600"></span>
                  )}
                </td>
                
                <td className="py-2 px-3">
                  <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs ${
                    child.status.is_closed 
                      ? 'bg-green-100 text-green-800' 
                      : 'bg-yellow-100 text-yellow-800'
                  }`}>
                    {child.status.name}
                  </span>
                </td>
                
                <td className="py-2 px-3">
                  <div className="flex items-center gap-1">
                    {child.assigned_to ? (
                      <>
                        <div className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center text-xs">
                          {child.assigned_to.name.charAt(0)}
                        </div>
                        <span className="text-xs">{child.assigned_to.name}</span>
                      </>
                    ) : (
                      <span className="text-gray-400 text-xs">未割当て</span>
                    )}
                  </div>
                </td>
                
                <td className="py-2 px-3">
                  <div className="flex items-center gap-2">
                    <div className="w-16 bg-gray-200 rounded-full h-2">
                      <div 
                        className="bg-green-500 h-2 rounded-full transition-all"
                        style={{ width: `${child.done_ratio}%` }}
                      ></div>
                    </div>
                    <span className="text-xs text-gray-600">{child.done_ratio}%</span>
                  </div>
                </td>
                
                <td className="py-2 px-3">
                  {child.due_date ? (
                    <span className={`text-xs ${
                      isOverdue(child.due_date) 
                        ? 'text-red-600 font-medium' 
                        : 'text-gray-600'
                    }`}>
                      {formatDate(child.due_date)}
                      {isOverdue(child.due_date) && ' ⚠️'}
                    </span>
                  ) : (
                    <span className="text-gray-400 text-xs">-</span>
                  )}
                </td>
                
                <td className="py-2 px-3">
                  <div className="flex items-center gap-1">
                    <button 
                      className="text-blue-500 hover:text-blue-700 p-1"
                      title="表示"
                      onClick={() => navigateToTicket(child.id)}
                    >
                      👁️
                    </button>
                    <button 
                      className="text-gray-500 hover:text-gray-700 p-1"
                      title="編集"
                      onClick={() => editTicket(child.id)}
                    >
                      ✏️
                    </button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    ) : (
      <div className="text-center py-6 text-gray-500">
        <div className="mb-2">📝</div>
        <p>子課題はまだありません</p>
        <button 
          className="mt-2 text-blue-600 hover:text-blue-800 text-sm"
          onClick={openChildTicketWizard}
        >
          最初の子課題を作成する
        </button>
      </div>
    )}
  </div>
  
  {/* 全体進捗サマリー */}
  {childTickets.length > 0 && (
    <div className="progress-summary mt-4 p-3 bg-white rounded border">
      <div className="flex items-center justify-between text-sm">
        <span className="font-medium">全体進捗</span>
        <span className="text-gray-600">
          {completedChildTickets}/{childTickets.length} 完了 
          ({Math.round((completedChildTickets / childTickets.length) * 100)}%)
        </span>
      </div>
      <div className="w-full bg-gray-200 rounded-full h-3 mt-2">
        <div 
          className="bg-green-500 h-3 rounded-full transition-all"
          style={{ 
            width: `${(completedChildTickets / childTickets.length) * 100}%` 
          }}
        ></div>
      </div>
    </div>
  )}
</div>

4. API連携仕様

A. データ取得API

// 親課題・子課題情報取得
const fetchTicketRelations = async (ticketId) => {
  try {
    const response = await axios.get(`/api/issues/${ticketId}/relations`, {
      headers: {
        'X-Redmine-API-Key': apiKey
      }
    });
    
    return {
      parentTicket: response.data.parent_issue,
      childTickets: response.data.children || []
    };
  } catch (error) {
    console.error('Failed to fetch ticket relations:', error);
    return { parentTicket: null, childTickets: [] };
  }
};

// 子課題進捗計算
const calculateChildProgress = (childTickets) => {
  if (!childTickets.length) return { completed: 0, total: 0, percentage: 0 };
  
  const completed = childTickets.filter(ticket => ticket.status.is_closed).length;
  const total = childTickets.length;
  const percentage = Math.round((completed / total) * 100);
  
  return { completed, total, percentage };
};

B. コンポーネント統合

const TicketRelationsSection = ({ ticketId }) => {
  const [relations, setRelations] = useState({ parentTicket: null, childTickets: [] });
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const loadRelations = async () => {
      setLoading(true);
      const data = await fetchTicketRelations(ticketId);
      setRelations(data);
      setLoading(false);
    };
    
    loadRelations();
  }, [ticketId]);
  
  if (loading) {
    return (
      <div className="relations-loading p-4">
        <div className="animate-pulse">関連課題を読み込み中...</div>
      </div>
    );
  }
  
  const hasParent = relations.parentTicket;
  const hasChildren = relations.childTickets.length > 0;
  
  if (!hasParent && !hasChildren) {
    return null; // 親子関係がない場合は表示しない
  }
  
  return (
    <div className="ticket-relations-section mt-6">
      {hasParent && <ParentTicketSection parentTicket={relations.parentTicket} />}
      {hasChildren && <ChildTicketsSection childTickets={relations.childTickets} />}
    </div>
  );
};

5. レスポンシブ対応

/* モバイル対応 */
@media (max-width: 768px) {
  .child-tickets-list table {
    font-size: 0.75rem;
  }
  
  .child-tickets-list th,
  .child-tickets-list td {
    padding: 0.5rem 0.25rem;
  }
  
  .ticket-meta {
    flex-direction: column;
    gap: 0.25rem;
  }
  
  .progress-summary {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }
}

6. 期待効果

  • 階層構造の可視化: 親子課題の関係性を明確表示
  • 進捗管理の改善: 子課題の完了状況を一覧で把握
  • ナビゲーション向上: 関連課題への素早いアクセス
  • プロジェクト管理効率化: 課題分解の全体像把握

7. 関連チケット

  • #194: AI支援による子課題自動作成機能(連携機能)
  • 「子課題を作成」ボタンからのシームレスな遷移

8. 優先度

  • 緊急度: 中(UI改善・情報表示)
  • 重要度: 高(プロジェクト管理の中核機能)

表示するデータがありません

他の形式にエクスポート: Atom PDF