import * as React from "react"; import { Box, Container, Paper, Typography, TextField, Button, ToggleButtonGroup, ToggleButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Chip, IconButton, CircularProgress, Alert, Snackbar, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Tooltip, Select, MenuItem, InputLabel, FormControl, OutlinedInput, Autocomplete, } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import RefreshIcon from "@mui/icons-material/Refresh"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ReplayIcon from "@mui/icons-material/Replay"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import ErrorIcon from "@mui/icons-material/Error"; import ScheduleIcon from "@mui/icons-material/Schedule"; import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty"; import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import { useFetchRequestsList, useCreateFetchRequest, useUpdateFetchRequest, useDeleteFetchRequest, useUploadFile, } from "./features/fetch-requests"; import type { FetchRequest, FetchRequestStatus, FileSource, EmailSource, } from "./features/fetch-requests"; import { RETRY_MAX } from "./features/fetch-requests"; import { useNavigate } from "react-router-dom"; import { useResourceByName, useConfig } from "../react-openapi"; const statusColors: Record = { pending: "default", processing: "info", paused: "warning", raw_expenses_done: "primary", enriched_done: "warning", completed: "success", failed: "error", }; const statusIcons: Record = { pending: , processing: , paused: , raw_expenses_done: , enriched_done: , completed: , failed: , }; function formatDate(iso: string) { const d = new Date(iso); return d.toLocaleString(); } function formatDateRange(start?: string, end?: string) { if (!start && !end) return "—"; const s = start ? new Date(start).toLocaleDateString() : "?"; const e = end ? new Date(end).toLocaleDateString() : "?"; return `${s} → ${e}`; } function shortId(fp: string) { return fp.length > 8 ? fp.slice(0, 8) + "…" : fp; } export default function FetchRequests() { const navigate = useNavigate(); const [sourceType, setSourceType] = React.useState<"file" | "email">("file"); const [accountName, setAccountName] = React.useState(""); const [payorUsername, setPayorUsername] = React.useState("aetos"); const [format, setFormat] = React.useState(""); const [file, setFile] = React.useState(null); const [uploadedPath, setUploadedPath] = React.useState(null); const [fromEmail, setFromEmail] = React.useState(""); const [subject, setSubject] = React.useState(""); const [rawTerms, setRawTerms] = React.useState(""); const [startDate, setStartDate] = React.useState(""); const [endDate, setEndDate] = React.useState(""); const [snackbar, setSnackbar] = React.useState<{ message: string; severity: "success" | "error" } | null>(null); const [deleteTarget, setDeleteTarget] = React.useState(null); const [statusFilter, setStatusFilter] = React.useState([]); const [accountFilter, setAccountFilter] = React.useState(""); const [sourceFilter, setSourceFilter] = React.useState<"all" | "file" | "email">("all"); const { data: listData, isLoading, isFetching, refetch } = useFetchRequestsList({ ...(statusFilter.length > 0 ? { status: statusFilter.join(",") } : {}), ...(accountFilter ? { account_name: accountFilter } : {}), ...(sourceFilter !== "all" ? { source_type: sourceFilter } : {}), }); const { useList: useAccountsList } = useResourceByName("accounts"); const { data: accountsData } = useAccountsList(); const accountOptions: string[] = React.useMemo(() => { return (accountsData?.data ?? []).map((a: any) => a.name).filter(Boolean); }, [accountsData]); const config = useConfig(); const fetchRes = config?.resources.find((r: any) => r.name === "fetch-requests"); const formatOptions: string[] = (fetchRes?.fields?.source?.schema?.format?.options as string[]) ?? ["axis", "icici_ocr"]; const createMutation = useCreateFetchRequest(); const updateMutation = useUpdateFetchRequest(); const deleteMutation = useDeleteFetchRequest(); const uploadMutation = useUploadFile(); const requests = listData?.data ?? []; const handleUpload = async () => { if (!file) return; const result = await uploadMutation.mutateAsync(file); if (result?.saved_as) { setUploadedPath(result.saved_as); if (!format) setFormat(file.name.split(".").pop() || ""); setSnackbar({ message: `File uploaded: ${result.saved_as}`, severity: "success" }); } }; const handleCreate = async () => { if (!accountName) return; let source: FileSource | EmailSource; if (sourceType === "file") { if (!uploadedPath || !format) return; source = { path: uploadedPath, format } as FileSource; } else { if (!format) return; const emailSource: EmailSource = { format }; if (fromEmail) emailSource.from_email = fromEmail; if (subject) emailSource.subject = subject; if (rawTerms.trim()) emailSource.raw_terms = rawTerms.split(",").map((s) => s.trim()).filter(Boolean); source = emailSource; } try { const result = await createMutation.mutateAsync({ source, account_name: accountName, payor_username: payorUsername, ...(startDate ? { start_date: new Date(startDate).toISOString() } : {}), ...(endDate ? { end_date: new Date(endDate).toISOString() } : {}), }); setSnackbar({ message: "Fetch request created", severity: "success" }); resetForm(); navigate(`/fetch-requests/${result.id}`); } catch (err: any) { if (err?.response?.status === 409) { setSnackbar({ message: "Duplicate — same fingerprint already exists", severity: "error" }); } else { setSnackbar({ message: err?.response?.data?.detail || "Failed to create fetch request", severity: "error" }); } } }; const resetForm = () => { setAccountName(""); setFormat(""); setFile(null); setUploadedPath(null); setFromEmail(""); setSubject(""); setRawTerms(""); setStartDate(""); setEndDate(""); }; const handleRetry = async (req: FetchRequest) => { try { await updateMutation.mutateAsync({ id: req.id, data: { status: "pending" } }); setSnackbar({ message: "Retrying fetch request", severity: "success" }); } catch { setSnackbar({ message: "Failed to retry", severity: "error" }); } }; const handleDelete = async () => { if (!deleteTarget) return; try { await deleteMutation.mutateAsync(deleteTarget.id); setSnackbar({ message: "Fetch request deleted", severity: "success" }); } catch { setSnackbar({ message: "Failed to delete", severity: "error" }); } setDeleteTarget(null); }; const sourceTypeOptions: ("all" | "file" | "email")[] = ["all", "file", "email"]; return ( Fetch Request Pipeline New Fetch Request val && setSourceType(val)} sx={{ mb: 3 }} size="small" > File Upload Email Fetch {sourceType === "file" ? ( <> {file ? file.name : "No file selected"} {uploadedPath && ( Uploaded as: {uploadedPath} )} Format ) : ( <> Format setFromEmail(e.target.value)} size="small" /> setSubject(e.target.value)} size="small" /> setRawTerms(e.target.value)} size="small" helperText="Comma-separated search terms" /> )} setAccountName(val ?? "")} renderInput={(params) => ( )} sx={{ "& .MuiOutlinedInput-root": { height: "auto", minHeight: "2.5rem" } }} /> setPayorUsername(e.target.value)} size="small" helperText="Default: aetos" /> setStartDate(e.target.value)} size="small" InputLabelProps={{ shrink: true }} inputProps={{ max: new Date().toISOString().split("T")[0] }} sx={{ flex: 1 }} /> setEndDate(e.target.value)} size="small" InputLabelProps={{ shrink: true }} inputProps={{ max: new Date().toISOString().split("T")[0] }} sx={{ flex: 1 }} /> Status setAccountFilter(e.target.value)} size="small" sx={{ minWidth: 160 }} /> val && setSourceFilter(val)} size="small" > {sourceTypeOptions.map((opt) => ( {opt === "all" ? "All" : opt === "file" ? "File" : "Email"} ))} refetch()} disabled={isFetching}> {isLoading ? ( ) : requests.length === 0 ? ( No fetch requests yet ) : ( ID Account Source Date Range Status Retries Created Actions {[...requests] .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .map((req: FetchRequest) => ( navigate(`/fetch-requests/${req.id}`)} sx={{ cursor: "pointer", "&:last-child td": { border: 0 } }} > {shortId(req.fingerprint)} { e.stopPropagation(); navigator.clipboard.writeText(req.fingerprint); setSnackbar({ message: "Copied!", severity: "success" }); }} sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }} > {req.account_name} {formatDateRange((req as any).start_date, (req as any).end_date)} {(req.retry_count ?? 0) > 0 ? ( {req.retry_count}/{RETRY_MAX} ) : ( )} {formatDate(req.created_at)} {req.status === "paused" && ( { e.stopPropagation(); navigate(`/fetch-requests/${req.id}`); }} > )} {req.status === "failed" && (req.retry_count ?? 0) < RETRY_MAX && ( { e.stopPropagation(); handleRetry(req); }} > )} { e.stopPropagation(); setDeleteTarget(req); }} > ))}
)} setSnackbar(null)} anchorOrigin={{ vertical: "bottom", horizontal: "center" }} > {snackbar ? setSnackbar(null)}>{snackbar.message} : undefined} setDeleteTarget(null)}> Delete Fetch Request? This will permanently delete the fetch request and all associated data.
); }