Loyalty Card updates

1. Card title fix: The "top-right text" feature was renamed and repositioned to be the actual card title (top-left, replacing the hardcoded "Loyalty Card"), matching what you asked for. This included a proper database migration (cardTopRightText → cardTitleText, etc., using CHANGE COLUMN so any already-configured value is preserved, not dropped) and updates through every layer — schema, service, owner form, and both the owner preview and customer-facing card. Verified live: typed "VIP Rewards" into the new "Card title" field and confirmed it renders top-left on both the owner's live preview and the real customer loyalty page, with the member number staying in its own badge top-right.

2. Tabs: The long "Card Appearance" control list is now split into Colours / Stamp / Text tabs (using the same Tabs component used elsewhere in the app), exactly as you suggested — Colours holds every colour + the background image, Stamp holds just the icon picker, Text holds the title, font, size, and terms line.

Along the way, while running full E2E regression, I found 6 test failures — but traced them via git stash to prove they're pre-existing bugs unrelated to this change (a booking/appointments-list bug), so I reverted-then-confirmed before restoring my work, and filed a separate follow-up task for them rather than mixing it into this change.

Both fixes are typechecked, linted, verified with the full unit (253) and integration (185) suites, confirmed live in a real browser end-to-end, and deployed to your running Docker container — the migration applied cleanly against your real database with no data loss.
This commit is contained in:
Stefan Lazic committed 2026-08-05 12:19:08 +01:00
1 parent d3bbad09c6
commit cf3a28f3cd
12 files changed
+116 -99

No files matched your search

