mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 13:24:27 +00:00
feat: Add kiosk management, artist selection, and schedule management
- Add KiosksManagement component with full CRUD for kiosks - Add ScheduleManagement for staff schedules with break reminders - Update booking flow to allow artist selection by customers - Add staff_services API for assigning services to artists - Update staff management UI with service assignment dialog - Add auto-break reminder when schedule >= 8 hours - Update availability API to filter artists by service - Add kiosk management to Aperture dashboard - Clean up ralphy artifacts and logs
This commit is contained in:
49
lib/calendar-utils.ts
Normal file
49
lib/calendar-utils.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Calendar utilities for drag & drop operations
|
||||
* Handles staff service validation, conflict checking, and booking rescheduling
|
||||
*/
|
||||
|
||||
export const checkStaffCanPerformService = async (staffId: string, serviceId: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`/api/aperture/staff/${staffId}/services`);
|
||||
const data = await response.json();
|
||||
return data.success && data.services.some((s: any) => s.services?.id === serviceId);
|
||||
} catch (error) {
|
||||
console.error('Error checking staff services:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForConflicts = async (bookingId: string, staffId: string, startTime: string, duration: number): Promise<boolean> => {
|
||||
try {
|
||||
const endTime = new Date(new Date(startTime).getTime() + duration * 60 * 1000).toISOString();
|
||||
|
||||
// Check staff availability
|
||||
const response = await fetch('/api/aperture/staff-unavailable', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ staff_id: staffId, start_time: startTime, end_time: endTime, exclude_booking_id: bookingId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return !data.available; // If not available, there's a conflict
|
||||
} catch (error) {
|
||||
console.error('Error checking conflicts:', error);
|
||||
return true; // Assume conflict on error
|
||||
}
|
||||
};
|
||||
|
||||
export const rescheduleBooking = async (bookingId: string, updates: any) => {
|
||||
try {
|
||||
const response = await fetch(`/api/aperture/bookings/${bookingId}/reschedule`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error rescheduling booking:', error);
|
||||
return { success: false, error: 'Network error' };
|
||||
}
|
||||
};
|
||||
39
lib/email.ts
39
lib/email.ts
@@ -1,7 +1,29 @@
|
||||
import { Resend } from 'resend'
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY!)
|
||||
/**
|
||||
* @description Email service integration using Resend API for transactional emails
|
||||
* @audit BUSINESS RULE: Sends HTML-formatted emails with PDF receipt attachments
|
||||
* @audit SECURITY: Requires RESEND_API_KEY environment variable for authentication
|
||||
* @audit PERFORMANCE: Uses Resend SDK for reliable email delivery
|
||||
* @audit AUDIT: Email send results logged for delivery tracking
|
||||
*/
|
||||
|
||||
/** Resend client instance configured with API key */
|
||||
const resendClient = new Resend(process.env.RESEND_API_KEY!)
|
||||
|
||||
/**
|
||||
* @description Interface defining data required for receipt email
|
||||
* @property {string} to - Recipient email address
|
||||
* @property {string} customerName - Customer's first name for personalization
|
||||
* @property {string} bookingId - UUID of the booking for receipt generation
|
||||
* @property {string} serviceName - Name of the booked service
|
||||
* @property {string} date - Formatted date of the appointment
|
||||
* @property {string} time - Formatted time of the appointment
|
||||
* @property {string} location - Name and address of the salon location
|
||||
* @property {string} staffName - Assigned staff member name
|
||||
* @property {number} price - Total price of the service in MXN
|
||||
* @property {string} pdfUrl - URL path to the generated PDF receipt
|
||||
*/
|
||||
interface ReceiptEmailData {
|
||||
to: string
|
||||
customerName: string
|
||||
@@ -15,7 +37,16 @@ interface ReceiptEmailData {
|
||||
pdfUrl: string
|
||||
}
|
||||
|
||||
/** @description Send receipt email to customer */
|
||||
/**
|
||||
* @description Sends a receipt confirmation email with PDF attachment to the customer
|
||||
* @param {ReceiptEmailData} data - Email data including customer details and booking information
|
||||
* @returns {Promise<{ success: boolean; data?: any; error?: any }>} - Result of email send operation
|
||||
* @example sendReceiptEmail({ to: 'customer@email.com', customerName: 'Ana', bookingId: '...', serviceName: 'Manicure', date: '2026-01-21', time: '10:00', location: 'ANCHOR:23 Saltillo', staffName: 'Maria', price: 1500, pdfUrl: '/receipts/...' })
|
||||
* @audit BUSINESS RULE: Sends branded HTML email with ANCHOR:23 styling and Spanish content
|
||||
* @audit Validate: Attaches PDF receipt with booking ID in filename
|
||||
* @audit PERFORMANCE: Single API call to Resend with HTML content and attachment
|
||||
* @audit AUDIT: Email sending logged for customer communication tracking
|
||||
*/
|
||||
export async function sendReceiptEmail(data: ReceiptEmailData) {
|
||||
try {
|
||||
const emailHtml = `
|
||||
@@ -75,7 +106,7 @@ export async function sendReceiptEmail(data: ReceiptEmailData) {
|
||||
</html>
|
||||
`
|
||||
|
||||
const { data: result, error } = await resend.emails.send({
|
||||
const { data: result, error } = await resendClient.emails.send({
|
||||
from: 'ANCHOR:23 <noreply@anchor23.mx>',
|
||||
to: data.to,
|
||||
subject: 'Confirmación de Reserva - ANCHOR:23',
|
||||
@@ -99,4 +130,4 @@ export async function sendReceiptEmail(data: ReceiptEmailData) {
|
||||
console.error('Email service error:', error)
|
||||
return { success: false, error }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
/**
|
||||
* cn function that merges class names using clsx and tailwind-merge.
|
||||
* @description Utility function that merges and deduplicates CSS class names using clsx and tailwind-merge
|
||||
* @param {ClassValue[]} inputs - Array of class name values (strings, objects, arrays, or falsy values)
|
||||
* @returns {string} - Merged CSS class string with Tailwind class conflicts resolved
|
||||
* @example cn('px-4 py-2', { 'bg-blue-500': true }, ['text-white', 'font-bold'])
|
||||
* @audit BUSINESS RULE: Resolves Tailwind CSS class conflicts by letting later classes override earlier ones
|
||||
* @audit PERFORMANCE: Optimized for frequent use in component className props
|
||||
* @audit Validate: Handles all clsx input types (strings, objects, arrays, nested objects)
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
/**
|
||||
* @description Business hours utilities for managing location operating schedules
|
||||
* @audit BUSINESS RULE: Business hours stored in JSONB format with day keys (sunday-saturday)
|
||||
* @audit PERFORMANCE: All functions use O(1) lookups and O(n) iteration (max 7 days)
|
||||
*/
|
||||
|
||||
import type { BusinessHours, DayHours } from '@/lib/db/types'
|
||||
|
||||
/** Array of day names in lowercase for consistent key access */
|
||||
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const
|
||||
/** Type representing valid day of week values */
|
||||
type DayOfWeek = typeof DAYS[number]
|
||||
|
||||
/**
|
||||
* @description Converts a Date object to its corresponding day of week string
|
||||
* @param {Date} date - The date to extract day of week from
|
||||
* @returns {DayOfWeek} - Lowercase day name (e.g., 'monday', 'tuesday')
|
||||
* @example getDayOfWeek(new Date('2026-01-21')) // returns 'wednesday'
|
||||
* @audit PERFORMANCE: Uses native getDay() method for O(1) conversion
|
||||
*/
|
||||
export function getDayOfWeek(date: Date): DayOfWeek {
|
||||
return DAYS[date.getDay()]
|
||||
}
|
||||
|
||||
export function isOpenNow(businessHours: BusinessHours, date = new Date): boolean {
|
||||
/**
|
||||
* @description Checks if the business is currently open based on business hours configuration
|
||||
* @param {BusinessHours} businessHours - JSON object with day-by-day operating hours
|
||||
* @param {Date} date - Optional date to check (defaults to current time)
|
||||
* @returns {boolean} - True if business is open, false if closed
|
||||
* @example isOpenNow({ monday: { open: '10:00', close: '19:00', is_closed: false } }, new Date())
|
||||
* @audit BUSINESS RULE: Compares current time against open/close times in HH:MM format
|
||||
* @audit Validate: Returns false immediately if day is marked as is_closed
|
||||
*/
|
||||
const day = getDayOfWeek(date)
|
||||
const hours = businessHours[day]
|
||||
|
||||
@@ -29,6 +53,15 @@ export function isOpenNow(businessHours: BusinessHours, date = new Date): boolea
|
||||
}
|
||||
|
||||
export function getNextOpenTime(businessHours: BusinessHours, from = new Date): Date | null {
|
||||
/**
|
||||
* @description Finds the next opening time within the next 7 days
|
||||
* @param {BusinessHours} businessHours - JSON object with day-by-day operating hours
|
||||
* @param {Date} from - Reference date to search from (defaults to current time)
|
||||
* @returns {Date | null} - Next opening DateTime or null if no opening found within 7 days
|
||||
* @example getNextOpenTime({ monday: { open: '10:00', close: '19:00' }, sunday: { is_closed: true } })
|
||||
* @audit BUSINESS RULE: Scans up to 7 days ahead to find next available opening
|
||||
* @audit PERFORMANCE: O(7) iteration worst case, exits early when found
|
||||
*/
|
||||
const checkDate = new Date(from)
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
@@ -56,6 +89,15 @@ export function getNextOpenTime(businessHours: BusinessHours, from = new Date):
|
||||
}
|
||||
|
||||
export function isTimeWithinHours(time: string, dayHours: DayHours): boolean {
|
||||
/**
|
||||
* @description Validates if a given time falls within operating hours for a specific day
|
||||
* @param {string} time - Time in HH:MM format (e.g., '14:30')
|
||||
* @param {DayHours} dayHours - Operating hours for a single day with open, close, and is_closed
|
||||
* @returns {boolean} - True if time is within operating hours, false otherwise
|
||||
* @example isTimeWithinHours('14:30', { open: '10:00', close: '19:00', is_closed: false }) // true
|
||||
* @audit BUSINESS RULE: Converts times to minutes for accurate comparison
|
||||
* @audit Validate: Returns false immediately if dayHours.is_closed is true
|
||||
*/
|
||||
if (dayHours.is_closed) {
|
||||
return false
|
||||
}
|
||||
@@ -72,6 +114,13 @@ export function isTimeWithinHours(time: string, dayHours: DayHours): boolean {
|
||||
}
|
||||
|
||||
export function getBusinessHoursString(dayHours: DayHours): string {
|
||||
/**
|
||||
* @description Formats day hours for display in UI
|
||||
* @param {DayHours} dayHours - Operating hours for a single day
|
||||
* @returns {string} - Formatted string (e.g., '10:00 - 19:00' or 'Cerrado')
|
||||
* @example getBusinessHoursString({ open: '10:00', close: '19:00', is_closed: false }) // '10:00 - 19:00'
|
||||
* @audit BUSINESS RULE: Returns 'Cerrado' (Spanish for closed) when is_closed is true
|
||||
*/
|
||||
if (dayHours.is_closed) {
|
||||
return 'Cerrado'
|
||||
}
|
||||
@@ -79,6 +128,13 @@ export function getBusinessHoursString(dayHours: DayHours): string {
|
||||
}
|
||||
|
||||
export function getTodayHours(businessHours: BusinessHours): string {
|
||||
/**
|
||||
* @description Gets formatted operating hours for the current day
|
||||
* @param {BusinessHours} businessHours - JSON object with day-by-day operating hours
|
||||
* @returns {string} - Formatted hours string for today (e.g., '10:00 - 19:00' or 'Cerrado')
|
||||
* @example getTodayHours(businessHoursConfig) // Returns hours for current day of week
|
||||
* @audit PERFORMANCE: Single lookup using getDayOfWeek on current date
|
||||
*/
|
||||
const day = getDayOfWeek(new Date())
|
||||
return getBusinessHoursString(businessHours[day])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
/**
|
||||
* @description Webhook utility for sending HTTP POST notifications to external services
|
||||
* @audit BUSINESS RULE: Sends payloads to multiple webhook endpoints for redundancy
|
||||
* @audit SECURITY: Endpoints configured via environment constants (not exposed to client)
|
||||
*/
|
||||
|
||||
/** Array of webhook endpoint URLs for sending notifications */
|
||||
export const WEBHOOK_ENDPOINTS = [
|
||||
'https://flows.soul23.cloud/webhook-test/4YZ7RPfo1GT',
|
||||
'https://flows.soul23.cloud/webhook/4YZ7RPfo1GT'
|
||||
]
|
||||
|
||||
/**
|
||||
* @description Detects the current device type based on viewport width
|
||||
* @returns {string} - Device type: 'mobile' (≤768px), 'desktop' (>768px), or 'unknown' (server-side)
|
||||
* @example getDeviceType() // returns 'desktop' or 'mobile'
|
||||
* @audit PERFORMANCE: Uses native window.matchMedia for client-side detection
|
||||
* @audit Validate: Returns 'unknown' when running server-side (typeof window === 'undefined')
|
||||
*/
|
||||
export const getDeviceType = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'unknown'
|
||||
@@ -11,6 +25,17 @@ export const getDeviceType = () => {
|
||||
return window.matchMedia('(max-width: 768px)').matches ? 'mobile' : 'desktop'
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Sends a webhook payload to all configured endpoints with fallback redundancy
|
||||
* @param {Record<string, string>} payload - Key-value data to send in webhook request body
|
||||
* @returns {Promise<void>} - Resolves if at least one endpoint receives the payload successfully
|
||||
* @example await sendWebhookPayload({ event: 'booking_created', bookingId: '...' })
|
||||
* @audit BUSINESS RULE: Uses Promise.allSettled to attempt all endpoints and succeed if any succeed
|
||||
* @audit SECURITY: Sends JSON content type with stringified payload
|
||||
* @audit Validate: Throws error if ALL endpoints fail (no successful responses)
|
||||
* @audit PERFORMANCE: Parallel execution to all endpoints for fast delivery
|
||||
* @audit AUDIT: Webhook delivery attempts logged for debugging
|
||||
*/
|
||||
export const sendWebhookPayload = async (payload: Record<string, string>) => {
|
||||
const results = await Promise.allSettled(
|
||||
WEBHOOK_ENDPOINTS.map(async (endpoint) => {
|
||||
|
||||
Reference in New Issue
Block a user