diff --git a/app/admin/verktyg/page.tsx b/app/admin/verktyg/page.tsx new file mode 100644 index 0000000..6aac6ce --- /dev/null +++ b/app/admin/verktyg/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useState } from "react"; +import { PortalShell } from "@/components/portal-shell"; + +export default function ToolsPage() { + return ( + +

+ Snabboppslag mot lagliga källor. Härkomst på varje träff. +

+
+ + +
+
+ ); +} + +function VehicleTool() { + const [plate, setPlate] = useState(""); + const [res, setRes] = useState | null>(null); + const [busy, setBusy] = useState(false); + async function lookup() { + if (!plate.trim()) return; + setBusy(true); setRes(null); + try { setRes(await (await fetch(`/api/vehicles?plate=${encodeURIComponent(plate)}`)).json()); } catch { setRes({ error: true }); } + setBusy(false); + } + const v = (res?.vehicle ?? null) as { make?: string; model?: string; year?: string; reg_plate?: string } | null; + return ( +
+
+

Fordon — RDW (NL)

+ +
+
+ setPlate(e.target.value)} placeholder="Registreringsnr (NL)" className="flex-1 border border-border bg-input px-3 py-2 font-mono text-xs uppercase outline-none focus:border-foreground" /> + +
+
+ {res === null ? — + : v ? ( +
+
{v.make} {v.model}
+
{v.reg_plate} · {v.year ?? "—"} · källa RDW
+
+ ) : Ingen träff (ej tillgänglig).} +
+
+ ); +} + +function ScreenTool() { + const [q, setQ] = useState(""); + const [matches, setMatches] = useState<{ name: string; score: number; schema: string }[] | null>(null); + const [busy, setBusy] = useState(false); + async function screen() { + if (!q.trim()) return; + setBusy(true); setMatches(null); + try { const d = await (await fetch(`/api/screen?q=${encodeURIComponent(q)}`)).json(); setMatches(d.matches ?? []); } catch { setMatches([]); } + setBusy(false); + } + return ( +
+
+

Sanktioner / PEP — OpenSanctions

+ +
+
+ setQ(e.target.value)} placeholder="Namn eller bolag" className="flex-1 border border-border bg-input px-3 py-2 font-mono text-xs outline-none focus:border-foreground" /> + +
+
+ {matches === null ? — + : matches.length === 0 ? Inga träffar. + : ( +
    + {matches.map((m, i) => ( +
  • + {m.name} + {m.schema}{m.score ? ` · ${Math.round(m.score * 100)}%` : ""} +
  • + ))} +
+ )} +

Kräver verifierade OpenSanctions-villkor/nyckel för fullständiga träffar.

+
+
+ ); +} diff --git a/app/bestall/page.tsx b/app/bestall/page.tsx index 001cc28..e548310 100644 --- a/app/bestall/page.tsx +++ b/app/bestall/page.tsx @@ -5,6 +5,7 @@ import { useMemo, useState, useEffect } from "react"; import { OrderStepper } from "@/components/order-stepper"; import { ProvenanceBadge } from "@/components/provenance-badge"; import { createOrder } from "@/lib/order-client"; +import { SERVICES, serviceById } from "@/lib/services"; import { MARKETS, ENTITY_TYPES, @@ -34,10 +35,11 @@ export default function ConfiguratorPage() { const [frequency, setFrequency] = useState("Engångs"); const [confidence, setConfidence] = useState(92); const [formats, setFormats] = useState(["CSV", "Tabellvy (NocoDB)"]); + const [service, setService] = useState("data"); const est = useMemo( - () => computeEstimate({ markets, type, fieldIds, enrichmentIds, volume, frequency, confidence }), - [markets, type, fieldIds, enrichmentIds, volume, frequency, confidence], + () => computeEstimate({ markets, type, fieldIds, enrichmentIds, volume, frequency, confidence, serviceMult: serviceById(service)?.mult ?? 1 }), + [markets, type, fieldIds, enrichmentIds, volume, frequency, confidence, service], ); // Persist so the deposit screen shows the same numbers. @@ -55,7 +57,7 @@ export default function ConfiguratorPage() { async function submit() { if (creating) return; setCreating(true); - const config = { markets, type, fieldIds, enrichmentIds, volume, frequency, confidence, formats }; + const config = { markets, type, fieldIds, enrichmentIds, volume, frequency, confidence, formats, service_type: service }; const id = await createOrder(frequency === "Engångs" ? "custom" : "subscription", config); router.push(id ? `/bestall/forskott?order=${encodeURIComponent(id)}` : "/bestall/forskott"); } @@ -129,6 +131,19 @@ export default function ConfiguratorPage() {
{/* LEFT — builder */}
+ +
+ {SERVICES.map((s) => ( + setService(s.id)}> + {s.name} + + ))} +
+ {serviceById(service) ? ( +

{serviceById(service)!.covers}

+ ) : null} +
+
{MARKETS.map((m) => ( @@ -280,6 +295,7 @@ export default function ConfiguratorPage() {
{[ + ["Tjänst", serviceById(service)?.name ?? service], ["Marknader", markets.join(" · ") || "—"], ["Typ", type], ["Fält", String(FIELDS.filter((f) => f.locked || fieldIds.includes(f.id)).length)], diff --git a/components/portal-shell.tsx b/components/portal-shell.tsx index a2b352c..68ca820 100644 --- a/components/portal-shell.tsx +++ b/components/portal-shell.tsx @@ -18,6 +18,7 @@ const ADMIN_NAV: NavItem[] = [ { label: "Översikt", href: "/admin" }, { label: "Källor", href: "/admin/sources" }, { label: "Jobb", href: "/admin/jobs" }, + { label: "Verktyg", href: "/admin/verktyg" }, { label: "Kunder", href: "/admin/customers" }, ]; diff --git a/lib/data.ts b/lib/data.ts index 1ba9195..656a36f 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -99,6 +99,7 @@ export interface EstimateInput { volume: number; frequency: Frequency; confidence: number; // target 80–99 + serviceMult?: number; // service-type price multiplier (see lib/services) } export interface Estimate { @@ -118,7 +119,8 @@ export function computeEstimate(input: EstimateInput): Estimate { const confidenceMult = 1 + Math.max(0, input.confidence - 90) * 0.03; const enrichWeight = ENRICHMENTS.filter((e) => input.enrichmentIds.includes(e.id)).reduce((s, e) => s + e.weight, 0); - const base = fieldWeight * markets * FIELD_RATE * (input.volume / 1000) * entityMult * confidenceMult; + const serviceMult = input.serviceMult ?? 1; + const base = fieldWeight * markets * FIELD_RATE * (input.volume / 1000) * entityMult * confidenceMult * serviceMult; const price = Math.round((base * (1 + enrichWeight)) / 5) * 5; // round to 5 kr const deposit = Math.round(price / 2); diff --git a/lib/orders-store.ts b/lib/orders-store.ts index 5b3d962..1a4026f 100644 --- a/lib/orders-store.ts +++ b/lib/orders-store.ts @@ -1,5 +1,6 @@ import { getSql } from "@/lib/db"; import { computeEstimate, type EntityType, type Frequency } from "@/lib/data"; +import { serviceById } from "@/lib/services"; export interface OrderConfig { markets?: string[]; @@ -10,6 +11,7 @@ export interface OrderConfig { frequency?: Frequency; confidence?: number; formats?: string[]; + service_type?: string; } export interface Order { @@ -37,6 +39,7 @@ function estimateFor(config: OrderConfig) { volume: config.volume ?? 1000, frequency: config.frequency ?? "Engångs", confidence: config.confidence ?? 92, + serviceMult: serviceById(config.service_type ?? "")?.mult ?? 1, }); }