diff --git a/web/package-lock.json b/web/package-lock.json index c0fc9c4..b9b83f4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "web", "version": "0.0.0", "dependencies": { + "@supabase/supabase-js": "^2.108.2", "html5-qrcode": "^2.3.8", "lucide-react": "^1.18.0", "react": "^19.2.6", @@ -840,6 +841,90 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.108.2.tgz", + "integrity": "sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.108.2.tgz", + "integrity": "sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.4.tgz", + "integrity": "sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.108.2.tgz", + "integrity": "sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.108.2.tgz", + "integrity": "sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.2", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.108.2.tgz", + "integrity": "sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.108.2.tgz", + "integrity": "sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.108.2", + "@supabase/functions-js": "2.108.2", + "@supabase/postgrest-js": "2.108.2", + "@supabase/realtime-js": "2.108.2", + "@supabase/storage-js": "2.108.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1758,6 +1843,15 @@ "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", "license": "Apache-2.0" }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2562,9 +2656,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/web/package.json b/web/package.json index e46c493..376a696 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@supabase/supabase-js": "^2.108.2", "html5-qrcode": "^2.3.8", "lucide-react": "^1.18.0", "react": "^19.2.6", diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx index 4f666bc..16eda07 100644 --- a/web/src/components/auth/ProtectedRoute.tsx +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -1,5 +1,4 @@ // src/components/auth/ProtectedRoute.tsx -import React from 'react'; import { Navigate } from 'react-router-dom'; import { useAuth } from '../../hooks/useAuth'; @@ -8,22 +7,26 @@ interface ProtectedRouteProps { } const ProtectedRoute: React.FC = ({ children }) => { - const { isAuthenticated, isLoading } = useAuth(); + const { isAuthenticated, loading } = useAuth(); + + // Check if we're in development with skip login + const isSkipLogin = localStorage.getItem('skipLogin') === 'true'; - if (isLoading) { + if (loading) { return ( -
- Loading... +
+
+

Loading...

); } - return isAuthenticated ? <>{children} : ; + // Allow access if skip login is enabled OR user is authenticated + if (isSkipLogin || isAuthenticated) { + return <>{children}; + } + + return ; }; export default ProtectedRoute; \ No newline at end of file diff --git a/web/src/components/common/Sidebar.tsx b/web/src/components/common/Sidebar.tsx index d29710d..486f414 100644 --- a/web/src/components/common/Sidebar.tsx +++ b/web/src/components/common/Sidebar.tsx @@ -1,183 +1,25 @@ -// src/components/common/Sidebar.tsx -import React from 'react'; -import { NavLink } from 'react-router-dom'; +// src/components/common/Sidebar.tsx (or Navbar.tsx) +import { Link, useLocation } from 'react-router-dom'; -interface SidebarProps { - collapsed: boolean; - onToggle: () => void; -} - -const Sidebar: React.FC = ({ collapsed, onToggle }) => { - const menuItems = [ - { path: '/dashboard', icon: '📊', label: 'Dashboard' }, - { path: '/appointments', icon: '📅', label: 'Appointments' }, - { path: '/medicine', icon: '💊', label: 'Medicine' }, - { path: '/scanner', icon: '📷', label: 'Scanner' }, - { path: '/profile', icon: '👤', label: 'Profile' }, - ]; +const Sidebar = () => { + const location = useLocation(); return ( - + ); }; -const styles = { - sidebar: { - background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)', - boxShadow: '2px 0 12px rgba(0,0,0,0.05)', - height: '100vh', - position: 'fixed' as const, - left: 0, - top: 0, - transition: 'width 0.3s ease', - display: 'flex', - flexDirection: 'column' as const, - zIndex: 1000, - }, - logoContainer: { - padding: '24px 20px', - borderBottom: '1px solid #e0e7ff', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - position: 'relative' as const, - }, - logo: { - display: 'flex', - alignItems: 'center', - gap: '12px', - }, - logoIcon: { - fontSize: '28px', - }, - logoText: { - fontSize: '20px', - fontWeight: '700', - background: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 100%)', - WebkitBackgroundClip: 'text', - WebkitTextFillColor: 'transparent', - letterSpacing: '-0.5px', - }, - toggleButton: { - background: 'white', - border: '1px solid #cbd5e1', - borderRadius: '8px', - width: '28px', - height: '28px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - cursor: 'pointer', - transition: 'all 0.2s', - color: '#475569', - position: 'absolute' as const, - right: '-14px', - top: '32px', - zIndex: 10, - fontSize: '14px', - }, - nav: { - flex: 1, - padding: '20px 0', - }, - menuList: { - listStyle: 'none', - padding: 0, - margin: 0, - }, - menuItem: { - marginBottom: '8px', - }, - navLink: { - display: 'flex', - alignItems: 'center', - gap: '12px', - textDecoration: 'none', - color: '#475569', - fontWeight: '500', - fontSize: '14px', - transition: 'all 0.2s', - borderRadius: '0 12px 12px 0', - marginRight: '16px', - position: 'relative' as const, - }, - icon: { - fontSize: '20px', - }, - linkText: { - flex: 1, - }, - footer: { - padding: '20px', - borderTop: '1px solid #e0e7ff', - textAlign: 'center' as const, - }, - version: { - fontSize: '12px', - color: '#94a3b8', - }, -}; - -// Add hover effects -const styleSheet = document.createElement("style"); -styleSheet.textContent = ` - .nav-link:hover { - background: linear-gradient(135deg, #1e3c7210 0%, #2a529810 100%); - transform: translateX(4px); - } - - .toggle-button:hover { - background: #f1f5f9; - transform: scale(1.05); - } -`; -document.head.appendChild(styleSheet); - export default Sidebar; \ No newline at end of file diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 3508cdb..44c0808 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -1,67 +1,323 @@ // src/contexts/AuthContext.tsx -import { createContext, useState, ReactNode } from 'react'; +import React, { createContext, useState, useEffect, ReactNode } from 'react'; +import { supabase, supabaseAdmin, DoctorProfile } from '../lib/supabase'; +import { Session, User as SupabaseUser } from '@supabase/supabase-js'; -interface User { +export interface User { id: string; - phoneNumber: string; - name: string; - role: 'doctor' | 'admin'; - email?: string; + fullName?: string; + email: string; + phoneNumber?: string; + role?: 'doctor'; + specialization?: string; + registrationNumber?: string; + qualification?: string; + yearsOfExperience?: string; + aboutMe?: string; + consultationFee?: string; + timings?: string; + dateOfBirth?: string; + gender?: string; + languages?: string; + isVerified?: boolean; } interface AuthContextType { user: User | null; + session: Session | null; + profile: DoctorProfile | null; + login: (identifier: string, password: string) => Promise; + signup: (userData: any) => Promise; + logout: () => Promise; isAuthenticated: boolean; - isLoading: boolean; - login: (phoneNumber: string, password: string) => Promise; - logout: () => void; + loading: boolean; + updateProfile: (data: Partial) => Promise; } export const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [user, setUser] = useState(() => { - const storedUser = localStorage.getItem('user'); - return storedUser ? JSON.parse(storedUser) : null; - }); - - const login = async (phoneNumber: string, password: string) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - // Validate phone number (10 digits) and password (min 6 chars) - if (phoneNumber.length === 10 && password.length >= 6) { - const mockUser: User = { - id: '1', - phoneNumber: phoneNumber, - name: 'Dr. John Doe', - role: 'doctor', - email: `${phoneNumber}@swasthya.com`, - }; - setUser(mockUser); - localStorage.setItem('user', JSON.stringify(mockUser)); - resolve(); + const [user, setUser] = useState(null); + const [profile, setProfile] = useState(null); + const [session, setSession] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Get session from Supabase + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + if (session?.user) { + fetchDoctorProfile(session.user); + } else { + setLoading(false); + } + }); + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => { + setSession(session); + if (session?.user) { + await fetchDoctorProfile(session.user); + } else { + setUser(null); + setProfile(null); + setIsAuthenticated(false); + setLoading(false); + } + }); + + return () => subscription.unsubscribe(); + }, []); + + const fetchDoctorProfile = async (supabaseUser: SupabaseUser) => { + try { + console.log('🔍 Fetching profile for user:', supabaseUser.id); + + const { data: doctorData, error } = await supabase + .from('doctor_profiles') + .select('*') + .eq('user_id', supabaseUser.id) + .maybeSingle(); + + if (error && error.code !== 'PGRST116') { + console.error('❌ Error fetching doctor profile:', error); + setLoading(false); + return; + } + + if (doctorData) { + console.log('✅ Profile found:', doctorData); + const userData: User = { + id: supabaseUser.id, + fullName: doctorData.full_name, + email: doctorData.email || supabaseUser.email || '', + phoneNumber: doctorData.phone_number, + role: doctorData.role, + specialization: doctorData.specialization, + registrationNumber: doctorData.registration_number, + qualification: doctorData.qualification, + yearsOfExperience: doctorData.years_of_experience, + aboutMe: doctorData.about_me, + consultationFee: doctorData.consultation_fee, + timings: doctorData.timings, + dateOfBirth: doctorData.date_of_birth, + gender: doctorData.gender, + languages: doctorData.languages, + isVerified: doctorData.is_verified, + }; + setUser(userData); + setProfile(doctorData); + setIsAuthenticated(true); + } else { + console.log('â„šī¸ No profile found for user'); + const userData: User = { + id: supabaseUser.id, + email: supabaseUser.email || '', + phoneNumber: supabaseUser.phone || '', + fullName: supabaseUser.user_metadata?.full_name, + role: 'doctor', + }; + setUser(userData); + setProfile(null); + setIsAuthenticated(true); + } + setLoading(false); + } catch (error) { + console.error('❌ Error fetching doctor profile:', error); + setLoading(false); + } + }; + + const login = async (identifier: string, password: string) => { + try { + console.log('🔑 Attempting login with:', identifier); + + const isEmail = identifier.includes('@'); + let email = identifier; + + if (!isEmail) { + const { data: doctor } = await supabase + .from('doctor_profiles') + .select('email') + .eq('phone_number', identifier) + .maybeSingle(); + + if (doctor && doctor.email) { + email = doctor.email; } else { - reject(new Error('Invalid credentials')); + email = `${identifier}@swasthya.com`; } - }, 1000); - }); + } + + console.log('📧 Logging in with email:', email); + + const { data, error } = await supabase.auth.signInWithPassword({ + email: email, + password: password, + }); + + if (error) { + console.error('❌ Login error:', error); + throw error; + } + + console.log('✅ Login successful:', data.user?.id); + } catch (error) { + console.error('❌ Login failed:', error); + throw error; + } + }; + + const signup = async (userData: any) => { + try { + console.log('📝 Starting signup with data:', userData); + + const email = userData.email; + const password = userData.password; + + if (!password || password.length < 6) { + throw new Error('Password must be at least 6 characters'); + } + + // Create auth user + const { data: authData, error: authError } = await supabase.auth.signUp({ + email: email, + password: password, + options: { + data: { + full_name: userData.fullName, + phone: userData.phoneNumber, + role: 'doctor', + }, + }, + }); + + if (authError) { + console.error('❌ Auth error:', authError); + throw authError; + } + + if (!authData.user) { + throw new Error('Failed to create user account'); + } + + console.log('✅ User created successfully:', authData.user.id); + + // Wait for user to be created + console.log('âŗ Waiting for user to be fully created...'); + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Create doctor profile using ONLY admin client (bypasses RLS) + const profileData = { + user_id: authData.user.id, + full_name: userData.fullName, + email: email, + phone_number: userData.phoneNumber, + specialization: userData.specialization || null, + registration_number: userData.medicalRegNumber || null, + role: 'doctor', + is_verified: false, + }; + + console.log('📝 Creating profile with data:', profileData); + + // ONLY use admin client to bypass RLS + const { data: profileResult, error: profileError } = await supabaseAdmin + .from('doctor_profiles') + .insert(profileData) + .select() + .single(); + + if (profileError) { + console.error('❌ Profile creation error:', profileError); + + // If admin client fails, try with RPC function + console.log('🔄 Trying with RPC function...'); + const { data: rpcResult, error: rpcError } = await supabase.rpc('create_doctor_profile', { + p_user_id: authData.user.id, + p_full_name: userData.fullName, + p_email: email, + p_phone_number: userData.phoneNumber, + p_specialization: userData.specialization || null, + p_registration_number: userData.medicalRegNumber || null, + }); + + if (rpcError) { + console.error('❌ RPC also failed:', rpcError); + throw new Error('Failed to create doctor profile. Please contact support.'); + } + + console.log('✅ Profile created with RPC:', rpcResult); + } else { + console.log('✅ Profile created successfully:', profileResult); + } + + console.log('✅ Signup completed successfully'); + + } catch (error: any) { + console.error('❌ Signup error:', error); + throw error; + } + }; + + const updateProfile = async (data: Partial) => { + if (!user?.id) throw new Error('User not authenticated'); + + const dbData: any = {}; + if (data.full_name !== undefined) dbData.full_name = data.full_name; + if (data.date_of_birth !== undefined) dbData.date_of_birth = data.date_of_birth; + if (data.email !== undefined) dbData.email = data.email; + if (data.phone_number !== undefined) dbData.phone_number = data.phone_number; + if (data.gender !== undefined) dbData.gender = data.gender; + if (data.languages !== undefined) dbData.languages = data.languages; + if (data.specialization !== undefined) dbData.specialization = data.specialization; + if (data.qualification !== undefined) dbData.qualification = data.qualification; + if (data.registration_number !== undefined) dbData.registration_number = data.registration_number; + if (data.years_of_experience !== undefined) dbData.years_of_experience = data.years_of_experience; + if (data.about_me !== undefined) dbData.about_me = data.about_me; + if (data.consultation_fee !== undefined) dbData.consultation_fee = data.consultation_fee; + if (data.timings !== undefined) dbData.timings = data.timings; + + const { error } = await supabase + .from('doctor_profiles') + .update(dbData) + .eq('user_id', user.id); + + if (error) throw error; + + // Refresh profile + const { data: { user: currentUser } } = await supabase.auth.getUser(); + if (currentUser) { + await fetchDoctorProfile(currentUser); + } }; - const logout = () => { - setUser(null); - localStorage.removeItem('user'); + const logout = async () => { + try { + const { error } = await supabase.auth.signOut(); + if (error) throw error; + } catch (error) { + console.error('Logout error:', error); + } finally { + setUser(null); + setProfile(null); + setIsAuthenticated(false); + } }; return ( - + {children} ); diff --git a/web/src/lib/supabase.ts b/web/src/lib/supabase.ts new file mode 100644 index 0000000..aa594c5 --- /dev/null +++ b/web/src/lib/supabase.ts @@ -0,0 +1,65 @@ +// src/lib/supabase.ts +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; +const supabaseServiceKey = import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY; + +console.log('🔍 Environment check:'); +console.log('Supabase URL:', supabaseUrl); +console.log('Anon Key exists:', !!supabaseAnonKey); +console.log('Service Key exists:', !!supabaseServiceKey); + +if (!supabaseUrl || !supabaseAnonKey) { + console.error('❌ Missing Supabase environment variables'); + throw new Error('Missing Supabase environment variables'); +} + +// Regular client for normal operations +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + auth: { + persistSession: true, + autoRefreshToken: true, + } +}); + +// Service role client for admin operations (bypasses RLS) +export const supabaseAdmin = supabaseServiceKey + ? createClient(supabaseUrl, supabaseServiceKey, { + auth: { + persistSession: false, + autoRefreshToken: false, + }, + global: { + headers: { + 'apikey': supabaseServiceKey, + 'Authorization': `Bearer ${supabaseServiceKey}`, + }, + }, + }) + : supabase; + +console.log('✅ Supabase client initialized'); +console.log('🔑 Admin client available:', !!supabaseServiceKey); + +export interface DoctorProfile { + id: string; + user_id: string; + full_name: string; + date_of_birth: string | null; + email: string; + phone_number: string; + gender: string | null; + languages: string | null; + specialization: string | null; + qualification: string | null; + registration_number: string | null; + years_of_experience: string | null; + about_me: string | null; + consultation_fee: string | null; + timings: string | null; + role: 'doctor'; + is_verified: boolean; + created_at: string; + updated_at: string; +} \ No newline at end of file diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index 5d799b2..654360d 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -15,7 +15,7 @@ const Auth: React.FC = () => { const { login, isAuthenticated } = useAuth(); const [loginData, setLoginData] = useState({ - phoneNumber: "", + identifier: "", password: "" }); @@ -30,10 +30,8 @@ const Auth: React.FC = () => { const validateLogin = () => { const newErrors: Record = {}; - if (!loginData.phoneNumber.trim()) { - newErrors.phoneNumber = "Phone number is required"; - } else if (!/^[0-9]{10}$/.test(loginData.phoneNumber.trim())) { - newErrors.phoneNumber = "Please enter a valid 10-digit phone number"; + if (!loginData.identifier.trim()) { + newErrors.identifier = "Phone number or email is required"; } if (!loginData.password) { @@ -58,15 +56,32 @@ const Auth: React.FC = () => { setLoading(true); try { - await login(loginData.phoneNumber, loginData.password); - } catch (err) { - setError("Invalid phone number or password. Please try again."); + await login(loginData.identifier, loginData.password); + navigate('/dashboard'); + } catch (err: any) { + setError(err.message || "Invalid credentials. Please try again."); triggerShake(); } finally { setLoading(false); } }; + // Skip login - go directly to dashboard + const handleSkipLogin = () => { + console.log('🚀 Skip login - going to dashboard'); + // Set flag to bypass authentication + localStorage.setItem('skipLogin', 'true'); + // Store some dummy user data + localStorage.setItem('user', JSON.stringify({ + id: 'skip-user', + fullName: 'Guest Doctor', + email: 'guest@swasthya.com', + role: 'doctor' + })); + // Navigate to dashboard + navigate('/dashboard', { replace: true }); + }; + const triggerShake = () => { setShake(true); setTimeout(() => setShake(false), 500); @@ -146,8 +161,8 @@ const Auth: React.FC = () => {
- -
+ +
@@ -155,21 +170,20 @@ const Auth: React.FC = () => { { - setLoginData({ ...loginData, phoneNumber: e.target.value.replace(/\D/g, '') }); + setLoginData({ ...loginData, identifier: e.target.value }); setError(""); - if (errors.phoneNumber) { - setErrors({ ...errors, phoneNumber: "" }); + if (errors.identifier) { + setErrors({ ...errors, identifier: "" }); } }} - maxLength={10} disabled={loading} />
- {errors.phoneNumber &&

{errors.phoneNumber}

} + {errors.identifier &&

{errors.identifier}

}
@@ -245,6 +259,15 @@ const Auth: React.FC = () => { Continue )} + + {/* Skip Login Button - Simple */} +

