Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
670e6675cc | ||
|
|
cdd854bd70 | ||
|
|
cbb1f52467 | ||
|
|
62e2255b5e | ||
|
|
d7d270510f | ||
|
|
a5c297b911 | ||
|
|
0011c9d4e5 | ||
|
|
db0d2cf215 | ||
|
|
ec370a3c17 | ||
|
|
b0d751540e | ||
|
|
06237579f0 | ||
|
|
c038a54f2e | ||
|
|
b014734f33 | ||
|
|
fe98c13206 | ||
|
|
febcdc111e | ||
|
|
73d2c4c1ed | ||
|
|
fc12cecd30 | ||
|
|
803c205994 | ||
|
|
28b5b52a8f | ||
|
|
684481fd2c | ||
|
|
bf532ba35d | ||
|
|
0896dd46d1 | ||
|
|
e9ba9e2700 | ||
|
|
3ab9908588 | ||
|
|
0183a8ea6a | ||
|
|
5d1d881f16 | ||
|
|
1b50bfd2ee | ||
|
|
6cbed4e8d1 | ||
|
|
76dafe9cd5 | ||
|
|
a96f1902c5 | ||
|
|
8b4cef33dc | ||
|
|
118efad70f | ||
|
|
daaf5aa2dd | ||
|
|
67636120d6 | ||
|
|
892cf1a040 | ||
|
|
4909815807 | ||
|
|
a87b94219d | ||
|
|
b1bb5d8166 | ||
|
|
39021dca35 | ||
|
|
33a698066f | ||
|
|
55ae37f304 | ||
|
|
b9f33fdcc6 | ||
|
|
64bc4aa58b | ||
|
|
3a58d420d6 | ||
|
|
ca790a7b97 |
No files matched your search
@@ -1,117 +0,0 @@
|
||||
# FitAI
|
||||
|
||||
Integrated AI solution for fitness houses and their clients.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
fitai/
|
||||
├── apps/
|
||||
│ ├── admin/ # Next.js admin dashboard
|
||||
│ └── mobile/ # React Native mobile app (Expo)
|
||||
├── packages/
|
||||
│ └── shared/ # Shared types and utilities
|
||||
└── AGENTS.md # Development guidelines
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js >= 18.0.0
|
||||
- npm >= 9.0.0
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Install root dependencies
|
||||
npm install
|
||||
|
||||
# Install admin dependencies
|
||||
cd apps/admin && npm install
|
||||
|
||||
# Install mobile dependencies
|
||||
cd apps/mobile && npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start both apps together
|
||||
npm run dev
|
||||
|
||||
# Or start individually:
|
||||
# Admin dashboard (http://localhost:3000)
|
||||
cd apps/admin && npm run dev
|
||||
|
||||
# Mobile app (http://localhost:8081) - Requires Expo SDK 54
|
||||
cd apps/mobile && npm start
|
||||
```
|
||||
|
||||
### Mobile App Setup
|
||||
- **Expo SDK**: 50 (stable, compatible with Expo Go)
|
||||
- **Assets**: Placeholder icons and splash screen included
|
||||
- **Navigation**: Expo Router with tab-based layout
|
||||
- **Authentication**: Secure storage with expo-secure-store
|
||||
- **Babel**: babel-preset-expo for proper transpilation
|
||||
|
||||
### Known Compatibility Notes
|
||||
- Use Expo Go with SDK 50 for mobile testing
|
||||
- For SDK 54, upgrade all dependencies to latest versions
|
||||
- Current setup prioritizes stability over latest features
|
||||
|
||||
### Build & Test
|
||||
```bash
|
||||
# Build all apps
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Admin Dashboard
|
||||
- Client management
|
||||
- Payment tracking
|
||||
- Attendance monitoring
|
||||
- Data visualization
|
||||
|
||||
### Mobile App
|
||||
- Client profile management
|
||||
- Attendance tracking
|
||||
- Payment notifications
|
||||
- Fitness progress tracking
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Admin**: Next.js 14, React 18, TypeScript, Tailwind CSS
|
||||
- **Mobile**: React Native, Expo Router, TypeScript
|
||||
- **Shared**: TypeScript, Zod for validation
|
||||
- **State Management**: React Query, React Hook Form
|
||||
- **Data Grid**: AG Grid for advanced user management
|
||||
- **Charts**: AG Charts for analytics and visualization
|
||||
|
||||
## Features
|
||||
|
||||
### Admin Dashboard
|
||||
- **User Management**: Advanced AG Grid with filtering, sorting, pagination
|
||||
- **Analytics**: Interactive charts (line, pie, bar) with AG Charts
|
||||
- **Data Export**: CSV export functionality
|
||||
- **Real-time Updates**: Live user data synchronization
|
||||
- **Responsive Design**: Mobile-first responsive interface
|
||||
|
||||
### Mobile App
|
||||
- **Authentication**: Secure registration and login
|
||||
- **User Profile**: Personal information management
|
||||
- **Protected Routes**: Authentication-based navigation
|
||||
- **Secure Storage**: Encrypted credential storage
|
||||
|
||||
### Data Visualization
|
||||
- **User Growth**: Line chart showing user acquisition over time
|
||||
- **Membership Distribution**: Pie chart of membership types
|
||||
- **Revenue Analytics**: Bar chart for monthly revenue tracking
|
||||
- **Key Metrics**: Real-time KPI dashboard
|
||||
@@ -0,0 +1,3 @@
|
||||
# DeepSeek AI API Key
|
||||
# Get your API key from https://platform.deepseek.com/
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||
@@ -0,0 +1,16 @@
|
||||
# Clerk Authentication
|
||||
# Get these values from https://dashboard.clerk.com
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_bmVlZGVkLWVsZXBoYW50LTY0LmNsZXJrLmFjY291bnRzLmRldiQ
|
||||
CLERK_SECRET_KEY=sk_test_qnWnZSem1ZkodRip9NZDXszDnCP91HwlNwtAUAcHZ1
|
||||
CLERK_WEBHOOK_SECRET=whsec_iYJM17VbGIDQnMX/5VsyjF7egjdlwuXC
|
||||
|
||||
|
||||
|
||||
# Clerk URLs (customize these for your app)
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
|
||||
|
||||
# Database
|
||||
DATABASE_URL=./data/fitai.db
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[Approve] Updating recommendation a2a4d10a-463d-4648-b2a3-3b97fd053078 with: {"status":"approved","approvedAt":"2025-11-24T16:18:30.211Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for a2a4d10a-463d-4648-b2a3-3b97fd053078: {"changes":0,"lastInsertRowid":0}
|
||||
[SQLite] Fetch after update for a2a4d10a-463d-4648-b2a3-3b97fd053078: Not Found
|
||||
[Approve] Result for a2a4d10a-463d-4648-b2a3-3b97fd053078: Not Found
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:21:37.787Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":1}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:22:27.297Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":1}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:24:28.104Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":0}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:24:29.552Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":0}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:25:42.561Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":0}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:25:43.627Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":0}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"approved","approvedAt":"2025-11-24T16:27:02.858Z","approvedBy":"user_35fyZb2QT54VYzLfvx1cSIdcnKI"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":2}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
[Approve] Updating recommendation fdacfa23-0116-452b-a40e-671583a6ce60 with: {"status":"rejected"}
|
||||
[SQLite] Update result for fdacfa23-0116-452b-a40e-671583a6ce60: {"changes":1,"lastInsertRowid":2}
|
||||
[SQLite] Fetch after update for fdacfa23-0116-452b-a40e-671583a6ce60: Found
|
||||
[Approve] Result for fdacfa23-0116-452b-a40e-671583a6ce60: Success
|
||||
Whitespace-only changes.
@@ -1,10 +1,11 @@
|
||||
const { pathsToModuleNameMapper } = require('ts-jest');
|
||||
const { compilerOptions } = require('./tsconfig');
|
||||
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
moduleNameMapping: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.(ts|tsx|js)',
|
||||
'**/*.(test|spec).(ts|tsx|js)',
|
||||
|
||||
@@ -1 +1 @@
|
||||
import '@testing-library/jest-dom'
|
||||
require('@testing-library/jest-dom')
|
||||
Generated
+2107
-1065
File diff suppressed because it is too large.
Load diff
+18
-4
@@ -11,9 +11,11 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^6.34.5",
|
||||
"@fitai/database": "file:../../packages/database",
|
||||
"@fitai/shared": "file:../../packages/shared",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/sqlite3": "^5.1.0",
|
||||
@@ -21,30 +23,42 @@
|
||||
"ag-charts-react": "^12.3.1",
|
||||
"ag-grid-community": "^34.3.1",
|
||||
"ag-grid-react": "^34.3.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "12.4.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"lucide-react": "^0.553.0",
|
||||
"next": "^16.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.4.31",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"recharts": "^3.3.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"svix": "^1.81.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import { getDatabase } from '../src/lib/database';
|
||||
import { DrizzleDatabase } from '../src/lib/database/drizzle';
|
||||
|
||||
async function checkUserRole() {
|
||||
const db = await getDatabase();
|
||||
const users = await db.getAllUsers();
|
||||
console.log('Users found:', users.length);
|
||||
users.forEach(u => {
|
||||
console.log(`User: ${u.email}, Role: ${u.role}, ID: ${u.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
checkUserRole().catch(console.error);
|
||||
@@ -0,0 +1,49 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '../data/fitai.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log('Creating fitness_goals table...');
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS fitness_goals (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
fitness_profile_id TEXT,
|
||||
goal_type TEXT NOT NULL CHECK(goal_type IN ('weight_target', 'strength_milestone', 'endurance_target', 'flexibility_goal', 'habit_building', 'custom')),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
target_value REAL,
|
||||
current_value REAL,
|
||||
unit TEXT,
|
||||
start_date INTEGER NOT NULL,
|
||||
target_date INTEGER,
|
||||
completed_date INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned', 'paused')),
|
||||
progress REAL DEFAULT 0,
|
||||
priority TEXT DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high')),
|
||||
notes TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles(userId) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes for better query performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_fitness_goals_user_id ON fitness_goals(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fitness_goals_status ON fitness_goals(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_fitness_goals_profile_id ON fitness_goals(fitness_profile_id);
|
||||
`);
|
||||
|
||||
console.log('✅ fitness_goals table created successfully');
|
||||
console.log('✅ Indexes created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
import { getDatabase } from '../src/lib/database';
|
||||
|
||||
async function makeAdmin(email: string) {
|
||||
const db = await getDatabase();
|
||||
const user = await db.getUserByEmail(email);
|
||||
if (!user) {
|
||||
console.log('User not found');
|
||||
return;
|
||||
}
|
||||
await db.updateUser(user.id, { role: 'admin' });
|
||||
console.log(`User ${email} updated to admin`);
|
||||
}
|
||||
|
||||
makeAdmin('taratur@gmail.com').catch(console.error);
|
||||
@@ -0,0 +1,33 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '../data/fitai.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log('Migrating fitness_profiles table...');
|
||||
|
||||
try {
|
||||
// Check if columns exist
|
||||
const tableInfo = db.prepare('PRAGMA table_info(fitness_profiles)').all();
|
||||
const columns = tableInfo.map(c => c.name);
|
||||
|
||||
if (!columns.includes('allergies')) {
|
||||
console.log('Adding allergies column...');
|
||||
db.prepare('ALTER TABLE fitness_profiles ADD COLUMN allergies TEXT').run();
|
||||
} else {
|
||||
console.log('allergies column already exists.');
|
||||
}
|
||||
|
||||
if (!columns.includes('injuries')) {
|
||||
console.log('Adding injuries column...');
|
||||
db.prepare('ALTER TABLE fitness_profiles ADD COLUMN injuries TEXT').run();
|
||||
} else {
|
||||
console.log('injuries column already exists.');
|
||||
}
|
||||
|
||||
console.log('Migration completed successfully.');
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'fitai.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
try {
|
||||
console.log('Attempting to add lastVisit column to clients table...');
|
||||
db.exec('ALTER TABLE clients ADD COLUMN lastVisit DATETIME');
|
||||
console.log('Successfully added lastVisit column.');
|
||||
} catch (error) {
|
||||
if (error.message.includes('duplicate column name')) {
|
||||
console.log('Column lastVisit already exists.');
|
||||
} else {
|
||||
console.error('Error adding column:', error);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
@@ -0,0 +1,62 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '../data/fitai.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
function migrateRoles() {
|
||||
try {
|
||||
console.log('Starting migration to update role constraints...');
|
||||
|
||||
// 1. Disable foreign keys
|
||||
db.pragma('foreign_keys = OFF');
|
||||
|
||||
// 2. Start transaction
|
||||
db.transaction(() => {
|
||||
// 3. Create new table with updated check constraint
|
||||
console.log('Creating new users table...');
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
firstName TEXT NOT NULL,
|
||||
lastName TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')),
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
// 4. Copy data
|
||||
console.log('Copying data...');
|
||||
db.prepare(`
|
||||
INSERT INTO users_new (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
|
||||
SELECT id, email, firstName, lastName, password, phone, role, createdAt, updatedAt FROM users
|
||||
`).run();
|
||||
|
||||
// 5. Drop old table
|
||||
console.log('Dropping old table...');
|
||||
db.prepare('DROP TABLE users').run();
|
||||
|
||||
// 6. Rename new table
|
||||
console.log('Renaming new table...');
|
||||
db.prepare('ALTER TABLE users_new RENAME TO users').run();
|
||||
|
||||
// 7. Re-enable foreign keys (in a separate step usually, but good to be safe)
|
||||
})();
|
||||
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
console.log('Migration completed successfully.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrateRoles();
|
||||
@@ -0,0 +1,51 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const dbPath = path.join(__dirname, '../data/fitai.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
async function seedSuperAdmin() {
|
||||
const email = 'taratur@gmail.com';
|
||||
const password = 'password123';
|
||||
const firstName = 'Super';
|
||||
const lastName = 'Admin';
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
const id = 'user_superadmin_' + Math.random().toString(36).substr(2, 9);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
console.log('Creating Super Admin...');
|
||||
|
||||
// Check if exists
|
||||
const existing = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
|
||||
if (existing) {
|
||||
console.log('Super Admin already exists. Updating role...');
|
||||
db.prepare('UPDATE users SET role = "superAdmin" WHERE email = ?').run(email);
|
||||
console.log('Role updated.');
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO users (id, email, firstName, lastName, password, role, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, email, firstName, lastName, hashedPassword, 'superAdmin', now, now);
|
||||
|
||||
console.log(`Super Admin created successfully.`);
|
||||
console.log(`Email: ${email}`);
|
||||
console.log(`Password: ${password}`);
|
||||
console.log(`ID: ${id}`);
|
||||
|
||||
console.log('\nIMPORTANT: You must also create this user in Clerk manually or sign up with this email to link the accounts if you want to log in as this user.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating Super Admin:', error);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
seedSuperAdmin();
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
import 'dotenv/config';
|
||||
import { getDatabase } from '../src/lib/database';
|
||||
import { clerkClient } from '@clerk/nextjs/server';
|
||||
|
||||
async function syncAllUsers() {
|
||||
console.log('Starting full user sync...');
|
||||
const db = await getDatabase();
|
||||
|
||||
// Fetch all users from Clerk
|
||||
// Note: pagination might be needed for large user bases, but for prototype this is fine.
|
||||
const client = await clerkClient();
|
||||
const response = await client.users.getUserList({ limit: 100 });
|
||||
const clerkUsers = response.data;
|
||||
|
||||
console.log(`Found ${clerkUsers.length} users in Clerk.`);
|
||||
|
||||
for (const clerkUser of clerkUsers) {
|
||||
const userId = clerkUser.id;
|
||||
const email = clerkUser.emailAddresses[0]?.emailAddress;
|
||||
|
||||
if (!email) {
|
||||
console.warn(`User ${userId} has no email, skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingUser = await db.getUserById(userId);
|
||||
if (existingUser) {
|
||||
console.log(`User ${email} (${userId}) already exists.`);
|
||||
// Optional: Update details if needed
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check by email to handle ID mismatches (e.g. from seeding)
|
||||
const existingByEmail = await db.getUserByEmail(email);
|
||||
if (existingByEmail) {
|
||||
console.log(`User ${email} found with old ID ${existingByEmail.id}. Migrating to ${userId}...`);
|
||||
await db.migrateUserId(existingByEmail.id, userId);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Creating new user ${email} (${userId})...`);
|
||||
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' | 'superAdmin') || 'client'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Sync complete.');
|
||||
}
|
||||
|
||||
syncAllUsers().catch(console.error);
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
import { getDatabase } from '../src/lib/database/index.ts';
|
||||
|
||||
async function verifyDatabase() {
|
||||
console.log('Starting database verification...');
|
||||
const db = await getDatabase();
|
||||
await db.connect();
|
||||
|
||||
try {
|
||||
// 1. Create User
|
||||
console.log('Creating test user...');
|
||||
const userId = `test-user-${Date.now()}`;
|
||||
const user = await db.createUser({
|
||||
id: userId,
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
password: 'password123',
|
||||
role: 'client',
|
||||
phone: '1234567890'
|
||||
});
|
||||
console.log('User created:', user.id);
|
||||
|
||||
// 2. Create Client
|
||||
console.log('Creating test client...');
|
||||
const client = await db.createClient({
|
||||
userId: user.id,
|
||||
membershipType: 'basic',
|
||||
membershipStatus: 'active',
|
||||
joinDate: new Date()
|
||||
});
|
||||
console.log('Client created:', client.id);
|
||||
|
||||
// 3. Create Fitness Profile
|
||||
console.log('Creating fitness profile...');
|
||||
const profile = await db.createFitnessProfile({
|
||||
userId: user.id,
|
||||
height: '180',
|
||||
weight: '75',
|
||||
age: '30',
|
||||
gender: 'male',
|
||||
activityLevel: 'moderately_active',
|
||||
fitnessGoals: ['weight_loss'],
|
||||
exerciseHabits: 'None',
|
||||
dietHabits: 'None',
|
||||
medicalConditions: 'None'
|
||||
});
|
||||
console.log('Fitness profile created for:', profile.userId);
|
||||
|
||||
// 4. Attendance Check-in
|
||||
console.log('Checking in...');
|
||||
const checkIn = await db.checkIn(user.id, 'gym', 'Test check-in');
|
||||
console.log('Checked in:', checkIn.id);
|
||||
|
||||
// 5. Verify Active Check-in
|
||||
const activeCheckIn = await db.getActiveCheckIn(user.id);
|
||||
if (!activeCheckIn || activeCheckIn.id !== checkIn.id) {
|
||||
throw new Error('Active check-in verification failed');
|
||||
}
|
||||
console.log('Active check-in verified');
|
||||
|
||||
// 6. Attendance Check-out
|
||||
console.log('Checking out...');
|
||||
const checkOut = await db.checkOut(checkIn.id);
|
||||
console.log('Checked out:', checkOut?.checkOutTime);
|
||||
|
||||
// 7. Cleanup
|
||||
console.log('Cleaning up...');
|
||||
await db.deleteUser(user.id);
|
||||
console.log('Cleanup complete');
|
||||
|
||||
console.log('✅ Verification successful!');
|
||||
} catch (error) {
|
||||
console.error('❌ Verification failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
verifyDatabase();
|
||||
@@ -0,0 +1,27 @@
|
||||
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) {
|
||||
try {
|
||||
const { userId } = await auth()
|
||||
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
|
||||
|
||||
const db = await getDatabase()
|
||||
|
||||
// Ensure user is synced (handles seed script ID mismatch)
|
||||
// We need to import ensureUserSynced
|
||||
const user = await ensureUserSynced(userId, db)
|
||||
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { setUserRole, isAdmin, type UserRole } from '@/lib/clerk-helpers';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
// Authenticate the request
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if the requesting user is an admin
|
||||
const requestingUserIsAdmin = await isAdmin(userId);
|
||||
|
||||
if (!requestingUserIsAdmin) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Forbidden: Admin access required' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body = await req.json();
|
||||
const { targetUserId, role } = body;
|
||||
|
||||
// Validate inputs
|
||||
if (!targetUserId || typeof targetUserId !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid or missing targetUserId' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!role || !['admin', 'trainer', 'client'].includes(role)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid role. Must be admin, trainer, or client' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent admin from changing their own role
|
||||
if (userId === targetUserId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot change your own role' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set the user's role
|
||||
const updatedUser = await setUserRole(targetUserId, role as UserRole);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `User role updated to ${role}`,
|
||||
user: {
|
||||
id: updatedUser.id,
|
||||
email: updatedUser.emailAddresses[0]?.emailAddress,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
role: updatedUser.publicMetadata.role,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting user role:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('not found')) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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() {
|
||||
try {
|
||||
const { userId } = await auth()
|
||||
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
|
||||
|
||||
const db = await getDatabase()
|
||||
const user = await ensureUserSynced(userId, db)
|
||||
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) {
|
||||
return new NextResponse('Forbidden', { status: 403 })
|
||||
}
|
||||
|
||||
const stats = await db.getDashboardStats()
|
||||
|
||||
return NextResponse.json(stats)
|
||||
} catch (error) {
|
||||
console.error('Dashboard stats error:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
import { POST as checkIn } from '../check-in/route'
|
||||
import { POST as checkOut } from '../check-out/route'
|
||||
import { GET as history } from '../history/route'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@clerk/nextjs/server', () => ({
|
||||
auth: jest.fn(() => Promise.resolve({ userId: 'test_user_id' })),
|
||||
currentUser: jest.fn(() => Promise.resolve({ id: 'test_user_id', emailAddresses: [{ emailAddress: 'test@example.com' }] }))
|
||||
}))
|
||||
|
||||
jest.mock('@/lib/sync-user', () => ({
|
||||
ensureUserSynced: jest.fn()
|
||||
}))
|
||||
|
||||
const mockDb = {
|
||||
checkIn: jest.fn(),
|
||||
checkOut: jest.fn(),
|
||||
getAttendanceHistory: jest.fn(),
|
||||
getActiveCheckIn: jest.fn(),
|
||||
getUserById: jest.fn(),
|
||||
createUser: jest.fn(),
|
||||
getClientByUserId: jest.fn(),
|
||||
createClient: jest.fn(),
|
||||
getFitnessProfileByUserId: jest.fn(),
|
||||
createFitnessProfile: jest.fn(),
|
||||
}
|
||||
|
||||
jest.mock('@/lib/database', () => ({
|
||||
getDatabase: jest.fn(() => Promise.resolve(mockDb))
|
||||
}))
|
||||
|
||||
describe('Attendance API', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('POST /api/attendance/check-in', () => {
|
||||
it('should successfully check in', async () => {
|
||||
mockDb.getUserById.mockResolvedValue({ id: 'test_user_id' })
|
||||
mockDb.getActiveCheckIn.mockResolvedValue(null)
|
||||
mockDb.checkIn.mockResolvedValue({
|
||||
id: 'attendance_id',
|
||||
userId: 'test_user_id',
|
||||
checkInTime: new Date(),
|
||||
type: 'gym'
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost/api/attendance/check-in', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'gym', notes: 'Test check-in' })
|
||||
})
|
||||
|
||||
const res = await checkIn(req)
|
||||
const data = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(data.id).toBe('attendance_id')
|
||||
expect(data.userId).toBe('test_user_id')
|
||||
expect(mockDb.checkIn).toHaveBeenCalledWith('test_user_id', 'gym', 'Test check-in')
|
||||
})
|
||||
|
||||
it('should fail if already checked in', async () => {
|
||||
mockDb.getUserById.mockResolvedValue({ id: 'test_user_id' })
|
||||
mockDb.getActiveCheckIn.mockResolvedValue({ id: 'existing_id' })
|
||||
|
||||
const req = new NextRequest('http://localhost/api/attendance/check-in', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'gym' })
|
||||
})
|
||||
|
||||
const res = await checkIn(req)
|
||||
const text = await res.text()
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
expect(text).toBe('Already checked in')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/attendance/check-out', () => {
|
||||
it('should successfully check out', async () => {
|
||||
mockDb.getActiveCheckIn.mockResolvedValue({ id: 'attendance_id' })
|
||||
mockDb.checkOut.mockResolvedValue({
|
||||
id: 'attendance_id',
|
||||
checkOutTime: new Date()
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost/api/attendance/check-out', {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
const res = await checkOut(req)
|
||||
const data = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(data.id).toBe('attendance_id')
|
||||
expect(data.checkOutTime).toBeDefined()
|
||||
expect(mockDb.checkOut).toHaveBeenCalledWith('attendance_id')
|
||||
})
|
||||
|
||||
it('should fail if not checked in', async () => {
|
||||
mockDb.getActiveCheckIn.mockResolvedValue(null)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/attendance/check-out', {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
const res = await checkOut(req)
|
||||
const text = await res.text()
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
expect(text).toBe('No active check-in found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/attendance/history', () => {
|
||||
it('should return attendance history', async () => {
|
||||
const historyData = [
|
||||
{ id: '1', checkInTime: new Date() },
|
||||
{ id: '2', checkInTime: new Date() }
|
||||
]
|
||||
mockDb.getUserById.mockResolvedValue({ id: 'test_user_id' })
|
||||
mockDb.getAttendanceHistory.mockResolvedValue(historyData)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/attendance/history')
|
||||
const res = await history(req)
|
||||
const data = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(data).toEqual(JSON.parse(JSON.stringify(historyData))) // Handle date serialization
|
||||
expect(mockDb.getAttendanceHistory).toHaveBeenCalledWith('test_user_id')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
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)
|
||||
|
||||
// Check if already checked in
|
||||
const activeCheckIn = await db.getActiveCheckIn(userId)
|
||||
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(userId, type, notes)
|
||||
return NextResponse.json(attendance)
|
||||
} catch (error) {
|
||||
console.error('Check-in error:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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 activeCheckIn = await db.getActiveCheckIn(userId)
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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)
|
||||
|
||||
const history = await db.getAttendanceHistory(userId)
|
||||
return NextResponse.json(history)
|
||||
} catch (error) {
|
||||
console.error('History error:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { getDatabase } from '@/lib/database';
|
||||
|
||||
// POST - Mark goal as complete
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
|
||||
// Verify goal exists and user owns it
|
||||
const existingGoal = await db.getFitnessGoalById(id);
|
||||
if (!existingGoal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
if (existingGoal.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
const completedGoal = await db.completeGoal(id);
|
||||
|
||||
return NextResponse.json(completedGoal);
|
||||
} catch (error) {
|
||||
console.error('Error completing fitness goal:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { getDatabase } from '@/lib/database';
|
||||
|
||||
// GET - Get specific goal
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
|
||||
const goal = await db.getFitnessGoalById(id);
|
||||
|
||||
if (!goal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (goal.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json(goal);
|
||||
} catch (error) {
|
||||
console.error('Error fetching fitness goal:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update goal
|
||||
export async function PUT(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
|
||||
// Verify goal exists and user owns it
|
||||
const existingGoal = await db.getFitnessGoalById(id);
|
||||
if (!existingGoal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
if (existingGoal.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updates = await req.json();
|
||||
|
||||
// Don't allow changing userId or id
|
||||
delete updates.userId;
|
||||
delete updates.id;
|
||||
delete updates.createdAt;
|
||||
|
||||
const updatedGoal = await db.updateFitnessGoal(id, updates);
|
||||
|
||||
return NextResponse.json(updatedGoal);
|
||||
} catch (error) {
|
||||
console.error('Error updating fitness goal:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Delete goal
|
||||
export async function DELETE(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
|
||||
// Verify goal exists and user owns it
|
||||
const existingGoal = await db.getFitnessGoalById(id);
|
||||
if (!existingGoal) {
|
||||
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||
}
|
||||
if (existingGoal.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const deleted = await db.deleteFitnessGoal(id);
|
||||
|
||||
if (deleted) {
|
||||
return NextResponse.json({ success: true });
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Failed to delete goal' }, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting fitness goal:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { getDatabase } from '@/lib/database';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
// GET - List user's fitness goals
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const targetUserId = searchParams.get('userId') || userId;
|
||||
const status = searchParams.get('status'); // active, completed, all
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Fetch goals with optional status filter
|
||||
const goals = await db.getFitnessGoalsByUserId(
|
||||
targetUserId,
|
||||
status && status !== 'all' ? status : undefined
|
||||
);
|
||||
|
||||
return NextResponse.json(goals);
|
||||
} catch (error) {
|
||||
console.error('Error fetching fitness goals:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create new fitness goal
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const {
|
||||
goalType,
|
||||
title,
|
||||
description,
|
||||
targetValue,
|
||||
currentValue,
|
||||
unit,
|
||||
targetDate,
|
||||
priority,
|
||||
notes,
|
||||
fitnessProfileId
|
||||
} = body;
|
||||
|
||||
// Validation
|
||||
if (!goalType || !title) {
|
||||
return NextResponse.json(
|
||||
{ error: 'goalType and title are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Create the goal
|
||||
const goal = await db.createFitnessGoal({
|
||||
id: randomBytes(16).toString('hex'),
|
||||
userId,
|
||||
fitnessProfileId: fitnessProfileId || undefined,
|
||||
goalType,
|
||||
title,
|
||||
description: description || undefined,
|
||||
targetValue: targetValue || undefined,
|
||||
currentValue: currentValue || 0,
|
||||
unit: unit || undefined,
|
||||
startDate: new Date(),
|
||||
targetDate: targetDate ? new Date(targetDate) : undefined,
|
||||
status: 'active',
|
||||
progress: 0,
|
||||
priority: priority || 'medium',
|
||||
notes: notes || undefined
|
||||
});
|
||||
|
||||
return NextResponse.json(goal, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Error creating fitness goal:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import Database from "better-sqlite3";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const db = new Database("./data/fitai.db");
|
||||
|
||||
// GET - Fetch fitness profile for the authenticated user
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const profile = db
|
||||
.prepare(
|
||||
`SELECT * FROM fitness_profiles WHERE userId = ?`
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
if (profile) {
|
||||
const p = profile as any;
|
||||
// Parse JSON fields
|
||||
try {
|
||||
p.fitnessGoals = JSON.parse(p.fitnessGoals || '[]');
|
||||
} catch (e) {
|
||||
p.fitnessGoals = [];
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ profile: profile || null });
|
||||
} catch (error) {
|
||||
console.error("Error fetching fitness profile:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch fitness profile" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create or update fitness profile for the authenticated user
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
height,
|
||||
weight,
|
||||
age,
|
||||
gender,
|
||||
fitnessGoals, // Changed from fitnessGoal
|
||||
activityLevel,
|
||||
medicalConditions,
|
||||
allergies,
|
||||
injuries,
|
||||
exerciseHabits,
|
||||
dietHabits
|
||||
} = body;
|
||||
|
||||
// Check if profile exists
|
||||
const existingProfile = db
|
||||
.prepare(`SELECT userId FROM fitness_profiles WHERE userId = ?`)
|
||||
.get(userId) as { userId: string } | undefined;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const fitnessGoalsJson = JSON.stringify(fitnessGoals || []);
|
||||
|
||||
if (existingProfile) {
|
||||
// Update existing profile
|
||||
db.prepare(
|
||||
`UPDATE fitness_profiles
|
||||
SET height = ?,
|
||||
weight = ?,
|
||||
age = ?,
|
||||
gender = ?,
|
||||
fitnessGoals = ?,
|
||||
activityLevel = ?,
|
||||
medicalConditions = ?,
|
||||
allergies = ?,
|
||||
injuries = ?,
|
||||
exerciseHabits = ?,
|
||||
dietHabits = ?,
|
||||
updatedAt = ?
|
||||
WHERE userId = ?`
|
||||
).run(
|
||||
height || null,
|
||||
weight || null,
|
||||
age || null,
|
||||
gender || null,
|
||||
fitnessGoalsJson,
|
||||
activityLevel || null,
|
||||
medicalConditions || null,
|
||||
allergies || null,
|
||||
injuries || null,
|
||||
exerciseHabits || null,
|
||||
dietHabits || null,
|
||||
now,
|
||||
userId
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
message: "Fitness profile updated successfully",
|
||||
userId: userId,
|
||||
});
|
||||
} else {
|
||||
// Create new profile
|
||||
db.prepare(
|
||||
`INSERT INTO fitness_profiles
|
||||
(userId, height, weight, age, gender, fitnessGoals, activityLevel,
|
||||
medicalConditions, allergies, injuries, exerciseHabits, dietHabits, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
userId,
|
||||
height || null,
|
||||
weight || null,
|
||||
age || null,
|
||||
gender || null,
|
||||
fitnessGoalsJson,
|
||||
activityLevel || null,
|
||||
medicalConditions || null,
|
||||
allergies || null,
|
||||
injuries || null,
|
||||
exerciseHabits || null,
|
||||
dietHabits || null,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Fitness profile created successfully",
|
||||
userId,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving fitness profile:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to save fitness profile" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() })
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { recommendationId, status, approvedBy } = await req.json();
|
||||
|
||||
if (!recommendationId || !status) {
|
||||
return NextResponse.json(
|
||||
{ error: "Recommendation ID and status are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Update recommendation status
|
||||
const updates: any = {
|
||||
status,
|
||||
approvedAt: status === "approved" ? new Date() : undefined,
|
||||
approvedBy: status === "approved" ? approvedBy : undefined,
|
||||
};
|
||||
|
||||
// Remove undefined keys
|
||||
Object.keys(updates).forEach(key => updates[key] === undefined && delete updates[key]);
|
||||
|
||||
const updatedRecommendation = await db.updateRecommendation(recommendationId, updates);
|
||||
|
||||
if (!updatedRecommendation) {
|
||||
return NextResponse.json(
|
||||
{ error: "Recommendation not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// If approved, create a notification for the user
|
||||
// Note: IDatabase doesn't have createNotification yet, so we'll skip it for now
|
||||
// or we need to add it to IDatabase/SQLiteDatabase
|
||||
// For now, let's assume the notification is handled elsewhere or add it later
|
||||
|
||||
return NextResponse.json(updatedRecommendation);
|
||||
} catch (error) {
|
||||
console.error("Error approving recommendation:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { buildAIContext } from "@/lib/ai/ai-context";
|
||||
import { buildEnhancedPrompt, buildBasicPrompt } from "@/lib/ai/prompt-builder";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { userId, useExternalModel } = await req.json();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Fetch fitness profile
|
||||
const profile = await db.getFitnessProfileByUserId(userId);
|
||||
|
||||
if (!profile) {
|
||||
return NextResponse.json(
|
||||
{ error: "Fitness profile not found for this user" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build AI context with goals and recommendations
|
||||
let prompt: string;
|
||||
try {
|
||||
const context = await buildAIContext(userId);
|
||||
prompt = buildEnhancedPrompt(context);
|
||||
console.log('Using enhanced AI context with goals and history');
|
||||
} catch (error) {
|
||||
// Fallback to basic prompt if context building fails
|
||||
console.warn('Failed to build AI context, using basic prompt:', error);
|
||||
prompt = buildBasicPrompt(profile);
|
||||
}
|
||||
|
||||
let parsedResponse;
|
||||
|
||||
if (useExternalModel) {
|
||||
// Use DeepSeek AI
|
||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
||||
|
||||
if (!deepseekApiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "DeepSeek API key not configured" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Using DeepSeek AI model...");
|
||||
|
||||
const deepseekResponse = await fetch("https://api.deepseek.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${deepseekApiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "deepseek-chat",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are a professional fitness trainer and nutritionist. Always respond with valid JSON only, no markdown or code blocks."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deepseekResponse.ok) {
|
||||
const errorText = await deepseekResponse.text();
|
||||
console.error("DeepSeek API error:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recommendation from DeepSeek AI" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const deepseekData = await deepseekResponse.json();
|
||||
console.log("Raw DeepSeek Response:", deepseekData);
|
||||
|
||||
try {
|
||||
const content = deepseekData.choices[0].message.content;
|
||||
let cleanResponse = content.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
|
||||
} else if (cleanResponse.startsWith("```")) {
|
||||
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
|
||||
}
|
||||
|
||||
// Find the first '{' and last '}' to extract the JSON object
|
||||
const firstBrace = cleanResponse.indexOf("{");
|
||||
const lastBrace = cleanResponse.lastIndexOf("}");
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
parsedResponse = JSON.parse(cleanResponse);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse DeepSeek response:", deepseekData);
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid response format from DeepSeek AI" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use local Ollama
|
||||
console.log("Using local Ollama model...");
|
||||
|
||||
const ollamaResponse = await fetch("http://localhost:11434/api/generate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "gemma3:latest",
|
||||
prompt: prompt,
|
||||
stream: false,
|
||||
format: "json",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
console.error("Ollama API error:", await ollamaResponse.text());
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recommendation from Ollama" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const aiData = await ollamaResponse.json();
|
||||
console.log("Raw Ollama Response:", aiData.response);
|
||||
|
||||
try {
|
||||
let cleanResponse = aiData.response.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
|
||||
} else if (cleanResponse.startsWith("```")) {
|
||||
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
|
||||
}
|
||||
|
||||
// Find the first '{' and last '}' to extract the JSON object
|
||||
const firstBrace = cleanResponse.indexOf("{");
|
||||
const lastBrace = cleanResponse.lastIndexOf("}");
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
parsedResponse = JSON.parse(cleanResponse);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse Ollama response:", aiData.response);
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid response format from Ollama" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
fitnessProfileId: profile.userId,
|
||||
type: 'ai_plan',
|
||||
content: parsedResponse.recommendationText,
|
||||
activityPlan: parsedResponse.activityPlan,
|
||||
dietPlan: parsedResponse.dietPlan,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
return NextResponse.json(recommendation);
|
||||
} catch (error) {
|
||||
console.error("Error generating recommendation:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { getDatabase } from '@/lib/database'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { userId: currentUserId } = await auth()
|
||||
if (!currentUserId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const targetUserId = searchParams.get('userId')
|
||||
|
||||
const db = await getDatabase()
|
||||
|
||||
// If no userId provided, check if staff and return all recommendations
|
||||
if (!targetUserId) {
|
||||
const currentUser = await db.getUserById(currentUserId)
|
||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||
|
||||
if (!isStaff) {
|
||||
return new NextResponse('User ID is required', { status: 400 })
|
||||
}
|
||||
|
||||
const recommendations = await db.getAllRecommendations()
|
||||
return NextResponse.json({ recommendations })
|
||||
}
|
||||
|
||||
// Check permissions: Users can view their own, Admins/Trainers can view anyone's
|
||||
const currentUser = await db.getUserById(currentUserId)
|
||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||
|
||||
if (currentUserId !== targetUserId) {
|
||||
if (!isStaff) {
|
||||
return new NextResponse('Forbidden', { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
let recommendations = await db.getRecommendationsByUserId(targetUserId)
|
||||
|
||||
// Non-staff users should only see approved recommendations
|
||||
if (!isStaff) {
|
||||
recommendations = recommendations.filter((rec: any) => rec.status === 'approved')
|
||||
}
|
||||
|
||||
return NextResponse.json(recommendations)
|
||||
} catch (error) {
|
||||
console.error('Error fetching recommendations:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { userId: currentUserId } = await auth()
|
||||
if (!currentUserId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const db = await getDatabase()
|
||||
const currentUser = await db.getUserById(currentUserId)
|
||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||
|
||||
if (!isStaff) {
|
||||
return new NextResponse('Forbidden', { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { userId, fitnessProfileId, recommendationText, activityPlan, dietPlan, status, type, content } = body
|
||||
|
||||
// Handle AI Plan (Legacy/Specific)
|
||||
if (recommendationText && activityPlan && dietPlan && fitnessProfileId) {
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
fitnessProfileId,
|
||||
type: 'ai_plan',
|
||||
content: recommendationText,
|
||||
activityPlan,
|
||||
dietPlan,
|
||||
status: status || 'pending'
|
||||
})
|
||||
return NextResponse.json(recommendation)
|
||||
}
|
||||
|
||||
// Handle User Goal (Generic)
|
||||
if (type && content) {
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
type,
|
||||
content,
|
||||
status: status || 'pending'
|
||||
})
|
||||
return NextResponse.json(recommendation)
|
||||
}
|
||||
|
||||
return NextResponse.json('Missing required fields', { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating recommendation:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const { userId: currentUserId } = await auth()
|
||||
if (!currentUserId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { id, status, recommendationText, activityPlan, dietPlan, content } = body
|
||||
|
||||
if (!id) {
|
||||
return new NextResponse('Recommendation ID is required', { status: 400 })
|
||||
}
|
||||
|
||||
const db = await getDatabase()
|
||||
|
||||
const updated = await db.updateRecommendation(id, {
|
||||
...(status && { status }),
|
||||
...(recommendationText && { content: recommendationText }), // Map legacy field
|
||||
...(content && { content }),
|
||||
...(activityPlan && { activityPlan }),
|
||||
...(dietPlan && { dietPlan })
|
||||
})
|
||||
|
||||
return NextResponse.json(updated)
|
||||
} catch (error) {
|
||||
console.error('Error updating recommendation:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "../../../lib/database/index";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -18,7 +19,52 @@ export async function GET(request: NextRequest) {
|
||||
users.map(async (user) => {
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
const client = await db.getClientByUserId(user.id);
|
||||
return { ...userWithoutPassword, client };
|
||||
|
||||
// Get active check-in status and statistics for ALL users
|
||||
let isCheckedIn = false;
|
||||
let checkInTime = null;
|
||||
let lastCheckInTime = null;
|
||||
let checkInsThisWeek = 0;
|
||||
let checkInsThisMonth = 0;
|
||||
|
||||
// Query attendance by userId (works for all user types now)
|
||||
const activeCheckIn = await db.getActiveCheckIn(user.id);
|
||||
if (activeCheckIn) {
|
||||
isCheckedIn = true;
|
||||
checkInTime = activeCheckIn.checkInTime;
|
||||
}
|
||||
|
||||
// Get attendance history for statistics
|
||||
const attendanceHistory = await db.getAttendanceHistory(user.id);
|
||||
|
||||
if (attendanceHistory.length > 0) {
|
||||
// Last check-in is the most recent attendance record
|
||||
lastCheckInTime = attendanceHistory[0].checkInTime;
|
||||
|
||||
// Calculate check-ins in last 7 days
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
checkInsThisWeek = attendanceHistory.filter(
|
||||
a => new Date(a.checkInTime) >= weekAgo
|
||||
).length;
|
||||
|
||||
// Calculate check-ins in last 30 days
|
||||
const monthAgo = new Date();
|
||||
monthAgo.setDate(monthAgo.getDate() - 30);
|
||||
checkInsThisMonth = attendanceHistory.filter(
|
||||
a => new Date(a.checkInTime) >= monthAgo
|
||||
).length;
|
||||
}
|
||||
|
||||
return {
|
||||
...userWithoutPassword,
|
||||
client,
|
||||
isCheckedIn,
|
||||
checkInTime,
|
||||
lastCheckInTime,
|
||||
checkInsThisWeek,
|
||||
checkInsThisMonth
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -34,18 +80,61 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const body = await request.json();
|
||||
const { email, password, firstName, lastName, role, phone } = body;
|
||||
const { userId: clerkUserId } = await auth();
|
||||
if (!clerkUserId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!email || !password || !firstName || !lastName || !role) {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Get current user to check role
|
||||
// Note: In a real app, we'd map Clerk ID to our DB ID.
|
||||
// For now, we'll assume we can find the user by some means or trust the Clerk metadata if we synced it.
|
||||
// Since we don't have Clerk ID in our local DB users table yet (we only have our own ID),
|
||||
// we might need to rely on the user being synced.
|
||||
// Let's assume the user calling this API is already in our DB.
|
||||
// For the prototype, we'll fetch the user by matching the Clerk ID if we stored it,
|
||||
// OR we'll assume the first user is Super Admin if no users exist?
|
||||
// Actually, we should look up the user by email if we can't by ID, or add a clerkId column.
|
||||
// For this step, let's assume we can get the user.
|
||||
|
||||
// WAIT: The current `users` table has `id` as a string. Is it the Clerk ID?
|
||||
// In `sync-user.ts`, we use `evt.data.id` as the `id` when creating the user.
|
||||
// So yes, `users.id` IS the Clerk ID.
|
||||
|
||||
const currentUser = await db.getUserById(clerkUserId);
|
||||
|
||||
if (!currentUser) {
|
||||
return NextResponse.json({ error: "Current user not found in database" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { email, firstName, lastName, role, phone } = body;
|
||||
|
||||
if (!email || !firstName || !lastName || !role) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
// Enforce Hierarchy
|
||||
const allowed: Record<string, string[]> = {
|
||||
superAdmin: ["admin", "trainer", "client"],
|
||||
admin: ["trainer", "client"],
|
||||
trainer: ["client"],
|
||||
client: []
|
||||
};
|
||||
|
||||
const userRole = currentUser.role as keyof typeof allowed;
|
||||
if (!allowed[userRole] || !allowed[userRole].includes(role)) {
|
||||
return NextResponse.json(
|
||||
{ error: `You are not authorized to create a ${role}` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists locally
|
||||
const existingUser = await db.getUserByEmail(email);
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
@@ -54,20 +143,55 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
// Create Clerk Invitation
|
||||
// Note: We pass the role in publicMetadata so it persists when they sign up
|
||||
try {
|
||||
const client = await clerkClient();
|
||||
await client.invitations.createInvitation({
|
||||
emailAddress: email,
|
||||
publicMetadata: {
|
||||
role,
|
||||
},
|
||||
ignoreExisting: true // Don't fail if invite exists
|
||||
});
|
||||
} catch (clerkError: any) {
|
||||
console.error("Clerk invitation error:", clerkError);
|
||||
// If user already exists in Clerk, we might want to handle it.
|
||||
// But for now, let's proceed to create local record if invite sent or if they exist.
|
||||
if (clerkError.errors?.[0]?.code === 'form_identifier_exists') {
|
||||
return NextResponse.json(
|
||||
{ error: "User already exists in Clerk system" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to send invitation: " + (clerkError.message || "Unknown error") },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
// Create user
|
||||
const userId = await db.createUser({
|
||||
// Create user in local DB with temporary ID (will be migrated on first login)
|
||||
// We set a placeholder password since it's required by schema but won't be used
|
||||
const newUserId = await db.createUser({
|
||||
email,
|
||||
password: hashedPassword,
|
||||
password: "INVITED_USER_PENDING",
|
||||
firstName,
|
||||
lastName,
|
||||
role,
|
||||
phone,
|
||||
});
|
||||
|
||||
return NextResponse.json({ userId }, { status: 201 });
|
||||
// If creating a client, create the client record too
|
||||
if (role === 'client') {
|
||||
await db.createClient({
|
||||
userId: newUserId.id,
|
||||
membershipType: 'basic',
|
||||
membershipStatus: 'active',
|
||||
joinDate: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ userId: newUserId.id, message: "Invitation sent" }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Create user error:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { Webhook } from "svix";
|
||||
import { headers } from "next/headers";
|
||||
import { WebhookEvent } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// Get the webhook secret from environment variables
|
||||
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
|
||||
|
||||
if (!WEBHOOK_SECRET) {
|
||||
throw new Error(
|
||||
"Please add CLERK_WEBHOOK_SECRET to your environment variables",
|
||||
);
|
||||
}
|
||||
|
||||
// Get the headers
|
||||
const headerPayload = await headers();
|
||||
const svix_id = headerPayload.get("svix-id");
|
||||
const svix_timestamp = headerPayload.get("svix-timestamp");
|
||||
const svix_signature = headerPayload.get("svix-signature");
|
||||
|
||||
// If there are no headers, error out
|
||||
if (!svix_id || !svix_timestamp || !svix_signature) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing svix headers" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Get the body
|
||||
const payload = await req.json();
|
||||
const body = JSON.stringify(payload);
|
||||
|
||||
// Create a new Svix instance with your webhook secret
|
||||
const wh = new Webhook(WEBHOOK_SECRET);
|
||||
|
||||
let evt: WebhookEvent;
|
||||
|
||||
// Verify the webhook signature
|
||||
try {
|
||||
evt = wh.verify(body, {
|
||||
"svix-id": svix_id,
|
||||
"svix-timestamp": svix_timestamp,
|
||||
"svix-signature": svix_signature,
|
||||
}) as WebhookEvent;
|
||||
} catch (err) {
|
||||
console.error("Error verifying webhook:", err);
|
||||
return NextResponse.json({ error: "Verification failed" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Handle the webhook
|
||||
const eventType = evt.type;
|
||||
console.log(`Received webhook with ID ${evt.data.id} and type ${eventType}`);
|
||||
|
||||
try {
|
||||
// Connect to database directly for webhook operations
|
||||
const dbPath = path.join(process.cwd(), "data", "fitai.db");
|
||||
const db = new Database(dbPath);
|
||||
|
||||
switch (eventType) {
|
||||
case "user.created": {
|
||||
const { id, email_addresses, first_name, last_name, public_metadata } =
|
||||
evt.data;
|
||||
|
||||
// Get primary email
|
||||
const primaryEmail = email_addresses.find(
|
||||
(email) => email.id === evt.data.primary_email_address_id,
|
||||
);
|
||||
|
||||
if (!primaryEmail?.email_address) {
|
||||
console.error("No primary email found for user:", id);
|
||||
db.close();
|
||||
return NextResponse.json(
|
||||
{ error: "No primary email" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Determine role from metadata or default to 'client'
|
||||
const role =
|
||||
(public_metadata?.role as "admin" | "trainer" | "client") || "client";
|
||||
|
||||
// Insert user into database with Clerk's user ID
|
||||
const now = new Date().toISOString();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO users (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
id,
|
||||
primaryEmail.email_address,
|
||||
first_name || "",
|
||||
last_name || "",
|
||||
"", // Clerk handles authentication
|
||||
null, // phone
|
||||
role,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
console.log(`✅ User ${id} created in database`);
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
|
||||
case "user.updated": {
|
||||
const { id, email_addresses, first_name, last_name, public_metadata } =
|
||||
evt.data;
|
||||
|
||||
// Get primary email
|
||||
const primaryEmail = email_addresses.find(
|
||||
(email) => email.id === evt.data.primary_email_address_id,
|
||||
);
|
||||
|
||||
if (!primaryEmail?.email_address) {
|
||||
console.error("No primary email found for user:", id);
|
||||
db.close();
|
||||
return NextResponse.json(
|
||||
{ error: "No primary email" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Determine role from metadata
|
||||
const role =
|
||||
(public_metadata?.role as "admin" | "trainer" | "client") || "client";
|
||||
|
||||
// Update user in database
|
||||
const now = new Date().toISOString();
|
||||
const stmt = db.prepare(`
|
||||
UPDATE users
|
||||
SET email = ?, firstName = ?, lastName = ?, role = ?, updatedAt = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
primaryEmail.email_address,
|
||||
first_name || "",
|
||||
last_name || "",
|
||||
role,
|
||||
now,
|
||||
id,
|
||||
);
|
||||
|
||||
console.log(`✅ User ${id} updated in database`);
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
|
||||
case "user.deleted": {
|
||||
const { id } = evt.data;
|
||||
|
||||
if (!id) {
|
||||
console.error("No user ID provided for deletion");
|
||||
db.close();
|
||||
return NextResponse.json({ error: "No user ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Delete user from database (cascade will handle related records)
|
||||
const stmt = db.prepare("DELETE FROM users WHERE id = ?");
|
||||
stmt.run(id);
|
||||
|
||||
console.log(`✅ User ${id} deleted from database`);
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unhandled webhook event type: ${eventType}`);
|
||||
db.close();
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Webhook processed successfully" },
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error processing webhook:", error);
|
||||
return NextResponse.json({ error: "Processing failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -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<Attendance[]>([])
|
||||
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 <div className="p-8">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<h1 className="text-2xl font-bold mb-6">Attendance Monitoring</h1>
|
||||
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Client ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Check In
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Check Out
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{attendance.map((record) => (
|
||||
<tr key={record.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{record.clientId}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 capitalize">
|
||||
{record.type}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{format(new Date(record.checkInTime), 'PP p')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{record.checkOutTime ? format(new Date(record.checkOutTime), 'PP p') : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${record.checkOutTime
|
||||
? 'bg-gray-100 text-gray-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{record.checkOutTime ? 'Completed' : 'Active'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,39 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import {
|
||||
ClerkProvider,
|
||||
SignedIn,
|
||||
SignedOut,
|
||||
SignInButton,
|
||||
UserButton,
|
||||
} from "@clerk/nextjs";
|
||||
import { Sidebar } from "@/components/ui/Sidebar";
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FitAI Admin',
|
||||
description: 'Fitness management admin dashboard',
|
||||
}
|
||||
title: "FitAI Admin",
|
||||
description: "Fitness management admin dashboard",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
<ClerkProvider>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<div className="flex min-h-screen bg-slate-50">
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-64 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
+90
-55
@@ -1,68 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
|
||||
import { StatsCard } from "@/components/ui/StatsCard";
|
||||
import { UserManagement } from "@/components/users/UserManagement";
|
||||
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||
import axios from "axios";
|
||||
|
||||
interface DashboardStats {
|
||||
totalUsers: number;
|
||||
activeClients: number;
|
||||
totalRevenue: number;
|
||||
revenueGrowth: number;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
totalUsers: 0,
|
||||
activeClients: 0,
|
||||
totalRevenue: 0,
|
||||
revenueGrowth: 0,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await axios.get("/api/admin/stats");
|
||||
setStats(response.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch dashboard stats:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900">
|
||||
FitAI Admin Dashboard
|
||||
</h1>
|
||||
<nav className="flex gap-4">
|
||||
<Link
|
||||
href="/users"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
User Management
|
||||
</Link>
|
||||
<Link
|
||||
href="/analytics"
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-slate-900">Dashboard</h2>
|
||||
<p className="text-slate-500 mt-2">Welcome back, here's what's happening today.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatsCard
|
||||
title="Total Users"
|
||||
value={loading ? "..." : stats.totalUsers}
|
||||
change="+12%" // Placeholder for now as we don't track historical growth yet
|
||||
trend="up"
|
||||
icon={Users}
|
||||
color="blue"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Active Clients"
|
||||
value={loading ? "..." : stats.activeClients}
|
||||
change="+5%"
|
||||
trend="up"
|
||||
icon={CalendarCheck}
|
||||
color="green"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Revenue"
|
||||
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
|
||||
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}%`}
|
||||
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
|
||||
icon={CreditCard}
|
||||
color="purple"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Growth"
|
||||
value="24%" // Placeholder
|
||||
change="-2%"
|
||||
trend="down"
|
||||
icon={TrendingUp}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-6">Recent Activity</h3>
|
||||
<UserManagement />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Client Management</h2>
|
||||
<p className="text-gray-600">
|
||||
Manage fitness clients and their profiles
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Payment Tracking</h2>
|
||||
<p className="text-gray-600">Monitor payments and subscriptions</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Attendance</h2>
|
||||
<p className="text-gray-600">Track client attendance and habits</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-semibold mb-6">
|
||||
Recent User Activity
|
||||
</h2>
|
||||
<div>
|
||||
<UserManagement />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-semibold mb-6">Quick Analytics</h2>
|
||||
<div>
|
||||
<AnalyticsDashboard />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-6">Quick Analytics</h3>
|
||||
<AnalyticsDashboard />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface UserProfile {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { user, isLoaded } = useUser();
|
||||
const [profile, setProfile] = useState<UserProfile>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
phoneNumber: "",
|
||||
});
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded && user) {
|
||||
setProfile({
|
||||
firstName: user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
email: user.emailAddresses[0]?.emailAddress || "",
|
||||
phoneNumber: user.phoneNumbers[0]?.phoneNumber || "",
|
||||
});
|
||||
}
|
||||
}, [isLoaded, user]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await user?.update({
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
});
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isLoaded) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="max-w-2xl mx-auto bg-white p-8 rounded-lg shadow">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Profile</h1>
|
||||
<Button
|
||||
variant={isEditing ? "primary" : "secondary"}
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
>
|
||||
{isEditing ? "Cancel" : "Edit Profile"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">First Name</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={profile.firstName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setProfile({ ...profile, firstName: e.target.value })
|
||||
}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Last Name</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={profile.lastName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setProfile({ ...profile, lastName: e.target.value })
|
||||
}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Email</label>
|
||||
<Input type="email" value={profile.email} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Phone Number
|
||||
</label>
|
||||
<Input type="tel" value={profile.phoneNumber} disabled />
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="pt-4">
|
||||
<Button onClick={handleSave}>Save Changes</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
activityPlan: string;
|
||||
dietPlan: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default function RecommendationsPage() {
|
||||
const { user } = useUser();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [pendingRecommendations, setPendingRecommendations] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState<string | null>(null);
|
||||
const [useExternalModel, setUseExternalModel] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch users
|
||||
const usersRes = await fetch("/api/users");
|
||||
const usersData = await usersRes.json();
|
||||
setUsers(usersData.users || []);
|
||||
|
||||
// Fetch pending recommendations
|
||||
const recsRes = await fetch("/api/recommendations");
|
||||
const recsData = await recsRes.json();
|
||||
const allRecs = recsData.recommendations || [];
|
||||
setPendingRecommendations(allRecs.filter((r: any) => r.status === 'pending'));
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async (userId: string) => {
|
||||
setGenerating(userId);
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId, useExternalModel }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(`Error: ${error.error}`);
|
||||
} else {
|
||||
alert("Recommendation generated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Failed to generate recommendation.");
|
||||
} finally {
|
||||
setGenerating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = async (recommendationId: string, status: "approved" | "rejected") => {
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/approve", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
recommendationId,
|
||||
status,
|
||||
approvedBy: user?.id || "admin",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(`Failed to update status: ${errorData.error || 'Unknown error'}`);
|
||||
} else {
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Error processing request");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (rec: Recommendation) => {
|
||||
const newContent = prompt("Edit Advice:", rec.content);
|
||||
const newActivityPlan = prompt("Edit Activity Plan:", rec.activityPlan);
|
||||
const newDietPlan = prompt("Edit Diet Plan:", rec.dietPlan);
|
||||
|
||||
if (newContent === null || newActivityPlan === null || newDietPlan === null) {
|
||||
// User cancelled one of the prompts
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/recommendations", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: rec.id,
|
||||
content: newContent,
|
||||
activityPlan: newActivityPlan,
|
||||
dietPlan: newDietPlan,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(`Failed to update recommendation: ${errorData.error || 'Unknown error'}`);
|
||||
} else {
|
||||
alert("Recommendation updated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating recommendation:", error);
|
||||
alert("Failed to update recommendation.");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-xl">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">AI Recommendations</h1>
|
||||
|
||||
{/* Model Selection Toggle */}
|
||||
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-lg shadow">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{useExternalModel ? "DeepSeek AI" : "Local Ollama"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setUseExternalModel(!useExternalModel)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${useExternalModel ? "bg-blue-600" : "bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${useExternalModel ? "translate-x-6" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{useExternalModel ? "External" : "Local"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Generate Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Generate Recommendations</h2>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<p className="mb-4 text-gray-600">
|
||||
Select a user to generate a new daily recommendation.
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{users.map((user) => (
|
||||
<li key={user.id} className="flex items-center justify-between border-b pb-2">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{user.firstName} {user.lastName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleGenerate(user.id)}
|
||||
disabled={generating === user.id}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{generating === user.id ? "Generating..." : "Generate"}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<p className="text-gray-500 italic">No users found.</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Approvals Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Pending Approvals</h2>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
{pendingRecommendations.length === 0 ? (
|
||||
<p className="text-gray-500 italic">No pending recommendations.</p>
|
||||
) : (
|
||||
<ul className="space-y-6">
|
||||
{pendingRecommendations.map((rec) => (
|
||||
<li key={rec.id} className="border rounded p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-bold">For: User {rec.userId}</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(rec.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm mb-4">
|
||||
<div>
|
||||
<span className="font-semibold">Advice:</span> {rec.content}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Activity:</span> {rec.activityPlan}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Diet:</span> {rec.dietPlan}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handleEdit(rec)}
|
||||
className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprove(rec.id, "approved")}
|
||||
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprove(rec.id, "rejected")}
|
||||
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { Recommendations } from "@/components/users/Recommendations";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function UserProfilePage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
const user = await db.getUserById(id);
|
||||
|
||||
if (!user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const client = await db.getClientByUserId(user.id);
|
||||
const fitnessProfile = await db.getFitnessProfileByUserId(user.id);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{user.firstName} {user.lastName}
|
||||
</h1>
|
||||
<span className="px-3 py-1 bg-gray-100 rounded-full text-sm font-medium capitalize">
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Basic Info */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Contact Information</h2>
|
||||
<div className="space-y-2">
|
||||
<p><span className="font-medium">Email:</span> {user.email}</p>
|
||||
<p><span className="font-medium">Phone:</span> {user.phone || "N/A"}</p>
|
||||
<p><span className="font-medium">Joined:</span> {user.createdAt.toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Client Info */}
|
||||
{client && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Membership Details</h2>
|
||||
<div className="space-y-2">
|
||||
<p><span className="font-medium">Type:</span> {client.membershipType}</p>
|
||||
<p><span className="font-medium">Status:</span> {client.membershipStatus}</p>
|
||||
<p><span className="font-medium">Member Since:</span> {client.joinDate.toLocaleDateString()}</p>
|
||||
<p><span className="font-medium">Last Visit:</span> {client.lastVisit?.toLocaleDateString() || "Never"}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fitness Profile */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Fitness Profile</h2>
|
||||
{fitnessProfile ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Physical Stats</h3>
|
||||
<p>Height: {fitnessProfile.height} cm</p>
|
||||
<p>Weight: {fitnessProfile.weight} kg</p>
|
||||
<p>Age: {fitnessProfile.age}</p>
|
||||
<p>Gender: {fitnessProfile.gender}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Health & Habits</h3>
|
||||
<p>Activity Level: {fitnessProfile.activityLevel.replace('_', ' ')}</p>
|
||||
<p>Diet: {fitnessProfile.dietHabits || "N/A"}</p>
|
||||
<p>Exercise: {fitnessProfile.exerciseHabits || "N/A"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Medical</h3>
|
||||
<p>Conditions: {fitnessProfile.medicalConditions || "None"}</p>
|
||||
<p>Allergies: {fitnessProfile.allergies || "None"}</p>
|
||||
<p>Injuries: {fitnessProfile.injuries || "None"}</p>
|
||||
</div>
|
||||
<div className="col-span-full mt-4">
|
||||
<h3 className="font-medium mb-2 text-gray-700">Goals</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fitnessProfile.fitnessGoals.map((goal, i) => (
|
||||
<span key={i} className="px-2 py-1 bg-blue-50 text-blue-700 rounded text-sm">
|
||||
{goal.replace('_', ' ')}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 italic">No fitness profile created yet.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recommendations Component */}
|
||||
<Recommendations userId={user.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactElement } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Home, Users, BarChart3, User, Brain } from "lucide-react";
|
||||
import { SignedIn, UserButton } from "@clerk/nextjs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
|
||||
interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
label: "Dashboard",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
href: "/users",
|
||||
label: "Clients",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
href: "/recommendations",
|
||||
label: "AI Recommendations",
|
||||
icon: Brain,
|
||||
},
|
||||
{
|
||||
href: "/analytics",
|
||||
label: "Analytics",
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
href: "/profile",
|
||||
label: "Profile",
|
||||
icon: User,
|
||||
},
|
||||
];
|
||||
|
||||
export function Navigation(): ReactElement {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<nav className="container mx-auto px-4 h-16">
|
||||
<div className="flex h-full items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl font-bold text-[#FF0000] hover:text-[#00FF00] transition-colors"
|
||||
>
|
||||
FitAI
|
||||
</Link>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<ul
|
||||
className="flex-1 flex justify-center items-center"
|
||||
style={{ gap: "3rem" }}
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link href={item.href} className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={pathname === item.href ? "primary" : "secondary"}
|
||||
className={cn(
|
||||
"h-9 px-4 py-2",
|
||||
pathname === item.href &&
|
||||
"bg-primary text-primary-foreground",
|
||||
)}
|
||||
aria-current={pathname === item.href ? "page" : undefined}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* User Button */}
|
||||
<SignedIn>
|
||||
<UserButton
|
||||
afterSignOutUrl="/"
|
||||
appearance={{
|
||||
elements: {
|
||||
avatarBox: "h-8 w-8",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SignedIn>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import { UserGrowthChart } from '@/components/charts/UserGrowthChart'
|
||||
import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart'
|
||||
import { RevenueChart } from '@/components/charts/RevenueChart'
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card'
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/card'
|
||||
|
||||
interface ChartData {
|
||||
label: string
|
||||
@@ -86,7 +86,7 @@ export function AnalyticsDashboard() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="text-center">
|
||||
@@ -95,7 +95,7 @@ export function AnalyticsDashboard() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="text-center">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export function GenerateButton({ userId }: { userId: string }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(`Error: ${error.error}`);
|
||||
} else {
|
||||
alert("Recommendation generated successfully! Check Pending Approvals.");
|
||||
// In a real app, we'd revalidate the path or update state
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Failed to generate recommendation.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={loading}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 flex items-center"
|
||||
>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Generate
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Loader2, Check, X } from "lucide-react";
|
||||
|
||||
type Recommendation = {
|
||||
recommendation: {
|
||||
id: string;
|
||||
recommendationText: string;
|
||||
activityPlan: string;
|
||||
dietPlan: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
user: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function RecommendationList({
|
||||
initialRecommendations,
|
||||
}: {
|
||||
initialRecommendations: Recommendation[];
|
||||
}) {
|
||||
const [recommendations, setRecommendations] = useState(initialRecommendations);
|
||||
const [processingId, setProcessingId] = useState<string | null>(null);
|
||||
|
||||
const handleAction = async (id: string, status: "approved" | "rejected") => {
|
||||
setProcessingId(id);
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/approve", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
recommendationId: id,
|
||||
status,
|
||||
approvedBy: "admin_placeholder", // In real app, get from auth context
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
alert("Failed to update status");
|
||||
} else {
|
||||
setRecommendations((prev) =>
|
||||
prev.filter((item) => item.recommendation.id !== id)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Error processing request");
|
||||
} finally {
|
||||
setProcessingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
return <p className="text-gray-500 italic">No pending recommendations.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-6">
|
||||
{recommendations.map(({ recommendation, user }) => (
|
||||
<li key={recommendation.id} className="border rounded p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-bold">
|
||||
For: {user.firstName} {user.lastName}
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(recommendation.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm mb-4">
|
||||
<div>
|
||||
<span className="font-semibold">Advice:</span>{" "}
|
||||
{recommendation.recommendationText}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Activity:</span>{" "}
|
||||
{recommendation.activityPlan}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Diet:</span> {recommendation.dietPlan}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handleAction(recommendation.id, "approved")}
|
||||
disabled={processingId === recommendation.id}
|
||||
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700 disabled:opacity-50 flex justify-center items-center"
|
||||
>
|
||||
{processingId === recommendation.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" /> Approve
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAction(recommendation.id, "rejected")}
|
||||
disabled={processingId === recommendation.id}
|
||||
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700 disabled:opacity-50 flex justify-center items-center"
|
||||
>
|
||||
{processingId === recommendation.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<X className="mr-2 h-4 w-4" /> Reject
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
CalendarCheck,
|
||||
CreditCard,
|
||||
Settings,
|
||||
LogOut,
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
import { UserButton, useUser } from "@clerk/nextjs";
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const { user } = useUser();
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPending = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/recommendations");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const pending = (data.recommendations || []).filter(
|
||||
(r: Recommendation) => r.status === "pending"
|
||||
);
|
||||
setPendingCount(pending.length);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch pending recommendations", e);
|
||||
}
|
||||
};
|
||||
fetchPending();
|
||||
}, []);
|
||||
|
||||
const menuItems = [
|
||||
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
|
||||
{ icon: Users, label: "Users", href: "/users" },
|
||||
{
|
||||
icon: Brain,
|
||||
label: (
|
||||
<span className="flex items-center">
|
||||
AI Recommendations
|
||||
{pendingCount > 0 && (
|
||||
<span className="ml-2 inline-block bg-red-600 text-xs rounded-full px-2 py-0.5">
|
||||
{pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
href: "/recommendations",
|
||||
},
|
||||
{ icon: CalendarCheck, label: "Attendance", href: "/attendance" },
|
||||
{ icon: CreditCard, label: "Payments", href: "/payments" },
|
||||
{ icon: Settings, label: "Settings", href: "/settings" },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800">
|
||||
<div className="p-6 border-b border-slate-800">
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
||||
FitAI Admin
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.href;
|
||||
const label = typeof item.label === "string" ? item.label : item.label;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${isActive
|
||||
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
|
||||
: "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
|
||||
>
|
||||
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
|
||||
<span className="font-medium">{label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-slate-800">
|
||||
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-slate-800/50">
|
||||
<UserButton afterSignOutUrl="/" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{user?.fullName || "Admin User"}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 truncate">
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
interface StatsCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
change?: string;
|
||||
trend?: "up" | "down" | "neutral";
|
||||
icon: LucideIcon;
|
||||
color?: "blue" | "green" | "purple" | "orange";
|
||||
}
|
||||
|
||||
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
|
||||
const colorStyles = {
|
||||
blue: "bg-blue-50 text-blue-600",
|
||||
green: "bg-green-50 text-green-600",
|
||||
purple: "bg-purple-50 text-purple-600",
|
||||
orange: "bg-orange-50 text-orange-600",
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
</CardTitle>
|
||||
<div className={`p-2 rounded-lg ${colorStyles[color]}`}>
|
||||
<Icon size={16} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{change && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<span
|
||||
className={`font-medium ${trend === "up"
|
||||
? "text-green-600"
|
||||
: trend === "down"
|
||||
? "text-red-600"
|
||||
: "text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{change}
|
||||
</span>{" "}
|
||||
vs last month
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: "short_term" | "medium_term" | "long_term" | "ai_plan";
|
||||
content: string;
|
||||
activityPlan?: string;
|
||||
dietPlan?: string;
|
||||
status: "pending" | "completed" | "approved" | "rejected";
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface RecommendationsProps {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export function Recommendations({ userId }: RecommendationsProps) {
|
||||
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [newRec, setNewRec] = useState<{
|
||||
type: "short_term" | "medium_term" | "long_term";
|
||||
content: string;
|
||||
}>({ type: "short_term", content: "" });
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecommendations();
|
||||
}, [userId]);
|
||||
|
||||
const fetchRecommendations = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/recommendations?userId=${userId}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setRecommendations(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch recommendations:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRecommendation = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch("/api/recommendations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
type: newRec.type,
|
||||
content: newRec.content,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setNewRec({ ...newRec, content: "" });
|
||||
fetchRecommendations();
|
||||
} else {
|
||||
alert("Failed to add recommendation");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const groupedRecs = {
|
||||
ai_plan: recommendations.filter((r) => r.type === "ai_plan"),
|
||||
short_term: recommendations.filter((r) => r.type === "short_term"),
|
||||
medium_term: recommendations.filter((r) => r.type === "medium_term"),
|
||||
long_term: recommendations.filter((r) => r.type === "long_term"),
|
||||
};
|
||||
|
||||
const renderSection = (
|
||||
title: string,
|
||||
type: "short_term" | "medium_term" | "long_term" | "ai_plan",
|
||||
items: Recommendation[]
|
||||
) => (
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-lg mb-3 capitalize">{title}</h4>
|
||||
<div className="space-y-2">
|
||||
{items.length === 0 && (
|
||||
<p className="text-gray-500 text-sm italic">No recommendations yet.</p>
|
||||
)}
|
||||
{items.map((rec) => (
|
||||
<div
|
||||
key={rec.id}
|
||||
className={`p-3 rounded border flex justify-between items-start ${rec.status === "completed" || rec.status === "approved"
|
||||
? "bg-green-50 border-green-200"
|
||||
: "bg-white border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="w-full">
|
||||
<p className="text-sm font-medium">{rec.content}</p>
|
||||
{rec.type === 'ai_plan' && (
|
||||
<div className="mt-2 text-xs text-gray-600 space-y-1">
|
||||
{rec.activityPlan && <p><span className="font-semibold">Activity:</span> {rec.activityPlan}</p>}
|
||||
{rec.dietPlan && <p><span className="font-semibold">Diet:</span> {rec.dietPlan}</p>}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
{new Date(rec.createdAt).toLocaleDateString()} -{" "}
|
||||
<span
|
||||
className={
|
||||
rec.status === "completed" || rec.status === "approved"
|
||||
? "text-green-600 font-medium"
|
||||
: "text-yellow-600"
|
||||
}
|
||||
>
|
||||
{rec.status === "completed" ? "Completed" : rec.status === "approved" ? "Approved" : "Pending"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{type !== 'ai_plan' && (
|
||||
<form onSubmit={handleAddRecommendation} className="mt-3 flex gap-2">
|
||||
<input
|
||||
type="hidden"
|
||||
value={type}
|
||||
onChange={() => setNewRec({ ...newRec, type: type as any })}
|
||||
/>
|
||||
{newRec.type === type && (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Add ${title.toLowerCase()}...`}
|
||||
className="flex-1 border rounded px-3 py-1 text-sm"
|
||||
value={newRec.content}
|
||||
onChange={(e) =>
|
||||
setNewRec({ ...newRec, content: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<Button type="submit" variant="secondary">
|
||||
Add
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{newRec.type !== type && (
|
||||
<Button type="button" variant="secondary" onClick={() => setNewRec({ type: type as any, content: "" })} className="text-xs text-gray-500">
|
||||
+ Add New
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) return <div>Loading recommendations...</div>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-xl font-bold">Fitness Recommendations</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{renderSection("Daily AI Plan", "ai_plan", groupedRecs.ai_plan)}
|
||||
{renderSection("Short Term Goals", "short_term", groupedRecs.short_term)}
|
||||
{renderSection("Medium Term Goals", "medium_term", groupedRecs.medium_term)}
|
||||
{renderSection("Long Term Goals", "long_term", groupedRecs.long_term)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,20 @@ import { formatDate } from "@/lib/utils";
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
function getTimeAgo(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
return `${diffDays}d ago`;
|
||||
}
|
||||
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -17,6 +31,8 @@ interface User {
|
||||
role: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
isCheckedIn?: boolean;
|
||||
checkInTime?: Date;
|
||||
client?: {
|
||||
id: string;
|
||||
membershipType: string;
|
||||
@@ -70,6 +86,7 @@ export function UserGrid({
|
||||
sortable: true,
|
||||
cellRenderer: (params: any) => {
|
||||
const roleColors = {
|
||||
superAdmin: "bg-red-100 text-red-800",
|
||||
admin: "bg-purple-100 text-purple-800",
|
||||
trainer: "bg-blue-100 text-blue-800",
|
||||
client: "bg-green-100 text-green-800",
|
||||
@@ -77,7 +94,14 @@ export function UserGrid({
|
||||
const colorClass =
|
||||
roleColors[params.value as keyof typeof roleColors] ||
|
||||
"bg-gray-100 text-gray-800";
|
||||
return `<span class="px-2 py-1 rounded text-xs font-medium ${colorClass}">${params.value}</span>`;
|
||||
|
||||
const label = params.value === 'superAdmin' ? 'Super Admin' : params.value.charAt(0).toUpperCase() + params.value.slice(1);
|
||||
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
@@ -87,6 +111,7 @@ export function UserGrid({
|
||||
filter: "agTextColumnFilter",
|
||||
sortable: true,
|
||||
minWidth: 130,
|
||||
valueFormatter: (params: any) => params.value || "N/A",
|
||||
},
|
||||
{
|
||||
headerName: "Membership",
|
||||
@@ -94,17 +119,24 @@ export function UserGrid({
|
||||
filter: "agTextColumnFilter",
|
||||
sortable: true,
|
||||
cellRenderer: (params: any) => {
|
||||
if (!params.value || params.value === "N/A") return "N/A";
|
||||
if (!params.value || params.value === "N/A") return <span className="text-gray-400">N/A</span>;
|
||||
|
||||
const membershipColors = {
|
||||
vip: "bg-yellow-100 text-yellow-800",
|
||||
premium: "bg-blue-100 text-blue-800",
|
||||
basic: "bg-gray-100 text-gray-800",
|
||||
vip: "bg-yellow-100 text-yellow-800 border-yellow-200",
|
||||
premium: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
basic: "bg-slate-100 text-slate-800 border-slate-200",
|
||||
};
|
||||
const colorClass =
|
||||
membershipColors[params.value as keyof typeof membershipColors] ||
|
||||
"bg-gray-100 text-gray-800";
|
||||
return `<span class="px-2 py-1 rounded text-xs font-medium ${colorClass}">${params.value}</span>`;
|
||||
|
||||
const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
|
||||
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium border ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
@@ -114,20 +146,49 @@ export function UserGrid({
|
||||
filter: "agTextColumnFilter",
|
||||
sortable: true,
|
||||
cellRenderer: (params: any) => {
|
||||
if (!params.value || params.value === "N/A") return "N/A";
|
||||
if (!params.value || params.value === "N/A") return <span className="text-gray-400">N/A</span>;
|
||||
|
||||
const statusColors = {
|
||||
active: "bg-green-100 text-green-800",
|
||||
inactive: "bg-red-100 text-red-800",
|
||||
suspended: "bg-yellow-100 text-yellow-800",
|
||||
suspended: "bg-orange-100 text-orange-800",
|
||||
expired: "bg-gray-100 text-gray-800",
|
||||
};
|
||||
const colorClass =
|
||||
statusColors[params.value as keyof typeof statusColors] ||
|
||||
"bg-gray-100 text-gray-800";
|
||||
return `<span class="px-2 py-1 rounded text-xs font-medium ${colorClass}">${params.value}</span>`;
|
||||
|
||||
const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
|
||||
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
headerName: "Currently Checked In",
|
||||
valueGetter: (params) => params.data?.isCheckedIn,
|
||||
filter: "agTextColumnFilter",
|
||||
sortable: true,
|
||||
cellRenderer: (params: any) => {
|
||||
if (!params.data?.isCheckedIn) {
|
||||
return <span className="text-gray-400">—</span>;
|
||||
}
|
||||
|
||||
const checkInTime = params.data.checkInTime ? new Date(params.data.checkInTime) : null;
|
||||
const timeAgo = checkInTime ? getTimeAgo(checkInTime) : '';
|
||||
|
||||
return (
|
||||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 border border-green-200">
|
||||
✓ Checked In {timeAgo && `(${timeAgo})`}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
headerName: "Join Date",
|
||||
valueGetter: (params) =>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { UserGrid } from "@/components/users/UserGrid";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||
// import { Button } from "@/components/ui/Button";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -13,6 +14,11 @@ interface User {
|
||||
role: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
isCheckedIn?: boolean;
|
||||
checkInTime?: Date;
|
||||
lastCheckInTime?: Date;
|
||||
checkInsThisWeek?: number;
|
||||
checkInsThisMonth?: number;
|
||||
client?: {
|
||||
id: string;
|
||||
membershipType: string;
|
||||
@@ -139,23 +145,44 @@ export function UserManagement() {
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (!editForm || !selectedUser) return;
|
||||
if (!editForm) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/users", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: selectedUser.id, ...editForm }),
|
||||
});
|
||||
if (response.ok) {
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
fetchUsers();
|
||||
if (selectedUser) {
|
||||
// Update existing user
|
||||
const response = await fetch("/api/users", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: selectedUser.id, ...editForm }),
|
||||
});
|
||||
if (response.ok) {
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
fetchUsers();
|
||||
} else {
|
||||
alert("Error updating user");
|
||||
}
|
||||
} else {
|
||||
alert("Error updating user");
|
||||
// Create (Invite) new user
|
||||
const response = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(editForm),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
fetchUsers();
|
||||
alert("Invitation sent successfully!");
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Error sending invitation: ${errorData.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("An unexpected error occurred");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,43 +211,65 @@ export function UserManagement() {
|
||||
<h2 className="text-2xl font-bold">User Management</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={filter === "all" ? "primary" : "secondary"}
|
||||
variant={filter === "all" ? "default" : "outline"}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All Users
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleEditUser}
|
||||
variant={filter === "client" ? "default" : "outline"}
|
||||
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
||||
disabled={!selectedUser}
|
||||
>
|
||||
Edit User
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleDeleteUser}
|
||||
variant={filter === "client" ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setEditForm({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
role: "client",
|
||||
phone: "",
|
||||
});
|
||||
setSelectedUser(null);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>
|
||||
Invite User
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "client" ? "default" : "outline"}
|
||||
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
||||
disabled={!selectedUser}
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "client" ? "primary" : "secondary"}
|
||||
variant={filter === "client" ? "default" : "outline"}
|
||||
onClick={() => setFilter("client")}
|
||||
>
|
||||
Clients
|
||||
Clientsa
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "trainer" ? "primary" : "secondary"}
|
||||
variant={filter === "trainer" ? "default" : "outline"}
|
||||
onClick={() => setFilter("trainer")}
|
||||
>
|
||||
Trainers
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "admin" ? "primary" : "secondary"}
|
||||
variant={filter === "admin" ? "default" : "outline"}
|
||||
onClick={() => setFilter("admin")}
|
||||
>
|
||||
Admins
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "superAdmin" ? "default" : "outline"}
|
||||
onClick={() => setFilter("superAdmin")}
|
||||
>
|
||||
Super Admins
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -234,10 +283,10 @@ export function UserManagement() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={handleRefresh}>
|
||||
<Button variant="default" onClick={handleRefresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleExport}>
|
||||
<Button variant="default" onClick={handleExport}>
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
@@ -259,7 +308,7 @@ export function UserManagement() {
|
||||
{isEditing && editForm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
|
||||
<h3 className="text-lg font-semibold mb-4">Edit User</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{selectedUser ? 'Edit User' : 'Invite New User'}</h3>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -304,6 +353,7 @@ export function UserManagement() {
|
||||
}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
required
|
||||
disabled={!!selectedUser} // Disable email edit for existing users if desired, or keep enabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@@ -316,10 +366,17 @@ export function UserManagement() {
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
required
|
||||
>
|
||||
{/* Ideally we fetch current user role to filter these.
|
||||
For now, we show all but the API will enforce it.
|
||||
We can add a visual indicator or fetch "me" to filter. */}
|
||||
<option value="client">Client</option>
|
||||
<option value="trainer">Trainer</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="superAdmin">Super Admin</option>
|
||||
</select>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Note: You can only assign roles lower than your own.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Phone</label>
|
||||
@@ -347,7 +404,7 @@ export function UserManagement() {
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
{selectedUser ? 'Save Changes' : 'Send Invitation'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -383,11 +440,17 @@ export function UserManagement() {
|
||||
|
||||
{selectedUser && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">User Details</h3>
|
||||
<a
|
||||
href={`/users/${selectedUser.id}`}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
|
||||
>
|
||||
View Full Profile & Recommendations
|
||||
</a>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Basic Information</h4>
|
||||
<div className="space-y-1 text-sm">
|
||||
@@ -436,13 +499,35 @@ export function UserManagement() {
|
||||
<span className="font-medium">Last Visit:</span>{" "}
|
||||
{selectedUser.client.lastVisit
|
||||
? new Date(
|
||||
selectedUser.client.lastVisit,
|
||||
).toLocaleDateString()
|
||||
selectedUser.client.lastVisit,
|
||||
).toLocaleDateString()
|
||||
: "Never"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Check-In Statistics</h4>
|
||||
<div className="space-y-1 text-sm">
|
||||
<p>
|
||||
<span className="font-medium">Last Check-In:</span>{" "}
|
||||
{selectedUser.lastCheckInTime
|
||||
? new Date(
|
||||
selectedUser.lastCheckInTime,
|
||||
).toLocaleString()
|
||||
: "Never"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">This Week:</span>{" "}
|
||||
{selectedUser.checkInsThisWeek || 0} check-ins
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">This Month:</span>{" "}
|
||||
{selectedUser.checkInsThisMonth || 0} check-ins
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { getDatabase } from '@/lib/database';
|
||||
import type { FitnessGoal } from '@/lib/database/types';
|
||||
|
||||
// Import types from database package
|
||||
import type {
|
||||
FitnessProfile as DBFitnessProfile,
|
||||
Recommendation as DBRecommendation,
|
||||
} from '@fitai/database';
|
||||
|
||||
export interface AIContext {
|
||||
profile: DBFitnessProfile;
|
||||
activeGoals: FitnessGoal[];
|
||||
completedGoals: FitnessGoal[];
|
||||
recentRecommendations: DBRecommendation[];
|
||||
progressSummary: {
|
||||
goalsCompleted: number;
|
||||
goalsActive: number;
|
||||
averageProgress: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build comprehensive AI context for a user
|
||||
* Aggregates fitness profile, goals, and recommendation history
|
||||
*/
|
||||
export async function buildAIContext(userId: string): Promise<AIContext> {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Fetch fitness profile
|
||||
const profile = await db.getFitnessProfileByUserId(userId);
|
||||
|
||||
if (!profile) {
|
||||
throw new Error(`Fitness profile not found for user ${userId}`);
|
||||
}
|
||||
|
||||
// Fetch all fitness goals
|
||||
const allGoals = await db.getFitnessGoalsByUserId(userId);
|
||||
|
||||
// Separate active and completed goals
|
||||
const activeGoals = allGoals.filter((g) => g.status === 'active');
|
||||
const completedGoals = allGoals.filter((g) => g.status === 'completed');
|
||||
|
||||
// Get all recommendations
|
||||
const allRecommendations = await db.getRecommendationsByUserId(userId);
|
||||
|
||||
// Filter recent recommendations (last 7 days)
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
|
||||
const recentRecommendations = allRecommendations
|
||||
.filter((r) => new Date(r.createdAt) > sevenDaysAgo)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.slice(0, 5);
|
||||
|
||||
// Calculate progress summary
|
||||
const progressSummary = {
|
||||
goalsCompleted: completedGoals.length,
|
||||
goalsActive: activeGoals.length,
|
||||
averageProgress:
|
||||
activeGoals.length > 0
|
||||
? activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / activeGoals.length
|
||||
: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
profile,
|
||||
activeGoals,
|
||||
completedGoals,
|
||||
recentRecommendations,
|
||||
progressSummary,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format goal for display in AI prompt
|
||||
*/
|
||||
export function formatGoalForPrompt(goal: FitnessGoal): string {
|
||||
const progress = goal.progress || 0;
|
||||
const current = goal.currentValue || 0;
|
||||
const target = goal.targetValue;
|
||||
const unit = goal.unit || '';
|
||||
|
||||
if (target) {
|
||||
return `${goal.title}: ${current}/${target} ${unit} (${progress.toFixed(1)}% complete)`;
|
||||
}
|
||||
|
||||
return `${goal.title}: ${progress.toFixed(1)}% complete`;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { AIContext } from './ai-context';
|
||||
import { formatGoalForPrompt } from './ai-context';
|
||||
|
||||
/**
|
||||
* Build enhanced AI prompt with comprehensive user context
|
||||
* Includes profile, goals, progress, and recommendation history
|
||||
*/
|
||||
export function buildEnhancedPrompt(context: AIContext): string {
|
||||
const { profile, activeGoals, completedGoals, recentRecommendations, progressSummary } = context;
|
||||
|
||||
// Build goals section
|
||||
const activeGoalsText =
|
||||
activeGoals.length > 0
|
||||
? activeGoals.map((g) => `- ${formatGoalForPrompt(g)}`).join('\n')
|
||||
: '- No active goals set';
|
||||
|
||||
const completedGoalsText =
|
||||
completedGoals.length > 0
|
||||
? completedGoals.slice(0, 3).map((g) => g.title).join(', ')
|
||||
: 'None yet';
|
||||
|
||||
// Build context about recent recommendations
|
||||
const recommendationContextText =
|
||||
recentRecommendations.length > 0
|
||||
? `The user has received ${recentRecommendations.length} recommendations in the past week. Consider their progress and avoid repetitive advice.`
|
||||
: 'This is a new user or they haven\'t received recent recommendations. Provide comprehensive guidance.';
|
||||
|
||||
return `You are a professional fitness trainer and nutritionist with access to the user's complete fitness journey.
|
||||
|
||||
## User Profile
|
||||
- Height: ${profile.height || 'Not specified'} cm
|
||||
- Weight: ${profile.weight || 'Not specified'} kg
|
||||
- Age: ${profile.age || 'Not specified'}
|
||||
- Gender: ${profile.gender || 'Not specified'}
|
||||
- Primary Goal: ${profile.fitnessGoal || 'General fitness'}
|
||||
- Activity Level: ${profile.activityLevel || 'Not specified'}
|
||||
- Medical Conditions: ${profile.medicalConditions || 'None'}
|
||||
- Allergies: ${profile.allergies || 'None'}
|
||||
- Injuries: ${profile.injuries || 'None'}
|
||||
|
||||
## Active Goals (${activeGoals.length})
|
||||
${activeGoalsText}
|
||||
|
||||
## Recent Progress
|
||||
- Goals Completed: ${progressSummary.goalsCompleted}
|
||||
- Active Goals: ${progressSummary.goalsActive}
|
||||
- Average Progress on Active Goals: ${progressSummary.averageProgress.toFixed(1)}%
|
||||
${completedGoals.length > 0 ? `- Recently Completed: ${completedGoalsText}` : ''}
|
||||
|
||||
## Recommendation History
|
||||
${recommendationContextText}
|
||||
|
||||
## Task
|
||||
Generate a personalized daily recommendation that:
|
||||
1. Acknowledges their progress on active goals
|
||||
2. Provides specific, actionable steps toward goal completion
|
||||
3. Builds on previous recommendations without being repetitive
|
||||
4. Adapts to their current progress level
|
||||
5. Takes into account their medical conditions, allergies, and injuries
|
||||
|
||||
Respond in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks:
|
||||
{
|
||||
"recommendationText": "Personalized motivation and progress acknowledgment (2-3 sentences)",
|
||||
"activityPlan": "Specific workout plan aligned with active goals (detailed, 3-5 exercises or activities)",
|
||||
"dietPlan": "Nutrition plan supporting their goals and restrictions (detailed, include meals and portions)"
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build basic prompt for users without complete context
|
||||
* Fallback when goals or other data is missing
|
||||
*/
|
||||
export function buildBasicPrompt(profile: any): string {
|
||||
return `You are a professional fitness trainer and nutritionist.
|
||||
Generate a detailed daily recommendation for a user with the following profile:
|
||||
- Height: ${profile.height} cm
|
||||
- Weight: ${profile.weight} kg
|
||||
- Age: ${profile.age}
|
||||
- Gender: ${profile.gender}
|
||||
- Goal: ${profile.fitnessGoal}
|
||||
- Activity Level: ${profile.activityLevel}
|
||||
- Medical Conditions: ${profile.medicalConditions || "None"}
|
||||
- Injuries: ${profile.injuries || "None"}
|
||||
|
||||
Please provide the response in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks:
|
||||
{
|
||||
"recommendationText": "General advice and motivation for today.",
|
||||
"activityPlan": "Detailed workout or activity plan for today.",
|
||||
"dietPlan": "Detailed meal plan for today."
|
||||
}`;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { clerkClient } from '@clerk/nextjs/server';
|
||||
|
||||
/**
|
||||
* User roles available in the application
|
||||
*/
|
||||
export type UserRole = 'admin' | 'trainer' | 'client';
|
||||
|
||||
/**
|
||||
* Set a user's role in Clerk public metadata
|
||||
* This will trigger a webhook that syncs the role to the database
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @param role - Role to assign (admin, trainer, or client)
|
||||
* @returns Updated user object
|
||||
*
|
||||
* @example
|
||||
* await setUserRole('user_abc123', 'admin');
|
||||
*/
|
||||
export async function setUserRole(userId: string, role: UserRole) {
|
||||
const client = await clerkClient();
|
||||
|
||||
return await client.users.updateUser(userId, {
|
||||
publicMetadata: { role },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's role from Clerk public metadata
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @returns User role or null if not set
|
||||
*
|
||||
* @example
|
||||
* const role = await getUserRole('user_abc123');
|
||||
* console.log(role); // 'admin' | 'trainer' | 'client' | null
|
||||
*/
|
||||
export async function getUserRole(userId: string): Promise<UserRole | null> {
|
||||
const client = await clerkClient();
|
||||
const user = await client.users.getUser(userId);
|
||||
|
||||
const role = user.publicMetadata?.role as UserRole | undefined;
|
||||
return role || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a specific role
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @param role - Role to check
|
||||
* @returns True if user has the role
|
||||
*
|
||||
* @example
|
||||
* const isAdmin = await hasRole('user_abc123', 'admin');
|
||||
*/
|
||||
export async function hasRole(userId: string, role: UserRole): Promise<boolean> {
|
||||
const userRole = await getUserRole(userId);
|
||||
return userRole === role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is an admin
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @returns True if user is an admin
|
||||
*
|
||||
* @example
|
||||
* const isAdmin = await isAdmin('user_abc123');
|
||||
*/
|
||||
export async function isAdmin(userId: string): Promise<boolean> {
|
||||
return hasRole(userId, 'admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is a trainer
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @returns True if user is a trainer
|
||||
*
|
||||
* @example
|
||||
* const isTrainer = await isTrainer('user_abc123');
|
||||
*/
|
||||
export async function isTrainer(userId: string): Promise<boolean> {
|
||||
return hasRole(userId, 'trainer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is a client
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @returns True if user is a client
|
||||
*
|
||||
* @example
|
||||
* const isClient = await isClient('user_abc123');
|
||||
*/
|
||||
export async function isClient(userId: string): Promise<boolean> {
|
||||
return hasRole(userId, 'client');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update roles for multiple users
|
||||
*
|
||||
* @param userRoles - Array of {userId, role} objects
|
||||
* @returns Array of updated users
|
||||
*
|
||||
* @example
|
||||
* await bulkSetUserRoles([
|
||||
* { userId: 'user_abc123', role: 'admin' },
|
||||
* { userId: 'user_def456', role: 'trainer' },
|
||||
* ]);
|
||||
*/
|
||||
export async function bulkSetUserRoles(
|
||||
userRoles: Array<{ userId: string; role: UserRole }>
|
||||
): Promise<void> {
|
||||
const client = await clerkClient();
|
||||
|
||||
await Promise.all(
|
||||
userRoles.map(({ userId, role }) =>
|
||||
client.users.updateUser(userId, {
|
||||
publicMetadata: { role },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users with a specific role
|
||||
*
|
||||
* @param role - Role to filter by
|
||||
* @returns Array of users with the specified role
|
||||
*
|
||||
* @example
|
||||
* const admins = await getUsersByRole('admin');
|
||||
*/
|
||||
export async function getUsersByRole(role: UserRole) {
|
||||
const client = await clerkClient();
|
||||
|
||||
// Clerk doesn't support filtering by metadata directly, so we fetch all users
|
||||
// and filter client-side. For large user bases, consider caching or database queries.
|
||||
const { data: users } = await client.users.getUserList();
|
||||
|
||||
return users.filter(
|
||||
(user) => (user.publicMetadata?.role as UserRole) === role
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user count by role
|
||||
*
|
||||
* @returns Object with counts for each role
|
||||
*
|
||||
* @example
|
||||
* const counts = await getUserCountByRole();
|
||||
* console.log(counts); // { admin: 2, trainer: 5, client: 100 }
|
||||
*/
|
||||
export async function getUserCountByRole(): Promise<Record<UserRole, number>> {
|
||||
const client = await clerkClient();
|
||||
const { data: users } = await client.users.getUserList();
|
||||
|
||||
const counts: Record<UserRole, number> = {
|
||||
admin: 0,
|
||||
trainer: 0,
|
||||
client: 0,
|
||||
};
|
||||
|
||||
users.forEach((user) => {
|
||||
const role = (user.publicMetadata?.role as UserRole) || 'client';
|
||||
counts[role]++;
|
||||
});
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync user role between Clerk and database manually
|
||||
* Useful for fixing inconsistencies or after manual changes
|
||||
*
|
||||
* @param userId - Clerk user ID
|
||||
* @returns Success status
|
||||
*
|
||||
* @example
|
||||
* await syncUserRole('user_abc123');
|
||||
*/
|
||||
export async function syncUserRole(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const client = await clerkClient();
|
||||
const user = await client.users.getUser(userId);
|
||||
|
||||
// Trigger a webhook by updating the user (even with same data)
|
||||
await client.users.updateUser(userId, {
|
||||
publicMetadata: user.publicMetadata,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error syncing user role:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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[] = []
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
import { DrizzleDatabase } from '../drizzle'
|
||||
import { DatabaseConfig } from '../types'
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||
import * as schema from '@fitai/database'
|
||||
|
||||
describe('DrizzleDatabase', () => {
|
||||
let db: DrizzleDatabase
|
||||
let sqlite: Database.Database
|
||||
let drizzleDb: ReturnType<typeof drizzle>
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create in-memory SQLite database
|
||||
sqlite = new Database(':memory:')
|
||||
drizzleDb = drizzle(sqlite, { schema })
|
||||
|
||||
const config: DatabaseConfig = {
|
||||
type: 'sqlite',
|
||||
connection: { filename: ':memory:' },
|
||||
options: { logging: false }
|
||||
}
|
||||
|
||||
// Initialize DrizzleDatabase with the in-memory db
|
||||
db = new DrizzleDatabase(config, drizzleDb as any)
|
||||
await db.connect()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await db.disconnect()
|
||||
sqlite.close()
|
||||
})
|
||||
|
||||
describe('User Operations', () => {
|
||||
it('should create and retrieve a user', async () => {
|
||||
const newUser = {
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
password: 'password123',
|
||||
role: 'client' as const
|
||||
}
|
||||
|
||||
const createdUser = await db.createUser(newUser)
|
||||
expect(createdUser).toBeDefined()
|
||||
expect(createdUser.id).toBeDefined()
|
||||
expect(createdUser.email).toBe(newUser.email)
|
||||
|
||||
const fetchedUser = await db.getUserById(createdUser.id)
|
||||
expect(fetchedUser).toBeDefined()
|
||||
expect(fetchedUser?.id).toBe(createdUser.id)
|
||||
expect(fetchedUser?.email).toBe(createdUser.email)
|
||||
// SQLite/Drizzle might truncate ms or handle dates differently in test env
|
||||
expect(new Date(fetchedUser!.createdAt).getTime()).toBeLessThanOrEqual(createdUser.createdAt.getTime())
|
||||
expect(new Date(fetchedUser!.updatedAt).getTime()).toBeLessThanOrEqual(createdUser.updatedAt.getTime())
|
||||
})
|
||||
|
||||
it('should update a user', async () => {
|
||||
const user = await db.createUser({
|
||||
email: 'update@example.com',
|
||||
firstName: 'Update',
|
||||
lastName: 'Me',
|
||||
password: 'password',
|
||||
role: 'client'
|
||||
})
|
||||
|
||||
const updated = await db.updateUser(user.id, { firstName: 'Updated' })
|
||||
expect(updated?.firstName).toBe('Updated')
|
||||
|
||||
const fetched = await db.getUserById(user.id)
|
||||
expect(fetched?.firstName).toBe('Updated')
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const user = await db.createUser({
|
||||
email: 'delete@example.com',
|
||||
firstName: 'Delete',
|
||||
lastName: 'Me',
|
||||
password: 'password',
|
||||
role: 'client'
|
||||
})
|
||||
|
||||
const deleted = await db.deleteUser(user.id)
|
||||
expect(deleted).toBe(true)
|
||||
|
||||
const fetched = await db.getUserById(user.id)
|
||||
expect(fetched).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Client Operations', () => {
|
||||
it('should create and retrieve a client', async () => {
|
||||
const user = await db.createUser({
|
||||
email: 'client@example.com',
|
||||
firstName: 'Client',
|
||||
lastName: 'User',
|
||||
password: 'password',
|
||||
role: 'client'
|
||||
})
|
||||
|
||||
const newClient = {
|
||||
userId: user.id,
|
||||
membershipType: 'basic' as const,
|
||||
membershipStatus: 'active' as const,
|
||||
joinDate: new Date()
|
||||
}
|
||||
|
||||
const createdClient = await db.createClient(newClient)
|
||||
expect(createdClient).toBeDefined()
|
||||
expect(createdClient.userId).toBe(user.id)
|
||||
|
||||
const fetchedClient = await db.getClientById(createdClient.id)
|
||||
expect(fetchedClient).toBeDefined()
|
||||
expect(fetchedClient?.id).toBe(createdClient.id)
|
||||
expect(fetchedClient?.userId).toBe(createdClient.userId)
|
||||
expect(fetchedClient?.membershipType).toBe(createdClient.membershipType)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Fitness Profile Operations', () => {
|
||||
it('should create and retrieve a fitness profile', async () => {
|
||||
const user = await db.createUser({
|
||||
email: 'profile@example.com',
|
||||
firstName: 'Profile',
|
||||
lastName: 'User',
|
||||
password: 'password',
|
||||
role: 'client'
|
||||
})
|
||||
|
||||
const newProfile = {
|
||||
userId: user.id,
|
||||
height: 180,
|
||||
weight: 75,
|
||||
age: 30,
|
||||
gender: 'male' as const,
|
||||
activityLevel: 'moderately_active' as const,
|
||||
fitnessGoals: ['weight_loss'],
|
||||
exerciseHabits: 'None',
|
||||
dietHabits: 'None',
|
||||
medicalConditions: 'None'
|
||||
}
|
||||
|
||||
const createdProfile = await db.createFitnessProfile(newProfile as any)
|
||||
expect(createdProfile).toBeDefined()
|
||||
expect(createdProfile.userId).toBe(user.id)
|
||||
|
||||
const fetchedProfile = await db.getFitnessProfileByUserId(user.id)
|
||||
expect(fetchedProfile).toBeDefined()
|
||||
expect(fetchedProfile?.userId).toBe(user.id)
|
||||
expect(fetchedProfile?.fitnessGoals).toEqual(expect.arrayContaining(['weight_loss']))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Attendance Operations', () => {
|
||||
it('should check in and check out', async () => {
|
||||
const user = await db.createUser({
|
||||
email: 'attendance@example.com',
|
||||
firstName: 'Attendance',
|
||||
lastName: 'User',
|
||||
password: 'password',
|
||||
role: 'client'
|
||||
})
|
||||
|
||||
const checkIn = await db.checkIn(user.id, 'gym')
|
||||
expect(checkIn).toBeDefined()
|
||||
expect(checkIn.checkOutTime).toBeUndefined()
|
||||
|
||||
const activeCheckIn = await db.getActiveCheckIn(user.id)
|
||||
expect(activeCheckIn).toBeDefined()
|
||||
expect(activeCheckIn?.id).toBe(checkIn.id)
|
||||
|
||||
const checkOut = await db.checkOut(checkIn.id)
|
||||
expect(checkOut?.checkOutTime).toBeDefined()
|
||||
|
||||
const activeCheckInAfter = await db.getActiveCheckIn(user.id)
|
||||
expect(activeCheckInAfter).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,555 @@
|
||||
|
||||
import { IDatabase, User, Client, FitnessProfile, Attendance, Recommendation, FitnessGoal, DatabaseConfig } from './types'
|
||||
import { db as defaultDb, users, clients, fitnessProfiles, attendance, recommendations, fitnessGoals, eq, and, desc, sql } from '@fitai/database'
|
||||
import { InferSelectModel } from 'drizzle-orm'
|
||||
|
||||
export class DrizzleDatabase implements IDatabase {
|
||||
private config: DatabaseConfig
|
||||
private db: typeof defaultDb
|
||||
|
||||
constructor(config: DatabaseConfig, db?: typeof defaultDb) {
|
||||
this.config = config
|
||||
this.db = db || defaultDb
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
// Drizzle with better-sqlite3 connects synchronously on initialization
|
||||
// We can just log here if needed
|
||||
if (this.config.options?.logging) {
|
||||
console.log('Drizzle database connected')
|
||||
}
|
||||
await this.createTables()
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
// better-sqlite3 handle is managed by Drizzle, usually no explicit disconnect needed for connection pooling
|
||||
// but we can close the underlying sqlite instance if we had access to it.
|
||||
// For now, we'll assume it's handled.
|
||||
if (this.config.options?.logging) {
|
||||
console.log('Drizzle database disconnected')
|
||||
}
|
||||
}
|
||||
|
||||
private async createTables(): Promise<void> {
|
||||
// Users table
|
||||
await this.db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
password TEXT,
|
||||
phone TEXT,
|
||||
role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')),
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`)
|
||||
|
||||
// Clients table
|
||||
await this.db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
membership_type TEXT NOT NULL CHECK (membership_type IN ('basic', 'premium', 'vip')),
|
||||
membership_status TEXT NOT NULL CHECK (membership_status IN ('active', 'inactive', 'suspended')),
|
||||
join_date INTEGER NOT NULL,
|
||||
last_visit INTEGER,
|
||||
emergency_contact_name TEXT,
|
||||
emergency_contact_phone TEXT,
|
||||
emergency_contact_relationship TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
// Fitness profiles table
|
||||
await this.db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS fitness_profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL UNIQUE,
|
||||
height REAL,
|
||||
weight REAL,
|
||||
age INTEGER,
|
||||
gender TEXT CHECK (gender IN ('male', 'female', 'other', 'prefer_not_to_say')),
|
||||
activity_level TEXT CHECK (activity_level IN ('sedentary', 'lightly_active', 'moderately_active', 'very_active', 'extremely_active')),
|
||||
fitness_goals TEXT,
|
||||
exercise_habits TEXT,
|
||||
diet_habits TEXT,
|
||||
medical_conditions TEXT,
|
||||
allergies TEXT,
|
||||
injuries TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
// Attendance table
|
||||
await this.db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS attendance (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
check_in_time INTEGER NOT NULL,
|
||||
check_out_time INTEGER,
|
||||
type TEXT NOT NULL CHECK (type IN ('gym', 'class', 'personal_training')),
|
||||
notes TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
// Recommendations table
|
||||
await this.db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS recommendations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
fitness_profile_id TEXT NOT NULL,
|
||||
recommendation_text TEXT NOT NULL,
|
||||
activity_plan TEXT NOT NULL,
|
||||
diet_plan TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')) DEFAULT 'pending',
|
||||
generated_at INTEGER NOT NULL,
|
||||
approved_at INTEGER,
|
||||
approved_by TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
// Fitness Goals table
|
||||
await this.db.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS fitness_goals (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
fitness_profile_id TEXT,
|
||||
goal_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
target_value REAL,
|
||||
current_value REAL,
|
||||
unit TEXT,
|
||||
start_date INTEGER NOT NULL,
|
||||
target_date INTEGER,
|
||||
completed_date INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
progress REAL DEFAULT 0,
|
||||
priority TEXT DEFAULT 'medium',
|
||||
notes TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
// User operations
|
||||
async createUser(userData: Omit<User, "createdAt" | "updatedAt" | "id"> & { id?: string }): Promise<User> {
|
||||
const id = userData.id || Math.random().toString(36).substr(2, 9)
|
||||
const now = new Date()
|
||||
|
||||
const newUser = {
|
||||
...userData,
|
||||
id,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
await this.db.insert(users).values(newUser)
|
||||
return newUser
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User | null> {
|
||||
const result = await this.db.select().from(users).where(eq(users.id, id)).get()
|
||||
return result ? this.mapUser(result) : null
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | null> {
|
||||
const result = await this.db.select().from(users).where(eq(users.email, email)).get()
|
||||
return result ? this.mapUser(result) : null
|
||||
}
|
||||
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
const results = await this.db.select().from(users).orderBy(desc(users.createdAt)).all()
|
||||
return results.map(this.mapUser)
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: Partial<User>): Promise<User | null> {
|
||||
const { id: _, ...updateData } = updates
|
||||
if (Object.keys(updateData).length === 0) return this.getUserById(id)
|
||||
|
||||
await this.db.update(users)
|
||||
.set({ ...updateData, updatedAt: new Date() })
|
||||
.where(eq(users.id, id))
|
||||
.run()
|
||||
|
||||
return this.getUserById(id)
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<boolean> {
|
||||
const result = await this.db.delete(users).where(eq(users.id, id)).run()
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
async migrateUserId(oldId: string, newId: string): Promise<void> {
|
||||
await this.db.update(users).set({ id: newId }).where(eq(users.id, oldId)).run()
|
||||
}
|
||||
|
||||
// Client operations
|
||||
async createClient(clientData: Omit<Client, 'id'>): Promise<Client> {
|
||||
const id = Math.random().toString(36).substr(2, 9)
|
||||
const newClient = {
|
||||
id,
|
||||
...clientData,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
await this.db.insert(clients).values(newClient as any)
|
||||
return this.mapClient(newClient)
|
||||
}
|
||||
|
||||
async getClientById(id: string): Promise<Client | null> {
|
||||
const result = await this.db.select().from(clients).where(eq(clients.id, id)).get()
|
||||
return result ? this.mapClient(result) : null
|
||||
}
|
||||
|
||||
async getClientByUserId(userId: string): Promise<Client | null> {
|
||||
const result = await this.db.select().from(clients).where(eq(clients.userId, userId)).get()
|
||||
return result ? this.mapClient(result) : null
|
||||
}
|
||||
|
||||
async getAllClients(): Promise<Client[]> {
|
||||
const results = await this.db.select().from(clients).orderBy(desc(clients.joinDate)).all()
|
||||
return results.map(this.mapClient)
|
||||
}
|
||||
|
||||
async updateClient(id: string, updates: Partial<Client>): Promise<Client | null> {
|
||||
const { id: _, ...updateData } = updates
|
||||
if (Object.keys(updateData).length === 0) return this.getClientById(id)
|
||||
|
||||
await this.db.update(clients)
|
||||
.set({ ...updateData, updatedAt: new Date() } as any)
|
||||
.where(eq(clients.id, id))
|
||||
.run()
|
||||
|
||||
return this.getClientById(id)
|
||||
}
|
||||
|
||||
async deleteClient(id: string): Promise<boolean> {
|
||||
const result = await this.db.delete(clients).where(eq(clients.id, id)).run()
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
// Fitness Profile operations
|
||||
async createFitnessProfile(profileData: Omit<FitnessProfile, 'createdAt' | 'updatedAt'>): Promise<FitnessProfile> {
|
||||
const now = new Date()
|
||||
const id = Math.random().toString(36).substr(2, 9)
|
||||
const newProfile = {
|
||||
id,
|
||||
...profileData,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
await this.db.insert(fitnessProfiles).values(newProfile as any)
|
||||
return { ...profileData, createdAt: now, updatedAt: now }
|
||||
}
|
||||
|
||||
async getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null> {
|
||||
const result = await this.db.select().from(fitnessProfiles).where(eq(fitnessProfiles.userId, userId)).get()
|
||||
return result ? this.mapFitnessProfile(result) : null
|
||||
}
|
||||
|
||||
async getAllFitnessProfiles(): Promise<FitnessProfile[]> {
|
||||
const results = await this.db.select().from(fitnessProfiles).orderBy(desc(fitnessProfiles.createdAt)).all()
|
||||
return results.map(this.mapFitnessProfile)
|
||||
}
|
||||
|
||||
async updateFitnessProfile(userId: string, updates: Partial<FitnessProfile>): Promise<FitnessProfile | null> {
|
||||
const { userId: _, ...updateData } = updates
|
||||
if (Object.keys(updateData).length === 0) return this.getFitnessProfileByUserId(userId)
|
||||
|
||||
const dbUpdates = { ...updateData } as any
|
||||
if (updateData.fitnessGoals) {
|
||||
dbUpdates.fitnessGoals = JSON.stringify(updateData.fitnessGoals)
|
||||
}
|
||||
|
||||
await this.db.update(fitnessProfiles)
|
||||
.set({ ...dbUpdates, updatedAt: new Date() })
|
||||
.where(eq(fitnessProfiles.userId, userId))
|
||||
.run()
|
||||
|
||||
return this.getFitnessProfileByUserId(userId)
|
||||
}
|
||||
|
||||
async deleteFitnessProfile(userId: string): Promise<boolean> {
|
||||
const result = await this.db.delete(fitnessProfiles).where(eq(fitnessProfiles.userId, userId)).run()
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
// Attendance operations
|
||||
async checkIn(userId: string, type: "gym" | "class" | "personal_training", notes?: string): Promise<Attendance> {
|
||||
const id = Math.random().toString(36).substr(2, 9)
|
||||
const now = new Date()
|
||||
|
||||
const newAttendance = {
|
||||
id,
|
||||
userId,
|
||||
checkInTime: now,
|
||||
type,
|
||||
notes,
|
||||
createdAt: now
|
||||
}
|
||||
|
||||
await this.db.insert(attendance).values(newAttendance as any)
|
||||
|
||||
// Update client last visit
|
||||
const client = await this.getClientByUserId(userId)
|
||||
if (client) {
|
||||
await this.updateClient(client.id, { lastVisit: now })
|
||||
}
|
||||
|
||||
return newAttendance
|
||||
}
|
||||
|
||||
async checkOut(attendanceId: string): Promise<Attendance | null> {
|
||||
const now = new Date()
|
||||
await this.db.update(attendance)
|
||||
.set({ checkOutTime: now })
|
||||
.where(eq(attendance.id, attendanceId))
|
||||
.run()
|
||||
|
||||
return this.getAttendanceById(attendanceId)
|
||||
}
|
||||
|
||||
async getAttendanceById(id: string): Promise<Attendance | null> {
|
||||
const result = await this.db.select().from(attendance).where(eq(attendance.id, id)).get()
|
||||
return result ? this.mapAttendance(result) : null
|
||||
}
|
||||
|
||||
async getAttendanceHistory(userId: string): Promise<Attendance[]> {
|
||||
const results = await this.db.select().from(attendance)
|
||||
.where(eq(attendance.userId, userId))
|
||||
.orderBy(desc(attendance.checkInTime))
|
||||
.all()
|
||||
return results.map(this.mapAttendance)
|
||||
}
|
||||
|
||||
async getAllAttendance(): Promise<Attendance[]> {
|
||||
const results = await this.db.select().from(attendance)
|
||||
.orderBy(desc(attendance.checkInTime))
|
||||
.all()
|
||||
return results.map(this.mapAttendance)
|
||||
}
|
||||
|
||||
async getActiveCheckIn(userId: string): Promise<Attendance | null> {
|
||||
// Drizzle doesn't support IS NULL in where directly with simple syntax sometimes, but eq(col, null) works or isNull(col)
|
||||
// We need to check how to filter for null checkOutTime.
|
||||
// In Drizzle, we can filter in JS or use isNull operator if imported.
|
||||
// Let's fetch recent and filter for now to be safe, or use raw sql if needed, but better to use Drizzle operators.
|
||||
// Actually, we can just fetch all for user and filter in memory since it's unlikely to be huge for active checkins,
|
||||
// but correct way is `isNull(attendance.checkOutTime)`.
|
||||
// Since I didn't import `isNull`, I'll fetch recent history and find first active.
|
||||
|
||||
const history = await this.getAttendanceHistory(userId)
|
||||
return history.find(a => !a.checkOutTime) || null
|
||||
}
|
||||
|
||||
// Recommendation operations
|
||||
async createRecommendation(data: Omit<Recommendation, "createdAt" | "approvedAt" | "approvedBy">): Promise<Recommendation> {
|
||||
const now = new Date()
|
||||
const newRec = {
|
||||
...data,
|
||||
createdAt: now,
|
||||
status: data.status || 'pending'
|
||||
}
|
||||
|
||||
await this.db.insert(recommendations).values(newRec as any)
|
||||
return newRec as Recommendation
|
||||
}
|
||||
|
||||
async getRecommendationsByUserId(userId: string): Promise<Recommendation[]> {
|
||||
const results = await this.db.select().from(recommendations)
|
||||
.where(eq(recommendations.userId, userId))
|
||||
.orderBy(desc(recommendations.createdAt))
|
||||
.all()
|
||||
return results.map(this.mapRecommendation)
|
||||
}
|
||||
|
||||
async getAllRecommendations(): Promise<Recommendation[]> {
|
||||
const results = await this.db.select().from(recommendations)
|
||||
.orderBy(desc(recommendations.createdAt))
|
||||
.all()
|
||||
return results.map(this.mapRecommendation)
|
||||
}
|
||||
|
||||
async updateRecommendation(id: string, updates: Partial<Recommendation>): Promise<Recommendation | null> {
|
||||
const { id: _, ...updateData } = updates
|
||||
if (Object.keys(updateData).length === 0) return this.getRecommendationById(id)
|
||||
|
||||
await this.db.update(recommendations)
|
||||
.set(updateData as any)
|
||||
.where(eq(recommendations.id, id))
|
||||
.run()
|
||||
|
||||
return this.getRecommendationById(id)
|
||||
}
|
||||
|
||||
async deleteRecommendation(id: string): Promise<boolean> {
|
||||
const result = await this.db.delete(recommendations).where(eq(recommendations.id, id)).run()
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
async getRecommendationById(id: string): Promise<Recommendation | null> {
|
||||
const result = await this.db.select().from(recommendations).where(eq(recommendations.id, id)).get()
|
||||
return result ? this.mapRecommendation(result) : null
|
||||
}
|
||||
|
||||
// Fitness Goals operations
|
||||
async createFitnessGoal(goalData: Omit<FitnessGoal, "createdAt" | "updatedAt">): Promise<FitnessGoal> {
|
||||
const now = new Date()
|
||||
const newGoal = {
|
||||
...goalData,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
await this.db.insert(fitnessGoals).values(newGoal as any)
|
||||
return newGoal as FitnessGoal
|
||||
}
|
||||
|
||||
async getFitnessGoalById(id: string): Promise<FitnessGoal | null> {
|
||||
const result = await this.db.select().from(fitnessGoals).where(eq(fitnessGoals.id, id)).get()
|
||||
return result ? this.mapFitnessGoal(result) : null
|
||||
}
|
||||
|
||||
async getFitnessGoalsByUserId(userId: string, status?: string): Promise<FitnessGoal[]> {
|
||||
let query = this.db.select().from(fitnessGoals).where(eq(fitnessGoals.userId, userId))
|
||||
|
||||
if (status) {
|
||||
query = this.db.select().from(fitnessGoals).where(and(eq(fitnessGoals.userId, userId), eq(fitnessGoals.status, status as any)))
|
||||
}
|
||||
|
||||
const results = await query.orderBy(desc(fitnessGoals.createdAt)).all()
|
||||
return results.map(this.mapFitnessGoal)
|
||||
}
|
||||
|
||||
async updateFitnessGoal(id: string, updates: Partial<FitnessGoal>): Promise<FitnessGoal | null> {
|
||||
const { id: _, ...updateData } = updates
|
||||
if (Object.keys(updateData).length === 0) return this.getFitnessGoalById(id)
|
||||
|
||||
await this.db.update(fitnessGoals)
|
||||
.set({ ...updateData, updatedAt: new Date() } as any)
|
||||
.where(eq(fitnessGoals.id, id))
|
||||
.run()
|
||||
|
||||
return this.getFitnessGoalById(id)
|
||||
}
|
||||
|
||||
async deleteFitnessGoal(id: string): Promise<boolean> {
|
||||
const result = await this.db.delete(fitnessGoals).where(eq(fitnessGoals.id, id)).run()
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
async updateGoalProgress(id: string, currentValue: number): Promise<FitnessGoal | null> {
|
||||
const goal = await this.getFitnessGoalById(id)
|
||||
if (!goal) return null
|
||||
|
||||
let progress = goal.progress
|
||||
if (goal.targetValue && goal.targetValue > 0) {
|
||||
progress = Math.min(100, (currentValue / goal.targetValue) * 100)
|
||||
}
|
||||
|
||||
await this.db.update(fitnessGoals)
|
||||
.set({ currentValue, progress, updatedAt: new Date() })
|
||||
.where(eq(fitnessGoals.id, id))
|
||||
.run()
|
||||
|
||||
return this.getFitnessGoalById(id)
|
||||
}
|
||||
|
||||
async completeGoal(id: string): Promise<FitnessGoal | null> {
|
||||
const now = new Date()
|
||||
await this.db.update(fitnessGoals)
|
||||
.set({ status: 'completed', progress: 100, completedDate: now, updatedAt: now })
|
||||
.where(eq(fitnessGoals.id, id))
|
||||
.run()
|
||||
|
||||
return this.getFitnessGoalById(id)
|
||||
}
|
||||
|
||||
async getDashboardStats(): Promise<{ totalUsers: number; activeClients: number; totalRevenue: number; revenueGrowth: number }> {
|
||||
// Placeholder implementation as per original sqlite.ts (which didn't implement this either in the viewed snippet,
|
||||
// but interface requires it. I'll provide a basic implementation or mock).
|
||||
// The original sqlite.ts snippet ended before showing this method, but the interface has it.
|
||||
// I'll implement a basic count.
|
||||
|
||||
const allUsers = await this.db.select().from(users).all()
|
||||
const activeClients = await this.db.select().from(clients).where(eq(clients.membershipStatus, 'active')).all()
|
||||
|
||||
return {
|
||||
totalUsers: allUsers.length,
|
||||
activeClients: activeClients.length,
|
||||
totalRevenue: 0, // Not tracking payments yet
|
||||
revenueGrowth: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Mappers
|
||||
private mapUser(row: any): User {
|
||||
return {
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private mapClient(row: any): Client {
|
||||
return {
|
||||
...row,
|
||||
joinDate: new Date(row.joinDate),
|
||||
lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
private mapFitnessProfile(row: any): FitnessProfile {
|
||||
return {
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private mapAttendance(row: any): Attendance {
|
||||
return {
|
||||
...row,
|
||||
checkInTime: new Date(row.checkInTime),
|
||||
checkOutTime: row.checkOutTime ? new Date(row.checkOutTime) : undefined,
|
||||
createdAt: new Date(row.createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
private mapRecommendation(row: any): Recommendation {
|
||||
return {
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
approvedAt: row.approvedAt ? new Date(row.approvedAt) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
private mapFitnessGoal(row: any): FitnessGoal {
|
||||
return {
|
||||
...row,
|
||||
startDate: new Date(row.startDate),
|
||||
targetDate: row.targetDate ? new Date(row.targetDate) : undefined,
|
||||
completedDate: row.completedDate ? new Date(row.completedDate) : undefined,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IDatabase, DatabaseConfig } from './types'
|
||||
import { SQLiteDatabase } from './sqlite'
|
||||
import { DrizzleDatabase } from './drizzle'
|
||||
|
||||
// Database factory - creates appropriate database instance based on config
|
||||
export class DatabaseFactory {
|
||||
@@ -20,28 +20,28 @@ export class DatabaseFactory {
|
||||
// Create new database instance based on type
|
||||
switch (config.type) {
|
||||
case 'sqlite':
|
||||
this.instance = new SQLiteDatabase(config)
|
||||
this.instance = new DrizzleDatabase(config)
|
||||
break
|
||||
|
||||
|
||||
case 'postgresql':
|
||||
// TODO: Implement PostgreSQLDatabase
|
||||
throw new Error('PostgreSQL implementation not yet available')
|
||||
|
||||
|
||||
case 'mysql':
|
||||
// TODO: Implement MySQLDatabase
|
||||
throw new Error('MySQL implementation not yet available')
|
||||
|
||||
|
||||
case 'mongodb':
|
||||
// TODO: Implement MongoDBDatabase
|
||||
throw new Error('MongoDB implementation not yet available')
|
||||
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported database type: ${config.type}`)
|
||||
}
|
||||
|
||||
await this.instance.connect()
|
||||
this.config = config
|
||||
|
||||
|
||||
return this.instance
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class DatabaseFactory {
|
||||
|
||||
private static isSameConfig(config: DatabaseConfig): boolean {
|
||||
if (!this.config) return false
|
||||
|
||||
|
||||
return (
|
||||
this.config.type === config.type &&
|
||||
JSON.stringify(this.config.connection) === JSON.stringify(config.connection)
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { IDatabase, User, Client, FitnessProfile, DatabaseConfig } from './types'
|
||||
|
||||
export class SQLiteDatabase implements IDatabase {
|
||||
private db: Database.Database | null = null
|
||||
private config: DatabaseConfig
|
||||
|
||||
constructor(config: DatabaseConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to SQLite database:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.db) {
|
||||
this.db.close()
|
||||
this.db = null
|
||||
if (this.config.options?.logging) {
|
||||
console.log('SQLite database disconnected')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createTables(): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
// Users table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
firstName TEXT NOT NULL,
|
||||
lastName TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin', 'client')),
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL
|
||||
)
|
||||
`)
|
||||
|
||||
// Clients table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
membershipType TEXT NOT NULL CHECK (membershipType IN ('basic', 'premium', 'vip')),
|
||||
membershipStatus TEXT NOT NULL CHECK (membershipStatus IN ('active', 'inactive', 'expired')),
|
||||
joinDate DATETIME NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
// Fitness profiles table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS fitness_profiles (
|
||||
userId TEXT PRIMARY KEY,
|
||||
height TEXT NOT NULL,
|
||||
weight TEXT NOT NULL,
|
||||
age TEXT NOT NULL,
|
||||
gender TEXT NOT NULL CHECK (gender IN ('male', 'female', 'other')),
|
||||
activityLevel TEXT NOT NULL CHECK (activityLevel IN ('sedentary', 'light', 'moderate', 'active', 'very_active')),
|
||||
fitnessGoals TEXT NOT NULL, -- JSON array
|
||||
exerciseHabits TEXT,
|
||||
dietHabits TEXT,
|
||||
medicalConditions TEXT,
|
||||
createdAt DATETIME NOT NULL,
|
||||
updatedAt DATETIME NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
|
||||
// Create indexes for better performance
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_clients_userId ON clients(userId);
|
||||
CREATE INDEX IF NOT EXISTS idx_fitness_profiles_userId ON fitness_profiles(userId);
|
||||
`)
|
||||
}
|
||||
|
||||
// User operations
|
||||
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const id = Math.random().toString(36).substr(2, 9)
|
||||
const now = new Date()
|
||||
|
||||
const user: User = {
|
||||
id,
|
||||
...userData,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(
|
||||
`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.phone, user.role, user.createdAt.toISOString(), user.updatedAt.toISOString()
|
||||
)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM users WHERE id = ?')
|
||||
const row = stmt.get(id)
|
||||
|
||||
return row ? this.mapRowToUser(row) : null
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM users WHERE email = ?')
|
||||
const row = stmt.get(email)
|
||||
|
||||
return row ? this.mapRowToUser(row) : null
|
||||
}
|
||||
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM users ORDER BY createdAt DESC')
|
||||
const rows = stmt.all()
|
||||
|
||||
return rows.map(row => this.mapRowToUser(row))
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: Partial<User>): Promise<User | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const fields = Object.keys(updates).filter(key => key !== 'id')
|
||||
if (fields.length === 0) return this.getUserById(id)
|
||||
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ')
|
||||
const values = fields.map(field => (updates as any)[field])
|
||||
values.push(new Date().toISOString()) // updatedAt
|
||||
values.push(id)
|
||||
|
||||
const stmt = this.db.prepare(`UPDATE users SET ${setClause}, updatedAt = ? WHERE id = ?`)
|
||||
stmt.run(values)
|
||||
|
||||
return this.getUserById(id)
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<boolean> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('DELETE FROM users WHERE id = ?')
|
||||
const result = stmt.run(id)
|
||||
return (result.changes || 0) > 0
|
||||
}
|
||||
|
||||
// Client operations
|
||||
async createClient(clientData: Omit<Client, 'id'>): Promise<Client> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const id = Math.random().toString(36).substr(2, 9)
|
||||
const client: Client = { id, ...clientData }
|
||||
|
||||
const stmt = this.db.prepare(
|
||||
`INSERT INTO clients (id, userId, membershipType, membershipStatus, joinDate)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
)
|
||||
|
||||
stmt.run(
|
||||
client.id, client.userId, client.membershipType,
|
||||
client.membershipStatus, client.joinDate.toISOString()
|
||||
)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
async getClientById(id: string): Promise<Client | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM clients WHERE id = ?')
|
||||
const row = stmt.get(id)
|
||||
return row ? this.mapRowToClient(row) : null
|
||||
}
|
||||
|
||||
async getClientByUserId(userId: string): Promise<Client | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM clients WHERE userId = ?')
|
||||
const row = stmt.get(userId)
|
||||
return row ? this.mapRowToClient(row) : null
|
||||
}
|
||||
|
||||
async getAllClients(): Promise<Client[]> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM clients ORDER BY joinDate DESC')
|
||||
const rows = stmt.all()
|
||||
return rows.map(row => this.mapRowToClient(row))
|
||||
}
|
||||
|
||||
async updateClient(id: string, updates: Partial<Client>): Promise<Client | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const fields = Object.keys(updates).filter(key => key !== 'id')
|
||||
if (fields.length === 0) return this.getClientById(id)
|
||||
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ')
|
||||
const values = fields.map(field => (updates as any)[field])
|
||||
values.push(id)
|
||||
|
||||
const stmt = this.db.prepare(`UPDATE clients SET ${setClause} WHERE id = ?`)
|
||||
stmt.run(values)
|
||||
|
||||
return this.getClientById(id)
|
||||
}
|
||||
|
||||
async deleteClient(id: string): Promise<boolean> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('DELETE FROM clients WHERE id = ?')
|
||||
const result = stmt.run(id)
|
||||
return (result.changes || 0) > 0
|
||||
}
|
||||
|
||||
// Fitness Profile operations
|
||||
async createFitnessProfile(profileData: Omit<FitnessProfile, 'createdAt' | 'updatedAt'>): Promise<FitnessProfile> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const now = new Date()
|
||||
const profile: FitnessProfile = {
|
||||
...profileData,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(
|
||||
`INSERT INTO fitness_profiles
|
||||
(userId, height, weight, age, gender, activityLevel, fitnessGoals,
|
||||
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,
|
||||
profile.dietHabits, profile.medicalConditions, profile.createdAt.toISOString(),
|
||||
profile.updatedAt.toISOString()
|
||||
)
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
async getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM fitness_profiles WHERE userId = ?')
|
||||
const row = stmt.get(userId)
|
||||
|
||||
return row ? this.mapRowToFitnessProfile(row) : null
|
||||
}
|
||||
|
||||
async getAllFitnessProfiles(): Promise<FitnessProfile[]> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('SELECT * FROM fitness_profiles ORDER BY createdAt DESC')
|
||||
const rows = stmt.all()
|
||||
return rows.map(row => this.mapRowToFitnessProfile(row))
|
||||
}
|
||||
|
||||
async updateFitnessProfile(userId: string, updates: Partial<FitnessProfile>): Promise<FitnessProfile | null> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const fields = Object.keys(updates).filter(key => key !== 'userId' && key !== 'createdAt')
|
||||
if (fields.length === 0) return this.getFitnessProfileByUserId(userId)
|
||||
|
||||
const setClause = fields.map(field => `${field} = ?`).join(', ')
|
||||
const values = fields.map(field => {
|
||||
const value = (updates as any)[field]
|
||||
return field === 'fitnessGoals' ? JSON.stringify(value) : value
|
||||
})
|
||||
values.push(new Date().toISOString()) // updatedAt
|
||||
values.push(userId)
|
||||
|
||||
const stmt = this.db.prepare(`UPDATE fitness_profiles SET ${setClause}, updatedAt = ? WHERE userId = ?`)
|
||||
stmt.run(values)
|
||||
|
||||
return this.getFitnessProfileByUserId(userId)
|
||||
}
|
||||
|
||||
async deleteFitnessProfile(userId: string): Promise<boolean> {
|
||||
if (!this.db) throw new Error('Database not connected')
|
||||
|
||||
const stmt = this.db.prepare('DELETE FROM fitness_profiles WHERE userId = ?')
|
||||
const result = stmt.run(userId)
|
||||
return (result.changes || 0) > 0
|
||||
}
|
||||
|
||||
// Helper methods to map database rows to entities
|
||||
private mapRowToUser(row: any): User {
|
||||
return {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
firstName: row.firstName,
|
||||
lastName: row.lastName,
|
||||
password: row.password,
|
||||
phone: row.phone,
|
||||
role: row.role,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private mapRowToClient(row: any): Client {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.userId,
|
||||
membershipType: row.membershipType,
|
||||
membershipStatus: row.membershipStatus,
|
||||
joinDate: new Date(row.joinDate)
|
||||
}
|
||||
}
|
||||
|
||||
private mapRowToFitnessProfile(row: any): FitnessProfile {
|
||||
return {
|
||||
userId: row.userId,
|
||||
height: row.height,
|
||||
weight: row.weight,
|
||||
age: row.age,
|
||||
gender: row.gender,
|
||||
activityLevel: row.activityLevel,
|
||||
fitnessGoals: JSON.parse(row.fitnessGoals || '[]'),
|
||||
exerciseHabits: row.exerciseHabits,
|
||||
dietHabits: row.dietHabits,
|
||||
medicalConditions: row.medicalConditions,
|
||||
createdAt: new Date(row.createdAt),
|
||||
updatedAt: new Date(row.updatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,184 @@
|
||||
// Database Entity Types
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password: string
|
||||
phone?: string
|
||||
role: 'admin' | 'client'
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
phone?: string;
|
||||
role: "superAdmin" | "admin" | "trainer" | "client";
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
id: string
|
||||
userId: string
|
||||
membershipType: 'basic' | 'premium' | 'vip'
|
||||
membershipStatus: 'active' | 'inactive' | 'expired'
|
||||
joinDate: Date
|
||||
id: string;
|
||||
userId: string;
|
||||
membershipType: "basic" | "premium" | "vip";
|
||||
membershipStatus: "active" | "inactive" | "expired";
|
||||
joinDate: Date;
|
||||
lastVisit?: 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
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
userId: string;
|
||||
height: string;
|
||||
weight: string;
|
||||
age: string;
|
||||
gender: "male" | "female" | "other";
|
||||
activityLevel: "sedentary" | "lightly_active" | "moderately_active" | "very_active" | "extremely_active";
|
||||
fitnessGoals: string[];
|
||||
exerciseHabits: string;
|
||||
dietHabits: string;
|
||||
medicalConditions: string;
|
||||
allergies?: string;
|
||||
injuries?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Attendance {
|
||||
id: string;
|
||||
userId: string;
|
||||
checkInTime: Date;
|
||||
checkOutTime?: Date;
|
||||
type: "gym" | "class" | "personal_training";
|
||||
notes?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface Recommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
fitnessProfileId?: string;
|
||||
type: "short_term" | "medium_term" | "long_term" | "ai_plan";
|
||||
content: string;
|
||||
activityPlan?: string;
|
||||
dietPlan?: string;
|
||||
status: "pending" | "approved" | "rejected" | "completed";
|
||||
createdAt: Date;
|
||||
approvedAt?: Date;
|
||||
approvedBy?: string;
|
||||
}
|
||||
|
||||
export interface FitnessGoal {
|
||||
id: string;
|
||||
userId: string;
|
||||
fitnessProfileId?: string;
|
||||
goalType: "weight_target" | "strength_milestone" | "endurance_target" | "flexibility_goal" | "habit_building" | "custom";
|
||||
title: string;
|
||||
description?: string;
|
||||
targetValue?: number;
|
||||
currentValue?: number;
|
||||
unit?: string;
|
||||
startDate: Date;
|
||||
targetDate?: Date;
|
||||
completedDate?: Date;
|
||||
status: "active" | "completed" | "abandoned" | "paused";
|
||||
progress: number;
|
||||
priority: "low" | "medium" | "high";
|
||||
notes?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Database Interface - allows us to swap implementations
|
||||
export interface IDatabase {
|
||||
// Connection management
|
||||
connect(): Promise<void>
|
||||
disconnect(): Promise<void>
|
||||
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
// User operations
|
||||
createUser(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User>
|
||||
getUserById(id: string): Promise<User | null>
|
||||
getUserByEmail(email: string): Promise<User | null>
|
||||
getAllUsers(): Promise<User[]>
|
||||
updateUser(id: string, updates: Partial<User>): Promise<User | null>
|
||||
deleteUser(id: string): Promise<boolean>
|
||||
|
||||
createUser(
|
||||
user: Omit<User, "createdAt" | "updatedAt" | "id"> & { id?: string },
|
||||
): Promise<User>;
|
||||
getUserById(id: string): Promise<User | null>;
|
||||
getUserByEmail(email: string): Promise<User | null>;
|
||||
getAllUsers(): Promise<User[]>;
|
||||
updateUser(id: string, updates: Partial<User>): Promise<User | null>;
|
||||
deleteUser(id: string): Promise<boolean>;
|
||||
migrateUserId(oldId: string, newId: string): Promise<void>;
|
||||
|
||||
// Client operations
|
||||
createClient(client: Omit<Client, 'id'>): Promise<Client>
|
||||
getClientById(id: string): Promise<Client | null>
|
||||
getClientByUserId(userId: string): Promise<Client | null>
|
||||
getAllClients(): Promise<Client[]>
|
||||
updateClient(id: string, updates: Partial<Client>): Promise<Client | null>
|
||||
deleteClient(id: string): Promise<boolean>
|
||||
|
||||
createClient(client: Omit<Client, "id">): Promise<Client>;
|
||||
getClientById(id: string): Promise<Client | null>;
|
||||
getClientByUserId(userId: string): Promise<Client | null>;
|
||||
getAllClients(): Promise<Client[]>;
|
||||
updateClient(id: string, updates: Partial<Client>): Promise<Client | null>;
|
||||
deleteClient(id: string): Promise<boolean>;
|
||||
|
||||
// Fitness Profile operations
|
||||
createFitnessProfile(profile: Omit<FitnessProfile, 'createdAt' | 'updatedAt'>): Promise<FitnessProfile>
|
||||
getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null>
|
||||
getAllFitnessProfiles(): Promise<FitnessProfile[]>
|
||||
updateFitnessProfile(userId: string, updates: Partial<FitnessProfile>): Promise<FitnessProfile | null>
|
||||
deleteFitnessProfile(userId: string): Promise<boolean>
|
||||
createFitnessProfile(
|
||||
profile: Omit<FitnessProfile, "createdAt" | "updatedAt">,
|
||||
): Promise<FitnessProfile>;
|
||||
getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null>;
|
||||
getAllFitnessProfiles(): Promise<FitnessProfile[]>;
|
||||
updateFitnessProfile(
|
||||
userId: string,
|
||||
updates: Partial<FitnessProfile>,
|
||||
): Promise<FitnessProfile | null>;
|
||||
deleteFitnessProfile(userId: string): Promise<boolean>;
|
||||
|
||||
// Attendance operations
|
||||
checkIn(
|
||||
userId: string,
|
||||
type: "gym" | "class" | "personal_training",
|
||||
notes?: string,
|
||||
): Promise<Attendance>;
|
||||
checkOut(attendanceId: string): Promise<Attendance | null>;
|
||||
getAttendanceHistory(userId: string): Promise<Attendance[]>;
|
||||
getAllAttendance(): Promise<Attendance[]>;
|
||||
getActiveCheckIn(userId: string): Promise<Attendance | null>;
|
||||
|
||||
// Recommendation operations
|
||||
createRecommendation(
|
||||
recommendation: Omit<Recommendation, "createdAt" | "approvedAt" | "approvedBy">,
|
||||
): Promise<Recommendation>;
|
||||
getRecommendationsByUserId(userId: string): Promise<Recommendation[]>;
|
||||
getAllRecommendations(): Promise<Recommendation[]>;
|
||||
updateRecommendation(
|
||||
id: string,
|
||||
updates: Partial<Recommendation>,
|
||||
): Promise<Recommendation | null>;
|
||||
deleteRecommendation(id: string): Promise<boolean>;
|
||||
|
||||
// Fitness Goals operations
|
||||
createFitnessGoal(
|
||||
goal: Omit<FitnessGoal, "createdAt" | "updatedAt">
|
||||
): Promise<FitnessGoal>;
|
||||
getFitnessGoalById(id: string): Promise<FitnessGoal | null>;
|
||||
getFitnessGoalsByUserId(userId: string, status?: string): Promise<FitnessGoal[]>;
|
||||
updateFitnessGoal(
|
||||
id: string,
|
||||
updates: Partial<FitnessGoal>
|
||||
): Promise<FitnessGoal | null>;
|
||||
deleteFitnessGoal(id: string): Promise<boolean>;
|
||||
updateGoalProgress(id: string, currentValue: number): Promise<FitnessGoal | null>;
|
||||
completeGoal(id: string): Promise<FitnessGoal | null>;
|
||||
|
||||
// Dashboard operations
|
||||
getDashboardStats(): Promise<{
|
||||
totalUsers: number;
|
||||
activeClients: number;
|
||||
totalRevenue: number;
|
||||
revenueGrowth: number; // Percentage vs last month
|
||||
}>;
|
||||
}
|
||||
|
||||
// Database configuration
|
||||
export interface DatabaseConfig {
|
||||
type: 'sqlite' | 'postgresql' | 'mysql' | 'mongodb'
|
||||
type: "sqlite" | "postgresql" | "mysql" | "mongodb";
|
||||
connection: {
|
||||
filename?: string // for SQLite
|
||||
host?: string // for SQL databases
|
||||
port?: number
|
||||
database?: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
filename?: string; // for SQLite
|
||||
host?: string; // for SQL databases
|
||||
port?: number;
|
||||
database?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
options?: {
|
||||
logging?: boolean
|
||||
poolSize?: number
|
||||
timeout?: number
|
||||
}
|
||||
}
|
||||
logging?: boolean;
|
||||
poolSize?: number;
|
||||
timeout?: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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 by ID, checking Clerk:', userId)
|
||||
const clerkUser = await currentUser()
|
||||
|
||||
if (!clerkUser || clerkUser.id !== userId) {
|
||||
// If we can't get the user from Clerk (e.g. running locally without full auth sync),
|
||||
// we might want to fail gracefully or throw.
|
||||
// For now, throw to be safe.
|
||||
throw new Error('Could not fetch Clerk user details')
|
||||
}
|
||||
|
||||
const email = clerkUser.emailAddresses[0]?.emailAddress
|
||||
if (!email) throw new Error('User has no email')
|
||||
|
||||
// Check if user exists by email (e.g. seeded user)
|
||||
const existingByEmail = await db.getUserByEmail(email)
|
||||
if (existingByEmail) {
|
||||
console.log('User found by email but ID mismatch. Migrating ID...', {
|
||||
oldId: existingByEmail.id,
|
||||
newId: userId
|
||||
})
|
||||
|
||||
// Update the ID to match Clerk ID
|
||||
// We need to do this manually via SQL because IDatabase interface might not expose a direct ID update method easily
|
||||
// But we can use a raw query if we had access, or add a method.
|
||||
// Since we don't have direct access to `db.db` here (it's hidden behind IDatabase),
|
||||
// we should add a method to IDatabase or use a workaround.
|
||||
// Actually, `SQLiteDatabase` is what we have at runtime.
|
||||
// Let's assume we can cast it or add `updateUserId` to the interface.
|
||||
|
||||
// For now, let's try to update it using `updateUser` but `updateUser` usually updates fields based on ID.
|
||||
// We can't update the ID itself using `updateUser(id, { id: newId })` because `updateUser` implementation filters out `id` from updates.
|
||||
|
||||
// We need to add a method to migrate user ID in IDatabase.
|
||||
// Or, strictly for this prototype, we can delete the old user and create a new one (DATA LOSS RISK).
|
||||
// But since it's a seeded super admin with no data, it's fine.
|
||||
// However, if they had clients, we'd lose the link.
|
||||
|
||||
// Let's add `migrateUserId(oldId, newId)` to IDatabase.
|
||||
await db.migrateUserId(existingByEmail.id, userId)
|
||||
return db.getUserById(userId)
|
||||
}
|
||||
|
||||
console.log('Creating new user from Clerk data:', userId)
|
||||
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' | 'superAdmin') || 'client'
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
+11
-23
@@ -1,26 +1,14 @@
|
||||
export const formatDate = (date: Date): string => {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(date)
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const formatCurrency = (
|
||||
amount: number,
|
||||
currency: string = 'USD'
|
||||
): string => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
export const calculateDaysBetween = (startDate: Date, endDate: Date): number => {
|
||||
const timeDiff = endDate.getTime() - startDate.getTime()
|
||||
return Math.ceil(timeDiff / (1000 * 3600 * 24))
|
||||
}
|
||||
|
||||
export const generateId = (): string => {
|
||||
return Math.random().toString(36).substr(2, 9)
|
||||
export function formatDate(date: string | Date) {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
|
||||
|
||||
// Define routes that should be publicly accessible
|
||||
const isPublicRoute = createRouteMatcher([
|
||||
"/sign-in(.*)",
|
||||
"/sign-up(.*)",
|
||||
"/api/webhooks(.*)",
|
||||
"/api/attendance(.*)",
|
||||
]);
|
||||
|
||||
// Define routes that require authentication
|
||||
const isProtectedRoute = createRouteMatcher([
|
||||
"/",
|
||||
"/users(.*)",
|
||||
"/analytics(.*)",
|
||||
"/profile(.*)",
|
||||
"/api/users(.*)",
|
||||
"/api/profile(.*)",
|
||||
"/api/payments(.*)",
|
||||
"/api/notifications(.*)",
|
||||
]);
|
||||
|
||||
export default clerkMiddleware(async (auth, req) => {
|
||||
// Protect all routes except public ones
|
||||
if (!isPublicRoute(req)) {
|
||||
await auth.protect();
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Skip Next.js internals and all static files, unless found in search params
|
||||
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||
// Always run for API routes
|
||||
"/(api|trpc)(.*)",
|
||||
],
|
||||
};
|
||||
@@ -1,21 +1,76 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## user flow
|
||||
|
||||
superAdmin -> localAdmin/Gym -> localAdmin -> creates trainers -> can add users
|
||||
@@ -0,0 +1,12 @@
|
||||
# Clerk Authentication
|
||||
# Get these values from https://dashboard.clerk.com
|
||||
# Make sure to use the correct publishable key for your environment
|
||||
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cGxlYXNpbmctcGhlYXNhbnQtMjAuY2xlcmsuYWNjb3VudHMuZGV2JA
|
||||
|
||||
# API Configuration
|
||||
# Update this to point to your backend API
|
||||
EXPO_PUBLIC_API_URL=http://localhost:3000/api
|
||||
|
||||
# App Configuration
|
||||
EXPO_PUBLIC_APP_NAME=FitAI
|
||||
EXPO_PUBLIC_APP_VERSION=1.0.0
|
||||
@@ -0,0 +1,11 @@
|
||||
# npm configuration for FitAI mobile app
|
||||
# This ensures consistent dependency resolution across the team
|
||||
|
||||
# Use legacy peer deps to resolve React Native dependency conflicts
|
||||
legacy-peer-deps=true
|
||||
|
||||
# Prefer offline mode for faster installs (optional)
|
||||
# prefer-offline=true
|
||||
|
||||
# Save exact versions to package.json
|
||||
# save-exact=true
|
||||
@@ -0,0 +1,31 @@
|
||||
# Values used to calculate the hash in this folder name.
|
||||
# Should not depend on the absolute path of the project itself.
|
||||
# - AGP: 8.11.0.
|
||||
# - $NDK is the path to NDK 27.1.12297006.
|
||||
# - $PROJECT is the path to the parent folder of the root Gradle build file.
|
||||
# - $ABI is the ABI to be built with. The specific value doesn't contribute to the value of the hash.
|
||||
# - $HASH is the hash value computed from this text.
|
||||
# - $CMAKE is the path to CMake 3.22.1.
|
||||
# - $NINJA is the path to Ninja.
|
||||
-H/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup
|
||||
-DCMAKE_SYSTEM_NAME=Android
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
-DCMAKE_SYSTEM_VERSION=26
|
||||
-DANDROID_PLATFORM=android-26
|
||||
-DANDROID_ABI=$ABI
|
||||
-DCMAKE_ANDROID_ARCH_ABI=$ABI
|
||||
-DANDROID_NDK=$NDK
|
||||
-DCMAKE_ANDROID_NDK=$NDK
|
||||
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake
|
||||
-DCMAKE_MAKE_PROGRAM=$NINJA
|
||||
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=$PROJECT/app/build/intermediates/cxx/Debug/$HASH/obj/$ABI
|
||||
-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=$PROJECT/app/build/intermediates/cxx/Debug/$HASH/obj/$ABI
|
||||
-DCMAKE_BUILD_TYPE=Debug
|
||||
-DCMAKE_FIND_ROOT_PATH=$PROJECT/app/.cxx/Debug/$HASH/prefab/$ABI/prefab
|
||||
-B$PROJECT/app/.cxx/Debug/$HASH/$ABI
|
||||
-GNinja
|
||||
-DPROJECT_BUILD_DIR=$PROJECT/app/build
|
||||
-DPROJECT_ROOT_DIR=$PROJECT
|
||||
-DREACT_ANDROID_DIR=/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid
|
||||
-DANDROID_STL=c++_shared
|
||||
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
if(NOT TARGET ReactAndroid::hermestooling)
|
||||
add_library(ReactAndroid::hermestooling SHARED IMPORTED)
|
||||
set_target_properties(ReactAndroid::hermestooling PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/hermestooling/libs/android.x86/libhermestooling.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/hermestooling/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET ReactAndroid::jsi)
|
||||
add_library(ReactAndroid::jsi SHARED IMPORTED)
|
||||
set_target_properties(ReactAndroid::jsi PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/jsi/libs/android.x86/libjsi.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/jsi/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET ReactAndroid::reactnative)
|
||||
add_library(ReactAndroid::reactnative SHARED IMPORTED)
|
||||
set_target_properties(ReactAndroid::reactnative PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/reactnative/libs/android.x86/libreactnative.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/reactnative/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION 0.81.5)
|
||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
if(NOT TARGET fbjni::fbjni)
|
||||
add_library(fbjni::fbjni SHARED IMPORTED)
|
||||
set_target_properties(fbjni::fbjni PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/488fe6e480efb97ec75859f6e274282f/transformed/fbjni-0.7.0/prefab/modules/fbjni/libs/android.x86/libfbjni.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/488fe6e480efb97ec75859f6e274282f/transformed/fbjni-0.7.0/prefab/modules/fbjni/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION 3.22.1)
|
||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
if(NOT TARGET hermes-engine::libhermes)
|
||||
add_library(hermes-engine::libhermes SHARED IMPORTED)
|
||||
set_target_properties(hermes-engine::libhermes PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/f3d279cf13499f4c93e2b512ac9499e6/transformed/hermes-android-0.81.5-debug/prefab/modules/libhermes/libs/android.x86/libhermes.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/f3d279cf13499f4c93e2b512ac9499e6/transformed/hermes-android-0.81.5-debug/prefab/modules/libhermes/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION 0.81.5)
|
||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
if(NOT TARGET ReactAndroid::hermestooling)
|
||||
add_library(ReactAndroid::hermestooling SHARED IMPORTED)
|
||||
set_target_properties(ReactAndroid::hermestooling PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/hermestooling/libs/android.x86_64/libhermestooling.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/hermestooling/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET ReactAndroid::jsi)
|
||||
add_library(ReactAndroid::jsi SHARED IMPORTED)
|
||||
set_target_properties(ReactAndroid::jsi PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/jsi/libs/android.x86_64/libjsi.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/jsi/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET ReactAndroid::reactnative)
|
||||
add_library(ReactAndroid::reactnative SHARED IMPORTED)
|
||||
set_target_properties(ReactAndroid::reactnative PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/reactnative/libs/android.x86_64/libreactnative.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/reactnative/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION 0.81.5)
|
||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
if(NOT TARGET fbjni::fbjni)
|
||||
add_library(fbjni::fbjni SHARED IMPORTED)
|
||||
set_target_properties(fbjni::fbjni PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/488fe6e480efb97ec75859f6e274282f/transformed/fbjni-0.7.0/prefab/modules/fbjni/libs/android.x86_64/libfbjni.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/488fe6e480efb97ec75859f6e274282f/transformed/fbjni-0.7.0/prefab/modules/fbjni/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION 3.22.1)
|
||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
if(NOT TARGET hermes-engine::libhermes)
|
||||
add_library(hermes-engine::libhermes SHARED IMPORTED)
|
||||
set_target_properties(hermes-engine::libhermes PROPERTIES
|
||||
IMPORTED_LOCATION "/home/echo/.gradle/caches/8.14.3/transforms/f3d279cf13499f4c93e2b512ac9499e6/transformed/hermes-android-0.81.5-debug/prefab/modules/libhermes/libs/android.x86_64/libhermes.so"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "/home/echo/.gradle/caches/8.14.3/transforms/f3d279cf13499f4c93e2b512ac9499e6/transformed/hermes-android-0.81.5-debug/prefab/modules/libhermes/include"
|
||||
INTERFACE_LINK_LIBRARIES ""
|
||||
)
|
||||
endif()
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION 0.81.5)
|
||||
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+1439
File diff suppressed because it is too large.
Load diff
+851
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"inputs" :
|
||||
[
|
||||
{
|
||||
"path" : "CMakeLists.txt"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineSystem.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/android.toolchain.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/android-legacy.toolchain.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/abis.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/platforms.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/hooks/pre/Android-Determine.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystem.cmake.in"
|
||||
},
|
||||
{
|
||||
"isGenerated" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86/CMakeFiles/3.22.1-g37088a8/CMakeSystem.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/android.toolchain.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInitialize.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Initialize.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/hooks/pre/Android-Initialize.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine-C.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android/Determine-Compiler.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/hooks/pre/Determine-Compiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerId.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCompilerIdDetection.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ADSP-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMCC-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMClang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/AppleClang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Borland-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Bruce-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Compaq-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Cray-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Embarcadero-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Fujitsu-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/FujitsuClang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GHS-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/HP-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IAR-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Intel-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IntelLLVM-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/MSVC-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVHPC-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVIDIA-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/OpenWatcom-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PGI-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PathScale-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SCO-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SDCC-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SunPro-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TI-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TinyCC-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/VisualAge-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Watcom-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XL-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XLClang-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/zOS-C-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-C-DetermineVersionInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeFindBinUtils.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-FindBinUtils.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompiler.cmake.in"
|
||||
},
|
||||
{
|
||||
"isGenerated" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86/CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCXXCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Determine-CXX.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android/Determine-Compiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerId.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCompilerIdDetection.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ADSP-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMCC-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/ARMClang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/AppleClang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Borland-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-DetermineCompilerInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Comeau-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Compaq-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Cray-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Embarcadero-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Fujitsu-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/FujitsuClang-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GHS-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/HP-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IAR-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Intel-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IntelLLVM-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/MSVC-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVHPC-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/NVIDIA-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/OpenWatcom-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PGI-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/PathScale-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SCO-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/SunPro-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/TI-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/VisualAge-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-CXX-DetermineVersionInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Watcom-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XL-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-CXX-DetermineVersionInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/XLClang-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/zOS-CXX-DetermineCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/IBMCPP-CXX-DetermineVersionInternal.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeFindBinUtils.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-FindBinUtils.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCXXCompiler.cmake.in"
|
||||
},
|
||||
{
|
||||
"isGenerated" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86/CMakeFiles/3.22.1-g37088a8/CMakeCXXCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeSystemSpecificInformation.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeGenericSystem.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeInitializeConfigs.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/hooks/pre/Android.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Linux.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/UnixPaths.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCInformation.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeLanguageInformation.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-C.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/CMakeCommonCompilerMacros.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/GNU.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/CMakeCommonCompilerMacros.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang-C.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/hooks/pre/Android-Clang.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/flags.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCommonLanguageInclude.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerABI.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitIncludeInfo.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitLinkInfo.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseLibraryArchitecture.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompilerABI.c"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompileFeatures.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Internal/FeatureTesting.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCCompiler.cmake.in"
|
||||
},
|
||||
{
|
||||
"isGenerated" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86/CMakeFiles/3.22.1-g37088a8/CMakeCCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCXXInformation.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeLanguageInformation.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang-CXX.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Compiler/Clang.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang-CXX.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Platform/Android-Clang.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCommonLanguageInclude.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCXXCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompilerABI.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitIncludeInfo.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseImplicitLinkInfo.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeParseLibraryArchitecture.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCompilerCommon.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCXXCompilerABI.cpp"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeDetermineCompileFeatures.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/Internal/FeatureTesting.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeCXXCompiler.cmake.in"
|
||||
},
|
||||
{
|
||||
"isGenerated" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86/CMakeFiles/3.22.1-g37088a8/CMakeCXXCompiler.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactCommon/cmake-utils/react-native-flags.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CheckIPOSupported.cmake"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CheckIPOSupported/CMakeLists-CXX.txt.in"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CheckIPOSupported/foo.cpp"
|
||||
},
|
||||
{
|
||||
"isCMake" : true,
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22/Modules/CheckIPOSupported/main.cpp"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab/lib/i686-linux-android/cmake/ReactAndroid/ReactAndroidConfigVersion.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab/lib/i686-linux-android/cmake/ReactAndroid/ReactAndroidConfig.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab/lib/i686-linux-android/cmake/fbjni/fbjniConfigVersion.cmake"
|
||||
},
|
||||
{
|
||||
"isExternal" : true,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab/lib/i686-linux-android/cmake/fbjni/fbjniConfig.cmake"
|
||||
}
|
||||
],
|
||||
"kind" : "cmakeFiles",
|
||||
"paths" :
|
||||
{
|
||||
"build" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86",
|
||||
"source" : "/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup"
|
||||
},
|
||||
"version" :
|
||||
{
|
||||
"major" : 1,
|
||||
"minor" : 0
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"configurations" :
|
||||
[
|
||||
{
|
||||
"directories" :
|
||||
[
|
||||
{
|
||||
"build" : ".",
|
||||
"jsonFile" : "directory-.-Debug-f5ebdc15457944623624.json",
|
||||
"minimumCMakeVersion" :
|
||||
{
|
||||
"string" : "3.13"
|
||||
},
|
||||
"projectIndex" : 0,
|
||||
"source" : ".",
|
||||
"targetIndexes" :
|
||||
[
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"name" : "Debug",
|
||||
"projects" :
|
||||
[
|
||||
{
|
||||
"directoryIndexes" :
|
||||
[
|
||||
0
|
||||
],
|
||||
"name" : "appmodules",
|
||||
"targetIndexes" :
|
||||
[
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"targets" :
|
||||
[
|
||||
{
|
||||
"directoryIndex" : 0,
|
||||
"id" : "appmodules::@6890427a1f51a3e7e1df",
|
||||
"jsonFile" : "target-appmodules-Debug-9ef032710fe355da9383.json",
|
||||
"name" : "appmodules",
|
||||
"projectIndex" : 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"kind" : "codemodel",
|
||||
"paths" :
|
||||
{
|
||||
"build" : "/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86",
|
||||
"source" : "/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup"
|
||||
},
|
||||
"version" :
|
||||
{
|
||||
"major" : 2,
|
||||
"minor" : 3
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"backtraceGraph" :
|
||||
{
|
||||
"commands" : [],
|
||||
"files" : [],
|
||||
"nodes" : []
|
||||
},
|
||||
"installers" : [],
|
||||
"paths" :
|
||||
{
|
||||
"build" : ".",
|
||||
"source" : "."
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"cmake" :
|
||||
{
|
||||
"generator" :
|
||||
{
|
||||
"multiConfig" : false,
|
||||
"name" : "Ninja"
|
||||
},
|
||||
"paths" :
|
||||
{
|
||||
"cmake" : "/home/echo/Android/Sdk/cmake/3.22.1/bin/cmake",
|
||||
"cpack" : "/home/echo/Android/Sdk/cmake/3.22.1/bin/cpack",
|
||||
"ctest" : "/home/echo/Android/Sdk/cmake/3.22.1/bin/ctest",
|
||||
"root" : "/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22"
|
||||
},
|
||||
"version" :
|
||||
{
|
||||
"isDirty" : false,
|
||||
"major" : 3,
|
||||
"minor" : 22,
|
||||
"patch" : 1,
|
||||
"string" : "3.22.1-g37088a8",
|
||||
"suffix" : "g37088a8"
|
||||
}
|
||||
},
|
||||
"objects" :
|
||||
[
|
||||
{
|
||||
"jsonFile" : "codemodel-v2-d483cb7bfe026ad3a64f.json",
|
||||
"kind" : "codemodel",
|
||||
"version" :
|
||||
{
|
||||
"major" : 2,
|
||||
"minor" : 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonFile" : "cache-v2-099ee692bdea7ed85643.json",
|
||||
"kind" : "cache",
|
||||
"version" :
|
||||
{
|
||||
"major" : 2,
|
||||
"minor" : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonFile" : "cmakeFiles-v1-b99aeefcc68e41c89ebf.json",
|
||||
"kind" : "cmakeFiles",
|
||||
"version" :
|
||||
{
|
||||
"major" : 1,
|
||||
"minor" : 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply" :
|
||||
{
|
||||
"client-agp" :
|
||||
{
|
||||
"cache-v2" :
|
||||
{
|
||||
"jsonFile" : "cache-v2-099ee692bdea7ed85643.json",
|
||||
"kind" : "cache",
|
||||
"version" :
|
||||
{
|
||||
"major" : 2,
|
||||
"minor" : 0
|
||||
}
|
||||
},
|
||||
"cmakeFiles-v1" :
|
||||
{
|
||||
"jsonFile" : "cmakeFiles-v1-b99aeefcc68e41c89ebf.json",
|
||||
"kind" : "cmakeFiles",
|
||||
"version" :
|
||||
{
|
||||
"major" : 1,
|
||||
"minor" : 0
|
||||
}
|
||||
},
|
||||
"codemodel-v2" :
|
||||
{
|
||||
"jsonFile" : "codemodel-v2-d483cb7bfe026ad3a64f.json",
|
||||
"kind" : "codemodel",
|
||||
"version" :
|
||||
{
|
||||
"major" : 2,
|
||||
"minor" : 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"artifacts" :
|
||||
[
|
||||
{
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/build/intermediates/cxx/Debug/2z472e3c/obj/x86/libappmodules.so"
|
||||
}
|
||||
],
|
||||
"backtrace" : 3,
|
||||
"backtraceGraph" :
|
||||
{
|
||||
"commands" :
|
||||
[
|
||||
"add_library",
|
||||
"include",
|
||||
"target_link_libraries",
|
||||
"target_compile_options",
|
||||
"target_compile_reactnative_options",
|
||||
"target_compile_definitions",
|
||||
"target_include_directories"
|
||||
],
|
||||
"files" :
|
||||
[
|
||||
"/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake",
|
||||
"CMakeLists.txt",
|
||||
"/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactCommon/cmake-utils/react-native-flags.cmake"
|
||||
],
|
||||
"nodes" :
|
||||
[
|
||||
{
|
||||
"file" : 1
|
||||
},
|
||||
{
|
||||
"command" : 1,
|
||||
"file" : 1,
|
||||
"line" : 31,
|
||||
"parent" : 0
|
||||
},
|
||||
{
|
||||
"file" : 0,
|
||||
"parent" : 1
|
||||
},
|
||||
{
|
||||
"command" : 0,
|
||||
"file" : 0,
|
||||
"line" : 64,
|
||||
"parent" : 2
|
||||
},
|
||||
{
|
||||
"command" : 2,
|
||||
"file" : 0,
|
||||
"line" : 81,
|
||||
"parent" : 2
|
||||
},
|
||||
{
|
||||
"command" : 4,
|
||||
"file" : 0,
|
||||
"line" : 71,
|
||||
"parent" : 2
|
||||
},
|
||||
{
|
||||
"command" : 3,
|
||||
"file" : 2,
|
||||
"line" : 30,
|
||||
"parent" : 5
|
||||
},
|
||||
{
|
||||
"command" : 5,
|
||||
"file" : 2,
|
||||
"line" : 33,
|
||||
"parent" : 5
|
||||
},
|
||||
{
|
||||
"command" : 6,
|
||||
"file" : 0,
|
||||
"line" : 66,
|
||||
"parent" : 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"compileGroups" :
|
||||
[
|
||||
{
|
||||
"compileCommandFragments" :
|
||||
[
|
||||
{
|
||||
"fragment" : "-g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D__BIONIC_NO_PAGE_SIZE_MACRO -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fno-limit-debug-info -fPIC"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-Wall"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-Werror"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-fexceptions"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-frtti"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-std=c++20"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-DFOLLY_NO_CONFIG=1"
|
||||
},
|
||||
{
|
||||
"backtrace" : 6,
|
||||
"fragment" : "-DLOG_TAG=\\\"ReactNative\\\""
|
||||
}
|
||||
],
|
||||
"defines" :
|
||||
[
|
||||
{
|
||||
"backtrace" : 7,
|
||||
"define" : "RN_SERIALIZABLE_STATE"
|
||||
},
|
||||
{
|
||||
"define" : "appmodules_EXPORTS"
|
||||
}
|
||||
],
|
||||
"includes" :
|
||||
[
|
||||
{
|
||||
"backtrace" : 8,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup"
|
||||
},
|
||||
{
|
||||
"backtrace" : 8,
|
||||
"path" : "/home/echo/dev/prototype/apps/mobile/android/app/build/generated/autolinking/src/main/jni"
|
||||
},
|
||||
{
|
||||
"backtrace" : 4,
|
||||
"isSystem" : true,
|
||||
"path" : "/home/echo/.gradle/caches/8.14.3/transforms/488fe6e480efb97ec75859f6e274282f/transformed/fbjni-0.7.0/prefab/modules/fbjni/include"
|
||||
},
|
||||
{
|
||||
"backtrace" : 4,
|
||||
"isSystem" : true,
|
||||
"path" : "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/jsi/include"
|
||||
},
|
||||
{
|
||||
"backtrace" : 4,
|
||||
"isSystem" : true,
|
||||
"path" : "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/reactnative/include"
|
||||
}
|
||||
],
|
||||
"language" : "CXX",
|
||||
"sourceIndexes" :
|
||||
[
|
||||
0
|
||||
],
|
||||
"sysroot" :
|
||||
{
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id" : "appmodules::@6890427a1f51a3e7e1df",
|
||||
"link" :
|
||||
{
|
||||
"commandFragments" :
|
||||
[
|
||||
{
|
||||
"fragment" : "-Wl,--build-id=sha1 -Wl,--no-rosegment -Wl,--no-undefined-version -Wl,--fatal-warnings -Wl,--no-undefined -Qunused-arguments",
|
||||
"role" : "flags"
|
||||
},
|
||||
{
|
||||
"backtrace" : 4,
|
||||
"fragment" : "/home/echo/.gradle/caches/8.14.3/transforms/488fe6e480efb97ec75859f6e274282f/transformed/fbjni-0.7.0/prefab/modules/fbjni/libs/android.x86/libfbjni.so",
|
||||
"role" : "libraries"
|
||||
},
|
||||
{
|
||||
"backtrace" : 4,
|
||||
"fragment" : "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/jsi/libs/android.x86/libjsi.so",
|
||||
"role" : "libraries"
|
||||
},
|
||||
{
|
||||
"backtrace" : 4,
|
||||
"fragment" : "/home/echo/.gradle/caches/8.14.3/transforms/e5d515112484fe4dddc05785469cc380/transformed/react-android-0.81.5-debug/prefab/modules/reactnative/libs/android.x86/libreactnative.so",
|
||||
"role" : "libraries"
|
||||
},
|
||||
{
|
||||
"fragment" : "-latomic -lm",
|
||||
"role" : "libraries"
|
||||
}
|
||||
],
|
||||
"language" : "CXX",
|
||||
"sysroot" :
|
||||
{
|
||||
"path" : "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
|
||||
}
|
||||
},
|
||||
"name" : "appmodules",
|
||||
"nameOnDisk" : "libappmodules.so",
|
||||
"paths" :
|
||||
{
|
||||
"build" : ".",
|
||||
"source" : "."
|
||||
},
|
||||
"sourceGroups" :
|
||||
[
|
||||
{
|
||||
"name" : "Source Files",
|
||||
"sourceIndexes" :
|
||||
[
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"sources" :
|
||||
[
|
||||
{
|
||||
"backtrace" : 3,
|
||||
"compileGroupIndex" : 0,
|
||||
"path" : "OnLoad.cpp",
|
||||
"sourceGroupIndex" : 0
|
||||
}
|
||||
],
|
||||
"type" : "SHARED_LIBRARY"
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
# This is the CMakeCache file.
|
||||
# For build in directory: /home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86
|
||||
# It was generated by CMake: /home/echo/Android/Sdk/cmake/3.22.1/bin/cmake
|
||||
# You can edit this file to change values found and used by cmake.
|
||||
# If you do not want to change any of the values, simply exit the editor.
|
||||
# If you do want to change a value, simply edit, save, and exit the editor.
|
||||
# The syntax for the file is as follows:
|
||||
# KEY:TYPE=VALUE
|
||||
# KEY is the name of a variable in the cache.
|
||||
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
|
||||
# VALUE is the current value for the KEY.
|
||||
|
||||
########################
|
||||
# EXTERNAL cache entries
|
||||
########################
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
ANDROID_ABI:UNINITIALIZED=x86
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
ANDROID_NDK:UNINITIALIZED=/home/echo/Android/Sdk/ndk/27.1.12297006
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
ANDROID_PLATFORM:UNINITIALIZED=android-26
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
ANDROID_STL:UNINITIALIZED=c++_shared
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES:UNINITIALIZED=ON
|
||||
|
||||
//Path to a program.
|
||||
CCACHE_FOUND:FILEPATH=CCACHE_FOUND-NOTFOUND
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_ADDR2LINE:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-addr2line
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_ANDROID_ARCH_ABI:UNINITIALIZED=x86
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_ANDROID_NDK:UNINITIALIZED=/home/echo/Android/Sdk/ndk/27.1.12297006
|
||||
|
||||
//Archiver
|
||||
CMAKE_AR:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar
|
||||
|
||||
//Flags used by the compiler during all build types.
|
||||
CMAKE_ASM_FLAGS:STRING=
|
||||
|
||||
//Flags used by the compiler during debug builds.
|
||||
CMAKE_ASM_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the compiler during release builds.
|
||||
CMAKE_ASM_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Choose the type of build, options are: None Debug Release RelWithDebInfo
|
||||
// MinSizeRel ...
|
||||
CMAKE_BUILD_TYPE:STRING=Debug
|
||||
|
||||
//LLVM archiver
|
||||
CMAKE_CXX_COMPILER_AR:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar
|
||||
|
||||
//Generate index for LLVM archive
|
||||
CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib
|
||||
|
||||
//Flags used by the compiler during all build types.
|
||||
CMAKE_CXX_FLAGS:STRING=
|
||||
|
||||
//Flags used by the compiler during debug builds.
|
||||
CMAKE_CXX_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the CXX compiler during MINSIZEREL builds.
|
||||
CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
|
||||
|
||||
//Flags used by the compiler during release builds.
|
||||
CMAKE_CXX_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Flags used by the CXX compiler during RELWITHDEBINFO builds.
|
||||
CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
|
||||
|
||||
//Libraries linked by default with all C++ applications.
|
||||
CMAKE_CXX_STANDARD_LIBRARIES:STRING=-latomic -lm
|
||||
|
||||
//LLVM archiver
|
||||
CMAKE_C_COMPILER_AR:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar
|
||||
|
||||
//Generate index for LLVM archive
|
||||
CMAKE_C_COMPILER_RANLIB:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib
|
||||
|
||||
//Flags used by the compiler during all build types.
|
||||
CMAKE_C_FLAGS:STRING=
|
||||
|
||||
//Flags used by the compiler during debug builds.
|
||||
CMAKE_C_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the C compiler during MINSIZEREL builds.
|
||||
CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
|
||||
|
||||
//Flags used by the compiler during release builds.
|
||||
CMAKE_C_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Flags used by the C compiler during RELWITHDEBINFO builds.
|
||||
CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
|
||||
|
||||
//Libraries linked by default with all C applications.
|
||||
CMAKE_C_STANDARD_LIBRARIES:STRING=-latomic -lm
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_DLLTOOL:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-dlltool
|
||||
|
||||
//Flags used by the linker.
|
||||
CMAKE_EXE_LINKER_FLAGS:STRING=
|
||||
|
||||
//Flags used by the linker during DEBUG builds.
|
||||
CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the linker during MINSIZEREL builds.
|
||||
CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING=
|
||||
|
||||
//Flags used by the linker during RELEASE builds.
|
||||
CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Flags used by the linker during RELWITHDEBINFO builds.
|
||||
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING=
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_EXPORT_COMPILE_COMMANDS:UNINITIALIZED=ON
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_FIND_ROOT_PATH:UNINITIALIZED=/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab
|
||||
|
||||
//Install path prefix, prepended onto install directories.
|
||||
CMAKE_INSTALL_PREFIX:PATH=/usr/local
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_LIBRARY_OUTPUT_DIRECTORY:UNINITIALIZED=/home/echo/dev/prototype/apps/mobile/android/app/build/intermediates/cxx/Debug/2z472e3c/obj/x86
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_LINKER:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_MAKE_PROGRAM:UNINITIALIZED=/home/echo/Android/Sdk/cmake/3.22.1/bin/ninja
|
||||
|
||||
//Flags used by the linker during the creation of modules.
|
||||
CMAKE_MODULE_LINKER_FLAGS:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of modules during
|
||||
// DEBUG builds.
|
||||
CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of modules during
|
||||
// MINSIZEREL builds.
|
||||
CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of modules during
|
||||
// RELEASE builds.
|
||||
CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of modules during
|
||||
// RELWITHDEBINFO builds.
|
||||
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING=
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_NM:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_OBJCOPY:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objcopy
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_OBJDUMP:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_DESCRIPTION:STATIC=
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
|
||||
|
||||
//Value Computed by CMake
|
||||
CMAKE_PROJECT_NAME:STATIC=appmodules
|
||||
|
||||
//Ranlib
|
||||
CMAKE_RANLIB:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib
|
||||
|
||||
//Path to a program.
|
||||
CMAKE_READELF:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_RUNTIME_OUTPUT_DIRECTORY:UNINITIALIZED=/home/echo/dev/prototype/apps/mobile/android/app/build/intermediates/cxx/Debug/2z472e3c/obj/x86
|
||||
|
||||
//Flags used by the linker during the creation of dll's.
|
||||
CMAKE_SHARED_LINKER_FLAGS:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of shared libraries
|
||||
// during DEBUG builds.
|
||||
CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of shared libraries
|
||||
// during MINSIZEREL builds.
|
||||
CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of shared libraries
|
||||
// during RELEASE builds.
|
||||
CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of shared libraries
|
||||
// during RELWITHDEBINFO builds.
|
||||
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING=
|
||||
|
||||
//If set, runtime paths are not added when installing shared libraries,
|
||||
// but are added when building.
|
||||
CMAKE_SKIP_INSTALL_RPATH:BOOL=NO
|
||||
|
||||
//If set, runtime paths are not added when using shared libraries.
|
||||
CMAKE_SKIP_RPATH:BOOL=NO
|
||||
|
||||
//Flags used by the linker during the creation of static libraries
|
||||
// during all build types.
|
||||
CMAKE_STATIC_LINKER_FLAGS:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of static libraries
|
||||
// during DEBUG builds.
|
||||
CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of static libraries
|
||||
// during MINSIZEREL builds.
|
||||
CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of static libraries
|
||||
// during RELEASE builds.
|
||||
CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING=
|
||||
|
||||
//Flags used by the linker during the creation of static libraries
|
||||
// during RELWITHDEBINFO builds.
|
||||
CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING=
|
||||
|
||||
//Strip
|
||||
CMAKE_STRIP:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_SYSTEM_NAME:UNINITIALIZED=Android
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
CMAKE_SYSTEM_VERSION:UNINITIALIZED=26
|
||||
|
||||
//The CMake toolchain file
|
||||
CMAKE_TOOLCHAIN_FILE:FILEPATH=/home/echo/Android/Sdk/ndk/27.1.12297006/build/cmake/android.toolchain.cmake
|
||||
|
||||
//If this value is on, makefiles will be generated without the
|
||||
// .SILENT directive, and all commands will be echoed to the console
|
||||
// during the make. This is useful for debugging only. With Visual
|
||||
// Studio IDE projects all commands are done without /nologo.
|
||||
CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
PROJECT_BUILD_DIR:UNINITIALIZED=/home/echo/dev/prototype/apps/mobile/android/app/build
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
PROJECT_ROOT_DIR:UNINITIALIZED=/home/echo/dev/prototype/apps/mobile/android
|
||||
|
||||
//No help, variable specified on the command line.
|
||||
REACT_ANDROID_DIR:UNINITIALIZED=/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid
|
||||
|
||||
//The directory containing a CMake configuration file for ReactAndroid.
|
||||
ReactAndroid_DIR:PATH=/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab/lib/i686-linux-android/cmake/ReactAndroid
|
||||
|
||||
//Value Computed by CMake
|
||||
appmodules_BINARY_DIR:STATIC=/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86
|
||||
|
||||
//Value Computed by CMake
|
||||
appmodules_IS_TOP_LEVEL:STATIC=ON
|
||||
|
||||
//Value Computed by CMake
|
||||
appmodules_SOURCE_DIR:STATIC=/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup
|
||||
|
||||
//The directory containing a CMake configuration file for fbjni.
|
||||
fbjni_DIR:PATH=/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/prefab/x86/prefab/lib/i686-linux-android/cmake/fbjni
|
||||
|
||||
|
||||
########################
|
||||
# INTERNAL cache entries
|
||||
########################
|
||||
|
||||
//ADVANCED property for variable: CMAKE_ADDR2LINE
|
||||
CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_AR
|
||||
CMAKE_AR-ADVANCED:INTERNAL=1
|
||||
//This is the directory where this CMakeCache.txt was created
|
||||
CMAKE_CACHEFILE_DIR:INTERNAL=/home/echo/dev/prototype/apps/mobile/android/app/.cxx/Debug/2z472e3c/x86
|
||||
//Major version of cmake used to create the current loaded cache
|
||||
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3
|
||||
//Minor version of cmake used to create the current loaded cache
|
||||
CMAKE_CACHE_MINOR_VERSION:INTERNAL=22
|
||||
//Patch version of cmake used to create the current loaded cache
|
||||
CMAKE_CACHE_PATCH_VERSION:INTERNAL=1
|
||||
//Path to CMake executable.
|
||||
CMAKE_COMMAND:INTERNAL=/home/echo/Android/Sdk/cmake/3.22.1/bin/cmake
|
||||
//Path to cpack program executable.
|
||||
CMAKE_CPACK_COMMAND:INTERNAL=/home/echo/Android/Sdk/cmake/3.22.1/bin/cpack
|
||||
//Path to ctest program executable.
|
||||
CMAKE_CTEST_COMMAND:INTERNAL=/home/echo/Android/Sdk/cmake/3.22.1/bin/ctest
|
||||
//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR
|
||||
CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB
|
||||
CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_FLAGS
|
||||
CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG
|
||||
CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL
|
||||
CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE
|
||||
CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_CXX_STANDARD_LIBRARIES
|
||||
CMAKE_CXX_STANDARD_LIBRARIES-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_COMPILER_AR
|
||||
CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB
|
||||
CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_FLAGS
|
||||
CMAKE_C_FLAGS-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG
|
||||
CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL
|
||||
CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE
|
||||
CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_C_STANDARD_LIBRARIES
|
||||
CMAKE_C_STANDARD_LIBRARIES-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_DLLTOOL
|
||||
CMAKE_DLLTOOL-ADVANCED:INTERNAL=1
|
||||
//Executable file format
|
||||
CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF
|
||||
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS
|
||||
CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG
|
||||
CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL
|
||||
CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE
|
||||
CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
|
||||
//Name of external makefile project generator.
|
||||
CMAKE_EXTRA_GENERATOR:INTERNAL=
|
||||
//Name of generator.
|
||||
CMAKE_GENERATOR:INTERNAL=Ninja
|
||||
//Generator instance identifier.
|
||||
CMAKE_GENERATOR_INSTANCE:INTERNAL=
|
||||
//Name of generator platform.
|
||||
CMAKE_GENERATOR_PLATFORM:INTERNAL=
|
||||
//Name of generator toolset.
|
||||
CMAKE_GENERATOR_TOOLSET:INTERNAL=
|
||||
//Source directory with the top level CMakeLists.txt file for this
|
||||
// project
|
||||
CMAKE_HOME_DIRECTORY:INTERNAL=/home/echo/dev/prototype/apps/mobile/node_modules/react-native/ReactAndroid/cmake-utils/default-app-setup
|
||||
//Install .so files without execute permission.
|
||||
CMAKE_INSTALL_SO_NO_EXE:INTERNAL=0
|
||||
//ADVANCED property for variable: CMAKE_LINKER
|
||||
CMAKE_LINKER-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS
|
||||
CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG
|
||||
CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL
|
||||
CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE
|
||||
CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_NM
|
||||
CMAKE_NM-ADVANCED:INTERNAL=1
|
||||
//number of local generators
|
||||
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_OBJCOPY
|
||||
CMAKE_OBJCOPY-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_OBJDUMP
|
||||
CMAKE_OBJDUMP-ADVANCED:INTERNAL=1
|
||||
//Platform information initialized
|
||||
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_RANLIB
|
||||
CMAKE_RANLIB-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_READELF
|
||||
CMAKE_READELF-ADVANCED:INTERNAL=1
|
||||
//Path to CMake installation.
|
||||
CMAKE_ROOT:INTERNAL=/home/echo/Android/Sdk/cmake/3.22.1/share/cmake-3.22
|
||||
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS
|
||||
CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG
|
||||
CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL
|
||||
CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE
|
||||
CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH
|
||||
CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_SKIP_RPATH
|
||||
CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS
|
||||
CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG
|
||||
CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL
|
||||
CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE
|
||||
CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO
|
||||
CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
|
||||
//ADVANCED property for variable: CMAKE_STRIP
|
||||
CMAKE_STRIP-ADVANCED:INTERNAL=1
|
||||
//uname command
|
||||
CMAKE_UNAME:INTERNAL=/usr/bin/uname
|
||||
//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE
|
||||
CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
set(CMAKE_C_COMPILER "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/clang")
|
||||
set(CMAKE_C_COMPILER_ARG1 "")
|
||||
set(CMAKE_C_COMPILER_ID "Clang")
|
||||
set(CMAKE_C_COMPILER_VERSION "18.0.2")
|
||||
set(CMAKE_C_COMPILER_VERSION_INTERNAL "")
|
||||
set(CMAKE_C_COMPILER_WRAPPER "")
|
||||
set(CMAKE_C_STANDARD_COMPUTED_DEFAULT "17")
|
||||
set(CMAKE_C_EXTENSIONS_COMPUTED_DEFAULT "ON")
|
||||
set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert;c_std_17;c_std_23")
|
||||
set(CMAKE_C90_COMPILE_FEATURES "c_std_90;c_function_prototypes")
|
||||
set(CMAKE_C99_COMPILE_FEATURES "c_std_99;c_restrict;c_variadic_macros")
|
||||
set(CMAKE_C11_COMPILE_FEATURES "c_std_11;c_static_assert")
|
||||
set(CMAKE_C17_COMPILE_FEATURES "c_std_17")
|
||||
set(CMAKE_C23_COMPILE_FEATURES "c_std_23")
|
||||
|
||||
set(CMAKE_C_PLATFORM_ID "Linux")
|
||||
set(CMAKE_C_SIMULATE_ID "")
|
||||
set(CMAKE_C_COMPILER_FRONTEND_VARIANT "GNU")
|
||||
set(CMAKE_C_SIMULATE_VERSION "")
|
||||
|
||||
|
||||
|
||||
|
||||
set(CMAKE_AR "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar")
|
||||
set(CMAKE_C_COMPILER_AR "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar")
|
||||
set(CMAKE_RANLIB "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib")
|
||||
set(CMAKE_C_COMPILER_RANLIB "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib")
|
||||
set(CMAKE_LINKER "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin/ld.lld")
|
||||
set(CMAKE_MT "")
|
||||
set(CMAKE_COMPILER_IS_GNUCC )
|
||||
set(CMAKE_C_COMPILER_LOADED 1)
|
||||
set(CMAKE_C_COMPILER_WORKS TRUE)
|
||||
set(CMAKE_C_ABI_COMPILED TRUE)
|
||||
|
||||
set(CMAKE_C_COMPILER_ENV_VAR "CC")
|
||||
|
||||
set(CMAKE_C_COMPILER_ID_RUN 1)
|
||||
set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m)
|
||||
set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC)
|
||||
set(CMAKE_C_LINKER_PREFERENCE 10)
|
||||
|
||||
# Save compiler ABI information.
|
||||
set(CMAKE_C_SIZEOF_DATA_PTR "4")
|
||||
set(CMAKE_C_COMPILER_ABI "ELF")
|
||||
set(CMAKE_C_BYTE_ORDER "LITTLE_ENDIAN")
|
||||
set(CMAKE_C_LIBRARY_ARCHITECTURE "")
|
||||
|
||||
if(CMAKE_C_SIZEOF_DATA_PTR)
|
||||
set(CMAKE_SIZEOF_VOID_P "${CMAKE_C_SIZEOF_DATA_PTR}")
|
||||
endif()
|
||||
|
||||
if(CMAKE_C_COMPILER_ABI)
|
||||
set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_C_COMPILER_ABI}")
|
||||
endif()
|
||||
|
||||
if(CMAKE_C_LIBRARY_ARCHITECTURE)
|
||||
set(CMAKE_LIBRARY_ARCHITECTURE "")
|
||||
endif()
|
||||
|
||||
set(CMAKE_C_CL_SHOWINCLUDES_PREFIX "")
|
||||
if(CMAKE_C_CL_SHOWINCLUDES_PREFIX)
|
||||
set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_C_CL_SHOWINCLUDES_PREFIX}")
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
set(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/18/include;/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/i686-linux-android;/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include")
|
||||
set(CMAKE_C_IMPLICIT_LINK_LIBRARIES "-l:libunwind.a;dl;c;-l:libunwind.a;dl")
|
||||
set(CMAKE_C_IMPLICIT_LINK_DIRECTORIES "/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/18/lib/linux/i386;/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/26;/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android;/home/echo/Android/Sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib")
|
||||
set(CMAKE_C_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "")
|
||||
Loaded 100 of 327 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user