Fixed an issue where media manager only shows files uploaded in specific sections.

Fixed an issue with icon picker names.

Removed jumbo rounding and a few other style tweaks.
This commit is contained in:
Stefan Lazic committed 2026-08-05 09:25:59 +01:00
1 parent 9e39908ea7
commit d3bbad09c6
74 files changed
+1622 -195

No files matched your search

@@ -0,0 +1,19 @@
-- AlterTable
ALTER TABLE `LoyaltyProgramme` ADD COLUMN `cardBorderColor` VARCHAR(191) NULL,
ADD COLUMN `cardFont` ENUM('SANS', 'SERIF', 'MONO') NOT NULL DEFAULT 'SANS',
ADD COLUMN `cardMemberNumberColor` VARCHAR(191) NULL,
ADD COLUMN `cardTopRightText` VARCHAR(191) NULL,
ADD COLUMN `cardTopRightTextColor` VARCHAR(191) NULL,
ADD COLUMN `cardTopRightTextSize` ENUM('SM', 'MD', 'LG') NOT NULL DEFAULT 'MD',
ADD COLUMN `rewardPosition` ENUM('AFTER_ALL_STAMPS', 'FINAL_SLOT_IS_REWARD') NOT NULL DEFAULT 'AFTER_ALL_STAMPS';
-- Stamp-limit enforcement (Phase 10 follow-up brief: "Valid range: 1–10").
-- Confirmed via a direct read of the live database before writing this
-- migration that no existing LoyaltyProgramme row has stampsRequired > 10
-- and no LoyaltyAccount has currentStamps > 10 — safe to add directly,
-- no data migration needed. Defense-in-depth alongside the identical
-- 1-10 check in validateProgrammeConfig() and the owner form's client
-- validation; MySQL 8.0.16+ enforces CHECK constraints (this app's
-- documented target, see schema.prisma's own header).
ALTER TABLE `LoyaltyProgramme` ADD CONSTRAINT `LoyaltyProgramme_stampsRequired_range`
CHECK (`stampsRequired` BETWEEN 1 AND 10);
@@ -0,0 +1,31 @@
-- Backfill MANAGE_MEDIA_LIBRARY and MANAGE_REVIEWS (added in Phase 9)
-- onto every role that already holds MANAGE_STAFF — the app's existing
-- definition of "owner" (see src/services/staff/staff.service.ts's
-- countOtherActiveManagers, "A role holding MANAGE_STAFF is this app's
-- definition of 'an owner'"). syncRolePermissions() only ever runs at
-- install/seed time, so any business installed before Phase 9 has its
-- Owner role stuck on the old 15-permission set — confirmed by reading
-- the actual RolePermission rows on a real installed business before
-- writing this migration.
--
-- This is what silently made /owner/settings/reviews redirect away
-- (hasPermission(..., MANAGE_REVIEWS) failing, redirecting to /staff,
-- which then renders the barber calendar — looking exactly like
-- "Reviews opens Calendar") and made Media Manager deletes silently fail
-- (MANAGE_MEDIA_LIBRARY). Not a routing bug — a missing data backfill.
--
-- Idempotent: a fresh install's Owner role already has both permissions
-- from syncRolePermissions() at install time, and INSERT IGNORE plus the
-- existing @@unique([roleId, permission]) constraint make re-running
-- this migration (or running it against an already-correct database)
-- a safe no-op.
INSERT IGNORE INTO `RolePermission` (`id`, `roleId`, `permission`)
SELECT UUID(), owner.`roleId`, 'MANAGE_MEDIA_LIBRARY'
FROM `RolePermission` owner
WHERE owner.`permission` = 'MANAGE_STAFF';
INSERT IGNORE INTO `RolePermission` (`id`, `roleId`, `permission`)
SELECT UUID(), owner.`roleId`, 'MANAGE_REVIEWS'
FROM `RolePermission` owner
WHERE owner.`permission` = 'MANAGE_STAFF';
@@ -0,0 +1,31 @@
-- Rewrite any already-stored `/media/...` URLs (the pre-Phase-10 upload
-- path, which never actually worked in production — Next.js 16's
-- `next start` does not dynamically serve files added to public/ after
-- the build; see src/lib/media/media-storage.ts's header for the full
-- account) to the new `/api/media/...` prefix served by
-- src/app/api/media/[...path]/route.ts.
--
-- Confirmed via a direct read of the live database before writing this
-- migration: exactly one MediaAsset row and its matching
-- BusinessGalleryImage row use the old prefix; every other image field
-- currently stores a full external URL (Business.logoUrl/coverImageUrl,
-- etc.) and is untouched by this. Every statement below is still
-- included for completeness/safety, covers every image column that can
-- receive a MediaPicker-sourced URL, and is a no-op wherever nothing
-- matches.
--
-- Idempotent: re-running finds nothing left matching `/media/%` (rows
-- already rewritten now start with `/api/media/%`), so this is safe to
-- apply more than once.
UPDATE `MediaAsset` SET `url` = CONCAT('/api', `url`) WHERE `url` LIKE '/media/%';
UPDATE `BusinessGalleryImage` SET `imageUrl` = CONCAT('/api', `imageUrl`) WHERE `imageUrl` LIKE '/media/%';
UPDATE `Business` SET `logoUrl` = CONCAT('/api', `logoUrl`) WHERE `logoUrl` LIKE '/media/%';
UPDATE `Business` SET `coverImageUrl` = CONCAT('/api', `coverImageUrl`) WHERE `coverImageUrl` LIKE '/media/%';
UPDATE `Business` SET `faviconUrl` = CONCAT('/api', `faviconUrl`) WHERE `faviconUrl` LIKE '/media/%';
UPDATE `Business` SET `appIconUrl` = CONCAT('/api', `appIconUrl`) WHERE `appIconUrl` LIKE '/media/%';
UPDATE `Business` SET `welcomeBackgroundDesktopUrl` = CONCAT('/api', `welcomeBackgroundDesktopUrl`) WHERE `welcomeBackgroundDesktopUrl` LIKE '/media/%';
UPDATE `Business` SET `welcomeBackgroundMobileUrl` = CONCAT('/api', `welcomeBackgroundMobileUrl`) WHERE `welcomeBackgroundMobileUrl` LIKE '/media/%';
UPDATE `LoyaltyProgramme` SET `cardBackgroundImageUrl` = CONCAT('/api', `cardBackgroundImageUrl`) WHERE `cardBackgroundImageUrl` LIKE '/media/%';
UPDATE `Service` SET `imageUrl` = CONCAT('/api', `imageUrl`) WHERE `imageUrl` LIKE '/media/%';
UPDATE `User` SET `image` = CONCAT('/api', `image`) WHERE `image` LIKE '/media/%';
+51 -2
View File
@@ -1143,14 +1143,49 @@ model AppointmentService {
// count"; every stamp add/remove/redemption is its own row).
// ─────────────────────────────────────────────────────────────────────────
/// Phase 10 — a small, curated set of card fonts with zero extra network
/// cost (Tailwind's own built-in generic font-family stacks), not an
/// arbitrary CSS font-family string — see loyalty-config-form.tsx's
/// IconPicker-style font Select.
enum LoyaltyCardFont {
SANS
SERIF
MONO
}
/// Phase 10 — an enum, not a free number, so a bad value can never break
/// card layout.
enum LoyaltyCardTextSize {
SM
MD
LG
}
/// Phase 10 — resolves the "stamps required" ambiguity the brief called
/// out: does the reward unlock only once every stamp position is filled,
/// or is the final position itself the reward (earned one stamp early)?
/// See resolveRewardThreshold() in src/services/loyalty/loyalty.service.ts
/// — the one place this is interpreted; automatic stamps, manual stamps,
/// the customer card, and progress text all read through it, so none of
/// them can disagree with each other.
enum RewardPositionMode {
AFTER_ALL_STAMPS
FINAL_SLOT_IS_REWARD
}
model LoyaltyProgramme {
id String @id @default(cuid())
businessId String @unique
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
isEnabled Boolean @default(false)
stampsRequired Int @default(10)
isEnabled Boolean @default(false)
/// Capped at 10 (see the CHECK constraint added in migration
/// 20260805000000_phase10_loyalty_card_and_permission_fixes) — enforced
/// identically in validateProgrammeConfig, the owner form, and here.
stampsRequired Int @default(10)
/// See RewardPositionMode's own doc comment.
rewardPosition RewardPositionMode @default(AFTER_ALL_STAMPS)
rewardType RewardType @default(PERCENTAGE)
/// Percentage points or a fixed-amount value depending on rewardType;
@@ -1179,6 +1214,20 @@ model LoyaltyProgramme {
stampIcon String?
termsLine String?
/// Phase 10 — free text shown in the card's top-right area, above the
/// member number. Separate from the member number itself (always
/// shown, has its own colour below) — null/empty means nothing renders
/// there beyond the member number, same as today.
cardTopRightText String?
/// Card-wide — applies to all card text, not just the top-right text.
/// See LoyaltyCardFont's doc comment for why this is card-wide rather
/// than a control per element.
cardFont LoyaltyCardFont @default(SANS)
cardTopRightTextSize LoyaltyCardTextSize @default(MD)
cardTopRightTextColor String?
cardBorderColor String?
cardMemberNumberColor String?
updatedAt DateTime @updatedAt
/// An empty eligibleServices/eligibleStaff list means "all services /
+15 -3
View File
@@ -546,7 +546,19 @@ async function main() {
},
});
// Leo: checked in, about to start — Current Appointment tier 2.
// Leo: checked in today — Current Appointment tier 2 (owner-journeys.spec.ts
// journey 10 also depends on this being CHECKED_IN + "today", see below).
// Fixed at 11:00, not minutesFromNow — selectCurrentAppointment's tier-2
// branch (appointment.service.ts) picks a CHECKED_IN row by status alone,
// never by clock time, so there was never a need to anchor this to "right
// now". Anchoring it that way was actively harmful: whenever the seed ran
// outside Leo's own 10:00-19:00 working hours (e.g. early morning), this
// appointment landed outside those hours too, which made
// findWorkingHoursConflicts (staff.service.ts) correctly-but-unhelpfully
// flag a conflict for *any* working-hours save Leo made that day —
// including ones that never touched this appointment's own weekday. A
// fixed time inside his working hours removes that time-of-day flakiness
// entirely.
await prisma.appointment.upsert({
where: { id: `${business.id}-appt-today-checkedin` },
update: {},
@@ -561,8 +573,8 @@ async function main() {
customerId: secondCustomer.id,
staffId: leo.id,
status: "CHECKED_IN",
startAt: minutesFromNow(5),
endAt: minutesFromNow(5 + haircut.durationMinutes),
startAt: daysFromNow(0, 11, 0),
endAt: daysFromNow(0, 11, haircut.durationMinutes),
services: {
create: [{ serviceId: haircut.id, priceAtBooking: haircut.price, durationAtBookingMinutes: haircut.durationMinutes }],
},