diff --git a/web/src/pages/Medicine.tsx b/web/src/pages/Medicine.tsx index a9160c8..1838989 100644 --- a/web/src/pages/Medicine.tsx +++ b/web/src/pages/Medicine.tsx @@ -1,11 +1,257 @@ // src/pages/Medicine.tsx -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { supabase } from '../lib/supabase'; +import { Medicine } from '../types/medicine'; +import '../styles/medicine.css'; const Medicine: React.FC = () => { + const [medicines, setMedicines] = useState([]); + const [filteredMedicines, setFilteredMedicines] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(50); + const [selectedMedicine, setSelectedMedicine] = useState(null); + + useEffect(() => { + loadMedicines(); + }, []); + + const loadMedicines = async () => { + try { + setLoading(true); + setError(null); + + console.log('🔍 Fetching medicines...'); + + // Simple query - just get all data + const { data, error } = await supabase + .from('Medicines') + .select('*'); + + if (error) { + console.error('Error:', error); + setError(error.message); + throw error; + } + + if (!data || data.length === 0) { + console.warn('No data found'); + setMedicines([]); + setFilteredMedicines([]); + setLoading(false); + return; + } + + console.log(`✅ Loaded ${data.length} medicines`); + setMedicines(data); + setFilteredMedicines(data); + } catch (err: any) { + console.error('Error:', err); + if (!error) { + setError(err.message || 'Failed to load medicines'); + } + } finally { + setLoading(false); + } + }; + + // Filter medicines based on search + useEffect(() => { + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase().trim(); + const filtered = medicines.filter(med => + med.product_name?.toLowerCase().includes(query) || + med.salt_composition?.toLowerCase().includes(query) || + med.sub_category?.toLowerCase().includes(query) || + med.product_manufactured?.toLowerCase().includes(query) + ); + setFilteredMedicines(filtered); + } else { + setFilteredMedicines(medicines); + } + setCurrentPage(1); + }, [medicines, searchQuery]); + + const handleSearch = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }; + + const formatPrice = (price: string) => { + if (!price) return 'N/A'; + return `₹${price}`; + }; + + // Pagination + const totalPages = Math.ceil(filteredMedicines.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentMedicines = filteredMedicines.slice(startIndex, endIndex); + + if (loading) { + return ( +

