- Add all new Frontend source files (pages, components, hooks, utils) - Add selenium_MHBatchPaymentCheckWorker.py and MHSinglePaymentCheckWorker.py - Add install-steps-5-13.sh setup script - Update .gitignore to exclude runtime/sensitive data (backups, uploads, chat-history, keys, downloads, generated .d.ts files) while keeping folders - Add .gitkeep to preserve empty runtime folders in git Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
8.9 KiB
JavaScript
198 lines
8.9 KiB
JavaScript
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
|
import { Plus, Pencil, Trash2, X, Check } from "lucide-react";
|
|
const EMPTY_FORM = { name: "", phoneNumber: "" };
|
|
export function InsuranceContactCard() {
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingId, setEditingId] = useState(null);
|
|
const [form, setForm] = useState(EMPTY_FORM);
|
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
const { data: contacts = [], isLoading } = useQuery({
|
|
queryKey: ["/api/insurance-contacts"],
|
|
queryFn: async () => {
|
|
const res = await apiRequest("GET", "/api/insurance-contacts");
|
|
if (!res.ok)
|
|
throw new Error("Failed to fetch");
|
|
return res.json();
|
|
},
|
|
});
|
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["/api/insurance-contacts"] });
|
|
const createMutation = useMutation({
|
|
mutationFn: async (data) => {
|
|
const res = await apiRequest("POST", "/api/insurance-contacts", data);
|
|
if (!res.ok) {
|
|
const e = await res.json().catch(() => null);
|
|
throw new Error(e?.message || "Failed to save");
|
|
}
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
invalidate();
|
|
setShowForm(false);
|
|
setForm(EMPTY_FORM);
|
|
toast({ title: "Insurance contact added" });
|
|
},
|
|
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
|
});
|
|
const updateMutation = useMutation({
|
|
mutationFn: async ({ id, data }) => {
|
|
const res = await apiRequest("PUT", `/api/insurance-contacts/${id}`, data);
|
|
if (!res.ok) {
|
|
const e = await res.json().catch(() => null);
|
|
throw new Error(e?.message || "Failed to update");
|
|
}
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
invalidate();
|
|
setEditingId(null);
|
|
setForm(EMPTY_FORM);
|
|
toast({ title: "Insurance contact updated" });
|
|
},
|
|
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
|
});
|
|
const deleteMutation = useMutation({
|
|
mutationFn: async (id) => {
|
|
const res = await apiRequest("DELETE", `/api/insurance-contacts/${id}`);
|
|
if (!res.ok)
|
|
throw new Error("Failed to delete");
|
|
},
|
|
onSuccess: () => {
|
|
invalidate();
|
|
setDeleteTarget(null);
|
|
toast({ title: "Insurance contact deleted" });
|
|
},
|
|
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
|
});
|
|
const openAdd = () => {
|
|
setEditingId(null);
|
|
setForm(EMPTY_FORM);
|
|
setShowForm(true);
|
|
};
|
|
const openEdit = (c) => {
|
|
setShowForm(false);
|
|
setEditingId(c.id);
|
|
setForm({ name: c.name, phoneNumber: c.phoneNumber ?? "" });
|
|
};
|
|
const cancelEdit = () => { setEditingId(null); setForm(EMPTY_FORM); };
|
|
const cancelAdd = () => { setShowForm(false); setForm(EMPTY_FORM); };
|
|
const handleSave = () => {
|
|
if (!form.name.trim()) {
|
|
toast({ title: "Name is required", variant: "destructive" });
|
|
return;
|
|
}
|
|
if (editingId !== null) {
|
|
updateMutation.mutate({ id: editingId, data: form });
|
|
}
|
|
else {
|
|
createMutation.mutate(form);
|
|
}
|
|
};
|
|
const isSaving = createMutation.isPending || updateMutation.isPending;
|
|
return (<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center p-4 border-b border-gray-200">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900">Insurance Contacts</h2>
|
|
<p className="text-sm text-gray-500 mt-0.5">Phone numbers for insurance companies</p>
|
|
</div>
|
|
<Button onClick={openAdd} size="sm">
|
|
<Plus className="h-4 w-4 mr-1"/> Add Contact
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Add form */}
|
|
{showForm && (<div className="p-4 border-b bg-gray-50">
|
|
<p className="text-sm font-medium text-gray-700 mb-3">New Insurance Contact</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">Company Name *</label>
|
|
<Input placeholder="e.g. Delta MA" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} className="h-9 text-sm"/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-1">Phone Number</label>
|
|
<Input placeholder="e.g. (800) 555-0100" value={form.phoneNumber} onChange={(e) => setForm((f) => ({ ...f, phoneNumber: e.target.value }))} className="h-9 text-sm"/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 mt-3">
|
|
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
|
<Check className="h-3.5 w-3.5 mr-1"/>
|
|
{isSaving ? "Saving..." : "Save"}
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={cancelAdd} disabled={isSaving}>
|
|
<X className="h-3.5 w-3.5 mr-1"/> Cancel
|
|
</Button>
|
|
</div>
|
|
</div>)}
|
|
|
|
{/* Table */}
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Insurance Company
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Phone Number
|
|
</th>
|
|
<th className="px-4 py-3 w-24"/>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{isLoading ? (<tr>
|
|
<td colSpan={3} className="text-center py-6 text-sm text-gray-400">
|
|
Loading...
|
|
</td>
|
|
</tr>) : contacts.length === 0 ? (<tr>
|
|
<td colSpan={3} className="text-center py-10 text-sm text-gray-400">
|
|
No insurance contacts yet. Click "Add Contact" to add one.
|
|
</td>
|
|
</tr>) : (contacts.map((c) => editingId === c.id ? (
|
|
/* Inline edit row */
|
|
<tr key={c.id} className="bg-blue-50">
|
|
<td className="px-4 py-2">
|
|
<Input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} className="h-8 text-sm" placeholder="Company name"/>
|
|
</td>
|
|
<td className="px-4 py-2">
|
|
<Input value={form.phoneNumber} onChange={(e) => setForm((f) => ({ ...f, phoneNumber: e.target.value }))} className="h-8 text-sm" placeholder="Phone number"/>
|
|
</td>
|
|
<td className="px-4 py-2 text-right">
|
|
<Button variant="ghost" size="sm" onClick={handleSave} disabled={isSaving} className="text-green-600 hover:text-green-700">
|
|
<Check className="h-4 w-4"/>
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={cancelEdit} disabled={isSaving}>
|
|
<X className="h-4 w-4"/>
|
|
</Button>
|
|
</td>
|
|
</tr>) : (
|
|
/* Display row */
|
|
<tr key={c.id} className="hover:bg-gray-50">
|
|
<td className="px-4 py-3 text-sm font-medium text-gray-900">{c.name}</td>
|
|
<td className="px-4 py-3 text-sm text-gray-600">
|
|
{c.phoneNumber || <span className="text-gray-300">—</span>}
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<Button variant="ghost" size="sm" onClick={() => openEdit(c)}>
|
|
<Pencil className="h-4 w-4 text-gray-500"/>
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(c)}>
|
|
<Trash2 className="h-4 w-4 text-red-500"/>
|
|
</Button>
|
|
</td>
|
|
</tr>)))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<DeleteConfirmationDialog isOpen={!!deleteTarget} onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget.id)} onCancel={() => setDeleteTarget(null)} entityName={deleteTarget?.name}/>
|
|
</div>);
|
|
}
|