Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84045a9c92 | ||
|
|
1b09a7d901 | ||
|
|
28dd8852af | ||
|
|
51057a3d26 | ||
|
|
0d6b901995 | ||
|
|
fa8db55f93 |
@@ -17,6 +17,16 @@ export enum ErrorCode {
|
|||||||
AUTH_SESSION_EXPIRED = 'AUTH_SESSION_EXPIRED',
|
AUTH_SESSION_EXPIRED = 'AUTH_SESSION_EXPIRED',
|
||||||
AUTH_EMAIL_NOT_VERIFIED = 'AUTH_EMAIL_NOT_VERIFIED',
|
AUTH_EMAIL_NOT_VERIFIED = 'AUTH_EMAIL_NOT_VERIFIED',
|
||||||
|
|
||||||
|
// Invite Code Errors (INVITE_CODE_*)
|
||||||
|
INVITE_CODE_INVALID = 'INVITE_CODE_INVALID',
|
||||||
|
INVITE_CODE_EXPIRED = 'INVITE_CODE_EXPIRED',
|
||||||
|
INVITE_CODE_ALREADY_USED = 'INVITE_CODE_ALREADY_USED',
|
||||||
|
INVITE_CODE_MAX_USES_REACHED = 'INVITE_CODE_MAX_USES_REACHED',
|
||||||
|
|
||||||
|
// Email Verification Errors (EMAIL_VERIFICATION_*)
|
||||||
|
EMAIL_VERIFICATION_TOKEN_INVALID = 'EMAIL_VERIFICATION_TOKEN_INVALID',
|
||||||
|
EMAIL_VERIFICATION_TOKEN_EXPIRED = 'EMAIL_VERIFICATION_TOKEN_EXPIRED',
|
||||||
|
|
||||||
// User Errors (USER_*)
|
// User Errors (USER_*)
|
||||||
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||||
USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
|
USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
|
||||||
@@ -182,6 +192,52 @@ export const ErrorMessages: Record<ErrorCode, Record<string, string>> = {
|
|||||||
'zh-CN': '请验证您的电子邮件地址以继续',
|
'zh-CN': '请验证您的电子邮件地址以继续',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Invite Code Errors
|
||||||
|
[ErrorCode.INVITE_CODE_INVALID]: {
|
||||||
|
'en-US': 'Invalid or inactive invite code. Please check the code and try again',
|
||||||
|
'es-ES': 'Código de invitación inválido o inactivo. Por favor verifica el código e intenta de nuevo',
|
||||||
|
'fr-FR': 'Code d\'invitation invalide ou inactif. Veuillez vérifier le code et réessayer',
|
||||||
|
'pt-BR': 'Código de convite inválido ou inativo. Por favor, verifique o código e tente novamente',
|
||||||
|
'zh-CN': '无效或未激活的邀请码。请检查代码并重试',
|
||||||
|
},
|
||||||
|
[ErrorCode.INVITE_CODE_EXPIRED]: {
|
||||||
|
'en-US': 'This invite code has expired. Please request a new code',
|
||||||
|
'es-ES': 'Este código de invitación ha expirado. Por favor solicita un nuevo código',
|
||||||
|
'fr-FR': 'Ce code d\'invitation a expiré. Veuillez demander un nouveau code',
|
||||||
|
'pt-BR': 'Este código de convite expirou. Por favor, solicite um novo código',
|
||||||
|
'zh-CN': '此邀请码已过期。请申请新代码',
|
||||||
|
},
|
||||||
|
[ErrorCode.INVITE_CODE_ALREADY_USED]: {
|
||||||
|
'en-US': 'This invite code has already been used',
|
||||||
|
'es-ES': 'Este código de invitación ya ha sido utilizado',
|
||||||
|
'fr-FR': 'Ce code d\'invitation a déjà été utilisé',
|
||||||
|
'pt-BR': 'Este código de convite já foi usado',
|
||||||
|
'zh-CN': '此邀请码已被使用',
|
||||||
|
},
|
||||||
|
[ErrorCode.INVITE_CODE_MAX_USES_REACHED]: {
|
||||||
|
'en-US': 'This invite code has reached its maximum number of uses',
|
||||||
|
'es-ES': 'Este código de invitación ha alcanzado su número máximo de usos',
|
||||||
|
'fr-FR': 'Ce code d\'invitation a atteint son nombre maximum d\'utilisations',
|
||||||
|
'pt-BR': 'Este código de convite atingiu seu número máximo de usos',
|
||||||
|
'zh-CN': '此邀请码已达到最大使用次数',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Email Verification Errors
|
||||||
|
[ErrorCode.EMAIL_VERIFICATION_TOKEN_INVALID]: {
|
||||||
|
'en-US': 'Invalid email verification token. Please request a new verification email',
|
||||||
|
'es-ES': 'Token de verificación de correo inválido. Por favor solicita un nuevo correo de verificación',
|
||||||
|
'fr-FR': 'Jeton de vérification d\'email invalide. Veuillez demander un nouvel email de vérification',
|
||||||
|
'pt-BR': 'Token de verificação de email inválido. Por favor, solicite um novo email de verificação',
|
||||||
|
'zh-CN': '无效的电子邮件验证令牌。请申请新的验证电子邮件',
|
||||||
|
},
|
||||||
|
[ErrorCode.EMAIL_VERIFICATION_TOKEN_EXPIRED]: {
|
||||||
|
'en-US': 'Email verification link has expired. Please request a new verification email',
|
||||||
|
'es-ES': 'El enlace de verificación de correo ha expirado. Por favor solicita un nuevo correo de verificación',
|
||||||
|
'fr-FR': 'Le lien de vérification d\'email a expiré. Veuillez demander un nouvel email de vérification',
|
||||||
|
'pt-BR': 'O link de verificação de email expirou. Por favor, solicite um novo email de verificação',
|
||||||
|
'zh-CN': '电子邮件验证链接已过期。请申请新的验证电子邮件',
|
||||||
|
},
|
||||||
|
|
||||||
// User Errors
|
// User Errors
|
||||||
[ErrorCode.USER_NOT_FOUND]: {
|
[ErrorCode.USER_NOT_FOUND]: {
|
||||||
'en-US': 'User not found',
|
'en-US': 'User not found',
|
||||||
|
|||||||
@@ -392,6 +392,62 @@ export class AnalyticsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced Analytics Dashboard - Aggregates all advanced analytics
|
||||||
|
* GET /api/v1/analytics/advanced/dashboard/:childId
|
||||||
|
*/
|
||||||
|
@Get('advanced/dashboard/:childId')
|
||||||
|
async getAdvancedDashboard(
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
@Param('childId') childId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Fetch all analytics data in parallel for better performance
|
||||||
|
const [
|
||||||
|
circadianRhythm,
|
||||||
|
anomalies,
|
||||||
|
correlations,
|
||||||
|
growthAnalysis,
|
||||||
|
sleepTrends,
|
||||||
|
feedingTrends,
|
||||||
|
sleepClusters,
|
||||||
|
feedingClusters,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.advancedPatternService.analyzeCircadianRhythm(childId, 14),
|
||||||
|
this.advancedPatternService.detectAnomalies(childId, 30),
|
||||||
|
this.advancedPatternService.analyzeCorrelations(childId, 14),
|
||||||
|
this.growthPercentileService.analyzeGrowth(childId),
|
||||||
|
this.advancedPatternService.analyzeTrends(childId, ActivityType.SLEEP),
|
||||||
|
this.advancedPatternService.analyzeTrends(childId, ActivityType.FEEDING),
|
||||||
|
this.advancedPatternService.clusterActivities(childId, ActivityType.SLEEP, 30),
|
||||||
|
this.advancedPatternService.clusterActivities(childId, ActivityType.FEEDING, 30),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Combine all analytics into a comprehensive dashboard
|
||||||
|
const dashboard = {
|
||||||
|
circadianRhythm,
|
||||||
|
anomalies,
|
||||||
|
correlations,
|
||||||
|
growthAnalysis,
|
||||||
|
trends: {
|
||||||
|
sleep: sleepTrends,
|
||||||
|
feeding: feedingTrends,
|
||||||
|
},
|
||||||
|
clusters: {
|
||||||
|
sleep: sleepClusters,
|
||||||
|
feeding: feedingClusters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: dashboard,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comprehensive Analytics Dashboard Endpoint
|
* Comprehensive Analytics Dashboard Endpoint
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateFamilyDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(100)
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -14,12 +14,33 @@ import {
|
|||||||
import { FamiliesService } from './families.service';
|
import { FamiliesService } from './families.service';
|
||||||
import { InviteFamilyMemberDto } from './dto/invite-family-member.dto';
|
import { InviteFamilyMemberDto } from './dto/invite-family-member.dto';
|
||||||
import { JoinFamilyDto } from './dto/join-family.dto';
|
import { JoinFamilyDto } from './dto/join-family.dto';
|
||||||
|
import { CreateFamilyDto } from './dto/create-family.dto';
|
||||||
import { FamilyRole } from '../../database/entities/family-member.entity';
|
import { FamilyRole } from '../../database/entities/family-member.entity';
|
||||||
|
|
||||||
@Controller('api/v1/families')
|
@Controller('api/v1/families')
|
||||||
export class FamiliesController {
|
export class FamiliesController {
|
||||||
constructor(private readonly familiesService: FamiliesService) {}
|
constructor(private readonly familiesService: FamiliesService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new family
|
||||||
|
* POST /api/v1/families
|
||||||
|
* Body: { name: string }
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
async createFamily(@Req() req: any, @Body() createFamilyDto: CreateFamilyDto) {
|
||||||
|
const family = await this.familiesService.createFamily(
|
||||||
|
req.user.userId,
|
||||||
|
createFamilyDto,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
family,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Post('invite')
|
@Post('invite')
|
||||||
async inviteMember(
|
async inviteMember(
|
||||||
@Req() req: any,
|
@Req() req: any,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { User } from '../../database/entities/user.entity';
|
import { User } from '../../database/entities/user.entity';
|
||||||
import { InviteFamilyMemberDto } from './dto/invite-family-member.dto';
|
import { InviteFamilyMemberDto } from './dto/invite-family-member.dto';
|
||||||
import { JoinFamilyDto } from './dto/join-family.dto';
|
import { JoinFamilyDto } from './dto/join-family.dto';
|
||||||
|
import { CreateFamilyDto } from './dto/create-family.dto';
|
||||||
import { EmailService } from '../../common/services/email.service';
|
import { EmailService } from '../../common/services/email.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -29,6 +30,53 @@ export class FamiliesService {
|
|||||||
private emailService: EmailService,
|
private emailService: EmailService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new family
|
||||||
|
*/
|
||||||
|
async createFamily(
|
||||||
|
userId: string,
|
||||||
|
createFamilyDto: CreateFamilyDto,
|
||||||
|
): Promise<Family> {
|
||||||
|
// Check if user already has a family
|
||||||
|
const existingMembership = await this.familyMemberRepository.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMembership) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'You are already a member of a family. Leave your current family before creating a new one.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the family (ID and shareCode are auto-generated via @BeforeInsert)
|
||||||
|
const family = this.familyRepository.create({
|
||||||
|
name: createFamilyDto.name,
|
||||||
|
shareCodeExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
||||||
|
createdBy: userId,
|
||||||
|
subscriptionTier: 'free', // Default tier
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedFamily = await this.familyRepository.save(family);
|
||||||
|
|
||||||
|
// Add creator as a parent (admin) of the family
|
||||||
|
const creatorMembership = this.familyMemberRepository.create({
|
||||||
|
userId,
|
||||||
|
familyId: savedFamily.id,
|
||||||
|
role: FamilyRole.PARENT,
|
||||||
|
permissions: {
|
||||||
|
canAddChildren: true,
|
||||||
|
canEditChildren: true,
|
||||||
|
canLogActivities: true,
|
||||||
|
canViewReports: true,
|
||||||
|
canInviteMembers: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.familyMemberRepository.save(creatorMembership);
|
||||||
|
|
||||||
|
return savedFamily;
|
||||||
|
}
|
||||||
|
|
||||||
async inviteMember(
|
async inviteMember(
|
||||||
userId: string,
|
userId: string,
|
||||||
familyId: string,
|
familyId: string,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { useTheme } from '@mui/material/styles';
|
|||||||
import { StepIconProps } from '@mui/material/StepIcon';
|
import { StepIconProps } from '@mui/material/StepIcon';
|
||||||
import { FamilySetupStep, ChildData } from '@/components/onboarding/FamilySetupStep';
|
import { FamilySetupStep, ChildData } from '@/components/onboarding/FamilySetupStep';
|
||||||
import { familiesApi } from '@/lib/api/families';
|
import { familiesApi } from '@/lib/api/families';
|
||||||
|
import { extractError } from '@/lib/utils/errorHandler';
|
||||||
|
|
||||||
const steps = ['Welcome', 'Preferences', 'Family Setup', 'Complete'];
|
const steps = ['Welcome', 'Preferences', 'Family Setup', 'Complete'];
|
||||||
|
|
||||||
@@ -230,7 +231,8 @@ export default function OnboardingPage() {
|
|||||||
setActiveStep((prev) => prev + 1);
|
setActiveStep((prev) => prev + 1);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to create family:', err);
|
console.error('Failed to create family:', err);
|
||||||
setError(err.response?.data?.message || 'Failed to create family. Please try again.');
|
const errorData = extractError(err);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -246,7 +248,8 @@ export default function OnboardingPage() {
|
|||||||
setActiveStep((prev) => prev + 1);
|
setActiveStep((prev) => prev + 1);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to join family:', err);
|
console.error('Failed to join family:', err);
|
||||||
setError(err.response?.data?.message || 'Failed to join family. Please check the code and try again.');
|
const errorData = extractError(err);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { LockReset, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-ma
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import apiClient from '@/lib/api/client';
|
import apiClient from '@/lib/api/client';
|
||||||
|
import { extractError } from '@/lib/utils/errorHandler';
|
||||||
|
|
||||||
export default function ResetPasswordPage() {
|
export default function ResetPasswordPage() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -92,9 +93,8 @@ export default function ResetPasswordPage() {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Reset password error:', err);
|
console.error('Reset password error:', err);
|
||||||
setError(
|
const errorData = extractError(err);
|
||||||
err.response?.data?.message || 'Failed to reset password. The link may have expired.'
|
setError(errorData.userMessage || errorData.message);
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import { Loader2, RefreshCw, Activity, Brain, TrendingUp, Baby, Link } from 'lucide-react';
|
import { Loader2, RefreshCw, Activity, Brain, TrendingUp, Baby, Link } from 'lucide-react';
|
||||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||||
|
import { extractError } from '@/lib/utils/errorHandler';
|
||||||
|
|
||||||
export default function AdvancedAnalyticsPage() {
|
export default function AdvancedAnalyticsPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -105,9 +106,10 @@ export default function AdvancedAnalyticsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setError('');
|
setError('');
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AdvancedAnalytics] Failed to load children:', error);
|
console.error('[AdvancedAnalytics] Failed to load children:', error);
|
||||||
setError('Failed to load children');
|
const errorData = extractError(error);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -129,8 +131,9 @@ export default function AdvancedAnalyticsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await analyticsApi.getCircadianRhythm(selectedChildId, 14);
|
const data = await analyticsApi.getCircadianRhythm(selectedChildId, 14);
|
||||||
setCircadianData(data);
|
setCircadianData(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AdvancedAnalytics] Failed to load circadian rhythm:', error);
|
console.error('[AdvancedAnalytics] Failed to load circadian rhythm:', error);
|
||||||
|
console.error("Circadian error:", extractError(error).message);
|
||||||
setCircadianError(error as Error);
|
setCircadianError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
setCircadianLoading(false);
|
setCircadianLoading(false);
|
||||||
@@ -145,8 +148,9 @@ export default function AdvancedAnalyticsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await analyticsApi.getAnomalies(selectedChildId, 30);
|
const data = await analyticsApi.getAnomalies(selectedChildId, 30);
|
||||||
setAnomalyData(data);
|
setAnomalyData(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AdvancedAnalytics] Failed to load anomalies:', error);
|
console.error('[AdvancedAnalytics] Failed to load anomalies:', error);
|
||||||
|
console.error("Anomalies error:", extractError(error).message);
|
||||||
setAnomalyError(error as Error);
|
setAnomalyError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
setAnomalyLoading(false);
|
setAnomalyLoading(false);
|
||||||
@@ -161,8 +165,9 @@ export default function AdvancedAnalyticsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await analyticsApi.getGrowthAnalysis(selectedChildId);
|
const data = await analyticsApi.getGrowthAnalysis(selectedChildId);
|
||||||
setGrowthData(data);
|
setGrowthData(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AdvancedAnalytics] Failed to load growth analysis:', error);
|
console.error('[AdvancedAnalytics] Failed to load growth analysis:', error);
|
||||||
|
console.error("Growth error:", extractError(error).message);
|
||||||
setGrowthError(error as Error);
|
setGrowthError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
setGrowthLoading(false);
|
setGrowthLoading(false);
|
||||||
@@ -177,8 +182,9 @@ export default function AdvancedAnalyticsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await analyticsApi.getCorrelations(selectedChildId, 14);
|
const data = await analyticsApi.getCorrelations(selectedChildId, 14);
|
||||||
setCorrelationData(data);
|
setCorrelationData(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AdvancedAnalytics] Failed to load correlations:', error);
|
console.error('[AdvancedAnalytics] Failed to load correlations:', error);
|
||||||
|
console.error("Correlations error:", extractError(error).message);
|
||||||
setCorrelationError(error as Error);
|
setCorrelationError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
setCorrelationLoading(false);
|
setCorrelationLoading(false);
|
||||||
@@ -197,8 +203,9 @@ export default function AdvancedAnalyticsPage() {
|
|||||||
]);
|
]);
|
||||||
setSleepTrendData(sleepTrend);
|
setSleepTrendData(sleepTrend);
|
||||||
setFeedingTrendData(feedingTrend);
|
setFeedingTrendData(feedingTrend);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AdvancedAnalytics] Failed to load trends:', error);
|
console.error('[AdvancedAnalytics] Failed to load trends:', error);
|
||||||
|
console.error("Trends error:", extractError(error).message);
|
||||||
setTrendError(error as Error);
|
setTrendError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
setTrendLoading(false);
|
setTrendLoading(false);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
|||||||
import { childrenApi, Child } from '@/lib/api/children';
|
import { childrenApi, Child } from '@/lib/api/children';
|
||||||
import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics';
|
import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics';
|
||||||
import PredictionsCard from '@/components/features/analytics/PredictionsCard';
|
import PredictionsCard from '@/components/features/analytics/PredictionsCard';
|
||||||
|
import { extractError } from '@/lib/utils/errorHandler';
|
||||||
import GrowthSpurtAlert from '@/components/features/analytics/GrowthSpurtAlert';
|
import GrowthSpurtAlert from '@/components/features/analytics/GrowthSpurtAlert';
|
||||||
import WeeklyReportCard from '@/components/features/analytics/WeeklyReportCard';
|
import WeeklyReportCard from '@/components/features/analytics/WeeklyReportCard';
|
||||||
import MonthlyReportCard from '@/components/features/analytics/MonthlyReportCard';
|
import MonthlyReportCard from '@/components/features/analytics/MonthlyReportCard';
|
||||||
@@ -121,9 +122,10 @@ export default function AnalyticsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setError('');
|
setError('');
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('[AnalyticsPage] Failed to load children:', error);
|
console.error('[AnalyticsPage] Failed to load children:', error);
|
||||||
setError('Failed to load children');
|
const errorData = extractError(error);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -136,8 +138,10 @@ export default function AnalyticsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await analyticsApi.getInsights(selectedChildId, days);
|
const data = await analyticsApi.getInsights(selectedChildId, days);
|
||||||
setInsights(data);
|
setInsights(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Failed to load insights:', error);
|
console.error('Failed to load insights:', error);
|
||||||
|
const errorData = extractError(error);
|
||||||
|
console.error('Insights error:', errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setInsightsLoading(false);
|
setInsightsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -150,8 +154,10 @@ export default function AnalyticsPage() {
|
|||||||
try {
|
try {
|
||||||
const data = await analyticsApi.getPredictions(selectedChildId);
|
const data = await analyticsApi.getPredictions(selectedChildId);
|
||||||
setPredictions(data);
|
setPredictions(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Failed to load predictions:', error);
|
console.error('Failed to load predictions:', error);
|
||||||
|
const errorData = extractError(error);
|
||||||
|
console.error('Predictions error:', errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setPredictionsLoading(false);
|
setPredictionsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { motion } from 'framer-motion';
|
|||||||
import { useTranslation } from '@/hooks/useTranslation';
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
|
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
|
||||||
import { useSelectedFamily } from '@/hooks/useSelectedFamily';
|
import { useSelectedFamily } from '@/hooks/useSelectedFamily';
|
||||||
|
import { extractError } from '@/lib/utils/errorHandler';
|
||||||
|
|
||||||
export default function ChildrenPage() {
|
export default function ChildrenPage() {
|
||||||
const { t } = useTranslation('children');
|
const { t } = useTranslation('children');
|
||||||
@@ -66,7 +67,8 @@ export default function ChildrenPage() {
|
|||||||
setChildren(data);
|
setChildren(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch children:', err);
|
console.error('Failed to fetch children:', err);
|
||||||
setError(err.response?.data?.message || t('errors.loadFailed'));
|
const errorData = extractError(err);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -103,7 +105,8 @@ export default function ChildrenPage() {
|
|||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to save child:', err);
|
console.error('Failed to save child:', err);
|
||||||
throw new Error(err.response?.data?.message || t('errors.saveFailed'));
|
const errorData = extractError(err);
|
||||||
|
throw new Error(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(false);
|
setActionLoading(false);
|
||||||
}
|
}
|
||||||
@@ -120,7 +123,8 @@ export default function ChildrenPage() {
|
|||||||
setChildToDelete(null);
|
setChildToDelete(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to delete child:', err);
|
console.error('Failed to delete child:', err);
|
||||||
setError(err.response?.data?.message || t('errors.deleteFailed'));
|
const errorData = extractError(err);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(false);
|
setActionLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { MeasurementUnitSelector } from '@/components/settings/MeasurementUnitSe
|
|||||||
import { TimeZoneSelector } from '@/components/settings/TimeZoneSelector';
|
import { TimeZoneSelector } from '@/components/settings/TimeZoneSelector';
|
||||||
import { TimeFormatSelector } from '@/components/settings/TimeFormatSelector';
|
import { TimeFormatSelector } from '@/components/settings/TimeFormatSelector';
|
||||||
import { PhotoUpload } from '@/components/common/PhotoUpload';
|
import { PhotoUpload } from '@/components/common/PhotoUpload';
|
||||||
|
import { extractError } from '@/lib/utils/errorHandler';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
import { useThemeContext } from '@/contexts/ThemeContext';
|
import { useThemeContext } from '@/contexts/ThemeContext';
|
||||||
@@ -103,7 +104,8 @@ export default function SettingsPage() {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('❌ Failed to save settings:', err);
|
console.error('❌ Failed to save settings:', err);
|
||||||
console.error('Error response:', err.response);
|
console.error('Error response:', err.response);
|
||||||
setError(err.response?.data?.message || err.message || 'Failed to save settings. Please try again.');
|
const errorData = extractError(err);
|
||||||
|
setError(errorData.userMessage || errorData.message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,25 @@
|
|||||||
# This script compares the development and production databases and automatically
|
# This script compares the development and production databases and automatically
|
||||||
# synchronizes the production database to match the development schema.
|
# synchronizes the production database to match the development schema.
|
||||||
#
|
#
|
||||||
|
# DATABASE ENVIRONMENTS:
|
||||||
|
# ----------------------
|
||||||
|
# Development Environment (this server: /root/maternal-app):
|
||||||
|
# - Uses database: parentflowdev
|
||||||
|
# - Location: 10.0.0.207
|
||||||
|
# - This is where we develop and test changes
|
||||||
|
#
|
||||||
|
# Production Environment (server: 10.0.0.240 /root/parentflowapp-prod):
|
||||||
|
# - Uses database: parentflow
|
||||||
|
# - Location: 10.0.0.207 (same database server, different database)
|
||||||
|
# - This is the live application serving users
|
||||||
|
#
|
||||||
|
# Admin Database (shared between environments):
|
||||||
|
# - Uses database: parentflowadmin
|
||||||
|
# - Location: 10.0.0.207
|
||||||
|
# - Shared admin panel database for both dev and production
|
||||||
|
#
|
||||||
|
# This script syncs: parentflowdev (dev) → parentflow (production)
|
||||||
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - Creates missing tables in production
|
# - Creates missing tables in production
|
||||||
# - Adds missing columns to existing tables
|
# - Adds missing columns to existing tables
|
||||||
@@ -36,8 +55,9 @@ NC='\033[0m' # No Color
|
|||||||
DB_HOST="10.0.0.207"
|
DB_HOST="10.0.0.207"
|
||||||
DB_USER="postgres"
|
DB_USER="postgres"
|
||||||
DB_PASSWORD="a3ppq"
|
DB_PASSWORD="a3ppq"
|
||||||
DEV_DB="parentflowdev"
|
DEV_DB="parentflowdev" # Development database (used by /root/maternal-app)
|
||||||
PROD_DB="parentflow"
|
PROD_DB="parentflow" # Production database (used by 10.0.0.240:/root/parentflowapp-prod)
|
||||||
|
ADMIN_DB="parentflowadmin" # Shared admin database (both environments)
|
||||||
|
|
||||||
# Backup configuration
|
# Backup configuration
|
||||||
BACKUP_DIR="/root/maternal-app/backups/db-schema-sync"
|
BACKUP_DIR="/root/maternal-app/backups/db-schema-sync"
|
||||||
|
|||||||
Reference in New Issue
Block a user