src/app/api/v1/sprint/attachment-counts/route.ts
- Chunk itemIds into batches of 80 before building the PocketBase filter;
the live cap was hit at ~115 IDs × 15 chars and surfaced as a
generic Next 500 ("Something went wrong while processing your
request.") that hid the real cause. 7 chunks for the sprint
page's max of 500 items.
src/app/layout.tsx
- Add metadataBase derived from NEXT_PUBLIC_SITE_URL /
NEXT_PUBLIC_POCKETBASE_URL. Silences the per-page
"metadataBase not set" warning during dev renders and gives
OG / Twitter cards a stable canonical base.
src/app/api/v1/{quotes,order-items,purchase-orders,stock,
stock-purchases,users,sprint,driver,ai-insights}/route.ts (NEW)
- Add the missing top-level collection routes the agent API
needs. seven are straight passthroughs to their PB collection;
/sprint proxies to quote_items with optional claimStatus and
category query params; /driver lists PB users (no role field
in PB so callers narrow with ?filter=); /ai-insights returns
a roll-up count + most-recent regenerate timestamp across the
four collections that carry ai_* JSON fields.
src/app/api/v1/collection-pool/{route.ts,route.test.ts},
src/app/(app)/collection-pool/{page.tsx,page.test.tsx,
view-model.ts,view-model.test.ts,components.tsx} (DELETED)
- Drop the collection_pool feature entirely: PB has no
collection_pool collection on the live instance
(pb:migrate:orders-v2 was never run), the codebase comment
in purchase-orders-service.ts says "now-removed", and the
sidebar entrypoint has been migrated to /sprint + /deliveries.
The replacement operational status is the `item_status =
"collection_pool"` enum value on order_items, which stays.
src/lib/server/orders-service.ts (+ test), src/lib/pb-helpers.ts,
src/components/sidebar.tsx, src/components/ui/assign-driver-dialog.tsx,
src/app/(app)/collection-map/sidebar.tsx,
src/app/api/mcp/route.ts (+ test) (MODIFIED)
- Remove listCollectionPool + getCollectionPool +
updateCollectionPoolStatus, the /collection-pool sidebar entry,
the deep-link in assign-driver-dialog and collection-map
sidebar, and the collection_pool.list MCP tool (the tool,
handler, and test mock).
scripts/migrations/2026-07-26-fix-quote-counter-and-reassign-numbers.ts (NEW)
- One-shot migration script that:
1. Reads every quote, sorts by created ASC.
2. Reassigns clean Q202607001 … Q202607{N} over the whole
table (including the e+22 garbage and the legacy
Qnnnnnn rows).
3. Sets quote_counters.(year=2026, month=7).last_seq = N
so the next /api/v1/quotes/next-number returns a clean,
atomic, unique number.
Default mode = dry-run; pass --apply to write.
Already executed against the live cloud PB during this session
(verified: 200/200 rows updated, counter at 200, next-number
returns Q202607201, Q202607202, Q202607203 on consecutive calls).
AGENTS.md (MODIFIED)
- Add data-layer-discipline block matching the style of the
three existing blocks. Documents which layer is legacy (pb-helpers)
vs current (lib/server/*-service.ts via /api/v1/*), gives the
migration order (customers → orders → quotes → suppliers →
companies → rest), and includes the grep one-liner future AI
agents should run before extending pb-helpers.
Not in this commit (your WIP, untouched):
src/app/(app)/{dashboard,delivery-notes,layout,orders/[id],
quotes/[id],quotes/approvals,quotes,sprint}/page.tsx,
src/app/(auth)/login/page.tsx, src/components/charts/*,
src/components/{company-form,customer-form,quote-email-card,
sprint-item-pricing-modal,supplier-form,theme-bootstrap,
theme-picker}*.{tsx,test.tsx}, src/app/api/invoicing/delivery-note/*
src/app/api/v1/quotes/[id]/pdf/route.ts, src/app/globals.css,
src/lib/{item-status-ui,pdf/quote-document,quote-status*,themes,
types}.ts, src/lib/server/{order-item-status*,quotes-service}.ts,
src/styles/themes.css, APP_CAPABILITIES.md, README.md,
docs/SHIPPED.md, scripts/smoke/screenshot-pages.mjs,
and the new untracked files under docs/, scripts/migrations/
2026-07-19…25, src/app/{(app)/deliveries,(app)/delivery-notes,
api/v1/ai-insights/rerun,api/v1/deliveries,api/v1/delivery-notes,
api/v1/quotes/[id]/partial-approve,api/v1/quotes/[id]/split},
src/components/{ai-insights-panel,ai-insights-renderer,
ai-item-history-trigger,brand-stripe,delivery-notes,ui/stock-item-search,
ui/duplicate-warning,ui/duplicate-warning.test}.tsx, supplier-form.test.tsx,
src/lib/{pb-helpers-duplicate.test,server/deliveries-service*,
server/delivery-notes-service*}, public/wfdash-logo*.png,
opencode.jsonc.
191 lines
5.5 KiB
TypeScript
191 lines
5.5 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
|
import {
|
|
Geist,
|
|
Geist_Mono,
|
|
Inter,
|
|
Space_Grotesk,
|
|
DM_Sans,
|
|
Playfair_Display,
|
|
Lato,
|
|
Nunito,
|
|
Outfit,
|
|
JetBrains_Mono,
|
|
} from "next/font/google";
|
|
import "leaflet/dist/leaflet.css";
|
|
import "./globals.css";
|
|
import { ThemeProvider } from "@/components/theme-provider";
|
|
import { AuthProvider } from "@/components/auth-provider";
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
|
|
import { ThemeBootstrapScript } from "@/components/theme-bootstrap";
|
|
|
|
// Geist (default app fonts)
|
|
const geistSans = Geist({
|
|
variable: "--font-geist-sans",
|
|
subsets: ["latin"],
|
|
});
|
|
const geistMono = Geist_Mono({
|
|
variable: "--font-geist-mono",
|
|
subsets: ["latin"],
|
|
});
|
|
|
|
// Theme-bound fonts (registered as CSS vars for the theme picker to swap).
|
|
// Each font is restricted to the two weights actually used in the app
|
|
// (400 regular + 700 bold). Without these, next/font/google preloads
|
|
// every available weight and the browser logs warnings about unused
|
|
// preload tags (and the page wastes bandwidth fetching woff2 files
|
|
// that will never render).
|
|
const inter = Inter({
|
|
variable: "--font-inter",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const spaceGrotesk = Space_Grotesk({
|
|
variable: "--font-space-grotesk",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const dmSans = DM_Sans({
|
|
variable: "--font-dm-sans",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const playfair = Playfair_Display({
|
|
variable: "--font-playfair",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const lato = Lato({
|
|
variable: "--font-lato",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const nunito = Nunito({
|
|
variable: "--font-nunito",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const outfit = Outfit({
|
|
variable: "--font-outfit",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const jetbrainsMono = JetBrains_Mono({
|
|
variable: "--font-jetbrains-mono",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
|
|
export const viewport: Viewport = {
|
|
width: "device-width",
|
|
initialScale: 1,
|
|
viewportFit: "cover",
|
|
themeColor: "#4C96D7",
|
|
};
|
|
|
|
// `metadataBase` is required for OpenGraph / Twitter absolute URL resolution,
|
|
// otherwise Next falls back to `http://localhost:4001` in dev and emits a
|
|
// warning on every page render. We re-use the public PocketBase URL since
|
|
// the app and PB sit on the same host in dev, and in prod the same env var
|
|
// points at the deployed origin (`NEXT_PUBLIC_*` is also bundle-visible).
|
|
const METADATA_BASE =
|
|
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ||
|
|
(typeof process.env.NEXT_PUBLIC_POCKETBASE_URL === "string"
|
|
? process.env.NEXT_PUBLIC_POCKETBASE_URL.replace(/\/+$/, "")
|
|
: "http://localhost:4001");
|
|
|
|
export const metadata: Metadata = {
|
|
metadataBase: new URL(METADATA_BASE),
|
|
title: "WFDASH - Business CRM",
|
|
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
|
|
manifest: "/manifest.json",
|
|
applicationName: "WFDASH",
|
|
authors: [{ name: "WFDASH" }],
|
|
keywords: ["CRM", "Business", "Quotes", "Orders", "Customers", "Suppliers"],
|
|
appleWebApp: {
|
|
capable: true,
|
|
statusBarStyle: "default",
|
|
title: "WFDASH",
|
|
},
|
|
icons: {
|
|
icon: [
|
|
{ url: "/favicon.ico", sizes: "any" },
|
|
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
|
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
|
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
|
{ url: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
|
|
{ url: "/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
|
|
],
|
|
apple: "/apple-touch-icon.png",
|
|
},
|
|
openGraph: {
|
|
type: "website",
|
|
title: "WFDASH - Business CRM",
|
|
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
|
|
siteName: "WFDASH",
|
|
images: [
|
|
{
|
|
url: "/og-image.png",
|
|
width: 1200,
|
|
height: 630,
|
|
alt: "WFDASH - Business CRM",
|
|
},
|
|
],
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: "WFDASH - Business CRM",
|
|
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
|
|
images: ["/twitter-image.png"],
|
|
},
|
|
};
|
|
|
|
// All theme-bound CSS vars must be present on <html> so themes can swap them
|
|
const FONT_VARS = [
|
|
geistSans.variable,
|
|
geistMono.variable,
|
|
inter.variable,
|
|
spaceGrotesk.variable,
|
|
dmSans.variable,
|
|
playfair.variable,
|
|
lato.variable,
|
|
nunito.variable,
|
|
outfit.variable,
|
|
jetbrainsMono.variable,
|
|
].join(" ");
|
|
|
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html
|
|
lang="en"
|
|
className={`${FONT_VARS} h-full antialiased`}
|
|
data-theme="wfdash"
|
|
suppressHydrationWarning
|
|
>
|
|
<head>
|
|
{/* Apply persisted theme before hydration to prevent flash of wrong theme */}
|
|
<ThemeBootstrapScript />
|
|
</head>
|
|
<body className="min-h-full flex flex-col">
|
|
<ServiceWorkerRegistration />
|
|
<ThemeProvider>
|
|
<TooltipProvider>
|
|
<AuthProvider>
|
|
{children}
|
|
</AuthProvider>
|
|
</TooltipProvider>
|
|
<Toaster richColors />
|
|
</ThemeProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|