- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt, Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status - /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung), Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot, Enumeration-Schutz, Slug-Kollisionen - Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern, umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit) - Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi), Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil - Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand - Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding - Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
3.4 KiB
TypeScript
65 lines
3.4 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { requireAppAccess } from "@/server/app-access";
|
|
import { can } from "@/server/services/context";
|
|
import { fieldPageContext } from "@/server/services/field/page-context";
|
|
import { getMyActiveSession } from "@/server/services/field/sessions";
|
|
import { countPendingTimeEntries } from "@/server/services/field/time-entries";
|
|
import { cn } from "@/lib/utils";
|
|
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
|
|
import { AccountInactiveNotice } from "@/components/account-inactive-notice";
|
|
import { BottomNav } from "@/components/field/bottom-nav";
|
|
import { OnlineBadge } from "@/components/field/online-badge";
|
|
import { RunningClockBar, type ClockSession } from "@/components/field/running-clock-bar";
|
|
import { OfflineRuntime } from "@/components/offline/offline-runtime";
|
|
import { TrialBanner } from "@/components/trial/trial-banner";
|
|
|
|
/** L12: own running/paused session + open approvals for the shell (never blocks the page). */
|
|
async function shellTimeState(): Promise<{ clock: ClockSession | null; approvals: number }> {
|
|
try {
|
|
const ctx = await fieldPageContext();
|
|
const [session, approvals] = await Promise.all([can(ctx, "field:execute") ? getMyActiveSession(ctx) : Promise.resolve(null), countPendingTimeEntries(ctx)]);
|
|
return {
|
|
clock: session
|
|
? { status: session.status, workOrderId: session.workOrderId, number: session.number, title: session.title, segmentType: session.segmentType, segmentStartedAt: session.segmentStartedAt, closedSeconds: session.closedSeconds }
|
|
: null,
|
|
approvals,
|
|
};
|
|
} catch {
|
|
// module "field" disabled or no field permissions: shell without clock/badge
|
|
return { clock: null, approvals: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mobile shell `/m` (ARCHITEKTUR §5): same session/account/MFA checks as the backoffice
|
|
* (src/server/app-access.ts), no sidebar, bottom navigation, online/offline badge.
|
|
* Module gates live one level below: (core) → "field", emergency → "emergency".
|
|
* L12: running clock bar above the bottom navigation on every /m page.
|
|
*/
|
|
export default async function FieldShell({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
const access = await requireAppAccess();
|
|
if (access.kind === "inactive") return <AccountInactiveNotice />;
|
|
const [t, time] = await Promise.all([getTranslations("field.nav"), shellTimeState()]);
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-1 flex-col bg-background">
|
|
<header className="sticky top-0 z-20 flex h-14 items-center justify-between gap-3 border-b bg-card px-4 pt-[env(safe-area-inset-top)]">
|
|
<Link href="/m" aria-label={t("today")} className="flex min-h-12 items-center">
|
|
<CraftviaLogo variant="horizontal" height={26} />
|
|
</Link>
|
|
<div className="flex items-center gap-2">
|
|
{/* Slot für L6: <NotificationBell variant="mobile" /> aus src/components/notifications/bell.tsx */}
|
|
<OfflineRuntime />
|
|
<OnlineBadge />
|
|
</div>
|
|
</header>
|
|
{/* L15 Testphase: Countdown ab 7 Tagen bzw. Nur-Lesen-Hinweis */}
|
|
<TrialBanner tenantId={access.session.user.tenantId} variant="mobile" />
|
|
<div className={cn("mx-auto w-full max-w-xl flex-1", time.clock ? "pb-48" : "pb-28")}>{children}</div>
|
|
<RunningClockBar initial={time.clock} />
|
|
<BottomNav approvals={time.approvals} />
|
|
</div>
|
|
);
|
|
}
|