Built No-show flag and block: Two new numbers under Booking Rules (blank means off). A symbol appears on the clients list, client page, calendar entries and appointment pages. Online booking is refused at the block number, and customers are told at the details step. Staff can still book a blocked customer in, and an owner can reset someone with a two-tap button. The count resets when they attend, and matches the same email or phone, so a guest can’t dodge it with a new profile. Email rules: staff and owners get email only for customer cancellations, schedule changes and invitations. Routine events (new booking, reschedule) are push-only. Nobody is told about their own action. A barber reassignment sends one customer email instead of two. Reward expiry sends one warning. Email redesign: your brand colour, a details card, Add to calendar and View on map links, a preferences link, hidden preview text, and dark mode. Replies now go to the business’s own email. WhatsApp “due now” queue: a list in time order with a highlighted next one, an owner setting for how many hours ahead (12–72), and a once-a-day push telling each barber how many reminders are waiting. Monitoring: /api/health now reports whether notifications are actually going out. The worker resets stuck deliveries and alerts the owner on the first setup failure (such as a bad SMTP login). The app emails your operator address about server errors, and the manager pushes that address to each business. Google scope: now only calendar.app.created. Events go to a calendar the app creates in the staff member’s Google account, called “<Business> bookings”.
This commit is contained in:
116 files changed
+3089
-766
No files matched your search
+22
@@ -0,0 +1,22 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `ApplicationInstallation` ADD COLUMN `lastCronRunAt` DATETIME(3) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `BookingRule` ADD COLUMN `noShowBlockThreshold` INTEGER NULL,
|
||||
ADD COLUMN `noShowFlagThreshold` INTEGER NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `BusinessSettings` ADD COLUMN `whatsappReminderLeadHours` INTEGER NOT NULL DEFAULT 24;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `CustomerProfile` ADD COLUMN `noShowResetAt` DATETIME(3) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Notification` MODIFY `type` ENUM('BOOKING_CONFIRMED', 'BOOKING_PENDING_APPROVAL', 'BOOKING_APPROVED', 'BOOKING_REMINDER', 'BOOKING_RESCHEDULED', 'BOOKING_CANCELLED', 'BOOKING_BARBER_CHANGED', 'BOOKING_COMPLETED', 'LOYALTY_STAMP_ADDED', 'LOYALTY_REWARD_EARNED', 'LOYALTY_REWARD_REDEEMED', 'LOYALTY_REWARD_EXPIRING', 'STAFF_NEW_APPOINTMENT', 'STAFF_APPOINTMENT_RESCHEDULED', 'STAFF_APPOINTMENT_CANCELLED', 'STAFF_APPOINTMENT_CHECKED_IN', 'STAFF_APPOINTMENT_REMINDER', 'STAFF_SCHEDULE_CHANGED', 'STAFF_INVITATION_RECEIVED', 'OWNER_NEW_BOOKING', 'OWNER_BOOKING_CANCELLED', 'OWNER_NO_SHOW', 'OWNER_STAFF_INVITATION_ACCEPTED', 'OWNER_REPEATED_DELIVERY_FAILURE', 'STAFF_WHATSAPP_REMINDERS_DUE') NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `NotificationPreference` MODIFY `type` ENUM('BOOKING_CONFIRMED', 'BOOKING_PENDING_APPROVAL', 'BOOKING_APPROVED', 'BOOKING_REMINDER', 'BOOKING_RESCHEDULED', 'BOOKING_CANCELLED', 'BOOKING_BARBER_CHANGED', 'BOOKING_COMPLETED', 'LOYALTY_STAMP_ADDED', 'LOYALTY_REWARD_EARNED', 'LOYALTY_REWARD_REDEEMED', 'LOYALTY_REWARD_EXPIRING', 'STAFF_NEW_APPOINTMENT', 'STAFF_APPOINTMENT_RESCHEDULED', 'STAFF_APPOINTMENT_CANCELLED', 'STAFF_APPOINTMENT_CHECKED_IN', 'STAFF_APPOINTMENT_REMINDER', 'STAFF_SCHEDULE_CHANGED', 'STAFF_INVITATION_RECEIVED', 'OWNER_NEW_BOOKING', 'OWNER_BOOKING_CANCELLED', 'OWNER_NO_SHOW', 'OWNER_STAFF_INVITATION_ACCEPTED', 'OWNER_REPEATED_DELIVERY_FAILURE', 'STAFF_WHATSAPP_REMINDERS_DUE') NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `StaffCalendarConnection` ADD COLUMN `appCreatedCalendar` BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
@@ -184,6 +184,11 @@ enum NotificationType {
|
||||
/// for the owner when it observes repeated delivery failures, rather
|
||||
/// than a domain workflow triggering it directly. See reminder-worker.ts.
|
||||
OWNER_REPEATED_DELIVERY_FAILURE
|
||||
|
||||
// Barber-facing (cont.)
|
||||
/// A nudge that some appointments' WhatsApp reminders are due to send by
|
||||
/// hand — see whatsapp-reminders.service.ts. Push only by default.
|
||||
STAFF_WHATSAPP_REMINDERS_DUE
|
||||
}
|
||||
|
||||
enum NotificationChannel {
|
||||
@@ -415,6 +420,10 @@ model BusinessSettings {
|
||||
bookingTerms String? @db.Text
|
||||
privacyPolicyUrl String?
|
||||
|
||||
/// How many hours ahead appointments become "due" in the staff WhatsApp
|
||||
/// reminders queue (/staff/reminders). See whatsapp-reminders.service.ts.
|
||||
whatsappReminderLeadHours Int @default(24)
|
||||
|
||||
/// Who the public Terms of Service / Privacy Policy pages name as "we" —
|
||||
/// see src/features/legal/legal-identity.ts. NEXTCHAIR is the default so
|
||||
/// existing businesses keep today's wording.
|
||||
@@ -490,6 +499,14 @@ model BookingRule {
|
||||
/// payments phase (see docs/implementation-plan.md §6, "later phase").
|
||||
requireDeposit Boolean @default(false)
|
||||
|
||||
/// Both null = no-show handling is off. A customer's no-show count is
|
||||
/// derived (see no-show.service.ts), never stored: NO_SHOW appointments
|
||||
/// since their last attended (COMPLETED) one or since an owner reset.
|
||||
/// At/above the flag threshold staff see a warning symbol; at/above the
|
||||
/// block threshold online booking is refused (staff can still book them).
|
||||
noShowFlagThreshold Int?
|
||||
noShowBlockThreshold Int?
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -646,6 +663,10 @@ model ApplicationInstallation {
|
||||
|
||||
failureReason String? @db.Text
|
||||
|
||||
/// Stamped by /api/cron/process-notifications on every authorised run, so
|
||||
/// /api/health can tell "the cron stopped" from "nothing to send".
|
||||
lastCronRunAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -808,6 +829,10 @@ model CustomerProfile {
|
||||
phone String?
|
||||
isGuest Boolean @default(true)
|
||||
|
||||
/// An owner's "forgive" — no-shows before this moment no longer count
|
||||
/// towards the flag/block thresholds. See no-show.service.ts.
|
||||
noShowResetAt DateTime?
|
||||
|
||||
preferredBarberId String?
|
||||
preferredBarber StaffProfile? @relation("PreferredBarber", fields: [preferredBarberId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@ -954,6 +979,11 @@ model StaffCalendarConnection {
|
||||
|
||||
selectedCalendarId String
|
||||
selectedCalendarName String
|
||||
/// True when the connection was made with the narrow calendar.app.created
|
||||
/// scope: selectedCalendarId is a secondary calendar this app created, and
|
||||
/// there is no calendar picker (the scope can't list the user's calendars).
|
||||
/// False = a pre-existing connection with the broader scopes.
|
||||
appCreatedCalendar Boolean @default(false)
|
||||
|
||||
connectedAt DateTime @default(now())
|
||||
lastSyncAt DateTime?
|
||||
|
||||
Reference in new issue
Block a user