Skip to content

Commit 37223c6

Browse files
authored
Merge pull request #60986 from parasharrajat/parasharrajat/schedule-call-list
Display scheduled call popup
2 parents fa08e61 + e236190 commit 37223c6

File tree

19 files changed

+267
-49
lines changed

19 files changed

+267
-49
lines changed
Lines changed: 45 additions & 0 deletions
Loading

src/CONST.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3178,6 +3178,7 @@ const CONST = {
31783178
HEADER: 'header',
31793179
MENTION_ICON: 'mention-icon',
31803180
SMALL_NORMAL: 'small-normal',
3181+
LARGE_NORMAL: 'large-normal',
31813182
},
31823183
COMPANY_CARD: {
31833184
FEED_BANK_NAME: {
@@ -7086,6 +7087,12 @@ const CONST = {
70867087
EMBEDDED_DEMO_WHITELIST: ['http://', 'https://', 'about:'] as string[],
70877088
EMBEDDED_DEMO_IFRAME_TITLE: 'Test Drive',
70887089
},
7090+
7091+
SCHEDULE_CALL_STATUS: {
7092+
CREATED: 'created',
7093+
RESCHEDULED: 'rescheduled',
7094+
CANCELLED: 'cancelled',
7095+
},
70897096
} as const;
70907097

70917098
type Country = keyof typeof CONST.ALL_COUNTRIES;

src/components/ButtonWithDropdownMenu/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function ButtonWithDropdownMenu<IValueType>({
4747
shouldShowSelectedItemCheck = false,
4848
testID,
4949
secondLineText = '',
50+
icon,
5051
}: ButtonWithDropdownMenuProps<IValueType>) {
5152
const theme = useTheme();
5253
const styles = useThemeStyles();
@@ -155,6 +156,7 @@ function ButtonWithDropdownMenu<IValueType>({
155156
isSplitButton={isSplitButton}
156157
testID={testID}
157158
secondLineText={secondLineText}
159+
icon={icon}
158160
/>
159161

160162
{isSplitButton && (
@@ -205,6 +207,7 @@ function ButtonWithDropdownMenu<IValueType>({
205207
innerStyles={[innerStyleDropButton]}
206208
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
207209
secondLineText={secondLineText}
210+
icon={icon}
208211
/>
209212
)}
210213
{(shouldAlwaysShowDropdownMenu || options.length > 1) && !!popoverAnchorPosition && (

src/components/ButtonWithDropdownMenu/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {RefObject} from 'react';
2-
import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
2+
import type {GestureResponderEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native';
33
import type {ValueOf} from 'type-fest';
44
import type {PopoverMenuItem} from '@components/PopoverMenu';
55
import type CONST from '@src/CONST';
@@ -34,8 +34,12 @@ type DropdownOption<TValueType> = {
3434
numberOfLinesTitle?: number;
3535
titleStyle?: ViewStyle;
3636
shouldCloseModalOnSelect?: boolean;
37+
description?: string;
38+
descriptionTextStyle?: StyleProp<TextStyle>;
39+
wrapperStyle?: StyleProp<ViewStyle>;
3740
displayInDefaultIconColor?: boolean;
3841
subMenuItems?: PopoverMenuItem[];
42+
avatarSize?: ValueOf<typeof CONST.AVATAR_SIZE>;
3943
};
4044

4145
type ButtonWithDropdownMenuProps<TValueType> = {
@@ -125,6 +129,9 @@ type ButtonWithDropdownMenuProps<TValueType> = {
125129

126130
/** The second line text displays under the first line */
127131
secondLineText?: string;
132+
133+
/** Icon for main button */
134+
icon?: IconAsset;
128135
};
129136

130137
export type {

src/components/Icon/Illustrations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import FolderOpen from '@assets/images/simple-illustrations/simple-illustration_
101101
import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg';
102102
import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg';
103103
import HandEarth from '@assets/images/simple-illustrations/simple-illustration__handearth.svg';
104+
import HeadSet from '@assets/images/simple-illustrations/simple-illustration__headset.svg';
104105
import HotDogStand from '@assets/images/simple-illustrations/simple-illustration__hotdogstand.svg';
105106
import Hourglass from '@assets/images/simple-illustrations/simple-illustration__hourglass.svg';
106107
import House from '@assets/images/simple-illustrations/simple-illustration__house.svg';
@@ -221,6 +222,7 @@ export {
221222
ThumbsUpStars,
222223
Hands,
223224
HandEarth,
225+
HeadSet,
224226
SmartScan,
225227
Hourglass,
226228
CommentBubbles,

src/languages/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ const translations = {
557557
comments: 'Comments',
558558
sharedIn: 'Shared in',
559559
unreported: 'Unreported',
560+
reschedule: 'Reschedule',
560561
general: 'General',
561562
workspacesTabTitle: 'Workspaces',
562563
},
@@ -6355,6 +6356,7 @@ const translations = {
63556356
dateTime: 'Date & time',
63566357
minutes: '30 minutes',
63576358
},
6359+
callScheduled: 'Call scheduled',
63586360
},
63596361
testDrive: {
63606362
quickAction: {

src/languages/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ const translations = {
548548
comments: 'Comentarios',
549549
sharedIn: 'Compartido en',
550550
unreported: 'No reportado',
551+
reschedule: 'Reprogramar',
551552
general: 'General',
552553
workspacesTabTitle: 'Espacios',
553554
},
@@ -6880,6 +6881,7 @@ const translations = {
68806881
dateTime: 'Fecha y hora',
68816882
minutes: '30 minutos',
68826883
},
6884+
callScheduled: 'Llamada programada',
68836885
},
68846886
testDrive: {
68856887
quickAction: {

src/libs/DateUtils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,16 @@ const isCurrentTimeWithinRange = (startTime: string, endTime: string): boolean =
964964
return isAfter(now, new Date(startTime)) && isBefore(now, new Date(endTime));
965965
};
966966

967+
const formatInTimeZoneWithFallback: typeof formatInTimeZone = (date, timeZone, formatStr, options?) => {
968+
try {
969+
return formatInTimeZone(date, timeZone, formatStr, options);
970+
// On macOs and iOS devices some platform use deprecated old timezone values which results in invalid time string error.
971+
// Try with backward timezone values on error.
972+
} catch {
973+
return formatInTimeZone(date, timezoneBackwardMap[timeZone], formatStr, options);
974+
}
975+
};
976+
967977
const DateUtils = {
968978
isDate,
969979
formatToDayOfWeek,
@@ -1021,6 +1031,7 @@ const DateUtils = {
10211031
isFutureDay,
10221032
getFormattedDateRangeForPerDiem,
10231033
isCurrentTimeWithinRange,
1034+
formatInTimeZoneWithFallback,
10241035
};
10251036

10261037
export default DateUtils;

src/libs/DebugUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,14 @@ function validateReportDraftProperty(key: keyof Report | keyof ReportNameValuePa
572572
data: 'object',
573573
errors: 'object',
574574
});
575+
case 'calendlyCalls':
576+
return validateArray<ArrayElement<ReportNameValuePairs, 'calendlyCalls'>>(value, {
577+
status: 'string',
578+
host: 'number',
579+
eventTime: 'string',
580+
eventURI: 'string',
581+
inserted: 'string',
582+
});
575583
case 'pendingAction':
576584
return validateConstantEnum(value, CONST.RED_BRICK_ROAD_PENDING_ACTION);
577585
case 'pendingFields':
@@ -637,6 +645,7 @@ function validateReportDraftProperty(key: keyof Report | keyof ReportNameValuePa
637645
createReport: CONST.RED_BRICK_ROAD_PENDING_ACTION,
638646
exportFailedTime: CONST.RED_BRICK_ROAD_PENDING_ACTION,
639647
calendlySchedule: CONST.RED_BRICK_ROAD_PENDING_ACTION,
648+
calendlyCalls: CONST.RED_BRICK_ROAD_PENDING_ACTION,
640649
});
641650
}
642651
}

src/libs/Permissions.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,6 @@ function canUseRetractNewDot(betas: OnyxEntry<Beta[]>): boolean {
8282
return !!betas?.includes(CONST.BETAS.RETRACT_NEWDOT) || canUseAllBetas(betas);
8383
}
8484

85-
function canUseCallScheduling() {
86-
return false;
87-
}
88-
8985
function canUseMultiFilesDragAndDrop(betas: OnyxEntry<Beta[]>): boolean {
9086
return !!betas?.includes(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) || canUseAllBetas(betas);
9187
}
@@ -107,7 +103,6 @@ export default {
107103
canUseGlobalReimbursementsOnND,
108104
canUsePrivateDomainOnboarding,
109105
canUseRetractNewDot,
110-
canUseCallScheduling,
111106
canUseMultiLevelTags,
112107
canUseMultiFilesDragAndDrop,
113108
canUsePlaidCompanyCards,

src/libs/actions/ScheduleCall.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {WRITE_COMMANDS} from '@libs/API/types';
66
import Navigation from '@libs/Navigation/Navigation';
77
import ONYXKEYS from '@src/ONYXKEYS';
88
import type {PersonalDetails, ScheduleCallDraft} from '@src/types/onyx';
9+
import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
10+
import type {CalendlyCall} from '@src/types/onyx/ReportNameValuePairs';
911
import {openExternalLink} from './Link';
1012

1113
function getGuideCallAvailabilitySchedule(reportID: string) {
@@ -66,13 +68,30 @@ function clearBookingDraft() {
6668
Onyx.set(`${ONYXKEYS.SCHEDULE_CALL_DRAFT}`, null);
6769
}
6870

69-
function confirmBooking(data: Required<ScheduleCallDraft>, currentUser: PersonalDetails) {
71+
function confirmBooking(data: Required<ScheduleCallDraft>, currentUser: PersonalDetails, timezone?: SelectedTimezone) {
7072
const scheduleURL = `${data.guide.scheduleURL}?name=${encodeURIComponent(currentUser.displayName ?? '')}&email=${encodeURIComponent(
7173
currentUser?.login ?? '',
72-
)}&utm_source=newDot&utm_medium=report&utm_content=${data.reportID}`;
74+
)}&utm_source=newDot&utm_medium=report&utm_content=${data.reportID}&timezone=${timezone}`;
7375

7476
openExternalLink(scheduleURL);
7577
clearBookingDraft();
7678
Navigation.dismissModal();
7779
}
78-
export {getGuideCallAvailabilitySchedule, saveBookingDraft, clearBookingDraft, confirmBooking};
80+
81+
function getEventIDFromURI(eventURI: string) {
82+
const parts = eventURI.split('/');
83+
// Last path in the URI is ID
84+
return parts.slice(-1).at(0);
85+
}
86+
87+
function rescheduleBooking(call: CalendlyCall) {
88+
const rescheduleURL = `https://calendly.com/reschedulings/${getEventIDFromURI(call.eventURI)}`;
89+
openExternalLink(rescheduleURL);
90+
}
91+
92+
function cancelBooking(call: CalendlyCall) {
93+
const cancelURL = `https://calendly.com/cancellations/${getEventIDFromURI(call.eventURI)}`;
94+
openExternalLink(cancelURL);
95+
}
96+
97+
export {getGuideCallAvailabilitySchedule, saveBookingDraft, clearBookingDraft, confirmBooking, rescheduleBooking, cancelBooking};

src/pages/ScheduleCall/ScheduleCallConfirmationPage.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ import CONST from '@src/CONST';
2525
import ONYXKEYS from '@src/ONYXKEYS';
2626
import ROUTES from '@src/ROUTES';
2727
import type SCREENS from '@src/SCREENS';
28-
import type {Timezone} from '@src/types/onyx/PersonalDetails';
2928

3029
function ScheduleCallConfirmationPage() {
3130
const styles = useThemeStyles();
3231
const {translate} = useLocalize();
3332
const [scheduleCallDraft] = useOnyx(`${ONYXKEYS.SCHEDULE_CALL_DRAFT}`, {canBeMissing: false});
3433
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
35-
const timezone: Timezone = currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE;
34+
const userTimezone = currentUserPersonalDetails?.timezone?.selected ? currentUserPersonalDetails?.timezone.selected : CONST.DEFAULT_TIME_ZONE.selected;
35+
3636
const personalDetails = usePersonalDetails();
3737
const route = useRoute<PlatformStackRouteProp<ScheduleCallParamList, typeof SCREENS.SCHEDULE_CALL.CONFIRMATION>>();
3838

@@ -48,8 +48,9 @@ function ScheduleCallConfirmationPage() {
4848
reportID: scheduleCallDraft.reportID,
4949
},
5050
currentUserPersonalDetails,
51+
userTimezone,
5152
);
52-
}, [currentUserPersonalDetails, scheduleCallDraft]);
53+
}, [currentUserPersonalDetails, scheduleCallDraft, userTimezone]);
5354

5455
const guideDetails = useMemo(
5556
() => (scheduleCallDraft?.guide?.accountID ? personalDetails?.[scheduleCallDraft?.guide?.accountID] : null),
@@ -61,12 +62,16 @@ function ScheduleCallConfirmationPage() {
6162
return '';
6263
}
6364
const dateString = format(scheduleCallDraft.date, CONST.DATE.MONTH_DAY_YEAR_FORMAT);
64-
const timeString = `${DateUtils.formatToLocalTime(scheduleCallDraft?.timeSlot)} - ${DateUtils.formatToLocalTime(addMinutes(scheduleCallDraft?.timeSlot, 30))}`;
65+
const timeString = `${DateUtils.formatInTimeZoneWithFallback(scheduleCallDraft?.timeSlot, userTimezone, CONST.DATE.LOCAL_TIME_FORMAT)} - ${DateUtils.formatInTimeZoneWithFallback(
66+
addMinutes(scheduleCallDraft?.timeSlot, 30),
67+
userTimezone,
68+
CONST.DATE.LOCAL_TIME_FORMAT,
69+
)}`;
6570

66-
const timeZoneStirng = timezone.selected ? DateUtils.getZoneAbbreviation(new Date(scheduleCallDraft?.timeSlot), timezone.selected) : '';
71+
const timeZoneStirng = DateUtils.getZoneAbbreviation(new Date(scheduleCallDraft?.timeSlot), userTimezone);
6772

6873
return `${dateString} from ${timeString} ${timeZoneStirng}`;
69-
}, [scheduleCallDraft?.date, scheduleCallDraft?.timeSlot, timezone.selected]);
74+
}, [scheduleCallDraft?.date, scheduleCallDraft?.timeSlot, userTimezone]);
7075

7176
return (
7277
<ScreenWrapper

src/pages/ScheduleCall/ScheduleCallPage.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
1717
import useLocalize from '@hooks/useLocalize';
1818
import useThemeStyles from '@hooks/useThemeStyles';
1919
import {getGuideCallAvailabilitySchedule, saveBookingDraft} from '@libs/actions/ScheduleCall';
20+
import DateUtils from '@libs/DateUtils';
2021
import {getLatestError} from '@libs/ErrorUtils';
2122
import Navigation from '@libs/Navigation/Navigation';
2223
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -25,7 +26,6 @@ import CONST from '@src/CONST';
2526
import ONYXKEYS from '@src/ONYXKEYS';
2627
import ROUTES from '@src/ROUTES';
2728
import type SCREENS from '@src/SCREENS';
28-
import type {Timezone} from '@src/types/onyx/PersonalDetails';
2929
import {isEmptyObject} from '@src/types/utils/EmptyObject';
3030
import AvailableBookingDay from './AvailableBookingDay';
3131

@@ -42,7 +42,7 @@ function ScheduleCallPage() {
4242
const route = useRoute<PlatformStackRouteProp<ScheduleCallParamList, typeof SCREENS.SCHEDULE_CALL.BOOK>>();
4343

4444
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
45-
const timezone: Timezone = currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE;
45+
const userTimezone = currentUserPersonalDetails?.timezone?.selected ? currentUserPersonalDetails?.timezone.selected : CONST.DEFAULT_TIME_ZONE.selected;
4646

4747
const [scheduleCallDraft] = useOnyx(`${ONYXKEYS.SCHEDULE_CALL_DRAFT}`, {canBeMissing: true});
4848
const reportID = route.params?.reportID;
@@ -93,14 +93,14 @@ function ScheduleCallPage() {
9393

9494
const timeSlotMap: Record<string, TimeSlot[]> = {};
9595
allTimeSlots.forEach((timeSlot) => {
96-
const timeSlotDate = format(new Date(timeSlot?.startTime), CONST.DATE.FNS_FORMAT_STRING);
96+
const timeSlotDate = DateUtils.formatInTimeZoneWithFallback(new Date(timeSlot?.startTime), userTimezone, CONST.DATE.FNS_FORMAT_STRING);
9797
if (!timeSlotMap[timeSlotDate]) {
9898
timeSlotMap[timeSlotDate] = [];
9999
}
100100
timeSlotMap[timeSlotDate].push(timeSlot);
101101
});
102102
return timeSlotMap;
103-
}, [calendlySchedule]);
103+
}, [calendlySchedule, userTimezone]);
104104

105105
const selectableDates = Object.keys(timeSlotDateMap).sort(compareAsc);
106106
const firstDate = selectableDates.at(0);
@@ -165,7 +165,7 @@ function ScheduleCallPage() {
165165
</View>
166166
<MenuItemWithTopDescription
167167
interactive={false}
168-
title={timezone.selected}
168+
title={userTimezone}
169169
description={translate('timezonePage.timezone')}
170170
style={[styles.mt3, styles.mb3]}
171171
/>
@@ -187,7 +187,7 @@ function ScheduleCallPage() {
187187
<Button
188188
key={`time-slot-${timeSlot.startTime}`}
189189
large
190-
success={scheduleCallDraft?.timeSlot === timeSlot?.startTime}
190+
success={scheduleCallDraft?.timeSlot === timeSlot.startTime}
191191
onPress={() => {
192192
saveBookingDraft({
193193
timeSlot: timeSlot.startTime,
@@ -202,7 +202,7 @@ function ScheduleCallPage() {
202202
}}
203203
shouldEnableHapticFeedback
204204
style={styles.twoColumnLayoutCol}
205-
text={format(timeSlot.startTime, 'p')}
205+
text={DateUtils.formatInTimeZoneWithFallback(timeSlot.startTime, userTimezone, CONST.DATE.LOCAL_TIME_FORMAT)}
206206
/>
207207
))}
208208
{timeFillerItem}

0 commit comments

Comments
 (0)