Updated calendar to have CRUD functionality (mostly).

This commit is contained in:
c-d-p
2025-04-18 19:30:02 +02:00
parent 8d884111fd
commit ee86374da6
15 changed files with 700 additions and 21 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -4,9 +4,10 @@ from sqlalchemy.orm import Session
from datetime import datetime
from modules.auth.dependencies import get_current_user
from core.database import get_db
from core.exceptions import not_found_exception
from modules.auth.models import User
from modules.calendar.schemas import CalendarEventCreate, CalendarEventResponse
from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event
from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate, CalendarEventResponse
from modules.calendar.service import create_calendar_event, get_calendar_event_by_id, get_calendar_events, update_calendar_event, delete_calendar_event
router = APIRouter(prefix="/calendar", tags=["calendar"])
@@ -29,10 +30,21 @@ def get_events(
end = None if end == "" else end
return get_calendar_events(db, user.id, start, end)
@router.put("/events/{event_id}", response_model=CalendarEventResponse)
@router.get("/events/{event_id}", response_model=CalendarEventResponse)
def get_event_by_id(
event_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
event = get_calendar_event_by_id(db, user.id, event_id)
if not event:
raise not_found_exception()
return event
@router.patch("/events/{event_id}", response_model=CalendarEventResponse)
def update_event(
event_id: int,
event: CalendarEventCreate,
event: CalendarEventUpdate,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):

View File

@@ -9,6 +9,13 @@ class CalendarEventCreate(BaseModel):
end: datetime | None = None
location: str | None = None
class CalendarEventUpdate(BaseModel):
title: str | None = None
description: str | None = None
start: datetime | None = None
end: datetime | None = None
location: str | None = None
class CalendarEventResponse(CalendarEventCreate):
id: int
user_id: int

View File

@@ -21,6 +21,15 @@ def get_calendar_events(db: Session, user_id: int, start: datetime, end: datetim
query = query.filter(CalendarEvent.end_time <= end)
return query.all()
def get_calendar_event_by_id(db: Session, user_id: int, event_id: int):
event = db.query(CalendarEvent).filter(
CalendarEvent.id == event_id,
CalendarEvent.user_id == user_id
).first()
if not event:
raise not_found_exception()
return event
def update_calendar_event(db: Session, user_id: int, event_id: int, event_data):
event = db.query(CalendarEvent).filter(
CalendarEvent.id == event_id,

View File

@@ -24,6 +24,7 @@
"react-native": "0.76.9",
"react-native-async-storage": "^0.0.1",
"react-native-calendars": "^1.1311.0",
"react-native-modal-datetime-picker": "^18.0.0",
"react-native-paper": "^5.13.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
@@ -3035,6 +3036,29 @@
"react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
"node_modules/@react-native-community/datetimepicker": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.3.0.tgz",
"integrity": "sha512-K/KgaJbLtjMpx4PaG4efrVIcSe6+DbLufeX1lwPB5YY8i3sq9dOh6WcAcMTLbaRTUpurebQTkl7puHPFm9GalA==",
"peer": true,
"dependencies": {
"invariant": "^2.2.4"
},
"peerDependencies": {
"expo": ">=50.0.0",
"react": "*",
"react-native": "*",
"react-native-windows": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"react-native-windows": {
"optional": true
}
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
@@ -8601,6 +8625,18 @@
"react-native": "*"
}
},
"node_modules/react-native-modal-datetime-picker": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-18.0.0.tgz",
"integrity": "sha512-0jdvhhraZQlRACwr7pM6vmZ2kxgzJ4CpnmV6J3TVA6MrXMXK6Zo/upRBKkRp0+fTOiKuNblzesA2U59rYo6SGA==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@react-native-community/datetimepicker": ">=6.7.0",
"react-native": ">=0.65.0"
}
},
"node_modules/react-native-paper": {
"version": "5.13.2",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.13.2.tgz",

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
@@ -24,12 +25,12 @@
"react-native": "0.76.9",
"react-native-async-storage": "^0.0.1",
"react-native-calendars": "^1.1311.0",
"react-native-modal-datetime-picker": "^18.0.0",
"react-native-paper": "^5.13.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-vector-icons": "^10.2.0",
"react-native-web": "~0.19.13",
"@react-native-async-storage/async-storage": "1.23.1"
"react-native-web": "~0.19.13"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View File

@@ -1,5 +1,6 @@
import apiClient from './client';
import { CalendarEvent } from '../types/calendar';
// Import the new types
import { CalendarEvent, CalendarEventCreate, CalendarEventUpdate } from '../types/calendar';
export const getCalendarEvents = async (start?: Date, end?: Date): Promise<CalendarEvent[]> => {
try {
@@ -18,3 +19,48 @@ export const getCalendarEvents = async (start?: Date, end?: Date): Promise<Calen
throw error;
}
}
export const getCalendarEventById = async (event_id: number): Promise<CalendarEvent> => {
try {
const response = await apiClient.get(`/calendar/events/${event_id}`);
console.log("[CAM] Got calendar event:", response);
return response.data;
} catch (error) {
console.error("Error fetching calendar event", error);
throw error;
}
}
// Use CalendarEventCreate type for the event parameter
export const createCalendarEvent = async (event: CalendarEventCreate): Promise<CalendarEvent> => {
try {
const response = await apiClient.post('/calendar/events', event);
console.log("[CAM] Created calendar event:", response);
return response.data; // Backend returns the full event
} catch (error) {
console.error("Error creating calendar event", error);
throw error;
}
}
// Use CalendarEventUpdate type for the event parameter
export const updateCalendarEvent = async (event_id: number, event: CalendarEventUpdate): Promise<CalendarEvent> => {
try {
const response = await apiClient.patch(`/calendar/events/${event_id}`, event);
console.log("[CAM] Updated calendar event:", response);
return response.data; // Backend returns the full event
} catch (error) {
console.error("Error updating calendar event", error);
throw error;
}
}
export const deleteCalendarEvent = async (event_id: number): Promise<void> => {
try {
const response = await apiClient.delete(`/calendar/events/${event_id}`);
console.log("[CAM] Deleted calendar event:", response);
} catch (error) {
console.error("Error deleting calendar event", error);
throw error;
}
}

View File

@@ -7,6 +7,7 @@ import DashboardScreen from '../screens/DashboardScreen';
import ChatScreen from '../screens/ChatScreen';
import CalendarScreen from '../screens/CalendarScreen';
import ProfileScreen from '../screens/ProfileScreen';
import EventFormScreen from '../screens/EventFormScreen';
import { WebContentStackParamList } from '../types/navigation';
@@ -37,7 +38,7 @@ const WebContentNavigator = () => {
<Stack.Screen name="Chat" component={ChatScreen} options={{ title: 'MAIA Chat' }}/>
<Stack.Screen name="Calendar" component={CalendarScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
{/* Add other detail screens here if needed */}
<Stack.Screen name="EventForm" component={EventFormScreen} />
</Stack.Navigator>
);
};

View File

@@ -1,8 +1,9 @@
// src/screens/CalendarScreen.tsx
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Calendar, DateData, LocaleConfig, CalendarProps, MarkingProps } from 'react-native-calendars'; // Import MarkingProps
import { Text, useTheme, ActivityIndicator, List, Divider } from 'react-native-paper';
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native'; // Import TouchableOpacity
import { Calendar, DateData, LocaleConfig, CalendarProps, MarkingProps } from 'react-native-calendars';
import { Text, useTheme, ActivityIndicator, List, Divider, FAB } from 'react-native-paper'; // Import FAB
import { useNavigation } from '@react-navigation/native'; // Import useNavigation
import {
format,
parseISO,
@@ -17,14 +18,20 @@ import {
import { getCalendarEvents } from '../api/calendar';
import { CalendarEvent } from '../types/calendar'; // Use updated type
import { AppStackParamList } from '../navigation/AppNavigator'; // Import navigation param list type
import { StackNavigationProp } from '@react-navigation/stack'; // Import StackNavigationProp
// Optional: Configure locale
// LocaleConfig.locales['en'] = { ... }; LocaleConfig.defaultLocale = 'en';
// Define navigation prop type
type CalendarScreenNavigationProp = StackNavigationProp<AppStackParamList, 'Calendar'>; // Adjust 'Calendar' if your route name is different
const getTodayDateString = () => format(new Date(), 'yyyy-MM-dd');
const CalendarScreen = () => {
const theme = useTheme();
const navigation = useNavigation<CalendarScreenNavigationProp>(); // Use the hook with the correct type
const todayString = useMemo(getTodayDateString, []);
const [selectedDate, setSelectedDate] = useState<string>(todayString);
@@ -315,15 +322,17 @@ const CalendarScreen = () => {
}
return (
<TouchableOpacity onPress={() => navigation.navigate('EventForm', { eventId: item.id })}>
<List.Item
title={item.title}
description={description}
left={props => <List.Icon {...props} icon="circle-slice-8" color={item.color || theme.colors.primary} />} // Use a filled circle or similar
left={props => <List.Icon {...props} icon="circle-slice-8" color={item.color || theme.colors.primary} />}
style={styles.eventItem}
titleStyle={{ color: theme.colors.text }}
descriptionStyle={{ color: theme.colors.textSecondary }}
descriptionNumberOfLines={3}
/>
</TouchableOpacity>
);
}
@@ -337,6 +346,13 @@ const CalendarScreen = () => {
eventItem: { backgroundColor: theme.colors.surface, marginBottom: 8, borderRadius: theme.roundness },
noEventsText: { textAlign: 'center', marginTop: 20, color: theme.colors.textSecondary, fontSize: 16 },
errorText: { textAlign: 'center', marginTop: 20, color: theme.colors.error, fontSize: 16 },
fab: { // Style for the FAB
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: theme.colors.primary, // Use theme color
},
});
// --- Calendar Theme ---
@@ -407,6 +423,14 @@ const CalendarScreen = () => {
)}
</View>
)}
{/* Add FAB for creating new events */}
<FAB
style={styles.fab}
icon="plus"
onPress={() => navigation.navigate('EventForm')} // Navigate without eventId for creation
color={theme.colors.onPrimary || '#ffffff'} // Ensure icon color contrasts with background
/>
</View>
);
};