+
+

Loading medicines...

+
+ ); + } + + if (error) { + return ( +
+

❌ {error}

+ +
+ ); + } + return ( -
-

Medicine Inventory

-

Medicine management page coming soon...

+
+
+
+

💊 Medicine Directory

+

Total: {medicines.length} medicines

+
+ +
+ + {/* Search Bar */} +
+
+ 🔍 + + {searchQuery && ( + + )} + + {filteredMedicines.length} results + +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + + {currentMedicines.map((medicine, index) => ( + + + + + + + + + + ))} + +
#Product NameCategorySalt CompositionManufacturerPriceAction
{startIndex + index + 1}{medicine.product_name || 'N/A'} + {medicine.sub_category || 'Uncategorized'} + {medicine.salt_composition || 'N/A'}{medicine.product_manufactured || 'N/A'}{formatPrice(medicine.product_price)} + +
+ + {filteredMedicines.length === 0 && ( +
+

No medicines found matching your search.

+ +
+ )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + (Showing {startIndex + 1} - {Math.min(endIndex, filteredMedicines.length)} of {filteredMedicines.length}) + + +
+ )} + + {/* Modal */} + {selectedMedicine && ( +
setSelectedMedicine(null)}> +
e.stopPropagation()}> + + +

{selectedMedicine.product_name || 'Unnamed Medicine'}

