import React, { useEffect, useMemo, useState } from "react"; import { Box, Container, Typography, Paper, Button, Alert, CircularProgress, IconButton, Tooltip, } from "@mui/material"; import { useNavigate } from "react-router-dom"; import ReceiptLongIcon from "@mui/icons-material/ReceiptLong"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import { useAppContext, useResource, ListCellRenderer, FormFieldRenderer, getApi, applyDisplayFormat } from "../../react-openapi"; import type { FieldConfig } from "../../react-openapi"; import { PageHeader } from "../ui/PageHeader"; import { EmptyState } from "../ui/EmptyState"; import { formatApiError } from "../features/fetch-requests"; import { useToast } from "../ui/Toast"; const CREATE_FIELDS = ["account", "bank", "pipeline", "trust_fallback", "start_date", "end_date", "source"]; function FetchRequestList() { const navigate = useNavigate(); const { list, remove, resource } = useResource("fetch-requests"); const { resources: allResources } = useAppContext(); const [rows, setRows] = useState(null); const [deletingId, setDeletingId] = useState(null); const { showToast } = useToast(); const reload = () => { if (!resource) return; list({ limit: 20 }).then((res) => setRows(res.items ?? [])); }; const columns = useMemo(() => { if (!resource) return []; return resource.listColumns .map((n) => resource.fields.find((f) => f.name === n)) .filter(Boolean) as FieldConfig[]; }, [resource]); useEffect(() => { if (!resource) return; list({ limit: 20 }).then((res) => setRows(res.items ?? [])); }, [resource?.name]); const handleDelete = async (e: React.MouseEvent, id: string) => { e.stopPropagation(); if ( !window.confirm( "Delete this fetch request along with all its expenses and ambiguities? This cannot be undone.", ) ) return; setDeletingId(id); try { await remove(id); showToast("Fetch request deleted"); reload(); } catch (err: any) { showToast(formatApiError(err), "error"); } finally { setDeletingId(null); } }; if (!rows) { return ( ); } if (rows.length === 0) { return ( } title="No fetch requests yet" description="Upload a bank statement or configure email ingestion to import your first transactions." actionLabel="New Fetch Request" onAction={() => document.getElementById("new-fetch-request")?.scrollIntoView({ behavior: "smooth", block: "center" })} /> ); } return ( {rows.map((row, i) => { const displayFormat = resource?.displayFormat ?? ""; return ( navigate(`/fetch-requests/${row.id}`)} > {columns.map((col) => ( {col.label} r.name === col.fk!.resource)?.displayFormat ?? displayFormat) : displayFormat} /> ))} handleDelete(e, row.id)} > ); })} ); } export default function FetchRequestCreate() { const { resources: allResources } = useAppContext(); const resource = useMemo(() => allResources.find((r) => r.name === "fetch-requests"), [allResources]); const { create } = useResource("fetch-requests"); const navigate = useNavigate(); const [formData, setFormData] = useState>({}); const [fkOptions, setFkOptions] = useState>({}); const [loading, setLoading] = useState(false); const [result, setResult] = useState<{ severity: "success" | "error"; message: string } | null>(null); useEffect(() => { if (!resource) return; resource.relationships.forEach((rel) => { const targetRes = allResources.find((r) => r.name === rel.config.resource); if (!targetRes) return; (async () => { try { const api = getApi(); const params: Record = {}; if (targetRes.pagination) params.limit = 0; const res = await api.get(targetRes.path, { params }); const items = targetRes.pagination ? (res.data.items ?? []) : (Array.isArray(res.data) ? res.data : []); const opts = items.map((item: any) => ({ value: item[targetRes.primaryKey], label: applyDisplayFormat(item, targetRes.displayFormat), })); setFkOptions((prev) => ({ ...prev, [rel.fieldName]: opts })); } catch (e) { console.warn(`Failed to load FK options for ${rel.fieldName}:`, e); } })(); }); }, [resource]); const formFields = useMemo(() => { if (!resource) return []; return resource.orderedFields.filter((f) => CREATE_FIELDS.includes(f.name)); }, [resource]); const handleChange = (fieldName: string, value: any) => { setFormData((prev) => ({ ...prev, [fieldName]: value })); setResult(null); }; const handleSubmit = async () => { setLoading(true); setResult(null); try { const created = await create(formData); const display = applyDisplayFormat(created, resource!.displayFormat); setResult({ severity: "success", message: `Created: ${display}` }); setFormData({}); const newId = (created as any)?.id; if (newId) { navigate(`/fetch-requests/${newId}`); } } catch (e: any) { const detail = e?.response?.data?.detail; const msg = Array.isArray(detail) ? detail.map((d: any) => d.msg).join("; ") : (detail ?? e?.message ?? "Unknown error"); setResult({ severity: "error", message: `Failed: ${msg}` }); } finally { setLoading(false); } }; if (!resource) { return ( The fetch-requests resource was not found in this spec. ); } return ( New Fetch Request Choose an account, pipeline, and a file or email source to kick off an import. {formFields.map((field) => ( handleChange(field.name, val)} fkOptions={fkOptions[field.name]} /> ))} {result && ( setResult(null)}> {result.message} )} Recent Fetch Requests ); }