diff --git a/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc b/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc index 7f3ff14..7612fb2 100644 Binary files a/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc and b/backend/modules/calendar/__pycache__/schemas.cpython-312.pyc differ diff --git a/backend/modules/calendar/schemas.py b/backend/modules/calendar/schemas.py index 7505c1a..3cec1c7 100644 --- a/backend/modules/calendar/schemas.py +++ b/backend/modules/calendar/schemas.py @@ -12,7 +12,7 @@ class CalendarEventBase(BaseModel): end: Optional[datetime] = None location: Optional[str] = None color: Optional[str] = None # Assuming color exists - all_day: Optional[bool] = None # Add all_day field + all_day: Optional[bool] = False # Add all_day field, default to False tags: Optional[List[str]] = None # Add optional tags @field_validator("tags", mode="before") @@ -51,6 +51,7 @@ class CalendarEventUpdate(BaseModel): class CalendarEventResponse(CalendarEventBase): id: int user_id: int + all_day: bool # Ensure all_day is always present in response tags: List[str] # Keep as List[str], remove default [] @field_validator("tags", mode="before") diff --git a/interfaces/nativeapp/src/components/calendar/EventItem.tsx b/interfaces/nativeapp/src/components/calendar/EventItem.tsx index c8defac..5d7918b 100644 --- a/interfaces/nativeapp/src/components/calendar/EventItem.tsx +++ b/interfaces/nativeapp/src/components/calendar/EventItem.tsx @@ -75,12 +75,15 @@ const EventItem: React.FC = ({ event, showTime = true }) => { navigation.navigate('EventForm', { eventId: event.id }); }; - const timeString = showTime ? format(startDate, 'p') : ''; // Format time like 1:00 PM + // Determine if time should be shown: only if showTime is true AND the event is NOT all_day + const shouldDisplayTime = showTime && !event.all_day; + const timeString = shouldDisplayTime ? format(startDate, 'p') : ''; // Format time like 1:00 PM return ( - {showTime && {timeString} } + {/* Conditionally render time based on shouldDisplayTime */} + {shouldDisplayTime && {timeString} } {event.title} {/* Optional: Display tags if they exist */} diff --git a/interfaces/nativeapp/src/screens/EventFormScreen.tsx b/interfaces/nativeapp/src/screens/EventFormScreen.tsx index 2485d2c..9bafc01 100644 --- a/interfaces/nativeapp/src/screens/EventFormScreen.tsx +++ b/interfaces/nativeapp/src/screens/EventFormScreen.tsx @@ -2,8 +2,8 @@ import React, { useState, useEffect, useCallback } from 'react'; // Add Platform import import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, Platform } from 'react-native'; -// Add Chip -import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText, Chip } from 'react-native-paper'; +// Add Chip and Switch +import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText, Chip, Switch } from 'react-native-paper'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; // Conditionally import DateTimePickerModal only if not on web // Note: This dynamic import might not work as expected depending on the bundler setup. @@ -35,6 +35,7 @@ const EventFormScreen = () => { const [endDate, setEndDate] = useState(null); const [color, setColor] = useState(''); // Basic color input for now const [location, setLocation] = useState(''); // Add location state + const [isAllDay, setIsAllDay] = useState(false); // Add all_day state const [tags, setTags] = useState([]); // Add tags state const [currentTagInput, setCurrentTagInput] = useState(''); // State for tag input field @@ -63,6 +64,7 @@ const EventFormScreen = () => { setDescription(event.description || ''); setColor(event.color || ''); // Use optional color setLocation(event.location || ''); // Set location state + setIsAllDay(event.all_day ?? false); // Set all_day state, default to false if null/undefined setTags(event.tags || []); // Load tags or default to empty array // Ensure dates are Date objects if (event.start && isValid(parseISO(event.start))) { @@ -116,6 +118,7 @@ const EventFormScreen = () => { setEndDate(null); setWebEndDateInput(''); } + setIsAllDay(false); // Default all_day to false for new events setTags([]); // Ensure tags start empty for new event } else { // Default start date to now if creating without a selected date @@ -124,6 +127,7 @@ const EventFormScreen = () => { setWebStartDateInput(formatForWebInput(now)); // Init web input setEndDate(null); setWebEndDateInput(''); + setIsAllDay(false); // Default all_day to false for new events setTags([]); // Ensure tags start empty for new event } }, [eventId, selectedDate]); @@ -281,6 +285,7 @@ const EventFormScreen = () => { end: endDate ? endDate.toISOString() : null, location: location.trim() || null, // Include location color: color.trim() || null, // Include color + all_day: isAllDay, // Include all_day state tags: tags.length > 0 ? tags : null, // Include tags, send null if empty }; @@ -334,8 +339,9 @@ const EventFormScreen = () => { const formatDateTime = (date: Date | null): string => { if (!date) return ''; try { - // Native uses 'MMM d, yyyy, p', web input is handled separately - return format(date, "MMM d, yyyy, p"); + // If all_day, only show the date part + const formatString = isAllDay ? "MMM d, yyyy" : "MMM d, yyyy, p"; + return format(date, formatString); } catch { return "Invalid Date"; } @@ -345,7 +351,9 @@ const EventFormScreen = () => { const formatForWebInput = (date: Date | null): string => { if (!date || !isValid(date)) return ''; try { - return format(date, "yyyy-MM-dd HH:mm"); + // If all_day, only format the date part for the input + const formatString = isAllDay ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm"; + return format(date, formatString); } catch { return ''; // Return empty if formatting fails } @@ -364,6 +372,32 @@ const EventFormScreen = () => { setTags(tags.filter(tag => tag !== tagToRemove)); }; + // --- All Day Toggle Logic --- + const onToggleAllDay = () => { + const newAllDay = !isAllDay; + setIsAllDay(newAllDay); + + // Adjust dates when toggling all_day + if (startDate) { + const newStartDate = new Date(startDate); + if (newAllDay) { + // Set time to beginning of the day + newStartDate.setHours(0, 0, 0, 0); + // Optionally clear end date or set it to end of the same day + setEndDate(null); + setWebEndDateInput(''); + } else { + // Restore a default time (e.g., 9 AM) or keep the previous time if stored + newStartDate.setHours(9, 0, 0, 0); + } + setStartDate(newStartDate); + setWebStartDateInput(formatForWebInput(newStartDate)); // Update web input + + // Re-validate if end date was cleared or start date changed + validateForm({ start: newStartDate, end: newAllDay ? null : endDate }); + } + }; + if (isLoading && !title) { // Show loading indicator only during initial fetch return ; } @@ -387,44 +421,54 @@ const EventFormScreen = () => { {formErrors.title} + {/* All Day Switch */} + + All-day event + + {/* Start Date Input - Conditional Logic */} - + handleWebDateInputChange(text, 'start') : undefined} - placeholder={Platform.OS === 'web' ? 'YYYY-MM-DD HH:mm' : ''} + editable={Platform.OS === 'web' && !isAllDay} + onChangeText={Platform.OS === 'web' && !isAllDay ? (text) => handleWebDateInputChange(text, 'start') : undefined} + placeholder={Platform.OS === 'web' ? (isAllDay ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm') : ''} mode="outlined" style={styles.input} - right={Platform.OS !== 'web' ? : null} + right={Platform.OS !== 'web' && !isAllDay ? : } error={!!formErrors.start} + disabled={isAllDay && Platform.OS !== 'web'} // Disable native text input editing for all-day /> {formErrors.start} - {/* End Date Input - Conditional Logic */} - - handleWebDateInputChange(text, 'end') : undefined} - placeholder={Platform.OS === 'web' ? 'YYYY-MM-DD HH:mm' : ''} - mode="outlined" - style={styles.input} - right={Platform.OS !== 'web' ? : null} - error={!!formErrors.end} - /> - - - {formErrors.end} - + {/* End Date Input - Conditionally render based on all_day */} + {!isAllDay && ( + <> + + handleWebDateInputChange(text, 'end') : undefined} + placeholder={Platform.OS === 'web' ? 'YYYY-MM-DD HH:mm' : ''} + mode="outlined" + style={styles.input} + right={Platform.OS !== 'web' ? : null} + error={!!formErrors.end} + /> + + + {formErrors.end} + + + )} {/* Add Location Input */} { <> - + {/* Only show end date picker if not all_day */} + {!isAllDay && ( + + )} )} @@ -549,6 +596,13 @@ const styles = StyleSheet.create({ input: { marginBottom: 5, // Reduced margin as HelperText adds space }, + switchContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 15, // Add some space below the switch + paddingHorizontal: 4, // Align text with TextInput label + }, button: { marginTop: 15, }, diff --git a/interfaces/nativeapp/src/types/calendar.ts b/interfaces/nativeapp/src/types/calendar.ts index 581a39e..eedf78d 100644 --- a/interfaces/nativeapp/src/types/calendar.ts +++ b/interfaces/nativeapp/src/types/calendar.ts @@ -8,6 +8,7 @@ export interface CalendarEvent { end?: string | null; // ISO string format location?: string | null; // Make optional color?: string | null; // Keep optional + all_day?: boolean | null; // Add all_day field tags?: string[]; // Add optional tags array user_id: number; // Add user_id as it's in the response } @@ -20,6 +21,7 @@ export type CalendarEventCreate = { end?: string | null; // ISO string format location?: string | null; color?: string | null; + all_day?: boolean | null; // Add all_day field tags?: string[]; // Add optional tags array }; @@ -31,5 +33,6 @@ export type CalendarEventUpdate = { end?: string | null; // ISO string format location?: string | null; color?: string | null; + all_day?: boolean | null; // Add all_day field tags?: string[]; // Add optional tags array }; \ No newline at end of file