+ +
+
+ Category + {selectedMedicine.sub_category || 'N/A'} +
+
+ Salt Composition + {selectedMedicine.salt_composition || 'N/A'} +
+
+ Price + {formatPrice(selectedMedicine.product_price)} +
+
+ Manufactured By + {selectedMedicine.product_manufactured || 'N/A'} +
+ {selectedMedicine.medicine_desc && ( +
+ Description +

{selectedMedicine.medicine_desc}

+
+ )} + {selectedMedicine.side_effects && ( +
+ Side Effects +

{selectedMedicine.side_effects}

+
+ )} +
+
+
+ )}
); }; diff --git a/web/src/pages/Profile.tsx b/web/src/pages/Profile.tsx index d0035c0..e9b5c63 100644 --- a/web/src/pages/Profile.tsx +++ b/web/src/pages/Profile.tsx @@ -1,39 +1,438 @@ // src/pages/Profile.tsx -import React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useAuth } from '../hooks/useAuth'; import '../styles/profile.css'; +interface PersonalInfo { + fullName: string; + dateOfBirth: string; + email: string; + phone: string; + gender: string; + languages: string; +} + +interface ProfessionalDetails { + specialization: string; + qualification: string; + registrationNumber: string; + yearsOfExperience: string; + aboutMe: string; +} + +interface Availability { + consultationFee: string; + timings: string; +} + const Profile: React.FC = () => { - const { user } = useAuth(); + const { user, profile, isAuthenticated, loading, updateProfile } = useAuth(); + const [editingSection, setEditingSection] = useState(null); + const [saving, setSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); + const aboutRef = useRef(null); + + const [personalInfo, setPersonalInfo] = useState({ + fullName: user?.fullName || '', + dateOfBirth: user?.dateOfBirth || '', + email: user?.email || '', + phone: user?.phoneNumber || '', + gender: user?.gender || '', + languages: user?.languages || '', + }); + + const [professionalDetails, setProfessionalDetails] = useState({ + specialization: user?.specialization || '', + qualification: user?.qualification || '', + registrationNumber: user?.registrationNumber || '', + yearsOfExperience: user?.yearsOfExperience || '', + aboutMe: user?.aboutMe || '', + }); + + const [availability, setAvailability] = useState({ + consultationFee: user?.consultationFee || '', + timings: user?.timings || '', + }); + + useEffect(() => { + if (user) { + setPersonalInfo(prev => ({ + ...prev, + fullName: user.fullName || prev.fullName, + email: user.email || prev.email, + phone: user.phoneNumber || prev.phone, + dateOfBirth: user.dateOfBirth || prev.dateOfBirth, + gender: user.gender || prev.gender, + languages: user.languages || prev.languages, + })); + + setProfessionalDetails(prev => ({ + ...prev, + specialization: user.specialization || prev.specialization, + registrationNumber: user.registrationNumber || prev.registrationNumber, + qualification: user.qualification || prev.qualification, + yearsOfExperience: user.yearsOfExperience || prev.yearsOfExperience, + aboutMe: user.aboutMe || prev.aboutMe, + })); + + setAvailability(prev => ({ + ...prev, + consultationFee: user.consultationFee || prev.consultationFee, + timings: user.timings || prev.timings, + })); + } + }, [user]); + + const toggleEdit = (section: string) => { + if (editingSection === section) { + saveUserData(); + } else { + setEditingSection(section); + if (section === 'about' && aboutRef.current) { + setTimeout(() => { + aboutRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + }, 100); + } + } + }; + + const saveUserData = async () => { + setSaving(true); + setSaveMessage(null); + + try { + const updateData: any = { + full_name: personalInfo.fullName, + date_of_birth: personalInfo.dateOfBirth || null, + email: personalInfo.email, + phone_number: personalInfo.phone, + gender: personalInfo.gender || null, + languages: personalInfo.languages || null, + specialization: professionalDetails.specialization || null, + qualification: professionalDetails.qualification || null, + registration_number: professionalDetails.registrationNumber || null, + years_of_experience: professionalDetails.yearsOfExperience || null, + about_me: professionalDetails.aboutMe || null, + consultation_fee: availability.consultationFee || null, + timings: availability.timings || null, + }; + + await updateProfile(updateData); + setSaveMessage({ type: 'success', text: 'Profile saved successfully!' }); + setEditingSection(null); + } catch (error: any) { + console.error('Error saving profile:', error); + setSaveMessage({ type: 'error', text: error.message || 'Failed to save profile' }); + } finally { + setSaving(false); + setTimeout(() => setSaveMessage(null), 3000); + } + }; + + useEffect(() => { + if (editingSection === 'about' && aboutRef.current) { + const scrollY = window.scrollY; + requestAnimationFrame(() => { + window.scrollTo(0, scrollY); + }); + } + }, [editingSection]); + + if (loading) { + return ( +
+
+
+

