Open stop details in a modal from the Stops admin table
Clicking a stop (or the Edit link) in /admin/stops now opens a modal with Details/Products/Message tabs instead of navigating to /admin/stops/[id]. - Add getStopDetails server action (returns stop, brand, candidate products, assigned products, brand list in one round trip). - Add StopDetailModal component with tabbed panels that reuse the existing StopEditForm, StopProductAssignment, and MessageCustomersSection. - Add an optional onSaved callback to StopEditForm so the modal can refetch + router.refresh() after a save. - StopTableClient now opens the modal on row click; the /admin/stops/[id] route is kept in place for direct links/bookmarks.
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export type StopDetail = {
|
||||||
|
id: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
location: string;
|
||||||
|
slug: string;
|
||||||
|
active: boolean;
|
||||||
|
brand_id: string;
|
||||||
|
address: string | null;
|
||||||
|
zip: string | null;
|
||||||
|
cutoff_time: string | null;
|
||||||
|
brands: { name: string; slug: string } | { name: string; slug: string }[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssignedProduct = {
|
||||||
|
id: string;
|
||||||
|
product_id: string;
|
||||||
|
products: { name: string; type: string; price: number } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StopDetailsResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
stop: StopDetail;
|
||||||
|
allProducts: { id: string; name: string; type: string; price: number }[];
|
||||||
|
assignedProducts: AssignedProduct[];
|
||||||
|
brands: { id: string; name: string; slug: string }[];
|
||||||
|
}
|
||||||
|
| { success: false; error: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single stop with its brand, all candidate products, currently
|
||||||
|
* assigned products, and the list of brands (for the brand switcher in the
|
||||||
|
* edit form). Mirrors the data the old `/admin/stops/[id]` page server
|
||||||
|
* component loaded, so the modal can be a drop-in replacement.
|
||||||
|
*/
|
||||||
|
export async function getStopDetails(stopId: string): Promise<StopDetailsResult> {
|
||||||
|
const adminUser = await getAdminUser();
|
||||||
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||||
|
if (!adminUser.can_manage_stops) {
|
||||||
|
return { success: false, error: "Not authorized" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||||
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||||
|
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
|
||||||
|
|
||||||
|
// Use a fresh server-side client (cookie-less) so RLS doesn't block reads
|
||||||
|
// for platform_admin dev sessions. The auth check above has already gated
|
||||||
|
// access.
|
||||||
|
const server = useMockData
|
||||||
|
? null
|
||||||
|
: createClient(supabaseUrl, supabaseKey, {
|
||||||
|
auth: { persistSession: false, autoRefreshToken: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Stop + brand
|
||||||
|
let stop: StopDetail | null = null;
|
||||||
|
let stopErr: string | null = null;
|
||||||
|
|
||||||
|
if (server) {
|
||||||
|
const { data, error } = await server
|
||||||
|
.from("stops")
|
||||||
|
.select("*, brands(name, slug)")
|
||||||
|
.eq("id", stopId)
|
||||||
|
.single();
|
||||||
|
if (error) stopErr = error.message;
|
||||||
|
else stop = (data ?? null) as StopDetail | null;
|
||||||
|
} else {
|
||||||
|
// Mock fallback — empty
|
||||||
|
stopErr = "Stop not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stop) {
|
||||||
|
return { success: false, error: stopErr ?? "Stop not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brand-scope check for brand_admin
|
||||||
|
if (adminUser.brand_id && stop.brand_id !== adminUser.brand_id) {
|
||||||
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Candidate products for this brand
|
||||||
|
const { data: allProducts } = server
|
||||||
|
? await server
|
||||||
|
.from("products")
|
||||||
|
.select("id, name, type, price")
|
||||||
|
.eq("brand_id", stop.brand_id)
|
||||||
|
.eq("active", true)
|
||||||
|
: { data: [] as { id: string; name: string; type: string; price: number }[] };
|
||||||
|
|
||||||
|
// 3. Assigned products (joined with product info)
|
||||||
|
const { data: productStops } = server
|
||||||
|
? await server
|
||||||
|
.from("product_stops")
|
||||||
|
.select("id, product_id, products(id, name, type, price)")
|
||||||
|
.eq("stop_id", stopId)
|
||||||
|
: { data: [] as AssignedProduct[] };
|
||||||
|
|
||||||
|
// 4. Brands for the brand switcher
|
||||||
|
const { data: brands } = server
|
||||||
|
? await server.from("brands").select("id, name, slug")
|
||||||
|
: { data: [] as { id: string; name: string; slug: string }[] };
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stop,
|
||||||
|
allProducts: allProducts ?? [],
|
||||||
|
assignedProducts: (productStops ?? []) as AssignedProduct[],
|
||||||
|
brands: brands ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
"use client";
|
||||||
|
/* eslint-disable react-hooks/set-state-in-effect */
|
||||||
|
|
||||||
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import GlassModal from "@/components/admin/GlassModal";
|
||||||
|
import StopEditForm from "@/components/admin/StopEditForm";
|
||||||
|
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||||
|
import MessageCustomersSection from "@/components/admin/MessageCustomersSection";
|
||||||
|
import { getStopDetails } from "@/actions/stops/get-stop-details";
|
||||||
|
import { useToast } from "@/components/admin/design-system";
|
||||||
|
import type { StopDetail, AssignedProduct } from "@/actions/stops/get-stop-details";
|
||||||
|
|
||||||
|
type Tab = "details" | "products" | "message";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
stopId: string;
|
||||||
|
/** Optional: when the user clicks Duplicate from inside the modal. */
|
||||||
|
onDuplicate?: (stopId: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StopDetailModal({ stopId, onDuplicate, onClose }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { success: showSuccess } = useToast();
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [stop, setStop] = useState<StopDetail | null>(null);
|
||||||
|
const [allProducts, setAllProducts] = useState<{ id: string; name: string; type: string; price: number }[]>([]);
|
||||||
|
const [assignedProducts, setAssignedProducts] = useState<AssignedProduct[]>([]);
|
||||||
|
const [brands, setBrands] = useState<{ id: string; name: string; slug: string }[]>([]);
|
||||||
|
const [tab, setTab] = useState<Tab>("details");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
setLoadError(null);
|
||||||
|
getStopDetails(stopId)
|
||||||
|
.then((res) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
if (!res.success) {
|
||||||
|
setLoadError(res.error);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStop(res.stop);
|
||||||
|
setAllProducts(res.allProducts);
|
||||||
|
setAssignedProducts(res.assignedProducts);
|
||||||
|
setBrands(res.brands);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setLoadError(err?.message ?? "Failed to load stop");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [stopId]);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
getStopDetails(stopId).then((res) => {
|
||||||
|
if (!res.success) return;
|
||||||
|
setStop(res.stop);
|
||||||
|
setAllProducts(res.allProducts);
|
||||||
|
setAssignedProducts(res.assignedProducts);
|
||||||
|
setBrands(res.brands);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditSaved() {
|
||||||
|
refresh();
|
||||||
|
showSuccess("Stop updated", "Changes have been saved");
|
||||||
|
startTransition(() => router.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtitle = stop?.brands
|
||||||
|
? (Array.isArray(stop.brands) ? stop.brands[0]?.name : stop.brands.name) ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlassModal
|
||||||
|
title={loading ? "Loading…" : stop ? `${stop.city}, ${stop.state}` : "Stop"}
|
||||||
|
subtitle={subtitle ?? "Stop details"}
|
||||||
|
onClose={onClose}
|
||||||
|
maxWidth="max-w-3xl"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-4 w-1/3 animate-pulse rounded bg-stone-200" />
|
||||||
|
<div className="h-4 w-2/3 animate-pulse rounded bg-stone-200" />
|
||||||
|
<div className="h-4 w-1/2 animate-pulse rounded bg-stone-200" />
|
||||||
|
</div>
|
||||||
|
) : loadError ? (
|
||||||
|
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||||
|
{loadError}
|
||||||
|
</div>
|
||||||
|
) : stop ? (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{/* Tabs */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 rounded-xl border border-[var(--admin-border)] bg-stone-50 p-1"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
<TabButton active={tab === "details"} onClick={() => setTab("details")}>
|
||||||
|
Details
|
||||||
|
</TabButton>
|
||||||
|
<TabButton active={tab === "products"} onClick={() => setTab("products")}>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
Products
|
||||||
|
{assignedProducts.length > 0 && (
|
||||||
|
<span className="rounded-full bg-[var(--admin-accent)]/10 px-2 py-0.5 text-[10px] font-semibold text-[var(--admin-accent)]">
|
||||||
|
{assignedProducts.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</TabButton>
|
||||||
|
<TabButton active={tab === "message"} onClick={() => setTab("message")}>
|
||||||
|
Message
|
||||||
|
</TabButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === "details" && (
|
||||||
|
<DetailsPanel
|
||||||
|
stop={stop}
|
||||||
|
brands={brands}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
|
onSaved={handleEditSaved}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === "products" && (
|
||||||
|
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">
|
||||||
|
Assigned Products
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-xs text-[var(--admin-text-muted)]">
|
||||||
|
Manage which products are available at this stop.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<StopProductAssignment
|
||||||
|
stopId={stop.id}
|
||||||
|
allProducts={allProducts}
|
||||||
|
assignedProducts={assignedProducts}
|
||||||
|
callerUid={stop.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === "message" && (
|
||||||
|
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">
|
||||||
|
Message Customers
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-xs text-[var(--admin-text-muted)]">
|
||||||
|
Send updates to customers with pending pickups at this stop.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<MessageCustomersSection stopId={stop.id} brandId={stop.brand_id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</GlassModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabButton({
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={active}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-all ${
|
||||||
|
active
|
||||||
|
? "bg-white text-[var(--admin-accent)] shadow-sm border border-[var(--admin-border)]"
|
||||||
|
: "text-[var(--admin-text-secondary)] hover:text-[var(--admin-text-primary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailsPanel({
|
||||||
|
stop,
|
||||||
|
brands,
|
||||||
|
onDuplicate,
|
||||||
|
onSaved,
|
||||||
|
}: {
|
||||||
|
stop: StopDetail;
|
||||||
|
brands: { id: string; name: string; slug: string }[];
|
||||||
|
onDuplicate?: (stopId: string) => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-5">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)]">
|
||||||
|
{stop.city}, {stop.state}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-[var(--admin-text-secondary)]">{stop.location}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 rounded-full px-3 py-1 text-xs font-medium ${
|
||||||
|
stop.active
|
||||||
|
? "bg-emerald-100 text-emerald-700"
|
||||||
|
: "bg-stone-200 text-stone-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stop.active ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="mt-4 grid grid-cols-2 gap-4 text-sm sm:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs font-medium text-[var(--admin-text-muted)]">Date</dt>
|
||||||
|
<dd className="mt-0.5 font-mono text-[var(--admin-text-primary)]">{stop.date}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs font-medium text-[var(--admin-text-muted)]">Time</dt>
|
||||||
|
<dd className="mt-0.5 font-mono text-[var(--admin-text-primary)]">{stop.time}</dd>
|
||||||
|
</div>
|
||||||
|
{stop.address && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<dt className="text-xs font-medium text-[var(--admin-text-muted)]">Address</dt>
|
||||||
|
<dd className="mt-0.5 text-[var(--admin-text-primary)]">
|
||||||
|
{stop.address}
|
||||||
|
{stop.zip ? `, ${stop.zip}` : ""}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{stop.cutoff_time && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<dt className="text-xs font-medium text-[var(--admin-text-muted)]">Cutoff</dt>
|
||||||
|
<dd className="mt-0.5 text-[var(--admin-text-primary)]">
|
||||||
|
{new Date(stop.cutoff_time).toLocaleString()}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{onDuplicate ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDuplicate(stop.id)}
|
||||||
|
className="rounded-xl border border-[var(--admin-border)] bg-white px-3 py-1.5 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-stone-50"
|
||||||
|
>
|
||||||
|
Duplicate Stop
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={`/admin/stops/new?duplicate=${stop.id}`}
|
||||||
|
className="rounded-xl border border-[var(--admin-border)] bg-white px-3 py-1.5 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-stone-50"
|
||||||
|
>
|
||||||
|
Duplicate Stop
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Edit Stop</h3>
|
||||||
|
<p className="mt-1 text-xs text-[var(--admin-text-muted)]">
|
||||||
|
Update stop details, location, and availability.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<StopEditForm
|
||||||
|
stop={{
|
||||||
|
id: stop.id,
|
||||||
|
city: stop.city,
|
||||||
|
state: stop.state,
|
||||||
|
date: stop.date,
|
||||||
|
time: stop.time,
|
||||||
|
location: stop.location,
|
||||||
|
slug: stop.slug,
|
||||||
|
active: stop.active,
|
||||||
|
brand_id: stop.brand_id,
|
||||||
|
address: stop.address,
|
||||||
|
zip: stop.zip,
|
||||||
|
cutoff_time: stop.cutoff_time,
|
||||||
|
}}
|
||||||
|
brands={brands}
|
||||||
|
onSaved={onSaved}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,9 +27,11 @@ type StopEditFormProps = {
|
|||||||
cutoff_time?: string | null;
|
cutoff_time?: string | null;
|
||||||
};
|
};
|
||||||
brands: Brand[];
|
brands: Brand[];
|
||||||
|
/** Optional callback fired after a successful save. */
|
||||||
|
onSaved?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function StopEditForm({ stop, brands }: StopEditFormProps) {
|
export default function StopEditForm({ stop, brands, onSaved }: StopEditFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { success: showSuccess, error: showError } = useToast();
|
const { success: showSuccess, error: showError } = useToast();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -99,6 +101,7 @@ export default function StopEditForm({ stop, brands }: StopEditFormProps) {
|
|||||||
showSuccess("Stop updated", `${city}, ${state} has been saved`);
|
showSuccess("Stop updated", `${city}, ${state} has been saved`);
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
|
onSaved?.();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@/components/admin/design-system";
|
} from "@/components/admin/design-system";
|
||||||
import ScheduleImportModal from "@/components/admin/ScheduleImportModal";
|
import ScheduleImportModal from "@/components/admin/ScheduleImportModal";
|
||||||
import AddStopModal from "@/components/admin/AddStopModal";
|
import AddStopModal from "@/components/admin/AddStopModal";
|
||||||
|
import StopDetailModal from "@/components/admin/StopDetailModal";
|
||||||
|
|
||||||
type Stop = {
|
type Stop = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,6 +56,7 @@ export default function StopTableClient({ stops }: Props) {
|
|||||||
const [bulkPublishing, setBulkPublishing] = useState(false);
|
const [bulkPublishing, setBulkPublishing] = useState(false);
|
||||||
const [showImport, setShowImport] = useState(false);
|
const [showImport, setShowImport] = useState(false);
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const [openStopId, setOpenStopId] = useState<string | null>(null);
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -349,6 +351,7 @@ export default function StopTableClient({ stops }: Props) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleStopSelection(stop.id);
|
toggleStopSelection(stop.id);
|
||||||
}}
|
}}
|
||||||
|
onOpen={() => setOpenStopId(stop.id)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -369,6 +372,13 @@ export default function StopTableClient({ stops }: Props) {
|
|||||||
brandId={stops[0]?.brand_id ?? ""}
|
brandId={stops[0]?.brand_id ?? ""}
|
||||||
onSuccess={refresh}
|
onSuccess={refresh}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{openStopId && (
|
||||||
|
<StopDetailModal
|
||||||
|
stopId={openStopId}
|
||||||
|
onClose={() => setOpenStopId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -400,16 +410,16 @@ function StopRowBase({
|
|||||||
onDeleteError,
|
onDeleteError,
|
||||||
isSelected,
|
isSelected,
|
||||||
onToggleSelect,
|
onToggleSelect,
|
||||||
|
onOpen,
|
||||||
}: {
|
}: {
|
||||||
stop: Stop;
|
stop: Stop;
|
||||||
onDeleted: () => void;
|
onDeleted: () => void;
|
||||||
onDeleteError: (msg: string) => void;
|
onDeleteError: (msg: string) => void;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onToggleSelect: (e: React.MouseEvent) => void;
|
onToggleSelect: (e: React.MouseEvent) => void;
|
||||||
|
onOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
|
||||||
const { success: showSuccess, error: showError } = useToast();
|
const { success: showSuccess, error: showError } = useToast();
|
||||||
const [, startTransition] = useTransition();
|
|
||||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
@@ -418,10 +428,6 @@ function StopRowBase({
|
|||||||
const { date, weekday } = formatDate(stop.date);
|
const { date, weekday } = formatDate(stop.date);
|
||||||
const time = formatTime(stop.time);
|
const time = formatTime(stop.time);
|
||||||
|
|
||||||
function goToEdit() {
|
|
||||||
startTransition(() => router.push(`/admin/stops/${stop.id}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePublish() {
|
async function handlePublish() {
|
||||||
setPublishingId(stop.id);
|
setPublishingId(stop.id);
|
||||||
const result = await publishStop(stop.id, stop.brand_id);
|
const result = await publishStop(stop.id, stop.brand_id);
|
||||||
@@ -461,7 +467,7 @@ function StopRowBase({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
onClick={goToEdit}
|
onClick={onOpen}
|
||||||
className={`group cursor-pointer transition-colors ${isSelected ? "bg-emerald-50/60" : "hover:bg-[var(--admin-bg-subtle)]/60"}`}
|
className={`group cursor-pointer transition-colors ${isSelected ? "bg-emerald-50/60" : "hover:bg-[var(--admin-bg-subtle)]/60"}`}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-3.5" onClick={(e) => e.stopPropagation()}>
|
<td className="px-4 py-3.5" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
Reference in New Issue
Block a user