feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
This commit is contained in:
@@ -8,12 +8,13 @@ import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { CheckCircle2, RefreshCcw } from 'lucide-react';
|
||||
import { CheckCircle2, RefreshCcw, BarChart3, AlertTriangle } from 'lucide-react';
|
||||
import {
|
||||
AiStagingRecord,
|
||||
AiStagingStatus,
|
||||
useAiStagingQueue,
|
||||
useApproveAiStagingRecord,
|
||||
useAiAnalyticsSummary,
|
||||
} from '@/lib/api/ai';
|
||||
import { projectService } from '@/lib/services/project.service';
|
||||
import { masterDataService } from '@/lib/services/master-data.service';
|
||||
@@ -47,6 +48,19 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/ui/tabs';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useTranslations } from '@/hooks/use-translations';
|
||||
|
||||
interface ProjectOption {
|
||||
@@ -102,10 +116,12 @@ function getStatusVariant(
|
||||
|
||||
export default function AiStagingPage() {
|
||||
const t = useTranslations();
|
||||
const [activeTab, setActiveTab] = useState('queue');
|
||||
const [selectedRecord, setSelectedRecord] = useState<AiStagingRecord | null>(
|
||||
null
|
||||
);
|
||||
const queueQuery = useAiStagingQueue();
|
||||
const analyticsQuery = useAiAnalyticsSummary();
|
||||
const approveMutation = useApproveAiStagingRecord();
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: ['ai-staging', 'projects'],
|
||||
@@ -202,8 +218,11 @@ export default function AiStagingPage() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => void queueQuery.refetch()}
|
||||
disabled={queueQuery.isFetching}
|
||||
onClick={() => {
|
||||
if (activeTab === 'queue') queueQuery.refetch();
|
||||
else analyticsQuery.refetch();
|
||||
}}
|
||||
disabled={queueQuery.isFetching || analyticsQuery.isFetching}
|
||||
>
|
||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||
{t('ai.staging.refresh')}
|
||||
@@ -212,64 +231,246 @@ export default function AiStagingPage() {
|
||||
|
||||
<AiStatusBanner isOffline={queueQuery.isError} />
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('ai.staging.file')}</TableHead>
|
||||
<TableHead>{t('ai.staging.batch')}</TableHead>
|
||||
<TableHead>{t('ai.staging.confidence')}</TableHead>
|
||||
<TableHead>{t('ai.staging.status')}</TableHead>
|
||||
<TableHead className="w-[120px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{records.map((record) => (
|
||||
<TableRow key={record.publicId}>
|
||||
<TableCell className="font-medium">
|
||||
{record.originalFileName}
|
||||
{record.errorReason ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{record.errorReason}
|
||||
</p>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>{record.batchId}</TableCell>
|
||||
<TableCell>
|
||||
{record.confidenceScore === undefined
|
||||
? t('ai.staging.empty')
|
||||
: `${Math.round(record.confidenceScore * 100)}%`}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getStatusVariant(record.status)}>
|
||||
{record.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={record.status !== AiStagingStatus.PENDING}
|
||||
onClick={() => openApprovalDialog(record)}
|
||||
>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
{t('ai.staging.review')}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{records.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
|
||||
{queueQuery.isLoading
|
||||
? t('ai.staging.loading')
|
||||
: t('ai.staging.emptyQueue')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="queue">{t('ai.staging.queueTab')}</TabsTrigger>
|
||||
<TabsTrigger value="analytics">
|
||||
<BarChart3 className="mr-2 h-4 w-4" />
|
||||
{t('ai.staging.analyticsTab')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="queue">
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('ai.staging.file')}</TableHead>
|
||||
<TableHead>{t('ai.staging.batch')}</TableHead>
|
||||
<TableHead>{t('ai.staging.confidence')}</TableHead>
|
||||
<TableHead>{t('ai.staging.status')}</TableHead>
|
||||
<TableHead className="w-[120px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{records.map((record) => (
|
||||
<TableRow key={record.publicId}>
|
||||
<TableCell className="font-medium">
|
||||
{record.originalFileName}
|
||||
{record.errorReason ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{record.errorReason}
|
||||
</p>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>{record.batchId}</TableCell>
|
||||
<TableCell>
|
||||
{record.confidenceScore === undefined
|
||||
? t('ai.staging.empty')
|
||||
: `${Math.round(record.confidenceScore * 100)}%`}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getStatusVariant(record.status)}>
|
||||
{record.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={record.status !== AiStagingStatus.PENDING}
|
||||
onClick={() => openApprovalDialog(record)}
|
||||
>
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
{t('ai.staging.review')}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{records.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
|
||||
{queueQuery.isLoading
|
||||
? t('ai.staging.loading')
|
||||
: t('ai.staging.emptyQueue')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics">
|
||||
<div className="space-y-6">
|
||||
{/* Phase 6 T038: AI Analytics Summary */}
|
||||
{analyticsQuery.isLoading ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
{t('ai.staging.loading')}
|
||||
</div>
|
||||
) : analyticsQuery.error ? (
|
||||
<div className="text-center text-destructive">
|
||||
{t('ai.staging.analyticsError')}
|
||||
</div>
|
||||
) : analyticsQuery.data ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t('ai.staging.avgConfidence')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{Math.round(analyticsQuery.data.overall.avgConfidence * 100)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t('ai.staging.overrideRate')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsQuery.data.overall.overrideRate.toFixed(1)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{t('ai.staging.rejectedRate')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{analyticsQuery.data.overall.rejectedRate.toFixed(1)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('ai.staging.byDocumentType')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('ai.staging.byDocumentTypeDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{analyticsQuery.data.byDocumentType.map((item) => (
|
||||
<div key={item.documentType} className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">{item.documentType}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{item.total} {t('ai.staging.documents')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-xs">
|
||||
<div>
|
||||
<div className="text-muted-foreground">
|
||||
{t('ai.staging.confidence')}
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{Math.round(item.avgConfidence * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">
|
||||
{t('ai.staging.override')}
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{item.overrideRate.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">
|
||||
{t('ai.staging.rejected')}
|
||||
</div>
|
||||
<div className="font-medium">
|
||||
{item.rejectedRate.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all"
|
||||
style={{ width: `${item.avgConfidence * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Phase 6 T039: Threshold Recalibration UI */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
{t('ai.staging.thresholdRecalibration')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('ai.staging.thresholdDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('ai.staging.highThreshold')}</Label>
|
||||
<div className="text-2xl font-bold">
|
||||
{process.env.NEXT_PUBLIC_AI_CONFIDENCE_HIGH || '0.85'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('ai.staging.midThreshold')}</Label>
|
||||
<div className="text-2xl font-bold">
|
||||
{process.env.NEXT_PUBLIC_AI_CONFIDENCE_MID || '0.60'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{analyticsQuery.data.overall.overrideRate > 40 && (
|
||||
<div className="rounded-md bg-yellow-50 border border-yellow-200 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-600 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium text-yellow-800">
|
||||
{t('ai.staging.thresholdWarning')}
|
||||
</div>
|
||||
<div className="text-sm text-yellow-700">
|
||||
{t('ai.staging.thresholdWarningDesc', {
|
||||
rate: analyticsQuery.data.overall.overrideRate.toFixed(1),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('ai.staging.thresholdNote')}
|
||||
<a
|
||||
href="/docs/ai-configuration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline ml-1"
|
||||
>
|
||||
{t('ai.staging.thresholdDocs')}
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog
|
||||
open={selectedRecord !== null}
|
||||
|
||||
@@ -9,21 +9,32 @@ import { useTranslations } from '@/hooks/use-translations';
|
||||
|
||||
interface AiStatusBannerProps {
|
||||
isOffline: boolean;
|
||||
queuePaused?: boolean;
|
||||
}
|
||||
|
||||
export function AiStatusBanner({ isOffline }: AiStatusBannerProps) {
|
||||
export function AiStatusBanner({ isOffline, queuePaused = false }: AiStatusBannerProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
if (isOffline) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>{t('ai.status.offlineTitle')}</AlertTitle>
|
||||
<AlertTitle>{t('ai.service_unavailable')}</AlertTitle>
|
||||
<AlertDescription>{t('ai.status.offlineDescription')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (queuePaused) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>{t('ai.status.queue-paused')}</AlertTitle>
|
||||
<AlertDescription>{t('ai.status.queuePausedDescription')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
// File: components/ai/ai-suggestion-field.tsx
|
||||
// Component แสดง AI Suggestion พร้อม Accept / Reject / Edit actions (ADR-018, ADR-020)
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Check, X, Edit2, Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// สีตาม confidence score (>= 0.95 สีเขียว, >= 0.85 สีเหลือง, < 0.85 สีแดง)
|
||||
// สีตาม confidence score ของ ADR-023A
|
||||
const getConfidenceClass = (confidence: number): string => {
|
||||
if (confidence >= 0.95) return 'text-green-700 bg-green-50 border-green-400';
|
||||
if (confidence >= 0.85) return 'text-yellow-700 bg-yellow-50 border-yellow-400';
|
||||
return 'text-red-700 bg-red-50 border-red-400';
|
||||
if (confidence >= 0.85) return 'text-green-700 bg-green-50 border-green-400';
|
||||
if (confidence >= 0.6) return 'text-yellow-700 bg-yellow-50 border-yellow-400';
|
||||
return 'text-muted-foreground bg-muted border-border';
|
||||
};
|
||||
|
||||
const getConfidenceLabel = (confidence: number): string => {
|
||||
if (confidence >= 0.85) return 'AI แนะนำ';
|
||||
if (confidence >= 0.6) return 'ตรวจสอบก่อนยืนยัน';
|
||||
return '';
|
||||
};
|
||||
|
||||
export interface AiSuggestionFieldProps {
|
||||
@@ -20,6 +28,10 @@ export interface AiSuggestionFieldProps {
|
||||
value: string;
|
||||
suggestion?: string;
|
||||
confidence?: number;
|
||||
isUnknown?: boolean;
|
||||
jobId?: string;
|
||||
onJobCompleted?: (result: unknown) => void;
|
||||
onJobFailed?: () => void;
|
||||
onAccept: () => void;
|
||||
onReject: () => void;
|
||||
onEdit: (newValue: string) => void;
|
||||
@@ -31,6 +43,10 @@ export function AiSuggestionField({
|
||||
value,
|
||||
suggestion,
|
||||
confidence,
|
||||
isUnknown = false,
|
||||
jobId,
|
||||
onJobCompleted,
|
||||
onJobFailed,
|
||||
onAccept,
|
||||
onReject,
|
||||
onEdit,
|
||||
@@ -54,18 +70,50 @@ export function AiSuggestionField({
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobId) return undefined;
|
||||
const interval = window.setInterval(() => {
|
||||
void apiClient
|
||||
.get(`/ai/jobs/${jobId}/status`)
|
||||
.then((response) => {
|
||||
const status = response.data?.status as string | undefined;
|
||||
if (status === 'completed') {
|
||||
window.clearInterval(interval);
|
||||
onJobCompleted?.(response.data?.result);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
window.clearInterval(interval);
|
||||
onJobFailed?.();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.clearInterval(interval);
|
||||
onJobFailed?.();
|
||||
});
|
||||
}, 3000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [jobId, onJobCompleted, onJobFailed]);
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-1', className)}>
|
||||
{/* Label พร้อม Confidence Badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">{label}</label>
|
||||
{hasSuggestion && confidence !== undefined && (
|
||||
{hasSuggestion && confidence !== undefined && getConfidenceLabel(confidence) && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn('text-xs gap-1 px-1.5 py-0', getConfidenceClass(confidence))}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
AI {Math.round(confidence * 100)}%
|
||||
{getConfidenceLabel(confidence)} {Math.round(confidence * 100)}%
|
||||
</Badge>
|
||||
)}
|
||||
{hasSuggestion && isUnknown && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs px-1.5 py-0 text-red-700 bg-red-50 border-red-300"
|
||||
>
|
||||
ไม่รู้จัก
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -156,10 +204,13 @@ export function AiSuggestionField({
|
||||
{/* แสดง AI Suggestion hint เมื่อค่าปัจจุบันต่างจาก AI */}
|
||||
{hasSuggestion && !isAiValue && (
|
||||
<p className="text-xs text-muted-foreground pl-1">
|
||||
AI แนะนำ:{' '}
|
||||
{isUnknown ? 'ไม่รู้จัก — กรุณาเลือกจาก dropdown: ' : 'AI แนะนำ: '}
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-yellow-700 hover:underline"
|
||||
className={cn(
|
||||
'font-medium hover:underline',
|
||||
isUnknown ? 'text-red-700' : 'text-yellow-700'
|
||||
)}
|
||||
onClick={onAccept}
|
||||
>
|
||||
{suggestion}
|
||||
|
||||
@@ -176,3 +176,50 @@ export function useApproveAiStagingRecord() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Phase 6: AI Monitoring & Analytics Hooks (T036, T037) ───────────────────
|
||||
|
||||
export interface AiAnalyticsSummary {
|
||||
byDocumentType: Array<{
|
||||
documentType: string;
|
||||
avgConfidence: number;
|
||||
overrideRate: number;
|
||||
rejectedRate: number;
|
||||
total: number;
|
||||
}>;
|
||||
overall: {
|
||||
avgConfidence: number;
|
||||
overrideRate: number;
|
||||
rejectedRate: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const aiAnalyticsKeys = {
|
||||
all: ['ai-analytics'] as const,
|
||||
summary: () => [...aiAnalyticsKeys.all, 'summary'] as const,
|
||||
};
|
||||
|
||||
export function useAiAnalyticsSummary() {
|
||||
return useQuery({
|
||||
queryKey: aiAnalyticsKeys.summary(),
|
||||
queryFn: async (): Promise<AiAnalyticsSummary> => {
|
||||
const response = await apiClient.get('/ai/analytics/summary');
|
||||
return extractData<AiAnalyticsSummary>(response.data);
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Analytics can be cached longer
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteAiAuditLog() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (publicId: string): Promise<{ deleted: boolean; publicId: string }> => {
|
||||
const response = await apiClient.delete(`/ai/audit-logs/${publicId}`);
|
||||
return extractData<{ deleted: boolean; publicId: string }>(response.data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: aiAnalyticsKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -70,5 +70,25 @@
|
||||
"ai.staging.body": "Body",
|
||||
"ai.staging.approve": "Approve",
|
||||
"ai.staging.approveSuccess": "Staging record approved.",
|
||||
"ai.staging.approveError": "Unable to approve staging record."
|
||||
"ai.staging.approveError": "Unable to approve staging record.",
|
||||
"ai.staging.queueTab": "Work Queue",
|
||||
"ai.staging.analyticsTab": "AI Analytics",
|
||||
"ai.staging.analyticsError": "Unable to load analytics data.",
|
||||
"ai.staging.avgConfidence": "Avg. Confidence",
|
||||
"ai.staging.overrideRate": "Human Override Rate",
|
||||
"ai.staging.rejectedRate": "Rejection Rate",
|
||||
"ai.staging.byDocumentType": "Stats by Document Type",
|
||||
"ai.staging.byDocumentTypeDesc": "Compare AI performance across different document categories.",
|
||||
"ai.staging.documents": "docs",
|
||||
"ai.staging.confidence": "Confidence",
|
||||
"ai.staging.override": "Override",
|
||||
"ai.staging.rejected": "Rejected",
|
||||
"ai.staging.thresholdRecalibration": "Threshold Recalibration",
|
||||
"ai.staging.thresholdDesc": "Verify confidence thresholds for auto-approval vs human review.",
|
||||
"ai.staging.highThreshold": "High Threshold (Auto-approve)",
|
||||
"ai.staging.midThreshold": "Mid Threshold (Human Review)",
|
||||
"ai.staging.thresholdWarning": "Improvement Recommended",
|
||||
"ai.staging.thresholdWarningDesc": "Override rate reached {{rate}}% in recent records.",
|
||||
"ai.staging.thresholdNote": "* Threshold values must be set via Backend Environment Variables.",
|
||||
"ai.staging.thresholdDocs": "View Configuration Guide"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"title": "Task Delegation",
|
||||
"subtitle": "Manage delegation of review tasks to others",
|
||||
|
||||
"list": {
|
||||
"title": "Delegation List",
|
||||
"createButton": "Create Delegation",
|
||||
"noActive": "No active delegations",
|
||||
"historyButton": "Delegation History"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"createTitle": "Create Delegation",
|
||||
"editTitle": "Edit Delegation",
|
||||
"delegateTo": "Delegate To",
|
||||
"delegateToPlaceholder": "Select user...",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date",
|
||||
"scope": "Delegation Scope",
|
||||
"scopeOptions": {
|
||||
"ALL": "All",
|
||||
"DISCIPLINE": "Discipline Only",
|
||||
"PROJECT": "Project Only"
|
||||
},
|
||||
"reason": "Reason",
|
||||
"reasonPlaceholder": "Enter reason for delegation...",
|
||||
"saveButton": "Save",
|
||||
"cancelButton": "Cancel",
|
||||
"revokeButton": "Revoke Delegation"
|
||||
},
|
||||
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"expired": "Expired",
|
||||
"revoked": "Revoked",
|
||||
"upcoming": "Starting Soon"
|
||||
},
|
||||
|
||||
"badge": {
|
||||
"delegatedFrom": "Delegated from: {{name}}",
|
||||
"delegatedTo": "Delegated to: {{name}}",
|
||||
"until": "Until: {{date}}"
|
||||
},
|
||||
|
||||
"notifications": {
|
||||
"createdTitle": "Delegation Received",
|
||||
"createdBody": "{{delegator}} has delegated review tasks to you",
|
||||
"expiringTitle": "Delegation Expiring Soon",
|
||||
"expiringBody": "Delegation will expire in {{days}} days",
|
||||
"expiredTitle": "Delegation Expired",
|
||||
"expiredBody": "Delegation has expired. Tasks returned to delegator"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"delegateToRequired": "Please select delegatee",
|
||||
"startDateRequired": "Start date is required",
|
||||
"endDateRequired": "End date is required",
|
||||
"invalidDateRange": "End date must be after start date",
|
||||
"circularDelegation": "Circular delegation not allowed ({{path}})",
|
||||
"selfDelegation": "Cannot delegate to yourself",
|
||||
"overlapExists": "Overlapping delegation already exists",
|
||||
"loadFailed": "Failed to load data",
|
||||
"saveFailed": "Failed to save delegation",
|
||||
"revokeFailed": "Failed to revoke delegation"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"created": "Delegation created successfully",
|
||||
"updated": "Delegation updated successfully",
|
||||
"revoked": "Delegation revoked successfully"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "Response Codes",
|
||||
"subtitle": "Manage review response codes and implications",
|
||||
|
||||
"list": {
|
||||
"title": "Response Code List",
|
||||
"createButton": "Create New Code",
|
||||
"searchPlaceholder": "Search codes...",
|
||||
"noResults": "No response codes found",
|
||||
"filterByCategory": "Filter by Category"
|
||||
},
|
||||
|
||||
"categories": {
|
||||
"ENGINEERING": "Engineering",
|
||||
"CONTRACT": "Contract",
|
||||
"QUALITY": "Quality",
|
||||
"SAFETY": "Safety",
|
||||
"PROCUREMENT": "Procurement",
|
||||
"GENERAL": "General"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"createTitle": "Create Response Code",
|
||||
"editTitle": "Edit Response Code",
|
||||
"code": "Code",
|
||||
"codePlaceholder": "e.g., 1A, 1B, 2",
|
||||
"subStatus": "Sub-status",
|
||||
"subStatusPlaceholder": "e.g., A, B, C",
|
||||
"category": "Category",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Description of response code",
|
||||
"descriptionThai": "Description (Thai)",
|
||||
"implications": "Implications",
|
||||
"implicationsPlaceholder": "Implications when this code is selected",
|
||||
"notifyRoles": "Notify Roles",
|
||||
"isActive": "Active",
|
||||
"saveButton": "Save",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
|
||||
"selector": {
|
||||
"placeholder": "Select response code...",
|
||||
"filterByDocType": "Filter by Document Type",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
|
||||
"implications": {
|
||||
"title": "Code Implications",
|
||||
"critical": "Critical",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"notificationsSent": "Notifications will be sent to: {{roles}}"
|
||||
},
|
||||
|
||||
"matrix": {
|
||||
"title": "Master Approval Matrix",
|
||||
"subtitle": "Define response codes by document type",
|
||||
"docType": "Document Type",
|
||||
"applicableCodes": "Applicable Codes",
|
||||
"projectOverride": "Project Override",
|
||||
"inheritanceNote": "Inherited from default",
|
||||
"addOverride": "Add Project Override"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"codeRequired": "Code is required",
|
||||
"codeExists": "Code already exists",
|
||||
"categoryRequired": "Please select a category",
|
||||
"invalidCode": "Invalid code format",
|
||||
"loadFailed": "Failed to load codes",
|
||||
"saveFailed": "Failed to save code"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"created": "Code created successfully",
|
||||
"updated": "Code updated successfully",
|
||||
"deleted": "Code deleted successfully"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"title": "Review Tasks",
|
||||
"subtitle": "Manage document review tasks and track status",
|
||||
|
||||
"inbox": {
|
||||
"title": "Review Inbox",
|
||||
"pending": "Pending",
|
||||
"inProgress": "In Progress",
|
||||
"completed": "Completed",
|
||||
"delegated": "Delegated",
|
||||
"noTasks": "No review tasks",
|
||||
"filterAll": "All",
|
||||
"filterByProject": "Filter by Project",
|
||||
"filterByDiscipline": "Filter by Discipline"
|
||||
},
|
||||
|
||||
"status": {
|
||||
"PENDING": "Pending",
|
||||
"IN_PROGRESS": "In Progress",
|
||||
"COMPLETED": "Completed",
|
||||
"DELEGATED": "Delegated",
|
||||
"OVERDUE": "Overdue"
|
||||
},
|
||||
|
||||
"actions": {
|
||||
"start": "Start Review",
|
||||
"complete": "Complete Review",
|
||||
"delegate": "Delegate",
|
||||
"return": "Return",
|
||||
"viewDetails": "View Details",
|
||||
"addComment": "Add Comment",
|
||||
"viewDocument": "View Document"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"completeTitle": "Complete Review",
|
||||
"responseCode": "Response Code",
|
||||
"responseCodePlaceholder": "Select code...",
|
||||
"comments": "Comments",
|
||||
"commentsPlaceholder": "Enter comments...",
|
||||
"attachments": "Attachments",
|
||||
"addAttachment": "Add File",
|
||||
"confirmComplete": "Confirm completion?",
|
||||
"saveButton": "Save",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
|
||||
"progress": {
|
||||
"title": "Review Progress",
|
||||
"completedOf": "{{completed}}/{{total}} Completed",
|
||||
"consensusStatus": "Consensus Status: {{status}}",
|
||||
"waitingFor": "Waiting for: {{disciplines}}",
|
||||
"parallelReview": "Parallel Review",
|
||||
"sequentialReview": "Sequential Review"
|
||||
},
|
||||
|
||||
"veto": {
|
||||
"button": "Veto Override",
|
||||
"title": "Veto Override - Force Approval",
|
||||
"justification": "Justification for Override",
|
||||
"justificationPlaceholder": "Enter justification for forcing approval...",
|
||||
"confirm": "Confirm Veto Override",
|
||||
"warning": "This action will be logged in Audit Log and notify relevant teams",
|
||||
"pmOnly": "Project Manager Only"
|
||||
},
|
||||
|
||||
"consensus": {
|
||||
"APPROVED": "Approved",
|
||||
"APPROVED_WITH_COMMENTS": "Approved with Comments",
|
||||
"REJECTED": "Rejected",
|
||||
"PENDING": "Pending",
|
||||
"VETO_OVERRIDE": "Veto Override",
|
||||
"calculating": "Calculating consensus..."
|
||||
},
|
||||
|
||||
"reminders": {
|
||||
"dueSoon": "Due in {{days}} days",
|
||||
"overdue": "Overdue by {{days}} days",
|
||||
"escalationLevel1": "Escalation Level 1",
|
||||
"escalationLevel2": "Escalation Level 2"
|
||||
},
|
||||
|
||||
"confirmation": {
|
||||
"title": "Confirm Action",
|
||||
"message": "Are you sure you want to proceed with this action?",
|
||||
"confirmButton": "Confirm",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"loadFailed": "Failed to load review tasks",
|
||||
"completeFailed": "Failed to complete review",
|
||||
"responseCodeRequired": "Please select a response code",
|
||||
"raceCondition": "Task status changed, please refresh and try again",
|
||||
"unauthorized": "You are not authorized for this action"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"started": "Review started successfully",
|
||||
"completed": "Review completed successfully",
|
||||
"delegated": "Delegated successfully",
|
||||
"commentAdded": "Comment added successfully"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"title": "Review Team Management",
|
||||
"subtitle": "Create and manage document review teams by discipline",
|
||||
|
||||
"list": {
|
||||
"title": "Review Team List",
|
||||
"createButton": "Create New Team",
|
||||
"searchPlaceholder": "Search teams...",
|
||||
"noResults": "No review teams found",
|
||||
"teamCount": "{{count}} teams",
|
||||
"memberCount": "{{count}} members"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"createTitle": "Create Review Team",
|
||||
"editTitle": "Edit Review Team",
|
||||
"name": "Team Name",
|
||||
"namePlaceholder": "Enter team name",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Enter team description (optional)",
|
||||
"disciplines": "Responsible Disciplines",
|
||||
"disciplinesPlaceholder": "Select disciplines...",
|
||||
"rfaTypes": "RFA Types to Review",
|
||||
"rfaTypesPlaceholder": "Select RFA types...",
|
||||
"isActive": "Active",
|
||||
"isDefault": "Set as Default",
|
||||
"saveButton": "Save",
|
||||
"cancelButton": "Cancel",
|
||||
"deleteButton": "Delete Team",
|
||||
"confirmDelete": "Confirm delete this team?"
|
||||
},
|
||||
|
||||
"members": {
|
||||
"title": "Team Members",
|
||||
"addButton": "Add Member",
|
||||
"removeButton": "Remove from Team",
|
||||
"role": {
|
||||
"LEAD": "Team Lead",
|
||||
"REVIEWER": "Reviewer",
|
||||
"OBSERVER": "Observer"
|
||||
},
|
||||
"noMembers": "No members in this team yet",
|
||||
"searchUserPlaceholder": "Search users..."
|
||||
},
|
||||
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"default": "Default"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"nameRequired": "Team name is required",
|
||||
"nameExists": "Team name already exists",
|
||||
"disciplineRequired": "Please select at least one discipline",
|
||||
"memberRequired": "Please add at least one member",
|
||||
"leadRequired": "Team must have at least one lead",
|
||||
"loadFailed": "Failed to load team data",
|
||||
"saveFailed": "Failed to save team",
|
||||
"deleteFailed": "Failed to delete team"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"created": "Team created successfully",
|
||||
"updated": "Team updated successfully",
|
||||
"deleted": "Team deleted successfully",
|
||||
"memberAdded": "Member added successfully",
|
||||
"memberRemoved": "Member removed successfully"
|
||||
}
|
||||
}
|
||||
@@ -70,5 +70,25 @@
|
||||
"ai.staging.body": "เนื้อหา",
|
||||
"ai.staging.approve": "อนุมัติ",
|
||||
"ai.staging.approveSuccess": "อนุมัติรายการเรียบร้อยแล้ว",
|
||||
"ai.staging.approveError": "ไม่สามารถอนุมัติรายการได้"
|
||||
"ai.staging.approveError": "ไม่สามารถอนุมัติรายการได้",
|
||||
"ai.staging.queueTab": "คิวงาน",
|
||||
"ai.staging.analyticsTab": "AI Analytics",
|
||||
"ai.staging.analyticsError": "ไม่สามารถโหลดข้อมูลสถิติได้",
|
||||
"ai.staging.avgConfidence": "ความมั่นใจเฉลี่ย",
|
||||
"ai.staging.overrideRate": "อัตราการแก้ไขโดยมนุษย์",
|
||||
"ai.staging.rejectedRate": "อัตราการปฏิเสธ",
|
||||
"ai.staging.byDocumentType": "สถิติแยกตามประเภทเอกสาร",
|
||||
"ai.staging.byDocumentTypeDesc": "เปรียบเทียบประสิทธิภาพของ AI ในแต่ละประเภทเอกสาร",
|
||||
"ai.staging.documents": "ฉบับ",
|
||||
"ai.staging.confidence": "ความมั่นใจ",
|
||||
"ai.staging.override": "แก้ไข",
|
||||
"ai.staging.rejected": "ปฏิเสธ",
|
||||
"ai.staging.thresholdRecalibration": "Threshold Recalibration",
|
||||
"ai.staging.thresholdDesc": "ตรวจสอบความเหมาะสมของค่าความมั่นใจ (Confidence Threshold)",
|
||||
"ai.staging.highThreshold": "High Threshold (Auto-approve)",
|
||||
"ai.staging.midThreshold": "Mid Threshold (Human Review)",
|
||||
"ai.staging.thresholdWarning": "ควรปรับปรุง Model หรือ Threshold",
|
||||
"ai.staging.thresholdWarningDesc": "ตรวจพบอัตราการแก้ไขสูงถึง {{rate}}% ในช่วงที่ผ่านมา",
|
||||
"ai.staging.thresholdNote": "* การเปลี่ยนค่า Threshold ต้องทำผ่าน Environment Variables ของ Backend",
|
||||
"ai.staging.thresholdDocs": "อ่านคู่มือการตั้งค่า"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"title": "การมอบหมายงานแทน",
|
||||
"subtitle": "จัดการการมอบหมายงานตรวจสอบให้ผู้อื่นทำแทน",
|
||||
|
||||
"list": {
|
||||
"title": "รายการมอบหมายงาน",
|
||||
"createButton": "สร้างการมอบหมาย",
|
||||
"noActive": "ไม่มีการมอบหมายงานที่ active",
|
||||
"historyButton": "ประวัติการมอบหมาย"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"createTitle": "สร้างการมอบหมายงาน",
|
||||
"editTitle": "แก้ไขการมอบหมายงาน",
|
||||
"delegateTo": "มอบหมายให้",
|
||||
"delegateToPlaceholder": "เลือกผู้ใช้...",
|
||||
"startDate": "วันที่เริ่มต้น",
|
||||
"endDate": "วันที่สิ้นสุด",
|
||||
"scope": "ขอบเขตการมอบหมาย",
|
||||
"scopeOptions": {
|
||||
"ALL": "ทั้งหมด",
|
||||
"DISCIPLINE": "เฉพาะสาขา",
|
||||
"PROJECT": "เฉพาะโครงการ"
|
||||
},
|
||||
"reason": "เหตุผล",
|
||||
"reasonPlaceholder": "ระบุเหตุผลการมอบหมายงาน...",
|
||||
"saveButton": "บันทึก",
|
||||
"cancelButton": "ยกเลิก",
|
||||
"revokeButton": "ยกเลิกการมอบหมาย"
|
||||
},
|
||||
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"expired": "หมดอายุ",
|
||||
"revoked": "ถูกยกเลิก",
|
||||
"upcoming": "เริ่มเร็วๆ นี้"
|
||||
},
|
||||
|
||||
"badge": {
|
||||
"delegatedFrom": "มอบหมายจาก: {{name}}",
|
||||
"delegatedTo": "มอบหมายให้: {{name}}",
|
||||
"until": "จนถึง: {{date}}"
|
||||
},
|
||||
|
||||
"notifications": {
|
||||
"createdTitle": "ได้รับการมอบหมายงาน",
|
||||
"createdBody": "{{delegator}} ได้มอบหมายงานตรวจสอบให้คุณ",
|
||||
"expiringTitle": "การมอบหมายงานใกล้หมดอายุ",
|
||||
"expiringBody": "การมอบหมายงานจะหมดอายุใน {{days}} วัน",
|
||||
"expiredTitle": "การมอบหมายงานหมดอายุ",
|
||||
"expiredBody": "การมอบหมายงานได้หมดอายุแล้ว งานจะกลับมาที่ผู้มอบหมาย"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"delegateToRequired": "กรุณาเลือกผู้รับมอบหมาย",
|
||||
"startDateRequired": "กรุณาระบุวันที่เริ่มต้น",
|
||||
"endDateRequired": "กรุณาระบุวันที่สิ้นสุด",
|
||||
"invalidDateRange": "วันที่สิ้นสุดต้องมากกว่าวันที่เริ่มต้น",
|
||||
"circularDelegation": "ไม่สามารถมอบหมายแบบวนซ้ำได้ ({{path}})",
|
||||
"selfDelegation": "ไม่สามารถมอบหมายให้ตัวเองได้",
|
||||
"overlapExists": "มีการมอบหมายที่ซ้อนทับกันอยู่แล้ว",
|
||||
"loadFailed": "ไม่สามารถโหลดข้อมูลได้",
|
||||
"saveFailed": "ไม่สามารถบันทึกการมอบหมายได้",
|
||||
"revokeFailed": "ไม่สามารถยกเลิกการมอบหมายได้"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"created": "สร้างการมอบหมายสำเร็จ",
|
||||
"updated": "อัปเดตการมอบหมายสำเร็จ",
|
||||
"revoked": "ยกเลิกการมอบหมายสำเร็จ"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"title": "รหัสผลการตรวจสอบ",
|
||||
"subtitle": "จัดการรหัสผลการตรวจสอบและผลกระทบ",
|
||||
|
||||
"list": {
|
||||
"title": "รายการรหัสผลตรวจสอบ",
|
||||
"createButton": "สร้างรหัสใหม่",
|
||||
"searchPlaceholder": "ค้นหารหัส...",
|
||||
"noResults": "ไม่พบรหัสผลตรวจสอบ",
|
||||
"filterByCategory": "กรองตามหมวดหมู่"
|
||||
},
|
||||
|
||||
"categories": {
|
||||
"ENGINEERING": "วิศวกรรม",
|
||||
"CONTRACT": "สัญญา",
|
||||
"QUALITY": "คุณภาพ",
|
||||
"SAFETY": "ความปลอดภัย",
|
||||
"PROCUREMENT": "จัดซื้อ",
|
||||
"GENERAL": "ทั่วไป"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"createTitle": "สร้างรหัสผลตรวจสอบ",
|
||||
"editTitle": "แก้ไขรหัสผลตรวจสอบ",
|
||||
"code": "รหัส",
|
||||
"codePlaceholder": "เช่น 1A, 1B, 2",
|
||||
"subStatus": "สถานะย่อย",
|
||||
"subStatusPlaceholder": "เช่น A, B, C",
|
||||
"category": "หมวดหมู่",
|
||||
"description": "คำอธิบาย",
|
||||
"descriptionPlaceholder": "คำอธิบายรหัสผลตรวจสอบ",
|
||||
"descriptionThai": "คำอธิบาย (ภาษาไทย)",
|
||||
"implications": "ผลกระทบ",
|
||||
"implicationsPlaceholder": "ผลกระทบที่เกิดขึ้นเมื่อเลือกรหัสนี้",
|
||||
"notifyRoles": "แจ้งเตือนถึงบทบาท",
|
||||
"isActive": "เปิดใช้งาน",
|
||||
"saveButton": "บันทึก",
|
||||
"cancelButton": "ยกเลิก"
|
||||
},
|
||||
|
||||
"selector": {
|
||||
"placeholder": "เลือกรหัสผลตรวจสอบ...",
|
||||
"filterByDocType": "กรองตามประเภทเอกสาร",
|
||||
"loading": "กำลังโหลด..."
|
||||
},
|
||||
|
||||
"implications": {
|
||||
"title": "ผลกระทบจากรหัส",
|
||||
"critical": "สำคัญ",
|
||||
"warning": "คำเตือน",
|
||||
"info": "ข้อมูล",
|
||||
"notificationsSent": "จะมีการแจ้งเตือนไปยัง: {{roles}}"
|
||||
},
|
||||
|
||||
"matrix": {
|
||||
"title": "Master Approval Matrix",
|
||||
"subtitle": "กำหนดรหัสผลตรวจสอบตามประเภทเอกสาร",
|
||||
"docType": "ประเภทเอกสาร",
|
||||
"applicableCodes": "รหัสที่ใช้ได้",
|
||||
"projectOverride": "การตั้งค่าเฉพาะโครงการ",
|
||||
"inheritanceNote": "สืบทอดจากค่าเริ่มต้น",
|
||||
"addOverride": "เพิ่มการตั้งค่าเฉพาะโครงการ"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"codeRequired": "กรุณาระบุรหัส",
|
||||
"codeExists": "รหัสนี้มีอยู่แล้ว",
|
||||
"categoryRequired": "กรุณาเลือกหมวดหมู่",
|
||||
"invalidCode": "รูปแบบรหัสไม่ถูกต้อง",
|
||||
"loadFailed": "ไม่สามารถโหลดรหัสได้",
|
||||
"saveFailed": "ไม่สามารถบันทึกรหัสได้"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"created": "สร้างรหัสสำเร็จ",
|
||||
"updated": "อัปเดตรหัสสำเร็จ",
|
||||
"deleted": "ลบรหัสสำเร็จ"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"title": "งานตรวจสอบ",
|
||||
"subtitle": "จัดการงานตรวจสอบเอกสารและติดตามสถานะ",
|
||||
|
||||
"inbox": {
|
||||
"title": "กล่องงานตรวจสอบ",
|
||||
"pending": "รอดำเนินการ",
|
||||
"inProgress": "กำลังดำเนินการ",
|
||||
"completed": "เสร็จสิ้น",
|
||||
"delegated": "มอบหมายแล้ว",
|
||||
"noTasks": "ไม่มีงานตรวจสอบ",
|
||||
"filterAll": "ทั้งหมด",
|
||||
"filterByProject": "กรองตามโครงการ",
|
||||
"filterByDiscipline": "กรองตามสาขา"
|
||||
},
|
||||
|
||||
"status": {
|
||||
"PENDING": "รอดำเนินการ",
|
||||
"IN_PROGRESS": "กำลังดำเนินการ",
|
||||
"COMPLETED": "เสร็จสิ้น",
|
||||
"DELEGATED": "มอบหมายแล้ว",
|
||||
"OVERDUE": "เลยกำหนด"
|
||||
},
|
||||
|
||||
"actions": {
|
||||
"start": "เริ่มตรวจสอบ",
|
||||
"complete": "ดำเนินการเสร็จสิ้น",
|
||||
"delegate": "มอบหมาย",
|
||||
"return": "ส่งคืน",
|
||||
"viewDetails": "ดูรายละเอียด",
|
||||
"addComment": "เพิ่มความเห็น",
|
||||
"viewDocument": "ดูเอกสาร"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"completeTitle": "ดำเนินการตรวจสอบ",
|
||||
"responseCode": "รหัสผลตรวจสอบ",
|
||||
"responseCodePlaceholder": "เลือกรหัส...",
|
||||
"comments": "ความเห็น",
|
||||
"commentsPlaceholder": "ระบุความเห็นประกอบ...",
|
||||
"attachments": "ไฟล์แนบ",
|
||||
"addAttachment": "เพิ่มไฟล์",
|
||||
"confirmComplete": "ยืนยันการดำเนินการเสร็จสิ้น?",
|
||||
"saveButton": "บันทึก",
|
||||
"cancelButton": "ยกเลิก"
|
||||
},
|
||||
|
||||
"progress": {
|
||||
"title": "ความคืบหน้าการตรวจสอบ",
|
||||
"completedOf": "{{completed}}/{{total}} เสร็จสิ้น",
|
||||
"consensusStatus": "สถานะฉันทามติ: {{status}}",
|
||||
"waitingFor": "รอจาก: {{disciplines}}",
|
||||
"parallelReview": "การตรวจสอบแบบขนาน",
|
||||
"sequentialReview": "การตรวจสอบแบบลำดับ"
|
||||
},
|
||||
|
||||
"veto": {
|
||||
"button": "Veto Override",
|
||||
"title": "Veto Override - บังคับอนุมัติ",
|
||||
"justification": "เหตุผลในการบังคับอนุมัติ",
|
||||
"justificationPlaceholder": "ระบุเหตุผลที่จำเป็นต้องบังคับอนุมัติ...",
|
||||
"confirm": "ยืนยัน Veto Override",
|
||||
"warning": "การกระทำนี้จะถูกบันทึกใน Audit Log และแจ้งเตือนทีมงานที่เกี่ยวข้อง",
|
||||
"pmOnly": "เฉพาะ Project Manager เท่านั้น"
|
||||
},
|
||||
|
||||
"consensus": {
|
||||
"APPROVED": "อนุมัติ",
|
||||
"APPROVED_WITH_COMMENTS": "อนุมัติพร้อมความเห็น",
|
||||
"REJECTED": "ไม่อนุมัติ",
|
||||
"PENDING": "รอการพิจารณา",
|
||||
"VETO_OVERRIDE": "Veto Override",
|
||||
"calculating": "กำลังคำนวณฉันทามติ..."
|
||||
},
|
||||
|
||||
"reminders": {
|
||||
"dueSoon": "ครบกำหนดใน {{days}} วัน",
|
||||
"overdue": "เลยกำหนด {{days}} วัน",
|
||||
"escalationLevel1": "การแจ้งเตือนระดับ 1",
|
||||
"escalationLevel2": "การแจ้งเตือนระดับ 2"
|
||||
},
|
||||
|
||||
"confirmation": {
|
||||
"title": "ยืนยันการดำเนินการ",
|
||||
"message": "คุณแน่ใจหรือไม่ว่าต้องการดำเนินการนี้?",
|
||||
"confirmButton": "ยืนยัน",
|
||||
"cancelButton": "ยกเลิก"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"loadFailed": "ไม่สามารถโหลดงานตรวจสอบได้",
|
||||
"completeFailed": "ไม่สามารถดำเนินการเสร็จสิ้นได้",
|
||||
"responseCodeRequired": "กรุณาเลือกรหัสผลตรวจสอบ",
|
||||
"raceCondition": "สถานะงานมีการเปลี่ยนแปลง กรุณารีเฟรชและลองใหม่",
|
||||
"unauthorized": "คุณไม่มีสิทธิ์ดำเนินการนี้"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"started": "เริ่มตรวจสอบสำเร็จ",
|
||||
"completed": "ดำเนินการเสร็จสิ้นสำเร็จ",
|
||||
"delegated": "มอบหมายสำเร็จ",
|
||||
"commentAdded": "เพิ่มความเห็นสำเร็จ"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"title": "จัดการทีมตรวจสอบ",
|
||||
"subtitle": "สร้างและจัดการทีมตรวจสอบเอกสารตามสาขา",
|
||||
|
||||
"list": {
|
||||
"title": "รายการทีมตรวจสอบ",
|
||||
"createButton": "สร้างทีมใหม่",
|
||||
"searchPlaceholder": "ค้นหาทีม...",
|
||||
"noResults": "ไม่พบทีมตรวจสอบ",
|
||||
"teamCount": "{{count}} ทีม",
|
||||
"memberCount": "{{count}} สมาชิก"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"createTitle": "สร้างทีมตรวจสอบ",
|
||||
"editTitle": "แก้ไขทีมตรวจสอบ",
|
||||
"name": "ชื่อทีม",
|
||||
"namePlaceholder": "ระบุชื่อทีมตรวจสอบ",
|
||||
"description": "รายละเอียด",
|
||||
"descriptionPlaceholder": "ระบุรายละเอียดทีม (ถ้ามี)",
|
||||
"disciplines": "สาขาที่รับผิดชอบ",
|
||||
"disciplinesPlaceholder": "เลือกสาขา...",
|
||||
"rfaTypes": "ประเภท RFA ที่ตรวจสอบ",
|
||||
"rfaTypesPlaceholder": "เลือกประเภท RFA...",
|
||||
"isActive": "เปิดใช้งาน",
|
||||
"isDefault": "ตั้งเป็นค่าเริ่มต้น",
|
||||
"saveButton": "บันทึก",
|
||||
"cancelButton": "ยกเลิก",
|
||||
"deleteButton": "ลบทีม",
|
||||
"confirmDelete": "ยืนยันการลบทีมนี้?"
|
||||
},
|
||||
|
||||
"members": {
|
||||
"title": "สมาชิกทีม",
|
||||
"addButton": "เพิ่มสมาชิก",
|
||||
"removeButton": "ลบออกจากทีม",
|
||||
"role": {
|
||||
"LEAD": "หัวหน้าทีม",
|
||||
"REVIEWER": "ผู้ตรวจสอบ",
|
||||
"OBSERVER": "ผู้สังเกตการณ์"
|
||||
},
|
||||
"noMembers": "ยังไม่มีสมาชิกในทีม",
|
||||
"searchUserPlaceholder": "ค้นหาผู้ใช้..."
|
||||
},
|
||||
|
||||
"status": {
|
||||
"active": "เปิดใช้งาน",
|
||||
"inactive": "ปิดใช้งาน",
|
||||
"default": "ค่าเริ่มต้น"
|
||||
},
|
||||
|
||||
"errors": {
|
||||
"nameRequired": "กรุณาระบุชื่อทีม",
|
||||
"nameExists": "ชื่อทีมนี้มีอยู่แล้ว",
|
||||
"disciplineRequired": "กรุณาเลือกอย่างน้อยหนึ่งสาขา",
|
||||
"memberRequired": "กรุณาเพิ่มอย่างน้อยหนึ่งสมาชิก",
|
||||
"leadRequired": "ทีมต้องมีหัวหน้าทีมอย่างน้อยหนึ่งคน",
|
||||
"loadFailed": "ไม่สามารถโหลดข้อมูลทีมได้",
|
||||
"saveFailed": "ไม่สามารถบันทึกทีมได้",
|
||||
"deleteFailed": "ไม่สามารถลบทีมได้"
|
||||
},
|
||||
|
||||
"success": {
|
||||
"created": "สร้างทีมสำเร็จ",
|
||||
"updated": "อัปเดตทีมสำเร็จ",
|
||||
"deleted": "ลบทีมสำเร็จ",
|
||||
"memberAdded": "เพิ่มสมาชิกสำเร็จ",
|
||||
"memberRemoved": "ลบสมาชิกสำเร็จ"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user