diff --git a/docs/android-tablet-safe-area-fix.md b/docs/android-tablet-safe-area-fix.md new file mode 100644 index 00000000..aa64663d --- /dev/null +++ b/docs/android-tablet-safe-area-fix.md @@ -0,0 +1,87 @@ +# Android Tablet Safe Area Handling Fix + +## Problem +On Android tablets, the new call view did not hide or properly handle the bottom Android system navigation bar, causing it to overlap with the bottom buttons of the form. + +## Root Cause +The new call screen was not using proper safe area handling for Android devices, specifically: +1. Missing `FocusAwareStatusBar` component for edge-to-edge experience +2. No safe area insets applied to prevent overlap with system UI +3. Bottom buttons were positioned without considering system navigation bar + +## Solution + +### 1. Added FocusAwareStatusBar Component +Added `FocusAwareStatusBar` import and usage to ensure proper edge-to-edge handling on Android: + +```tsx +import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar'; + +// In component render: + +``` + +The `FocusAwareStatusBar` component automatically: +- Makes the navigation bar transparent with overlay behavior +- Sets system UI flags to hide navigation bar when needed +- Provides a seamless edge-to-edge experience + +### 2. Added Safe Area Insets +Imported and used `useSafeAreaInsets` from `react-native-safe-area-context`: + +```tsx +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +const insets = useSafeAreaInsets(); +``` + +### 3. Applied Safe Area Padding +Applied safe area insets to both top and bottom of the screen: + +**Top Padding (ScrollView):** +```tsx + +``` + +**Bottom Padding (Button Container):** +```tsx + +``` + +### 4. Safe Area Implementation Details + +- **Minimum Padding**: Uses `Math.max(insets.bottom, 16)` to ensure at least 16px of padding even when insets are smaller +- **Dynamic Padding**: Adapts to different device configurations and orientations +- **Android Tablets**: Typical navigation bar height is ~48px, which gets properly handled +- **Cross-Platform**: Works on both iOS and Android devices + +## Benefits + +1. **No UI Overlap**: Bottom buttons are no longer hidden behind the system navigation bar +2. **Professional Appearance**: Provides a seamless edge-to-edge experience +3. **Device Compatibility**: Works across different Android tablet sizes and configurations +4. **Accessibility**: Ensures all interactive elements are accessible to users +5. **Consistent UX**: Matches the behavior of other screens in the app + +## Files Modified + +- `/src/app/call/new/index.tsx`: Added safe area handling and FocusAwareStatusBar + +## Testing + +The fix should be tested on: +1. Android tablets with different screen sizes +2. Devices with different navigation bar heights +3. Both portrait and landscape orientations +4. Light and dark themes + +## Future Considerations + +This pattern should be applied to other screens that might have similar issues with system UI overlap on Android devices. \ No newline at end of file diff --git a/expo-env.d.ts b/expo-env.d.ts index 5411fdde..bf3c1693 100644 --- a/expo-env.d.ts +++ b/expo-env.d.ts @@ -1,3 +1,3 @@ /// -// NOTE: This file should not be edited and should be in your git ignore \ No newline at end of file +// NOTE: This file should not be edited and should be in your git ignore diff --git a/jest-platform-setup.ts b/jest-platform-setup.ts index d19ff6dc..411610cb 100644 --- a/jest-platform-setup.ts +++ b/jest-platform-setup.ts @@ -19,4 +19,4 @@ Object.defineProperty(global, 'Platform', { jest.doMock('react-native/Libraries/Utilities/Platform', () => mockPlatform); // Ensure Platform is available in the global scope for React Navigation and other libs -(global as any).Platform = mockPlatform; \ No newline at end of file +(global as any).Platform = mockPlatform; diff --git a/src/api/calls/calls.ts b/src/api/calls/calls.ts index 62513c3e..dfae5528 100644 --- a/src/api/calls/calls.ts +++ b/src/api/calls/calls.ts @@ -87,16 +87,20 @@ export const createCall = async (callData: CreateCallRequest) => { const dispatchEntries: string[] = []; if (callData.dispatchUsers) { - dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`)); + //dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`)); + dispatchEntries.push(...callData.dispatchUsers); } if (callData.dispatchGroups) { - dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`)); + //dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`)); + dispatchEntries.push(...callData.dispatchGroups); } if (callData.dispatchRoles) { - dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`)); + //dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`)); + dispatchEntries.push(...callData.dispatchRoles); } if (callData.dispatchUnits) { - dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`)); + //dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`)); + dispatchEntries.push(...callData.dispatchUnits); } dispatchList = dispatchEntries.join('|'); @@ -130,16 +134,20 @@ export const updateCall = async (callData: UpdateCallRequest) => { const dispatchEntries: string[] = []; if (callData.dispatchUsers) { - dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`)); + //dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`)); + dispatchEntries.push(...callData.dispatchUsers); } if (callData.dispatchGroups) { - dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`)); + //dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`)); + dispatchEntries.push(...callData.dispatchGroups); } if (callData.dispatchRoles) { - dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`)); + //dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`)); + dispatchEntries.push(...callData.dispatchRoles); } if (callData.dispatchUnits) { - dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`)); + //dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`)); + dispatchEntries.push(...callData.dispatchUnits); } dispatchList = dispatchEntries.join('|'); diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index e6ecf903..e49fffdf 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -22,6 +22,7 @@ import { APIProvider } from '@/api'; import { CountlyProvider } from '@/components/common/countly-provider'; import { LiveKitBottomSheet } from '@/components/livekit'; import { PushNotificationModal } from '@/components/push-notification/push-notification-modal'; +import { ToastContainer } from '@/components/toast/toast-container'; import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; import { loadKeepAliveState } from '@/lib/hooks/use-keep-alive'; import { loadSelectedTheme } from '@/lib/hooks/use-selected-theme'; @@ -181,6 +182,7 @@ function Providers({ children }: { children: React.ReactNode }) { + diff --git a/src/app/call/new/index.tsx b/src/app/call/new/index.tsx index a8fbe2a0..ad623221 100644 --- a/src/app/call/new/index.tsx +++ b/src/app/call/new/index.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { ScrollView, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as z from 'zod'; import { createCall } from '@/api/calls/calls'; @@ -20,13 +21,14 @@ import { CustomBottomSheet } from '@/components/ui/bottom-sheet'; import { Box } from '@/components/ui/box'; import { Button, ButtonText } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; +import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar'; import { FormControl, FormControlError, FormControlLabel, FormControlLabelText } from '@/components/ui/form-control'; import { Input, InputField } from '@/components/ui/input'; import { Select, SelectBackdrop, SelectContent, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '@/components/ui/select'; import { Text } from '@/components/ui/text'; import { Textarea, TextareaInput } from '@/components/ui/textarea'; -import { useToast } from '@/components/ui/toast'; import { useAnalytics } from '@/hooks/use-analytics'; +import { useToast } from '@/hooks/use-toast'; import { useCoreStore } from '@/stores/app/core-store'; import { useCallsStore } from '@/stores/calls/store'; import { type DispatchSelection } from '@/stores/dispatch/store'; @@ -102,6 +104,7 @@ interface What3WordsResponse { export default function NewCall() { const { t } = useTranslation(); const { colorScheme } = useColorScheme(); + const insets = useSafeAreaInsets(); const { callPriorities, callTypes, isLoading, error, fetchCallPriorities, fetchCallTypes } = useCallsStore(); const { config } = useCoreStore(); const { trackEvent } = useAnalytics(); @@ -179,17 +182,27 @@ export default function NewCall() { data.longitude = selectedLocation.longitude; } - // TODO: Implement the API call to create a new call - console.log('Creating new call with data:', data); - + // Validate priority and type before proceeding const priority = callPriorities.find((p) => p.Name === data.priority); const type = callTypes.find((t) => t.Name === data.type); + if (!priority) { + toast.error(t('calls.invalid_priority')); + return; + } + + if (!type) { + toast.error(t('calls.invalid_type')); + return; + } + + console.log('Creating new call with data:', data); + const response = await createCall({ name: data.name, nature: data.nature, - priority: priority?.Id || 0, - type: type?.Id || '', + priority: priority.Id, + type: type.Id, note: data.note, address: data.address, latitude: data.latitude, @@ -204,16 +217,7 @@ export default function NewCall() { }); // Show success toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.create_success')} - - ); - }, - }); + toast.success(t('calls.create_success')); // Navigate back to calls list router.push('/calls'); @@ -221,16 +225,7 @@ export default function NewCall() { console.error('Error creating call:', error); // Show error toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.create_error')} - - ); - }, - }); + toast.error(t('calls.create_error')); } }; @@ -288,16 +283,7 @@ export default function NewCall() { */ const handleAddressSearch = async (address: string) => { if (!address.trim()) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.address_required')} - - ); - }, - }); + toast.warning(t('calls.address_required')); return; } @@ -329,16 +315,7 @@ export default function NewCall() { handleLocationSelected(newLocation); // Show success toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.address_found')} - - ); - }, - }); + toast.success(t('calls.address_found')); } else { // Multiple results - show selection bottom sheet setAddressResults(results); @@ -346,31 +323,13 @@ export default function NewCall() { } } else { // Show error toast for no results - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.address_not_found')} - - ); - }, - }); + toast.error(t('calls.address_not_found')); } } catch (error) { console.error('Error geocoding address:', error); // Show error toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.geocoding_error')} - - ); - }, - }); + toast.error(t('calls.geocoding_error')); } finally { setIsGeocodingAddress(false); } @@ -389,16 +348,7 @@ export default function NewCall() { setShowAddressSelection(false); // Show success toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.address_found')} - - ); - }, - }); + toast.success(t('calls.address_found')); }; /** @@ -416,32 +366,14 @@ export default function NewCall() { */ const handleWhat3WordsSearch = async (what3words: string) => { if (!what3words.trim()) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.what3words_required')} - - ); - }, - }); + toast.warning(t('calls.what3words_required')); return; } // Validate what3words format - should be 3 words separated by dots const w3wRegex = /^[a-z]+\.[a-z]+\.[a-z]+$/; if (!w3wRegex.test(what3words.trim().toLowerCase())) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.what3words_invalid_format')} - - ); - }, - }); + toast.warning(t('calls.what3words_invalid_format')); return; } @@ -468,43 +400,16 @@ export default function NewCall() { handleLocationSelected(newLocation); // Show success toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.what3words_found')} - - ); - }, - }); + toast.success(t('calls.what3words_found')); } else { // Show error toast for no results - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.what3words_not_found')} - - ); - }, - }); + toast.error(t('calls.what3words_not_found')); } } catch (error) { console.error('Error geocoding what3words:', error); // Show error toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.what3words_geocoding_error')} - - ); - }, - }); + toast.error(t('calls.what3words_geocoding_error')); } finally { setIsGeocodingWhat3Words(false); } @@ -525,16 +430,7 @@ export default function NewCall() { */ const handlePlusCodeSearch = async (plusCode: string) => { if (!plusCode.trim()) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.plus_code_required')} - - ); - }, - }); + toast.warning(t('calls.plus_code_required')); return; } @@ -562,43 +458,16 @@ export default function NewCall() { handleLocationSelected(newLocation); // Show success toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.plus_code_found')} - - ); - }, - }); + toast.success(t('calls.plus_code_found')); } else { // Show error toast for no results - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.plus_code_not_found')} - - ); - }, - }); + toast.error(t('calls.plus_code_not_found')); } } catch (error) { console.error('Error geocoding plus code:', error); // Show error toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.plus_code_geocoding_error')} - - ); - }, - }); + toast.error(t('calls.plus_code_geocoding_error')); } finally { setIsGeocodingPlusCode(false); } @@ -619,16 +488,7 @@ export default function NewCall() { */ const handleCoordinatesSearch = async (coordinates: string) => { if (!coordinates.trim()) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.coordinates_required')} - - ); - }, - }); + toast.warning(t('calls.coordinates_required')); return; } @@ -637,16 +497,7 @@ export default function NewCall() { const match = coordinates.trim().match(coordRegex); if (!match) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.coordinates_invalid_format')} - - ); - }, - }); + toast.warning(t('calls.coordinates_invalid_format')); return; } @@ -655,16 +506,7 @@ export default function NewCall() { // Validate coordinate ranges if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) { - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.coordinates_out_of_range')} - - ); - }, - }); + toast.warning(t('calls.coordinates_out_of_range')); return; } @@ -692,16 +534,7 @@ export default function NewCall() { handleLocationSelected(newLocation); // Show success toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.coordinates_found')} - - ); - }, - }); + toast.success(t('calls.coordinates_found')); } else { // Even if no address found, still set the location on the map const newLocation = { @@ -713,16 +546,7 @@ export default function NewCall() { handleLocationSelected(newLocation); // Show info toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.coordinates_no_address')} - - ); - }, - }); + toast.info(t('calls.coordinates_no_address')); } } catch (error) { console.error('Error reverse geocoding coordinates:', error); @@ -737,16 +561,7 @@ export default function NewCall() { handleLocationSelected(newLocation); // Show warning toast - toast.show({ - placement: 'top', - render: () => { - return ( - - {t('calls.coordinates_geocoding_error')} - - ); - }, - }); + toast.warning(t('calls.coordinates_geocoding_error')); } finally { setIsGeocodingCoordinates(false); } @@ -768,6 +583,7 @@ export default function NewCall() { return ( <> + - + {t('calls.create_new_call')} @@ -1055,7 +871,7 @@ export default function NewCall() { - + diff --git a/src/app/onboarding.tsx b/src/app/onboarding.tsx index e6989c03..7a96679f 100644 --- a/src/app/onboarding.tsx +++ b/src/app/onboarding.tsx @@ -41,10 +41,12 @@ const onboardingData: OnboardingItemProps[] = [ const OnboardingItem: React.FC = ({ title, description, icon }) => { return ( - + {icon} - {title} - {description} + {title} + + {description} + ); }; @@ -64,7 +66,7 @@ export default function Onboarding() { const { status, setIsOnboarding } = useAuthStore(); const router = useRouter(); const [currentIndex, setCurrentIndex] = useState(0); - const flatListRef = useRef(null); // FlashList ref type + const flatListRef = useRef>(null); const buttonOpacity = useSharedValue(0); const { colorScheme } = useColorScheme(); @@ -108,18 +110,21 @@ export default function Onboarding() { - } - horizontal - showsHorizontalScrollIndicator={false} - pagingEnabled - bounces={false} - keyExtractor={(item) => item.title} - onScroll={handleScroll} - scrollEventThrottle={16} - /> + + } + horizontal + showsHorizontalScrollIndicator={false} + pagingEnabled + bounces={false} + keyExtractor={(item) => item.title} + onScroll={handleScroll} + scrollEventThrottle={16} + className="flex-1" + /> + diff --git a/src/components/toast/__tests__/toast-container.test.tsx b/src/components/toast/__tests__/toast-container.test.tsx new file mode 100644 index 00000000..47b1d2d2 --- /dev/null +++ b/src/components/toast/__tests__/toast-container.test.tsx @@ -0,0 +1,78 @@ +import { render } from '@testing-library/react-native'; +import React from 'react'; + +import { useToastStore } from '@/stores/toast/store'; + +import { ToastContainer } from '../toast-container'; + +// Mock the toast store +jest.mock('@/stores/toast/store', () => ({ + useToastStore: jest.fn(), +})); + +// Mock react-native-safe-area-context +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn(() => ({ + top: 44, + bottom: 0, + left: 0, + right: 0, + })), +})); + +// Mock the ToastMessage component +jest.mock('../toast', () => ({ + ToastMessage: ({ type, message, title }: any) => { + const { Text } = require('react-native'); + return ( + + {title && `${title}: `} + {message} + + ); + }, +})); + +describe('ToastContainer', () => { + const mockUseToastStore = useToastStore as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders nothing when no toasts are present', () => { + mockUseToastStore.mockReturnValue([]); + + const { queryByTestId } = render(); + + expect(queryByTestId('toast-success')).toBeNull(); + expect(queryByTestId('toast-error')).toBeNull(); + }); + + it('renders toasts when they are present in the store', () => { + const mockToasts = [ + { id: '1', type: 'success' as const, message: 'Success message' }, + { id: '2', type: 'error' as const, message: 'Error message', title: 'Error Title' }, + ]; + + mockUseToastStore.mockReturnValue(mockToasts); + + const { getByTestId } = render(); + + expect(getByTestId('toast-success')).toBeTruthy(); + expect(getByTestId('toast-error')).toBeTruthy(); + }); + + it('renders toast with title and message correctly', () => { + const mockToasts = [ + { id: '1', type: 'warning' as const, message: 'Warning message', title: 'Warning Title' }, + ]; + + mockUseToastStore.mockReturnValue(mockToasts); + + const { getByTestId } = render(); + + const toastElement = getByTestId('toast-warning'); + expect(toastElement.props.children).toEqual(['Warning Title: ', 'Warning message']); + }); +}); \ No newline at end of file diff --git a/src/components/toast/toast-container.tsx b/src/components/toast/toast-container.tsx index 171cf2a5..3d063244 100644 --- a/src/components/toast/toast-container.tsx +++ b/src/components/toast/toast-container.tsx @@ -1,14 +1,21 @@ import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { VStack } from '@/components/ui/vstack'; import { useToastStore } from '../../stores/toast/store'; import { ToastMessage } from './toast'; + export const ToastContainer: React.FC = () => { const toasts = useToastStore((state) => state.toasts); + const insets = useSafeAreaInsets(); + + // Position below status bar and navigation header + // Use a larger offset to ensure toasts appear below any navigation content + const topOffset = insets.top + 70; // Status bar height + generous navigation header height return ( - + {toasts.map((toast) => ( ))} diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx index f789b3d7..95b88f7e 100644 --- a/src/components/toast/toast.tsx +++ b/src/components/toast/toast.tsx @@ -6,29 +6,6 @@ import { VStack } from '@/components/ui/vstack'; import { type ToastType, useToastStore } from '../../stores/toast/store'; import { Toast, ToastDescription, ToastTitle } from '../ui/toast'; -const toastStyles = { - info: { - bg: '$info700', - borderColor: '$info800', - }, - success: { - bg: '$success700', - borderColor: '$success800', - }, - warning: { - bg: '$warning700', - borderColor: '$warning800', - }, - error: { - bg: '$error700', - borderColor: '$error800', - }, - muted: { - bg: '$muted700', - borderColor: '$muted800', - }, -}; - export const ToastMessage: React.FC<{ //id: string; type: ToastType; @@ -39,7 +16,7 @@ export const ToastMessage: React.FC<{ const { t } = useTranslation(); return ( - + {title && {t(title)}} {t(message)} diff --git a/src/components/ui/bottomsheet/index.tsx b/src/components/ui/bottomsheet/index.tsx index e83bc302..06800a6f 100644 --- a/src/components/ui/bottomsheet/index.tsx +++ b/src/components/ui/bottomsheet/index.tsx @@ -42,8 +42,8 @@ const BottomSheetContext = createContext<{ }>({ visible: false, bottomSheetRef: { current: null }, - handleClose: () => { }, - handleOpen: () => { }, + handleClose: () => {}, + handleOpen: () => {}, }); type IBottomSheetProps = React.ComponentProps; @@ -166,14 +166,14 @@ export const BottomSheetContent = ({ ...props }: IBottomSheetContent) => { const keyDownHandlers = useMemo(() => { return Platform.OS === 'web' ? { - onKeyDown: (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - handleClose(); - return; - } - }, - } + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + handleClose(); + return; + } + }, + } : {}; }, [handleClose]); diff --git a/src/hooks/__tests__/use-toast.test.tsx b/src/hooks/__tests__/use-toast.test.tsx new file mode 100644 index 00000000..5ce94243 --- /dev/null +++ b/src/hooks/__tests__/use-toast.test.tsx @@ -0,0 +1,69 @@ +import { renderHook } from '@testing-library/react-native'; + +import { useToast } from '@/hooks/use-toast'; +import { useToastStore } from '@/stores/toast/store'; + +// Mock the toast store +jest.mock('@/stores/toast/store', () => ({ + useToastStore: jest.fn(), +})); + +describe('useToast', () => { + const mockShowToast = jest.fn(); + const mockUseToastStore = useToastStore as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseToastStore.mockReturnValue({ + showToast: mockShowToast, + }); + }); + + it('should provide show method that calls showToast', () => { + const { result } = renderHook(() => useToast()); + + result.current.show('info', 'Test message', 'Test title'); + + expect(mockShowToast).toHaveBeenCalledWith('info', 'Test message', 'Test title'); + }); + + it('should provide success method that calls showToast with success type', () => { + const { result } = renderHook(() => useToast()); + + result.current.success('Success message', 'Success title'); + + expect(mockShowToast).toHaveBeenCalledWith('success', 'Success message', 'Success title'); + }); + + it('should provide error method that calls showToast with error type', () => { + const { result } = renderHook(() => useToast()); + + result.current.error('Error message', 'Error title'); + + expect(mockShowToast).toHaveBeenCalledWith('error', 'Error message', 'Error title'); + }); + + it('should provide warning method that calls showToast with warning type', () => { + const { result } = renderHook(() => useToast()); + + result.current.warning('Warning message', 'Warning title'); + + expect(mockShowToast).toHaveBeenCalledWith('warning', 'Warning message', 'Warning title'); + }); + + it('should provide info method that calls showToast with info type', () => { + const { result } = renderHook(() => useToast()); + + result.current.info('Info message', 'Info title'); + + expect(mockShowToast).toHaveBeenCalledWith('info', 'Info message', 'Info title'); + }); + + it('should work without title parameter', () => { + const { result } = renderHook(() => useToast()); + + result.current.success('Success message'); + + expect(mockShowToast).toHaveBeenCalledWith('success', 'Success message', undefined); + }); +}); \ No newline at end of file diff --git a/src/translations/ar.json b/src/translations/ar.json index 160b5c50..9ab68a22 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -231,6 +231,8 @@ }, "geocoding_error": "فشل البحث عن العنوان، يرجى المحاولة مرة أخرى", "groups": "المجموعات", + "invalid_priority": "أولوية غير صحيحة محددة. يرجى اختيار أولوية صحيحة.", + "invalid_type": "نوع غير صحيح محدد. يرجى اختيار نوع مكالمة صحيح.", "loading": "جاري تحميل المكالمات...", "loading_calls": "جاري تحميل المكالمات...", "name": "الاسم", diff --git a/src/translations/en.json b/src/translations/en.json index 9ed96f77..d37836e2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -231,6 +231,8 @@ }, "geocoding_error": "Failed to search for address, please try again", "groups": "Groups", + "invalid_priority": "Invalid priority selected. Please select a valid priority.", + "invalid_type": "Invalid type selected. Please select a valid call type.", "loading": "Loading calls...", "loading_calls": "Loading calls...", "name": "Name", diff --git a/src/translations/es.json b/src/translations/es.json index ac432e47..ee1b4a29 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -231,6 +231,8 @@ }, "geocoding_error": "Error al buscar la dirección, por favor inténtelo de nuevo", "groups": "Grupos", + "invalid_priority": "Prioridad inválida seleccionada. Por favor seleccione una prioridad válida.", + "invalid_type": "Tipo inválido seleccionado. Por favor seleccione un tipo de llamada válido.", "loading": "Cargando llamadas...", "loading_calls": "Cargando llamadas...", "name": "Nombre",