103 lines
4.7 KiB
TypeScript
103 lines
4.7 KiB
TypeScript
// ADR-021 T027: useWorkflowAction — hook สำหรับส่ง Approve/Reject/Return action
|
|
// สร้าง Idempotency-Key ครั้งเดียวต่อ action intent (via useState) ป้องกัน duplicate submission
|
|
// ADR-021 T027a (Clarify Q1+Q2): จัดการ HTTP 409 (state violation) และ 503 (Redlock fail-closed)
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { toast } from 'sonner';
|
|
import { workflowEngineService } from '@/lib/services/workflow-engine.service';
|
|
import type { ApiErrorResponse } from '@/lib/api/client';
|
|
import type { WorkflowTransitionWithAttachmentsDto } from '@/types/dto/workflow-engine/workflow-engine.dto';
|
|
|
|
// Type guard — ตรวจสอบว่า error ที่ได้มาเป็น ApiErrorResponse (จาก parseApiError interceptor)
|
|
function isApiErrorResponse(err: unknown): err is ApiErrorResponse {
|
|
return (
|
|
typeof err === 'object' &&
|
|
err !== null &&
|
|
'error' in err &&
|
|
typeof (err as ApiErrorResponse).error === 'object'
|
|
);
|
|
}
|
|
|
|
export function useWorkflowAction(instanceId: string | undefined) {
|
|
const queryClient = useQueryClient();
|
|
|
|
// สร้าง idempotency key ครั้งแรก — reset หลัง submit สำเร็จ เพื่อป้องกัน replay
|
|
const [idempotencyKey, setIdempotencyKey] = useState(() => uuidv4());
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (dto: WorkflowTransitionWithAttachmentsDto) => {
|
|
if (!instanceId) {
|
|
return Promise.reject(new Error('ไม่พบ Workflow Instance ID'));
|
|
}
|
|
return workflowEngineService.transition(instanceId, dto, idempotencyKey);
|
|
},
|
|
onSuccess: () => {
|
|
// Reset key สำหรับ action ครั้งถัดไป
|
|
setIdempotencyKey(uuidv4());
|
|
|
|
// Invalidate ประวัติ Workflow ของ Instance นี้
|
|
if (instanceId) {
|
|
void queryClient.invalidateQueries({
|
|
queryKey: ['workflow-history', instanceId],
|
|
});
|
|
}
|
|
|
|
// Invalidate รายการเอกสารหลักทั้งหมด (RFA, Correspondence)
|
|
void queryClient.invalidateQueries({ queryKey: ['rfas'] });
|
|
void queryClient.invalidateQueries({ queryKey: ['correspondences'] });
|
|
void queryClient.invalidateQueries({ queryKey: ['transmittals'] });
|
|
void queryClient.invalidateQueries({ queryKey: ['circulations'] });
|
|
|
|
toast.success('ดำเนินการเรียบร้อยแล้ว');
|
|
},
|
|
onError: (error: unknown) => {
|
|
// ADR-021 T027a: แยก handler ตาม status code
|
|
if (isApiErrorResponse(error)) {
|
|
const { statusCode, message, recoveryActions } = error.error;
|
|
|
|
// Clarify Q2: 503 Service Unavailable (Redlock Fail-closed)
|
|
if (statusCode === 503) {
|
|
toast.error('ระบบยุ่งชั่วคราว กรุณาลองใหม่อีกครั้งภายหลัง', {
|
|
description: 'การทำรายการไม่ถูกดำเนินการ ข้อมูลของคุณปลอดภัย',
|
|
});
|
|
// Keep idempotencyKey unchanged — user can retry ด้วย key เดิม
|
|
return;
|
|
}
|
|
|
|
// Clarify Q1: 409 Conflict (ไม่อยู่ในสถานะที่อนุญาตให้อัปโหลด)
|
|
if (statusCode === 409) {
|
|
toast.error(message || 'ไม่สามารถดำเนินการในสถานะนี้ได้', {
|
|
description: recoveryActions?.[0],
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 403 Forbidden — ไม่มีสิทธิ์
|
|
if (statusCode === 403) {
|
|
toast.error('คุณไม่มีสิทธิ์ดำเนินการในขั้นตอนนี้', {
|
|
description: recoveryActions?.[0],
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Fallback — ใช้ message จาก backend
|
|
toast.error(message || 'เกิดข้อผิดพลาด กรุณาลองใหม่');
|
|
return;
|
|
}
|
|
|
|
// Fallback — plain Error (เช่น ไม่พบ instanceId)
|
|
if (error instanceof Error) {
|
|
toast.error(error.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่');
|
|
return;
|
|
}
|
|
|
|
toast.error('เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่');
|
|
},
|
|
});
|
|
|
|
return mutation;
|
|
}
|