adujstments

This commit is contained in:
Milan Paunovic
2025-02-16 01:07:12 +01:00
parent d9ee1cd921
commit c184eb3293
5 changed files with 477 additions and 216 deletions

View File

@ -1,15 +1,18 @@
import React, {useCallback} from "react"; import React, {memo, useCallback, useMemo} from "react";
import {Drawer} from "expo-router/drawer"; import {Drawer} from "expo-router/drawer";
import {DrawerContentScrollView, DrawerContentComponentProps, DrawerNavigationOptions} from "@react-navigation/drawer"; import {
DrawerContentComponentProps,
DrawerContentScrollView,
DrawerNavigationOptions,
DrawerNavigationProp
} from "@react-navigation/drawer";
import {ImageBackground, Pressable, StyleSheet} from "react-native"; import {ImageBackground, Pressable, StyleSheet} from "react-native";
import {Button, ButtonSize, Text, View} from "react-native-ui-lib"; import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
import * as Device from "expo-device"; import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import {Ionicons} from "@expo/vector-icons"; import {Ionicons} from "@expo/vector-icons";
import {DeviceType} from "expo-device"; import {ParamListBase, RouteProp, Theme} from '@react-navigation/native';
import {DrawerNavigationProp} from "@react-navigation/drawer";
import {ParamListBase, Theme} from '@react-navigation/native';
import {RouteProp} from "@react-navigation/native";
import {useSignOut} from "@/hooks/firebase/useSignOut"; import {useSignOut} from "@/hooks/firebase/useSignOut";
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader"; import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
@ -205,7 +208,7 @@ interface HeaderRightProps {
navigation: DrawerScreenNavigationProp; navigation: DrawerScreenNavigationProp;
} }
const HeaderRight: React.FC<HeaderRightProps> = ({route, navigation}) => { const HeaderRight: React.FC<HeaderRightProps> = memo(({route, navigation}) => {
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name); const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
const isCalendarPage = ["calendar", "index"].includes(route.name); const isCalendarPage = ["calendar", "index"].includes(route.name);
@ -222,43 +225,7 @@ const HeaderRight: React.FC<HeaderRightProps> = ({route, navigation}) => {
)} )}
<ViewSwitch navigation={navigation}/> <ViewSwitch navigation={navigation}/>
</View> </View>
); )
};
const screenOptions: (props: {
route: RouteProp<ParamListBase, string>;
navigation: DrawerNavigationProp<ParamListBase, string>;
theme: Theme;
}) => DrawerNavigationOptions = ({route, navigation}) => ({
lazy: true,
headerShown: true,
headerTitleAlign: "left",
headerTitle: ({children}) => {
const isCalendarRoute = ["calendar", "index"].includes(route.name);
if (isCalendarRoute) return null;
return (
<View flexG centerV paddingL-10>
<Text style={styles.headerTitle}>
{children}
</Text>
</View>
);
},
headerLeft: () => (
<Pressable
onPress={() => navigation.toggleDrawer()}
hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}
style={({pressed}) => [styles.drawerTrigger, {opacity: pressed ? 0.4 : 1}]}
>
<DrawerIcon/>
</Pressable>
),
headerRight: () => <HeaderRight
route={route as RouteProp<DrawerParamList>}
navigation={navigation as DrawerNavigationProp<DrawerParamList>}
/>,
drawerStyle: styles.drawer,
}); });
interface DrawerScreen { interface DrawerScreen {
@ -280,6 +247,45 @@ const DRAWER_SCREENS: DrawerScreen[] = [
]; ];
const TabLayout: React.FC = () => { const TabLayout: React.FC = () => {
const screenOptions = useMemo(() => {
return ({route, navigation}: {
route: RouteProp<ParamListBase, string>;
navigation: DrawerNavigationProp<ParamListBase, string>;
theme: Theme;
}): DrawerNavigationOptions => ({
lazy: true,
headerShown: true,
headerTitleAlign: "left",
headerTitle: ({children}) => {
const isCalendarRoute = ["calendar", "index"].includes(route.name);
if (isCalendarRoute) return null;
return (
<View flexG centerV paddingL-10>
<Text style={styles.headerTitle}>
{children}
</Text>
</View>
);
},
headerLeft: () => (
<Pressable
onPress={() => navigation.toggleDrawer()}
hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}
style={({pressed}) => [styles.drawerTrigger, {opacity: pressed ? 0.4 : 1}]}
>
<DrawerIcon/>
</Pressable>
),
headerRight: () => <HeaderRight
route={route as RouteProp<DrawerParamList>}
navigation={navigation as DrawerNavigationProp<DrawerParamList>}
/>,
drawerStyle: styles.drawer,
});
}, []);
return ( return (
<Drawer <Drawer
initialRouteName="index" initialRouteName="index"

View File

@ -70,9 +70,9 @@ if (Platform.OS === 'ios') {
} }
if (__DEV__) { if (__DEV__) {
functions().useEmulator("localhost", 5001); // functions().useEmulator("localhost", 5001);
firestore().useEmulator("localhost", 5471); // firestore().useEmulator("localhost", 5471);
auth().useEmulator("http://localhost:9099"); // auth().useEmulator("http://localhost:9099");
} }
type TextStyleBase = type TextStyleBase =

View File

@ -1,30 +1,46 @@
import { SegmentedControl, View } from "react-native-ui-lib"; import {SegmentedControl, View} from "react-native-ui-lib";
import React, { memo, useCallback } from "react"; import React, {memo, useCallback, useMemo, useRef, useEffect} from "react";
import { StyleSheet } from "react-native"; import {StyleSheet} from "react-native";
import { NavigationProp, useNavigationState } from "@react-navigation/native"; import {NavigationProp, useNavigationState} from "@react-navigation/native";
interface ViewSwitchProps { interface ViewSwitchProps {
navigation: NavigationProp<any>; navigation: NavigationProp<any>;
} }
const ViewSwitch = memo(function ViewSwitch({ navigation }: ViewSwitchProps) { const ViewSwitch = memo(function ViewSwitch({navigation}: ViewSwitchProps) {
const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0); const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0);
const isInitialMount = useRef(true);
const navigationPending = useRef(false);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
}, []);
const handleSegmentChange = useCallback( const handleSegmentChange = useCallback(
(index: number) => { (index: number) => {
if (index === currentIndex) return; if (navigationPending.current) return;
navigation.navigate(index === 0 ? "calendar" : "todos");
navigationPending.current = true;
setTimeout(() => {
navigation.navigate(index === 0 ? "calendar" : "todos");
navigationPending.current = false;
}, 300);
}, },
[navigation, currentIndex] [navigation]
); );
const segments = useMemo(() => [
{label: "Calendar", segmentLabelStyle: styles.labelStyle},
{label: "To Dos", segmentLabelStyle: styles.labelStyle},
], []);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<SegmentedControl <SegmentedControl
segments={[ segments={segments}
{ label: "Calendar", segmentLabelStyle: styles.labelStyle },
{ label: "To Dos", segmentLabelStyle: styles.labelStyle },
]}
containerStyle={styles.segmentContainer} containerStyle={styles.segmentContainer}
style={styles.segment} style={styles.segment}
backgroundColor="#ebebeb" backgroundColor="#ebebeb"
@ -45,7 +61,7 @@ const styles = StyleSheet.create({
borderRadius: 30, borderRadius: 30,
backgroundColor: "#ebebeb", backgroundColor: "#ebebeb",
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { width: 0, height: 0 }, shadowOffset: {width: 0, height: 0},
shadowOpacity: 0, shadowOpacity: 0,
shadowRadius: 0, shadowRadius: 0,
elevation: 0, elevation: 0,
@ -65,4 +81,4 @@ const styles = StyleSheet.create({
}, },
}); });
export default ViewSwitch; export default ViewSwitch;

View File

@ -1,4 +1,4 @@
import React, {memo, useState, useMemo, useCallback} from "react"; import React, {memo, useCallback, useMemo, useState} from "react";
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib"; import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
import {MaterialIcons} from "@expo/vector-icons"; import {MaterialIcons} from "@expo/vector-icons";
@ -71,29 +71,32 @@ export const CalendarHeader = memo(() => {
}, [mode]); }, [mode]);
const renderMonthPicker = () => ( const renderMonthPicker = () => (
<View row centerV gap-1 flexS> <>
{isTablet && ( {isTablet && <View flexG/>}
<Text style={styles.yearText}> <View row centerV gap-1 flexS>
{selectedDate.getFullYear()} {isTablet && (
</Text> <Text style={styles.yearText}>
)} {selectedDate.getFullYear()}
<Picker </Text>
value={months[selectedDate.getMonth()]} )}
placeholder="Select Month" <Picker
style={styles.monthPicker} value={months[selectedDate.getMonth()]}
mode={PickerModes.SINGLE} placeholder="Select Month"
onChange={value => handleMonthChange(value as string)} style={styles.monthPicker}
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>} mode={PickerModes.SINGLE}
topBarProps={{ onChange={value => handleMonthChange(value as string)}
title: selectedDate.getFullYear().toString(), trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
titleStyle: styles.yearText, topBarProps={{
}} title: selectedDate.getFullYear().toString(),
> titleStyle: styles.yearText,
{months.map(month => ( }}
<Picker.Item key={month} label={month} value={month}/> >
))} {months.map(month => (
</Picker> <Picker.Item key={month} label={month} value={month}/>
</View> ))}
</Picker>
</View>
</>
); );
return ( return (

View File

@ -308,59 +308,296 @@ async function fetchAndSaveMicrosoftEvents({ token, refreshToken, email, familyI
} }
} }
// Check Upcoming Events every 5 minutes send reminders exports.sendOverviews = functions.pubsub
exports.checkUpcomingEvents = functions.pubsub .schedule('0 20 * * *') // Runs at 8 PM daily
.schedule("every 5 minutes")
.onRun(async (context) => { .onRun(async (context) => {
const now = Timestamp.now(); const familiesSnapshot = await admin.firestore().collection('Families').get();
const tenMinutesFromNow = new Date(now.toDate().getTime() + 10 * 60 * 1000);
const eventsSnapshot = await db.collection("Events") for (const familyDoc of familiesSnapshot.docs) {
const familyId = familyDoc.id;
const familySettings = familyDoc.data()?.settings || {};
const overviewTime = familySettings.overviewTime || '20:00';
const [hours, minutes] = overviewTime.split(':');
const now = new Date();
if (now.getHours() !== parseInt(hours) || now.getMinutes() !== parseInt(minutes)) {
continue;
}
try {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const tomorrowEnd = new Date(tomorrow);
tomorrowEnd.setHours(23, 59, 59, 999);
const weekEnd = new Date(tomorrow);
weekEnd.setDate(weekEnd.getDate() + 7);
weekEnd.setHours(23, 59, 59, 999);
const [tomorrowEvents, weekEvents] = await Promise.all([
admin.firestore()
.collection('Events')
.where('familyId', '==', familyId)
.where('startDate', '>=', tomorrow)
.where('startDate', '<=', tomorrowEnd)
.orderBy('startDate')
.limit(3)
.get(),
admin.firestore()
.collection('Events')
.where('familyId', '==', familyId)
.where('startDate', '>', tomorrowEnd)
.where('startDate', '<=', weekEnd)
.orderBy('startDate')
.limit(3)
.get()
]);
if (tomorrowEvents.empty && weekEvents.empty) {
continue;
}
let notificationBody = '';
if (!tomorrowEvents.empty) {
notificationBody += 'Tomorrow: ';
const tomorrowTitles = tomorrowEvents.docs.map(doc => doc.data().title);
notificationBody += tomorrowTitles.join(', ');
if (tomorrowEvents.size === 3) notificationBody += ' and more...';
}
if (!weekEvents.empty) {
if (notificationBody) notificationBody += '\n\n';
notificationBody += 'This week: ';
const weekTitles = weekEvents.docs.map(doc => doc.data().title);
notificationBody += weekTitles.join(', ');
if (weekEvents.size === 3) notificationBody += ' and more...';
}
const pushTokens = await getPushTokensForFamily(familyId);
await sendNotifications(pushTokens, {
title: "Family Calendar Overview",
body: notificationBody,
data: {
type: 'calendar_overview',
date: tomorrow.toISOString()
}
});
await storeNotification({
type: 'calendar_overview',
familyId,
content: notificationBody,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
});
} catch (error) {
console.error(`Error sending overview for family ${familyId}:`, error);
}
}
});
exports.sendWeeklyOverview = functions.pubsub
.schedule('0 20 * * 0') // Runs at 8 PM every Sunday
.onRun(async (context) => {
const familiesSnapshot = await admin.firestore().collection('Families').get();
for (const familyDoc of familiesSnapshot.docs) {
const familyId = familyDoc.id;
const familySettings = familyDoc.data()?.settings || {};
const overviewTime = familySettings.weeklyOverviewTime || '20:00';
const [hours, minutes] = overviewTime.split(':');
const now = new Date();
if (now.getHours() !== parseInt(hours) || now.getMinutes() !== parseInt(minutes)) {
continue;
}
try {
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() + 1);
weekStart.setHours(0, 0, 0, 0);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 7);
const weekEvents = await admin.firestore()
.collection('Events')
.where('familyId', '==', familyId)
.where('startDate', '>=', weekStart)
.where('startDate', '<=', weekEnd)
.orderBy('startDate')
.limit(3)
.get();
if (weekEvents.empty) continue;
const eventTitles = weekEvents.docs.map(doc => doc.data().title);
const notificationBody = `Next week: ${eventTitles.join(', ')}${weekEvents.size === 3 ? ' and more...' : ''}`;
const pushTokens = await getPushTokensForFamily(familyId);
await sendNotifications(pushTokens, {
title: "Weekly Calendar Overview",
body: notificationBody,
data: {
type: 'weekly_overview',
weekStart: weekStart.toISOString()
}
});
await storeNotification({
type: 'weekly_overview',
familyId,
content: notificationBody,
timestamp: admin.firestore.FieldValue.serverTimestamp()
});
} catch (error) {
console.error(`Error sending weekly overview for family ${familyId}:`, error);
}
}
});
exports.checkUpcomingEvents = functions.pubsub
.schedule("every 1 minutes")
.onRun(async (context) => {
const now = admin.firestore.Timestamp.now();
const oneHourFromNow = new Date(now.toDate().getTime() + 60 * 60 * 1000);
logger.info("Checking upcoming events", {
currentTime: now.toDate().toISOString(),
lookAheadTime: oneHourFromNow.toISOString()
});
const eventsSnapshot = await admin.firestore()
.collection("Events")
.where("startDate", ">=", now) .where("startDate", ">=", now)
.where("startDate", "<=", Timestamp.fromDate(tenMinutesFromNow)) .where("startDate", "<=", admin.firestore.Timestamp.fromDate(oneHourFromNow))
.get(); .get();
await Promise.all(eventsSnapshot.docs.map(async (doc) => { logger.info(`Found ${eventsSnapshot.size} upcoming events to check`);
const processPromises = eventsSnapshot.docs.map(async (doc) => {
const event = doc.data(); const event = doc.data();
if (!event?.startDate) return; const eventId = doc.id;
const { familyId, title, allDay } = event;
// Skip if reminder already sent
if (event.reminderSent === true ||
event.notifiedAt ||
(event.reminderSentAt &&
now.toMillis() - event.reminderSentAt.toMillis() < 60000)) {
return;
}
try { try {
const familyDoc = await db.collection("Households").doc(familyId).get(); const householdSnapshot = await admin.firestore()
if (!familyDoc.exists) return; .collection("Households")
const familySettings = familyDoc.data()?.settings || {}; .where("familyId", "==", event.familyId)
const reminderTime = familySettings.defaultReminderTime || 5; .limit(1)
.get();
if (householdSnapshot.empty) {
return;
}
const householdDoc = householdSnapshot.docs[0];
const householdSettings = householdDoc.data()?.settings || {};
const reminderTime = householdSettings.defaultReminderTime || 15;
const eventTime = event.startDate.toDate(); const eventTime = event.startDate.toDate();
const reminderThreshold = new Date(now.toDate().getTime() + reminderTime * 60 * 1000); const currentTime = now.toDate();
if (allDay) { const minutesUntilEvent = Math.round((eventTime - currentTime) / (60 * 1000));
const eveningBefore = new Date(eventTime);
eveningBefore.setDate(eveningBefore.getDate() - 1); // Only send exactly at the reminder time
eveningBefore.setHours(20, 0, 0, 0); if (minutesUntilEvent === reminderTime) {
if (now.toDate() >= eveningBefore && !event.eveningReminderSent) { // Double-check reminder hasn't been sent
const pushTokens = await getPushTokensForFamily(familyId); const freshEventDoc = await doc.ref.get();
if (pushTokens.length) { if (freshEventDoc.data()?.reminderSent === true) {
await sendNotifications(pushTokens, { return;
title: "Tomorrow's All-Day Event",
body: `Tomorrow: ${title}`,
data: { type: "event_reminder", eventId: doc.id },
});
await doc.ref.update({ eveningReminderSent: true });
}
} }
} else if (eventTime <= reminderThreshold && !event.reminderSent) {
const pushTokens = await getPushTokensForFamily(familyId); // Mark as sent FIRST
if (pushTokens.length) { await doc.ref.update({
reminderSent: true,
reminderSentAt: admin.firestore.FieldValue.serverTimestamp()
});
const pushTokens = await getPushTokensForFamily(event.familyId);
if (pushTokens.length > 0) {
// Send notification
await sendNotifications(pushTokens, { await sendNotifications(pushTokens, {
title: "Upcoming Event", title: "Upcoming Event Reminder",
body: `In ${reminderTime} minutes: ${title}`, body: `In ${reminderTime} minutes: ${event.title}`,
data: { type: "event_reminder", eventId: doc.id }, data: {
type: "event_reminder",
eventId: eventId,
familyId: event.familyId
}
});
// Store notification record
await storeNotification({
type: "event_reminder",
familyId: event.familyId,
content: `Reminder: ${event.title} starts in ${reminderTime} minutes`,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
eventId: eventId
});
logger.info(`Sent reminder for event: ${event.title}`, {
eventId,
minutesUntilEvent,
reminderTime,
currentTime: currentTime.toISOString(),
eventTime: eventTime.toISOString()
}); });
await doc.ref.update({ reminderSent: true });
} }
} }
} catch (error) { } catch (error) {
logger.error(`Error processing reminder for event ${doc.id}`, error); logger.error(`Error processing reminder for event ${eventId}:`, error);
} }
})); });
await Promise.all(processPromises);
});
// 3. Add a function to reset reminder flags for testing
exports.resetReminderFlags = functions.https.onRequest(async (req, res) => {
try {
const batch = admin.firestore().batch();
const events = await admin.firestore()
.collection("Events")
.where("reminderSent", "==", true)
.get();
events.docs.forEach(doc => {
batch.update(doc.ref, {
reminderSent: false,
reminderSentAt: null
});
});
await batch.commit();
res.status(200).send(`Reset ${events.size} events`);
} catch (error) {
res.status(500).send(error.message);
}
});
exports.initializeEventFlags = functions.firestore
.document('Events/{eventId}')
.onCreate(async (snapshot, context) => {
try {
const eventData = snapshot.data();
if (eventData.reminderSent === undefined) {
await snapshot.ref.update({
reminderSent: false,
reminderSentAt: null
});
}
} catch (error) {
logger.error(`Error initializing event ${context.params.eventId}:`, error);
}
}); });
/* ───────────────────────────────── /* ─────────────────────────────────
@ -465,11 +702,13 @@ exports.syncNewEventToGoogle = functions.firestore
const newEvent = snapshot.data(); const newEvent = snapshot.data();
const eventId = context.params.eventId; const eventId = context.params.eventId;
await snapshot.ref.update({ if (newEvent.reminderSent === undefined) {
reminderSent: false, await snapshot.ref.update({
eveningReminderSent: false, reminderSent: false,
notifiedAt: null eveningReminderSent: false,
}); notifiedAt: null
});
}
if (newEvent.externalOrigin === "google") { if (newEvent.externalOrigin === "google") {
logger.info("[GOOGLE_SYNC] Skipping sync for Google-originated event", eventId); logger.info("[GOOGLE_SYNC] Skipping sync for Google-originated event", eventId);
@ -1138,93 +1377,6 @@ exports.notifyOnEventUpdate = functions.firestore
} }
}); });
/* ─────────────────────────────────
REMINDER FUNCTION (for upcoming events)
───────────────────────────────── */
// We keep the reminder scheduler mostly as-is but ensure that once a notification is sent, the event is updated
exports.checkUpcomingEvents = functions.pubsub
.schedule("every 5 minutes")
.onRun(async (context) => {
const now = Timestamp.now();
const thirtyMinutesFromNow = new Date(now.toDate().getTime() + 30 * 60 * 1000);
logger.info(`Running check at ${now.toDate().toISOString()}`);
const eventsSnapshot = await db.collection("Events")
.where("startDate", ">=", now)
.where("startDate", "<=", Timestamp.fromDate(thirtyMinutesFromNow))
.get();
const batch = db.batch();
const notificationPromises = [];
for (const doc of eventsSnapshot.docs) {
const event = doc.data();
if (!event?.startDate) continue;
const { familyId, title, allDay } = event;
// Skip if reminder already sent
if (event.reminderSent) {
logger.info(`Reminder already sent for event: ${title}`);
continue;
}
try {
const familyDoc = await db.collection("Households").doc(familyId).get();
if (!familyDoc.exists) continue;
const familySettings = familyDoc.data()?.settings || {};
const reminderTime = familySettings.defaultReminderTime || 10;
const eventTime = event.startDate.toDate();
const timeUntilEvent = eventTime.getTime() - now.toDate().getTime();
const minutesUntilEvent = Math.floor(timeUntilEvent / (60 * 1000));
logger.info(`Checking event: "${title}"`, {
minutesUntilEvent,
reminderTime,
eventTime: eventTime.toISOString()
});
// Modified timing logic: Send reminder when we're close to the reminder time
// This ensures we don't miss the window between function executions
if (minutesUntilEvent <= reminderTime && minutesUntilEvent > 0) {
logger.info(`Preparing to send reminder for: ${title}`);
const pushTokens = await getPushTokensForFamily(familyId);
if (pushTokens.length) {
await sendNotifications(pushTokens, {
title: "Upcoming Event",
body: `In ${minutesUntilEvent} minutes: ${title}`,
data: { type: 'event_reminder', eventId: doc.id }
});
batch.update(doc.ref, {
reminderSent: true,
lastReminderSent: Timestamp.now()
});
logger.info(`Reminder sent for: ${title}`);
}
} else {
logger.info(`Not yet time for reminder: ${title}`, {
minutesUntilEvent,
reminderTime
});
}
} catch (error) {
logger.error(`Error processing reminder for event ${doc.id}:`, error);
}
}
// Commit batch if there are any operations
if (batch._ops.length > 0) {
await batch.commit();
logger.info(`Committed ${batch._ops.length} updates`);
}
});
/* ───────────────────────────────── /* ─────────────────────────────────
MIGRATION UTILITY MIGRATION UTILITY
───────────────────────────────── */ ───────────────────────────────── */
@ -1352,7 +1504,7 @@ exports.sendSyncNotification = onRequest(async (req, res) => {
createdBy: userId, createdBy: userId,
lastSyncTimestamp: admin.firestore.FieldValue.serverTimestamp(), lastSyncTimestamp: admin.firestore.FieldValue.serverTimestamp(),
settings: { settings: {
defaultReminderTime: 15, // Default 15 minutes reminder defaultReminderTime: 30, // Default 30 minutes reminder
} }
}); });
logger.info(`[SYNC] Created new household for family ${familyId}`); logger.info(`[SYNC] Created new household for family ${familyId}`);
@ -1699,4 +1851,88 @@ exports.forceWatchRenewal = onRequest(async (req, res) => {
}); });
await batch.commit(); await batch.commit();
res.status(200).send('Forced renewal of all watches'); res.status(200).send('Forced renewal of all watches');
});
exports.deleteFamily = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'User must be authenticated');
}
const { familyId } = data;
if (!familyId) {
throw new functions.https.HttpsError('invalid-argument', 'Family ID is required');
}
try {
const db = admin.firestore();
const requestingUserProfile = await db.collection('Profiles')
.doc(context.auth.uid)
.get();
if (!requestingUserProfile.exists || requestingUserProfile.data().userType !== 'parent') {
throw new functions.https.HttpsError('permission-denied', 'Only parents can delete families');
}
if (requestingUserProfile.data().familyId !== familyId) {
throw new functions.https.HttpsError('permission-denied', 'You can not delete other families');
}
const profilesSnapshot = await db.collection('Profiles')
.where('familyId', '==', familyId)
.get();
const batch = db.batch();
const profileIds = [];
for (const profile of profilesSnapshot.docs) {
const userId = profile.id;
profileIds.push(userId);
const collections = [
'BrainDumps',
'Groceries',
'Todos',
'Events',
//'Feedbacks'
];
for (const collectionName of collections) {
const userDocsSnapshot = await db.collection(collectionName)
.where('creatorId', '==', userId)
.get();
userDocsSnapshot.docs.forEach(doc => {
batch.delete(doc.ref);
});
}
batch.delete(profile.ref);
}
const householdSnapshot = await db.collection('Households')
.where('familyId', '==', familyId)
.get();
if (!householdSnapshot.empty) {
const householdDoc = householdSnapshot.docs[0];
batch.delete(householdDoc.ref);
} else {
console.log('Household not found for familyId:', familyId);
}
await batch.commit();
// Delete Firebase Auth accounts
await Promise.all(profileIds.map(userId =>
admin.auth().deleteUser(userId)
));
return { success: true, message: 'Family deleted successfully' };
} catch (error) {
console.error('Error deleting family:', error);
throw new functions.https.HttpsError('internal', 'Error deleting family data');
}
}); });