Produktseite ausgebaut: Unterseiten und Bildschirmfotos aus der App
Neue öffentliche Routen unter src/app/(marketing): Übersicht /funktionen, sechs Detailseiten (planung, baustelle, zeiterfassung, berichte, abrechnung, lotse), /sicherheit und /preise, gemeinsames Gerüst mit Kopf- und Fußzeile. scripts/marketing-shots.ts nimmt die Bilder direkt aus der laufenden App auf (Playwright über den installierten Chrome, Session wie im Smoke-Test, ohne Passworteingabe); 14 Aufnahmen in public/marketing ersetzen die gezeichneten Vorschauen. Plantafel: „Vorschläge" hängt jetzt als Fußzeile in der Auftragskarte statt frei zwischen zwei Karten zu stehen (neue Prop footer an OrderCard, Schlüssel unplanned.suggestionsFor). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ArrowRight, Check } from "lucide-react";
|
||||
import type { Shot } from "@/lib/marketing/pages";
|
||||
|
||||
/** Bausteine der öffentlichen Produktseiten. Bildschirmfotos stammen aus dem Demo-Mandanten. */
|
||||
|
||||
const SIZE = {
|
||||
desktop: { width: 2880, height: 1800, sizes: "(min-width: 1024px) 900px, 100vw" },
|
||||
mobile: { width: 1170, height: 2532, sizes: "(min-width: 1024px) 320px, 60vw" },
|
||||
} as const;
|
||||
|
||||
export function ShotFrame({ shot, caption, priority }: { shot: Shot; caption: string; priority?: boolean }) {
|
||||
const s = SIZE[shot.kind];
|
||||
return (
|
||||
<figure className={shot.kind === "mobile" ? "mx-auto w-full max-w-[280px]" : "w-full"}>
|
||||
<div className="shadow-card overflow-hidden rounded-xl border bg-card">
|
||||
<Image src={`/marketing/${shot.src}.png`} alt={caption} width={s.width} height={s.height} sizes={s.sizes} priority={priority} className="h-auto w-full" />
|
||||
</div>
|
||||
<figcaption className="mt-2 text-[12.5px] text-muted-foreground">{caption}</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export function PointList({ points }: { points: string[] }) {
|
||||
return (
|
||||
<ul className="grid gap-2.5">
|
||||
{points.map((p) => (
|
||||
<li key={p} className="flex items-start gap-2 text-[14.5px]">
|
||||
<Check className="mt-0.5 size-[18px] shrink-0 text-[var(--ui-primary)]" aria-hidden />
|
||||
{p}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHero({ eyebrow, title, lead }: { eyebrow?: string; title: string; lead: string }) {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 pt-12 pb-8 text-center sm:px-6">
|
||||
{eyebrow && <p className="text-[13px] font-semibold tracking-wide text-[var(--ui-accent)] uppercase">{eyebrow}</p>}
|
||||
<h1 className="mt-2 font-heading text-[30px] leading-tight font-bold text-[var(--ui-primary)] sm:text-[40px]">{title}</h1>
|
||||
<p className="mt-4 text-[16px] leading-relaxed text-muted-foreground">{lead}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function CtaBand() {
|
||||
const t = await getTranslations("marketing.cta");
|
||||
return (
|
||||
<section aria-labelledby="cta-title" className="border-t bg-card">
|
||||
<div className="mx-auto max-w-4xl px-4 py-14 text-center sm:px-6">
|
||||
<h2 id="cta-title" className="font-heading text-[26px] font-semibold text-[var(--ui-primary)]">{t("title")}</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-[15.5px] text-muted-foreground">{t("text")}</p>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/testen"
|
||||
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-lg bg-cta px-5 text-[15px] font-semibold text-cta-foreground hover:brightness-95"
|
||||
>
|
||||
{t("primary")}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
<Link href="/funktionen" className="inline-flex min-h-12 items-center justify-center gap-2 rounded-lg border px-5 text-[15px] font-semibold hover:bg-muted">
|
||||
{t("secondary")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -24,9 +24,8 @@ import {
|
||||
WifiOff,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { BRAND } from "@/lib/brand";
|
||||
import { BoardMockup, FieldMockup, LotseMockup } from "./mockups";
|
||||
import { ShotFrame } from "./blocks";
|
||||
import { FieldMockup } from "./mockups";
|
||||
|
||||
/**
|
||||
* Öffentliche Startseite (Produktseite). Wird von `/` gerendert, solange keine Session besteht;
|
||||
@@ -58,7 +57,6 @@ const SECURITY: { key: string; icon: LucideIcon }[] = [
|
||||
{ key: "item6", icon: EyeOff },
|
||||
];
|
||||
|
||||
const NAV = ["features", "planning", "mobile", "security", "pricing", "faq"] as const;
|
||||
const FAQ = ["q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8"] as const;
|
||||
const INDUSTRIES = ["shk", "electric", "cooling", "lifts", "windows", "service"] as const;
|
||||
|
||||
@@ -71,35 +69,7 @@ export async function MarketingHome({ contactEmail }: { contactEmail?: string })
|
||||
const t = await getTranslations("marketing");
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<header className="sticky top-0 z-20 border-b bg-background/95 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-6xl items-center gap-4 px-4 py-3 sm:px-6">
|
||||
<Link href="/" className="flex min-h-11 items-center" aria-label={BRAND.name}>
|
||||
<CraftviaLogo variant="horizontal" height={30} />
|
||||
</Link>
|
||||
<nav aria-label={t("nav.label")} className="ml-auto hidden lg:block">
|
||||
<ul className="flex items-center gap-1">
|
||||
{NAV.map((key) => (
|
||||
<li key={key}>
|
||||
<a href={`#${key}`} className="inline-flex min-h-11 items-center rounded-md px-3 text-[14px] font-medium text-muted-foreground hover:text-foreground">
|
||||
{t(`nav.${key}`)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Link href="/login" className="inline-flex min-h-11 items-center px-2 text-[14px] font-semibold text-[var(--ui-primary)]">
|
||||
{t("nav.login")}
|
||||
</Link>
|
||||
<Link href="/testen" className="inline-flex min-h-11 items-center rounded-lg bg-cta px-4 text-[14px] font-semibold text-cta-foreground hover:brightness-95">
|
||||
{t("nav.cta")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1">
|
||||
<>
|
||||
{/* Hero */}
|
||||
<section className="border-b bg-card">
|
||||
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-12 sm:px-6 lg:grid-cols-[1.1fr_0.9fr] lg:items-center lg:py-20">
|
||||
@@ -125,7 +95,7 @@ export async function MarketingHome({ contactEmail }: { contactEmail?: string })
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<BoardMockup />
|
||||
<ShotFrame shot={{ src: "plantafel", kind: "desktop" }} caption={t("pages.planung.shot1")} priority />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -177,7 +147,7 @@ export async function MarketingHome({ contactEmail }: { contactEmail?: string })
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<BoardMockup />
|
||||
<ShotFrame shot={{ src: "live-lage", kind: "desktop" }} caption={t("pages.planung.shot2")} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -206,7 +176,7 @@ export async function MarketingHome({ contactEmail }: { contactEmail?: string })
|
||||
<p className="mt-3 text-[15px] leading-relaxed text-muted-foreground">{t("lotse.text")}</p>
|
||||
<p className="mt-4 text-[13.5px] text-muted-foreground">{t("lotse.note")}</p>
|
||||
</div>
|
||||
<LotseMockup />
|
||||
<ShotFrame shot={{ src: "mobil-lotse", kind: "mobile" }} caption={t("pages.lotse.shot1")} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -324,34 +294,6 @@ export async function MarketingHome({ contactEmail }: { contactEmail?: string })
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="border-t bg-card">
|
||||
<div className="mx-auto grid max-w-6xl gap-8 px-4 py-10 sm:px-6 md:grid-cols-[1.4fr_1fr_1fr]">
|
||||
<div>
|
||||
<CraftviaLogo variant="horizontal" height={28} />
|
||||
<p className="mt-3 max-w-xs text-[13.5px] text-muted-foreground">{t("footer.tagline")}</p>
|
||||
</div>
|
||||
<nav aria-labelledby="footer-product">
|
||||
<h2 id="footer-product" className="text-[13px] font-semibold tracking-wide uppercase">{t("footer.product")}</h2>
|
||||
<ul className="mt-2 grid">
|
||||
<li><a href="#features" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.features")}</a></li>
|
||||
<li><Link href="/testen" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.trial")}</Link></li>
|
||||
<li><Link href="/login" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.login")}</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav aria-labelledby="footer-legal">
|
||||
<h2 id="footer-legal" className="text-[13px] font-semibold tracking-wide uppercase">{t("footer.company")}</h2>
|
||||
<ul className="mt-2 grid">
|
||||
<li><Link href="/testen/nutzungsbedingungen" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.terms")}</Link></li>
|
||||
<li><Link href="/testen/datenschutz" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.privacy")}</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<p className="border-t px-4 py-4 text-center text-[12.5px] text-muted-foreground sm:px-6">
|
||||
© {new Date().getFullYear()} {BRAND.name} · {BRAND.tagline} · {t("footer.rights")}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,11 @@
|
||||
import { Check, MapPin, PenLine, Truck, Wrench } from "lucide-react";
|
||||
import { Check, PenLine, Truck, Wrench } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
/**
|
||||
* Abstrakte Produktvorschauen für die Startseite — bewusst als Markup statt Screenshot:
|
||||
* Ablaufdarstellung für die Startseite — bewusst als Markup statt Screenshot:
|
||||
* bleibt scharf, mehrsprachig, ohne Bildassets und ohne Daten aus einem echten Mandanten.
|
||||
*/
|
||||
|
||||
const BAR = "h-2 rounded-full";
|
||||
|
||||
export async function BoardMockup() {
|
||||
const t = await getTranslations("marketing.hero.mockup");
|
||||
const rows = [
|
||||
{ crew: t("crewNorth"), load: t("load"), percent: 94, jobs: [t("job1"), t("job2")], tone: "var(--ok)" },
|
||||
{ crew: t("crewSouth"), load: t("conflict"), percent: 113, jobs: [t("job3")], tone: "var(--warn)" },
|
||||
];
|
||||
return (
|
||||
<figure aria-label={t("label")} className="shadow-card rounded-2xl border bg-card p-4">
|
||||
<figcaption className="mb-3 flex items-center justify-between">
|
||||
<span className="font-heading text-[15px] font-semibold">{t("board")}</span>
|
||||
<span className="rounded-full border px-2 py-0.5 text-[11px] font-semibold text-muted-foreground">{t("today")}</span>
|
||||
</figcaption>
|
||||
<div className="space-y-3">
|
||||
{rows.map((row) => (
|
||||
<div key={row.crew} className="rounded-xl border p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-[13px] font-semibold">{row.crew}</span>
|
||||
<span className="text-[11px] font-semibold" style={{ color: row.tone }}>
|
||||
{row.load}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 w-full rounded-full bg-muted" role="presentation">
|
||||
<div className={BAR} style={{ width: `${Math.min(row.percent, 100)}%`, background: row.tone }} />
|
||||
</div>
|
||||
<ul className="mt-2 flex flex-wrap gap-1.5">
|
||||
{row.jobs.map((job) => (
|
||||
<li key={job} className="rounded-md border bg-background px-2 py-1 text-[11.5px]">
|
||||
{job}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
|
||||
<MapPin className="size-3.5" aria-hidden />
|
||||
{t("live")}
|
||||
</p>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export async function FieldMockup() {
|
||||
const t = await getTranslations("marketing.mobile");
|
||||
const steps = [
|
||||
@@ -78,28 +34,3 @@ export async function FieldMockup() {
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
export async function LotseMockup() {
|
||||
const t = await getTranslations("marketing.lotse");
|
||||
return (
|
||||
<div className="shadow-card space-y-3 rounded-2xl border bg-card p-4">
|
||||
<p className="ml-auto max-w-[85%] rounded-2xl rounded-br-sm bg-[var(--ui-primary)] px-3 py-2 text-[13.5px] text-white">{t("chatUser")}</p>
|
||||
<p className="max-w-[90%] rounded-2xl rounded-bl-sm border bg-background px-3 py-2 text-[13.5px]">{t("chatAssistant")}</p>
|
||||
<div className="rounded-xl border border-l-4 border-l-[var(--ui-accent)] bg-background p-3">
|
||||
<p className="text-[12px] font-semibold text-muted-foreground uppercase">{t("cardTitle")}</p>
|
||||
<ul className="mt-1.5 space-y-1 text-[13.5px]">
|
||||
{[t("cardLine1"), t("cardLine2"), t("cardLine3")].map((line) => (
|
||||
<li key={line} className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 size-4 shrink-0 text-[var(--ok)]" aria-hidden />
|
||||
{line}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<span className="rounded-lg bg-cta px-3 py-1.5 text-[13px] font-semibold text-cta-foreground">{t("cardConfirm")}</span>
|
||||
<span className="rounded-lg border px-3 py-1.5 text-[13px] font-semibold">{t("cardDiscard")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
||||
import { BRAND } from "@/lib/brand";
|
||||
|
||||
/** Gemeinsames Gerüst der öffentlichen Produktseiten: Kopfzeile, Inhalt, Fußzeile. */
|
||||
|
||||
const NAV = [
|
||||
{ key: "features", href: "/funktionen" },
|
||||
{ key: "planning", href: "/funktionen/planung" },
|
||||
{ key: "mobile", href: "/funktionen/baustelle" },
|
||||
{ key: "security", href: "/sicherheit" },
|
||||
{ key: "pricing", href: "/preise" },
|
||||
] as const;
|
||||
|
||||
export async function MarketingShell({ children }: { children: React.ReactNode }) {
|
||||
const t = await getTranslations("marketing");
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<header className="sticky top-0 z-20 border-b bg-background/95 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-6xl items-center gap-4 px-4 py-3 sm:px-6">
|
||||
<Link href="/" className="flex min-h-11 items-center" aria-label={BRAND.name}>
|
||||
<CraftviaLogo variant="horizontal" height={30} />
|
||||
</Link>
|
||||
<nav aria-label={t("nav.label")} className="ml-auto hidden lg:block">
|
||||
<ul className="flex items-center gap-1">
|
||||
{NAV.map((item) => (
|
||||
<li key={item.key}>
|
||||
<Link href={item.href} className="inline-flex min-h-11 items-center rounded-md px-3 text-[14px] font-medium text-muted-foreground hover:text-foreground">
|
||||
{t(`nav.${item.key}`)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Link href="/login" className="inline-flex min-h-11 items-center px-2 text-[14px] font-semibold text-[var(--ui-primary)]">
|
||||
{t("nav.login")}
|
||||
</Link>
|
||||
<Link href="/testen" className="inline-flex min-h-11 items-center rounded-lg bg-cta px-4 text-[14px] font-semibold text-cta-foreground hover:brightness-95">
|
||||
{t("nav.cta")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1">{children}</main>
|
||||
|
||||
<footer className="border-t bg-card">
|
||||
<div className="mx-auto grid max-w-6xl gap-8 px-4 py-10 sm:px-6 md:grid-cols-[1.4fr_1fr_1fr]">
|
||||
<div>
|
||||
<CraftviaLogo variant="horizontal" height={28} />
|
||||
<p className="mt-3 max-w-xs text-[13.5px] text-muted-foreground">{t("footer.tagline")}</p>
|
||||
</div>
|
||||
<nav aria-labelledby="footer-product">
|
||||
<h2 id="footer-product" className="text-[13px] font-semibold tracking-wide uppercase">{t("footer.product")}</h2>
|
||||
<ul className="mt-2 grid">
|
||||
{NAV.map((item) => (
|
||||
<li key={item.key}>
|
||||
<Link href={item.href} className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">
|
||||
{t(`nav.${item.key}`)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<Link href="/testen" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.trial")}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav aria-labelledby="footer-legal">
|
||||
<h2 id="footer-legal" className="text-[13px] font-semibold tracking-wide uppercase">{t("footer.company")}</h2>
|
||||
<ul className="mt-2 grid">
|
||||
<li><Link href="/login" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.login")}</Link></li>
|
||||
<li><Link href="/testen/nutzungsbedingungen" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.terms")}</Link></li>
|
||||
<li><Link href="/testen/datenschutz" className="inline-flex min-h-11 items-center text-[14px] text-muted-foreground hover:text-foreground">{t("footer.privacy")}</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<p className="border-t px-4 py-4 text-center text-[12.5px] text-muted-foreground sm:px-6">
|
||||
© {new Date().getFullYear()} {BRAND.name} · {BRAND.tagline} · {t("footer.rights")}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -432,25 +432,42 @@ export function PlanningBoardView({
|
||||
{board.unplanned.length === 0 && <p className="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground">{t("unplanned.empty")}</p>}
|
||||
<ul className="space-y-3">
|
||||
{board.unplanned.map((o) => (
|
||||
<li key={o.id} className="space-y-1.5">
|
||||
<OrderCard order={o} {...cardProps} unplanned />
|
||||
{canSchedule && (
|
||||
<button type="button" aria-expanded={panelFor === o.id} onClick={() => setPanelFor(panelFor === o.id ? null : o.id)} className={`${buttonCls("ghost")} w-full`}>
|
||||
<Lightbulb className="size-4" aria-hidden />
|
||||
{t("unplanned.suggestions")}
|
||||
</button>
|
||||
)}
|
||||
{panelFor === o.id && (
|
||||
<RecommendationsPanel
|
||||
workOrderId={o.id}
|
||||
locale={locale}
|
||||
onClose={() => setPanelFor(null)}
|
||||
onTake={(r: Recommendation) => open(o, r.teamId, r.day, wallClock(new Date(r.suggestedStart), tz))}
|
||||
onPlanTogether={(nearby, r, required) =>
|
||||
open(nearby, r?.teamId ?? null, r?.day ?? null, r ? wallClock(new Date(new Date(r.suggestedStart).getTime() + required * 60_000), tz) : null)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<li key={o.id}>
|
||||
<OrderCard
|
||||
order={o}
|
||||
{...cardProps}
|
||||
unplanned
|
||||
footer={
|
||||
canSchedule && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={panelFor === o.id}
|
||||
aria-label={t("unplanned.suggestionsFor", { number: o.number })}
|
||||
onClick={() => setPanelFor(panelFor === o.id ? null : o.id)}
|
||||
className={`${buttonCls("ghost")} w-full justify-between`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Lightbulb className="size-4" aria-hidden />
|
||||
{t("unplanned.suggestions")}
|
||||
</span>
|
||||
<ChevronDown className={cn("size-4 transition-transform", panelFor === o.id && "rotate-180")} aria-hidden />
|
||||
</button>
|
||||
{panelFor === o.id && (
|
||||
<RecommendationsPanel
|
||||
workOrderId={o.id}
|
||||
locale={locale}
|
||||
onClose={() => setPanelFor(null)}
|
||||
onTake={(r: Recommendation) => open(o, r.teamId, r.day, wallClock(new Date(r.suggestedStart), tz))}
|
||||
onPlanTogether={(nearby, r, required) =>
|
||||
open(nearby, r?.teamId ?? null, r?.day ?? null, r ? wallClock(new Date(new Date(r.suggestedStart).getTime() + required * 60_000), tz) : null)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -559,6 +576,7 @@ function OrderCard({
|
||||
tw,
|
||||
unplanned,
|
||||
compact,
|
||||
footer,
|
||||
}: {
|
||||
order: BoardOrder;
|
||||
canSchedule: boolean;
|
||||
@@ -569,6 +587,8 @@ function OrderCard({
|
||||
tw: ReturnType<typeof useTranslations<"workOrders">>;
|
||||
unplanned?: boolean;
|
||||
compact?: boolean;
|
||||
/** an die Karte angehängter Bereich (z. B. „Vorschläge" in der Spalte „Ungeplante Aufträge") */
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
const movable = canSchedule && SCHEDULABLE_STATUSES.includes(order.status);
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: `order:${order.id}`, disabled: !movable });
|
||||
@@ -678,6 +698,7 @@ function OrderCard({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{footer && <div className="mt-1.5 border-t pt-1">{footer}</div>}
|
||||
{movable && (
|
||||
<button type="button" onClick={() => onOpen(order, null, null, null)} className="mt-1 inline-flex min-h-11 w-full items-center justify-center gap-1 rounded-md border text-[11px] font-semibold hover:bg-muted">
|
||||
<CalendarPlus className="size-3.5" aria-hidden />
|
||||
|
||||
Reference in New Issue
Block a user