diff --git a/docs/maternal-web-frontend-plan.md b/docs/maternal-web-frontend-plan.md
new file mode 100644
index 0000000..5df28a9
--- /dev/null
+++ b/docs/maternal-web-frontend-plan.md
@@ -0,0 +1,1733 @@
+# Web Frontend Implementation Plan - Maternal Organization App
+## Mobile-First Progressive Web Application
+
+---
+
+## Executive Summary
+
+This document outlines the implementation plan for building the Maternal Organization App as a **mobile-first responsive web application** that will serve as the foundation for the native mobile apps. The web app will be built using **Next.js 14** with **TypeScript**, implementing all core features while maintaining a native app-like experience through PWA capabilities.
+
+### Key Principles
+- **Mobile-first design** with responsive scaling for tablets and desktop
+- **PWA features** for offline support and app-like experience
+- **Component reusability** for future React Native migration
+- **Touch-optimized** interactions and gestures
+- **Performance-first** with sub-2-second load times
+
+---
+
+## Technology Stack
+
+### Core Framework
+```javascript
+// Package versions for consistency
+{
+ "next": "^14.2.0",
+ "react": "^18.3.0",
+ "react-dom": "^18.3.0",
+ "typescript": "^5.4.0"
+}
+```
+
+### UI Framework & Styling
+```javascript
+{
+ // Material UI for consistent Material Design
+ "@mui/material": "^5.15.0",
+ "@mui/icons-material": "^5.15.0",
+ "@emotion/react": "^11.11.0",
+ "@emotion/styled": "^11.11.0",
+
+ // Tailwind for utility classes
+ "tailwindcss": "^3.4.0",
+
+ // Animation libraries
+ "framer-motion": "^11.0.0",
+ "react-spring": "^9.7.0"
+}
+```
+
+### State Management & Data
+```javascript
+{
+ // Redux Toolkit for state management
+ "@reduxjs/toolkit": "^2.2.0",
+ "react-redux": "^9.1.0",
+ "redux-persist": "^6.0.0",
+
+ // API and real-time
+ "@tanstack/react-query": "^5.28.0",
+ "socket.io-client": "^4.7.0",
+ "axios": "^1.6.0",
+
+ // Forms and validation
+ "react-hook-form": "^7.51.0",
+ "zod": "^3.22.0"
+}
+```
+
+### PWA & Performance
+```javascript
+{
+ "workbox-webpack-plugin": "^7.0.0",
+ "next-pwa": "^5.6.0",
+ "@sentry/nextjs": "^7.100.0",
+ "web-vitals": "^3.5.0"
+}
+```
+
+---
+
+## Project Structure
+
+```
+maternal-web/
+├── src/
+│ ├── app/ # Next.js 14 app directory
+│ │ ├── (auth)/ # Auth group routes
+│ │ │ ├── login/
+│ │ │ ├── register/
+│ │ │ └── onboarding/
+│ │ ├── (dashboard)/ # Protected routes
+│ │ │ ├── page.tsx # Family dashboard
+│ │ │ ├── children/
+│ │ │ │ ├── [id]/
+│ │ │ │ └── new/
+│ │ │ ├── track/
+│ │ │ │ ├── feeding/
+│ │ │ │ ├── sleep/
+│ │ │ │ └── diaper/
+│ │ │ ├── ai-assistant/
+│ │ │ ├── insights/
+│ │ │ └── settings/
+│ │ ├── api/ # API routes
+│ │ │ └── auth/[...nextauth]/
+│ │ ├── layout.tsx
+│ │ └── global.css
+│ │
+│ ├── components/
+│ │ ├── ui/ # Base UI components
+│ │ │ ├── Button/
+│ │ │ ├── Card/
+│ │ │ ├── Input/
+│ │ │ ├── Modal/
+│ │ │ └── ...
+│ │ ├── features/ # Feature-specific components
+│ │ │ ├── tracking/
+│ │ │ ├── ai-chat/
+│ │ │ ├── family/
+│ │ │ └── analytics/
+│ │ ├── layouts/
+│ │ │ ├── MobileNav/
+│ │ │ ├── TabBar/
+│ │ │ └── AppShell/
+│ │ └── common/
+│ │ ├── ErrorBoundary/
+│ │ ├── LoadingStates/
+│ │ └── OfflineIndicator/
+│ │
+│ ├── hooks/
+│ │ ├── useVoiceInput.ts
+│ │ ├── useOfflineSync.ts
+│ │ ├── useRealtime.ts
+│ │ └── useMediaQuery.ts
+│ │
+│ ├── lib/
+│ │ ├── api/
+│ │ ├── websocket/
+│ │ ├── storage/
+│ │ └── utils/
+│ │
+│ ├── store/ # Redux store
+│ │ ├── slices/
+│ │ ├── middleware/
+│ │ └── store.ts
+│ │
+│ ├── styles/
+│ │ ├── themes/
+│ │ └── globals.css
+│ │
+│ └── types/
+│
+├── public/
+│ ├── manifest.json
+│ ├── service-worker.js
+│ └── icons/
+│
+└── tests/
+```
+
+---
+
+## Phase 1: Foundation & Setup (Week 1)
+
+### 1.1 Project Initialization
+
+```bash
+# Create Next.js project with TypeScript
+npx create-next-app@latest maternal-web --typescript --tailwind --app
+
+# Install core dependencies
+cd maternal-web
+npm install @mui/material @emotion/react @emotion/styled
+npm install @reduxjs/toolkit react-redux redux-persist
+npm install @tanstack/react-query axios socket.io-client
+npm install react-hook-form zod
+npm install framer-motion
+```
+
+### 1.2 PWA Configuration
+
+```javascript
+// next.config.js
+const withPWA = require('next-pwa')({
+ dest: 'public',
+ register: true,
+ skipWaiting: true,
+ disable: process.env.NODE_ENV === 'development',
+ runtimeCaching: [
+ {
+ urlPattern: /^https?.*/,
+ handler: 'NetworkFirst',
+ options: {
+ cacheName: 'offlineCache',
+ expiration: {
+ maxEntries: 200,
+ },
+ },
+ },
+ ],
+});
+
+module.exports = withPWA({
+ reactStrictMode: true,
+ images: {
+ domains: ['api.maternalapp.com'],
+ },
+});
+```
+
+### 1.3 Theme Configuration
+
+```typescript
+// src/styles/themes/maternalTheme.ts
+import { createTheme } from '@mui/material/styles';
+
+export const maternalTheme = createTheme({
+ palette: {
+ primary: {
+ main: '#FFB6C1', // Light pink/rose
+ light: '#FFE4E1', // Misty rose
+ dark: '#DB7093', // Pale violet red
+ },
+ secondary: {
+ main: '#FFDAB9', // Peach puff
+ light: '#FFE5CC',
+ dark: '#FFB347', // Deep peach
+ },
+ background: {
+ default: '#FFF9F5', // Warm white
+ paper: '#FFFFFF',
+ },
+ },
+ typography: {
+ fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
+ h1: {
+ fontSize: '2rem',
+ fontWeight: 600,
+ },
+ },
+ shape: {
+ borderRadius: 16,
+ },
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ borderRadius: 24,
+ textTransform: 'none',
+ minHeight: 48, // Touch target size
+ fontSize: '1rem',
+ fontWeight: 500,
+ },
+ },
+ },
+ MuiTextField: {
+ styleOverrides: {
+ root: {
+ '& .MuiInputBase-root': {
+ minHeight: 48,
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+### 1.4 Mobile-First Layout Component
+
+```typescript
+// src/components/layouts/AppShell/AppShell.tsx
+import { useState, useEffect } from 'react';
+import { Box, Container } from '@mui/material';
+import { MobileNav } from '../MobileNav';
+import { TabBar } from '../TabBar';
+import { useMediaQuery } from '@/hooks/useMediaQuery';
+
+export const AppShell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const isMobile = useMediaQuery('(max-width: 768px)');
+ const isTablet = useMediaQuery('(max-width: 1024px)');
+
+ return (
+
+ {!isMobile && }
+
+
+ {children}
+
+
+ {isMobile && }
+
+ );
+};
+```
+
+---
+
+## Phase 2: Authentication & Onboarding (Week 1-2)
+
+### 2.1 Auth Provider Setup
+
+```typescript
+// src/lib/auth/AuthContext.tsx
+import { createContext, useContext, useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { api } from '@/lib/api';
+
+interface AuthContextType {
+ user: User | null;
+ login: (credentials: LoginCredentials) => Promise;
+ register: (data: RegisterData) => Promise;
+ logout: () => void;
+ isLoading: boolean;
+}
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const router = useRouter();
+
+ useEffect(() => {
+ // Check for stored token and validate
+ const checkAuth = async () => {
+ const token = localStorage.getItem('accessToken');
+ if (token) {
+ try {
+ const response = await api.get('/auth/me');
+ setUser(response.data);
+ } catch {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ }
+ }
+ setIsLoading(false);
+ };
+
+ checkAuth();
+ }, []);
+
+ // Implementation continues...
+};
+```
+
+### 2.2 Mobile-Optimized Auth Screens
+
+```typescript
+// src/app/(auth)/login/page.tsx
+'use client';
+
+import { useState } from 'react';
+import {
+ Box,
+ TextField,
+ Button,
+ Typography,
+ Paper,
+ InputAdornment,
+ IconButton,
+ Divider
+} from '@mui/material';
+import { Visibility, VisibilityOff, Google, Apple } from '@mui/icons-material';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { motion } from 'framer-motion';
+import * as z from 'zod';
+
+const loginSchema = z.object({
+ email: z.string().email('Invalid email'),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+});
+
+export default function LoginPage() {
+ const [showPassword, setShowPassword] = useState(false);
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ resolver: zodResolver(loginSchema),
+ });
+
+ return (
+
+
+
+
+ Welcome Back
+
+
+
+
+
+
+ setShowPassword(!showPassword)}>
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+
+
+ OR
+
+ }
+ size="large"
+ sx={{ mb: 2 }}
+ >
+ Continue with Google
+
+
+ }
+ size="large"
+ >
+ Continue with Apple
+
+
+
+
+ );
+}
+```
+
+### 2.3 Progressive Onboarding Flow
+
+```typescript
+// src/app/(auth)/onboarding/page.tsx
+import { useState } from 'react';
+import { Stepper, Step, StepLabel } from '@mui/material';
+import { WelcomeStep } from '@/components/onboarding/WelcomeStep';
+import { AddChildStep } from '@/components/onboarding/AddChildStep';
+import { InviteFamilyStep } from '@/components/onboarding/InviteFamilyStep';
+import { NotificationStep } from '@/components/onboarding/NotificationStep';
+
+const steps = ['Welcome', 'Add Child', 'Invite Family', 'Notifications'];
+
+export default function OnboardingPage() {
+ const [activeStep, setActiveStep] = useState(0);
+
+ return (
+
+ {/* Mobile-optimized stepper */}
+
+ {steps.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+ {activeStep === 0 && }
+ {activeStep === 1 && }
+ {activeStep === 2 && }
+ {activeStep === 3 && }
+
+
+ );
+}
+```
+
+---
+
+## Phase 3: Core Tracking Features (Week 2-3)
+
+### 3.1 Quick Action FAB
+
+```typescript
+// src/components/features/tracking/QuickActionFAB.tsx
+import { useState } from 'react';
+import { SpeedDial, SpeedDialAction, SpeedDialIcon } from '@mui/material';
+import {
+ Restaurant,
+ Hotel,
+ BabyChangingStation,
+ Mic,
+ Add
+} from '@mui/icons-material';
+import { motion, AnimatePresence } from 'framer-motion';
+
+export const QuickActionFAB = () => {
+ const [open, setOpen] = useState(false);
+
+ const actions = [
+ { icon: , name: 'Feeding', color: '#FFB6C1', route: '/track/feeding' },
+ { icon: , name: 'Sleep', color: '#B6D7FF', route: '/track/sleep' },
+ { icon: , name: 'Diaper', color: '#FFE4B5', route: '/track/diaper' },
+ { icon: , name: 'Voice', color: '#E6E6FA', action: 'voice' },
+ ];
+
+ return (
+ } />}
+ onClose={() => setOpen(false)}
+ onOpen={() => setOpen(true)}
+ open={open}
+ >
+ {actions.map((action) => (
+ handleAction(action)}
+ sx={{
+ bgcolor: action.color,
+ '&:hover': {
+ bgcolor: action.color,
+ transform: 'scale(1.1)',
+ }
+ }}
+ />
+ ))}
+
+ );
+};
+```
+
+### 3.2 Voice Input Hook
+
+```typescript
+// src/hooks/useVoiceInput.ts
+import { useState, useEffect, useCallback } from 'react';
+
+interface VoiceInputOptions {
+ language?: string;
+ continuous?: boolean;
+ onResult?: (transcript: string) => void;
+ onEnd?: () => void;
+}
+
+export const useVoiceInput = (options: VoiceInputOptions = {}) => {
+ const [isListening, setIsListening] = useState(false);
+ const [transcript, setTranscript] = useState('');
+ const [error, setError] = useState(null);
+
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ const recognition = new SpeechRecognition();
+
+ useEffect(() => {
+ recognition.continuous = options.continuous ?? false;
+ recognition.interimResults = true;
+ recognition.lang = options.language ?? 'en-US';
+
+ recognition.onresult = (event) => {
+ const current = event.resultIndex;
+ const transcript = event.results[current][0].transcript;
+ setTranscript(transcript);
+
+ if (event.results[current].isFinal) {
+ options.onResult?.(transcript);
+ }
+ };
+
+ recognition.onerror = (event) => {
+ setError(event.error);
+ setIsListening(false);
+ };
+
+ recognition.onend = () => {
+ setIsListening(false);
+ options.onEnd?.();
+ };
+ }, []);
+
+ const startListening = useCallback(() => {
+ setTranscript('');
+ setError(null);
+ recognition.start();
+ setIsListening(true);
+ }, [recognition]);
+
+ const stopListening = useCallback(() => {
+ recognition.stop();
+ setIsListening(false);
+ }, [recognition]);
+
+ return {
+ isListening,
+ transcript,
+ error,
+ startListening,
+ stopListening,
+ };
+};
+```
+
+### 3.3 Tracking Form Component
+
+```typescript
+// src/components/features/tracking/FeedingTracker.tsx
+import { useState } from 'react';
+import {
+ Box,
+ Paper,
+ TextField,
+ ToggleButtonGroup,
+ ToggleButton,
+ Button,
+ Typography,
+ Chip,
+ IconButton
+} from '@mui/material';
+import { Timer, Mic, MicOff } from '@mui/icons-material';
+import { useVoiceInput } from '@/hooks/useVoiceInput';
+import { motion } from 'framer-motion';
+
+export const FeedingTracker = () => {
+ const [feedingType, setFeedingType] = useState<'breast' | 'bottle' | 'solid'>('breast');
+ const [duration, setDuration] = useState(0);
+ const [isTimerRunning, setIsTimerRunning] = useState(false);
+
+ const { isListening, transcript, startListening, stopListening } = useVoiceInput({
+ onResult: (text) => {
+ // Parse voice input for commands
+ parseVoiceCommand(text);
+ },
+ });
+
+ return (
+
+
+
+
+ Track Feeding
+
+
+
+ {isListening ? : }
+
+
+
+ value && setFeedingType(value)}
+ fullWidth
+ sx={{ mb: 3 }}
+ >
+
+ Breastfeeding
+
+
+ Bottle
+
+
+ Solid Food
+
+
+
+ {/* Timer component for breastfeeding */}
+ {feedingType === 'breast' && (
+
+
+ {formatDuration(duration)}
+
+ }
+ onClick={toggleTimer}
+ sx={{ mt: 2 }}
+ >
+ {isTimerRunning ? 'Stop Timer' : 'Start Timer'}
+
+
+ )}
+
+ {/* Form fields for bottle/solid */}
+ {feedingType !== 'breast' && (
+
+ {/* Additional form fields */}
+
+ )}
+
+
+
+
+ );
+};
+```
+
+---
+
+## Phase 4: AI Assistant Integration (Week 3-4)
+
+### 4.1 AI Chat Interface
+
+```typescript
+// src/components/features/ai-chat/AIChatInterface.tsx
+import { useState, useRef, useEffect } from 'react';
+import {
+ Box,
+ TextField,
+ IconButton,
+ Paper,
+ Typography,
+ Avatar,
+ Chip,
+ CircularProgress
+} from '@mui/material';
+import { Send, Mic, AttachFile } from '@mui/icons-material';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useAIChat } from '@/hooks/useAIChat';
+
+export const AIChatInterface = () => {
+ const [input, setInput] = useState('');
+ const [isTyping, setIsTyping] = useState(false);
+ const messagesEndRef = useRef(null);
+
+ const { messages, sendMessage, isLoading } = useAIChat();
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+ return (
+
+ {/* Quick action chips */}
+
+ {['Sleep tips', 'Feeding schedule', 'Developmental milestones', 'Emergency'].map((action) => (
+ handleQuickAction(action)}
+ sx={{
+ bgcolor: 'background.paper',
+ '&:hover': { bgcolor: 'primary.light' }
+ }}
+ />
+ ))}
+
+
+ {/* Messages */}
+
+
+ {messages.map((message, index) => (
+
+
+ {message.role === 'assistant' && (
+ AI
+ )}
+
+
+ {message.content}
+ {message.disclaimer && (
+
+ ⚠️ {message.disclaimer}
+
+ )}
+
+
+ {message.role === 'user' && (
+ U
+ )}
+
+
+ ))}
+
+
+ {isTyping && (
+
+ AI
+
+
+
+
+
+
+ )}
+
+
+
+
+ {/* Input area */}
+
+
+
+
+
+
+ setInput(e.target.value)}
+ placeholder="Ask about your child's development, sleep, feeding..."
+ variant="outlined"
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 3,
+ },
+ }}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+```
+
+### 4.2 AI Context Provider
+
+```typescript
+// src/lib/ai/AIContextProvider.tsx
+import { createContext, useContext, useState } from 'react';
+import { api } from '@/lib/api';
+
+interface AIContextType {
+ childContext: ChildContext[];
+ recentActivities: Activity[];
+ updateContext: (data: Partial) => void;
+ getRelevantContext: (query: string) => Promise;
+}
+
+export const AIContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [childContext, setChildContext] = useState([]);
+ const [recentActivities, setRecentActivities] = useState([]);
+
+ const getRelevantContext = async (query: string) => {
+ // Implement context prioritization based on query
+ const context = {
+ children: childContext,
+ recentActivities: recentActivities.slice(0, 10),
+ timestamp: new Date().toISOString(),
+ };
+
+ return context;
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+```
+
+---
+
+## Phase 5: Family Dashboard & Analytics (Week 4-5)
+
+### 5.1 Responsive Dashboard Grid
+
+```typescript
+// src/app/(dashboard)/page.tsx
+import { Grid, Box } from '@mui/material';
+import { ChildCard } from '@/components/features/family/ChildCard';
+import { ActivityTimeline } from '@/components/features/tracking/ActivityTimeline';
+import { SleepPrediction } from '@/components/features/analytics/SleepPrediction';
+import { QuickStats } from '@/components/features/analytics/QuickStats';
+import { useMediaQuery } from '@/hooks/useMediaQuery';
+
+export default function DashboardPage() {
+ const isMobile = useMediaQuery('(max-width: 768px)');
+ const { children, activities } = useDashboardData();
+
+ return (
+
+ {/* Child selector carousel for mobile */}
+ {isMobile && (
+
+ {children.map((child) => (
+
+ ))}
+
+ )}
+
+
+ {/* Desktop child cards */}
+ {!isMobile && children.map((child) => (
+
+
+
+ ))}
+
+ {/* Sleep prediction */}
+
+
+
+
+ {/* Quick stats */}
+
+
+
+
+ {/* Activity timeline */}
+
+
+
+
+
+ );
+}
+```
+
+### 5.2 Interactive Charts
+
+```typescript
+// src/components/features/analytics/SleepChart.tsx
+import { Line } from 'react-chartjs-2';
+import { Box, Paper, Typography, ToggleButtonGroup, ToggleButton } from '@mui/material';
+import { useState } from 'react';
+
+export const SleepChart = () => {
+ const [timeRange, setTimeRange] = useState<'week' | 'month'>('week');
+
+ const chartData = {
+ labels: generateLabels(timeRange),
+ datasets: [
+ {
+ label: 'Sleep Duration',
+ data: sleepData,
+ fill: true,
+ backgroundColor: 'rgba(182, 215, 255, 0.2)',
+ borderColor: '#B6D7FF',
+ tension: 0.4,
+ },
+ {
+ label: 'Predicted',
+ data: predictedData,
+ borderColor: '#FFB6C1',
+ borderDash: [5, 5],
+ fill: false,
+ },
+ ],
+ };
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ mode: 'index',
+ intersect: false,
+ },
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: {
+ display: false,
+ },
+ },
+ x: {
+ grid: {
+ display: false,
+ },
+ },
+ },
+ };
+
+ return (
+
+
+
+ Sleep Patterns
+
+
+ value && setTimeRange(value)}
+ size="small"
+ >
+ Week
+ Month
+
+
+
+
+
+
+
+ );
+};
+```
+
+---
+
+## Phase 6: Offline Support & PWA Features (Week 5)
+
+### 6.1 Service Worker Setup
+
+```javascript
+// public/service-worker.js
+import { precacheAndRoute } from 'workbox-precaching';
+import { registerRoute } from 'workbox-routing';
+import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
+import { Queue } from 'workbox-background-sync';
+
+// Precache all static assets
+precacheAndRoute(self.__WB_MANIFEST);
+
+// Create sync queue for offline requests
+const queue = new Queue('maternal-sync-queue', {
+ onSync: async ({ queue }) => {
+ let entry;
+ while ((entry = await queue.shiftRequest())) {
+ try {
+ await fetch(entry.request);
+ } catch (error) {
+ await queue.unshiftRequest(entry);
+ throw error;
+ }
+ }
+ },
+});
+
+// API routes - network first with offline queue
+registerRoute(
+ /^https:\/\/api\.maternalapp\.com\/api/,
+ async (args) => {
+ try {
+ const response = await new NetworkFirst({
+ cacheName: 'api-cache',
+ networkTimeoutSeconds: 5,
+ }).handle(args);
+ return response;
+ } catch (error) {
+ await queue.pushRequest({ request: args.request });
+ return new Response(
+ JSON.stringify({
+ offline: true,
+ message: 'Your request has been queued and will be synced when online'
+ }),
+ { headers: { 'Content-Type': 'application/json' } }
+ );
+ }
+ }
+);
+
+// Static assets - cache first
+registerRoute(
+ /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
+ new CacheFirst({
+ cacheName: 'image-cache',
+ plugins: [
+ new ExpirationPlugin({
+ maxEntries: 100,
+ maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
+ }),
+ ],
+ })
+);
+```
+
+### 6.2 Offline State Management
+
+```typescript
+// src/store/slices/offlineSlice.ts
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { REHYDRATE } from 'redux-persist';
+
+interface OfflineState {
+ isOnline: boolean;
+ pendingActions: PendingAction[];
+ lastSyncTime: string | null;
+ syncInProgress: boolean;
+}
+
+const offlineSlice = createSlice({
+ name: 'offline',
+ initialState: {
+ isOnline: true,
+ pendingActions: [],
+ lastSyncTime: null,
+ syncInProgress: false,
+ } as OfflineState,
+ reducers: {
+ setOnlineStatus: (state, action: PayloadAction) => {
+ state.isOnline = action.payload;
+ if (action.payload && state.pendingActions.length > 0) {
+ state.syncInProgress = true;
+ }
+ },
+ addPendingAction: (state, action: PayloadAction) => {
+ state.pendingActions.push({
+ ...action.payload,
+ id: generateId(),
+ timestamp: new Date().toISOString(),
+ });
+ },
+ removePendingAction: (state, action: PayloadAction) => {
+ state.pendingActions = state.pendingActions.filter(a => a.id !== action.payload);
+ },
+ setSyncInProgress: (state, action: PayloadAction) => {
+ state.syncInProgress = action.payload;
+ },
+ updateLastSyncTime: (state) => {
+ state.lastSyncTime = new Date().toISOString();
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(REHYDRATE, (state, action) => {
+ // Handle rehydration from localStorage
+ if (action.payload) {
+ return {
+ ...state,
+ ...action.payload.offline,
+ isOnline: navigator.onLine,
+ };
+ }
+ });
+ },
+});
+
+export const offlineActions = offlineSlice.actions;
+export default offlineSlice.reducer;
+```
+
+### 6.3 Offline Indicator Component
+
+```typescript
+// src/components/common/OfflineIndicator.tsx
+import { Alert, Snackbar, LinearProgress } from '@mui/material';
+import { useSelector } from 'react-redux';
+import { motion, AnimatePresence } from 'framer-motion';
+
+export const OfflineIndicator = () => {
+ const { isOnline, pendingActions, syncInProgress } = useSelector(state => state.offline);
+
+ return (
+
+ {!isOnline && (
+
+
+ You're offline. {pendingActions.length} actions will sync when you're back online.
+
+
+ )}
+
+ {syncInProgress && (
+
+ )}
+
+ );
+};
+```
+
+---
+
+## Phase 7: Performance Optimization (Week 6)
+
+### 7.1 Code Splitting & Lazy Loading
+
+```typescript
+// src/app/(dashboard)/layout.tsx
+import { lazy, Suspense } from 'react';
+import { LoadingFallback } from '@/components/common/LoadingFallback';
+
+// Lazy load heavy components
+const AIChatInterface = lazy(() => import('@/components/features/ai-chat/AIChatInterface'));
+const AnalyticsDashboard = lazy(() => import('@/components/features/analytics/AnalyticsDashboard'));
+
+export default function DashboardLayout({ children }) {
+ return (
+ }>
+ {children}
+
+ );
+}
+```
+
+### 7.2 Image Optimization
+
+```typescript
+// src/components/common/OptimizedImage.tsx
+import Image from 'next/image';
+import { useState } from 'react';
+import { Skeleton } from '@mui/material';
+
+export const OptimizedImage = ({ src, alt, ...props }) => {
+ const [isLoading, setIsLoading] = useState(true);
+
+ return (
+
+ {isLoading && (
+
+ )}
+ setIsLoading(false)}
+ placeholder="blur"
+ blurDataURL={generateBlurDataURL()}
+ />
+
+ );
+};
+```
+
+### 7.3 Performance Monitoring
+
+```typescript
+// src/lib/performance/monitoring.ts
+import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
+
+export const initPerformanceMonitoring = () => {
+ // Send metrics to analytics
+ const sendToAnalytics = (metric: any) => {
+ // Send to your analytics service
+ if (window.gtag) {
+ window.gtag('event', metric.name, {
+ value: Math.round(metric.value),
+ metric_id: metric.id,
+ metric_value: metric.value,
+ metric_delta: metric.delta,
+ });
+ }
+ };
+
+ getCLS(sendToAnalytics);
+ getFID(sendToAnalytics);
+ getFCP(sendToAnalytics);
+ getLCP(sendToAnalytics);
+ getTTFB(sendToAnalytics);
+};
+```
+
+---
+
+## Phase 8: Testing & Deployment (Week 6-7)
+
+### 8.1 Testing Setup
+
+```typescript
+// jest.config.js
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'jsdom',
+ setupFilesAfterEnv: ['/jest.setup.ts'],
+ moduleNameMapper: {
+ '^@/(.*)$': '/src/$1',
+ '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
+ },
+ collectCoverageFrom: [
+ 'src/**/*.{ts,tsx}',
+ '!src/**/*.d.ts',
+ '!src/**/*.stories.tsx',
+ ],
+ coverageThreshold: {
+ global: {
+ branches: 80,
+ functions: 80,
+ lines: 80,
+ statements: 80,
+ },
+ },
+};
+```
+
+### 8.2 E2E Testing with Playwright
+
+```typescript
+// tests/e2e/tracking.spec.ts
+import { test, expect } from '@playwright/test';
+
+test.describe('Activity Tracking Flow', () => {
+ test('should track feeding activity', async ({ page }) => {
+ // Login
+ await page.goto('/login');
+ await page.fill('[name="email"]', 'test@example.com');
+ await page.fill('[name="password"]', 'password123');
+ await page.click('button[type="submit"]');
+
+ // Navigate to tracking
+ await page.waitForSelector('[data-testid="quick-action-fab"]');
+ await page.click('[data-testid="quick-action-fab"]');
+ await page.click('[data-testid="action-feeding"]');
+
+ // Fill form
+ await page.click('[value="bottle"]');
+ await page.fill('[name="amount"]', '120');
+ await page.click('[data-testid="save-feeding"]');
+
+ // Verify
+ await expect(page.locator('[data-testid="success-toast"]')).toBeVisible();
+ });
+});
+```
+
+### 8.3 Deployment Configuration
+
+```yaml
+# .github/workflows/deploy.yml
+name: Deploy to Production
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: '18'
+ - run: npm ci
+ - run: npm test
+ - run: npm run test:e2e
+
+ build-and-deploy:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ - run: npm ci
+ - run: npm run build
+
+ # Deploy to Vercel
+ - uses: amondnet/vercel-action@v20
+ with:
+ vercel-token: ${{ secrets.VERCEL_TOKEN }}
+ vercel-org-id: ${{ secrets.ORG_ID }}
+ vercel-project-id: ${{ secrets.PROJECT_ID }}
+ working-directory: ./
+```
+
+---
+
+## Migration Path to Mobile
+
+### Component Compatibility Strategy
+
+```typescript
+// Shared component interface example
+// src/components/shared/Button.tsx
+
+interface ButtonProps {
+ variant?: 'contained' | 'outlined' | 'text';
+ color?: 'primary' | 'secondary';
+ size?: 'small' | 'medium' | 'large';
+ onPress?: () => void; // Mobile
+ onClick?: () => void; // Web
+ children: React.ReactNode;
+}
+
+// Web implementation
+export const Button: React.FC = ({
+ onClick,
+ onPress,
+ children,
+ ...props
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// Future React Native implementation
+// export const Button: React.FC = ({
+// onClick,
+// onPress,
+// children,
+// ...props
+// }) => {
+// return (
+//
+// {children}
+//
+// );
+// };
+```
+
+### Shared Business Logic
+
+```typescript
+// src/lib/shared/tracking.logic.ts
+// This file can be shared between web and mobile
+
+export const calculateFeedingDuration = (start: Date, end: Date): number => {
+ return Math.round((end.getTime() - start.getTime()) / 60000);
+};
+
+export const validateFeedingData = (data: FeedingData): ValidationResult => {
+ const errors = [];
+
+ if (!data.type) {
+ errors.push('Feeding type is required');
+ }
+
+ if (data.type === 'bottle' && !data.amount) {
+ errors.push('Amount is required for bottle feeding');
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ };
+};
+
+export const formatActivityForDisplay = (activity: Activity): DisplayActivity => {
+ // Shared formatting logic
+ return {
+ ...activity,
+ displayTime: formatRelativeTime(activity.timestamp),
+ icon: getActivityIcon(activity.type),
+ color: getActivityColor(activity.type),
+ };
+};
+```
+
+---
+
+## Performance Targets
+
+### Web Vitals Goals
+- **LCP** (Largest Contentful Paint): < 2.5s
+- **FID** (First Input Delay): < 100ms
+- **CLS** (Cumulative Layout Shift): < 0.1
+- **TTI** (Time to Interactive): < 3.5s
+- **Bundle Size**: < 200KB initial JS
+
+### Mobile Experience Metrics
+- Touch target size: minimum 44x44px
+- Font size: minimum 16px for body text
+- Scroll performance: 60fps
+- Offline capability: Full CRUD operations
+- Data usage: < 1MB per session average
+
+---
+
+## Security Considerations
+
+### Authentication & Authorization
+```typescript
+// src/lib/auth/security.ts
+export const securityHeaders = {
+ 'Content-Security-Policy': `
+ default-src 'self';
+ script-src 'self' 'unsafe-eval' 'unsafe-inline';
+ style-src 'self' 'unsafe-inline';
+ img-src 'self' data: https:;
+ connect-src 'self' https://api.maternalapp.com wss://api.maternalapp.com;
+ `,
+ 'X-Frame-Options': 'DENY',
+ 'X-Content-Type-Options': 'nosniff',
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
+ 'Permissions-Policy': 'camera=(), microphone=(self), geolocation=()',
+};
+```
+
+### Data Protection
+- Implement field-level encryption for sensitive data
+- Use secure HttpOnly cookies for auth tokens
+- Sanitize all user inputs
+- Implement rate limiting on all API calls
+- Regular security audits with OWASP guidelines
+
+---
+
+## Monitoring & Analytics
+
+### Analytics Implementation
+```typescript
+// src/lib/analytics/index.ts
+export const trackEvent = (eventName: string, properties?: any) => {
+ // PostHog
+ if (window.posthog) {
+ window.posthog.capture(eventName, properties);
+ }
+
+ // Google Analytics
+ if (window.gtag) {
+ window.gtag('event', eventName, properties);
+ }
+};
+
+// Usage
+trackEvent('feeding_tracked', {
+ type: 'bottle',
+ amount: 120,
+ duration: 15,
+});
+```
+
+### Error Tracking
+```typescript
+// src/lib/monitoring/sentry.ts
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+ environment: process.env.NODE_ENV,
+ tracesSampleRate: 0.1,
+ beforeSend(event, hint) {
+ // Filter sensitive data
+ if (event.request) {
+ delete event.request.cookies;
+ }
+ return event;
+ },
+});
+```
+
+---
+
+## Launch Checklist
+
+### Pre-Launch
+- [ ] Cross-browser testing (Chrome, Safari, Firefox, Edge)
+- [ ] Mobile device testing (iOS Safari, Chrome Android)
+- [ ] Accessibility audit (WCAG AA compliance)
+- [ ] Performance audit (Lighthouse score > 90)
+- [ ] Security audit
+- [ ] SEO optimization
+- [ ] Analytics implementation verified
+- [ ] Error tracking configured
+- [ ] SSL certificate installed
+- [ ] CDN configured
+- [ ] Backup system tested
+
+### Post-Launch
+- [ ] Monitor error rates
+- [ ] Track Core Web Vitals
+- [ ] Gather user feedback
+- [ ] A/B testing setup
+- [ ] Performance monitoring dashboard
+- [ ] User behavior analytics
+- [ ] Conversion funnel tracking
+- [ ] Support system ready
+
+---
+
+## Conclusion
+
+This web-first approach provides a solid foundation for the Maternal Organization App while maintaining the flexibility to expand to native mobile platforms. The progressive web app capabilities ensure users get a native-like experience on mobile devices, while the component architecture and shared business logic will facilitate the eventual migration to React Native.
+
+Key advantages of this approach:
+1. **Faster time to market** - Single codebase to maintain initially
+2. **Broader reach** - Works on any device with a modern browser
+3. **Easy updates** - No app store approval process for web updates
+4. **Cost-effective** - Reduced development and maintenance costs
+5. **SEO benefits** - Web app can be indexed by search engines
+6. **Progressive enhancement** - Can add native features gradually
+
+The architecture is designed to support the future mobile app development by keeping business logic separate and using compatible patterns wherever possible.
\ No newline at end of file
diff --git a/maternal-app-backend/src/config/database.config.ts b/maternal-app-backend/src/config/database.config.ts
deleted file mode 100644
index 38f0f13..0000000
--- a/maternal-app-backend/src/config/database.config.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { TypeOrmModuleOptions } from '@nestjs/typeorm';
-import { ConfigService } from '@nestjs/config';
-
-export const getDatabaseConfig = (
- configService: ConfigService,
-): TypeOrmModuleOptions => ({
- type: 'postgres',
- host: configService.get('DATABASE_HOST', 'localhost'),
- port: configService.get('DATABASE_PORT', 5555),
- username: configService.get('DATABASE_USER', 'maternal_user'),
- password: configService.get('DATABASE_PASSWORD'),
- database: configService.get('DATABASE_NAME', 'maternal_app'),
- entities: [__dirname + '/../**/*.entity{.ts,.js}'],
- migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
- synchronize: false, // Always use migrations in production
- logging: configService.get('NODE_ENV') === 'development',
- ssl: configService.get('NODE_ENV') === 'production',
-});
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/database.module.ts b/maternal-app-backend/src/database/database.module.ts
deleted file mode 100644
index c3f890c..0000000
--- a/maternal-app-backend/src/database/database.module.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Module } from '@nestjs/common';
-import { TypeOrmModule } from '@nestjs/typeorm';
-import { ConfigModule, ConfigService } from '@nestjs/config';
-import { getDatabaseConfig } from '../config/database.config';
-
-@Module({
- imports: [
- TypeOrmModule.forRootAsync({
- imports: [ConfigModule],
- useFactory: getDatabaseConfig,
- inject: [ConfigService],
- }),
- ],
-})
-export class DatabaseModule {}
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/entities/child.entity.ts b/maternal-app-backend/src/database/entities/child.entity.ts
deleted file mode 100644
index 643f80d..0000000
--- a/maternal-app-backend/src/database/entities/child.entity.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import {
- Entity,
- Column,
- PrimaryColumn,
- ManyToOne,
- JoinColumn,
- CreateDateColumn,
- BeforeInsert,
-} from 'typeorm';
-import { Family } from './family.entity';
-
-@Entity('children')
-export class Child {
- @PrimaryColumn({ length: 20 })
- id: string;
-
- @Column({ name: 'family_id', length: 20 })
- familyId: string;
-
- @Column({ length: 100 })
- name: string;
-
- @Column({ name: 'birth_date', type: 'date' })
- birthDate: Date;
-
- @Column({ length: 20, nullable: true })
- gender?: string;
-
- @Column({ name: 'photo_url', type: 'text', nullable: true })
- photoUrl?: string;
-
- @Column({ name: 'medical_info', type: 'jsonb', default: {} })
- medicalInfo: Record;
-
- @CreateDateColumn({ name: 'created_at' })
- createdAt: Date;
-
- @Column({ name: 'deleted_at', type: 'timestamp', nullable: true })
- deletedAt?: Date;
-
- @ManyToOne(() => Family, (family) => family.children, { onDelete: 'CASCADE' })
- @JoinColumn({ name: 'family_id' })
- family: Family;
-
- @BeforeInsert()
- generateId() {
- if (!this.id) {
- this.id = `chd_${this.generateNanoId()}`;
- }
- }
-
- private generateNanoId(): string {
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
- let result = '';
- for (let i = 0; i < 12; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
- }
-}
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/entities/device-registry.entity.ts b/maternal-app-backend/src/database/entities/device-registry.entity.ts
deleted file mode 100644
index 5a68c91..0000000
--- a/maternal-app-backend/src/database/entities/device-registry.entity.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import {
- Entity,
- Column,
- PrimaryColumn,
- ManyToOne,
- JoinColumn,
- CreateDateColumn,
- BeforeInsert,
- Index,
-} from 'typeorm';
-import { User } from './user.entity';
-
-@Entity('device_registry')
-@Index(['userId', 'deviceFingerprint'], { unique: true })
-export class DeviceRegistry {
- @PrimaryColumn({ length: 20 })
- id: string;
-
- @Column({ name: 'user_id', length: 20 })
- userId: string;
-
- @Column({ name: 'device_fingerprint', length: 255 })
- deviceFingerprint: string;
-
- @Column({ length: 20 })
- platform: string;
-
- @Column({ default: false })
- trusted: boolean;
-
- @Column({ name: 'last_seen', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
- lastSeen: Date;
-
- @ManyToOne(() => User, (user) => user.devices, { onDelete: 'CASCADE' })
- @JoinColumn({ name: 'user_id' })
- user: User;
-
- @BeforeInsert()
- generateId() {
- if (!this.id) {
- this.id = `dev_${this.generateNanoId()}`;
- }
- }
-
- private generateNanoId(): string {
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
- let result = '';
- for (let i = 0; i < 12; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
- }
-}
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/entities/family-member.entity.ts b/maternal-app-backend/src/database/entities/family-member.entity.ts
deleted file mode 100644
index eb4db3a..0000000
--- a/maternal-app-backend/src/database/entities/family-member.entity.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import {
- Entity,
- Column,
- PrimaryColumn,
- ManyToOne,
- JoinColumn,
- CreateDateColumn,
-} from 'typeorm';
-import { User } from './user.entity';
-import { Family } from './family.entity';
-
-export enum FamilyRole {
- PARENT = 'parent',
- CAREGIVER = 'caregiver',
- VIEWER = 'viewer',
-}
-
-export interface FamilyPermissions {
- canAddChildren: boolean;
- canEditChildren: boolean;
- canLogActivities: boolean;
- canViewReports: boolean;
-}
-
-@Entity('family_members')
-export class FamilyMember {
- @PrimaryColumn({ name: 'user_id', length: 20 })
- userId: string;
-
- @PrimaryColumn({ name: 'family_id', length: 20 })
- familyId: string;
-
- @Column({
- type: 'varchar',
- length: 20,
- enum: FamilyRole,
- })
- role: FamilyRole;
-
- @Column({
- type: 'jsonb',
- default: {
- canAddChildren: false,
- canEditChildren: false,
- canLogActivities: true,
- canViewReports: true,
- },
- })
- permissions: FamilyPermissions;
-
- @CreateDateColumn({ name: 'joined_at' })
- joinedAt: Date;
-
- @ManyToOne(() => User, (user) => user.familyMemberships, { onDelete: 'CASCADE' })
- @JoinColumn({ name: 'user_id' })
- user: User;
-
- @ManyToOne(() => Family, (family) => family.members, { onDelete: 'CASCADE' })
- @JoinColumn({ name: 'family_id' })
- family: Family;
-}
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/entities/family.entity.ts b/maternal-app-backend/src/database/entities/family.entity.ts
deleted file mode 100644
index 4eacfb5..0000000
--- a/maternal-app-backend/src/database/entities/family.entity.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import {
- Entity,
- Column,
- PrimaryColumn,
- ManyToOne,
- OneToMany,
- JoinColumn,
- CreateDateColumn,
- BeforeInsert,
-} from 'typeorm';
-import { User } from './user.entity';
-import { FamilyMember } from './family-member.entity';
-import { Child } from './child.entity';
-
-@Entity('families')
-export class Family {
- @PrimaryColumn({ length: 20 })
- id: string;
-
- @Column({ length: 100, nullable: true })
- name?: string;
-
- @Column({ name: 'share_code', length: 10, unique: true })
- shareCode: string;
-
- @Column({ name: 'created_by', length: 20 })
- createdBy: string;
-
- @Column({ name: 'subscription_tier', length: 20, default: 'free' })
- subscriptionTier: string;
-
- @CreateDateColumn({ name: 'created_at' })
- createdAt: Date;
-
- @ManyToOne(() => User)
- @JoinColumn({ name: 'created_by' })
- creator: User;
-
- @OneToMany(() => FamilyMember, (member) => member.family)
- members: FamilyMember[];
-
- @OneToMany(() => Child, (child) => child.family)
- children: Child[];
-
- @BeforeInsert()
- generateId() {
- if (!this.id) {
- this.id = `fam_${this.generateNanoId()}`;
- }
- if (!this.shareCode) {
- this.shareCode = this.generateShareCode();
- }
- }
-
- private generateNanoId(): string {
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
- let result = '';
- for (let i = 0; i < 12; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
- }
-
- private generateShareCode(): string {
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
- let result = '';
- for (let i = 0; i < 6; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
- }
-}
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/entities/index.ts b/maternal-app-backend/src/database/entities/index.ts
deleted file mode 100644
index bd198a9..0000000
--- a/maternal-app-backend/src/database/entities/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export { User } from './user.entity';
-export { DeviceRegistry } from './device-registry.entity';
-export { Family } from './family.entity';
-export { FamilyMember, FamilyRole, FamilyPermissions } from './family-member.entity';
-export { Child } from './child.entity';
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/entities/user.entity.ts b/maternal-app-backend/src/database/entities/user.entity.ts
deleted file mode 100644
index 27638ca..0000000
--- a/maternal-app-backend/src/database/entities/user.entity.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- Entity,
- Column,
- PrimaryColumn,
- CreateDateColumn,
- UpdateDateColumn,
- OneToMany,
- BeforeInsert,
-} from 'typeorm';
-import { DeviceRegistry } from './device-registry.entity';
-import { FamilyMember } from './family-member.entity';
-
-@Entity('users')
-export class User {
- @PrimaryColumn({ length: 20 })
- id: string;
-
- @Column({ length: 255, unique: true })
- email: string;
-
- @Column({ length: 20, nullable: true })
- phone?: string;
-
- @Column({ name: 'password_hash', length: 255 })
- passwordHash: string;
-
- @Column({ length: 100 })
- name: string;
-
- @Column({ length: 10, default: 'en-US' })
- locale: string;
-
- @Column({ length: 50, default: 'UTC' })
- timezone: string;
-
- @Column({ name: 'email_verified', default: false })
- emailVerified: boolean;
-
- @CreateDateColumn({ name: 'created_at' })
- createdAt: Date;
-
- @UpdateDateColumn({ name: 'updated_at' })
- updatedAt: Date;
-
- @OneToMany(() => DeviceRegistry, (device) => device.user)
- devices: DeviceRegistry[];
-
- @OneToMany(() => FamilyMember, (familyMember) => familyMember.user)
- familyMemberships: FamilyMember[];
-
- @BeforeInsert()
- generateId() {
- if (!this.id) {
- this.id = `usr_${this.generateNanoId()}`;
- }
- }
-
- private generateNanoId(): string {
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
- let result = '';
- for (let i = 0; i < 12; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
- }
-}
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/migrations/V001_create_core_auth.sql b/maternal-app-backend/src/database/migrations/V001_create_core_auth.sql
deleted file mode 100644
index 26868a4..0000000
--- a/maternal-app-backend/src/database/migrations/V001_create_core_auth.sql
+++ /dev/null
@@ -1,47 +0,0 @@
--- V001_20240110120000_create_core_auth.sql
--- Migration V001: Core Authentication Tables
-
--- Create extension for generating random IDs
-CREATE EXTENSION IF NOT EXISTS pgcrypto;
-
--- Users table
-CREATE TABLE IF NOT EXISTS users (
- id VARCHAR(20) PRIMARY KEY,
- email VARCHAR(255) UNIQUE NOT NULL,
- phone VARCHAR(20),
- password_hash VARCHAR(255) NOT NULL,
- name VARCHAR(100) NOT NULL,
- locale VARCHAR(10) DEFAULT 'en-US',
- timezone VARCHAR(50) DEFAULT 'UTC',
- email_verified BOOLEAN DEFAULT FALSE,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- Device registry table
-CREATE TABLE IF NOT EXISTS device_registry (
- id VARCHAR(20) PRIMARY KEY,
- user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- device_fingerprint VARCHAR(255) NOT NULL,
- platform VARCHAR(20) NOT NULL,
- trusted BOOLEAN DEFAULT FALSE,
- last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- UNIQUE(user_id, device_fingerprint)
-);
-
--- Indexes
-CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-CREATE INDEX IF NOT EXISTS idx_devices_user ON device_registry(user_id);
-
--- Update timestamp trigger function
-CREATE OR REPLACE FUNCTION update_updated_at_column()
-RETURNS TRIGGER AS $$
-BEGIN
- NEW.updated_at = CURRENT_TIMESTAMP;
- RETURN NEW;
-END;
-$$ language 'plpgsql';
-
--- Apply trigger to users table
-CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
- FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/migrations/V002_create_family_structure.sql b/maternal-app-backend/src/database/migrations/V002_create_family_structure.sql
deleted file mode 100644
index 43d0ac1..0000000
--- a/maternal-app-backend/src/database/migrations/V002_create_family_structure.sql
+++ /dev/null
@@ -1,41 +0,0 @@
--- V002_20240110130000_create_family_structure.sql
--- Migration V002: Family Structure
-
--- Families table
-CREATE TABLE IF NOT EXISTS families (
- id VARCHAR(20) PRIMARY KEY,
- name VARCHAR(100),
- share_code VARCHAR(10) UNIQUE,
- created_by VARCHAR(20) REFERENCES users(id),
- subscription_tier VARCHAR(20) DEFAULT 'free',
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- Family members table (junction table with additional data)
-CREATE TABLE IF NOT EXISTS family_members (
- user_id VARCHAR(20) REFERENCES users(id) ON DELETE CASCADE,
- family_id VARCHAR(20) REFERENCES families(id) ON DELETE CASCADE,
- role VARCHAR(20) NOT NULL CHECK (role IN ('parent', 'caregiver', 'viewer')),
- permissions JSONB DEFAULT '{"canAddChildren": false, "canEditChildren": false, "canLogActivities": true, "canViewReports": true}'::jsonb,
- joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (user_id, family_id)
-);
-
--- Children table
-CREATE TABLE IF NOT EXISTS children (
- id VARCHAR(20) PRIMARY KEY,
- family_id VARCHAR(20) NOT NULL REFERENCES families(id) ON DELETE CASCADE,
- name VARCHAR(100) NOT NULL,
- birth_date DATE NOT NULL,
- gender VARCHAR(20),
- photo_url TEXT,
- medical_info JSONB DEFAULT '{}'::jsonb,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- deleted_at TIMESTAMP
-);
-
--- Indexes
-CREATE INDEX IF NOT EXISTS idx_families_share_code ON families(share_code);
-CREATE INDEX IF NOT EXISTS idx_family_members_family ON family_members(family_id);
-CREATE INDEX IF NOT EXISTS idx_children_family ON children(family_id);
-CREATE INDEX IF NOT EXISTS idx_children_active ON children(deleted_at) WHERE deleted_at IS NULL;
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/migrations/V003_create_refresh_tokens.sql b/maternal-app-backend/src/database/migrations/V003_create_refresh_tokens.sql
deleted file mode 100644
index d08a76e..0000000
--- a/maternal-app-backend/src/database/migrations/V003_create_refresh_tokens.sql
+++ /dev/null
@@ -1,20 +0,0 @@
--- V003_20240110140000_create_refresh_tokens.sql
--- Migration V003: Refresh Tokens Table
-
--- Refresh tokens table for JWT authentication
-CREATE TABLE IF NOT EXISTS refresh_tokens (
- id VARCHAR(20) PRIMARY KEY,
- user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- device_id VARCHAR(20) REFERENCES device_registry(id) ON DELETE CASCADE,
- token_hash VARCHAR(255) NOT NULL,
- expires_at TIMESTAMP NOT NULL,
- revoked BOOLEAN DEFAULT FALSE,
- revoked_at TIMESTAMP,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- Indexes for refresh token lookups
-CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
-CREATE INDEX IF NOT EXISTS idx_refresh_tokens_device ON refresh_tokens(device_id);
-CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash);
-CREATE INDEX IF NOT EXISTS idx_refresh_tokens_active ON refresh_tokens(expires_at, revoked) WHERE revoked = FALSE;
\ No newline at end of file
diff --git a/maternal-app-backend/src/database/migrations/run-migrations.ts b/maternal-app-backend/src/database/migrations/run-migrations.ts
deleted file mode 100644
index 78dac64..0000000
--- a/maternal-app-backend/src/database/migrations/run-migrations.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Client } from 'pg';
-import * as fs from 'fs';
-import * as path from 'path';
-import * as dotenv from 'dotenv';
-
-// Load environment variables
-dotenv.config();
-
-const client = new Client({
- host: process.env.DATABASE_HOST || 'localhost',
- port: parseInt(process.env.DATABASE_PORT || '5555'),
- user: process.env.DATABASE_USER || 'maternal_user',
- password: process.env.DATABASE_PASSWORD,
- database: process.env.DATABASE_NAME || 'maternal_app',
-});
-
-const MIGRATIONS_DIR = __dirname;
-
-async function runMigrations() {
- try {
- await client.connect();
- console.log('Connected to database');
-
- // Create migrations tracking table
- await client.query(`
- CREATE TABLE IF NOT EXISTS schema_migrations (
- version VARCHAR(50) PRIMARY KEY,
- executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- );
- `);
-
- // Get list of migration files
- const files = fs
- .readdirSync(MIGRATIONS_DIR)
- .filter((file) => file.startsWith('V') && file.endsWith('.sql'))
- .sort();
-
- console.log(`Found ${files.length} migration files`);
-
- for (const file of files) {
- const version = file.split('_')[0]; // Extract V001, V002, etc.
-
- // Check if migration already executed
- const result = await client.query(
- 'SELECT version FROM schema_migrations WHERE version = $1',
- [version],
- );
-
- if (result.rows.length > 0) {
- console.log(`✓ Migration ${version} already executed`);
- continue;
- }
-
- // Read and execute migration
- const migrationPath = path.join(MIGRATIONS_DIR, file);
- const sql = fs.readFileSync(migrationPath, 'utf-8');
-
- console.log(`Running migration ${version}...`);
- await client.query(sql);
-
- // Record migration
- await client.query(
- 'INSERT INTO schema_migrations (version) VALUES ($1)',
- [version],
- );
-
- console.log(`✓ Migration ${version} completed`);
- }
-
- console.log('All migrations completed successfully');
- } catch (error) {
- console.error('Migration error:', error);
- process.exit(1);
- } finally {
- await client.end();
- }
-}
-
-runMigrations();
\ No newline at end of file