Integrate local business workflow and SaaS redesign

This commit is contained in:
2026-06-12 21:08:35 +02:00
parent f00c5a3193
commit 21c7e4c9a4
88 changed files with 2683 additions and 849 deletions

View File

@@ -4,7 +4,16 @@ import { useMemo, useState } from "react";
import { useAction, useMutation, useQuery } from "convex/react";
import type { FunctionReturnType } from "convex/server";
import { ChevronDown, ChevronRight, ExternalLink, MailCheck, Save } from "lucide-react";
import {
CheckCircle2,
ChevronDown,
ChevronRight,
ExternalLink,
FileSearch,
MailCheck,
Save,
ShieldCheck,
} from "lucide-react";
import Link from "next/link";
import { api } from "@/convex/_generated/api";
@@ -176,9 +185,9 @@ function FieldPair({ label, value }: { label: string; value?: string | null }) {
function WorkspaceLoading() {
return (
<section className="space-y-4">
<header className="space-y-2">
<p className="text-sm text-muted-foreground">Interne Outreach-Prüfung</p>
<h1 className="text-2xl font-semibold tracking-normal">Review Workspace</h1>
<header className="agency-panel space-y-2 p-4">
<p className="agency-kicker">Approval Bench</p>
<h1 className="font-heading text-2xl font-semibold tracking-normal">Review Workspace</h1>
</header>
<div className="space-y-3">
{Array.from({ length: 3 }, (_, index) => (
@@ -244,11 +253,11 @@ export function OutreachReviewWorkspace() {
if (rows.length === 0) {
return (
<section className="space-y-4">
<header className="space-y-2">
<p className="text-sm text-muted-foreground">Interne Outreach-Prüfung</p>
<h1 className="text-2xl font-semibold tracking-normal">Review Workspace</h1>
<header className="agency-panel space-y-2 p-4">
<p className="agency-kicker">Approval Bench</p>
<h1 className="font-heading text-2xl font-semibold tracking-normal">Review Workspace</h1>
</header>
<Card>
<Card className="agency-panel">
<CardContent className="p-4">
<p className="text-sm font-medium">Keine offenen Reviews</p>
<p className="mt-1 text-sm text-muted-foreground">
@@ -482,9 +491,9 @@ export function OutreachReviewWorkspace() {
return (
<section className="space-y-4">
<header className="space-y-2">
<p className="text-sm text-muted-foreground">Interne Outreach-Prüfung</p>
<h1 className="text-2xl font-semibold tracking-normal">Review Workspace</h1>
<header className="agency-panel space-y-2 p-4">
<p className="agency-kicker">Approval Bench</p>
<h1 className="font-heading text-2xl font-semibold tracking-normal">Review Workspace</h1>
<p className="max-w-3xl text-sm text-muted-foreground">
Audits, E-Mail-Empfehlung und Telefonnotizen prüfen, bevor etwas öffentlich
wird oder eine Freigabe erhält.
@@ -492,7 +501,7 @@ export function OutreachReviewWorkspace() {
</header>
{notice ? (
<p className="rounded-md border bg-muted/30 px-3 py-2 text-sm" role="status">{notice}</p>
<p className="rounded-md border border-border/75 bg-muted/30 px-3 py-2 text-sm" role="status">{notice}</p>
) : null}
<Dialog
@@ -560,7 +569,7 @@ export function OutreachReviewWorkspace() {
size="sm"
type="button"
>
Senden
Final senden
</Button>
<Button onClick={closeEmailConfirmation} size="sm" type="button" variant="outline">
Abbrechen
@@ -570,14 +579,15 @@ export function OutreachReviewWorkspace() {
) : null}
</Dialog>
<section className="space-y-3" aria-label="Review-Queue">
<div className="grid gap-4 xl:grid-cols-[minmax(18rem,0.78fr)_minmax(0,1.22fr)]">
<section className="agency-panel space-y-3 p-3" aria-label="Review-Queue">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="text-sm font-semibold">Review-Queue</h2>
<div className="flex flex-wrap gap-2" aria-label="Review-Filter">
{reviewStatusFilters.map((filter) => (
<button
aria-pressed={activeFilter === filter.value}
className="inline-flex min-h-8 items-center gap-2 rounded-md border px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted aria-pressed:border-foreground aria-pressed:text-foreground"
className="agency-tab"
key={filter.value}
onClick={() => setActiveFilter(filter.value)}
type="button"
@@ -589,7 +599,7 @@ export function OutreachReviewWorkspace() {
</div>
</div>
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-3">
{filteredRows.map((record) => {
const lead = record.lead;
const audit = record.audit;
@@ -602,8 +612,10 @@ export function OutreachReviewWorkspace() {
<Card
aria-labelledby={queueTitleId}
className={cn(
"flex min-w-0 flex-col",
selectedRecord?.id === record.id ? "border-foreground" : "",
"flex min-w-0 flex-col overflow-hidden",
selectedRecord?.id === record.id
? "border-primary bg-[var(--surface-evidence)]"
: "bg-background/60",
)}
key={record.id}
>
@@ -656,7 +668,7 @@ export function OutreachReviewWorkspace() {
);
})}
{filteredRows.length === 0 ? (
<Card className="lg:col-span-2 xl:col-span-3">
<Card className="agency-panel">
<CardHeader>
<CardTitle>Keine Treffer</CardTitle>
<CardDescription>
@@ -695,8 +707,8 @@ export function OutreachReviewWorkspace() {
const publicAuditHref = audit?.slug ? `/audit/${audit.slug}` : null;
return (
<Card className="overflow-hidden" key={record.id}>
<CardHeader className="gap-3 border-b bg-muted/20 p-4">
<Card className="agency-panel overflow-hidden" key={record.id}>
<CardHeader className="gap-4 border-b bg-muted/20 p-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 space-y-1">
<CardTitle className="break-words text-lg">
@@ -719,11 +731,49 @@ export function OutreachReviewWorkspace() {
</Badge>
</div>
</div>
<div className="grid gap-2 sm:grid-cols-4">
<div className="evidence-surface rounded-md px-3 py-2">
<span className="inline-flex items-center gap-2 text-xs font-bold uppercase text-muted-foreground">
<FileSearch className="size-3.5" />
Evidence
</span>
<p className="mt-1 text-sm font-semibold">
{audit ? "Audit vorhanden" : "Audit offen"}
</p>
</div>
<div className="review-surface rounded-md px-3 py-2">
<span className="inline-flex items-center gap-2 text-xs font-bold uppercase text-muted-foreground">
<ShieldCheck className="size-3.5" />
Public Audit
</span>
<p className="mt-1 text-sm font-semibold">
{audit?.status === "published" ? "Veröffentlicht" : "Prüfung offen"}
</p>
</div>
<div className="safe-surface rounded-md px-3 py-2">
<span className="inline-flex items-center gap-2 text-xs font-bold uppercase text-muted-foreground">
<MailCheck className="size-3.5" />
E-Mail
</span>
<p className="mt-1 text-sm font-semibold">
{isEmailDraftReady(record) ? "Bereit" : "Entwurf offen"}
</p>
</div>
<div className="rounded-md border border-border/75 bg-background/70 px-3 py-2">
<span className="inline-flex items-center gap-2 text-xs font-bold uppercase text-muted-foreground">
<CheckCircle2 className="size-3.5 text-primary" />
Final Send
</span>
<p className="mt-1 text-sm font-semibold">
{isQueuedSend ? "Wird gesendet" : "Bestätigung nötig"}
</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-5 p-4">
<section className="grid gap-4 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
<div className="space-y-3">
<div className="space-y-3 rounded-md border border-border/75 bg-background/60 p-3">
<h2 className="text-sm font-semibold">Lead-Details</h2>
<dl className="grid gap-3 sm:grid-cols-2">
<FieldPair label="Nische" value={lead?.niche} />
@@ -752,7 +802,7 @@ export function OutreachReviewWorkspace() {
</div>
</div>
<div className="space-y-3">
<div className="space-y-3 rounded-md border border-border/75 bg-background/60 p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<h2 className="text-sm font-semibold">Audit-Zusammenfassung</h2>
{publicAuditHref ? (
@@ -828,7 +878,7 @@ export function OutreachReviewWorkspace() {
</section>
<section className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="space-y-3">
<div className="space-y-3 rounded-md border border-border/75 bg-background/60 p-3">
<h2 className="text-sm font-semibold">Empfohlene E-Mail</h2>
<label className="block space-y-1">
<span className="text-xs font-medium text-muted-foreground">
@@ -877,12 +927,12 @@ export function OutreachReviewWorkspace() {
type="button"
>
<MailCheck className="size-3.5" />
E-Mail freigeben und senden
E-Mail freigeben
</Button>
</div>
</div>
<div className="space-y-3">
<div className="space-y-3 rounded-md border border-border/75 bg-background/60 p-3">
<h2 className="text-sm font-semibold">Telefon & Follow-up</h2>
{hasCallablePhone ? (
<label className="block space-y-1">
@@ -997,6 +1047,7 @@ export function OutreachReviewWorkspace() {
);
})() : null}
</div>
</div>
</section>
);
}