0cf2ee7948
Deploy to route.crispygoat.com / deploy (push) Failing after 3m13s
- Fetch dashboard stats server-side in admin/page.tsx to eliminate client-side useEffect waterfall and the '—' placeholder flash - Pass pre-fetched stats as props to DashboardClient - Lazy-load UpgradePlanModal via next/dynamic (only loads when needed) - Redesign stats cards with compact layout, smaller icons, Fraunces serif values - Improve Quick Actions + Usage row: side-by-side on desktop, stacked mobile - Clean up Recent Orders as a proper list with icon/name/time/amount/badge - Add staggered fade-up entrance animations (0/60/120/180/240ms delays) - Consolidate loading.tsx skeleton to match actual dashboard structure - Mobile-responsive: 2-col stats on mobile, stacked usage, collapsible header
192 lines
7.2 KiB
JavaScript
192 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Seed Tuxedo Corn 2026 tour: locations + stops.
|
|
*
|
|
* Uses sql`...` tagged template from @neondatabase/serverless (WebSocket mode)
|
|
* for INSERTs — this is the only Neon serverless API that actually executes writes.
|
|
*
|
|
* Run with:
|
|
* node scripts/seed-tuxedo-2026.js
|
|
*
|
|
* Requires DATABASE_URL in .env.local (or pass explicitly).
|
|
*/
|
|
require("dotenv").config({ path: ".env.local" });
|
|
const { neon } = require("@neondatabase/serverless");
|
|
const ExcelJS = require("exceljs");
|
|
const path = require("path");
|
|
|
|
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
|
const YEAR = 2026;
|
|
|
|
const url = process.env.DATABASE_URL;
|
|
if (!url) {
|
|
console.error("❌ DATABASE_URL not set in .env.local");
|
|
process.exit(1);
|
|
}
|
|
|
|
const cleanUrl = url.replace(/[?&]channel_binding=require/gi, "").trim();
|
|
// Use direct compute endpoint with WebSocket for full DDL + DML support
|
|
const directUrl = cleanUrl.replace("-pooler.", ".");
|
|
const sql = neon(directUrl, { webSocket: true });
|
|
|
|
const MONTH_MAP = {
|
|
Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06",
|
|
Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12",
|
|
};
|
|
|
|
function parseExcelDate(s) {
|
|
if (!s) return null;
|
|
const m = /^([A-Za-z]{3})\s+(\d{1,2})$/.exec(String(s).trim());
|
|
if (!m) return null;
|
|
const mm = MONTH_MAP[m[1]];
|
|
if (!mm) return null;
|
|
return `${YEAR}-${mm}-${parseInt(m[2], 10).toString().padStart(2, "0")}`;
|
|
}
|
|
|
|
function parseTimeRange(s) {
|
|
if (!s) return "";
|
|
return String(s).replace(/[–—]/g, "-").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
async function parseXlsx(xlsxPath) {
|
|
const workbook = new ExcelJS.Workbook();
|
|
await workbook.xlsx.readFile(xlsxPath);
|
|
|
|
// --- Locations from Stop Directory ---
|
|
const locSheet = workbook.getWorksheet("Stop Directory");
|
|
const locations = [];
|
|
const seenLoc = new Set();
|
|
|
|
if (locSheet) {
|
|
locSheet.eachRow((row, rowNum) => {
|
|
if (rowNum === 1) return;
|
|
// Column mapping via getCell():
|
|
// col1=Truck, col2=City, col3=State, col4=Host(venue name), col5=Address, col6=Phone, col7=Contact, col12=Notes
|
|
const rawCity = String(row.getCell(2).value || "").trim();
|
|
const rawState = String(row.getCell(3).value || "").trim();
|
|
const rawName = String(row.getCell(4).value || "").trim();
|
|
const rawAddress = String(row.getCell(5).value || "").trim();
|
|
if (!rawCity || !rawName) return;
|
|
const key = `${rawName}|${rawCity}|${rawState}`;
|
|
if (seenLoc.has(key)) return;
|
|
seenLoc.add(key);
|
|
locations.push({
|
|
name: rawName,
|
|
address: rawAddress || null,
|
|
city: rawCity,
|
|
state: rawState,
|
|
zip: null,
|
|
phone: String(row.getCell(6).value || "").trim() || null,
|
|
contact_name: String(row.getCell(7).value || "").trim() || null,
|
|
contact_email: null,
|
|
notes: String(row.getCell(12).value || "").trim() || null,
|
|
active: true,
|
|
});
|
|
});
|
|
}
|
|
console.log(`Parsed ${locations.length} locations`);
|
|
|
|
// --- Stops from Full Schedule ---
|
|
const schedSheet = workbook.getWorksheet("Full Schedule");
|
|
const stops = [];
|
|
const venueAddressMap = new Map();
|
|
|
|
if (locSheet) {
|
|
locSheet.eachRow((row, rowNum) => {
|
|
if (rowNum === 1) return;
|
|
const name = String(row.getCell(4).value || "").trim();
|
|
const address = String(row.getCell(5).value || "").trim();
|
|
if (name && address) venueAddressMap.set(name, address);
|
|
});
|
|
}
|
|
|
|
if (schedSheet) {
|
|
schedSheet.eachRow((row) => {
|
|
const rowValues = row.values || [];
|
|
// Column mapping (1-indexed, col1=null):
|
|
// col2=Wk, col3=Type, col4=Date, col5=Day, col6=City/Location, col7=Host/Venue, col8=Time, col9=Truck, col10=Status, col11=Notes
|
|
const firstCell = String(rowValues[1] || "").trim();
|
|
// Skip title rows, week headers ("Wk 1"), OFF, Cross-Dock
|
|
if (!firstCell || /^Wk\s*\d+/i.test(firstCell) || firstCell === "OFF" || firstCell === "Cross-Dock") return;
|
|
|
|
const dateStr = parseExcelDate(rowValues[3]);
|
|
if (!dateStr) return;
|
|
|
|
const cityStateRaw = String(rowValues[5] || "").trim();
|
|
const cityParts = cityStateRaw.split(",").map((s) => s.trim());
|
|
const city = cityParts[0] || "";
|
|
const state = cityParts[1] || "";
|
|
const location = String(rowValues[6] || "").trim();
|
|
if (!city || !state || !location) return;
|
|
|
|
const rawTime = rowValues[7];
|
|
const time = rawTime ? parseTimeRange(rawTime) : "";
|
|
const truck = String(rowValues[8] || "").trim();
|
|
const statusText = String(rowValues[9] || "").trim();
|
|
const notesText = String(rowValues[10] || "").trim();
|
|
|
|
const venueAddress = venueAddressMap.get(location) || null;
|
|
const notes = [truck ? `Truck: ${truck}` : "", statusText, notesText].filter(Boolean).join(" | ");
|
|
|
|
stops.push({
|
|
name: `${location} - ${city}, ${state}`,
|
|
location,
|
|
address: venueAddress,
|
|
city,
|
|
state,
|
|
zip: null,
|
|
date: dateStr,
|
|
time,
|
|
cutoff_date: null,
|
|
status: "active",
|
|
is_public: true,
|
|
notes: notes || null,
|
|
});
|
|
});
|
|
}
|
|
console.log(`Parsed ${stops.length} stops`);
|
|
return { locations, stops };
|
|
}
|
|
|
|
async function seedLocations(locs) {
|
|
if (locs.length === 0) return;
|
|
await sql`DELETE FROM locations WHERE brand_id = ${TUXEDO_BRAND_ID}::uuid`;
|
|
for (const loc of locs) {
|
|
await sql`INSERT INTO locations (brand_id, name, address, city, state, zip, phone, contact_name, contact_email, notes, active)
|
|
VALUES (${TUXEDO_BRAND_ID}::uuid, ${loc.name}, ${loc.address}, ${loc.city}, ${loc.state}, ${loc.zip}, ${loc.phone}, ${loc.contact_name}, ${loc.contact_email}, ${loc.notes}, ${loc.active})`;
|
|
}
|
|
console.log(`Seeded ${locs.length} locations`);
|
|
}
|
|
|
|
async function seedStops(stops) {
|
|
if (stops.length === 0) return;
|
|
const minDate = `${YEAR}-07-01`;
|
|
const maxDate = `${YEAR}-09-30`;
|
|
await sql`DELETE FROM stops WHERE brand_id = ${TUXEDO_BRAND_ID}::uuid AND date >= ${minDate} AND date <= ${maxDate}`;
|
|
for (const stop of stops) {
|
|
await sql`INSERT INTO stops (brand_id, name, location, address, city, state, zip, date, "time", cutoff_date, status, is_public, notes)
|
|
VALUES (${TUXEDO_BRAND_ID}::uuid, ${stop.name}, ${stop.location}, ${stop.address}, ${stop.city}, ${stop.state}, ${stop.zip}, ${stop.date}, ${stop.time}, ${stop.cutoff_date}, ${stop.status}, ${stop.is_public}, ${stop.notes})`;
|
|
}
|
|
console.log(`Seeded ${stops.length} stops`);
|
|
}
|
|
|
|
async function main() {
|
|
const xlsxPath = path.join(__dirname, "..", "Tuxedo_Corn_2026_Tour_Schedule-3.xlsx");
|
|
console.log("Parsing:", xlsxPath);
|
|
const { locations, stops } = await parseXlsx(xlsxPath);
|
|
if (locations.length === 0 && stops.length === 0) {
|
|
console.error("❌ No data parsed from xlsx");
|
|
process.exit(1);
|
|
}
|
|
console.log(`Seeding ${locations.length} locations + ${stops.length} stops for Tuxedo Corn...`);
|
|
await seedLocations(locations);
|
|
await seedStops(stops);
|
|
const locCount = await sql.query(`SELECT COUNT(*) FROM locations WHERE brand_id = '${TUXEDO_BRAND_ID}'`);
|
|
const stopCount = await sql.query(`SELECT COUNT(*) FROM stops WHERE brand_id = '${TUXEDO_BRAND_ID}'`);
|
|
console.log(`\n✅ Done — ${locCount[0].count} locations, ${stopCount[0].count} stops in DB`);
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error("❌ Seed failed:", e.message);
|
|
process.exit(1);
|
|
}); |