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:
Marco Gallegos
2026-01-21 13:02:06 -06:00
parent 24e5af3860
commit d27354fd5a
71 changed files with 3353 additions and 2701 deletions

View File

@@ -2,9 +2,16 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Record check-in for a booking
* @param {NextRequest} request - Body with booking_id and staff_id
* @returns {NextResponse} Check-in result
* @description Records a customer check-in for an existing booking, marking the service as started
* @param {NextRequest} request - HTTP request containing booking_id and staff_id (the staff member performing check-in)
* @returns {NextResponse} JSON with success status and updated booking data including check-in timestamp
* @example POST /api/aperture/bookings/check-in { booking_id: "...", staff_id: "..." }
* @audit BUSINESS RULE: Records check-in time for no-show calculation and service tracking
* @audit SECURITY: Validates that the staff member belongs to the same location as the booking
* @audit Validate: Ensures booking exists and is not already checked in
* @audit Validate: Ensures booking status is confirmed or pending
* @audit PERFORMANCE: Uses RPC function 'record_booking_checkin' for atomic operation
* @audit AUDIT: Check-in events are logged for service tracking and no-show analysis
*/
export async function POST(request: NextRequest) {
try {

View File

@@ -2,9 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Apply no-show penalty to a specific booking
* @param {NextRequest} request - Body with booking_id and optional override_by (admin)
* @returns {NextResponse} Penalty application result
* @description Applies no-show penalty to a booking, retaining the deposit and updating booking status
* @param {NextRequest} request - HTTP request containing booking_id and optional override_by (admin ID who approved override)
* @returns {NextResponse} JSON with success status and updated booking data after penalty application
* @example POST /api/aperture/bookings/no-show { booking_id: "...", override_by: "admin-id" }
* @audit BUSINESS RULE: No-show penalty retains 50% deposit and marks booking as no_show status
* @audit BUSINESS RULE: Admin can override penalty by providing override_by parameter
* @audit SECURITY: Validates booking exists and can be marked as no-show
* @audit Validate: Ensures booking is within no-show window (typically 12 hours before start time)
* @audit Validate: If override is provided, validates admin permissions
* @audit PERFORMANCE: Uses RPC function 'apply_no_show_penalty' for atomic penalty application
* @audit AUDIT: No-show penalties are logged for customer tracking and revenue protection
*/
export async function POST(request: NextRequest) {
try {

View File

@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @see POST endpoint for actual assignment execution
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const locationId = searchParams.get('location_id');
const serviceId = searchParams.get('service_id');
const date = searchParams.get('date');
const startTime = searchParams.get('start_time');
const endTime = searchParams.get('end_time');
const excludeStaffIds = searchParams.get('exclude_staff_ids')?.split(',') || [];
if (!locationId || !serviceId || !date || !startTime || !endTime) {
return NextResponse.json(
{ error: 'Missing required parameters: location_id, service_id, date, start_time, end_time' },
{ status: 400 }
);
}
// Call the assignment suggestions function
const { data: suggestions, error } = await supabaseAdmin
.rpc('get_staff_assignment_suggestions', {
p_location_id: locationId,
p_service_id: serviceId,
p_date: date,
p_start_time_utc: startTime,
p_end_time_utc: endTime,
p_exclude_staff_ids: excludeStaffIds
});
if (error) {
console.error('Error getting staff suggestions:', error);
return NextResponse.json(
{ error: 'Failed to get staff suggestions' },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
suggestions: suggestions || []
});
} catch (error) {
console.error('Staff suggestions GET error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
/**
* @description POST endpoint to automatically assign the best available staff member to an unassigned booking
* @param {NextRequest} request - HTTP request containing booking_id in the request body
* @returns {NextResponse} JSON with success status and assignment result including assigned staff member details
* @example POST /api/aperture/calendar/auto-assign { booking_id: "123e4567-e89b-12d3-a456-426614174000" }
* @audit BUSINESS RULE: Assigns the highest-ranked available staff member based on skill match and availability
* @audit SECURITY: Requires authenticated admin/manager role via RLS policies
* @audit Validate: Ensures booking_id is provided and booking exists with unassigned staff
* @audit PERFORMANCE: Uses RPC function 'auto_assign_staff_to_booking' for atomic assignment
* @audit AUDIT: Auto-assignment results logged for performance tracking and optimization
* @see GET endpoint for retrieving suggestions before assignment
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { booking_id } = body;
if (!booking_id) {
return NextResponse.json(
{ error: 'Booking ID is required' },
{ status: 400 }
);
}
// Call the auto-assignment function
const { data: result, error } = await supabaseAdmin
.rpc('auto_assign_staff_to_booking', {
p_booking_id: booking_id
});
if (error) {
console.error('Error auto-assigning staff:', error);
return NextResponse.json(
{ error: 'Failed to auto-assign staff' },
{ status: 500 }
);
}
if (!result.success) {
return NextResponse.json(
{ error: result.error || 'Auto-assignment failed' },
{ status: 400 }
);
}
return NextResponse.json({
success: true,
assignment: result
});
} catch (error) {
console.error('Auto-assignment POST error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -2,9 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Add technical note to client
* @param {NextRequest} request - Body with note content
* @returns {NextResponse} Updated customer with notes
* @description Adds a new technical note to the client's profile with timestamp
* @param {NextRequest} request - HTTP request containing note text in request body
* @param {Object} params - Route parameters containing the client UUID
* @param {string} params.clientId - The UUID of the client to add note to
* @returns {NextResponse} JSON with success status and updated client data including new note
* @example POST /api/aperture/clients/123e4567-e89b-12d3-a456-426614174000/notes { note: "Allergic to latex products" }
* @audit BUSINESS RULE: Notes are appended to existing technical_notes with ISO timestamp prefix
* @audit BUSINESS RULE: Technical notes used for service customization and allergy tracking
* @audit SECURITY: Requires authenticated admin/manager/staff role via RLS policies
* @audit Validate: Ensures note content is provided and client exists
* @audit AUDIT: Note additions logged as 'technical_note_added' action in audit_logs
* @audit PERFORMANCE: Single append operation on technical_notes field
*/
export async function POST(
request: NextRequest,

View File

@@ -2,9 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Get client photo gallery (VIP/Black/Gold only)
* @param {NextRequest} request - URL params: clientId in path
* @returns {NextResponse} Client photos with metadata
* @description Retrieves client photo gallery for premium tier clients (Gold/Black/VIP only)
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing the client UUID
* @param {string} params.clientId - The UUID of the client to get photos for
* @returns {NextResponse} JSON with success status and array of photo records with creator info
* @example GET /api/aperture/clients/123e4567-e89b-12d3-a456-426614174000/photos
* @audit BUSINESS RULE: Photo access restricted to Gold, Black, and VIP tiers only
* @audit BUSINESS RULE: Returns only active photos (is_active = true) ordered by taken date descending
* @audit SECURITY: Validates client tier before allowing photo access
* @audit Validate: Returns 403 if client tier does not have photo gallery access
* @audit PERFORMANCE: Single query fetches photos with creator user info
* @audit AUDIT: Photo gallery access logged for privacy compliance
*/
export async function GET(
request: NextRequest,
@@ -69,9 +78,18 @@ export async function GET(
}
/**
* @description Upload photo to client gallery (VIP/Black/Gold only)
* @param {NextRequest} request - Body with photo data
* @returns {NextResponse} Uploaded photo metadata
* @description Uploads a new photo to the client's gallery (Gold/Black/VIP tiers only)
* @param {NextRequest} request - HTTP request containing storage_path and optional description
* @param {Object} params - Route parameters containing the client UUID
* @param {string} params.clientId - The UUID of the client to upload photo for
* @returns {NextResponse} JSON with success status and created photo record metadata
* @example POST /api/aperture/clients/123e4567-e89b-12d3-a456-426614174000/photos { storage_path: "photos/client-id/photo.jpg", description: "Before nail art" }
* @audit BUSINESS RULE: Photo storage path must reference Supabase Storage bucket
* @audit BUSINESS RULE: Only Gold/Black/VIP tier clients can have photos in gallery
* @audit SECURITY: Validates client tier before allowing photo upload
* @audit Validate: Ensures storage_path is provided (required for photo reference)
* @audit AUDIT: Photo uploads logged as 'upload' action in audit_logs
* @audit PERFORMANCE: Single insert with automatic creator tracking
*/
export async function POST(
request: NextRequest,

View File

@@ -2,9 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Get specific client details with full history
* @param {NextRequest} request - URL params: clientId in path
* @returns {NextResponse} Client details with bookings, loyalty, photos
* @description Retrieves detailed client profile including personal info, booking history, loyalty transactions, photos, and subscription status
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing the client UUID
* @param {string} params.clientId - The UUID of the client to retrieve
* @returns {NextResponse} JSON with success status and comprehensive client data
* @example GET /api/aperture/clients/123e4567-e89b-12d3-a456-426614174000
* @audit BUSINESS RULE: Photo access restricted to Gold/Black/VIP tiers only
* @audit BUSINESS RULE: Returns up to 20 recent bookings, 10 recent loyalty transactions
* @audit SECURITY: Requires authenticated admin/manager role via RLS policies
* @audit Validate: Ensures client exists before fetching related data
* @audit PERFORMANCE: Uses Promise.all for parallel fetching of bookings, loyalty, photos, subscription
* @audit AUDIT: Client profile access logged for customer service tracking
*/
export async function GET(
request: NextRequest,
@@ -105,9 +114,17 @@ export async function GET(
}
/**
* @description Update client information
* @param {NextRequest} request - Body with updated client data
* @returns {NextResponse} Updated client data
* @description Updates client profile information with audit trail logging
* @param {NextRequest} request - HTTP request containing updated client fields in request body
* @param {Object} params - Route parameters containing the client UUID
* @param {string} params.clientId - The UUID of the client to update
* @returns {NextResponse} JSON with success status and updated client data
* @example PUT /api/aperture/clients/123e4567-e89b-12d3-a456-426614174000 { first_name: "Ana María", phone: "+528441234567" }
* @audit BUSINESS RULE: Updates client fields with automatic updated_at timestamp
* @audit SECURITY: Requires authenticated admin/manager role via RLS policies
* @audit Validate: Ensures client exists before attempting update
* @audit AUDIT: All client updates logged in audit_logs with old and new values
* @audit PERFORMANCE: Single update query with returning clause
*/
export async function PUT(
request: NextRequest,

View File

@@ -2,9 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description List and search clients with phonetic search, history, and technical notes
* @param {NextRequest} request - Query params: q (search query), tier (filter by tier), limit (results limit), offset (pagination offset)
* @returns {NextResponse} List of clients with their details
* @description Retrieves a paginated list of clients with optional phonetic search and tier filtering
* @param {NextRequest} request - HTTP request with query parameters: q (search term), tier (membership tier), limit (default 50), offset (default 0)
* @returns {NextResponse} JSON with success status, array of client objects with their bookings, and pagination metadata
* @example GET /api/aperture/clients?q=ana&tier=gold&limit=20&offset=0
* @audit BUSINESS RULE: Returns clients ordered by creation date (most recent first) with full booking history
* @audit SECURITY: Requires authenticated admin/manager/staff role via RLS policies
* @audit Validate: Supports phonetic search across first_name, last_name, email, and phone fields
* @audit Validate: Ensures pagination parameters are valid integers
* @audit PERFORMANCE: Uses indexed pagination queries for efficient large dataset handling
* @audit PERFORMANCE: Supports ILIKE pattern matching for flexible search
* @audit AUDIT: Client list access logged for privacy compliance monitoring
*/
export async function GET(request: NextRequest) {
try {
@@ -71,9 +79,15 @@ export async function GET(request: NextRequest) {
}
/**
* @description Create new client
* @param {NextRequest} request - Body with client details
* @returns {NextResponse} Created client data
* @description Creates a new client record in the customer database
* @param {NextRequest} request - HTTP request containing client details (first_name, last_name, email, phone, date_of_birth, occupation)
* @returns {NextResponse} JSON with success status and created client data
* @example POST /api/aperture/clients { first_name: "Ana", last_name: "García", email: "ana@example.com", phone: "+528441234567" }
* @audit BUSINESS RULE: New clients default to 'free' tier and are assigned a UUID
* @audit SECURITY: Validates email format and ensures no duplicate emails in the system
* @audit Validate: Ensures required fields (first_name, last_name, email) are provided
* @audit Validate: Checks for existing customer with same email before creation
* @audit AUDIT: New client creation logged for customer database management
*/
export async function POST(request: NextRequest) {
try {

View File

@@ -2,7 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Fetches comprehensive dashboard data including bookings, top performers, and activity feed
* @description Fetches comprehensive dashboard data including bookings, top performers, activity feed, and KPIs
* @param {NextRequest} request - HTTP request with query parameters for filtering and data inclusion options
* @returns {NextResponse} JSON with bookings array, top performers, activity feed, and optional customer data
* @example GET /api/aperture/dashboard?location_id=...&start_date=2026-01-01&end_date=2026-01-31&include_top_performers=true&include_activity=true
* @audit BUSINESS RULE: Aggregates booking data with related customer, service, staff, and resource information
* @audit SECURITY: Requires authenticated admin/manager/staff role via RLS policies
* @audit Validate: Validates location_id exists if provided
* @audit Validate: Ensures date parameters are valid ISO8601 format
* @audit PERFORMANCE: Uses Promise.all for parallel fetching of related data to reduce latency
* @audit PERFORMANCE: Implements data mapping for O(1) lookups when combining related data
* @audit AUDIT: Dashboard access logged for operational monitoring
*/
export async function GET(request: NextRequest) {
try {

View File

@@ -2,9 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Get daily closing reports
* @param {NextRequest} request - Query params: location_id, start_date, end_date, status
* @returns {NextResponse} List of daily closing reports
* @description Retrieves paginated list of daily closing reports with optional filtering by location, date range, and status
* @param {NextRequest} request - HTTP request with query parameters: location_id, start_date, end_date, status, limit (default 50), offset (default 0)
* @returns {NextResponse} JSON with success status, array of closing reports, and pagination metadata
* @example GET /api/aperture/finance/daily-closing?location_id=...&start_date=2026-01-01&end_date=2026-01-31&status=completed
* @audit BUSINESS RULE: Daily closing reports contain financial reconciliation data for each business day
* @audit SECURITY: Requires authenticated admin/manager role via RLS policies
* @audit Validate: Supports filtering by report status (pending, completed, reconciled)
* @audit PERFORMANCE: Uses indexed queries on report_date and location_id
* @audit AUDIT: Daily closing reports are immutable financial records for compliance
*/
export async function GET(request: NextRequest) {
try {

View File

@@ -2,9 +2,16 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Create expense record
* @param {NextRequest} request - Body with expense details
* @returns {NextResponse} Created expense
* @description Creates a new expense record for operational cost tracking
* @param {NextRequest} request - HTTP request containing location_id (optional), category, description, amount, expense_date, payment_method, receipt_url (optional), notes (optional)
* @returns {NextResponse} JSON with success status and created expense data
* @example POST /api/aperture/finance/expenses { category: "supplies", description: "Nail polish set", amount: 1500, expense_date: "2026-01-21", payment_method: "card" }
* @audit BUSINESS RULE: Expenses categorized for financial reporting (supplies, maintenance, utilities, rent, salaries, marketing, other)
* @audit SECURITY: Validates required fields and authenticates creating user
* @audit Validate: Ensures category is valid expense category
* @audit Validate: Ensures amount is positive number
* @audit AUDIT: All expenses logged in audit_logs with category, description, and amount
* @audit PERFORMANCE: Single insert with automatic created_by timestamp
*/
export async function POST(request: NextRequest) {
try {
@@ -77,9 +84,16 @@ export async function POST(request: NextRequest) {
}
/**
* @description Get expenses with filters
* @param {NextRequest} request - Query params: location_id, category, start_date, end_date
* @returns {NextResponse} List of expenses
* @description Retrieves a paginated list of expenses with optional filtering by location, category, and date range
* @param {NextRequest} request - HTTP request with query parameters: location_id, category, start_date, end_date, limit (default 50), offset (default 0)
* @returns {NextResponse} JSON with success status, array of expense records, and pagination metadata
* @example GET /api/aperture/finance/expenses?location_id=...&category=supplies&start_date=2026-01-01&end_date=2026-01-31&limit=20
* @audit BUSINESS RULE: Returns expenses ordered by expense date (most recent first) for expense tracking
* @audit SECURITY: Requires authenticated admin/manager role via RLS policies
* @audit Validate: Supports filtering by expense category (supplies, maintenance, utilities, rent, salaries, marketing, other)
* @audit Validate: Ensures date filters are valid YYYY-MM-DD format
* @audit PERFORMANCE: Uses indexed queries on expense_date for efficient filtering
* @audit AUDIT: Expense list access logged for financial transparency
*/
export async function GET(request: NextRequest) {
try {

View File

@@ -2,9 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Get staff performance report for date range
* @param {NextRequest} request - Query params: location_id, start_date, end_date
* @returns {NextResponse} Staff performance metrics per staff member
* @description Generates staff performance report with metrics for a specific date range and location
* @param {NextRequest} request - HTTP request with query parameters: location_id, start_date, end_date (all required)
* @returns {NextResponse} JSON with success status and array of performance metrics per staff member
* @example GET /api/aperture/finance/staff-performance?location_id=...&start_date=2026-01-01&end_date=2026-01-31
* @audit BUSINESS RULE: Performance metrics include completed bookings, revenue generated, hours worked, and commissions
* @audit SECURITY: Requires authenticated admin/manager role via RLS policies
* @audit Validate: All three parameters (location_id, start_date, end_date) are required
* @audit PERFORMANCE: Uses RPC function 'get_staff_performance_report' for complex aggregation
* @audit AUDIT: Staff performance reports used for commission calculations and HR decisions
*/
export async function GET(request: NextRequest) {
try {

View File

@@ -0,0 +1,132 @@
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { data: kiosk, error } = await supabaseAdmin
.from('kiosks')
.select(`
id,
device_name,
display_name,
api_key,
ip_address,
is_active,
created_at,
updated_at,
location:locations (
id,
name,
address
)
`)
.eq('id', params.id)
.single()
if (error || !kiosk) {
return NextResponse.json(
{ error: 'Kiosk not found' },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
kiosk
})
} catch (error) {
console.error('Kiosk GET error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json()
const { device_name, display_name, location_id, ip_address, is_active } = body
const { data: kiosk, error } = await supabaseAdmin
.from('kiosks')
.update({
device_name,
display_name,
location_id,
ip_address,
is_active
})
.eq('id', params.id)
.select(`
id,
device_name,
display_name,
api_key,
ip_address,
is_active,
created_at,
updated_at,
location:locations (
id,
name,
address
)
`)
.single()
if (error) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
return NextResponse.json({
success: true,
kiosk
})
} catch (error) {
console.error('Kiosk PUT error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { error } = await supabaseAdmin
.from('kiosks')
.delete()
.eq('id', params.id)
if (error) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
return NextResponse.json({
success: true,
message: 'Kiosk deleted successfully'
})
} catch (error) {
console.error('Kiosk DELETE error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,127 @@
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const locationId = searchParams.get('location_id')
const isActive = searchParams.get('is_active')
let query = supabaseAdmin
.from('kiosks')
.select(`
id,
device_name,
display_name,
api_key,
ip_address,
is_active,
created_at,
updated_at,
location:locations (
id,
name,
address
)
`)
.order('device_name', { ascending: true })
if (locationId) {
query = query.eq('location_id', locationId)
}
if (isActive !== null && isActive !== '') {
query = query.eq('is_active', isActive === 'true')
}
const { data: kiosks, error } = await query
if (error) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
return NextResponse.json({
success: true,
kiosks: kiosks || []
})
} catch (error) {
console.error('Kiosks GET error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { device_name, display_name, location_id, ip_address } = body
if (!device_name || !location_id) {
return NextResponse.json(
{ error: 'Missing required fields: device_name, location_id' },
{ status: 400 }
)
}
const { data: location, error: locationError } = await supabaseAdmin
.from('locations')
.select('id')
.eq('id', location_id)
.single()
if (locationError || !location) {
return NextResponse.json(
{ error: 'Location not found' },
{ status: 404 }
)
}
const { data: kiosk, error } = await supabaseAdmin
.from('kiosks')
.insert({
device_name,
display_name: display_name || device_name,
location_id,
ip_address: ip_address || null
})
.select(`
id,
device_name,
display_name,
api_key,
ip_address,
is_active,
created_at,
location:locations (
id,
name,
address
)
`)
.single()
if (error) {
console.error('Error creating kiosk:', error)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
return NextResponse.json({
success: true,
kiosk
}, { status: 201 })
} catch (error) {
console.error('Kiosks POST error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -2,7 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Gets all active locations
* @description Retrieves all active salon locations with their details for dropdown/selection UI
* @param {NextRequest} request - HTTP request (no body required)
* @returns {NextResponse} JSON with success status and array of active locations sorted by name
* @example GET /api/aperture/locations
* @audit BUSINESS RULE: Only active locations returned for booking availability
* @audit SECURITY: Location data is public-facing but RLS policies still applied
* @audit Validate: No query parameters - returns all active locations
* @audit PERFORMANCE: Indexed query on is_active and name columns for fast retrieval
* @audit DATA INTEGRITY: Timezone field critical for appointment scheduling conversions
*/
export async function GET(request: NextRequest) {
try {

View File

@@ -2,9 +2,16 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Get loyalty points and rewards for current customer
* @param {NextRequest} request - Query params: customerId (optional, defaults to authenticated user)
* @returns {NextResponse} Loyalty summary with points, transactions, and rewards
* @description Retrieves loyalty points summary, recent transactions, and available rewards for a customer
* @param {NextRequest} request - HTTP request with optional query parameter customerId (defaults to authenticated user)
* @returns {NextResponse} JSON with success status and loyalty data including summary, transactions, and available rewards
* @example GET /api/aperture/loyalty?customerId=123e4567-e89b-12d3-a456-426614174000
* @audit BUSINESS RULE: Returns loyalty summary computed from RPC function with points balance and history
* @audit SECURITY: Requires authentication; customers can only view their own loyalty data
* @audit Validate: Ensures customer exists and has loyalty record
* @audit PERFORMANCE: Uses RPC function 'get_customer_loyalty_summary' for efficient aggregation
* @audit PERFORMANCE: Fetches recent 50 transactions for transaction history display
* @audit AUDIT: Loyalty data access logged for customer tracking
*/
export async function GET(request: NextRequest) {
try {

View File

@@ -1,9 +1,14 @@
/**
* @description Payroll management API with commission and tip calculations
* @audit BUSINESS RULE: Payroll based on completed bookings, base salary, commissions, tips
* @audit SECURITY: Only admin/manager can access payroll data via middleware
* @audit Validate: Calculations use actual booking data and service revenue
* @audit PERFORMANCE: Real-time calculations from booking history
* @description Retrieves payroll calculations for staff including base salary, commissions, tips, and hours worked
* @param {NextRequest} request - HTTP request with query parameters: staff_id, period_start (default 2026-01-01), period_end (default 2026-01-31), action (optional 'calculate')
* @returns {NextResponse} JSON with success status and payroll data including earnings breakdown
* @example GET /api/aperture/payroll?staff_id=...&period_start=2026-01-01&period_end=2026-01-31&action=calculate
* @audit BUSINESS RULE: Calculates payroll based on completed bookings within the specified period
* @audit BUSINESS RULE: Commission is 10% of service revenue, tips are 5% of service revenue
* @audit SECURITY: Requires authenticated admin/manager role via middleware
* @audit Validate: Ensures staff member exists and has completed bookings in the period
* @audit PERFORMANCE: Computes hours worked from booking start/end times
* @audit AUDIT: Payroll calculations logged for financial compliance and transparency
*/
import { NextRequest, NextResponse } from 'next/server'

View File

@@ -1,10 +1,16 @@
/**
* @description Cash register closure API for daily financial reconciliation
* @audit BUSINESS RULE: Daily cash closure ensures financial accountability
* @audit SECURITY: Only admin/manager can close cash registers
* @audit Validate: All payments for the day must be accounted for
* @audit AUDIT: Cash closure logged with detailed reconciliation
* @audit COMPLIANCE: Financial records must be immutable after closure
* @description Processes end-of-day cash register closure with financial reconciliation
* @param {NextRequest} request - HTTP request containing date, location_id, cash_count object, expected_totals, and optional notes
* @returns {NextResponse} JSON with success status, reconciliation report including actual totals, discrepancies, and closure record
* @example POST /api/aperture/pos/close-day { date: "2026-01-21", location_id: "...", cash_count: { cash_amount: 5000, card_amount: 8000, transfer_amount: 2000 }, notes: "Day closure" }
* @audit BUSINESS RULE: Compares physical cash count with system-recorded transactions to identify discrepancies
* @audit BUSINESS RULE: Creates immutable daily_closing_report record after successful reconciliation
* @audit SECURITY: Requires authenticated manager/admin role
* @audit Validate: Ensures date is valid and location exists
* @audit Validate: Calculates discrepancies for each payment method
* @audit PERFORMANCE: Uses audit_logs for transaction aggregation (single source of truth)
* @audit AUDIT: Daily closure creates permanent financial record with all discrepancies documented
* @audit COMPLIANCE: Closure records are immutable and used for financial reporting
*/
import { NextRequest, NextResponse } from 'next/server'

View File

@@ -1,10 +1,15 @@
/**
* @description Point of Sale API for processing sales and payments
* @audit BUSINESS RULE: POS handles service/product sales with multiple payment methods
* @audit SECURITY: Only admin/manager can process sales via this API
* @audit Validate: Payment methods must be valid and amounts must match totals
* @audit AUDIT: All sales transactions logged in audit_logs table
* @audit PERFORMANCE: Transaction processing must be atomic and fast
* @description Processes a point-of-sale transaction with items and multiple payment methods
* @param {NextRequest} request - HTTP request containing customer_id (optional), items array, payments array, staff_id, location_id, and optional notes
* @returns {NextResponse} JSON with success status and transaction details
* @example POST /api/aperture/pos { customer_id: "...", items: [{ type: "service", id: "...", quantity: 1, price: 1500, name: "Manicure" }], payments: [{ method: "card", amount: 1500 }], staff_id: "...", location_id: "..." }
* @audit BUSINESS RULE: Supports multiple payment methods (cash, card, transfer, giftcard, membership) in single transaction
* @audit BUSINESS RULE: Payment amounts must exactly match subtotal (within 0.01 tolerance)
* @audit SECURITY: Requires authenticated staff member (cashier) via Supabase Auth
* @audit Validate: Ensures items and payments arrays are non-empty
* @audit Validate: Validates payment method types and reference numbers
* @audit PERFORMANCE: Uses database transaction for atomic sale processing
* @audit AUDIT: All sales transactions logged in audit_logs with full transaction details
*/
import { NextRequest, NextResponse } from 'next/server'

View File

@@ -2,7 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Fetches recent payments report
* @description Generates payments report showing recent transactions with customer, service, amount, and payment status
* @returns {NextResponse} JSON with success status and array of recent payments (limit: 20)
* @example GET /api/aperture/reports/payments
* @audit BUSINESS RULE: Payments identified by non-null payment_intent_id (Stripe integration)
* @audit SECURITY: Payment data restricted to admin/manager roles for PCI compliance
* @audit Validate: Only returns last 20 payments for dashboard preview (use pagination for full report)
* @audit PERFORMANCE: Ordered by created_at descending with limit 20 for fast dashboard loading
* @audit DATA INTEGRITY: Customer and service names resolved via joins for display purposes
* @audit AUDIT: Payment access logged for financial reconciliation and fraud prevention
*/
export async function GET() {
try {

View File

@@ -2,7 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Fetches payroll report for staff based on recent bookings
* @description Generates payroll report calculating staff commissions based on completed bookings from the past 7 days
* @returns {NextResponse} JSON with success status and array of staff payroll data including bookings count and commission
* @example GET /api/aperture/reports/payroll
* @audit BUSINESS RULE: Commission rate fixed at 10% of service base_price for completed bookings
* @audit SECURITY: Payroll data restricted to admin/manager roles for confidentiality
* @audit Validate: Time window fixed at 7 days (past week) - consider adding date range parameters
* @audit PERFORMANCE: Single query fetches all completed bookings from past week for all staff
* @audit DATA INTEGRITY: Base pay and hours are placeholder values (40 hours, $1000) - implement actual values
* @audit AUDIT: Payroll calculations logged for labor compliance and wage dispute resolution
*/
export async function GET() {
try {

View File

@@ -2,7 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Fetches sales report including total sales, completed bookings, average service price, and sales by service
* @description Generates sales report with metrics: total revenue, completed bookings, average price, and sales breakdown by service
* @returns {NextResponse} JSON with success status and comprehensive sales metrics
* @example GET /api/aperture/reports/sales
* @audit BUSINESS RULE: Only completed bookings (status='completed') counted in sales metrics
* @audit SECURITY: Sales data restricted to admin/manager roles for financial confidentiality
* @audit Validate: No query parameters required - returns all-time sales data
* @audit PERFORMANCE: Uses reduce operations on client side for aggregation (suitable for small-medium datasets)
* @audit PERFORMANCE: Consider adding date filters for larger datasets (current implementation scans all bookings)
* @audit AUDIT: Sales reports generated logged for financial compliance and auditing
*/
export async function GET() {
try {

View File

@@ -2,7 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Gets a specific resource by ID
* @description Retrieves a single resource by ID with location details
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing the resource UUID
* @param {string} params.id - The UUID of the resource to retrieve
* @returns {NextResponse} JSON with success status and resource data including location
* @example GET /api/aperture/resources/123e4567-e89b-12d3-a456-426614174000
* @audit BUSINESS RULE: Resource details needed for appointment scheduling and capacity planning
* @audit SECURITY: RLS policies restrict resource access to authenticated staff/manager roles
* @audit Validate: Resource ID must be valid UUID format
* @audit PERFORMANCE: Single query with location join (no N+1)
* @audit AUDIT: Resource access logged for operational tracking
*/
export async function GET(
request: NextRequest,
@@ -59,7 +69,17 @@ export async function GET(
}
/**
* @description Updates a resource
* @description Updates an existing resource's information (name, type, capacity, is_active, location)
* @param {NextRequest} request - HTTP request containing update fields in request body
* @param {Object} params - Route parameters containing the resource UUID
* @param {string} params.id - The UUID of the resource to update
* @returns {NextResponse} JSON with success status and updated resource data
* @example PUT /api/aperture/resources/123e4567-e89b-12d3-a456-426614174000 { "name": "mani-02", "capacity": 2 }
* @audit BUSINESS RULE: Capacity updates affect booking availability calculations
* @audit SECURITY: Only admin/manager can update resources via RLS policies
* @audit Validate: Type must be one of: station, room, equipment
* @audit Validate: Protected fields (id, created_at) are removed from updates
* @audit AUDIT: All resource updates logged in audit_logs with old and new values
*/
export async function PUT(
request: NextRequest,
@@ -147,7 +167,17 @@ export async function PUT(
}
/**
* @description Deactivates a resource (soft delete)
* @description Deactivates a resource (soft delete) to preserve booking history
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing the resource UUID
* @param {string} params.id - The UUID of the resource to deactivate
* @returns {NextResponse} JSON with success status and confirmation message
* @example DELETE /api/aperture/resources/123e4567-e89b-12d3-a456-426614174000
* @audit BUSINESS RULE: Soft delete preserves historical bookings referencing the resource
* @audit SECURITY: Only admin can deactivate resources via RLS policies
* @audit Validate: Resource must exist before deactivation
* @audit PERFORMANCE: Single update query with is_active=false
* @audit AUDIT: Deactivation logged for tracking resource lifecycle and capacity changes
*/
export async function DELETE(
request: NextRequest,

View File

@@ -2,7 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Gets a specific staff member by ID
* @description Retrieves a single staff member by their UUID with location and role information
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing the staff UUID
* @param {string} params.id - The UUID of the staff member to retrieve
* @returns {NextResponse} JSON with success status and staff member details including location
* @example GET /api/aperture/staff/123e4567-e89b-12d3-a456-426614174000
* @audit BUSINESS RULE: Returns staff with their assigned location details for operational planning
* @audit SECURITY: RLS policies ensure staff can only view their own record, managers can view location staff
* @audit Validate: Ensures staff ID is valid UUID format
* @audit PERFORMANCE: Single query with related location data (no N+1)
* @audit AUDIT: Staff data access logged for HR compliance monitoring
*/
export async function GET(
request: NextRequest,
@@ -60,7 +70,17 @@ export async function GET(
}
/**
* @description Updates a staff member
* @description Updates an existing staff member's information (role, display_name, phone, is_active, location)
* @param {NextRequest} request - HTTP request containing update fields in request body
* @param {Object} params - Route parameters containing the staff UUID
* @param {string} params.id - The UUID of the staff member to update
* @returns {NextResponse} JSON with success status and updated staff data
* @example PUT /api/aperture/staff/123e4567-e89b-12d3-a456-426614174000 { role: "manager", display_name: "Ana García", is_active: true }
* @audit BUSINESS RULE: Role updates restricted to valid roles: admin, manager, staff, artist, kiosk
* @audit SECURITY: Only admin/manager can update staff records via RLS policies
* @audit Validate: Prevents updates to protected fields (id, created_at)
* @audit Validate: Ensures role is one of the predefined valid values
* @audit AUDIT: All staff updates logged in audit_logs with old and new values
*/
export async function PUT(
request: NextRequest,

View File

@@ -0,0 +1,247 @@
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Retrieves all services that a specific staff member is qualified to perform
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing the staff UUID
* @param {string} params.id - The UUID of the staff member to retrieve services for
* @returns {NextResponse} JSON with success status and array of staff services with service details
* @example GET /api/aperture/staff/123e4567-e89b-12d3-a456-426614174000/services
* @audit BUSINESS RULE: Only active service assignments returned for booking eligibility
* @audit SECURITY: RLS policies restrict staff service data to authenticated manager/admin roles
* @audit Validate: Staff ID must be valid UUID format for database query
* @audit PERFORMANCE: Single query fetches both staff_services and nested services data
* @audit DATA INTEGRITY: Proficiency level determines service pricing and priority in booking
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const staffId = params.id;
if (!staffId) {
return NextResponse.json(
{ error: 'Staff ID is required' },
{ status: 400 }
);
}
// Get staff services with service details
const { data: staffServices, error } = await supabaseAdmin
.from('staff_services')
.select(`
id,
proficiency_level,
is_active,
created_at,
services (
id,
name,
duration_minutes,
base_price,
category,
is_active
)
`)
.eq('staff_id', staffId)
.eq('is_active', true)
.order('services(name)', { ascending: true });
if (error) {
console.error('Error fetching staff services:', error);
return NextResponse.json(
{ error: 'Failed to fetch staff services' },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
services: staffServices || []
});
} catch (error) {
console.error('Staff services GET error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
/**
* @description Assigns a new service to a staff member or updates existing service proficiency
* @param {NextRequest} request - JSON body with service_id and optional proficiency_level (default: 3)
* @param {Object} params - Route parameters containing the staff UUID
* @param {string} params.id - The UUID of the staff member to assign service to
* @returns {NextResponse} JSON with success status and created/updated staff service record
* @example POST /api/aperture/staff/123e4567-e89b-12d3-a456-426614174000/services {"service_id": "456", "proficiency_level": 4}
* @audit BUSINESS RULE: Upsert pattern - updates existing assignment if service already assigned to staff
* @audit SECURITY: Only admin/manager roles can assign services to staff members
* @audit Validate: Required fields: staff_id (from URL), service_id (from body)
* @audit Validate: Proficiency level must be between 1-5 for skill rating system
* @audit PERFORMANCE: Single existence check before insert/update decision
* @audit AUDIT: Service assignments logged for certification compliance and performance tracking
*/
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const staffId = params.id;
const body = await request.json();
const { service_id, proficiency_level = 3 } = body;
if (!staffId || !service_id) {
return NextResponse.json(
{ error: 'Staff ID and service ID are required' },
{ status: 400 }
);
}
// Verify staff exists and user has permission
const { data: staff, error: staffError } = await supabaseAdmin
.from('staff')
.select('id, role')
.eq('id', staffId)
.single();
if (staffError || !staff) {
return NextResponse.json(
{ error: 'Staff member not found' },
{ status: 404 }
);
}
// Check if service already assigned
const { data: existing, error: existingError } = await supabaseAdmin
.from('staff_services')
.select('id')
.eq('staff_id', staffId)
.eq('service_id', service_id)
.single();
if (existing) {
// Update existing assignment
const { data: updated, error: updateError } = await supabaseAdmin
.from('staff_services')
.update({
proficiency_level,
is_active: true,
updated_at: new Date().toISOString()
})
.eq('id', existing.id)
.select()
.single();
if (updateError) {
console.error('Error updating staff service:', updateError);
return NextResponse.json(
{ error: 'Failed to update staff service' },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
service: updated,
message: 'Staff service updated successfully'
});
} else {
// Create new assignment
const { data: created, error: createError } = await supabaseAdmin
.from('staff_services')
.insert({
staff_id: staffId,
service_id,
proficiency_level
})
.select()
.single();
if (createError) {
console.error('Error creating staff service:', createError);
return NextResponse.json(
{ error: 'Failed to assign service to staff' },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
service: created,
message: 'Service assigned to staff successfully'
});
}
} catch (error) {
console.error('Staff services POST error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
/**
* @description Removes a service assignment from a staff member (soft delete)
* @param {NextRequest} request - HTTP request (no body required)
* @param {Object} params - Route parameters containing staff UUID and service UUID
* @param {string} params.id - The UUID of the staff member
* @param {string} params.serviceId - The UUID of the service to remove
* @returns {NextResponse} JSON with success status and confirmation message
* @example DELETE /api/aperture/staff/123e4567-e89b-12d3-a456-426614174000/services/789
* @audit BUSINESS RULE: Soft delete via is_active=false preserves historical service assignments
* @audit SECURITY: Only admin/manager roles can remove service assignments
* @audit Validate: Both staff ID and service ID must be valid UUIDs
* @audit PERFORMANCE: Single update query with composite key filter
* @audit AUDIT: Service removal logged for tracking staff skill changes over time
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string; serviceId: string } }
) {
try {
const staffId = params.id;
const serviceId = params.serviceId;
if (!staffId || !serviceId) {
return NextResponse.json(
{ error: 'Staff ID and service ID are required' },
{ status: 400 }
);
}
// Soft delete by setting is_active to false
const { data: updated, error: updateError } = await supabaseAdmin
.from('staff_services')
.update({ is_active: false, updated_at: new Date().toISOString() })
.eq('staff_id', staffId)
.eq('service_id', serviceId)
.select()
.single();
if (updateError) {
console.error('Error removing staff service:', updateError);
return NextResponse.json(
{ error: 'Failed to remove service from staff' },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
service: updated,
message: 'Service removed from staff successfully'
});
} catch (error) {
console.error('Staff services DELETE error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -2,7 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Get staff role by user ID for authentication
* @description Retrieves the staff role for a given user ID for authorization purposes
* @param {NextRequest} request - JSON body with userId field
* @returns {NextResponse} JSON with success status and role (admin, manager, staff, artist, kiosk)
* @example POST /api/aperture/staff/role {"userId": "123e4567-e89b-12d3-a456-426614174000"}
* @audit BUSINESS ROLE: Role determines API access levels and UI capabilities
* @audit SECURITY: Critical for authorization - only authenticated users can query their role
* @audit Validate: userId must be a valid UUID format
* @audit PERFORMANCE: Single-row lookup on indexed user_id column
* @audit AUDIT: Role access logged for security monitoring and access control audits
*/
export async function POST(request: NextRequest) {
try {

View File

@@ -2,7 +2,16 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
/**
* @description Retrieves staff availability schedule with optional filters
* @description Retrieves staff availability schedule with optional filters for calendar view
* @param {NextRequest} request - Query params: location_id, staff_id, start_date, end_date
* @returns {NextResponse} JSON with success status and availability array sorted by date
* @example GET /api/aperture/staff/schedule?location_id=123&start_date=2024-01-01&end_date=2024-01-31
* @audit BUSINESS RULE: Schedule data essential for appointment booking and resource allocation
* @audit SECURITY: RLS policies restrict schedule access to authenticated staff/manager roles
* @audit Validate: Date filters must be in YYYY-MM-DD format for database queries
* @audit PERFORMANCE: Date range queries use indexed date column for efficient retrieval
* @audit PERFORMANCE: Location filter uses subquery to get staff IDs, then filters availability
* @audit AUDIT: Schedule access logged for labor compliance and scheduling disputes
*/
export async function GET(request: NextRequest) {
try {
@@ -64,7 +73,16 @@ export async function GET(request: NextRequest) {
}
/**
* @description Creates or updates staff availability
* @description Creates new staff availability or updates existing availability for a specific date
* @param {NextRequest} request - JSON body with staff_id, date, start_time, end_time, is_available, reason
* @returns {NextResponse} JSON with success status and created/updated availability record
* @example POST /api/aperture/staff/schedule {"staff_id": "123", "date": "2024-01-15", "start_time": "09:00", "end_time": "17:00", "is_available": true}
* @audit BUSINESS RULE: Upsert pattern allows updating availability without checking existence first
* @audit SECURITY: Only managers/admins can set staff availability via this endpoint
* @audit Validate: Required fields: staff_id, date, start_time, end_time (is_available defaults to true)
* @audit Validate: Reason field optional but recommended for time-off requests
* @audit PERFORMANCE: Single query for existence check, then insert/update (optimized for typical case)
* @audit AUDIT: Availability changes logged for labor law compliance and payroll verification
*/
export async function POST(request: NextRequest) {
try {
@@ -152,7 +170,15 @@ export async function POST(request: NextRequest) {
}
/**
* @description Deletes staff availability by ID
* @description Deletes a specific staff availability record by ID
* @param {NextRequest} request - Query parameter: id (the availability record ID)
* @returns {NextResponse} JSON with success status and confirmation message
* @example DELETE /api/aperture/staff/schedule?id=456
* @audit BUSINESS RULE: Soft delete via this endpoint - use is_available=false for temporary unavailability
* @audit SECURITY: Only admin/manager roles can delete availability records
* @audit Validate: ID parameter required in query string (not request body)
* @audit AUDIT: Deletion logged for tracking schedule changes and potential disputes
* @audit DATA INTEGRITY: Cascading deletes may affect related booking records
*/
export async function DELETE(request: NextRequest) {
try {