Loading profile...

+
+
+ ); + } + + if (!isAuthenticated || !user) { + return ( +
+
+

Please Log In

+

You need to be logged in to view your profile.

+ +
+
+ ); + } return ( -
-
-
-
- {user?.name?.[0] || 'D'} -
-

{user?.name || 'Doctor'}

-

{user?.role || 'Doctor'}

- ● Active +
+
+
+

{personalInfo.fullName || 'User'}

+ + {professionalDetails.specialization || 'Not Specified'} + + {user.role && ( + + {user.role === 'doctor' ? 'đŸ‘¨â€âš•ī¸ Doctor' : '👤 Patient'} + + )}
+
-
-
- Phone Number - {user?.phoneNumber || 'Not provided'} -
-
- Email - {user?.email || 'Not provided'} -
-
- Role - {user?.role || 'Doctor'} + {saveMessage && ( +
+ {saveMessage.text} +
+ )} + +
+
+
+
+
+

Personal Information

+ +
+
+
+
Full Name
+ {editingSection === 'personal' ? ( + setPersonalInfo({...personalInfo, fullName: e.target.value})} + placeholder="Enter your full name" + /> + ) : ( +
{personalInfo.fullName || 'Not provided'}
+ )} +
+
+
Date of Birth
+ {editingSection === 'personal' ? ( + setPersonalInfo({...personalInfo, dateOfBirth: e.target.value})} + /> + ) : ( +
{personalInfo.dateOfBirth || 'Not provided'}
+ )} +
+
+
Email Address
+ {editingSection === 'personal' ? ( + setPersonalInfo({...personalInfo, email: e.target.value})} + placeholder="Enter your email" + /> + ) : ( +
{personalInfo.email || 'Not provided'}
+ )} +
+
+
Phone Number
+ {editingSection === 'personal' ? ( + setPersonalInfo({...personalInfo, phone: e.target.value})} + placeholder="Enter your phone number" + /> + ) : ( +
{personalInfo.phone || 'Not provided'}
+ )} +
+
+
Gender
+ {editingSection === 'personal' ? ( + + ) : ( +
{personalInfo.gender || 'Not provided'}
+ )} +
+
+
Languages Known
+ {editingSection === 'personal' ? ( + setPersonalInfo({...personalInfo, languages: e.target.value})} + placeholder="e.g., English, Spanish" + /> + ) : ( +
{personalInfo.languages || 'Not provided'}
+ )} +
+
+
+ +
+
+

About Me

+ +
+
+ {editingSection === 'about' ? ( +