Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
This commit adds a complete onboarding improvements system including progress tracking, streamlined UI, and role-based family invitation system. ## Backend Changes ### Database Migrations - Add onboarding tracking fields to users table (onboarding_completed, onboarding_step, onboarding_data) - Add role-based invite codes to families table (parent/caregiver/viewer codes with expiration) - Add indexes for fast invite code lookups ### User Preferences Module - Add UserPreferencesController with onboarding endpoints - Add UserPreferencesService with progress tracking methods - Add UpdateOnboardingProgressDto for validation - Endpoints: GET/PUT /api/v1/preferences/onboarding, POST /api/v1/preferences/onboarding/complete ### Families Module - Role-Based Invites - Add generateRoleInviteCode() - Generate role-specific codes with expiration - Add getRoleInviteCodes() - Retrieve all active codes for a family - Add joinFamilyWithRoleCode() - Join family with automatic role assignment - Add revokeRoleInviteCode() - Revoke specific role invite codes - Add sendEmailInvite() - Generate code and send email invitation - Endpoints: POST/GET/DELETE /api/v1/families/:id/invite-codes, POST /api/v1/families/join-with-role, POST /api/v1/families/:id/email-invite ### Email Service - Add sendFamilyInviteEmail() - Send role-based invitation emails - Beautiful HTML templates with role badges (👨👩👧 parent, 🤝 caregiver, 👁️ viewer) - Role-specific permission descriptions - Graceful fallback if email sending fails ### Auth Service - Fix duplicate family creation bug in joinFamily() - Ensure users only join family once during onboarding ## Frontend Changes ### Onboarding Page - Reduce steps from 5 to 4 (combined language + measurements) - Replace card-based selection with dropdown selectors - Add automatic progress saving after each step - Add progress restoration on page mount - Extract FamilySetupStep into reusable component ### Family Page - Add RoleInvitesSection component with accordion UI - Generate/view/copy/regenerate/revoke controls for each role - Send email invites directly from UI - Display expiration dates (e.g., "Expires in 5 days") - Info tooltips explaining role permissions - Only visible to users with parent role ### API Client - Add role-based invite methods to families API - Add onboarding progress methods to users API - TypeScript interfaces for all new data structures ## Features ✅ Streamlined 4-step onboarding with dropdown selectors ✅ Automatic progress save/restore across sessions ✅ Role-based family invites (parent/caregiver/viewer) ✅ Beautiful email invitations with role descriptions ✅ Automatic role assignment when joining with invite codes ✅ Granular permission control per role ✅ Email fallback if sending fails ✅ All changes tested and production-ready 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
252 lines
7.1 KiB
TypeScript
252 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Box,
|
|
Card,
|
|
CardActionArea,
|
|
CardContent,
|
|
Typography,
|
|
TextField,
|
|
Button,
|
|
Alert,
|
|
MenuItem,
|
|
} from '@mui/material';
|
|
import { FamilyRestroom, PersonAdd } from '@mui/icons-material';
|
|
|
|
export interface ChildData {
|
|
name: string;
|
|
birthDate: string;
|
|
gender: 'male' | 'female' | 'other';
|
|
}
|
|
|
|
interface FamilySetupStepProps {
|
|
onCreateFamily: (childData: ChildData) => Promise<void>;
|
|
onJoinFamily: (shareCode: string) => Promise<void>;
|
|
loading: boolean;
|
|
error: string;
|
|
t: (key: string) => string;
|
|
}
|
|
|
|
export function FamilySetupStep({
|
|
onCreateFamily,
|
|
onJoinFamily,
|
|
loading,
|
|
error,
|
|
t,
|
|
}: FamilySetupStepProps) {
|
|
const [choice, setChoice] = useState<'create' | 'join' | null>(null);
|
|
const [shareCode, setShareCode] = useState('');
|
|
const [childName, setChildName] = useState('');
|
|
const [childBirthDate, setChildBirthDate] = useState('');
|
|
const [childGender, setChildGender] = useState<'male' | 'female' | 'other'>('other');
|
|
|
|
if (choice === 'create') {
|
|
return (
|
|
<Box sx={{ py: 4 }}>
|
|
<Typography variant="h5" gutterBottom fontWeight="600">
|
|
{t('child.title')}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
|
{t('child.subtitle')}
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label={t('child.name')}
|
|
value={childName}
|
|
onChange={(e) => setChildName(e.target.value)}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
}}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label={t('child.dateOfBirth')}
|
|
type="date"
|
|
value={childBirthDate}
|
|
onChange={(e) => setChildBirthDate(e.target.value)}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
InputLabelProps={{
|
|
shrink: true,
|
|
}}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
}}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
select
|
|
label={t('child.gender')}
|
|
value={childGender}
|
|
onChange={(e) => setChildGender(e.target.value as 'male' | 'female' | 'other')}
|
|
margin="normal"
|
|
disabled={loading}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
}}
|
|
>
|
|
<MenuItem value="male">{t('child.genders.male')}</MenuItem>
|
|
<MenuItem value="female">{t('child.genders.female')}</MenuItem>
|
|
<MenuItem value="other">{t('child.genders.preferNotToSay')}</MenuItem>
|
|
</TextField>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
|
|
<Button onClick={() => setChoice(null)} disabled={loading}>
|
|
{t('navigation.back')}
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
if (!childName.trim() || !childBirthDate) {
|
|
return;
|
|
}
|
|
onCreateFamily({ name: childName, birthDate: childBirthDate, gender: childGender });
|
|
}}
|
|
disabled={loading || !childName.trim() || !childBirthDate}
|
|
fullWidth
|
|
>
|
|
Create Family
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (choice === 'join') {
|
|
return (
|
|
<Box sx={{ py: 4 }}>
|
|
<Typography variant="h5" gutterBottom fontWeight="600">
|
|
Join Existing Family
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
|
Enter the family code to join an existing family
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Family Code"
|
|
value={shareCode}
|
|
onChange={(e) => setShareCode(e.target.value.toUpperCase())}
|
|
margin="normal"
|
|
placeholder="ABC123DEF"
|
|
helperText="Ask your family admin for the code"
|
|
required
|
|
disabled={loading}
|
|
InputProps={{
|
|
sx: { borderRadius: 3 },
|
|
}}
|
|
/>
|
|
|
|
<Alert severity="info" sx={{ mt: 2, borderRadius: 2 }}>
|
|
You'll be added as a viewer by default. The admin can change your role later.
|
|
</Alert>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
|
|
<Button onClick={() => setChoice(null)} disabled={loading}>
|
|
{t('navigation.back')}
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => onJoinFamily(shareCode)}
|
|
disabled={loading || !shareCode}
|
|
fullWidth
|
|
>
|
|
Join Family
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ py: 4 }}>
|
|
<Typography variant="h5" gutterBottom align="center" fontWeight="600">
|
|
Choose Your Setup
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" align="center" sx={{ mb: 4 }}>
|
|
Start tracking or join an existing family
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<Card
|
|
sx={{
|
|
transition: 'all 0.2s',
|
|
'&:hover': {
|
|
transform: 'translateY(-4px)',
|
|
boxShadow: 4,
|
|
},
|
|
}}
|
|
>
|
|
<CardActionArea onClick={() => setChoice('create')} sx={{ p: 2 }}>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<FamilyRestroom fontSize="large" color="primary" />
|
|
<Box>
|
|
<Typography variant="h6" fontWeight="600">
|
|
Create New Family
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Start tracking for your child. Invite family members later.
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</CardContent>
|
|
</CardActionArea>
|
|
</Card>
|
|
|
|
<Card
|
|
sx={{
|
|
transition: 'all 0.2s',
|
|
'&:hover': {
|
|
transform: 'translateY(-4px)',
|
|
boxShadow: 4,
|
|
},
|
|
}}
|
|
>
|
|
<CardActionArea onClick={() => setChoice('join')} sx={{ p: 2 }}>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<PersonAdd fontSize="large" color="primary" />
|
|
<Box>
|
|
<Typography variant="h6" fontWeight="600">
|
|
Join Existing Family
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Enter family code to join. Access shared tracking.
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</CardContent>
|
|
</CardActionArea>
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|