From 1de21044d6145183555ec790efe0cc5833906a4d Mon Sep 17 00:00:00 2001 From: andupetcu <47487320+andupetcu@users.noreply.github.com> Date: Tue, 30 Sep 2025 20:48:57 +0300 Subject: [PATCH] Remove unused stub backend folder and add web frontend plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleanup project structure by removing duplicate maternal-app-backend/ folder at root level. The working backend is located at maternal-app/maternal-app-backend/. Also added web frontend implementation plan documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/maternal-web-frontend-plan.md | 1733 +++++++++++++++++ .../src/config/database.config.ts | 18 - .../src/database/database.module.ts | 15 - .../src/database/entities/child.entity.ts | 60 - .../entities/device-registry.entity.ts | 53 - .../database/entities/family-member.entity.ts | 61 - .../src/database/entities/family.entity.ts | 72 - .../src/database/entities/index.ts | 5 - .../src/database/entities/user.entity.ts | 66 - .../migrations/V001_create_core_auth.sql | 47 - .../V002_create_family_structure.sql | 41 - .../migrations/V003_create_refresh_tokens.sql | 20 - .../src/database/migrations/run-migrations.ts | 79 - 13 files changed, 1733 insertions(+), 537 deletions(-) create mode 100644 docs/maternal-web-frontend-plan.md delete mode 100644 maternal-app-backend/src/config/database.config.ts delete mode 100644 maternal-app-backend/src/database/database.module.ts delete mode 100644 maternal-app-backend/src/database/entities/child.entity.ts delete mode 100644 maternal-app-backend/src/database/entities/device-registry.entity.ts delete mode 100644 maternal-app-backend/src/database/entities/family-member.entity.ts delete mode 100644 maternal-app-backend/src/database/entities/family.entity.ts delete mode 100644 maternal-app-backend/src/database/entities/index.ts delete mode 100644 maternal-app-backend/src/database/entities/user.entity.ts delete mode 100644 maternal-app-backend/src/database/migrations/V001_create_core_auth.sql delete mode 100644 maternal-app-backend/src/database/migrations/V002_create_family_structure.sql delete mode 100644 maternal-app-backend/src/database/migrations/V003_create_refresh_tokens.sql delete mode 100644 maternal-app-backend/src/database/migrations/run-migrations.ts 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 + + + + + + + + ); +} +``` + +### 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)} + + + + )} + + {/* 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 && ( + + )} + {alt} 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