Compare commits
2 Commits
c1396096ad
...
eabc709076
| Author | SHA1 | Date | |
|---|---|---|---|
| eabc709076 | |||
| 4bd2ed0db2 |
@@ -29,15 +29,6 @@ jobs:
|
|||||||
npm run migrate:one
|
npm run migrate:one
|
||||||
node scripts/postflight-check.js
|
node scripts/postflight-check.js
|
||||||
|
|
||||||
- name: Seed Tuxedo Corn 2026 tour data
|
|
||||||
env:
|
|
||||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
||||||
run: |
|
|
||||||
# Use the batch-RPC Node script instead of raw psql — handles connection pooling
|
|
||||||
# and per-batch progress output. Skips the slow neon pooler path by replacing
|
|
||||||
# the pooler hostname with direct compute.
|
|
||||||
npx tsx scripts/import-tuxedo-stops.ts --no-clean 2>&1 || true
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Cleanup script: remove duplicate stops from the Tuxedo Corn 2026 tour.
|
||||||
|
* Keeps the row with the latest created_at per (brand_id, date, city, state, location, time).
|
||||||
|
* Run: DATABASE_URL="postgresql://..." npx tsx scripts/cleanup-duplicate-stops.ts
|
||||||
|
*/
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString:
|
||||||
|
process.env.DATABASE_ADMIN_URL ??
|
||||||
|
process.env.DATABASE_URL ??
|
||||||
|
"postgres://postgres:postgres@localhost:5432/route_commerce",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
|
// Find duplicate groups (same brand_id, date, city, state, location, time)
|
||||||
|
const dupes = await client.query(`
|
||||||
|
SELECT brand_id::text, date, city, state, location, time, count(*) as cnt, min(created_at::text) as oldest, max(created_at::text) as newest
|
||||||
|
FROM stops
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
GROUP BY brand_id, date, city, state, location, time
|
||||||
|
HAVING count(*) > 1
|
||||||
|
ORDER BY date, city
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Found ${dupes.rowCount} duplicate groups`);
|
||||||
|
if (dupes.rowCount === 0) {
|
||||||
|
await client.query("COMMIT");
|
||||||
|
console.log("No duplicates found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalDeleted = 0;
|
||||||
|
for (const row of dupes.rows) {
|
||||||
|
// Keep the row with the latest created_at, delete the rest
|
||||||
|
const del = await client.query(`
|
||||||
|
DELETE FROM stops
|
||||||
|
WHERE brand_id = $1
|
||||||
|
AND date = $2
|
||||||
|
AND city = $3
|
||||||
|
AND state = $4
|
||||||
|
AND location = $5
|
||||||
|
AND time IS NOT DISTINCT FROM $6
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND created_at < (
|
||||||
|
SELECT max(created_at) FROM stops s2
|
||||||
|
WHERE s2.brand_id = stops.brand_id
|
||||||
|
AND s2.date = stops.date
|
||||||
|
AND s2.city = stops.city
|
||||||
|
AND s2.state = stops.state
|
||||||
|
AND s2.location = stops.location
|
||||||
|
AND (s2.time IS NOT DISTINCT FROM stops.time)
|
||||||
|
AND s2.deleted_at IS NULL
|
||||||
|
)
|
||||||
|
`, [row.brand_id, row.date, row.city, row.state, row.location, row.time]);
|
||||||
|
totalDeleted += del.rowCount ?? 0;
|
||||||
|
console.log(` Deleted ${del.rowCount} duplicate(s) for ${row.city}, ${row.state} on ${row.date}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
console.log(`\nDone. Total duplicates removed: ${totalDeleted}`);
|
||||||
|
} catch (e) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,27 +1,11 @@
|
|||||||
import StopsDashboardClient from "@/components/admin/stops/StopsDashboardClient";
|
import StopsDashboardClient from "@/components/admin/stops/StopsDashboardClient";
|
||||||
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
||||||
import { supabase } from "@/lib/supabase";
|
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||||
import { PageHeader } from "@/components/admin/design-system";
|
import { PageHeader } from "@/components/admin/design-system";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { pool } from "@/lib/db";
|
||||||
interface Stop {
|
import { type Stop } from "@/components/admin/stops/types";
|
||||||
id: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
date: string;
|
|
||||||
time: string;
|
|
||||||
location: string;
|
|
||||||
active: boolean;
|
|
||||||
deleted_at: string | null;
|
|
||||||
brand_id: string;
|
|
||||||
address: string | null;
|
|
||||||
zip: string | null;
|
|
||||||
cutoff_time: string | null;
|
|
||||||
status: string;
|
|
||||||
brands: { name: string } | { name: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const StopIcon = () => (
|
const StopIcon = () => (
|
||||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
@@ -39,35 +23,56 @@ export default async function AdminStopsPage() {
|
|||||||
redirect("/admin/pickup");
|
redirect("/admin/pickup");
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = supabase
|
let stops: Stop[] = [];
|
||||||
.from("stops")
|
let error: { message: string } | null = null;
|
||||||
.select(`
|
|
||||||
id,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
date,
|
|
||||||
time,
|
|
||||||
location,
|
|
||||||
active,
|
|
||||||
deleted_at,
|
|
||||||
brand_id,
|
|
||||||
address,
|
|
||||||
zip,
|
|
||||||
cutoff_time,
|
|
||||||
status,
|
|
||||||
brands (
|
|
||||||
name
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.is("deleted_at", null)
|
|
||||||
.order("date", { ascending: true });
|
|
||||||
|
|
||||||
if (adminUser?.brand_id) {
|
try {
|
||||||
query = query.eq("brand_id", adminUser.brand_id);
|
const brandFilter = adminUser.brand_id
|
||||||
|
? `AND brand_id = '${adminUser.brand_id}'`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const { rows } = await pool.query<Stop& { brand_name: string }>(`
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.city,
|
||||||
|
s.state,
|
||||||
|
s.date,
|
||||||
|
s.time,
|
||||||
|
s.location,
|
||||||
|
s.status,
|
||||||
|
s.deleted_at,
|
||||||
|
s.brand_id,
|
||||||
|
s.address,
|
||||||
|
s.zip,
|
||||||
|
s.cutoff_time,
|
||||||
|
b.name as brand_name
|
||||||
|
FROM stops s
|
||||||
|
LEFT JOIN brands b ON b.id = s.brand_id
|
||||||
|
WHERE s.deleted_at IS NULL
|
||||||
|
${brandFilter}
|
||||||
|
ORDER BY s.date ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
stops = rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
city: r.city ?? "",
|
||||||
|
state: r.state ?? "",
|
||||||
|
date: r.date ? String(r.date) : "",
|
||||||
|
time: r.time ?? "",
|
||||||
|
location: r.location ?? "",
|
||||||
|
active: r.status === "active",
|
||||||
|
deleted_at: r.deleted_at,
|
||||||
|
brand_id: r.brand_id,
|
||||||
|
address: r.address,
|
||||||
|
zip: r.zip,
|
||||||
|
cutoff_time: r.cutoff_time,
|
||||||
|
status: r.status ?? "active",
|
||||||
|
brands: r.brand_name ? { name: r.brand_name } : { name: "" },
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
error = { message: e instanceof Error ? e.message : String(e) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: stops, error } = await query as unknown as { data: Stop[] | null; error: { message: string } | null };
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||||
@@ -107,7 +112,7 @@ export default async function AdminStopsPage() {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
||||||
<StopsDashboardClient stops={stops ?? []} />
|
<StopsDashboardClient stops={stops} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user