From 286887440e619a09faf6b1afc7782222e0ae3319 Mon Sep 17 00:00:00 2001 From: andupetcu <47487320+andupetcu@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:14:56 +0300 Subject: [PATCH] Implement Family page with full backend integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Fetch and display family details and members - Family share code with copy-to-clipboard functionality - Invite family members via email with role selection - Join another family using share code - Remove family members with confirmation - Visual indicators for current user - Role-based chip colors (Parent/Caregiver/Viewer) - Loading states and error handling - Empty state when no members exist - Success notifications via Snackbar Components Created: - components/family/InviteMemberDialog.tsx: Invite form with email and role - components/family/JoinFamilyDialog.tsx: Join family via share code - components/family/RemoveMemberDialog.tsx: Remove member confirmation All features fully integrated with backend API using familiesApi 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- maternal-web/app/family/page.tsx | 432 +++++++++++++----- .../components/family/InviteMemberDialog.tsx | 126 +++++ .../components/family/JoinFamilyDialog.tsx | 95 ++++ .../components/family/RemoveMemberDialog.tsx | 52 +++ 4 files changed, 603 insertions(+), 102 deletions(-) create mode 100644 maternal-web/components/family/InviteMemberDialog.tsx create mode 100644 maternal-web/components/family/JoinFamilyDialog.tsx create mode 100644 maternal-web/components/family/RemoveMemberDialog.tsx diff --git a/maternal-web/app/family/page.tsx b/maternal-web/app/family/page.tsx index 9960a1c..18e66af 100644 --- a/maternal-web/app/family/page.tsx +++ b/maternal-web/app/family/page.tsx @@ -1,126 +1,354 @@ 'use client'; -import { Box, Typography, Grid, Card, CardContent, Button, Avatar, Chip } from '@mui/material'; -import { PersonAdd, ContentCopy, People } from '@mui/icons-material'; +import { useState, useEffect } from 'react'; +import { + Box, + Typography, + Grid, + Card, + CardContent, + Button, + Avatar, + Chip, + CircularProgress, + Alert, + IconButton, + Divider, + Snackbar, +} from '@mui/material'; +import { PersonAdd, ContentCopy, People, Delete, GroupAdd } from '@mui/icons-material'; import { useAuth } from '@/lib/auth/AuthContext'; import { AppShell } from '@/components/layouts/AppShell/AppShell'; import { ProtectedRoute } from '@/components/common/ProtectedRoute'; +import { familiesApi, Family, FamilyMember, InviteMemberData, JoinFamilyData } from '@/lib/api/families'; +import { InviteMemberDialog } from '@/components/family/InviteMemberDialog'; +import { JoinFamilyDialog } from '@/components/family/JoinFamilyDialog'; +import { RemoveMemberDialog } from '@/components/family/RemoveMemberDialog'; +import { motion } from 'framer-motion'; export default function FamilyPage() { - const { user } = useAuth(); + const { user, refreshUser } = useAuth(); + const [family, setFamily] = useState(null); + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [inviteDialogOpen, setInviteDialogOpen] = useState(false); + const [joinDialogOpen, setJoinDialogOpen] = useState(false); + const [removeDialogOpen, setRemoveDialogOpen] = useState(false); + const [memberToRemove, setMemberToRemove] = useState(null); + const [actionLoading, setActionLoading] = useState(false); + const [snackbar, setSnackbar] = useState({ open: false, message: '' }); - const handleInvite = () => { - // Invite functionality to be implemented - alert('Family invitation feature coming soon!'); + // Get familyId from user + const familyId = user?.families?.[0]?.familyId; + + useEffect(() => { + if (familyId) { + fetchFamilyData(); + } else { + setLoading(false); + setError('No family found. Please complete onboarding first.'); + } + }, [familyId]); + + const fetchFamilyData = async () => { + if (!familyId) return; + + try { + setLoading(true); + setError(''); + const [familyData, membersData] = await Promise.all([ + familiesApi.getFamily(familyId), + familiesApi.getFamilyMembers(familyId), + ]); + setFamily(familyData); + setMembers(membersData); + } catch (err: any) { + console.error('Failed to fetch family data:', err); + setError(err.response?.data?.message || 'Failed to load family information'); + } finally { + setLoading(false); + } }; - const handleCopyCode = () => { - // Copy share code to clipboard - navigator.clipboard.writeText('FAMILY-CODE-123'); - alert('Family code copied to clipboard!'); + const handleCopyCode = async () => { + if (!family?.shareCode) return; + + try { + await navigator.clipboard.writeText(family.shareCode); + setSnackbar({ open: true, message: 'Share code copied to clipboard!' }); + } catch (err) { + setSnackbar({ open: true, message: 'Failed to copy share code' }); + } + }; + + const handleInviteMember = async (data: InviteMemberData) => { + if (!familyId) { + throw new Error('No family ID found'); + } + + try { + setActionLoading(true); + await familiesApi.inviteMember(familyId, data); + setSnackbar({ open: true, message: 'Invitation sent successfully!' }); + await fetchFamilyData(); + setInviteDialogOpen(false); + } catch (err: any) { + console.error('Failed to invite member:', err); + throw new Error(err.response?.data?.message || 'Failed to send invitation'); + } finally { + setActionLoading(false); + } + }; + + const handleJoinFamily = async (data: JoinFamilyData) => { + try { + setActionLoading(true); + await familiesApi.joinFamily(data); + setSnackbar({ open: true, message: 'Successfully joined family!' }); + await refreshUser(); + await fetchFamilyData(); + setJoinDialogOpen(false); + } catch (err: any) { + console.error('Failed to join family:', err); + throw new Error(err.response?.data?.message || 'Failed to join family'); + } finally { + setActionLoading(false); + } + }; + + const handleRemoveClick = (member: FamilyMember) => { + setMemberToRemove(member); + setRemoveDialogOpen(true); + }; + + const handleRemoveConfirm = async () => { + if (!familyId || !memberToRemove) return; + + try { + setActionLoading(true); + await familiesApi.removeMember(familyId, memberToRemove.userId); + setSnackbar({ open: true, message: 'Member removed successfully' }); + await fetchFamilyData(); + setRemoveDialogOpen(false); + setMemberToRemove(null); + } catch (err: any) { + console.error('Failed to remove member:', err); + setError(err.response?.data?.message || 'Failed to remove member'); + } finally { + setActionLoading(false); + } + }; + + const getRoleColor = (role: string): 'primary' | 'secondary' | 'default' | 'success' | 'warning' | 'info' => { + switch (role) { + case 'parent': + return 'primary'; + case 'caregiver': + return 'secondary'; + case 'viewer': + return 'info'; + default: + return 'default'; + } + }; + + const isCurrentUser = (userId: string) => { + return user?.id === userId; }; return ( - - - - Family - - - Manage your family members and share access - + + + + Family + + + Manage your family members and share access + + + + + + + + + {error && ( + setError('')}> + {error} + + )} + + {loading ? ( + + + + ) : ( + + {/* Family Share Code */} + {family && ( + + + + + Family Share Code + + + Share this code with family members to give them access to your family's data + + + + + + + + + )} + + {/* Family Members */} + + + + + Family Members ({members.length}) + + + {members.length === 0 ? ( + + + + No family members yet + + + Invite family members to collaborate on child care + + + + ) : ( + + {members.map((member, index) => ( + + + {index > 0 && } + + + {member.user?.name?.charAt(0).toUpperCase() || 'U'} + + + + + {member.user?.name || 'Unknown User'} + + {isCurrentUser(member.userId) && ( + + )} + + + {member.user?.email || 'No email'} + + + + {!isCurrentUser(member.userId) && ( + handleRemoveClick(member)} + color="error" + > + + + )} + + + + ))} + + )} + + + + + )} - - - - {/* Family Share Code */} - - - - - Family Share Code - - - Share this code with family members to give them access to your family's data - - - - - - - - + setInviteDialogOpen(false)} + onSubmit={handleInviteMember} + isLoading={actionLoading} + /> - {/* Family Members */} - - - - - Family Members - + setJoinDialogOpen(false)} + onSubmit={handleJoinFamily} + isLoading={actionLoading} + /> - {/* Current User */} - - - {user?.name?.charAt(0).toUpperCase()} - - - - {user?.name} - - - {user?.email} - - - - + setRemoveDialogOpen(false)} + onConfirm={handleRemoveConfirm} + memberName={memberToRemove?.user?.name || ''} + isLoading={actionLoading} + /> - {/* Empty State */} - - - - No other family members yet - - - Invite family members to collaborate on child care - - - - - - - - + setSnackbar({ ...snackbar, open: false })} + message={snackbar.message} + /> ); diff --git a/maternal-web/components/family/InviteMemberDialog.tsx b/maternal-web/components/family/InviteMemberDialog.tsx new file mode 100644 index 0000000..4402798 --- /dev/null +++ b/maternal-web/components/family/InviteMemberDialog.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + MenuItem, + Box, + Alert, +} from '@mui/material'; +import { InviteMemberData } from '@/lib/api/families'; + +interface InviteMemberDialogProps { + open: boolean; + onClose: () => void; + onSubmit: (data: InviteMemberData) => Promise; + isLoading?: boolean; +} + +export function InviteMemberDialog({ + open, + onClose, + onSubmit, + isLoading = false, +}: InviteMemberDialogProps) { + const [formData, setFormData] = useState({ + email: '', + role: 'viewer', + }); + const [error, setError] = useState(''); + + useEffect(() => { + if (open) { + setFormData({ + email: '', + role: 'viewer', + }); + setError(''); + } + }, [open]); + + const handleChange = (field: keyof InviteMemberData) => ( + e: React.ChangeEvent + ) => { + setFormData({ ...formData, [field]: e.target.value }); + }; + + const handleSubmit = async () => { + setError(''); + + // Validation + if (!formData.email.trim()) { + setError('Please enter an email address'); + return; + } + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + setError('Please enter a valid email address'); + return; + } + + try { + await onSubmit(formData); + onClose(); + } catch (err: any) { + setError(err.message || 'Failed to invite member'); + } + }; + + return ( + + Invite Family Member + + + {error && ( + setError('')}> + {error} + + )} + + + + + Parent - Full access to all features + Caregiver - Can manage daily activities + Viewer - Can only view information + + + + + + + + + ); +} diff --git a/maternal-web/components/family/JoinFamilyDialog.tsx b/maternal-web/components/family/JoinFamilyDialog.tsx new file mode 100644 index 0000000..868aa34 --- /dev/null +++ b/maternal-web/components/family/JoinFamilyDialog.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Box, + Alert, + Typography, +} from '@mui/material'; +import { JoinFamilyData } from '@/lib/api/families'; + +interface JoinFamilyDialogProps { + open: boolean; + onClose: () => void; + onSubmit: (data: JoinFamilyData) => Promise; + isLoading?: boolean; +} + +export function JoinFamilyDialog({ + open, + onClose, + onSubmit, + isLoading = false, +}: JoinFamilyDialogProps) { + const [shareCode, setShareCode] = useState(''); + const [error, setError] = useState(''); + + useEffect(() => { + if (open) { + setShareCode(''); + setError(''); + } + }, [open]); + + const handleSubmit = async () => { + setError(''); + + // Validation + if (!shareCode.trim()) { + setError('Please enter a share code'); + return; + } + + try { + await onSubmit({ shareCode: shareCode.trim() }); + onClose(); + } catch (err: any) { + setError(err.message || 'Failed to join family'); + } + }; + + return ( + + Join a Family + + + {error && ( + setError('')}> + {error} + + )} + + + Enter the share code provided by the family administrator to join their family. + + + setShareCode(e.target.value)} + fullWidth + required + autoFocus + disabled={isLoading} + placeholder="Enter family share code" + helperText="Ask a family member for their share code" + /> + + + + + + + + ); +} diff --git a/maternal-web/components/family/RemoveMemberDialog.tsx b/maternal-web/components/family/RemoveMemberDialog.tsx new file mode 100644 index 0000000..6f37758 --- /dev/null +++ b/maternal-web/components/family/RemoveMemberDialog.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, +} from '@mui/material'; +import { Warning } from '@mui/icons-material'; + +interface RemoveMemberDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + memberName: string; + isLoading?: boolean; +} + +export function RemoveMemberDialog({ + open, + onClose, + onConfirm, + memberName, + isLoading = false, +}: RemoveMemberDialogProps) { + return ( + + + + Remove Family Member + + + + Are you sure you want to remove {memberName} from your family? + + + This member will lose access to all family data and activities. + + + + + + + + ); +}