Files
Nattanin ef20839f99 feat(rfa): complete RFA Approval Refactor - all 9 phases (T001-T080)
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
2026-05-12 16:17:27 +07:00

146 lines
4.1 KiB
TypeScript

'use client';
// File: components/review-team/ReviewTeamForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { X } from 'lucide-react';
import { useState } from 'react';
const reviewTeamSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
description: z.string().max(255).optional(),
defaultForRfaTypes: z.array(z.string()).optional(),
});
type ReviewTeamFormValues = z.infer<typeof reviewTeamSchema>;
interface ReviewTeamFormProps {
projectPublicId: string;
defaultValues?: Partial<ReviewTeamFormValues>;
onSubmit: (values: ReviewTeamFormValues & { projectPublicId: string }) => void;
isLoading?: boolean;
}
const RFA_TYPE_OPTIONS = ['SDW', 'DDW', 'ADW', 'MS', 'MAT', 'BOQ'];
export function ReviewTeamForm({
projectPublicId,
defaultValues,
onSubmit,
isLoading,
}: ReviewTeamFormProps) {
const [typeInput, setTypeInput] = useState('');
const form = useForm<ReviewTeamFormValues>({
resolver: zodResolver(reviewTeamSchema),
defaultValues: {
name: defaultValues?.name ?? '',
description: defaultValues?.description ?? '',
defaultForRfaTypes: defaultValues?.defaultForRfaTypes ?? [],
},
});
const rfaTypes = form.watch('defaultForRfaTypes') ?? [];
const addRfaType = (type: string) => {
if (type && !rfaTypes.includes(type)) {
form.setValue('defaultForRfaTypes', [...rfaTypes, type]);
}
setTypeInput('');
};
const removeRfaType = (type: string) => {
form.setValue(
'defaultForRfaTypes',
rfaTypes.filter((t) => t !== type),
);
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) =>
onSubmit({ ...values, projectPublicId }),
)}
className="space-y-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Team Name</FormLabel>
<FormControl>
<Input placeholder="e.g. Structural Review Team" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Optional description..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormItem>
<FormLabel>Default for RFA Types</FormLabel>
<div className="flex flex-wrap gap-2 mb-2">
{rfaTypes.map((type) => (
<Badge key={type} variant="secondary" className="gap-1">
{type}
<button
type="button"
onClick={() => removeRfaType(type)}
className="ml-1 hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
{RFA_TYPE_OPTIONS.filter((t) => !rfaTypes.includes(t)).map((type) => (
<button
key={type}
type="button"
onClick={() => addRfaType(type)}
className="text-xs px-2 py-1 rounded border border-dashed hover:bg-accent"
>
+ {type}
</button>
))}
</div>
<input type="hidden" value={typeInput} onChange={(e) => setTypeInput(e.target.value)} />
</FormItem>
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save Team'}
</Button>
</form>
</Form>
);
}