ef20839f99
Phase 1-2: Setup, SQL schema, enums, queue constants, base entities
Phase 3 (US1): ReviewTeam, ReviewTeamMember, ReviewTask, TaskCreationService
Phase 4 (US2): ResponseCode, ResponseCodeRule, ImplicationsService, NotificationTriggerService
Phase 5 (US3): Delegation entity, CircularDetectionService, DelegationService/Controller/Module
Phase 6 (US4): ReminderRule, SchedulerService, EscalationService, ReminderProcessor, ReminderModule
Phase 7 (US5): DistributionMatrix, DistributionRecipient, ApprovalListenerService (Strangler),
TransmittalCreatorService, DistributionProcessor, DistributionModule
Phase 8 (US6): MatrixManagementService, InheritanceService (global→project override)
Phase 9 (Polish): AggregateStatusService, ConsensusService, VetoOverrideService,
ParallelGatewayHandler, review-validators, optimistic locking in completeReview,
test stubs (unit/integration/e2e), jest.config.js updated for tests/ directory
Frontend: ReviewTaskInbox, ParallelProgress, VetoOverrideDialog, DelegationForm,
DelegatedBadge, MatrixEditor, ProjectOverrideManager, DistributionStatus,
ReminderHistory, ResponseCodeSelector, CodeImplications, CompleteReviewForm,
ReviewTeamForm, ReviewTeamSelector, TeamMemberManager
Closes #1
168 lines
5.9 KiB
TypeScript
168 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
// File: app/(dashboard)/settings/review-teams/page.tsx
|
|
// หน้าจัดการ Review Teams (FR-001, FR-002)
|
|
import { useState } from 'react';
|
|
import { Plus, Users, ChevronDown, ChevronUp } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { useReviewTeams, useCreateReviewTeam, useUpdateReviewTeam } from '@/hooks/use-review-teams';
|
|
import { ReviewTeamForm } from '@/components/review-team/ReviewTeamForm';
|
|
import { TeamMemberManager } from '@/components/review-team/TeamMemberManager';
|
|
import { ReviewTeam } from '@/types/review-team';
|
|
|
|
// TODO: ดึง projectPublicId จาก context หรือ URL param จริง
|
|
const MOCK_PROJECT_ID = 'current-project-public-id';
|
|
|
|
export default function ReviewTeamsPage() {
|
|
const [expandedTeam, setExpandedTeam] = useState<string | null>(null);
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [editTeam, setEditTeam] = useState<ReviewTeam | null>(null);
|
|
|
|
const { data: teams = [], isLoading } = useReviewTeams({
|
|
projectPublicId: MOCK_PROJECT_ID,
|
|
});
|
|
|
|
const createTeam = useCreateReviewTeam();
|
|
const updateTeam = useUpdateReviewTeam();
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Review Teams</h1>
|
|
<p className="text-muted-foreground text-sm mt-1">
|
|
จัดการทีมตรวจสอบแยกตาม Discipline สำหรับ Parallel Review
|
|
</p>
|
|
</div>
|
|
|
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Team
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create Review Team</DialogTitle>
|
|
</DialogHeader>
|
|
<ReviewTeamForm
|
|
projectPublicId={MOCK_PROJECT_ID}
|
|
onSubmit={(values) =>
|
|
createTeam.mutate(values, {
|
|
onSuccess: () => setCreateOpen(false),
|
|
})
|
|
}
|
|
isLoading={createTeam.isPending}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{isLoading && (
|
|
<div className="text-center text-muted-foreground py-8">Loading teams...</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
{(teams as ReviewTeam[]).map((team) => (
|
|
<Card key={team.publicId}>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Users className="h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">{team.name}</CardTitle>
|
|
{!team.isActive && (
|
|
<Badge variant="secondary">Inactive</Badge>
|
|
)}
|
|
{(team.defaultForRfaTypes ?? []).map((type) => (
|
|
<Badge key={type} variant="outline" className="text-xs">
|
|
{type}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setEditTeam(team)}
|
|
>
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() =>
|
|
setExpandedTeam(expandedTeam === team.publicId ? null : team.publicId)
|
|
}
|
|
>
|
|
{expandedTeam === team.publicId ? (
|
|
<ChevronUp className="h-4 w-4" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{team.description && (
|
|
<p className="text-xs text-muted-foreground mt-1">{team.description}</p>
|
|
)}
|
|
</CardHeader>
|
|
|
|
{expandedTeam === team.publicId && (
|
|
<CardContent>
|
|
<div className="text-sm font-medium mb-3">
|
|
Members ({(team.members ?? []).length})
|
|
</div>
|
|
<TeamMemberManager
|
|
teamPublicId={team.publicId}
|
|
members={team.members ?? []}
|
|
availableUsers={[]}
|
|
availableDisciplines={[]}
|
|
/>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
))}
|
|
|
|
{!isLoading && (teams as ReviewTeam[]).length === 0 && (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<Users className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
|
<p>No Review Teams yet. Create one to enable Parallel Review.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Edit Dialog */}
|
|
<Dialog open={!!editTeam} onOpenChange={() => setEditTeam(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Review Team</DialogTitle>
|
|
</DialogHeader>
|
|
{editTeam && (
|
|
<ReviewTeamForm
|
|
projectPublicId={MOCK_PROJECT_ID}
|
|
defaultValues={editTeam}
|
|
onSubmit={(values) =>
|
|
updateTeam.mutate(
|
|
{ publicId: editTeam.publicId, data: values },
|
|
{ onSuccess: () => setEditTeam(null) },
|
|
)
|
|
}
|
|
isLoading={updateTeam.isPending}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|