View File

@@ -0,0 +1,519 @@
// src/screens/EventFormScreen.tsx
import React, { useState, useEffect, useCallback } from 'react';
// Add Platform import
import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, Platform } from 'react-native';
import { TextInput, Button, useTheme, Text, ActivityIndicator, HelperText } 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.
// A more robust approach might involve platform-specific files or checking Platform.OS before rendering.
// For now, we'll check Platform.OS before using it.
import DateTimePickerModal from "react-native-modal-datetime-picker";
import { format, parseISO, isValid, parse } from 'date-fns'; // Added parse
import { getCalendarEventById, createCalendarEvent, updateCalendarEvent, deleteCalendarEvent } from '../api/calendar';
// Import the new types
import { CalendarEventCreate, CalendarEventUpdate } from '../types/calendar';
import { WebContentStackParamList } from '../types/navigation'; // Adjust path if needed
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
type EventFormRouteProp = RouteProp<WebContentStackParamList, 'EventForm'>;
type EventFormNavigationProp = NativeStackNavigationProp<WebContentStackParamList, 'EventForm'>;
const EventFormScreen = () => {
const theme = useTheme();
const navigation = useNavigation<EventFormNavigationProp>();
const route = useRoute<EventFormRouteProp>();
const eventId = route.params?.eventId; // Get eventId from navigation params
const selectedDate = route.params?.selectedDate; // Get pre-selected date
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null);
const [color, setColor] = useState(''); // Basic color input for now
const [location, setLocation] = useState(''); // Add location state
// Add state for raw web date input
const [webStartDateInput, setWebStartDateInput] = useState<string>('');
const [webEndDateInput, setWebEndDateInput] = useState<string>('');
const [isStartDatePickerVisible, setStartDatePickerVisibility] = useState(false);
const [isEndDatePickerVisible, setEndDatePickerVisibility] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formErrors, setFormErrors] = useState<{ title?: string; start?: string; end?: string }>({});
// --- Fetch Event Data on Edit ---
useEffect(() => {
if (eventId) {
const fetchEvent = async () => {
setIsLoading(true);
setError(null);
try {
const event = await getCalendarEventById(eventId);
setTitle(event.title);
setDescription(event.description || '');
setColor(event.color || ''); // Use optional color
setLocation(event.location || ''); // Set location state
// Ensure dates are Date objects
if (event.start && isValid(parseISO(event.start))) {
const parsedDate = parseISO(event.start);
setStartDate(parsedDate);
setWebStartDateInput(formatForWebInput(parsedDate)); // Init web input
} else {
console.warn("Fetched event has invalid start date:", event.start);
const now = new Date();
setStartDate(now);
setWebStartDateInput(formatForWebInput(now)); // Init web input
}
if (event.end && isValid(parseISO(event.end))) {
const parsedDate = parseISO(event.end);
setEndDate(parsedDate);
setWebEndDateInput(formatForWebInput(parsedDate)); // Init web input
} else {
setEndDate(null);
setWebEndDateInput(''); // Init web input
}
} catch (err) {
setError('Failed to load event details.');
console.error(err);
} finally {
setIsLoading(false);
}
};
fetchEvent();
} else if (selectedDate) {
// Pre-fill start date if creating from a specific day
try {
const initialDate = parseISO(selectedDate);
if (isValid(initialDate)) {
// Set start time to a default (e.g., 9 AM) on the selected date
initialDate.setHours(9, 0, 0, 0);
setStartDate(initialDate);
setWebStartDateInput(formatForWebInput(initialDate)); // Init web input
setEndDate(null); // Ensure end date starts null
setWebEndDateInput('');
} else {
const now = new Date();
setStartDate(now);
setWebStartDateInput(formatForWebInput(now)); // Init web input
setEndDate(null);
setWebEndDateInput('');
}
} catch {
const now = new Date();
setStartDate(now);
setWebStartDateInput(formatForWebInput(now)); // Init web input
setEndDate(null);
setWebEndDateInput('');
}
} else {
// Default start date to now if creating without a selected date
const now = new Date();
setStartDate(now);
setWebStartDateInput(formatForWebInput(now)); // Init web input
setEndDate(null);
setWebEndDateInput('');
}
}, [eventId, selectedDate]);
// --- Date Picker Logic ---
const showStartDatePicker = () => {
if (Platform.OS !== 'web') {
setStartDatePickerVisibility(true);
}
// On web, clicking the input field allows manual editing
};
const hideStartDatePicker = () => setStartDatePickerVisibility(false);
const handleStartDateConfirm = (date: Date) => {
setStartDate(date);
setWebStartDateInput(formatForWebInput(date)); // Update web input state
// Optional: Auto-set end date if it's before start date or null
if (!endDate || endDate < date) {
const newEndDate = new Date(date);
newEndDate.setHours(date.getHours() + 1); // Default to 1 hour later
setEndDate(newEndDate);
setWebEndDateInput(formatForWebInput(newEndDate)); // Update web input state
}
validateForm({ start: date }); // Validate after setting
hideStartDatePicker();
};
const showEndDatePicker = () => {
if (Platform.OS !== 'web') {
setEndDatePickerVisibility(true);
}
// On web, clicking the input field allows manual editing
};
const hideEndDatePicker = () => setEndDatePickerVisibility(false);
const handleEndDateConfirm = (date: Date) => {
setEndDate(date);
setWebEndDateInput(formatForWebInput(date)); // Update web input state
validateForm({ end: date }); // Validate after setting
hideEndDatePicker();
};
// --- Web Date Input Handling ---
const handleWebDateInputChange = (text: string, type: 'start' | 'end') => {
// 1. Update the raw input state immediately
if (type === 'start') {
setWebStartDateInput(text);
} else {
setWebEndDateInput(text);
}
// 2. Attempt to parse the input text
// Allow partial input (e.g., "yyyy-MM-dd") for validation purposes later if needed,
// but only update the Date state if the format is fully matched.
try {
// Use a reference date (like 'new Date()') for parsing robustness
const parsedDate = parse(text, 'yyyy-MM-dd HH:mm', new Date());
// 3. Check if the parsed date is valid *and* the input string roughly matches the expected format length
// This prevents updating the Date state too early with partial input like "202"
if (isValid(parsedDate) && text.length >= 15) { // Basic length check for 'yyyy-MM-dd HH:mm'
if (type === 'start') {
setStartDate(parsedDate);
// Optional: Auto-set end date
if (!endDate || endDate < parsedDate) {
const newEndDate = new Date(parsedDate);
newEndDate.setHours(parsedDate.getHours() + 1);
setEndDate(newEndDate);
setWebEndDateInput(formatForWebInput(newEndDate)); // Update other web input too
}
validateForm({ start: parsedDate }); // Validate with the actual Date
} else {
setEndDate(parsedDate);
validateForm({ end: parsedDate }); // Validate with the actual Date
}
} else {
// Input is incomplete or invalid format, don't update Date state yet.
// Validation should reflect that the required Date state might be missing or invalid.
if (type === 'start') {
// If text is empty, treat as null for validation. If partially filled,
// it's technically invalid until complete. Keep startDate as is for now.
validateForm({ start: text.trim() === '' ? null : startDate });
} else {
validateForm({ end: text.trim() === '' ? null : endDate });
}
}
} catch (e) {
// Parsing likely failed due to highly invalid format
console.error("Error parsing date string:", e);
if (type === 'start') {
validateForm({ start: null }); // Treat as invalid for validation
} else {
validateForm({ end: null }); // Treat as invalid for validation
}
}
};
// --- Form Validation ---
const validateForm = (changedValues: { title?: string; start?: Date | null; end?: Date | null } = {}): boolean => {
const errors: { title?: string; start?: string; end?: string } = { ...formErrors }; // Start with existing errors
let isValidForm = true;
const currentTitle = changedValues.title !== undefined ? changedValues.title : title;
const currentStart = changedValues.start !== undefined ? changedValues.start : startDate;
const currentEnd = changedValues.end !== undefined ? changedValues.end : endDate;
// Validate Title
if (currentTitle.trim() === '') {
errors.title = 'Title is required.';
isValidForm = false;
} else {
delete errors.title; // Clear error if valid
}
// Validate Start Date
if (!currentStart) {
errors.start = 'Start date/time is required.';
isValidForm = false;
} else {
delete errors.start; // Clear error if valid
}
// Validate End Date (must be after start date if provided)
if (currentStart && currentEnd && currentEnd < currentStart) {
errors.end = 'End time cannot be before start time.';
isValidForm = false;
} else {
delete errors.end; // Clear error if valid
}
setFormErrors(errors);
return isValidForm;
};
// --- Save Event ---
const handleSave = async () => {
if (!validateForm({ title, start: startDate, end: endDate })) {
setError("Please fix the errors in the form.");
return;
}
if (!startDate) { // Should be caught by validation, but double-check
setError("Start date is missing.");
return;
}
setIsLoading(true);
setError(null);
// Construct data based on CalendarEventCreate/Update types
const eventData: CalendarEventCreate | CalendarEventUpdate = {
title: title.trim(),
description: description.trim() || null,
start: startDate.toISOString(),
end: endDate ? endDate.toISOString() : null,
location: location.trim() || null, // Include location
color: color.trim() || null, // Include color
};
try {
if (eventId) {
await updateCalendarEvent(eventId, eventData as CalendarEventUpdate);
} else {
await createCalendarEvent(eventData as CalendarEventCreate);
}
navigation.goBack(); // Go back to calendar screen on success
} catch (err) {
setError(eventId ? 'Failed to update event.' : 'Failed to create event.');
console.error(err);
} finally {
setIsLoading(false);
}
};
// --- Delete Event ---
const handleDelete = () => {
if (!eventId) return;
Alert.alert(
"Confirm Deletion",
"Are you sure you want to delete this event?",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: async () => {
setIsDeleting(true);
setError(null);
try {
await deleteCalendarEvent(eventId);
navigation.goBack(); // Go back on successful deletion
} catch (err) {
setError('Failed to delete event.');
console.error(err);
} finally {
setIsDeleting(false);
}
},
},
]
);
};
// --- Format Date/Time for Display ---
// Keep this for native display or potentially other uses
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");
} catch {
return "Invalid Date";
}
};
// Helper to format date specifically for web input state
const formatForWebInput = (date: Date | null): string => {
if (!date || !isValid(date)) return '';
try {
return format(date, "yyyy-MM-dd HH:mm");
} catch {
return ''; // Return empty if formatting fails
}
};
if (isLoading && !title) { // Show loading indicator only during initial fetch
return <ActivityIndicator animating={true} style={styles.loading} />;
}
return (
<ScrollView style={[styles.container, { backgroundColor: theme.colors.background }]}>
<View style={styles.content}>
<Text style={styles.header}>{eventId ? 'Edit Event' : 'Create Event'}</Text>
{error && <Text style={styles.errorText}>{error}</Text>}
<TextInput
label="Title *"
value={title}
onChangeText={(text) => { setTitle(text); validateForm({ title: text }); }}
mode="outlined"
style={styles.input}
error={!!formErrors.title}
/>
<HelperText type="error" visible={!!formErrors.title}>
{formErrors.title}
</HelperText>
{/* Start Date Input - Conditional Logic */}
<TouchableOpacity onPress={showStartDatePicker} disabled={Platform.OS === 'web'}>
<TextInput
label="Start Date & Time *"
// Use raw web input state for value on web, formatted Date otherwise
value={Platform.OS === 'web' ? webStartDateInput : formatDateTime(startDate)}
editable={Platform.OS === 'web'}
onChangeText={Platform.OS === 'web' ? (text) => handleWebDateInputChange(text, 'start') : undefined}
placeholder={Platform.OS === 'web' ? 'YYYY-MM-DD HH:mm' : ''}
mode="outlined"
style={styles.input}
right={Platform.OS !== 'web' ? <TextInput.Icon icon="calendar-clock" onPress={showStartDatePicker} /> : null}
error={!!formErrors.start}
/>
</TouchableOpacity>
<HelperText type="error" visible={!!formErrors.start}>
{formErrors.start}
</HelperText>
{/* End Date Input - Conditional Logic */}
<TouchableOpacity onPress={showEndDatePicker} disabled={Platform.OS === 'web'}>
<TextInput
label="End Date & Time"
// Use raw web input state for value on web, formatted Date otherwise
value={Platform.OS === 'web' ? webEndDateInput : formatDateTime(endDate)}
editable={Platform.OS === 'web'}
onChangeText={Platform.OS === 'web' ? (text) => handleWebDateInputChange(text, 'end') : undefined}
placeholder={Platform.OS === 'web' ? 'YYYY-MM-DD HH:mm' : ''}
mode="outlined"
style={styles.input}
right={Platform.OS !== 'web' ? <TextInput.Icon icon="calendar-clock" onPress={showEndDatePicker} /> : null}
error={!!formErrors.end}
/>
</TouchableOpacity>
<HelperText type="error" visible={!!formErrors.end}>
{formErrors.end}
</HelperText>
{/* Add Location Input */}
<TextInput
label="Location"
value={location}
onChangeText={setLocation}
mode="outlined"
style={styles.input}
/>
<TextInput
label="Description"
value={description}
onChangeText={setDescription}
mode="outlined"
style={styles.input}
multiline
numberOfLines={3}
/>
<TextInput
label="Color (e.g., #ff0000)" // Simple text input for color for now
value={color}
onChangeText={setColor}
mode="outlined"
style={styles.input}
placeholder={theme.colors.primary} // Show default color hint
/>
<Button
mode="contained"
onPress={handleSave}
style={styles.button}
loading={isLoading && !!title} // Show loading on button during save/update
disabled={isLoading || isDeleting}
>
{eventId ? 'Update Event' : 'Create Event'}
</Button>
{eventId && (
<Button
mode="outlined"
onPress={handleDelete}
style={styles.button}
textColor={theme.colors.error}
loading={isDeleting}
disabled={isLoading || isDeleting}
>
Delete Event
</Button>
)}
{/* Conditionally render DateTimePickerModal only on native platforms */}
{Platform.OS !== 'web' && (
<>
<DateTimePickerModal
isVisible={isStartDatePickerVisible}
mode="datetime"
date={startDate || new Date()} // Default picker to current start date or now
onConfirm={handleStartDateConfirm}
onCancel={hideStartDatePicker}
is24Hour={false} // Adjust based on preference
/>
<DateTimePickerModal
isVisible={isEndDatePickerVisible}
mode="datetime"
date={endDate || startDate || new Date()} // Default picker to end, start, or now
minimumDate={startDate || undefined} // Prevent end date being before start date
onConfirm={handleEndDateConfirm}
onCancel={hideEndDatePicker}
is24Hour={false}
/>
</>
)}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 20,
},
header: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
input: {
marginBottom: 5, // Reduced margin as HelperText adds space
},
button: {
marginTop: 15,
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
color: 'red', // Use theme.colors.error in practice
textAlign: 'center',
marginBottom: 10,
},
});
export default EventFormScreen;

View File

@@ -7,4 +7,25 @@ export interface CalendarEvent {
start: string;
end: string;
location: string;
color?: string; // Add optional color property
}
// Type for creating an event (matches backend schema)
export type CalendarEventCreate = {
title: string;
description?: string | null;
start: string; // ISO string format
end?: string | null; // ISO string format
location?: string | null;
color?: string | null;
};
// Type for updating an event (matches backend schema)
export type CalendarEventUpdate = {
title?: string | null;
description?: string | null;
start?: string | null; // ISO string format
end?: string | null; // ISO string format
location?: string | null;
color?: string | null;
};

View File

@@ -1,4 +1,5 @@
// src/types/navigation.ts
import { NavigationContainerRef } from '@react-navigation/native'; // Import NavigationContainerRef
// Screens within the main Mobile Bottom Tab Navigator
export type MobileTabParamList = {
@@ -14,6 +15,7 @@ export type WebContentStackParamList = {
Chat: undefined;
Calendar: undefined;
Profile: undefined;
EventForm?: { eventId?: number; selectedDate?: string }; // Add EventForm with optional params
};
// Screens managed by the Root Navigator (Auth vs App)