@@ -0,0 +1,9 @@
-- Phase 11: the loyalty card's owner-configurable text now replaces the
-- hardcoded "Loyalty Card" title (top-left), not a secondary badge next to
-- the member number (top-right) — renamed to match, using CHANGE COLUMN so
-- any value an owner already configured (e.g. a custom title) survives the
-- rename instead of being dropped.
ALTER TABLE `LoyaltyProgramme`
CHANGE COLUMN `cardTopRightText` `cardTitleText` VARCHAR(191) NULL,
CHANGE COLUMN `cardTopRightTextSize` `cardTitleTextSize` ENUM('SM', 'MD', 'LG') NOT NULL DEFAULT 'MD',
CHANGE COLUMN `cardTopRightTextColor` `cardTitleTextColor` VARCHAR(191) NULL;
+9 -10
View File
@@ -1214,17 +1214,16 @@ model LoyaltyProgramme {
stampIcon String? stampIcon String?
termsLine String? termsLine String?
/// Phase 10 — free text shown in the card's top-right area, above the /// Phase 11 — replaces the hardcoded "Loyalty Card" title shown top-left
/// member number. Separate from the member number itself (always /// on the card. Null/empty falls back to "Loyalty Card", same as before
/// shown, has its own colour below) — null/empty means nothing renders /// this was configurable.
/// there beyond the member number, same as today. cardTitleText String?
cardTopRightText String? /// Card-wide — applies to all card text, not just the title. See
/// Card-wide — applies to all card text, not just the top-right text. /// LoyaltyCardFont's doc comment for why this is card-wide rather than a
/// See LoyaltyCardFont's doc comment for why this is card-wide rather /// control per element.
/// than a control per element.
cardFont LoyaltyCardFont @default(SANS) cardFont LoyaltyCardFont @default(SANS)
cardTopRightTextSize LoyaltyCardTextSize @default(MD) cardTitleTextSize LoyaltyCardTextSize @default(MD)
cardTopRightTextColor String? cardTitleTextColor String?
cardBorderColor String? cardBorderColor String?
cardMemberNumberColor String? cardMemberNumberColor String?
+6 -4
View File
@@ -143,17 +143,17 @@ export default async function CustomerHomePage() {
<div> <div>
{/* Hero — an owner-managed gallery (see /owner/settings/gallery), matching the content width rather than bleeding edge-to-edge. */} {/* Hero — an owner-managed gallery (see /owner/settings/gallery), matching the content width rather than bleeding edge-to-edge. */}
{heroGalleryImages.length > 0 && ( {heroGalleryImages.length > 0 && (
<div className="mx-auto max-w-2xl px-6 pt-4 bg-white"> <div className="mx-auto max-w-2xl px-6 pt-4 bg-white border-line border-b">
<ImageGallery images={heroGalleryImages} /> <ImageGallery images={heroGalleryImages} />
</div> </div>
)} )}
<main className="mx-auto max-w-2xl bg-white px-6 py-6"> <main className="mx-auto max-w-2xl bg-white px-6 py-6">
{session?.user && ( {/* {session?.user && (
<p className="text-muted mb-1 text-sm"> <p className="text-muted mb-1 text-sm">
Good to see you, {session.user.name?.split(" ")[0] ?? "there"} Good to see you, {session.user.name?.split(" ")[0] ?? "there"}
</p> </p>
)} )} */}
<h1 className="mb-1 text-2xl font-bold tracking-tight">{business.name}</h1> <h1 className="mb-1 text-2xl font-bold tracking-tight">{business.name}</h1>
{location && ( {location && (
<p className="text-muted mb-3 text-xs"> <p className="text-muted mb-3 text-xs">
@@ -176,9 +176,11 @@ export default async function CustomerHomePage() {
{/* Upcoming appointment */} {/* Upcoming appointment */}
{upcomingAppointment && ( {upcomingAppointment && (
<section className="mb-6"> <section className="mb-6">
{session?.user && (
<h2 className="text-muted mb-3 text-xs font-bold tracking-widest uppercase"> <h2 className="text-muted mb-3 text-xs font-bold tracking-widest uppercase">
Your Next Visit {session.user.name?.split(" ")[0] ?? "Your Next Appointment"}, Your Next Appointment
</h2> </h2>
)}
<AppointmentCard <AppointmentCard
status={STATUS_MAP[upcomingAppointment.status] ?? "confirmed"} status={STATUS_MAP[upcomingAppointment.status] ?? "confirmed"}
serviceName={upcomingAppointment.services.map((s) => s.service.name).join(" + ")} serviceName={upcomingAppointment.services.map((s) => s.service.name).join(" + ")}
+3 -3
View File
@@ -86,10 +86,10 @@ export default async function LoyaltyPage() {
cardBackgroundImageUrl={loyalty.cardBackgroundImageUrl} cardBackgroundImageUrl={loyalty.cardBackgroundImageUrl}
stampIcon={loyalty.stampIcon} stampIcon={loyalty.stampIcon}
rewardPosition={loyalty.rewardPosition} rewardPosition={loyalty.rewardPosition}
cardTopRightText={loyalty.cardTopRightText} cardTitleText={loyalty.cardTitleText}
cardFont={loyalty.cardFont} cardFont={loyalty.cardFont}
cardTopRightTextSize={loyalty.cardTopRightTextSize} cardTitleTextSize={loyalty.cardTitleTextSize}
cardTopRightTextColor={loyalty.cardTopRightTextColor} cardTitleTextColor={loyalty.cardTitleTextColor}
cardBorderColor={loyalty.cardBorderColor} cardBorderColor={loyalty.cardBorderColor}
cardMemberNumberColor={loyalty.cardMemberNumberColor} cardMemberNumberColor={loyalty.cardMemberNumberColor}
/> />
@@ -51,10 +51,10 @@ export default async function OwnerLoyaltyConfigPage() {
cardBackgroundImageUrl: programme.cardBackgroundImageUrl ?? undefined, cardBackgroundImageUrl: programme.cardBackgroundImageUrl ?? undefined,
stampIcon: programme.stampIcon ?? undefined, stampIcon: programme.stampIcon ?? undefined,
termsLine: programme.termsLine ?? undefined, termsLine: programme.termsLine ?? undefined,
cardTopRightText: programme.cardTopRightText ?? undefined, cardTitleText: programme.cardTitleText ?? undefined,
cardFont: programme.cardFont, cardFont: programme.cardFont,
cardTopRightTextSize: programme.cardTopRightTextSize, cardTitleTextSize: programme.cardTitleTextSize,
cardTopRightTextColor: programme.cardTopRightTextColor ?? undefined, cardTitleTextColor: programme.cardTitleTextColor ?? undefined,
cardBorderColor: programme.cardBorderColor ?? undefined, cardBorderColor: programme.cardBorderColor ?? undefined,
cardMemberNumberColor: programme.cardMemberNumberColor ?? undefined, cardMemberNumberColor: programme.cardMemberNumberColor ?? undefined,
} }
@@ -68,7 +68,7 @@ export default async function OwnerLoyaltyConfigPage() {
earnOnCancelled: false, earnOnCancelled: false,
earnOnNoShow: false, earnOnNoShow: false,
cardFont: LoyaltyCardFont.SANS, cardFont: LoyaltyCardFont.SANS,
cardTopRightTextSize: LoyaltyCardTextSize.MD, cardTitleTextSize: LoyaltyCardTextSize.MD,
}; };
return ( return (
+19 -22
View File
@@ -13,10 +13,11 @@ const FONT_CLASSES: Record<LoyaltyCardFont, string> = {
[LoyaltyCardFont.MONO]: "font-mono", [LoyaltyCardFont.MONO]: "font-mono",
}; };
const TOP_RIGHT_TEXT_SIZE_CLASSES: Record<LoyaltyCardTextSize, string> = { /** MD matches the size "Loyalty Card" rendered at before this was configurable, so leaving this unset doesn't change the default look. */
[LoyaltyCardTextSize.SM]: "text-[8px]", const TITLE_TEXT_SIZE_CLASSES: Record<LoyaltyCardTextSize, string> = {
[LoyaltyCardTextSize.MD]: "text-[10px]", [LoyaltyCardTextSize.SM]: "text-base",
[LoyaltyCardTextSize.LG]: "text-[13px]", [LoyaltyCardTextSize.MD]: "text-lg",
[LoyaltyCardTextSize.LG]: "text-2xl",
}; };
export interface LoyaltyCardProps { export interface LoyaltyCardProps {
@@ -32,10 +33,11 @@ export interface LoyaltyCardProps {
stampIcon?: string | null; stampIcon?: string | null;
/** See resolveRewardThreshold's doc comment — determines whether the final stamp position renders as a normal stamp or as the reward slot itself. */ /** See resolveRewardThreshold's doc comment — determines whether the final stamp position renders as a normal stamp or as the reward slot itself. */
rewardPosition?: RewardPositionMode; rewardPosition?: RewardPositionMode;
cardTopRightText?: string | null; /** Replaces the "Loyalty Card" title (top-left) when set. */
cardTitleText?: string | null;
cardFont?: LoyaltyCardFont; cardFont?: LoyaltyCardFont;
cardTopRightTextSize?: LoyaltyCardTextSize; cardTitleTextSize?: LoyaltyCardTextSize;
cardTopRightTextColor?: string | null; cardTitleTextColor?: string | null;
cardBorderColor?: string | null; cardBorderColor?: string | null;
cardMemberNumberColor?: string | null; cardMemberNumberColor?: string | null;
} }
@@ -63,10 +65,10 @@ export function LoyaltyCard({
cardBackgroundImageUrl, cardBackgroundImageUrl,
stampIcon, stampIcon,
rewardPosition = RewardPositionMode.AFTER_ALL_STAMPS, rewardPosition = RewardPositionMode.AFTER_ALL_STAMPS,
cardTopRightText, cardTitleText,
cardFont = LoyaltyCardFont.SANS, cardFont = LoyaltyCardFont.SANS,
cardTopRightTextSize = LoyaltyCardTextSize.MD, cardTitleTextSize = LoyaltyCardTextSize.MD,
cardTopRightTextColor, cardTitleTextColor,
cardBorderColor, cardBorderColor,
cardMemberNumberColor, cardMemberNumberColor,
}: LoyaltyCardProps) { }: LoyaltyCardProps) {
@@ -111,24 +113,19 @@ export function LoyaltyCard({
<div className="relative flex h-full flex-col justify-between p-4"> <div className="relative flex h-full flex-col justify-between p-4">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<h2 className="text-lg font-black italic tracking-tight text-white">Loyalty Card</h2> <h2
<div className="flex flex-col items-end gap-1"> className={cn(TITLE_TEXT_SIZE_CLASSES[cardTitleTextSize], "font-black italic tracking-tight text-white")}
{cardTopRightText && ( style={{ color: cardTitleTextColor || undefined }}
<span
className={cn(TOP_RIGHT_TEXT_SIZE_CLASSES[cardTopRightTextSize], "text-right font-bold text-white")}
style={{ color: cardTopRightTextColor || undefined }}
> >
{cardTopRightText} {cardTitleText || "Loyalty Card"}
</span> </h2>
)}
<span <span
className="border-accent-loyalty/30 text-accent-loyalty rounded border px-2 py-0.5 text-[8px] font-bold tracking-widest uppercase" className="border-accent-loyalty/30 text-accent-loyalty shrink-0 rounded border px-2 py-0.5 text-[8px] font-bold tracking-widest uppercase"
style={{ color: cardMemberNumberColor || undefined, borderColor: cardMemberNumberColor ? `${cardMemberNumberColor}4d` : undefined }} style={{ color: cardMemberNumberColor || undefined, borderColor: cardMemberNumberColor ? `${cardMemberNumberColor}4d` : undefined }}
> >
Member #{memberNumber} Member #{memberNumber}
</span> </span>
</div> </div>
</div>
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-5 gap-2">
{Array.from({ length: stampsRequired }, (_, i) => { {Array.from({ length: stampsRequired }, (_, i) => {
const isRewardSlot = i === rewardSlotIndex; const isRewardSlot = i === rewardSlotIndex;
@@ -161,7 +158,7 @@ export function LoyaltyCard({
})} })}
</div> </div>
{termsLine && ( {termsLine && (
<p className="text-center text-[7px] mt-2 font-bold tracking-widest text-white/40 uppercase">{termsLine}</p> <p className="mt-2 text-center text-[7px] font-bold tracking-widest text-white/40 uppercase">{termsLine}</p>
)} )}
</div> </div>
</div> </div>
+2 -2
View File
@@ -14,7 +14,7 @@ export const Tabs = TabsPrimitive.Root;
export function TabsList({ className, ...props }: TabsPrimitive.TabsListProps) { export function TabsList({ className, ...props }: TabsPrimitive.TabsListProps) {
return ( return (
<TabsPrimitive.List <TabsPrimitive.List
className={cn("flex gap-2 rounded-full bg-stone p-1", className)} className={cn("flex gap-1 rounded-full bg-white p-1", className)}
{...props} {...props}
/> />
); );
@@ -24,7 +24,7 @@ export function TabsTrigger({ className, ...props }: TabsPrimitive.TabsTriggerPr
return ( return (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
className={cn( className={cn(
"flex-1 rounded-full px-4 py-2.5 text-xs font-bold uppercase tracking-widest text-muted transition-all", "flex-1 rounded-md px-3 py-2.5 text-xs font-bold uppercase text-muted transition-all",
"data-[state=active]:bg-ink data-[state=active]:text-white", "data-[state=active]:bg-ink data-[state=active]:text-white",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-stone", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-stone",
className, className,
@@ -10,6 +10,7 @@ import { Input } from "@/components/ui/input";
import { LoyaltyCard } from "@/components/ui/loyalty-card"; import { LoyaltyCard } from "@/components/ui/loyalty-card";
import { Select } from "@/components/ui/select"; import { Select } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { MediaPicker } from "@/features/media/components/media-picker"; import { MediaPicker } from "@/features/media/components/media-picker";
import { LoyaltyCardFont, LoyaltyCardTextSize, RewardPositionMode } from "@/generated/prisma/enums"; import { LoyaltyCardFont, LoyaltyCardTextSize, RewardPositionMode } from "@/generated/prisma/enums";
import { cn } from "@/lib/utils/cn"; import { cn } from "@/lib/utils/cn";
@@ -29,7 +30,7 @@ const FONT_OPTIONS: { value: LoyaltyCardFont; label: string }[] = [
{ value: LoyaltyCardFont.MONO, label: "Monospace" }, { value: LoyaltyCardFont.MONO, label: "Monospace" },
]; ];
const TEXT_SIZE_OPTIONS: { value: LoyaltyCardTextSize; label: string }[] = [ const TITLE_SIZE_OPTIONS: { value: LoyaltyCardTextSize; label: string }[] = [
{ value: LoyaltyCardTextSize.SM, label: "Small" }, { value: LoyaltyCardTextSize.SM, label: "Small" },
{ value: LoyaltyCardTextSize.MD, label: "Medium" }, { value: LoyaltyCardTextSize.MD, label: "Medium" },
{ value: LoyaltyCardTextSize.LG, label: "Large" }, { value: LoyaltyCardTextSize.LG, label: "Large" },
@@ -273,15 +274,23 @@ export function LoyaltyConfigForm({
cardBackgroundImageUrl={config.cardBackgroundImageUrl} cardBackgroundImageUrl={config.cardBackgroundImageUrl}
stampIcon={config.stampIcon} stampIcon={config.stampIcon}
rewardPosition={config.rewardPosition} rewardPosition={config.rewardPosition}
cardTopRightText={config.cardTopRightText} cardTitleText={config.cardTitleText}
cardFont={config.cardFont} cardFont={config.cardFont}
cardTopRightTextSize={config.cardTopRightTextSize} cardTitleTextSize={config.cardTitleTextSize}
cardTopRightTextColor={config.cardTopRightTextColor} cardTitleTextColor={config.cardTitleTextColor}
cardBorderColor={config.cardBorderColor} cardBorderColor={config.cardBorderColor}
cardMemberNumberColor={config.cardMemberNumberColor} cardMemberNumberColor={config.cardMemberNumberColor}
/> />
</div> </div>
<div className="mt-4 space-y-4 lg:mt-0"> <div className="mt-4 lg:mt-0">
<Tabs defaultValue="colours">
<TabsList>
<TabsTrigger value="colours">Colours</TabsTrigger>
<TabsTrigger value="stamp">Stamp</TabsTrigger>
<TabsTrigger value="text">Text</TabsTrigger>
</TabsList>
<TabsContent value="colours" className="space-y-4">
<ColorPicker <ColorPicker
label="Background colour" label="Background colour"
value={config.cardBackgroundColor} value={config.cardBackgroundColor}
@@ -298,23 +307,29 @@ export function LoyaltyConfigForm({
value={config.cardBorderColor} value={config.cardBorderColor}
onChange={(hex) => set("cardBorderColor", hex)} onChange={(hex) => set("cardBorderColor", hex)}
/> />
<IconPicker label="Stamp icon" value={config.stampIcon} onChange={(iconName) => set("stampIcon", iconName)} /> <ColorPicker
<Input label="Title text colour"
label="Terms line" value={config.cardTitleTextColor}
value={config.termsLine ?? ""} onChange={(hex) => set("cardTitleTextColor", hex)}
onChange={(e) => set("termsLine", e.target.value)}
placeholder="Valid for 12 months. One reward per visit."
/> />
<ColorPicker
label="Member number colour"
value={config.cardMemberNumberColor}
onChange={(hex) => set("cardMemberNumberColor", hex)}
/>
</TabsContent>
<div className="border-line border-t pt-4"> <TabsContent value="stamp" className="space-y-4">
<p className="mb-3 text-xs font-bold">Top-right card text</p> <IconPicker label="Stamp icon" value={config.stampIcon} onChange={(iconName) => set("stampIcon", iconName)} />
<div className="space-y-4"> </TabsContent>
<TabsContent value="text" className="space-y-4">
<Input <Input
label="Text" label="Card title"
value={config.cardTopRightText ?? ""} value={config.cardTitleText ?? ""}
onChange={(e) => set("cardTopRightText", e.target.value)} onChange={(e) => set("cardTitleText", e.target.value)}
placeholder="e.g. VIP Member" placeholder="Loyalty Card"
hint="Shown above the member number. Leave blank to show only the member number, as today." hint={`Shown top-left on the card, in place of the default "Loyalty Card". Leave blank to keep the default.`}
/> />
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Select <Select
@@ -329,31 +344,26 @@ export function LoyaltyConfigForm({
))} ))}
</Select> </Select>
<Select <Select
label="Text size" label="Title size"
value={config.cardTopRightTextSize} value={config.cardTitleTextSize}
onChange={(e) => set("cardTopRightTextSize", e.target.value as ProgrammeConfigInput["cardTopRightTextSize"])} onChange={(e) => set("cardTitleTextSize", e.target.value as ProgrammeConfigInput["cardTitleTextSize"])}
> >
{TEXT_SIZE_OPTIONS.map((s) => ( {TITLE_SIZE_OPTIONS.map((s) => (
<option key={s.value} value={s.value}> <option key={s.value} value={s.value}>
{s.label} {s.label}
</option> </option>
))} ))}
</Select> </Select>
</div> </div>
<p className="text-muted -mt-2 text-[10px]">Font applies to the whole card, not just this text.</p> <p className="text-muted -mt-2 text-[10px]">Font applies to the whole card, not just the title.</p>
<ColorPicker <Input
label="Text colour" label="Terms line"
value={config.cardTopRightTextColor} value={config.termsLine ?? ""}
onChange={(hex) => set("cardTopRightTextColor", hex)} onChange={(e) => set("termsLine", e.target.value)}
/> placeholder="Valid for 12 months. One reward per visit."
</div>
</div>
<ColorPicker
label="Member number colour"
value={config.cardMemberNumberColor}
onChange={(hex) => set("cardMemberNumberColor", hex)}
/> />
</TabsContent>
</Tabs>
</div> </div>
</div> </div>
</section> </section>
@@ -14,7 +14,7 @@ const base: ProgrammeConfigInput = {
earnOnCancelled: false, earnOnCancelled: false,
earnOnNoShow: false, earnOnNoShow: false,
cardFont: LoyaltyCardFont.SANS, cardFont: LoyaltyCardFont.SANS,
cardTopRightTextSize: LoyaltyCardTextSize.MD, cardTitleTextSize: LoyaltyCardTextSize.MD,
}; };
describe("validateProgrammeConfig", () => { describe("validateProgrammeConfig", () => {
@@ -41,7 +41,7 @@ describe("LoyaltyService — owner configuration and redemption (integration)",
earnOnCancelled: false, earnOnCancelled: false,
earnOnNoShow: false, earnOnNoShow: false,
cardFont: LoyaltyCardFont.SANS, cardFont: LoyaltyCardFont.SANS,
cardTopRightTextSize: LoyaltyCardTextSize.MD, cardTitleTextSize: LoyaltyCardTextSize.MD,
}, },
}); });
expect(first.success).toBe(true); expect(first.success).toBe(true);
@@ -63,7 +63,7 @@ describe("LoyaltyService — owner configuration and redemption (integration)",
earnOnCancelled: false, earnOnCancelled: false,
earnOnNoShow: false, earnOnNoShow: false,
cardFont: LoyaltyCardFont.SANS, cardFont: LoyaltyCardFont.SANS,
cardTopRightTextSize: LoyaltyCardTextSize.MD, cardTitleTextSize: LoyaltyCardTextSize.MD,
}, },
}); });
expect(second.success).toBe(true); expect(second.success).toBe(true);
@@ -92,7 +92,7 @@ describe("LoyaltyService — owner configuration and redemption (integration)",
earnOnCancelled: false, earnOnCancelled: false,
earnOnNoShow: false, earnOnNoShow: false,
cardFont: LoyaltyCardFont.SANS, cardFont: LoyaltyCardFont.SANS,
cardTopRightTextSize: LoyaltyCardTextSize.MD, cardTitleTextSize: LoyaltyCardTextSize.MD,
}, },
}); });
expect(result.success).toBe(false); expect(result.success).toBe(false);
+13 -13
View File
@@ -56,10 +56,10 @@ export interface LoyaltySummary {
cardBackgroundColor: string | null; cardBackgroundColor: string | null;
cardBackgroundImageUrl: string | null; cardBackgroundImageUrl: string | null;
termsLine: string | null; termsLine: string | null;
cardTopRightText: string | null; cardTitleText: string | null;
cardFont: LoyaltyCardFont; cardFont: LoyaltyCardFont;
cardTopRightTextSize: LoyaltyCardTextSize; cardTitleTextSize: LoyaltyCardTextSize;
cardTopRightTextColor: string | null; cardTitleTextColor: string | null;
cardBorderColor: string | null; cardBorderColor: string | null;
cardMemberNumberColor: string | null; cardMemberNumberColor: string | null;
} }
@@ -105,10 +105,10 @@ export async function getLoyaltySummary(
cardBackgroundColor: programme.cardBackgroundColor, cardBackgroundColor: programme.cardBackgroundColor,
cardBackgroundImageUrl: programme.cardBackgroundImageUrl, cardBackgroundImageUrl: programme.cardBackgroundImageUrl,
termsLine: programme.termsLine, termsLine: programme.termsLine,
cardTopRightText: programme.cardTopRightText, cardTitleText: programme.cardTitleText,
cardFont: programme.cardFont, cardFont: programme.cardFont,
cardTopRightTextSize: programme.cardTopRightTextSize, cardTitleTextSize: programme.cardTitleTextSize,
cardTopRightTextColor: programme.cardTopRightTextColor, cardTitleTextColor: programme.cardTitleTextColor,
cardBorderColor: programme.cardBorderColor, cardBorderColor: programme.cardBorderColor,
cardMemberNumberColor: programme.cardMemberNumberColor, cardMemberNumberColor: programme.cardMemberNumberColor,
}; };
@@ -546,11 +546,11 @@ export interface ProgrammeConfigInput {
cardBackgroundImageUrl?: string; cardBackgroundImageUrl?: string;
stampIcon?: string; stampIcon?: string;
termsLine?: string; termsLine?: string;
/** Free text shown in the card's top-right area — see schema.prisma's cardTopRightText doc comment. */ /** Replaces the "Loyalty Card" title (top-left) when set — see schema.prisma's cardTitleText doc comment. */
cardTopRightText?: string; cardTitleText?: string;
cardFont: LoyaltyCardFont; cardFont: LoyaltyCardFont;
cardTopRightTextSize: LoyaltyCardTextSize; cardTitleTextSize: LoyaltyCardTextSize;
cardTopRightTextColor?: string; cardTitleTextColor?: string;
cardBorderColor?: string; cardBorderColor?: string;
cardMemberNumberColor?: string; cardMemberNumberColor?: string;
} }
@@ -627,10 +627,10 @@ export async function updateProgrammeConfig(
cardBackgroundImageUrl: params.input.cardBackgroundImageUrl || null, cardBackgroundImageUrl: params.input.cardBackgroundImageUrl || null,
stampIcon: params.input.stampIcon || null, stampIcon: params.input.stampIcon || null,
termsLine: params.input.termsLine?.trim() || null, termsLine: params.input.termsLine?.trim() || null,
cardTopRightText: params.input.cardTopRightText?.trim() || null, cardTitleText: params.input.cardTitleText?.trim() || null,
cardFont: params.input.cardFont, cardFont: params.input.cardFont,
cardTopRightTextSize: params.input.cardTopRightTextSize, cardTitleTextSize: params.input.cardTitleTextSize,
cardTopRightTextColor: params.input.cardTopRightTextColor || null, cardTitleTextColor: params.input.cardTitleTextColor || null,
cardBorderColor: params.input.cardBorderColor || null, cardBorderColor: params.input.cardBorderColor || null,
cardMemberNumberColor: params.input.cardMemberNumberColor || null, cardMemberNumberColor: params.input.cardMemberNumberColor || null,
}; };
@@ -227,7 +227,7 @@ export async function getConfigurationWarnings(prisma: PrismaClient, businessId:
earnOnCancelled: loyaltyProgramme.earnOnCancelled, earnOnCancelled: loyaltyProgramme.earnOnCancelled,
earnOnNoShow: loyaltyProgramme.earnOnNoShow, earnOnNoShow: loyaltyProgramme.earnOnNoShow,
cardFont: loyaltyProgramme.cardFont, cardFont: loyaltyProgramme.cardFont,
cardTopRightTextSize: loyaltyProgramme.cardTopRightTextSize, cardTitleTextSize: loyaltyProgramme.cardTitleTextSize,
}); });
if (!validation.valid) { if (!validation.valid) {
warnings.push({ warnings.push({