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
This commit is contained in:
Nattanin
2026-05-12 16:17:27 +07:00
parent 3df8707b7f
commit ef20839f99
82 changed files with 7052 additions and 104 deletions
@@ -0,0 +1,163 @@
'use client';
// File: components/response-code/MatrixEditor.tsx
// Visual editor สำหรับ Master Approval Matrix (T064, FR-022)
import React, { useState } from 'react';
import { Check, X, AlertTriangle, Lock } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface MatrixRule {
publicId: string;
responseCode: {
publicId: string;
code: string;
descriptionEn: string;
category: string;
};
isEnabled: boolean;
requiresComments: boolean;
triggersNotification: boolean;
isOverridden: boolean;
isSystem?: boolean;
}
interface MatrixEditorProps {
documentTypeCode: string;
rules: MatrixRule[];
isProjectLevel?: boolean;
onToggleEnabled: (rulePublicId: string, enabled: boolean) => void;
onToggleRequiresComments: (rulePublicId: string, value: boolean) => void;
onToggleNotification: (rulePublicId: string, value: boolean) => void;
isLoading?: boolean;
}
const CATEGORY_ORDER = ['ENGINEERING', 'MATERIAL', 'CONTRACT', 'TESTING', 'ESG'];
export function MatrixEditor({
documentTypeCode,
rules,
isProjectLevel = false,
onToggleEnabled,
onToggleRequiresComments,
onToggleNotification,
isLoading,
}: MatrixEditorProps) {
const [filter, setFilter] = useState<string>('ALL');
const grouped = CATEGORY_ORDER.reduce<Record<string, MatrixRule[]>>((acc, cat) => {
const catRules = rules.filter(
(r) => r.responseCode.category === cat && (filter === 'ALL' || r.responseCode.category === filter),
);
if (catRules.length > 0) acc[cat] = catRules;
return acc;
}, {});
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
Matrix: {documentTypeCode}
{isProjectLevel && (
<Badge variant="secondary" className="ml-2 text-xs">Project Override</Badge>
)}
</CardTitle>
<select
value={filter}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFilter(e.target.value)}
className="text-sm border rounded px-2 py-1"
>
<option value="ALL">All Categories</option>
{CATEGORY_ORDER.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
</CardHeader>
<CardContent className="pt-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16">Code</TableHead>
<TableHead>Description</TableHead>
<TableHead className="w-24 text-center">Enabled</TableHead>
<TableHead className="w-28 text-center">Req. Comments</TableHead>
<TableHead className="w-28 text-center">Notify</TableHead>
<TableHead className="w-20 text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Object.entries(grouped).map(([cat, catRules]) => (
<React.Fragment key={cat}>
<TableRow className="bg-muted/30">
<TableCell colSpan={6} className="py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{cat}
</TableCell>
</TableRow>
{catRules.map((rule) => (
<TableRow key={rule.publicId} className={!rule.isEnabled ? 'opacity-50' : ''}>
<TableCell>
<span className="font-mono text-sm font-bold">{rule.responseCode.code}</span>
</TableCell>
<TableCell className="text-sm">{rule.responseCode.descriptionEn}</TableCell>
<TableCell className="text-center">
{rule.isSystem ? (
<Lock className="h-4 w-4 mx-auto text-muted-foreground" />
) : (
<Switch
checked={rule.isEnabled}
onCheckedChange={(v: boolean) => onToggleEnabled(rule.publicId, v)}
disabled={isLoading}
/>
)}
</TableCell>
<TableCell className="text-center">
<Switch
checked={rule.requiresComments}
onCheckedChange={(v: boolean) => onToggleRequiresComments(rule.publicId, v)}
disabled={isLoading || !rule.isEnabled}
/>
</TableCell>
<TableCell className="text-center">
<Switch
checked={rule.triggersNotification}
onCheckedChange={(v: boolean) => onToggleNotification(rule.publicId, v)}
disabled={isLoading || !rule.isEnabled}
/>
</TableCell>
<TableCell className="text-center">
{rule.isOverridden ? (
<AlertTriangle className="h-4 w-4 mx-auto text-amber-500" />
) : rule.isEnabled ? (
<Check className="h-4 w-4 mx-auto text-green-500" />
) : (
<X className="h-4 w-4 mx-auto text-muted-foreground" />
)}
</TableCell>
</TableRow>
))}
</React.Fragment>
))}
{Object.keys(grouped).length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-6">
No rules configured for this document type.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
);
}