Replace per-row inline action buttons (Edit, Activate/Deactivate, Delete, Grant SuperUser) across all six admin tables with a Radix DropdownMenu triggered by a ⋯ button. Introduces RowActionsMenu/Item/DestructiveItem/ Separator primitives and a DeleteConfirmDialog modal. Each Edit*Button gains controlled open/onOpenChange props so the dialog can be driven from the table's per-row ActionsMenu sub-component. Toggle and delete actions use useTransition + router.refresh() directly in the table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
5.5 KiB
TypeScript
119 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { AdminDialog } from "@/components/ui/admin-dialog";
|
|
import { createSite, updateSite } from "./actions";
|
|
|
|
const INPUT = "w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20";
|
|
|
|
type SiteRow = { id: string; name: string; code: string; address: string | null; latitude: number | null; longitude: number | null; isActive: boolean };
|
|
|
|
function SiteFormFields({ site }: { site?: SiteRow }) {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="col-span-2">
|
|
<label className="block text-xs font-medium text-neutral-700 mb-1">Site Name *</label>
|
|
<input name="name" defaultValue={site?.name} required className={INPUT} placeholder="e.g. Navi Mumbai Port" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-neutral-700 mb-1">Code *</label>
|
|
<input name="code" defaultValue={site?.code} required className={INPUT} placeholder="e.g. NMB" style={{ textTransform: "uppercase" }} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-neutral-700 mb-1">Pincode (for auto-geocoding)</label>
|
|
<input name="pincode" className={INPUT} placeholder="e.g. 400614" />
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="block text-xs font-medium text-neutral-700 mb-1">Address</label>
|
|
<textarea name="address" defaultValue={site?.address ?? ""} rows={2} className={INPUT} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-neutral-700 mb-1">Latitude (override)</label>
|
|
<input name="latitude" type="number" step="any" defaultValue={site?.latitude ?? ""} className={INPUT} placeholder="18.6117" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-neutral-700 mb-1">Longitude (override)</label>
|
|
<input name="longitude" type="number" step="any" defaultValue={site?.longitude ?? ""} className={INPUT} placeholder="73.0059" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AddSiteButton() {
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault(); setPending(true); setError("");
|
|
const result = await createSite(new FormData(e.currentTarget));
|
|
if ("error" in result) { setError(result.error); setPending(false); }
|
|
else { setPending(false); setOpen(false); router.refresh(); }
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<button onClick={() => setOpen(true)} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700">
|
|
+ Add Site
|
|
</button>
|
|
<AdminDialog open={open} onClose={() => setOpen(false)} title="Add Site">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<SiteFormFields />
|
|
{error && <p className="text-sm text-danger-700 bg-danger-50 rounded px-3 py-2">{error}</p>}
|
|
<div className="flex gap-3 justify-end">
|
|
<button type="button" onClick={() => setOpen(false)} className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700">Cancel</button>
|
|
<button type="submit" disabled={pending} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">{pending ? "Saving…" : "Create Site"}</button>
|
|
</div>
|
|
</form>
|
|
</AdminDialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function EditSiteButton({
|
|
site,
|
|
open: controlledOpen,
|
|
onOpenChange,
|
|
}: {
|
|
site: SiteRow;
|
|
open?: boolean;
|
|
onOpenChange?: (v: boolean) => void;
|
|
}) {
|
|
const router = useRouter();
|
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
const isControlled = controlledOpen !== undefined;
|
|
const open = isControlled ? controlledOpen : internalOpen;
|
|
const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen;
|
|
|
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault(); setPending(true); setError("");
|
|
const result = await updateSite(site.id, new FormData(e.currentTarget));
|
|
if ("error" in result) { setError(result.error); setPending(false); }
|
|
else { setPending(false); setOpen(false); router.refresh(); }
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{!isControlled && (
|
|
<button onClick={() => setOpen(true)} className="text-xs text-primary-600 hover:underline font-medium">Edit</button>
|
|
)}
|
|
<AdminDialog open={open} onClose={() => setOpen(false)} title={`Edit — ${site.name}`}>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<SiteFormFields site={site} />
|
|
{error && <p className="text-sm text-danger-700 bg-danger-50 rounded px-3 py-2">{error}</p>}
|
|
<div className="flex justify-end gap-3">
|
|
<button type="button" onClick={() => setOpen(false)} className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700">Cancel</button>
|
|
<button type="submit" disabled={pending} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">{pending ? "Saving…" : "Save"}</button>
|
|
</div>
|
|
</form>
|
|
</AdminDialog>
|
|
</>
|
|
);
|
|
}
|