issueboard-panel.tsx 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. 'use client';
  2. import type { PBLIssueboard, PBLIssue } from '@/lib/pbl/types';
  3. import { useI18n } from '@/lib/hooks/use-i18n';
  4. interface IssueboardPanelProps {
  5. readonly issueboard: PBLIssueboard;
  6. }
  7. export function IssueboardPanel({ issueboard }: IssueboardPanelProps) {
  8. const { t } = useI18n();
  9. const sortedIssues = [...issueboard.issues].sort((a, b) => a.index - b.index);
  10. const doneCount = sortedIssues.filter((i) => i.is_done).length;
  11. const totalCount = sortedIssues.length;
  12. const progressPercent = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
  13. return (
  14. <div className="flex flex-col h-full">
  15. {/* Header */}
  16. <div className="px-4 py-3 border-b">
  17. <h2 className="font-semibold text-sm">{t('pbl.issueboard.title')}</h2>
  18. <div className="mt-2 flex items-center gap-2">
  19. <div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
  20. <div
  21. className="h-full bg-primary rounded-full transition-all duration-500"
  22. style={{ width: `${progressPercent}%` }}
  23. />
  24. </div>
  25. <span className="text-xs text-muted-foreground whitespace-nowrap">
  26. {doneCount}/{totalCount}
  27. </span>
  28. </div>
  29. </div>
  30. {/* Issue List */}
  31. <div className="flex-1 overflow-y-auto p-3 space-y-2">
  32. {sortedIssues.map((issue) => (
  33. <IssueCard key={issue.id} issue={issue} />
  34. ))}
  35. {sortedIssues.length === 0 && (
  36. <p className="text-sm text-muted-foreground text-center py-4">
  37. {t('pbl.issueboard.noIssues')}
  38. </p>
  39. )}
  40. </div>
  41. </div>
  42. );
  43. }
  44. function IssueCard({ issue }: { issue: PBLIssue }) {
  45. const { t } = useI18n();
  46. const statusColor = issue.is_done
  47. ? 'border-green-500/50 bg-green-50 dark:bg-green-950/20'
  48. : issue.is_active
  49. ? 'border-primary bg-primary/5'
  50. : 'border-muted';
  51. const statusLabel = issue.is_done
  52. ? t('pbl.issueboard.statusDone')
  53. : issue.is_active
  54. ? t('pbl.issueboard.statusActive')
  55. : t('pbl.issueboard.statusPending');
  56. const statusBadgeColor = issue.is_done
  57. ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
  58. : issue.is_active
  59. ? 'bg-primary/10 text-primary'
  60. : 'bg-muted text-muted-foreground';
  61. return (
  62. <div className={`border rounded-lg p-3 ${statusColor} transition-colors`}>
  63. <div className="flex items-start justify-between gap-2">
  64. <h3 className="font-medium text-sm leading-tight">{issue.title}</h3>
  65. <span
  66. className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full whitespace-nowrap ${statusBadgeColor}`}
  67. >
  68. {statusLabel}
  69. </span>
  70. </div>
  71. <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{issue.description}</p>
  72. <div className="text-[10px] text-muted-foreground mt-1.5">{issue.person_in_charge}</div>
  73. </div>
  74. );
  75. }