manual check in/check out
implemented, to be implemented: qrcode or/and geofence
This commit is contained in:
1 parent
33a698066f
commit
39021dca35
18 files changed
+650
-72
No files matched your search
@@ -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[] = []
|
||||
@@ -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<void> {
|
||||
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<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
|
||||
async createUser(userData: Omit<User, 'createdAt' | 'updatedAt'>): Promise<User> {
|
||||
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<Attendance> {
|
||||
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<Attendance | null> {
|
||||
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<Attendance | null> {
|
||||
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<Attendance[]> {
|
||||
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<Attendance[]> {
|
||||
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<Attendance | null> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void>;
|
||||
|
||||
// User operations
|
||||
createUser(user: Omit<User, "id" | "createdAt" | "updatedAt">): Promise<User>;
|
||||
createUser(user: Omit<User, "createdAt" | "updatedAt">): Promise<User>;
|
||||
getUserById(id: string): Promise<User | null>;
|
||||
getUserByEmail(email: string): Promise<User | null>;
|
||||
getAllUsers(): Promise<User[]>;
|
||||
@@ -67,6 +77,17 @@ export interface IDatabase {
|
||||
updates: Partial<FitnessProfile>,
|
||||
): Promise<FitnessProfile | null>;
|
||||
deleteFitnessProfile(userId: string): Promise<boolean>;
|
||||
|
||||
// Attendance operations
|
||||
checkIn(
|
||||
clientId: string,
|
||||
type: "gym" | "class" | "personal_training",
|
||||
notes?: string,
|
||||
): Promise<Attendance>;
|
||||
checkOut(attendanceId: string): Promise<Attendance | null>;
|
||||
getAttendanceHistory(clientId: string): Promise<Attendance[]>;
|
||||
getAllAttendance(): Promise<Attendance[]>;
|
||||
getActiveCheckIn(clientId: string): Promise<Attendance | null>;
|
||||
}
|
||||
|
||||
// Database configuration
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user