mirror of
https://github.com/urosran/cally.git
synced 2025-07-15 01:35:22 +00:00
Adding and displaying family members
This commit is contained in:
@ -1,33 +1,25 @@
|
|||||||
import {
|
import {Avatar, Card, Colors, FloatingButton, Text, View} from "react-native-ui-lib";
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Colors,
|
|
||||||
Dialog,
|
|
||||||
FloatingButton,
|
|
||||||
PanningProvider,
|
|
||||||
Picker,
|
|
||||||
Text,
|
|
||||||
TextField,
|
|
||||||
TouchableOpacity,
|
|
||||||
View
|
|
||||||
} from "react-native-ui-lib";
|
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {ScrollView, StyleSheet} from "react-native";
|
import {ScrollView, StyleSheet} from "react-native";
|
||||||
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
|
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
|
||||||
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
|
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
|
||||||
import {ProfileType} from "@/contexts/AuthContext";
|
import {ProfileType} from "@/contexts/AuthContext";
|
||||||
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
|
|
||||||
const MyGroup = () => {
|
const MyGroup = () => {
|
||||||
const [showAddUserDialog, setShowAddUserDialog] = useState(false)
|
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
|
||||||
const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false)
|
const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
|
||||||
|
|
||||||
const [selectedStatus, setSelectedStatus] = useState<string | PickerSingleValue>('CHILD');
|
const [selectedStatus, setSelectedStatus] = useState<string | PickerSingleValue>('CHILD');
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const {mutateAsync: createSubUser} = useCreateSubUser()
|
const {mutateAsync: createSubUser, isLoading: loading, isError} = useCreateSubUser();
|
||||||
|
const {data: familyMembers} = useGetFamilyMembers(true);
|
||||||
|
|
||||||
|
const parents = familyMembers?.filter(x => x.userType === ProfileType.PARENT) ?? [];
|
||||||
|
const children = familyMembers?.filter(x => x.userType === ProfileType.CHILD) ?? [];
|
||||||
|
const caregivers = familyMembers?.filter(x => x.userType === ProfileType.CAREGIVER) ?? [];
|
||||||
|
|
||||||
const handleCreateSubUser = async () => {
|
const handleCreateSubUser = async () => {
|
||||||
if (!firstName || !lastName || !email) {
|
if (!firstName || !lastName || !email) {
|
||||||
@ -47,122 +39,76 @@ const MyGroup = () => {
|
|||||||
password: email,
|
password: email,
|
||||||
userType: selectedStatus as ProfileType
|
userType: selectedStatus as ProfileType
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isError) {
|
||||||
|
setShowNewUserInfoDialog(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, height: "100%"}}>
|
<View style={{flex: 1, height: "100%"}}>
|
||||||
<View>
|
<View>
|
||||||
<ScrollView style={styles.card}>
|
<ScrollView style={styles.card}>
|
||||||
|
{(!parents.length && !children.length && !caregivers.length) && (
|
||||||
|
<Text text70 marginV-10>
|
||||||
|
No user devices added
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(!!parents.length || !!children.length) && (
|
||||||
|
<>
|
||||||
<Text text70 marginV-10>
|
<Text text70 marginV-10>
|
||||||
Family
|
Family
|
||||||
</Text>
|
</Text>
|
||||||
|
{[...parents, ...children]?.map(member => (
|
||||||
|
<Card enableShadow={false} elevation={0} key={`${member.firstName}_${member.lastName}`}
|
||||||
|
style={styles.familyCard} row centerV padding-10>
|
||||||
|
<Avatar
|
||||||
|
source={{uri: 'https://via.placeholder.com/60'}}
|
||||||
|
size={40}
|
||||||
|
backgroundColor={Colors.grey60}
|
||||||
|
/>
|
||||||
|
<View marginL-10>
|
||||||
|
<Text text70M>{member.firstName} {member.lastName}</Text>
|
||||||
|
<Text text90
|
||||||
|
grey40>{member.userType === ProfileType.PARENT ? "Admin (You)" : "Child"}</Text>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!caregivers.length && (
|
||||||
|
<>
|
||||||
<Text text70 marginB-10 marginT-15>
|
<Text text70 marginB-10 marginT-15>
|
||||||
Caregivers
|
Caregivers
|
||||||
</Text>
|
</Text>
|
||||||
|
{caregivers?.map(member => (
|
||||||
|
<Card enableShadow={false} elevation={0} key={`${member.firstName}_${member.lastName}`}
|
||||||
|
style={styles.familyCard} row centerV padding-10>
|
||||||
|
<Avatar
|
||||||
|
source={{uri: 'https://via.placeholder.com/60'}}
|
||||||
|
size={40}
|
||||||
|
backgroundColor={Colors.grey60}
|
||||||
|
/>
|
||||||
|
<View marginL-10>
|
||||||
|
<Text text70M>{member.firstName} {member.lastName}</Text>
|
||||||
|
<Text text90 grey40>Caregiver</Text>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FloatingButton fullWidth hideBackgroundOverlay visible
|
<FloatingButton fullWidth hideBackgroundOverlay visible
|
||||||
button={{label: '+ Add a user device', onPress: () => setShowAddUserDialog(true)}}/>
|
button={{label: '+ Add a user device', onPress: () => setShowAddUserDialog(true)}}/>
|
||||||
|
|
||||||
<Dialog
|
{/* Add user dialog here */}
|
||||||
visible={showAddUserDialog}
|
|
||||||
onDismiss={() => setShowAddUserDialog(false)}
|
|
||||||
panDirection={PanningProvider.Directions.DOWN}
|
|
||||||
>
|
|
||||||
<Card padding-25 gap-10>
|
|
||||||
<Text>
|
|
||||||
Add a new user device
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Button backgroundColor={"#FD1775"}><Text white>Show a QR Code</Text></Button>
|
{/* New user information dialog here */}
|
||||||
<Button backgroundColor={"#05A8B6"} onPress={() => {
|
|
||||||
setShowAddUserDialog(false)
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowNewUserInfoDialog(true)
|
|
||||||
}, 500)
|
|
||||||
}}><Text white>Enter email address</Text></Button>
|
|
||||||
|
|
||||||
|
|
||||||
<TouchableOpacity onPress={() => setShowAddUserDialog(false)} center><Text>Return to user
|
|
||||||
settings</Text></TouchableOpacity>
|
|
||||||
</Card>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
panDirection={PanningProvider.Directions.DOWN}
|
|
||||||
visible={showNewUserInfoDialog}
|
|
||||||
onDismiss={() => setShowNewUserInfoDialog(false)}
|
|
||||||
>
|
|
||||||
<Card padding-25 style={styles.dialogCard}>
|
|
||||||
<View row spread>
|
|
||||||
<Text text60M>
|
|
||||||
New User Information
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity onPress={() => setShowAddUserDialog(false)}>
|
|
||||||
<Text>X</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View row centerV gap-20 marginV-20>
|
|
||||||
<Avatar imageStyle={{borderRadius: 10}} containerStyle={{borderRadius: 10,}} size={60}
|
|
||||||
backgroundColor={Colors.grey60}/>
|
|
||||||
<TouchableOpacity onPress={() => {
|
|
||||||
}}>
|
|
||||||
<Text style={{color: Colors.green10}}>Upload User Profile Photo</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Member Status</Text>
|
|
||||||
<Picker
|
|
||||||
value={selectedStatus}
|
|
||||||
//@ts-ignore
|
|
||||||
onChange={item => setSelectedStatus(item)}
|
|
||||||
style={styles.picker}
|
|
||||||
showSearch
|
|
||||||
floatingPlaceholder
|
|
||||||
>
|
|
||||||
<Picker.Item label="Child" value="CHILD"/>
|
|
||||||
<Picker.Item label="Parent" value="PARENT"/>
|
|
||||||
<Picker.Item label="Caregiver" value="CAREGIVER"/>
|
|
||||||
</Picker>
|
|
||||||
|
|
||||||
|
|
||||||
<Text style={styles.label}>First Name</Text>
|
|
||||||
<TextField
|
|
||||||
placeholder="First name"
|
|
||||||
value={firstName}
|
|
||||||
onChangeText={setFirstName}
|
|
||||||
style={styles.inputField}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Last Name</Text>
|
|
||||||
<TextField
|
|
||||||
placeholder="Last name"
|
|
||||||
value={lastName}
|
|
||||||
onChangeText={setLastName}
|
|
||||||
style={styles.inputField}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Email Address</Text>
|
|
||||||
<TextField
|
|
||||||
placeholder="Email address"
|
|
||||||
value={email}
|
|
||||||
onChangeText={setEmail}
|
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
|
||||||
style={styles.inputField}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={!firstName || !lastName || !email}
|
|
||||||
label="Add group member"
|
|
||||||
backgroundColor="#FD1775"
|
|
||||||
style={{marginTop: 20}}
|
|
||||||
onPress={handleCreateSubUser}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Dialog>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -175,19 +121,11 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 15,
|
borderRadius: 15,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
pfp: {
|
familyCard: {
|
||||||
aspectRatio: 1,
|
marginBottom: 10,
|
||||||
width: 60,
|
borderRadius: 10,
|
||||||
backgroundColor: "green",
|
backgroundColor: Colors.white,
|
||||||
borderRadius: 20,
|
width: "100%"
|
||||||
},
|
|
||||||
txtBox: {
|
|
||||||
backgroundColor: "#fafafa",
|
|
||||||
borderRadius: 50,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: "#cecece",
|
|
||||||
padding: 15,
|
|
||||||
height: 45,
|
|
||||||
},
|
},
|
||||||
inputField: {
|
inputField: {
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
@ -220,4 +158,5 @@ const styles = StyleSheet.create({
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MyGroup;
|
export default MyGroup;
|
||||||
|
@ -4,31 +4,72 @@ const {getFirestore} = require("firebase-admin/firestore");
|
|||||||
const admin = require("firebase-admin");
|
const admin = require("firebase-admin");
|
||||||
const logger = require("firebase-functions/logger");
|
const logger = require("firebase-functions/logger");
|
||||||
|
|
||||||
admin.initializeApp();
|
|
||||||
|
|
||||||
exports.createSubUser = onRequest(async (request, response) => {
|
|
||||||
try {
|
try {
|
||||||
logger.info("Processing user creation", {requestBody: request.body.data});
|
admin.initializeApp();
|
||||||
|
} catch (error) {
|
||||||
const {userType, name, email, password} = request.body.data;
|
console.error(error)
|
||||||
|
|
||||||
if (!email || !password || !name || !userType) {
|
|
||||||
throw new Error("Missing required fields");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRecord = await getAuth().createUser({
|
exports.createSubUser = onRequest(async (request, response) => {
|
||||||
email, password, displayName: name,
|
const authHeader = request.get('Authorization');
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
logger.warn("Missing or incorrect Authorization header", {authHeader});
|
||||||
|
response.status(401).json({error: 'Unauthorized'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = authHeader.split('Bearer ')[1];
|
||||||
|
logger.info("Verifying ID token", {token});
|
||||||
|
|
||||||
|
let decodedToken;
|
||||||
|
try {
|
||||||
|
decodedToken = await getAuth().verifyIdToken(token);
|
||||||
|
logger.info("ID token verified successfully", {uid: decodedToken.uid});
|
||||||
|
} catch (verifyError) {
|
||||||
|
logger.error("ID token verification failed", {error: verifyError.message});
|
||||||
|
response.status(401).json({error: 'Unauthorized: Invalid token'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Processing user creation", {requestBody: request.body.data});
|
||||||
|
|
||||||
|
const {userType, firstName, lastName, email, password} = request.body.data;
|
||||||
|
|
||||||
|
if (!email || !password || !firstName || !lastName || !userType) {
|
||||||
|
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
|
||||||
|
response.status(400).json({error: "Missing required fields"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userRecord;
|
||||||
|
try {
|
||||||
|
userRecord = await getAuth().createUser({
|
||||||
|
email, password, displayName: `${firstName} ${lastName}`,
|
||||||
});
|
});
|
||||||
|
logger.info("User record created", {userId: userRecord.uid});
|
||||||
|
} catch (createUserError) {
|
||||||
|
logger.error("User creation failed", {error: createUserError.message});
|
||||||
|
response.status(500).json({error: "Failed to create user"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userProfile = {
|
const userProfile = {
|
||||||
userType, name, email, uid: userRecord.uid,
|
userType, name: `${firstName} ${lastName}`, email, uid: userRecord.uid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
await getFirestore().collection("Profiles").doc(userRecord.uid).set(userProfile);
|
await getFirestore().collection("Profiles").doc(userRecord.uid).set(userProfile);
|
||||||
|
logger.info("User profile saved to Firestore", {userId: userRecord.uid});
|
||||||
|
} catch (firestoreError) {
|
||||||
|
logger.error("Failed to save user profile to Firestore", {error: firestoreError.message});
|
||||||
|
response.status(500).json({error: "Failed to save user profile"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
response.status(200).json({
|
response.status(200).json({
|
||||||
data: {
|
data: {
|
||||||
|
|
||||||
message: "User created successfully", userId: userRecord.uid,
|
message: "User created successfully", userId: userRecord.uid,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,7 @@ export interface User {
|
|||||||
contact?: string;
|
contact?: string;
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
familyId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParentProfile extends UserProfile {
|
export interface ParentProfile extends UserProfile {
|
||||||
|
@ -5,13 +5,16 @@ import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
|||||||
|
|
||||||
export const useCreateSubUser = () => {
|
export const useCreateSubUser = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { profileType } = useAuthContext()
|
const {profileType, profileData} = useAuthContext()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["createSubUser"],
|
mutationKey: ["createSubUser"],
|
||||||
mutationFn: async ({email, ...userProfile}: { email: string } & UserProfile) => {
|
mutationFn: async ({email, ...userProfile}: { email: string } & UserProfile) => {
|
||||||
if (profileType === ProfileType.PARENT) {
|
if (profileType === ProfileType.PARENT) {
|
||||||
return await functions().httpsCallable("createSubUser")({email, ...userProfile})
|
return await functions().httpsCallable("createSubUser")({
|
||||||
|
email,
|
||||||
|
userProfile: {...userProfile, familyId: profileData?.familyId}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
throw Error("Can't create sub-users as a non-parent.")
|
throw Error("Can't create sub-users as a non-parent.")
|
||||||
}
|
}
|
||||||
|
30
hooks/firebase/useEditFamily.ts
Normal file
30
hooks/firebase/useEditFamily.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {useMutation} from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
|
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
||||||
|
|
||||||
|
export const useEditFamily = () => {
|
||||||
|
const {user: currentUser, setProfileData} = useAuthContext()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["editFamily"],
|
||||||
|
mutationFn: async ({newUserData, customUser}: {newUserData: Partial<UserProfile>, customUser?: FirebaseAuthTypes.User }) => {
|
||||||
|
const user = currentUser ?? customUser
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
try {
|
||||||
|
await firestore()
|
||||||
|
.collection("Families")
|
||||||
|
.doc(user.uid)
|
||||||
|
.set(newUserData);
|
||||||
|
|
||||||
|
const profileData = await firestore().collection("Profiles").doc(user?.uid!).get()
|
||||||
|
setProfileData(profileData.data() as UserProfile)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
24
hooks/firebase/useGetFamilyMembers.ts
Normal file
24
hooks/firebase/useGetFamilyMembers.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {useQuery} from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
|
|
||||||
|
export const useGetFamilyMembers = (excludeSelf?: boolean) => {
|
||||||
|
const {profileData, user} = useAuthContext()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["familyMembers", user?.uid],
|
||||||
|
queryFn: async (): Promise<undefined | UserProfile[]> => {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Profiles")
|
||||||
|
.where("familyId", "==", profileData?.familyId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (excludeSelf) {
|
||||||
|
return snapshot.docs.map((doc) => doc.data()).filter((doc) => doc.id !== user?.uid) as UserProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => doc.data()) as UserProfile[];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { useMutation } from "react-query";
|
|||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import { ProfileType } from "@/contexts/AuthContext";
|
import { ProfileType } from "@/contexts/AuthContext";
|
||||||
import { useSetUserData } from "./useSetUserData";
|
import { useSetUserData } from "./useSetUserData";
|
||||||
|
import {uuidv4} from "@firebase/util";
|
||||||
|
|
||||||
export const useSignUp = () => {
|
export const useSignUp = () => {
|
||||||
const { mutateAsync: setUserData } = useSetUserData();
|
const { mutateAsync: setUserData } = useSetUserData();
|
||||||
@ -28,6 +29,7 @@ export const useSignUp = () => {
|
|||||||
userType: ProfileType.PARENT,
|
userType: ProfileType.PARENT,
|
||||||
firstName: firstName,
|
firstName: firstName,
|
||||||
lastName: lastName,
|
lastName: lastName,
|
||||||
|
familyId: uuidv4(),
|
||||||
},
|
},
|
||||||
customUser: res.user,
|
customUser: res.user,
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user