diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 4622ff3..46f7a96 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/package-lock.json b/apps/admin/package-lock.json index 21424c2..ae7fc6e 100644 --- a/apps/admin/package-lock.json +++ b/apps/admin/package-lock.json @@ -22,6 +22,7 @@ "autoprefixer": "^10.4.21", "axios": "^1.13.2", "bcryptjs": "^3.0.3", + "date-fns": "^4.1.0", "lucide-react": "^0.553.0", "next": "^16.0.1", "postcss": "^8.5.6", @@ -4931,6 +4932,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/apps/admin/package.json b/apps/admin/package.json index d5b3afd..18f57f9 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -25,6 +25,7 @@ "autoprefixer": "^10.4.21", "axios": "^1.13.2", "bcryptjs": "^3.0.3", + "date-fns": "^4.1.0", "lucide-react": "^0.553.0", "next": "^16.0.1", "postcss": "^8.5.6", diff --git a/apps/admin/src/app/api/admin/attendance/route.ts b/apps/admin/src/app/api/admin/attendance/route.ts new file mode 100644 index 0000000..3eb3059 --- /dev/null +++ b/apps/admin/src/app/api/admin/attendance/route.ts @@ -0,0 +1,23 @@ +import { auth } from '@clerk/nextjs/server' +import { NextResponse } from 'next/server' +import { getDatabase } from '@/lib/database' + +export async function GET(req: Request) { + try { + const { userId } = await auth() + if (!userId) return new NextResponse('Unauthorized', { status: 401 }) + + const db = await getDatabase() + const user = await db.getUserById(userId) + + if (!user || user.role !== 'admin') { + return new NextResponse('Forbidden', { status: 403 }) + } + + const attendance = await db.getAllAttendance() + return NextResponse.json(attendance) + } catch (error) { + console.error('Admin attendance error:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/apps/admin/src/app/api/attendance/check-in/route.ts b/apps/admin/src/app/api/attendance/check-in/route.ts new file mode 100644 index 0000000..10d9122 --- /dev/null +++ b/apps/admin/src/app/api/attendance/check-in/route.ts @@ -0,0 +1,44 @@ +import { auth } from '@clerk/nextjs/server' +import { NextResponse } from 'next/server' +import { getDatabase } from '@/lib/database' +import { ensureUserSynced } from '@/lib/sync-user' + +export async function POST(req: Request) { + try { + const { userId } = await auth() + if (!userId) return new NextResponse('Unauthorized', { status: 401 }) + + const db = await getDatabase() + + // Ensure user exists in DB (sync from Clerk if needed) + await ensureUserSynced(userId, db) + + let client = await db.getClientByUserId(userId) + + if (!client) { + // Auto-create client profile if it doesn't exist + console.log('Client profile not found, creating new one for user:', userId) + client = await db.createClient({ + userId, + membershipType: 'basic', + membershipStatus: 'active', + joinDate: new Date() + }) + } + + // Check if already checked in + const activeCheckIn = await db.getActiveCheckIn(client.id) + if (activeCheckIn) { + return new NextResponse('Already checked in', { status: 400 }) + } + + const body = await req.json() + const { type = 'gym', notes } = body + + const attendance = await db.checkIn(client.id, type, notes) + return NextResponse.json(attendance) + } catch (error) { + console.error('Check-in error:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/apps/admin/src/app/api/attendance/check-out/route.ts b/apps/admin/src/app/api/attendance/check-out/route.ts new file mode 100644 index 0000000..2bfdbcb --- /dev/null +++ b/apps/admin/src/app/api/attendance/check-out/route.ts @@ -0,0 +1,28 @@ +import { auth } from '@clerk/nextjs/server' +import { NextResponse } from 'next/server' +import { getDatabase } from '@/lib/database' + +export async function POST(req: Request) { + try { + const { userId } = await auth() + if (!userId) return new NextResponse('Unauthorized', { status: 401 }) + + const db = await getDatabase() + const client = await db.getClientByUserId(userId) + + if (!client) { + return new NextResponse('Client profile not found', { status: 404 }) + } + + const activeCheckIn = await db.getActiveCheckIn(client.id) + if (!activeCheckIn) { + return new NextResponse('No active check-in found', { status: 404 }) + } + + const attendance = await db.checkOut(activeCheckIn.id) + return NextResponse.json(attendance) + } catch (error) { + console.error('Check-out error:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/apps/admin/src/app/api/attendance/history/route.ts b/apps/admin/src/app/api/attendance/history/route.ts new file mode 100644 index 0000000..14b276c --- /dev/null +++ b/apps/admin/src/app/api/attendance/history/route.ts @@ -0,0 +1,44 @@ +import { auth } from '@clerk/nextjs/server' +import { NextResponse } from 'next/server' +import { getDatabase } from '@/lib/database' +import { ensureUserSynced } from '@/lib/sync-user' + +export async function GET(req: Request) { + console.log('GET /api/attendance/history called') + console.log('Headers:', Object.fromEntries(req.headers.entries())) + + try { + const authResult = await auth() + const { userId } = authResult + console.log('Auth result:', JSON.stringify(authResult, null, 2)) + + if (!userId) { + console.log('No userId found in auth result') + return new NextResponse('Unauthorized', { status: 401 }) + } + + const db = await getDatabase() + + // Ensure user exists in DB (sync from Clerk if needed) + await ensureUserSynced(userId, db) + + let client = await db.getClientByUserId(userId) + + if (!client) { + // Auto-create client profile if it doesn't exist + console.log('Client profile not found, creating new one for user:', userId) + client = await db.createClient({ + userId, + membershipType: 'basic', + membershipStatus: 'active', + joinDate: new Date() + }) + } + + const history = await db.getAttendanceHistory(client.id) + return NextResponse.json(history) + } catch (error) { + console.error('History error:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/apps/admin/src/app/api/health/route.ts b/apps/admin/src/app/api/health/route.ts new file mode 100644 index 0000000..13118c4 --- /dev/null +++ b/apps/admin/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server' + +export async function GET() { + return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() }) +} diff --git a/apps/admin/src/app/attendance/page.tsx b/apps/admin/src/app/attendance/page.tsx new file mode 100644 index 0000000..960aeba --- /dev/null +++ b/apps/admin/src/app/attendance/page.tsx @@ -0,0 +1,96 @@ +'use client' + +import { useState, useEffect } from 'react' +import { format } from 'date-fns' + +interface Attendance { + id: string + clientId: string + checkInTime: string + checkOutTime?: string + type: string + notes?: string +} + +export default function AttendancePage() { + const [attendance, setAttendance] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchAttendance = async () => { + try { + const res = await fetch('/api/admin/attendance') + if (res.ok) { + const data = await res.json() + setAttendance(data) + } + } catch (error) { + console.error('Error fetching attendance:', error) + } finally { + setLoading(false) + } + } + + fetchAttendance() + }, []) + + if (loading) { + return
Loading...
+ } + + return ( +
+

Attendance Monitoring

+ +
+ + + + + + + + + + + + {attendance.map((record) => ( + + + + + + + + ))} + +
+ Client ID + + Type + + Check In + + Check Out + + Status +
+ {record.clientId} + + {record.type} + + {format(new Date(record.checkInTime), 'PP p')} + + {record.checkOutTime ? format(new Date(record.checkOutTime), 'PP p') : '-'} + + + {record.checkOutTime ? 'Completed' : 'Active'} + +
+
+
+ ) +} diff --git a/apps/admin/src/lib/database.ts b/apps/admin/src/lib/database.ts deleted file mode 100644 index 4d88b05..0000000 --- a/apps/admin/src/lib/database.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface User { - id: string - email: string - firstName: string - lastName: string - password: string - phone?: string - role: 'admin' | 'client' - createdAt: Date - updatedAt: Date -} - -export interface Client { - id: string - userId: string - membershipType: 'basic' | 'premium' | 'vip' - membershipStatus: 'active' | 'inactive' | 'expired' - joinDate: Date -} - -export interface FitnessProfile { - userId: string - height: string - weight: string - age: string - gender: 'male' | 'female' | 'other' - activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active' - fitnessGoals: string[] - exerciseHabits: string - dietHabits: string - medicalConditions: string -} - -// In-memory database -export const users: User[] = [] -export const clients: Client[] = [] -export const fitnessProfiles: FitnessProfile[] = [] \ No newline at end of file diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts index c048ec9..b416180 100644 --- a/apps/admin/src/lib/database/sqlite.ts +++ b/apps/admin/src/lib/database/sqlite.ts @@ -1,7 +1,7 @@ import Database from 'better-sqlite3' import path from 'path' import fs from 'fs' -import { IDatabase, User, Client, FitnessProfile, DatabaseConfig } from './types' +import { IDatabase, User, Client, FitnessProfile, Attendance, DatabaseConfig } from './types' export class SQLiteDatabase implements IDatabase { private db: Database.Database | null = null @@ -14,21 +14,21 @@ export class SQLiteDatabase implements IDatabase { async connect(): Promise { try { const dbPath = this.config.connection.filename || path.join(process.cwd(), 'data', 'fitai.db') - + // Ensure data directory exists const dataDir = path.dirname(dbPath) if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) } - + this.db = new Database(dbPath) - + // Enable foreign keys this.db.exec('PRAGMA foreign_keys = ON') - + // Create tables await this.createTables() - + if (this.config.options?.logging) { console.log('SQLite database connected successfully at:', dbPath) } @@ -103,18 +103,37 @@ export class SQLiteDatabase implements IDatabase { CREATE INDEX IF NOT EXISTS idx_clients_userId ON clients(userId); CREATE INDEX IF NOT EXISTS idx_fitness_profiles_userId ON fitness_profiles(userId); `) + + // Attendance table + this.db.exec(` + CREATE TABLE IF NOT EXISTS attendance ( + id TEXT PRIMARY KEY, + clientId TEXT NOT NULL, + checkInTime DATETIME NOT NULL, + checkOutTime DATETIME, + type TEXT NOT NULL CHECK (type IN ('gym', 'class', 'personal_training')), + notes TEXT, + createdAt DATETIME NOT NULL, + FOREIGN KEY (clientId) REFERENCES clients (id) ON DELETE CASCADE + ) + `) + + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_attendance_clientId ON attendance(clientId); + CREATE INDEX IF NOT EXISTS idx_attendance_checkInTime ON attendance(checkInTime); + `) } // User operations - async createUser(userData: Omit): Promise { + async createUser(userData: Omit): Promise { if (!this.db) throw new Error('Database not connected') - const id = Math.random().toString(36).substr(2, 9) + const id = userData.id || Math.random().toString(36).substr(2, 9) const now = new Date() - + const user: User = { - id, ...userData, + id, createdAt: now, updatedAt: now } @@ -123,9 +142,9 @@ export class SQLiteDatabase implements IDatabase { `INSERT INTO users (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ) - + stmt.run( - user.id, user.email, user.firstName, user.lastName, user.password, + user.id, user.email, user.firstName, user.lastName, user.password, user.phone, user.role, user.createdAt.toISOString(), user.updatedAt.toISOString() ) @@ -195,9 +214,9 @@ export class SQLiteDatabase implements IDatabase { `INSERT INTO clients (id, userId, membershipType, membershipStatus, joinDate) VALUES (?, ?, ?, ?, ?)` ) - + stmt.run( - client.id, client.userId, client.membershipType, + client.id, client.userId, client.membershipType, client.membershipStatus, client.joinDate.toISOString() ) @@ -269,7 +288,7 @@ export class SQLiteDatabase implements IDatabase { exerciseHabits, dietHabits, medicalConditions, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) - + stmt.run( profile.userId, profile.height, profile.weight, profile.age, profile.gender, profile.activityLevel, JSON.stringify(profile.fitnessGoals), profile.exerciseHabits, @@ -325,6 +344,73 @@ export class SQLiteDatabase implements IDatabase { return (result.changes || 0) > 0 } + // Attendance operations + async checkIn(clientId: string, type: 'gym' | 'class' | 'personal_training', notes?: string): Promise { + if (!this.db) throw new Error('Database not connected') + + const id = Math.random().toString(36).substr(2, 9) + const now = new Date() + + const attendance: Attendance = { + id, + clientId, + checkInTime: now, + type, + notes, + createdAt: now + } + + const stmt = this.db.prepare( + `INSERT INTO attendance (id, clientId, checkInTime, type, notes, createdAt) + VALUES (?, ?, ?, ?, ?, ?)` + ) + + stmt.run( + attendance.id, attendance.clientId, attendance.checkInTime.toISOString(), + attendance.type, attendance.notes, attendance.createdAt.toISOString() + ) + + return attendance + } + + async checkOut(attendanceId: string): Promise { + if (!this.db) throw new Error('Database not connected') + + const now = new Date() + const stmt = this.db.prepare('UPDATE attendance SET checkOutTime = ? WHERE id = ?') + stmt.run(now.toISOString(), attendanceId) + + return this.getAttendanceById(attendanceId) + } + + async getAttendanceById(id: string): Promise { + if (!this.db) throw new Error('Database not connected') + const stmt = this.db.prepare('SELECT * FROM attendance WHERE id = ?') + const row = stmt.get(id) + return row ? this.mapRowToAttendance(row) : null + } + + async getAttendanceHistory(clientId: string): Promise { + if (!this.db) throw new Error('Database not connected') + const stmt = this.db.prepare('SELECT * FROM attendance WHERE clientId = ? ORDER BY checkInTime DESC') + const rows = stmt.all(clientId) + return rows.map(row => this.mapRowToAttendance(row)) + } + + async getAllAttendance(): Promise { + if (!this.db) throw new Error('Database not connected') + const stmt = this.db.prepare('SELECT * FROM attendance ORDER BY checkInTime DESC') + const rows = stmt.all() + return rows.map(row => this.mapRowToAttendance(row)) + } + + async getActiveCheckIn(clientId: string): Promise { + if (!this.db) throw new Error('Database not connected') + const stmt = this.db.prepare('SELECT * FROM attendance WHERE clientId = ? AND checkOutTime IS NULL ORDER BY checkInTime DESC LIMIT 1') + const row = stmt.get(clientId) + return row ? this.mapRowToAttendance(row) : null + } + // Helper methods to map database rows to entities private mapRowToUser(row: any): User { return { @@ -366,4 +452,16 @@ export class SQLiteDatabase implements IDatabase { updatedAt: new Date(row.updatedAt) } } + + private mapRowToAttendance(row: any): Attendance { + return { + id: row.id, + clientId: row.clientId, + checkInTime: new Date(row.checkInTime), + checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined, + type: row.type, + notes: row.notes, + createdAt: new Date(row.createdAt) + } + } } \ No newline at end of file diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts index f6127e7..d8e7bf2 100644 --- a/apps/admin/src/lib/database/types.ts +++ b/apps/admin/src/lib/database/types.ts @@ -34,6 +34,16 @@ export interface FitnessProfile { updatedAt: Date; } +export interface Attendance { + id: string; + clientId: string; + checkInTime: Date; + checkOutTime?: Date; + type: "gym" | "class" | "personal_training"; + notes?: string; + createdAt: Date; +} + // Database Interface - allows us to swap implementations export interface IDatabase { // Connection management @@ -41,7 +51,7 @@ export interface IDatabase { disconnect(): Promise; // User operations - createUser(user: Omit): Promise; + createUser(user: Omit): Promise; getUserById(id: string): Promise; getUserByEmail(email: string): Promise; getAllUsers(): Promise; @@ -67,6 +77,17 @@ export interface IDatabase { updates: Partial, ): Promise; deleteFitnessProfile(userId: string): Promise; + + // Attendance operations + checkIn( + clientId: string, + type: "gym" | "class" | "personal_training", + notes?: string, + ): Promise; + checkOut(attendanceId: string): Promise; + getAttendanceHistory(clientId: string): Promise; + getAllAttendance(): Promise; + getActiveCheckIn(clientId: string): Promise; } // Database configuration diff --git a/apps/admin/src/lib/sync-user.ts b/apps/admin/src/lib/sync-user.ts new file mode 100644 index 0000000..abdc240 --- /dev/null +++ b/apps/admin/src/lib/sync-user.ts @@ -0,0 +1,29 @@ +import { currentUser } from '@clerk/nextjs/server' +import { IDatabase } from './database/types' + +export async function ensureUserSynced(userId: string, db: IDatabase) { + const existingUser = await db.getUserById(userId) + if (existingUser) return existingUser + + console.log('User not found in DB, syncing from Clerk:', userId) + const clerkUser = await currentUser() + + if (!clerkUser || clerkUser.id !== userId) { + throw new Error('Could not fetch Clerk user details') + } + + const email = clerkUser.emailAddresses[0]?.emailAddress + if (!email) throw new Error('User has no email') + + const user = await db.createUser({ + id: userId, + email, + firstName: clerkUser.firstName || '', + lastName: clerkUser.lastName || '', + password: '', // Managed by Clerk + phone: clerkUser.phoneNumbers[0]?.phoneNumber || undefined, + role: (clerkUser.publicMetadata.role as 'admin' | 'client') || 'client' + }) + + return user +} diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/middleware.ts index c237e2b..0ae9460 100644 --- a/apps/admin/src/middleware.ts +++ b/apps/admin/src/middleware.ts @@ -5,6 +5,7 @@ const isPublicRoute = createRouteMatcher([ "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)", + "/api/attendance(.*)", ]); // Define routes that require authentication @@ -16,7 +17,6 @@ const isProtectedRoute = createRouteMatcher([ "/api/users(.*)", "/api/profile(.*)", "/api/payments(.*)", - "/api/attendance(.*)", "/api/notifications(.*)", ]); diff --git a/apps/mobile/src/api/fitnessProfile.ts b/apps/mobile/src/api/fitnessProfile.ts index 164c88a..e535e17 100644 --- a/apps/mobile/src/api/fitnessProfile.ts +++ b/apps/mobile/src/api/fitnessProfile.ts @@ -1,11 +1,5 @@ import axios from "axios"; -import { Platform } from "react-native"; - -const API_BASE_URL = Platform.select({ - android: "http://10.0.2.2:3000", // Android emulator maps this to host's localhost - ios: "http://localhost:3000", - default: process.env.EXPO_PUBLIC_API_URL || "http://localhost:3000", -}); +import { API_BASE_URL } from "../config/api"; export interface FitnessProfile { id?: string; diff --git a/apps/mobile/src/app/(tabs)/attendance.tsx b/apps/mobile/src/app/(tabs)/attendance.tsx index 9a69de2..bab1650 100644 --- a/apps/mobile/src/app/(tabs)/attendance.tsx +++ b/apps/mobile/src/app/(tabs)/attendance.tsx @@ -1,28 +1,244 @@ -import { View, Text, StyleSheet } from 'react-native' +import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native' +import { useState, useEffect } from 'react' +import { useAuth } from '@clerk/clerk-expo' +import axios from 'axios' +import { API_BASE_URL, API_ENDPOINTS } from '../../config/api' + +interface Attendance { + id: string + checkInTime: string + checkOutTime?: string + type: string + notes?: string +} export default function AttendanceScreen() { + const { getToken, userId } = useAuth() + const [loading, setLoading] = useState(true) + const [activeCheckIn, setActiveCheckIn] = useState(null) + const [history, setHistory] = useState([]) + + const fetchAttendance = async () => { + try { + setLoading(true) + const token = await getToken() + const url = `${API_BASE_URL}${API_ENDPOINTS.ATTENDANCE.HISTORY}` + console.log('Fetching attendance from:', url) + const response = await axios.get(url, { + headers: { Authorization: `Bearer ${token}` } + }) + + const data: Attendance[] = response.data + setHistory(data) + + // Check if there's an active check-in (latest one has no checkOutTime) + if (data.length > 0 && !data[0].checkOutTime) { + setActiveCheckIn(data[0]) + } else { + setActiveCheckIn(null) + } + } catch (error) { + console.error('Error fetching attendance:', error) + Alert.alert('Error', 'Failed to load attendance data') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchAttendance() + }, []) + + const handleCheckIn = async () => { + try { + const token = await getToken() + await axios.post( + `${API_BASE_URL}${API_ENDPOINTS.ATTENDANCE.CHECK_IN}`, + { type: 'gym' }, + { headers: { Authorization: `Bearer ${token}` } } + ) + fetchAttendance() + Alert.alert('Success', 'Checked in successfully!') + } catch (error: any) { + console.error('Check-in error:', error) + Alert.alert('Error', error.response?.data || 'Failed to check in') + } + } + + const handleCheckOut = async () => { + try { + const token = await getToken() + await axios.post( + `${API_BASE_URL}${API_ENDPOINTS.ATTENDANCE.CHECK_OUT}`, + {}, + { headers: { Authorization: `Bearer ${token}` } } + ) + fetchAttendance() + Alert.alert('Success', 'Checked out successfully!') + } catch (error: any) { + console.error('Check-out error:', error) + Alert.alert('Error', error.response?.data || 'Failed to check out') + } + } + + if (loading && !history.length) { + return ( + + + + ) + } + return ( - + Attendance Track your gym visits - + + + {activeCheckIn ? ( + + Currently Checked In + + Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + Check Out + + + ) : ( + + Check In + + )} + + + Recent History + {history.map((item) => ( + + + + {new Date(item.checkInTime).toLocaleDateString()} + + {item.type.toUpperCase()} + + + + In: {new Date(item.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + {item.checkOutTime && ( + + Out: {new Date(item.checkOutTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + )} + + + ))} + ) } const styles = StyleSheet.create({ container: { flex: 1, - justifyContent: 'center', - alignItems: 'center', backgroundColor: '#f5f5f5', }, + content: { + padding: 20, + }, + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, title: { - fontSize: 24, + fontSize: 28, fontWeight: 'bold', - marginBottom: 8, + marginBottom: 4, + color: '#1a1a1a', }, subtitle: { fontSize: 16, color: '#666', + marginBottom: 24, + }, + actionContainer: { + marginBottom: 32, + }, + checkInButton: { + backgroundColor: '#000', + paddingVertical: 16, + borderRadius: 12, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + checkOutButton: { + backgroundColor: '#ff3b30', + paddingVertical: 16, + borderRadius: 12, + alignItems: 'center', + marginTop: 16, + }, + buttonText: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + }, + activeCard: { + backgroundColor: '#fff', + padding: 20, + borderRadius: 16, + borderWidth: 1, + borderColor: '#e0e0e0', + }, + activeText: { + fontSize: 18, + fontWeight: '600', + color: '#2e7d32', + marginBottom: 4, + }, + timeText: { + fontSize: 14, + color: '#666', + }, + sectionTitle: { + fontSize: 20, + fontWeight: '600', + marginBottom: 16, + color: '#1a1a1a', + }, + historyItem: { + backgroundColor: '#fff', + padding: 16, + borderRadius: 12, + marginBottom: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 2, + }, + dateText: { + fontSize: 16, + fontWeight: '500', + color: '#1a1a1a', + }, + typeText: { + fontSize: 12, + color: '#666', + marginTop: 2, + }, + timeContainer: { + alignItems: 'flex-end', + }, + historyTime: { + fontSize: 14, + color: '#444', }, }) \ No newline at end of file diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index 45937d6..0ecc78c 100644 --- a/apps/mobile/src/config/api.ts +++ b/apps/mobile/src/config/api.ts @@ -1,5 +1,5 @@ -export const API_BASE_URL = __DEV__ - ? 'http://192.168.1.24:3000' +export const API_BASE_URL = __DEV__ + ? 'https://5cb23f31d8c1.ngrok-free.app' : 'https://your-production-url.com' export const API_ENDPOINTS = { @@ -12,4 +12,9 @@ export const API_ENDPOINTS = { }, CLIENTS: '/api/clients', USERS: '/api/users', + ATTENDANCE: { + CHECK_IN: '/api/attendance/check-in', + CHECK_OUT: '/api/attendance/check-out', + HISTORY: '/api/attendance/history', + }, } \ No newline at end of file diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 9dc08d8..a0c1326 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -8,7 +8,7 @@ "lib": [ "es2017" ], - "moduleResolution": "node", + "moduleResolution": "bundler", "noEmit": true, "strict": true, "target": "esnext", @@ -35,4 +35,4 @@ "jest.config.js" ], "extends": "expo/tsconfig.base" -} +} \ No newline at end of file