mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
+2






![Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)


Dhairyashil Shinde
GitHub
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Anik Dhabal Babu
Keith Williams
Anshumancanrock
Morgan
cal.com
Anas Najam
Eunjae Lee
Amit Sharma
Volnei Munhoz
2d514a2a3e
* feat: companion alert system for browser extension
* feat: implement cross-platform alert system for browser extension
- Add ToastContext.tsx with global toast provider for managing toast state
- Add GlobalToast.tsx centered toast component for web/browser extension
- Modify alerts.ts to be platform-aware (native Alert on iOS/Android, toast on web)
- Add showInfoAlert function for informational alerts
- Update _layout.tsx to wrap app with ToastProvider and GlobalToast
- Export new toast context and alert functions from index files
* style: update toast to white/black design system
- Use white background with gray border
- Use black icons and text colors
- Match companion app design system
* open the toast at the center
* refactor: migrate event-types/index.tsx to unified alert system
- Remove inline toast state (showToast, toastMessage) and showToastMessage function
- Remove inline toast UI component at the end of the file
- Migrate all platform-specific Alert.alert calls to showSuccessAlert/showErrorAlert
- Simplifies code by using cross-platform alert utilities
* refactor: migrate useBookingActions.ts to unified alert system
- Add showSuccessAlert import alongside showErrorAlert
- Migrate all Alert.alert('Success', ...) calls to showSuccessAlert
- Affected handlers: handleSubmitReschedule, handleRescheduleWithValues,
handleSubmitCancel, handleCancelBooking, handleConfirmBooking,
handleRejectBooking, handleSubmitReject, handleInlineConfirm
* refactor: migrate useBookingActionModals.ts to unified alert system
- Add showSuccessAlert import alongside showErrorAlert
- Migrate all Alert.alert('Success', ...) calls to showSuccessAlert
- Affected handlers: handleAddGuests, handleUpdateLocation, handleMarkNoShow
* refactor: migrate medium-priority screens to unified alert system
- AvailabilityDetailScreen.ios.tsx: 4 Alert.alert calls migrated
- RescheduleScreen.tsx/ios/android: 3 Alert.alert calls each migrated
- EditLocationScreen.tsx/ios: 4 Alert.alert calls each migrated
- Removed unused Alert imports
* refactor: migrate remaining medium-priority screens to unified alert system
- BookingDetailScreen.tsx: 2 Alert.alert calls migrated
- AvailabilityListScreen.tsx: 1 Alert.alert call migrated
- AddGuestsScreen.tsx: 6 Alert.alert calls migrated
- MarkNoShowScreen.tsx: 2 Alert.alert calls migrated
- EditAvailabilityOverrideScreen.tsx: 4 Alert.alert calls migrated
- EditAvailabilityOverrideScreen.ios.tsx: 2 Alert.alert calls migrated
- Removed unused Alert imports
* refactor: migrate medium-priority app routes to unified alert system
Migrated 24 app route files from direct Alert.alert() calls to unified
alert utilities (showErrorAlert, showSuccessAlert, showInfoAlert):
- reschedule.tsx, reschedule.ios.tsx
- edit-location.tsx, edit-location.ios.tsx
- add-guests.tsx, add-guests.ios.tsx
- mark-no-show.tsx, mark-no-show.ios.tsx
- view-recordings.tsx, view-recordings.ios.tsx
- meeting-session-details.tsx, meeting-session-details.ios.tsx
- profile-sheet.tsx, profile-sheet.ios.tsx
- edit-availability-hours.tsx, edit-availability-hours.ios.tsx
- edit-availability-day.tsx, edit-availability-day.ios.tsx
- edit-availability-name.tsx, edit-availability-name.ios.tsx
- edit-availability-override.tsx, edit-availability-override.ios.tsx
- booking-detail.tsx, booking-detail.ios.tsx
This enables cross-platform alert support where native Alert.alert()
is used on iOS/Android and toast notifications on web (browser extension).
* refactor: migrate lower-priority components to unified alert system
Migrated Alert.alert calls to showSuccessAlert/showErrorAlert/showInfoAlert in:
- event-types/index.ios.tsx (copy link, delete, duplicate success alerts)
- event-type-detail.tsx (copy link, create/update success, validation errors, info alerts)
- deep-links.ts (error alerts for link failures)
- LogoutButton.tsx (error alert)
- BookingModals.tsx and .ios.tsx (report booking info alerts)
- AdvancedTab.tsx (info alert for unsaved event type)
- BookingListScreen.tsx (success/error/info alerts for bulk actions)
Confirmation dialogs with buttons remain as Alert.alert (out of scope).
* fix: restore Alert import for confirmation dialogs, migrate remaining info alert
- AdvancedTab.tsx: Added back Alert import for timezone selector confirmation dialog (out of scope), migrated private links info alert to showInfoAlert
- deep-links.ts: Added back Alert import for request reschedule confirmation dialog (out of scope)
* some ui dialoes were missing
* fixed multi hours ui issues on Availability detail page for android and web
* fix: add __DEV__ check for web platform in showErrorAlert
The showErrorAlert function was showing error toasts to production users
on web platform, bypassing the __DEV__ check that exists for native platforms.
This fix ensures consistent behavior across all platforms - error alerts
are only shown in development mode, while production errors are logged
to console.
Co-Authored-By: unknown <>
* fix: address dual dialogs on web and onSuccess timing issues
- BookingDetailScreen.tsx: Add Platform.OS === 'android' guard to AlertDialog
to prevent dual dialogs rendering on web (FullScreenModal + AlertDialog)
- RescheduleScreen.android.tsx: Restore original Alert.alert with callback
to ensure onSuccess() is called after user dismisses the alert
Co-Authored-By: unknown <>
* fix: restore Alert.alert with callback for native platforms
- AddGuestsScreen.tsx: Use platform-specific handling - Alert.alert with
callback on iOS/Android, showSuccessAlert on web
- EditAvailabilityOverrideScreen.tsx: Same pattern - wait for user
acknowledgment on native platforms before calling onSuccess()
Co-Authored-By: unknown <>
* fix: RescheduleScreen timing and GlobalToast useState pattern
- RescheduleScreen.tsx: Use platform-specific handling - Alert.alert with
callback on iOS/Android, showSuccessAlert on web
- GlobalToast.tsx: Replace useMemo with useState for Animated.Value to
guarantee instance preservation across renders
Co-Authored-By: unknown <>
* fix: restore Alert.alert with callback for iOS delete success
AvailabilityDetailScreen.ios.tsx: Use Alert.alert with callback to ensure
router.back() is called after user dismisses the success alert
Co-Authored-By: unknown <>
* feat(companion): new event type detail page (#26678)
* fix: skip stale run-ci label check on workflow re-runs (#26590)
* fix: trust community PRs when maintainer pushes to the branch
When a maintainer merges main into a community PR branch, the new SHA
invalidates the run-ci label timing check because the label was added
before the new push. This fix adds a check to trust the PR if the
person who pushed the latest commit has write access.
This handles the case where a maintainer:
- Merges main into a community PR to resolve conflicts
- Pushes any changes to help the contributor
The security model is maintained because:
- Only users with write access can trigger this trust
- The maintainer has reviewed the code by pushing to it
Co-Authored-By: [email protected] <[email protected]>
* fix: skip stale label check on workflow re-runs
Instead of implicitly trusting maintainer pushes (which could be just
a sync action without code review), use github.run_attempt to detect
re-runs. If run_attempt > 1, it means the workflow was explicitly
re-triggered (via run-ci.yml or manual re-run), so we skip the stale
label check.
This avoids the need to remove and re-add the 'run-ci' label after
syncing a community PR with main, while keeping the explicit approval
flow intact.
Co-Authored-By: [email protected] <[email protected]>
* Simplify comment about re-runs in PR workflow
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <[email protected]>
* fix: Text cursor barely visible in calendar event name field (#26563)
* fix: cursor visibility in input fields
* fix: cursor visibility in input fields with suffix addons
* fix: text cursor visibility in input fields
---------
Co-authored-by: Keith Williams <[email protected]>
* fix: PayPal setup page inconsistent spacing and button styling (#26612)
* chore: api v2 generate swagger only in dev (#26617)
* feat: add BUILD_FROM_BRANCH option to release-docker workflow (#26615)
* feat: add BUILD_FROM_BRANCH option to release-docker workflow
Co-Authored-By: [email protected] <[email protected]>
* fix: sanitize branch names with slashes for valid Docker tags
Co-Authored-By: [email protected] <[email protected]>
* refactor: extract common logic into prepare job to avoid duplication
Co-Authored-By: [email protected] <[email protected]>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: add Cubic AI to Devin review integration workflow (#26618)
* feat: add Cubic AI to Devin review integration workflow
Co-Authored-By: [email protected] <[email protected]>
* fix: update permissions to allow posting PR comments
Co-Authored-By: [email protected] <[email protected]>
* fix: remove sensitive API response logging from CI workflow
Address Cubic AI review feedback: Remove the debug log that outputs the
full Devin API response, which could expose sensitive session tokens or
authentication data in CI logs. The session URL is still logged when
successfully extracted.
Co-Authored-By: unknown <>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: enable/disable slots workers via env (#26621)
* chore: enable/disable slots workers via env
* fix: address Cubic AI review feedback
- Fix incorrect JSDoc comment for getSerializableContext method
- Remove debug console.log statement from slots controller
- Fix port suffix condition to preserve original behavior
Co-Authored-By: unknown <>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: USE_POOL env var for api v2 prisma pooling
* fix list numbering in Manual setup section of README (#26620)
* Revert "chore: USE_POOL env var for api v2 prisma pooling"
This reverts commit a97092619f.
* feat: limit badges to 2 with hover/click popover in UserListTable (#26556)
* feat: limit badges to 2 with hover tooltip in UserListTable
Co-Authored-By: [email protected] <[email protected]>
* refactor: reuse LimitedBadges component with clickable popover for mobile
Co-Authored-By: [email protected] <[email protected]>
* refactor: move LimitedBadges to components/ui with hover+click support
Co-Authored-By: [email protected] <[email protected]>
* refactor: fix Biome lint issues in LimitedBadges and UserListTable
- Refactor LimitedBadges to concrete component with BadgeItem type
- Move exports to end of files to comply with useExportsLast rule
- Add explicit types to callback parameters for useExplicitType rule
- Convert nested ternary to if-else for filterType calculation
- Remove unused imports (Row, Table types)
- Update ResponseValueCell and UserListTable to use new LimitedBadges API
Co-Authored-By: [email protected] <[email protected]>
* fix: add proper types for filterType and getFacetedUniqueValues
- Add FilterType import from @calcom/types/data-table
- Add FacetedValue import from @calcom/features/data-table
- Type filterType as FilterType to allow reassignment to different ColumnFilterType values
- Type getFacetedUniqueValues return as Map<FacetedValue, number>
Co-Authored-By: [email protected] <[email protected]>
* add vertical gap
* fix: address code review feedback for LimitedBadges
- Add index to id for unique keys in ResponseValueCell.tsx
- Restore orange variant for group options in UserListTable.tsx
- Fix useMemo dependency array (add t, remove dispatch)
- Fix import ordering with biome
- Convert ternary operators to if-else statements for biome compliance
Co-Authored-By: [email protected] <[email protected]>
* refactor: remove id from BadgeItem type, use label as key
- Remove id field from BadgeItem type in LimitedBadges
- Use label as React key instead of id
- Remove unused rowId parameter from ResponseValueCell
- Update all consumers to not pass id field
Co-Authored-By: [email protected] <[email protected]>
* refactor: use index for key instead of label in LimitedBadges
Co-Authored-By: [email protected] <[email protected]>
* Revert "refactor: use index for key instead of label in LimitedBadges"
This reverts commit 1daaac47e596fd6b5f5583847faa10b131783349.
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: add database-backed feature flag for sidebar tips section (#26516)
* chore: remove tips section from sidebar
Co-Authored-By: [email protected] <[email protected]>
* chore: add feature flag for sidebar tips section
Co-Authored-By: [email protected] <[email protected]>
* chore: use database-backed feature flag for sidebar tips section
Co-Authored-By: [email protected] <[email protected]>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: add automated stale PR completion workflow with Devin API (#26627)
* feat: add stale PR completion workflow with Devin API
Co-Authored-By: [email protected] <[email protected]>
* feat: only trigger for community PRs (non-calcom org members)
Co-Authored-By: [email protected] <[email protected]>
* refactor: use author_association pattern from pr.yml for community check
Co-Authored-By: [email protected] <[email protected]>
* refactor: simplify workflow - use community label check and let Devin gather PR details
Co-Authored-By: [email protected] <[email protected]>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: add Devin PR conflict resolver workflow (#26624)
* feat: add Devin PR conflict resolver workflow
Adds a GitHub Actions workflow that automatically detects PRs with merge
conflicts and spins up Devin sessions to resolve them.
How it works:
1. Triggers on push to main branch (when main updates could cause conflicts)
2. Also supports manual trigger via workflow_dispatch
3. Lists all open PRs and checks their mergeable status
4. For PRs with conflicts (mergeable=false, mergeable_state=dirty):
- Checks if a Devin session was already created (avoids duplicates)
- Creates a new Devin session with instructions to resolve conflicts
- Posts a comment on the PR with the Devin session link
The workflow follows the same pattern as cubic-devin-review.yml for
consistency.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: skip draft PRs in conflict detection workflow
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor: use GitHub Search API to filter draft PRs at API level
Instead of filtering draft PRs in the loop, use the GitHub Search API
with 'draft:false' filter which is more efficient as it filters at the
API level rather than fetching all PRs and filtering locally.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* style: remove explanatory comments from workflow
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* style: remove all unnecessary explanatory comments
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor: use GraphQL for batched PR fetching and labels for tracking
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: add support for resolving conflicts on fork PRs
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: check response.ok before parsing Devin API response
Address Cubic AI review feedback: Add response.ok check before parsing
the response body to explicitly handle HTTP error status codes from the
Devin API. This distinguishes API failures (authentication errors,
server errors) from successful responses that might be missing expected
fields.
Co-Authored-By: unknown <>
* fix: improve Devin API error logging with PR number
Co-Authored-By: unknown <>
* feat: add pr_number input for manual fork PR conflict resolution
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: add maximum 15 PRs safety net limit
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: make stale PR workflow fork-aware with workflow_dispatch support (#26633)
* feat: make stale PR workflow fork-aware with workflow_dispatch support
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: add null check for pr.head.repo in stale PR workflow
When a fork is deleted after a PR is created, pr.head.repo can be null
(documented GitHub API behavior). This would cause a TypeError when
accessing properties like fork, full_name, or clone_url.
Added a null check that fails gracefully with a clear error message
when the source repository has been deleted.
Co-Authored-By: unknown <>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat: reuse existing Devin session for Cubic AI review feedback (#26632)
* feat: reuse existing Devin session for Cubic AI review feedback
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor: parse session ID from PR comments instead of API search
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: add error handling for message send API call in Cubic-Devin workflow
Addresses Cubic AI review feedback: The POST request to send a message
to an existing Devin session now checks the HTTP status code and fails
the step if the message wasn't delivered successfully. This prevents
posting a misleading comment claiming feedback was sent when the API
call actually failed.
Co-Authored-By: unknown <>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: Remove stale label when work is completed (#26638)
* chore: Remove stale label when work is completed
* Mark as ready for review too
* feat: add retry mechanism for UNKNOWN mergeable status PRs (#26635)
* feat: add retry mechanism for UNKNOWN mergeable status PRs
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: remove redundant filtering logic for unknown mergeable status PRs
Address Cubic AI review feedback by:
- Having processPR return isTargetPR in the unknown case (like the conflict case)
- Simplifying the main loop to just push PRs to unknownPRs without re-checking
draft status and devin label (already checked in processPR)
Co-Authored-By: unknown <>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: Add sentry http integration (#26634)
* chore: ignore fork PRs in devin-conflict-resolver workflow (#26640)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* limits and advanced tabs
* availability tab
* basics tab
* black toggles!!
* new recurring, others tab and update basics
* version 1
---------
Co-authored-by: Anik Dhabal Babu <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <[email protected]>
Co-authored-by: Anshumancanrock <[email protected]>
Co-authored-by: Morgan <[email protected]>
Co-authored-by: cal.com <[email protected]>
Co-authored-by: Anas Najam <[email protected]>
Co-authored-by: Eunjae Lee <[email protected]>
Co-authored-by: Amit Sharma <[email protected]>
Co-authored-by: Volnei Munhoz <[email protected]>
* fix: address Cubic AI review feedback
- Add HTTP error handling for Devin API session creation in cubic-devin-review.yml
- Localize aria-label in LimitedBadges.tsx using t() function
- Improve Docker tag sanitization in release-docker.yaml to handle all invalid characters
- Avoid logging raw error objects and sensitive API response data in devin-conflict-resolver.yml
- Fix selectedSchedule dependency array issue in event-type-detail.tsx using ref pattern
Co-Authored-By: unknown <>
* chore: add translation key for show_x_more_items aria-label
Co-Authored-By: unknown <>
* fix: reset schedule ref on id change and add i18n pluralization
- Reset hasAutoSelectedScheduleRef when event type id changes to handle component reuse
- Add proper i18next pluralization variants (_one/_other) for show_x_more_items
Co-Authored-By: unknown <>
* revert: remove i18n changes from LimitedBadges and common.json
Per user request - i18n not needed in companion for now
Co-Authored-By: unknown <>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <[email protected]>
Co-authored-by: Keith Williams <[email protected]>
Co-authored-by: Anshumancanrock <[email protected]>
Co-authored-by: Morgan <[email protected]>
Co-authored-by: cal.com <[email protected]>
Co-authored-by: Anas Najam <[email protected]>
Co-authored-by: Eunjae Lee <[email protected]>
Co-authored-by: Amit Sharma <[email protected]>
Co-authored-by: Volnei Munhoz <[email protected]>
507 lines
14 KiB
TypeScript
507 lines
14 KiB
TypeScript
import type { useRouter } from "expo-router";
|
|
import { useState } from "react";
|
|
import { Alert, Platform } from "react-native";
|
|
import type { Booking } from "@/services/calcom";
|
|
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
|
|
|
|
interface UseBookingActionsParams {
|
|
router: ReturnType<typeof useRouter>;
|
|
cancelMutation: (
|
|
params: { uid: string; reason: string },
|
|
options?: {
|
|
onSuccess?: () => void;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
) => void;
|
|
confirmMutation: (
|
|
params: { uid: string },
|
|
options?: {
|
|
onSuccess?: () => void;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
) => void;
|
|
declineMutation: (
|
|
params: { uid: string; reason?: string },
|
|
options?: {
|
|
onSuccess?: () => void;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
) => void;
|
|
rescheduleMutation: (
|
|
params: { uid: string; start: string; reschedulingReason?: string },
|
|
options?: {
|
|
onSuccess?: () => void;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
) => void;
|
|
isConfirming: boolean;
|
|
isDeclining: boolean;
|
|
isRescheduling: boolean;
|
|
}
|
|
|
|
export const useBookingActions = ({
|
|
router,
|
|
cancelMutation,
|
|
confirmMutation,
|
|
declineMutation,
|
|
rescheduleMutation,
|
|
isConfirming,
|
|
isDeclining,
|
|
isRescheduling,
|
|
}: UseBookingActionsParams) => {
|
|
// Reschedule modal state
|
|
const [showRescheduleModal, setShowRescheduleModal] = useState(false);
|
|
const [rescheduleBooking, setRescheduleBooking] = useState<Booking | null>(null);
|
|
const [rescheduleDate, setRescheduleDate] = useState("");
|
|
const [rescheduleTime, setRescheduleTime] = useState("");
|
|
const [rescheduleReason, setRescheduleReason] = useState("");
|
|
|
|
// Reject modal state
|
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
|
const [rejectBooking, setRejectBooking] = useState<Booking | null>(null);
|
|
const [rejectReason, setRejectReason] = useState("");
|
|
|
|
// Cancel modal state (for Android AlertDialog)
|
|
const [showCancelModal, setShowCancelModal] = useState(false);
|
|
const [cancelBooking, setCancelBooking] = useState<Booking | null>(null);
|
|
const [cancelReason, setCancelReason] = useState("");
|
|
|
|
// Selected booking for actions modal
|
|
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
|
|
|
|
/**
|
|
* Navigate to booking detail page
|
|
*/
|
|
const handleBookingPress = (booking: Booking) => {
|
|
router.push({
|
|
pathname: "/(tabs)/(bookings)/booking-detail",
|
|
params: { uid: booking.uid },
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Open reschedule modal with pre-filled data
|
|
*/
|
|
const handleRescheduleBooking = (booking: Booking) => {
|
|
// Get the start time from booking (prefer startTime, fallback to start)
|
|
const startTimeValue = booking.startTime || booking.start;
|
|
|
|
// Validate we have a valid date
|
|
if (!startTimeValue) {
|
|
showErrorAlert("Error", "Unable to reschedule: booking has no start time");
|
|
return;
|
|
}
|
|
|
|
const currentDate = new Date(startTimeValue);
|
|
|
|
// Check if the date is valid
|
|
if (Number.isNaN(currentDate.getTime())) {
|
|
showErrorAlert("Error", "Unable to reschedule: invalid booking date");
|
|
return;
|
|
}
|
|
|
|
// Use local timezone consistently (not UTC) to avoid date/time mismatch
|
|
const year = currentDate.getFullYear();
|
|
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
|
const day = String(currentDate.getDate()).padStart(2, "0");
|
|
const hours = String(currentDate.getHours()).padStart(2, "0");
|
|
const minutes = String(currentDate.getMinutes()).padStart(2, "0");
|
|
|
|
const dateStr = `${year}-${month}-${day}`;
|
|
const timeStr = `${hours}:${minutes}`;
|
|
|
|
setRescheduleBooking(booking);
|
|
setRescheduleDate(dateStr);
|
|
setRescheduleTime(timeStr);
|
|
setRescheduleReason("");
|
|
setShowRescheduleModal(true);
|
|
};
|
|
|
|
/**
|
|
* Validate and submit reschedule request (uses internal state)
|
|
*/
|
|
const handleSubmitReschedule = () => {
|
|
if (!rescheduleBooking || !rescheduleDate || !rescheduleTime) {
|
|
showErrorAlert("Error", "Please enter both date and time");
|
|
return;
|
|
}
|
|
|
|
// Parse the date and time
|
|
const dateTimeStr = `${rescheduleDate}T${rescheduleTime}:00`;
|
|
const newDateTime = new Date(dateTimeStr);
|
|
|
|
// Validate the date
|
|
if (Number.isNaN(newDateTime.getTime())) {
|
|
showErrorAlert(
|
|
"Error",
|
|
"Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time."
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check if the new time is in the future
|
|
if (newDateTime <= new Date()) {
|
|
showErrorAlert("Error", "Please select a future date and time");
|
|
return;
|
|
}
|
|
|
|
// Convert to UTC ISO string
|
|
const startUtc = newDateTime.toISOString();
|
|
|
|
rescheduleMutation(
|
|
{
|
|
uid: rescheduleBooking.uid,
|
|
start: startUtc,
|
|
reschedulingReason: rescheduleReason || undefined,
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
setShowRescheduleModal(false);
|
|
setRescheduleBooking(null);
|
|
showSuccessAlert("Success", "Booking rescheduled successfully");
|
|
},
|
|
onError: (error) => {
|
|
showErrorAlert("Error", error.message || "Failed to reschedule booking");
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Submit reschedule with provided values (used by RescheduleModal component)
|
|
*/
|
|
const handleRescheduleWithValues = async (
|
|
date: string,
|
|
time: string,
|
|
reason?: string
|
|
): Promise<void> => {
|
|
if (!rescheduleBooking) {
|
|
throw new Error("No booking selected");
|
|
}
|
|
|
|
// Parse the date and time
|
|
const dateTimeStr = `${date}T${time}:00`;
|
|
const newDateTime = new Date(dateTimeStr);
|
|
|
|
// Validate the date
|
|
if (Number.isNaN(newDateTime.getTime())) {
|
|
throw new Error(
|
|
"Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time."
|
|
);
|
|
}
|
|
|
|
// Check if the new time is in the future
|
|
if (newDateTime <= new Date()) {
|
|
throw new Error("Please select a future date and time");
|
|
}
|
|
|
|
// Convert to UTC ISO string
|
|
const startUtc = newDateTime.toISOString();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
rescheduleMutation(
|
|
{
|
|
uid: rescheduleBooking.uid,
|
|
start: startUtc,
|
|
reschedulingReason: reason || undefined,
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
setShowRescheduleModal(false);
|
|
setRescheduleBooking(null);
|
|
showSuccessAlert("Success", "Booking rescheduled successfully");
|
|
resolve();
|
|
},
|
|
onError: (error) => {
|
|
reject(new Error(error.message || "Failed to reschedule booking"));
|
|
},
|
|
}
|
|
);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Close reschedule modal and reset state
|
|
*/
|
|
const handleCloseRescheduleModal = () => {
|
|
setShowRescheduleModal(false);
|
|
setRescheduleBooking(null);
|
|
};
|
|
|
|
/**
|
|
* Open cancel modal for Android, use Alert.prompt for iOS
|
|
*/
|
|
const handleOpenCancelModal = (booking: Booking) => {
|
|
setCancelBooking(booking);
|
|
setCancelReason("");
|
|
setShowCancelModal(true);
|
|
};
|
|
|
|
/**
|
|
* Submit cancel with reason (used by Android AlertDialog)
|
|
*/
|
|
const handleSubmitCancel = () => {
|
|
if (!cancelBooking) return;
|
|
|
|
const reason = cancelReason.trim() || "Event cancelled by host";
|
|
|
|
cancelMutation(
|
|
{ uid: cancelBooking.uid, reason },
|
|
{
|
|
onSuccess: () => {
|
|
setShowCancelModal(false);
|
|
setCancelBooking(null);
|
|
setCancelReason("");
|
|
showSuccessAlert("Success", "Event cancelled successfully");
|
|
},
|
|
onError: (_error) => {
|
|
console.error("Failed to cancel booking");
|
|
showErrorAlert("Error", "Failed to cancel event. Please try again.");
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Close cancel modal and reset state
|
|
*/
|
|
const handleCloseCancelModal = () => {
|
|
setShowCancelModal(false);
|
|
setCancelBooking(null);
|
|
setCancelReason("");
|
|
};
|
|
|
|
/**
|
|
* Show alert and cancel booking (iOS Alert.prompt pattern, Android opens modal)
|
|
*/
|
|
const handleCancelBooking = (booking: Booking) => {
|
|
// For Android and Web, open the cancel modal (AlertDialog on Android, content-modal on Web)
|
|
if (Platform.OS === "android" || Platform.OS === "web") {
|
|
handleOpenCancelModal(booking);
|
|
return;
|
|
}
|
|
|
|
// For iOS, use native Alert.prompt
|
|
Alert.alert("Cancel Event", `Are you sure you want to cancel "${booking.title}"?`, [
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Cancel Event",
|
|
style: "destructive",
|
|
onPress: () => {
|
|
// Prompt for cancellation reason
|
|
Alert.prompt(
|
|
"Cancellation Reason",
|
|
"Please provide a reason for cancelling this booking:",
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Cancel Event",
|
|
style: "destructive",
|
|
onPress: (reason?: string) => {
|
|
const cancellationReason = reason?.trim() || "Event cancelled by host";
|
|
cancelMutation(
|
|
{ uid: booking.uid, reason: cancellationReason },
|
|
{
|
|
onSuccess: () => {
|
|
showSuccessAlert("Success", "Event cancelled successfully");
|
|
},
|
|
onError: (_error) => {
|
|
console.error("Failed to cancel booking");
|
|
showErrorAlert("Error", "Failed to cancel event. Please try again.");
|
|
},
|
|
}
|
|
);
|
|
},
|
|
},
|
|
],
|
|
"plain-text",
|
|
"",
|
|
"default"
|
|
);
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* Show alert and confirm booking
|
|
*/
|
|
const handleConfirmBooking = (booking: Booking) => {
|
|
Alert.alert("Confirm Booking", `Are you sure you want to confirm "${booking.title}"?`, [
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Confirm",
|
|
onPress: () => {
|
|
confirmMutation(
|
|
{ uid: booking.uid },
|
|
{
|
|
onSuccess: () => {
|
|
showSuccessAlert("Success", "Booking confirmed successfully");
|
|
},
|
|
onError: (error) => {
|
|
showErrorAlert("Error", error.message || "Failed to confirm booking");
|
|
},
|
|
}
|
|
);
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* Open reject modal (used by iOS for custom modal UI)
|
|
*/
|
|
const handleOpenRejectModal = (booking: Booking) => {
|
|
setRejectBooking(booking);
|
|
setRejectReason("");
|
|
setShowRejectModal(true);
|
|
};
|
|
|
|
/**
|
|
* Show alert and reject booking (Android Alert.prompt pattern)
|
|
*/
|
|
const handleRejectBooking = (booking: Booking) => {
|
|
Alert.alert("Decline Booking", `Are you sure you want to decline "${booking.title}"?`, [
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Decline",
|
|
style: "destructive",
|
|
onPress: () => {
|
|
// Show optional reason input
|
|
Alert.prompt(
|
|
"Decline Reason",
|
|
"Optionally provide a reason for declining (press OK to skip)",
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "OK",
|
|
onPress: (reason?: string) => {
|
|
declineMutation(
|
|
{ uid: booking.uid, reason: reason || undefined },
|
|
{
|
|
onSuccess: () => {
|
|
showSuccessAlert("Success", "Booking declined successfully");
|
|
},
|
|
onError: (error) => {
|
|
showErrorAlert("Error", error.message || "Failed to decline booking");
|
|
},
|
|
}
|
|
);
|
|
},
|
|
},
|
|
],
|
|
"plain-text",
|
|
"",
|
|
"default"
|
|
);
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
/**
|
|
* Submit rejection with reason (used by iOS custom modal)
|
|
* @param reasonOverride - Optional reason passed directly to avoid race condition with state updates
|
|
*/
|
|
const handleSubmitReject = (reasonOverride?: string) => {
|
|
if (!rejectBooking) return;
|
|
|
|
// Use passed reason if provided, otherwise fall back to state
|
|
const reason = reasonOverride !== undefined ? reasonOverride : rejectReason;
|
|
|
|
declineMutation(
|
|
{ uid: rejectBooking.uid, reason: reason || undefined },
|
|
{
|
|
onSuccess: () => {
|
|
setShowRejectModal(false);
|
|
setRejectBooking(null);
|
|
setRejectReason("");
|
|
showSuccessAlert("Success", "Booking rejected successfully");
|
|
},
|
|
onError: (_error) => {
|
|
showErrorAlert("Error", "Failed to reject booking. Please try again.");
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Close reject modal and reset state
|
|
*/
|
|
const handleCloseRejectModal = () => {
|
|
setShowRejectModal(false);
|
|
setRejectBooking(null);
|
|
setRejectReason("");
|
|
};
|
|
|
|
/**
|
|
* Inline confirm handler (for use directly in JSX)
|
|
*/
|
|
const handleInlineConfirm = (booking: Booking) => {
|
|
confirmMutation(
|
|
{ uid: booking.uid },
|
|
{
|
|
onSuccess: () => {
|
|
showSuccessAlert("Success", "Booking confirmed successfully");
|
|
},
|
|
onError: (_error) => {
|
|
showErrorAlert("Error", "Failed to confirm booking. Please try again.");
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
return {
|
|
// Reschedule state
|
|
showRescheduleModal,
|
|
setShowRescheduleModal,
|
|
rescheduleBooking,
|
|
rescheduleDate,
|
|
setRescheduleDate,
|
|
rescheduleTime,
|
|
setRescheduleTime,
|
|
rescheduleReason,
|
|
setRescheduleReason,
|
|
|
|
// Reject state
|
|
showRejectModal,
|
|
setShowRejectModal,
|
|
rejectBooking,
|
|
rejectReason,
|
|
setRejectReason,
|
|
|
|
// Cancel state (for Android AlertDialog)
|
|
showCancelModal,
|
|
setShowCancelModal,
|
|
cancelBooking,
|
|
cancelReason,
|
|
setCancelReason,
|
|
|
|
// Selected booking state
|
|
selectedBooking,
|
|
setSelectedBooking,
|
|
|
|
// Handlers
|
|
handleBookingPress,
|
|
handleRescheduleBooking,
|
|
handleSubmitReschedule,
|
|
handleRescheduleWithValues,
|
|
handleCloseRescheduleModal,
|
|
handleCancelBooking,
|
|
handleOpenCancelModal,
|
|
handleSubmitCancel,
|
|
handleCloseCancelModal,
|
|
handleConfirmBooking,
|
|
handleOpenRejectModal,
|
|
handleRejectBooking,
|
|
handleSubmitReject,
|
|
handleCloseRejectModal,
|
|
handleInlineConfirm,
|
|
|
|
// Loading states (pass through)
|
|
isConfirming,
|
|
isDeclining,
|
|
isRescheduling,
|
|
};
|
|
};
|