feat: Add customer appointment details modal and ATM-style currency input

- Add appointment detail modal to CustomerDashboard with payment info display
  - Shows service, date/time, duration, status, and notes
  - Displays payment summary: service price, deposit paid, payment made, amount due
  - Print receipt functionality with secure DOM manipulation
  - Cancel appointment button for upcoming appointments

- Add CurrencyInput component for ATM-style price entry
  - Digits entered as cents, shift left as more digits added (e.g., "1234" → $12.34)
  - Robust input validation: handles keyboard, mobile, paste, drop, IME
  - Only allows integer digits (0-9)

- Update useAppointments hook to map payment fields from backend
  - Converts amounts from cents to dollars for display

- Update Services page to use CurrencyInput for price and deposit fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-09 12:46:10 -05:00
parent 7f389830f8
commit 90fa628cb5
4 changed files with 954 additions and 62 deletions

View File

@@ -52,6 +52,14 @@ export const useAppointments = (filters?: AppointmentFilters) => {
durationMinutes: a.duration_minutes || calculateDuration(a.start_time, a.end_time),
status: a.status as AppointmentStatus,
notes: a.notes || '',
// Payment fields (amounts stored in cents, convert to dollars for display)
depositAmount: a.deposit_amount ? parseFloat(a.deposit_amount) / 100 : null,
depositTransactionId: a.deposit_transaction_id || '',
finalPrice: a.final_price ? parseFloat(a.final_price) / 100 : null,
finalChargeTransactionId: a.final_charge_transaction_id || '',
isVariablePricing: a.is_variable_pricing || false,
remainingBalance: a.remaining_balance ? parseFloat(a.remaining_balance) / 100 : null,
overpaidAmount: a.overpaid_amount ? parseFloat(a.overpaid_amount) / 100 : null,
}));
},
});
@@ -85,6 +93,14 @@ export const useAppointment = (id: string) => {
durationMinutes: data.duration_minutes || calculateDuration(data.start_time, data.end_time),
status: data.status as AppointmentStatus,
notes: data.notes || '',
// Payment fields (amounts stored in cents, convert to dollars for display)
depositAmount: data.deposit_amount ? parseFloat(data.deposit_amount) / 100 : null,
depositTransactionId: data.deposit_transaction_id || '',
finalPrice: data.final_price ? parseFloat(data.final_price) / 100 : null,
finalChargeTransactionId: data.final_charge_transaction_id || '',
isVariablePricing: data.is_variable_pricing || false,
remainingBalance: data.remaining_balance ? parseFloat(data.remaining_balance) / 100 : null,
overpaidAmount: data.overpaid_amount ? parseFloat(data.overpaid_amount) / 100 : null,
};
},
enabled: !!id,