Post Phase 8 - Fixes 1

Delivered:

Super Administrator — env-var provisioned, no StaffProfile at all (structural exclusion from Team/barber-picker/appointments/staff-totals, not a filter). hasPermission short-circuit + new getActingBusinessContext() helper retrofitted across 16 owner pages so it can actually manage the business.
Media Manager — one upload/library/external-URL modal + field picker, backing every image field app-wide. Local-disk storage + Docker volume, SVG rejected, size-capped.
Welcome backgrounds — desktop/mobile images with CSS breakpoint swap.
Business Branding settings — logo/cover/welcome/favicon/icon/colours all editable post-install; cover image finally rendered (was dead config).
Service descriptions — now surfaced on Home and in the booking flow.
Loyalty card — live preview, ColorPicker/MediaPicker/IconPicker, flip removed for a static premium-styled card, Loyalty added to bottom nav. Fixed a real pre-existing bug where the card's configured colour/image/icon were stored but never actually rendered.
Home Screen Redesign — hero gallery + sticky Services/Portfolio/Reviews/Details tabs, backed by a new unified gallery model/service (no hero/portfolio duplication). Details tab surfaces social links and a map that were never shown before.
Google Reviews — real sync pipeline (24h cron, CRON_SECRET-gated, mirrors the existing notification cron exactly), DB-only customer reads, owner hide/unhide + policy management page.
Architecture cleanup — SettingsPageShell extracted from 6 duplicated page wrappers; fixed a second pre-existing bug where branding image fields could never actually be cleared once set.
Verification: 418 tests passing (236 unit + 182 integration, all against a real MySQL database), full typecheck/lint clean across src, and a real Docker build+run cycle — which caught and fixed a genuine bug (scripts/ wasn't copied into the production image, so Super Admin sync would have crashed on every real deployment).
This commit is contained in:
Stefan Lazic committed 2026-08-04 17:10:30 +01:00
1 parent 77f25c0489
commit 9e39908ea7
79 files changed
+4121 -450

No files matched your search

@@ -0,0 +1,51 @@
import { cache } from "react";
import { auth } from "@/auth";
import { prisma } from "@/lib/db/prisma";
import { isSuperAdmin } from "@/lib/auth/permissions";
import { getPrimaryBusiness } from "@/lib/business/get-primary-business";
/**
* Resolves "which business is the signed-in user acting on behalf of" for
* pages/actions that only need the business id/name/timezone — not full
* staff identity (see getActingStaff for that, which stays the right
* choice for anything that needs a real StaffProfile id, e.g. loyalty
* stamp authorship or appointment reassignment).
*
* A regular staff member/owner resolves via their StaffProfile, same as
* getActingStaff. A Super Administrator (see scripts/ensure-super-admin.ts)
* deliberately has no StaffProfile at all — that's what keeps them out of
* the Team list/barber picker/appointment FKs by construction rather than
* a filter — so they resolve straight to the app's one business instead
* (v1 is single-business, same assumption getPrimaryBusiness() already
* makes everywhere else). Returns null only for a signed-in user who is
* neither staff nor a super admin.
*/
export const getActingBusinessContext = cache(async () => {
const session = await auth();
if (!session?.user?.id) return null;
const staffProfile = await prisma.staffProfile.findUnique({
where: { userId: session.user.id },
select: { businessId: true, business: { select: { name: true, timezone: true } } },
});
if (staffProfile) {
return {
userId: session.user.id,
businessId: staffProfile.businessId,
businessName: staffProfile.business.name,
businessTimezone: staffProfile.business.timezone,
};
}
if (!(await isSuperAdmin(session.user.id))) return null;
const business = await getPrimaryBusiness();
return {
userId: session.user.id,
businessId: business.id,
businessName: business.name,
businessTimezone: business.timezone,
};
});
export type ActingBusinessContext = NonNullable<Awaited<ReturnType<typeof getActingBusinessContext>>>;