Updated calendar to have CRUD functionality (mostly).
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
36
interfaces/nativeapp/package-lock.json
generated
36
interfaces/nativeapp/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
@@ -17,4 +18,49 @@ export const getCalendarEvents = async (start?: Date, end?: Date): Promise<Calen
|
||||
console.error("Error fetching calendar events", error);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<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
|
||||
style={styles.eventItem}
|
||||
titleStyle={{ color: theme.colors.text }}
|
||||
descriptionStyle={{ color: theme.colors.textSecondary }}
|
||||
descriptionNumberOfLines={3}
|
||||
/>
|
||||
<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} />}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
519
interfaces/nativeapp/src/screens/EventFormScreen.tsx
Normal file
519
interfaces/nativeapp/src/screens/EventFormScreen.tsx
Normal 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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user