feat(vessels): editable custom code when creating a vessel
- Add Vessel dialog now shows an editable Code field pre-filled with the next auto-generated code (e.g. SITE-004) — user can change it freely - Edit Vessel dialog keeps the code read-only (changing codes on existing data would break PO references) - createVessel action: uses submitted code if provided, auto-generates if left blank, and validates uniqueness before saving Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0d17672ea9
commit
a1b77d8b00
4 changed files with 48 additions and 12 deletions
|
|
@ -11,6 +11,7 @@ type ActionResult = { ok: true } | { error: string };
|
|||
|
||||
const vesselSchema = z.object({
|
||||
name: z.string().min(1, "Vessel name is required"),
|
||||
code: z.string().optional(),
|
||||
siteId: z.string().optional(),
|
||||
});
|
||||
|
||||
|
|
@ -22,12 +23,23 @@ export async function createVessel(formData: FormData): Promise<ActionResult> {
|
|||
|
||||
const parsed = vesselSchema.safeParse({
|
||||
name: formData.get("name"),
|
||||
code: (formData.get("code") as string).trim() || undefined,
|
||||
siteId: (formData.get("siteId") as string) || undefined,
|
||||
});
|
||||
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
|
||||
|
||||
const existingCodes = await db.vessel.findMany({ select: { code: true } });
|
||||
const code = nextId("SITE", existingCodes.map((v) => v.code));
|
||||
|
||||
let code: string;
|
||||
if (parsed.data.code) {
|
||||
// User supplied a custom code — validate uniqueness
|
||||
const conflict = await db.vessel.findUnique({ where: { code: parsed.data.code } });
|
||||
if (conflict) return { error: `Code "${parsed.data.code}" is already in use by another vessel.` };
|
||||
code = parsed.data.code;
|
||||
} else {
|
||||
// Auto-generate next available code
|
||||
code = nextId("SITE", existingCodes.map((v) => v.code));
|
||||
}
|
||||
|
||||
await db.vessel.create({ data: { name: parsed.data.name, code, siteId: parsed.data.siteId ?? null } });
|
||||
revalidatePath("/admin/vessels");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { db } from "@/lib/db";
|
|||
import { hasPermission } from "@/lib/permissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import { VesselsTable } from "./vessels-table";
|
||||
import { nextId } from "@/lib/id-generators";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = { title: "Vessel Management" };
|
||||
|
|
@ -21,8 +22,11 @@ export default async function AdminVesselsPage() {
|
|||
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
]);
|
||||
|
||||
const suggestedCode = nextId("SITE", vessels.map((v) => v.code));
|
||||
|
||||
return (
|
||||
<VesselsTable
|
||||
suggestedCode={suggestedCode}
|
||||
vessels={vessels.map((v) => ({
|
||||
id: v.id,
|
||||
code: v.code,
|
||||
|
|
|
|||
|
|
@ -17,15 +17,35 @@ type VesselRow = {
|
|||
|
||||
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";
|
||||
|
||||
function VesselFormFields({ vessel, sites }: { vessel?: VesselRow; sites: SiteOption[] }) {
|
||||
function VesselFormFields({
|
||||
vessel,
|
||||
sites,
|
||||
suggestedCode,
|
||||
}: {
|
||||
vessel?: VesselRow;
|
||||
sites: SiteOption[];
|
||||
suggestedCode?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{vessel && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-neutral-700 mb-1">Code</label>
|
||||
<p className="font-mono text-sm text-neutral-700 px-3 py-2 bg-neutral-50 rounded-lg border border-neutral-200">{vessel.code}</p>
|
||||
</div>
|
||||
<label className="block text-xs font-medium text-neutral-700 mb-1">Code *</label>
|
||||
{vessel ? (
|
||||
/* Editing: show code read-only — code changes on existing data would break references */
|
||||
<p className="font-mono text-sm text-neutral-700 px-3 py-2 bg-neutral-50 rounded-lg border border-neutral-200">
|
||||
{vessel.code}
|
||||
</p>
|
||||
) : (
|
||||
/* Creating: editable, pre-filled with next available code */
|
||||
<input
|
||||
name="code"
|
||||
required
|
||||
defaultValue={suggestedCode}
|
||||
placeholder="e.g. SITE-001"
|
||||
className={`${INPUT} font-mono`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-neutral-700 mb-1">Vessel Name *</label>
|
||||
<input name="name" defaultValue={vessel?.name} required className={INPUT} />
|
||||
|
|
@ -45,7 +65,7 @@ function VesselFormFields({ vessel, sites }: { vessel?: VesselRow; sites: SiteOp
|
|||
);
|
||||
}
|
||||
|
||||
export function AddVesselButton({ sites }: { sites: SiteOption[] }) {
|
||||
export function AddVesselButton({ sites, suggestedCode }: { sites: SiteOption[]; suggestedCode?: string }) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
|
@ -68,7 +88,7 @@ export function AddVesselButton({ sites }: { sites: SiteOption[] }) {
|
|||
</button>
|
||||
<AdminDialog title="Add Vessel" open={open} onClose={() => setOpen(false)}>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<VesselFormFields sites={sites} />
|
||||
<VesselFormFields sites={sites} suggestedCode={suggestedCode} />
|
||||
{error && <p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>}
|
||||
<div className="flex justify-end gap-3 pt-1">
|
||||
<button type="button" onClick={() => setOpen(false)}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function VesselActionsMenu({ vessel, sites }: { vessel: VesselRow; sites: SiteOp
|
|||
);
|
||||
}
|
||||
|
||||
export function VesselsTable({ vessels, sites }: { vessels: VesselRow[]; sites: SiteOption[] }) {
|
||||
export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: VesselRow[]; sites: SiteOption[]; suggestedCode?: string }) {
|
||||
const { search, setSearch, sortKey, sortDir, toggleSort, activeFilters, toggleFilter, filtered } =
|
||||
useTableControls<VesselRow>({
|
||||
rows: vessels,
|
||||
|
|
@ -87,7 +87,7 @@ export function VesselsTable({ vessels, sites }: { vessels: VesselRow[]; sites:
|
|||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold text-neutral-900">Vessel Management</h1>
|
||||
<AddVesselButton sites={sites} />
|
||||
<AddVesselButton sites={sites} suggestedCode={suggestedCode} />
|
||||
</div>
|
||||
|
||||
<TableControls
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue