操作
機能 #195
未完了チケット詳細画面 - 親課題・子課題一覧表示機能追加
開始日:
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改善・情報表示)
- 重要度: 高(プロジェクト管理の中核機能)
表示するデータがありません
操作