add allday to calendar model

This commit is contained in:
c-d-p
2025-04-23 19:25:50 +02:00
parent 1f8b81e69c
commit 4d41aa9136
5 changed files with 103 additions and 42 deletions

View File

@@ -12,7 +12,7 @@ class CalendarEventBase(BaseModel):
end: Optional[datetime] = None end: Optional[datetime] = None
location: Optional[str] = None location: Optional[str] = None
color: Optional[str] = None # Assuming color exists 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 tags: Optional[List[str]] = None # Add optional tags
@field_validator("tags", mode="before") @field_validator("tags", mode="before")
@@ -51,6 +51,7 @@ class CalendarEventUpdate(BaseModel):
class CalendarEventResponse(CalendarEventBase): class CalendarEventResponse(CalendarEventBase):
id: int id: int
user_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 [] tags: List[str] # Keep as List[str], remove default []
@field_validator("tags", mode="before") @field_validator("tags", mode="before")

View File

@@ -75,12 +75,15 @@ const EventItem: React.FC<EventItemProps> = ({ event, showTime = true }) => {
navigation.navigate('EventForm', { eventId: event.id }); 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 ( return (
<TouchableOpacity onPress={handlePress} style={styles.container}> <TouchableOpacity onPress={handlePress} style={styles.container}>
<Text style={styles.text} numberOfLines={2} ellipsizeMode='clip'> <Text style={styles.text} numberOfLines={2} ellipsizeMode='clip'>
{showTime && <Text style={styles.timeText}>{timeString} </Text>} {/* Conditionally render time based on shouldDisplayTime */}
{shouldDisplayTime && <Text style={styles.timeText}>{timeString} </Text>}
{event.title} {event.title}
</Text> </Text>
{/* Optional: Display tags if they exist */} {/* Optional: Display tags if they exist */}

View File

@@ -2,8 +2,8 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
// Add Platform import // Add Platform import
import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, Platform } from 'react-native'; import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, Platform } from 'react-native';
// Add Chip // Add Chip and Switch
import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText, Chip } from 'react-native-paper'; import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText, Chip, Switch } from 'react-native-paper';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
// Conditionally import DateTimePickerModal only if not on web // Conditionally import DateTimePickerModal only if not on web
// Note: This dynamic import might not work as expected depending on the bundler setup. // Note: This dynamic import might not work as expected depending on the bundler setup.
@@ -35,6 +35,7 @@ const EventFormScreen = () => {
const [endDate, setEndDate] = useState<Date | null>(null); const [endDate, setEndDate] = useState<Date | null>(null);
const [color, setColor] = useState(''); // Basic color input for now const [color, setColor] = useState(''); // Basic color input for now
const [location, setLocation] = useState(''); // Add location state const [location, setLocation] = useState(''); // Add location state
const [isAllDay, setIsAllDay] = useState(false); // Add all_day state
const [tags, setTags] = useState<string[]>([]); // Add tags state const [tags, setTags] = useState<string[]>([]); // Add tags state
const [currentTagInput, setCurrentTagInput] = useState(''); // State for tag input field const [currentTagInput, setCurrentTagInput] = useState(''); // State for tag input field
@@ -63,6 +64,7 @@ const EventFormScreen = () => {
setDescription(event.description || ''); setDescription(event.description || '');
setColor(event.color || ''); // Use optional color setColor(event.color || ''); // Use optional color
setLocation(event.location || ''); // Set location state 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 setTags(event.tags || []); // Load tags or default to empty array
// Ensure dates are Date objects // Ensure dates are Date objects
if (event.start && isValid(parseISO(event.start))) { if (event.start && isValid(parseISO(event.start))) {
@@ -116,6 +118,7 @@ const EventFormScreen = () => {
setEndDate(null); setEndDate(null);
setWebEndDateInput(''); setWebEndDateInput('');
} }
setIsAllDay(false); // Default all_day to false for new events
setTags([]); // Ensure tags start empty for new event setTags([]); // Ensure tags start empty for new event
} else { } else {
// Default start date to now if creating without a selected date // Default start date to now if creating without a selected date
@@ -124,6 +127,7 @@ const EventFormScreen = () => {
setWebStartDateInput(formatForWebInput(now)); // Init web input setWebStartDateInput(formatForWebInput(now)); // Init web input
setEndDate(null); setEndDate(null);
setWebEndDateInput(''); setWebEndDateInput('');
setIsAllDay(false); // Default all_day to false for new events
setTags([]); // Ensure tags start empty for new event setTags([]); // Ensure tags start empty for new event
} }
}, [eventId, selectedDate]); }, [eventId, selectedDate]);
@@ -281,6 +285,7 @@ const EventFormScreen = () => {
end: endDate ? endDate.toISOString() : null, end: endDate ? endDate.toISOString() : null,
location: location.trim() || null, // Include location location: location.trim() || null, // Include location
color: color.trim() || null, // Include color 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 tags: tags.length > 0 ? tags : null, // Include tags, send null if empty
}; };
@@ -334,8 +339,9 @@ const EventFormScreen = () => {
const formatDateTime = (date: Date | null): string => { const formatDateTime = (date: Date | null): string => {
if (!date) return ''; if (!date) return '';
try { try {
// Native uses 'MMM d, yyyy, p', web input is handled separately // If all_day, only show the date part
return format(date, "MMM d, yyyy, p"); const formatString = isAllDay ? "MMM d, yyyy" : "MMM d, yyyy, p";
return format(date, formatString);
} catch { } catch {
return "Invalid Date"; return "Invalid Date";
} }
@@ -345,7 +351,9 @@ const EventFormScreen = () => {
const formatForWebInput = (date: Date | null): string => { const formatForWebInput = (date: Date | null): string => {
if (!date || !isValid(date)) return ''; if (!date || !isValid(date)) return '';
try { 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 { } catch {
return ''; // Return empty if formatting fails return ''; // Return empty if formatting fails
} }
@@ -364,6 +372,32 @@ const EventFormScreen = () => {
setTags(tags.filter(tag => tag !== tagToRemove)); 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 if (isLoading && !title) { // Show loading indicator only during initial fetch
return <ActivityIndicator animating={true} style={styles.loading} />; return <ActivityIndicator animating={true} style={styles.loading} />;
} }
@@ -387,27 +421,35 @@ const EventFormScreen = () => {
{formErrors.title} {formErrors.title}
</HelperText> </HelperText>
{/* All Day Switch */}
<View style={styles.switchContainer}>
<Text>All-day event</Text>
<Switch value={isAllDay} onValueChange={onToggleAllDay} />
</View>
{/* Start Date Input - Conditional Logic */} {/* Start Date Input - Conditional Logic */}
<TouchableOpacity onPress={showStartDatePicker} disabled={Platform.OS === 'web'}> <TouchableOpacity onPress={showStartDatePicker} disabled={Platform.OS === 'web' || isAllDay}>
<TextInput <TextInput
label="Start Date & Time *" label={isAllDay ? "Date *" : "Start Date & Time *"}
// Use raw web input state for value on web, formatted Date otherwise // Use raw web input state for value on web, formatted Date otherwise
value={Platform.OS === 'web' ? webStartDateInput : formatDateTime(startDate)} value={Platform.OS === 'web' ? webStartDateInput : formatDateTime(startDate)}
editable={Platform.OS === 'web'} editable={Platform.OS === 'web' && !isAllDay}
onChangeText={Platform.OS === 'web' ? (text) => handleWebDateInputChange(text, 'start') : undefined} onChangeText={Platform.OS === 'web' && !isAllDay ? (text) => handleWebDateInputChange(text, 'start') : undefined}
placeholder={Platform.OS === 'web' ? 'YYYY-MM-DD HH:mm' : ''} placeholder={Platform.OS === 'web' ? (isAllDay ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm') : ''}
mode="outlined" mode="outlined"
style={styles.input} style={styles.input}
right={Platform.OS !== 'web' ? <TextInput.Icon icon="calendar-clock" onPress={showStartDatePicker} /> : null} right={Platform.OS !== 'web' && !isAllDay ? <TextInput.Icon icon="calendar-clock" onPress={showStartDatePicker} /> : <TextInput.Icon icon="calendar" onPress={showStartDatePicker} />}
error={!!formErrors.start} error={!!formErrors.start}
disabled={isAllDay && Platform.OS !== 'web'} // Disable native text input editing for all-day
/> />
</TouchableOpacity> </TouchableOpacity>
<HelperText type="error" visible={!!formErrors.start}> <HelperText type="error" visible={!!formErrors.start}>
{formErrors.start} {formErrors.start}
</HelperText> </HelperText>
{/* End Date Input - Conditional Logic */} {/* End Date Input - Conditionally render based on all_day */}
{!isAllDay && (
<>
<TouchableOpacity onPress={showEndDatePicker} disabled={Platform.OS === 'web'}> <TouchableOpacity onPress={showEndDatePicker} disabled={Platform.OS === 'web'}>
<TextInput <TextInput
label="End Date & Time" label="End Date & Time"
@@ -425,6 +467,8 @@ const EventFormScreen = () => {
<HelperText type="error" visible={!!formErrors.end}> <HelperText type="error" visible={!!formErrors.end}>
{formErrors.end} {formErrors.end}
</HelperText> </HelperText>
</>
)}
{/* Add Location Input */} {/* Add Location Input */}
<TextInput <TextInput
@@ -511,12 +555,14 @@ const EventFormScreen = () => {
<> <>
<DateTimePickerModal <DateTimePickerModal
isVisible={isStartDatePickerVisible} isVisible={isStartDatePickerVisible}
mode="datetime" mode={isAllDay ? "date" : "datetime"} // Use 'date' mode if all_day
date={startDate || new Date()} // Default picker to current start date or now date={startDate || new Date()} // Default picker to current start date or now
onConfirm={handleStartDateConfirm} onConfirm={handleStartDateConfirm}
onCancel={hideStartDatePicker} onCancel={hideStartDatePicker}
is24Hour={false} // Adjust based on preference is24Hour={false} // Adjust based on preference
/> />
{/* Only show end date picker if not all_day */}
{!isAllDay && (
<DateTimePickerModal <DateTimePickerModal
isVisible={isEndDatePickerVisible} isVisible={isEndDatePickerVisible}
mode="datetime" mode="datetime"
@@ -526,6 +572,7 @@ const EventFormScreen = () => {
onCancel={hideEndDatePicker} onCancel={hideEndDatePicker}
is24Hour={false} is24Hour={false}
/> />
)}
</> </>
)} )}
</View> </View>
@@ -549,6 +596,13 @@ const styles = StyleSheet.create({
input: { input: {
marginBottom: 5, // Reduced margin as HelperText adds space 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: { button: {
marginTop: 15, marginTop: 15,
}, },

View File

@@ -8,6 +8,7 @@ export interface CalendarEvent {
end?: string | null; // ISO string format end?: string | null; // ISO string format
location?: string | null; // Make optional location?: string | null; // Make optional
color?: string | null; // Keep optional color?: string | null; // Keep optional
all_day?: boolean | null; // Add all_day field
tags?: string[]; // Add optional tags array tags?: string[]; // Add optional tags array
user_id: number; // Add user_id as it's in the response 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 end?: string | null; // ISO string format
location?: string | null; location?: string | null;
color?: string | null; color?: string | null;
all_day?: boolean | null; // Add all_day field
tags?: string[]; // Add optional tags array tags?: string[]; // Add optional tags array
}; };
@@ -31,5 +33,6 @@ export type CalendarEventUpdate = {
end?: string | null; // ISO string format end?: string | null; // ISO string format
location?: string | null; location?: string | null;
color?: string | null; color?: string | null;
all_day?: boolean | null; // Add all_day field
tags?: string[]; // Add optional tags array tags?: string[]; // Add optional tags array
}; };