diff --git a/gym-management-system/.gitignore b/gym-management-system/.gitignore new file mode 100644 index 0000000000..a64814f6ec --- /dev/null +++ b/gym-management-system/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +dist-electron/ +release/ +storage/database.sqlite +storage/backups/*.sqlite +storage/uploads/* +storage/temp/* +.angular/ +*.log +.env diff --git a/gym-management-system/angular.json b/gym-management-system/angular.json new file mode 100644 index 0000000000..e013c34b69 --- /dev/null +++ b/gym-management-system/angular.json @@ -0,0 +1,69 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "gym-management-system": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "standalone": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/assets" + ], + "styles": [ + "node_modules/material-icons/iconfont/material-icons.css", + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "gym-management-system:build:production" + }, + "development": { + "buildTarget": "gym-management-system:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/gym-management-system/electron/database/init.ts b/gym-management-system/electron/database/init.ts new file mode 100644 index 0000000000..ad691c1137 --- /dev/null +++ b/gym-management-system/electron/database/init.ts @@ -0,0 +1,63 @@ +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcryptjs'; +import * as path from 'path'; +import * as fs from 'fs'; +import { app } from 'electron'; + +let prisma: PrismaClient; + +export function getPrisma(): PrismaClient { + if (!prisma) { + const dbPath = path.join(app.getPath('userData'), 'database.sqlite'); + const storageDir = path.dirname(dbPath); + + if (!fs.existsSync(storageDir)) { + fs.mkdirSync(storageDir, { recursive: true }); + } + + process.env['DATABASE_URL'] = `file:${dbPath}`; + prisma = new PrismaClient(); + } + return prisma; +} + +export async function initializeDatabase(): Promise { + const db = getPrisma(); + + // Create default admin user if not exists + const adminExists = await db.user.findUnique({ + where: { username: 'Admin' } + }); + + if (!adminExists) { + const hashedPassword = await bcrypt.hash('Admin', 10); + await db.user.create({ + data: { + username: 'Admin', + password: hashedPassword, + fullName: 'مدير النظام', + role: 'admin', + } + }); + console.log('Default admin user created: Admin/Admin'); + } + + // Create default settings + const settings = [ + { key: 'gym_name', value: 'النادي الرياضي' }, + { key: 'gym_phone', value: '' }, + { key: 'gym_address', value: '' }, + { key: 'currency', value: 'SAR' }, + { key: 'auto_backup', value: 'true' }, + { key: 'backup_time', value: '00:00' }, + { key: 'session_timeout', value: '30' }, + { key: 'theme', value: 'dark' }, + ]; + + for (const setting of settings) { + const exists = await db.setting.findUnique({ where: { key: setting.key } }); + if (!exists) { + await db.setting.create({ data: setting }); + } + } +} diff --git a/gym-management-system/electron/ipc/attendance.handlers.ts b/gym-management-system/electron/ipc/attendance.handlers.ts new file mode 100644 index 0000000000..35ad42ec56 --- /dev/null +++ b/gym-management-system/electron/ipc/attendance.handlers.ts @@ -0,0 +1,139 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerAttendanceHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('attendance:checkIn', async (_event, memberId: string) => { + try { + const member = await prisma.member.findUnique({ where: { id: memberId } }); + if (!member) { + return { success: false, error: 'العضو غير موجود' }; + } + + // Check for active subscription + const activeSubscription = await prisma.subscription.findFirst({ + where: { + memberId, + status: 'active', + endDate: { gte: new Date() }, + }, + }); + + if (!activeSubscription) { + return { success: false, error: 'لا يوجد اشتراك فعال لهذا العضو' }; + } + + const attendance = await prisma.attendance.create({ + data: { memberId }, + include: { member: true }, + }); + + return { success: true, data: attendance }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('attendance:checkInByQR', async (_event, qrCode: string) => { + try { + const member = await prisma.member.findUnique({ + where: { qrCode }, + }); + + if (!member) { + return { success: false, error: 'رمز QR غير صالح' }; + } + + // Check for active subscription + const activeSubscription = await prisma.subscription.findFirst({ + where: { + memberId: member.id, + status: 'active', + endDate: { gte: new Date() }, + }, + }); + + if (!activeSubscription) { + return { success: false, error: `لا يوجد اشتراك فعال للعضو: ${member.fullName}` }; + } + + const attendance = await prisma.attendance.create({ + data: { memberId: member.id }, + include: { member: true }, + }); + + return { success: true, data: { attendance, member } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('attendance:today', async () => { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const attendances = await prisma.attendance.findMany({ + where: { + checkIn: { gte: today }, + }, + include: { member: true }, + orderBy: { checkIn: 'desc' }, + }); + + return { success: true, data: attendances }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('attendance:history', async (_event, params?: any) => { + try { + const { page = 1, limit = 50, memberId, date } = params || {}; + const skip = (page - 1) * limit; + + const where: any = {}; + if (memberId) where.memberId = memberId; + if (date) { + const start = new Date(date); + start.setHours(0, 0, 0, 0); + const end = new Date(date); + end.setHours(23, 59, 59, 999); + where.checkIn = { gte: start, lte: end }; + } + + const [attendances, total] = await Promise.all([ + prisma.attendance.findMany({ + where, + skip, + take: limit, + include: { member: true }, + orderBy: { checkIn: 'desc' }, + }), + prisma.attendance.count({ where }), + ]); + + return { success: true, data: { attendances, total, page, limit } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('attendance:currentCount', async () => { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const count = await prisma.attendance.count({ + where: { + checkIn: { gte: today }, + }, + }); + + return { success: true, data: count }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/auth.handlers.ts b/gym-management-system/electron/ipc/auth.handlers.ts new file mode 100644 index 0000000000..7ec66a7d1d --- /dev/null +++ b/gym-management-system/electron/ipc/auth.handlers.ts @@ -0,0 +1,66 @@ +import { ipcMain } from 'electron'; +import * as bcrypt from 'bcryptjs'; +import * as jwt from 'jsonwebtoken'; +import { getPrisma } from '../database/init'; + +const JWT_SECRET = 'gym-management-secret-key-2024'; +const TOKEN_EXPIRY = '24h'; + +export function registerAuthHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('auth:login', async (_event, username: string, password: string) => { + try { + const user = await prisma.user.findUnique({ where: { username } }); + + if (!user || !user.isActive) { + return { success: false, error: 'اسم المستخدم أو كلمة المرور غير صحيحة' }; + } + + const isValid = await bcrypt.compare(password, user.password); + if (!isValid) { + return { success: false, error: 'اسم المستخدم أو كلمة المرور غير صحيحة' }; + } + + const token = jwt.sign( + { id: user.id, username: user.username, role: user.role }, + JWT_SECRET, + { expiresIn: TOKEN_EXPIRY } + ); + + return { + success: true, + data: { + token, + user: { + id: user.id, + username: user.username, + fullName: user.fullName, + role: user.role, + } + } + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('auth:logout', async () => { + return { success: true }; + }); + + ipcMain.handle('auth:getCurrentUser', async (_event) => { + // This would typically verify the token + return { success: true }; + }); + + ipcMain.handle('auth:changePassword', async (_event, oldPassword: string, newPassword: string) => { + try { + // Implementation would use current user context + const hashedPassword = await bcrypt.hash(newPassword, 10); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/backup.handlers.ts b/gym-management-system/electron/ipc/backup.handlers.ts new file mode 100644 index 0000000000..010ed2a250 --- /dev/null +++ b/gym-management-system/electron/ipc/backup.handlers.ts @@ -0,0 +1,49 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; +import { createBackup as createBackupFile, getBackupsList, restoreFromBackup, deleteBackupFile } from '../services/backup.service'; + +export function registerBackupHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('backup:create', async () => { + try { + const result = await createBackupFile(); + return { success: true, data: result }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('backup:getAll', async () => { + try { + const backups = await prisma.backup.findMany({ + orderBy: { createdAt: 'desc' }, + }); + return { success: true, data: backups }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('backup:restore', async (_event, filename: string) => { + try { + await restoreFromBackup(filename); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('backup:delete', async (_event, id: string) => { + try { + const backup = await prisma.backup.findUnique({ where: { id } }); + if (backup) { + deleteBackupFile(backup.filename); + await prisma.backup.delete({ where: { id } }); + } + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/dashboard.handlers.ts b/gym-management-system/electron/ipc/dashboard.handlers.ts new file mode 100644 index 0000000000..ccfc2d3cfc --- /dev/null +++ b/gym-management-system/electron/ipc/dashboard.handlers.ts @@ -0,0 +1,87 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerDashboardHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('dashboard:stats', async () => { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const [ + totalMembers, + attendanceToday, + expiredSubscriptions, + dailyRevenue, + ] = await Promise.all([ + prisma.member.count({ where: { isActive: true } }), + prisma.attendance.count({ where: { checkIn: { gte: today } } }), + prisma.subscription.count({ + where: { status: 'active', endDate: { lt: new Date() } }, + }), + prisma.payment.aggregate({ + _sum: { amount: true }, + where: { createdAt: { gte: today } }, + }), + ]); + + return { + success: true, + data: { + totalMembers, + attendanceToday, + expiredSubscriptions, + dailyRevenue: dailyRevenue._sum.amount || 0, + } + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('dashboard:monthlySubscriptions', async (_event, year: number) => { + try { + const monthlyData: number[] = []; + + for (let month = 0; month < 12; month++) { + const startDate = new Date(year, month, 1); + const endDate = new Date(year, month + 1, 0, 23, 59, 59, 999); + + const count = await prisma.subscription.count({ + where: { + createdAt: { gte: startDate, lte: endDate }, + }, + }); + monthlyData.push(count); + } + + return { success: true, data: monthlyData }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('dashboard:monthlyRevenue', async (_event, year: number) => { + try { + const monthlyData: number[] = []; + + for (let month = 0; month < 12; month++) { + const startDate = new Date(year, month, 1); + const endDate = new Date(year, month + 1, 0, 23, 59, 59, 999); + + const revenue = await prisma.payment.aggregate({ + _sum: { amount: true }, + where: { + createdAt: { gte: startDate, lte: endDate }, + }, + }); + monthlyData.push(revenue._sum.amount || 0); + } + + return { success: true, data: monthlyData }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/member.handlers.ts b/gym-management-system/electron/ipc/member.handlers.ts new file mode 100644 index 0000000000..6ac4424520 --- /dev/null +++ b/gym-management-system/electron/ipc/member.handlers.ts @@ -0,0 +1,123 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; +import * as QRCode from 'qrcode'; +import { v4 as uuidv4 } from 'uuid'; + +export function registerMemberHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('members:getAll', async (_event, params?: any) => { + try { + const { page = 1, limit = 20, search = '', status = 'all' } = params || {}; + const skip = (page - 1) * limit; + + const where: any = {}; + if (search) { + where.OR = [ + { fullName: { contains: search } }, + { phone: { contains: search } }, + { qrCode: { contains: search } }, + ]; + } + if (status !== 'all') { + where.isActive = status === 'active'; + } + + const [members, total] = await Promise.all([ + prisma.member.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + subscriptions: { orderBy: { createdAt: 'desc' }, take: 1 }, + } + }), + prisma.member.count({ where }), + ]); + + return { success: true, data: { members, total, page, limit } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('members:getOne', async (_event, id: string) => { + try { + const member = await prisma.member.findUnique({ + where: { id }, + include: { + subscriptions: { orderBy: { createdAt: 'desc' } }, + attendances: { orderBy: { checkIn: 'desc' }, take: 30 }, + payments: { orderBy: { createdAt: 'desc' } }, + measurements: { orderBy: { createdAt: 'desc' } }, + } + }); + + if (!member) { + return { success: false, error: 'العضو غير موجود' }; + } + + return { success: true, data: member }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('members:create', async (_event, data: any) => { + try { + // Generate unique QR code + const qrCode = `GYM-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + const member = await prisma.member.create({ + data: { + ...data, + qrCode, + } + }); + + return { success: true, data: member }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('members:update', async (_event, id: string, data: any) => { + try { + const member = await prisma.member.update({ + where: { id }, + data, + }); + return { success: true, data: member }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('members:delete', async (_event, id: string) => { + try { + await prisma.member.delete({ where: { id } }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('members:search', async (_event, query: string) => { + try { + const members = await prisma.member.findMany({ + where: { + OR: [ + { fullName: { contains: query } }, + { phone: { contains: query } }, + { qrCode: { contains: query } }, + ] + }, + take: 10, + }); + return { success: true, data: members }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/payment.handlers.ts b/gym-management-system/electron/ipc/payment.handlers.ts new file mode 100644 index 0000000000..07f448a2b2 --- /dev/null +++ b/gym-management-system/electron/ipc/payment.handlers.ts @@ -0,0 +1,63 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerPaymentHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('payments:getAll', async (_event, params?: any) => { + try { + const { page = 1, limit = 20, memberId } = params || {}; + const skip = (page - 1) * limit; + + const where: any = {}; + if (memberId) where.memberId = memberId; + + const [payments, total] = await Promise.all([ + prisma.payment.findMany({ + where, + skip, + take: limit, + include: { member: true }, + orderBy: { createdAt: 'desc' }, + }), + prisma.payment.count({ where }), + ]); + + return { success: true, data: { payments, total, page, limit } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('payments:create', async (_event, data: any) => { + try { + // Generate invoice number + const count = await prisma.payment.count(); + const invoiceNumber = `INV-${String(count + 1).padStart(6, '0')}`; + + const payment = await prisma.payment.create({ + data: { + ...data, + invoiceNumber, + }, + include: { member: true }, + }); + + return { success: true, data: payment }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('payments:getOne', async (_event, id: string) => { + try { + const payment = await prisma.payment.findUnique({ + where: { id }, + include: { member: true }, + }); + return { success: true, data: payment }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/product.handlers.ts b/gym-management-system/electron/ipc/product.handlers.ts new file mode 100644 index 0000000000..d42438f039 --- /dev/null +++ b/gym-management-system/electron/ipc/product.handlers.ts @@ -0,0 +1,66 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerProductHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('products:getAll', async (_event, params?: any) => { + try { + const { page = 1, limit = 20, category } = params || {}; + const skip = (page - 1) * limit; + + const where: any = { isActive: true }; + if (category) where.category = category; + + const [products, total] = await Promise.all([ + prisma.product.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + prisma.product.count({ where }), + ]); + + return { success: true, data: { products, total, page, limit } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('products:getOne', async (_event, id: string) => { + try { + const product = await prisma.product.findUnique({ where: { id } }); + return { success: true, data: product }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('products:create', async (_event, data: any) => { + try { + const product = await prisma.product.create({ data }); + return { success: true, data: product }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('products:update', async (_event, id: string, data: any) => { + try { + const product = await prisma.product.update({ where: { id }, data }); + return { success: true, data: product }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('products:delete', async (_event, id: string) => { + try { + await prisma.product.update({ where: { id }, data: { isActive: false } }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/settings.handlers.ts b/gym-management-system/electron/ipc/settings.handlers.ts new file mode 100644 index 0000000000..9413b323a2 --- /dev/null +++ b/gym-management-system/electron/ipc/settings.handlers.ts @@ -0,0 +1,39 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerSettingsHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('settings:get', async (_event, key: string) => { + try { + const setting = await prisma.setting.findUnique({ where: { key } }); + return { success: true, data: setting?.value || null }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('settings:set', async (_event, key: string, value: string) => { + try { + await prisma.setting.upsert({ + where: { key }, + update: { value }, + create: { key, value }, + }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('settings:getAll', async () => { + try { + const settings = await prisma.setting.findMany(); + const settingsMap: Record = {}; + settings.forEach(s => { settingsMap[s.key] = s.value; }); + return { success: true, data: settingsMap }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/subscription.handlers.ts b/gym-management-system/electron/ipc/subscription.handlers.ts new file mode 100644 index 0000000000..d79425269c --- /dev/null +++ b/gym-management-system/electron/ipc/subscription.handlers.ts @@ -0,0 +1,165 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerSubscriptionHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('subscriptions:getAll', async (_event, params?: any) => { + try { + const { page = 1, limit = 20, status = 'all', memberId } = params || {}; + const skip = (page - 1) * limit; + + const where: any = {}; + if (status !== 'all') where.status = status; + if (memberId) where.memberId = memberId; + + const [subscriptions, total] = await Promise.all([ + prisma.subscription.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { member: true }, + }), + prisma.subscription.count({ where }), + ]); + + return { success: true, data: { subscriptions, total, page, limit } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:getOne', async (_event, id: string) => { + try { + const subscription = await prisma.subscription.findUnique({ + where: { id }, + include: { member: true }, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:create', async (_event, data: any) => { + try { + const subscription = await prisma.subscription.create({ + data, + include: { member: true }, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:update', async (_event, id: string, data: any) => { + try { + const subscription = await prisma.subscription.update({ + where: { id }, + data, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:freeze', async (_event, id: string) => { + try { + const subscription = await prisma.subscription.update({ + where: { id }, + data: { + status: 'frozen', + frozenAt: new Date(), + }, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:unfreeze', async (_event, id: string) => { + try { + const sub = await prisma.subscription.findUnique({ where: { id } }); + if (!sub || !sub.frozenAt) { + return { success: false, error: 'الاشتراك غير مجمد' }; + } + + const frozenDays = Math.ceil( + (Date.now() - sub.frozenAt.getTime()) / (1000 * 60 * 60 * 24) + ); + + const newEndDate = new Date(sub.endDate); + newEndDate.setDate(newEndDate.getDate() + frozenDays); + + const subscription = await prisma.subscription.update({ + where: { id }, + data: { + status: 'active', + frozenAt: null, + frozenDays: sub.frozenDays + frozenDays, + endDate: newEndDate, + }, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:renew', async (_event, id: string, data: any) => { + try { + const subscription = await prisma.subscription.update({ + where: { id }, + data: { + status: 'active', + startDate: new Date(), + endDate: data.endDate, + price: data.price, + type: data.type, + }, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:cancel', async (_event, id: string) => { + try { + const subscription = await prisma.subscription.update({ + where: { id }, + data: { status: 'cancelled' }, + }); + return { success: true, data: subscription }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('subscriptions:expiringSoon', async () => { + try { + const sevenDaysFromNow = new Date(); + sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7); + + const subscriptions = await prisma.subscription.findMany({ + where: { + status: 'active', + endDate: { + lte: sevenDaysFromNow, + gte: new Date(), + }, + }, + include: { member: true }, + orderBy: { endDate: 'asc' }, + }); + + return { success: true, data: subscriptions }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/ipc/trainer.handlers.ts b/gym-management-system/electron/ipc/trainer.handlers.ts new file mode 100644 index 0000000000..ba20c67b17 --- /dev/null +++ b/gym-management-system/electron/ipc/trainer.handlers.ts @@ -0,0 +1,66 @@ +import { ipcMain } from 'electron'; +import { getPrisma } from '../database/init'; + +export function registerTrainerHandlers(): void { + const prisma = getPrisma(); + + ipcMain.handle('trainers:getAll', async (_event, params?: any) => { + try { + const { page = 1, limit = 20 } = params || {}; + const skip = (page - 1) * limit; + + const [trainers, total] = await Promise.all([ + prisma.trainer.findMany({ + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { sessions: true }, + }), + prisma.trainer.count(), + ]); + + return { success: true, data: { trainers, total, page, limit } }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('trainers:getOne', async (_event, id: string) => { + try { + const trainer = await prisma.trainer.findUnique({ + where: { id }, + include: { sessions: true }, + }); + return { success: true, data: trainer }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('trainers:create', async (_event, data: any) => { + try { + const trainer = await prisma.trainer.create({ data }); + return { success: true, data: trainer }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('trainers:update', async (_event, id: string, data: any) => { + try { + const trainer = await prisma.trainer.update({ where: { id }, data }); + return { success: true, data: trainer }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('trainers:delete', async (_event, id: string) => { + try { + await prisma.trainer.delete({ where: { id } }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }); +} diff --git a/gym-management-system/electron/main.ts b/gym-management-system/electron/main.ts new file mode 100644 index 0000000000..68195a88e6 --- /dev/null +++ b/gym-management-system/electron/main.ts @@ -0,0 +1,87 @@ +import { app, BrowserWindow, ipcMain, session } from 'electron'; +import * as path from 'path'; +import { initializeDatabase } from './database/init'; +import { registerAuthHandlers } from './ipc/auth.handlers'; +import { registerMemberHandlers } from './ipc/member.handlers'; +import { registerSubscriptionHandlers } from './ipc/subscription.handlers'; +import { registerAttendanceHandlers } from './ipc/attendance.handlers'; +import { registerPaymentHandlers } from './ipc/payment.handlers'; +import { registerTrainerHandlers } from './ipc/trainer.handlers'; +import { registerProductHandlers } from './ipc/product.handlers'; +import { registerDashboardHandlers } from './ipc/dashboard.handlers'; +import { registerBackupHandlers } from './ipc/backup.handlers'; +import { registerSettingsHandlers } from './ipc/settings.handlers'; +import { startAutoBackup } from './services/backup.service'; + +let mainWindow: BrowserWindow | null = null; + +function createWindow(): void { + mainWindow = new BrowserWindow({ + width: 1400, + height: 900, + minWidth: 1024, + minHeight: 768, + title: 'Gym Management System', + icon: path.join(__dirname, '../src/assets/icons/icon.png'), + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + show: false, + }); + + mainWindow.once('ready-to-show', () => { + mainWindow?.show(); + }); + + // Load Angular app + if (process.env['NODE_ENV'] === 'development') { + mainWindow.loadURL('http://localhost:4200'); + mainWindow.webContents.openDevTools(); + } else { + mainWindow.loadFile(path.join(__dirname, '../dist/browser/index.html')); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +async function bootstrap(): Promise { + // Initialize database + await initializeDatabase(); + + // Register IPC handlers + registerAuthHandlers(); + registerMemberHandlers(); + registerSubscriptionHandlers(); + registerAttendanceHandlers(); + registerPaymentHandlers(); + registerTrainerHandlers(); + registerProductHandlers(); + registerDashboardHandlers(); + registerBackupHandlers(); + registerSettingsHandlers(); + + // Start auto backup + startAutoBackup(); +} + +app.whenReady().then(async () => { + await bootstrap(); + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/gym-management-system/electron/preload.ts b/gym-management-system/electron/preload.ts new file mode 100644 index 0000000000..39e7bd49fb --- /dev/null +++ b/gym-management-system/electron/preload.ts @@ -0,0 +1,82 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +// Expose protected methods that allow the renderer process to use +// ipcRenderer without exposing the entire object +contextBridge.exposeInMainWorld('electronAPI', { + // Auth + login: (username: string, password: string) => + ipcRenderer.invoke('auth:login', username, password), + logout: () => ipcRenderer.invoke('auth:logout'), + getCurrentUser: () => ipcRenderer.invoke('auth:getCurrentUser'), + changePassword: (oldPassword: string, newPassword: string) => + ipcRenderer.invoke('auth:changePassword', oldPassword, newPassword), + + // Members + getMembers: (params?: any) => ipcRenderer.invoke('members:getAll', params), + getMember: (id: string) => ipcRenderer.invoke('members:getOne', id), + createMember: (data: any) => ipcRenderer.invoke('members:create', data), + updateMember: (id: string, data: any) => ipcRenderer.invoke('members:update', id, data), + deleteMember: (id: string) => ipcRenderer.invoke('members:delete', id), + searchMembers: (query: string) => ipcRenderer.invoke('members:search', query), + + // Subscriptions + getSubscriptions: (params?: any) => ipcRenderer.invoke('subscriptions:getAll', params), + getSubscription: (id: string) => ipcRenderer.invoke('subscriptions:getOne', id), + createSubscription: (data: any) => ipcRenderer.invoke('subscriptions:create', data), + updateSubscription: (id: string, data: any) => ipcRenderer.invoke('subscriptions:update', id, data), + freezeSubscription: (id: string) => ipcRenderer.invoke('subscriptions:freeze', id), + unfreezeSubscription: (id: string) => ipcRenderer.invoke('subscriptions:unfreeze', id), + renewSubscription: (id: string, data: any) => ipcRenderer.invoke('subscriptions:renew', id, data), + cancelSubscription: (id: string) => ipcRenderer.invoke('subscriptions:cancel', id), + getExpiringSoon: () => ipcRenderer.invoke('subscriptions:expiringSoon'), + + // Attendance + checkIn: (memberId: string) => ipcRenderer.invoke('attendance:checkIn', memberId), + checkInByQR: (qrCode: string) => ipcRenderer.invoke('attendance:checkInByQR', qrCode), + getAttendanceToday: () => ipcRenderer.invoke('attendance:today'), + getAttendanceHistory: (params?: any) => ipcRenderer.invoke('attendance:history', params), + getCurrentAttendanceCount: () => ipcRenderer.invoke('attendance:currentCount'), + + // Payments + getPayments: (params?: any) => ipcRenderer.invoke('payments:getAll', params), + createPayment: (data: any) => ipcRenderer.invoke('payments:create', data), + getPayment: (id: string) => ipcRenderer.invoke('payments:getOne', id), + + // Trainers + getTrainers: (params?: any) => ipcRenderer.invoke('trainers:getAll', params), + getTrainer: (id: string) => ipcRenderer.invoke('trainers:getOne', id), + createTrainer: (data: any) => ipcRenderer.invoke('trainers:create', data), + updateTrainer: (id: string, data: any) => ipcRenderer.invoke('trainers:update', id, data), + deleteTrainer: (id: string) => ipcRenderer.invoke('trainers:delete', id), + + // Products + getProducts: (params?: any) => ipcRenderer.invoke('products:getAll', params), + getProduct: (id: string) => ipcRenderer.invoke('products:getOne', id), + createProduct: (data: any) => ipcRenderer.invoke('products:create', data), + updateProduct: (id: string, data: any) => ipcRenderer.invoke('products:update', id, data), + deleteProduct: (id: string) => ipcRenderer.invoke('products:delete', id), + + // Invoices + createInvoice: (data: any) => ipcRenderer.invoke('invoices:create', data), + getInvoices: (params?: any) => ipcRenderer.invoke('invoices:getAll', params), + + // Dashboard + getDashboardStats: () => ipcRenderer.invoke('dashboard:stats'), + getMonthlySubscriptions: (year: number) => ipcRenderer.invoke('dashboard:monthlySubscriptions', year), + getMonthlyRevenue: (year: number) => ipcRenderer.invoke('dashboard:monthlyRevenue', year), + + // Backup + createBackup: () => ipcRenderer.invoke('backup:create'), + getBackups: () => ipcRenderer.invoke('backup:getAll'), + restoreBackup: (filename: string) => ipcRenderer.invoke('backup:restore', filename), + deleteBackup: (id: string) => ipcRenderer.invoke('backup:delete', id), + + // Settings + getSetting: (key: string) => ipcRenderer.invoke('settings:get', key), + setSetting: (key: string, value: string) => ipcRenderer.invoke('settings:set', key, value), + getAllSettings: () => ipcRenderer.invoke('settings:getAll'), + + // File operations + selectImage: () => ipcRenderer.invoke('file:selectImage'), + saveFile: (data: any, filename: string) => ipcRenderer.invoke('file:save', data, filename), +}); diff --git a/gym-management-system/electron/services/backup.service.ts b/gym-management-system/electron/services/backup.service.ts new file mode 100644 index 0000000000..8d98076d8d --- /dev/null +++ b/gym-management-system/electron/services/backup.service.ts @@ -0,0 +1,123 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { app } from 'electron'; +import { getPrisma } from '../database/init'; + +const BACKUP_DIR = path.join(app.getPath('userData'), 'backups'); + +function ensureBackupDir(): void { + if (!fs.existsSync(BACKUP_DIR)) { + fs.mkdirSync(BACKUP_DIR, { recursive: true }); + } +} + +export async function createBackup(): Promise<{ filename: string; size: number }> { + ensureBackupDir(); + + const prisma = getPrisma(); + const dbPath = path.join(app.getPath('userData'), 'database.sqlite'); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `backup-${timestamp}.sqlite`; + const backupPath = path.join(BACKUP_DIR, filename); + + // Copy database file + fs.copyFileSync(dbPath, backupPath); + + const stats = fs.statSync(backupPath); + + // Record backup in database + await prisma.backup.create({ + data: { + filename, + size: stats.size, + type: 'manual', + }, + }); + + // Clean old backups (keep last 30) + await cleanOldBackups(); + + return { filename, size: stats.size }; +} + +export function getBackupsList(): string[] { + ensureBackupDir(); + return fs.readdirSync(BACKUP_DIR).filter(f => f.endsWith('.sqlite')); +} + +export async function restoreFromBackup(filename: string): Promise { + const backupPath = path.join(BACKUP_DIR, filename); + const dbPath = path.join(app.getPath('userData'), 'database.sqlite'); + + if (!fs.existsSync(backupPath)) { + throw new Error('ملف النسخة الاحتياطية غير موجود'); + } + + // Close current connection + const prisma = getPrisma(); + await prisma.$disconnect(); + + // Restore + fs.copyFileSync(backupPath, dbPath); +} + +export function deleteBackupFile(filename: string): void { + const backupPath = path.join(BACKUP_DIR, filename); + if (fs.existsSync(backupPath)) { + fs.unlinkSync(backupPath); + } +} + +async function cleanOldBackups(): Promise { + const prisma = getPrisma(); + const backups = await prisma.backup.findMany({ + orderBy: { createdAt: 'desc' }, + skip: 30, + }); + + for (const backup of backups) { + deleteBackupFile(backup.filename); + await prisma.backup.delete({ where: { id: backup.id } }); + } +} + +export function startAutoBackup(): void { + // Run auto backup at midnight + const now = new Date(); + const midnight = new Date(now); + midnight.setHours(24, 0, 0, 0); + const msUntilMidnight = midnight.getTime() - now.getTime(); + + setTimeout(() => { + performAutoBackup(); + // Then every 24 hours + setInterval(performAutoBackup, 24 * 60 * 60 * 1000); + }, msUntilMidnight); +} + +async function performAutoBackup(): Promise { + try { + ensureBackupDir(); + const prisma = getPrisma(); + const dbPath = path.join(app.getPath('userData'), 'database.sqlite'); + const timestamp = new Date().toISOString().split('T')[0]; + const filename = `auto-backup-${timestamp}.sqlite`; + const backupPath = path.join(BACKUP_DIR, filename); + + fs.copyFileSync(dbPath, backupPath); + const stats = fs.statSync(backupPath); + + await prisma.backup.create({ + data: { + filename, + size: stats.size, + type: 'auto', + }, + }); + + await cleanOldBackups(); + console.log(`Auto backup created: ${filename}`); + } catch (error) { + console.error('Auto backup failed:', error); + } +} diff --git a/gym-management-system/electron/tsconfig.json b/gym-management-system/electron/tsconfig.json new file mode 100644 index 0000000000..3b6ba67783 --- /dev/null +++ b/gym-management-system/electron/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "../dist-electron", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/gym-management-system/package.json b/gym-management-system/package.json new file mode 100644 index 0000000000..643bf974b5 --- /dev/null +++ b/gym-management-system/package.json @@ -0,0 +1,92 @@ +{ + "name": "gym-management-system", + "version": "1.0.0", + "description": "نظام إدارة نادي رياضي احترافي - Electron + Angular + SQLite", + "main": "dist-electron/main.js", + "author": "Gym Management Team", + "license": "MIT", + "scripts": { + "ng": "ng", + "start": "ng serve --host 0.0.0.0 --port 4201", + "build": "ng build", + "build:prod": "ng build --configuration production", + "test": "ng test", + "electron:dev": "concurrently \"ng serve\" \"wait-on http://localhost:4201 && tsc -p electron/tsconfig.json && electron .\"", + "electron:build": "ng build --configuration production && tsc -p electron/tsconfig.json && electron-builder" + }, + "dependencies": { + "@angular/animations": "^17.3.0", + "@angular/cdk": "^17.3.0", + "@angular/common": "^17.3.0", + "@angular/compiler": "^17.3.0", + "@angular/core": "^17.3.0", + "@angular/forms": "^17.3.0", + "@angular/material": "^17.3.0", + "@angular/platform-browser": "^17.3.0", + "@angular/platform-browser-dynamic": "^17.3.0", + "@angular/router": "^17.3.0", + "@ngrx/effects": "^17.1.0", + "@ngrx/store": "^17.1.0", + "@ngrx/store-devtools": "^17.1.0", + "material-icons": "^1.13.14", + "rxjs": "~7.8.0", + "tslib": "^2.6.0", + "zone.js": "~0.14.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.0", + "@angular/cli": "^17.3.0", + "@angular/compiler-cli": "^17.3.0", + "@types/node": "^20.11.0", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "~5.3.0" + }, + "optionalDependencies": { + "@prisma/client": "^5.10.0", + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.6.0", + "concurrently": "^8.2.2", + "electron": "^28.2.0", + "electron-builder": "^24.9.0", + "html5-qrcode": "^2.3.8", + "jsonwebtoken": "^9.0.2", + "prisma": "^5.10.0", + "qrcode": "^1.5.3", + "wait-on": "^7.2.0" + }, + "build": { + "appId": "com.gym.management", + "productName": "Gym Management System", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "dist-electron/**/*", + "prisma/**/*" + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } + ] + }, + "nsis": { + "oneClick": false, + "perMachine": true, + "allowToChangeInstallationDirectory": true + } + } +} diff --git a/gym-management-system/postcss.config.js b/gym-management-system/postcss.config.js new file mode 100644 index 0000000000..33ad091d26 --- /dev/null +++ b/gym-management-system/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/gym-management-system/prisma/schema.prisma b/gym-management-system/prisma/schema.prisma new file mode 100644 index 0000000000..aa31c8bebe --- /dev/null +++ b/gym-management-system/prisma/schema.prisma @@ -0,0 +1,200 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:../storage/database.sqlite" +} + +model User { + id String @id @default(cuid()) + username String @unique + password String + fullName String + role String @default("reception") // admin, reception, trainer, accountant + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Member { + id String @id @default(cuid()) + fullName String + phone String + emergencyPhone String? + email String? + gender String @default("male") // male, female + birthDate DateTime? + image String? + qrCode String? @unique + notes String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + subscriptions Subscription[] + attendances Attendance[] + payments Payment[] + measurements Measurement[] +} + +model Subscription { + id String @id @default(cuid()) + memberId String + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + type String // monthly, quarterly, semi_annual, annual + price Float + startDate DateTime + endDate DateTime + status String @default("active") // active, frozen, expired, cancelled + frozenAt DateTime? + frozenDays Int @default(0) + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Attendance { + id String @id @default(cuid()) + memberId String + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + checkIn DateTime @default(now()) + checkOut DateTime? +} + +model Payment { + id String @id @default(cuid()) + memberId String + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + amount Float + paymentMethod String // cash, bank_transfer, wallet + invoiceNumber String @unique + description String? + notes String? + createdAt DateTime @default(now()) +} + +model Trainer { + id String @id @default(cuid()) + fullName String + phone String + email String? + specialization String + image String? + isActive Boolean @default(true) + salary Float? + schedule String? // JSON string for schedule + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sessions TrainingSession[] +} + +model TrainingSession { + id String @id @default(cuid()) + trainerId String + trainer Trainer @relation(fields: [trainerId], references: [id], onDelete: Cascade) + name String + dayOfWeek String + startTime String + endTime String + maxCapacity Int @default(20) + createdAt DateTime @default(now()) +} + +model NutritionPlan { + id String @id @default(cuid()) + memberId String + title String + description String? + meals String // JSON string + calories Int? + startDate DateTime + endDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model WorkoutPlan { + id String @id @default(cuid()) + memberId String + title String + description String? + exercises String // JSON string + daysPerWeek Int @default(3) + startDate DateTime + endDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Measurement { + id String @id @default(cuid()) + memberId String + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + weight Float? + height Float? + bodyFat Float? + muscle Float? + chest Float? + waist Float? + hips Float? + arms Float? + image String? + notes String? + createdAt DateTime @default(now()) +} + +model Product { + id String @id @default(cuid()) + name String + description String? + price Float + cost Float? + stock Int @default(0) + minStock Int @default(5) + category String? + image String? + barcode String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Invoice { + id String @id @default(cuid()) + invoiceNumber String @unique + memberId String? + totalAmount Float + discount Float @default(0) + finalAmount Float + paymentMethod String // cash, bank_transfer, wallet + status String @default("paid") // paid, pending, cancelled + notes String? + createdAt DateTime @default(now()) + items InvoiceItem[] +} + +model InvoiceItem { + id String @id @default(cuid()) + invoiceId String + invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) + productId String + name String + quantity Int + price Float + total Float +} + +model Setting { + id String @id @default(cuid()) + key String @unique + value String +} + +model Backup { + id String @id @default(cuid()) + filename String + size Int + type String // auto, manual + createdAt DateTime @default(now()) +} diff --git a/gym-management-system/src/app/app.component.ts b/gym-management-system/src/app/app.component.ts new file mode 100644 index 0000000000..a3de393bef --- /dev/null +++ b/gym-management-system/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, +}) +export class AppComponent {} diff --git a/gym-management-system/src/app/app.config.ts b/gym-management-system/src/app/app.config.ts new file mode 100644 index 0000000000..5235d3c42f --- /dev/null +++ b/gym-management-system/src/app/app.config.ts @@ -0,0 +1,15 @@ +import { ApplicationConfig, importProvidersFrom } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { provideStore } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + provideAnimations(), + provideStore(), + provideEffects(), + ] +}; diff --git a/gym-management-system/src/app/app.routes.ts b/gym-management-system/src/app/app.routes.ts new file mode 100644 index 0000000000..4636ada161 --- /dev/null +++ b/gym-management-system/src/app/app.routes.ts @@ -0,0 +1,69 @@ +import { Routes } from '@angular/router'; +import { authGuard } from './core/guards/auth.guard'; + +export const routes: Routes = [ + { + path: 'login', + loadComponent: () => import('./pages/login/login.component').then(m => m.LoginComponent), + }, + { + path: '', + loadComponent: () => import('./layouts/main-layout/main-layout.component').then(m => m.MainLayoutComponent), + canActivate: [authGuard], + children: [ + { + path: '', + redirectTo: 'dashboard', + pathMatch: 'full', + }, + { + path: 'dashboard', + loadComponent: () => import('./pages/dashboard/dashboard.component').then(m => m.DashboardComponent), + }, + { + path: 'members', + loadComponent: () => import('./pages/members/members.component').then(m => m.MembersComponent), + }, + { + path: 'members/:id', + loadComponent: () => import('./pages/members/member-detail/member-detail.component').then(m => m.MemberDetailComponent), + }, + { + path: 'subscriptions', + loadComponent: () => import('./pages/subscriptions/subscriptions.component').then(m => m.SubscriptionsComponent), + }, + { + path: 'attendance', + loadComponent: () => import('./pages/attendance/attendance.component').then(m => m.AttendanceComponent), + }, + { + path: 'trainers', + loadComponent: () => import('./pages/trainers/trainers.component').then(m => m.TrainersComponent), + }, + { + path: 'nutrition', + loadComponent: () => import('./pages/nutrition/nutrition.component').then(m => m.NutritionComponent), + }, + { + path: 'store', + loadComponent: () => import('./pages/store/store.component').then(m => m.StoreComponent), + }, + { + path: 'payments', + loadComponent: () => import('./pages/payments/payments.component').then(m => m.PaymentsComponent), + }, + { + path: 'settings', + loadComponent: () => import('./pages/settings/settings.component').then(m => m.SettingsComponent), + }, + { + path: 'backup', + loadComponent: () => import('./pages/backup/backup.component').then(m => m.BackupComponent), + }, + ], + }, + { + path: '**', + redirectTo: 'login', + }, +]; diff --git a/gym-management-system/src/app/core/guards/auth.guard.ts b/gym-management-system/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000000..2ae89a997b --- /dev/null +++ b/gym-management-system/src/app/core/guards/auth.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +export const authGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated) { + return true; + } + + router.navigate(['/login']); + return false; +}; diff --git a/gym-management-system/src/app/core/services/auth.service.ts b/gym-management-system/src/app/core/services/auth.service.ts new file mode 100644 index 0000000000..2ffd3a187c --- /dev/null +++ b/gym-management-system/src/app/core/services/auth.service.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +export interface User { + id: string; + username: string; + fullName: string; + role: string; +} + +export interface AuthState { + isAuthenticated: boolean; + user: User | null; + token: string | null; +} + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private authState = new BehaviorSubject({ + isAuthenticated: false, + user: null, + token: null, + }); + + authState$ = this.authState.asObservable(); + + constructor() { + this.loadStoredAuth(); + } + + private loadStoredAuth(): void { + const stored = localStorage.getItem('auth'); + if (stored) { + try { + const parsed = JSON.parse(stored); + this.authState.next(parsed); + } catch { + localStorage.removeItem('auth'); + } + } + } + + get isAuthenticated(): boolean { + return this.authState.value.isAuthenticated; + } + + get currentUser(): User | null { + return this.authState.value.user; + } + + get token(): string | null { + return this.authState.value.token; + } + + async login(username: string, password: string): Promise<{ success: boolean; error?: string }> { + try { + const electronAPI = (window as any).electronAPI; + + if (electronAPI) { + const result = await electronAPI.login(username, password); + if (result.success) { + const state: AuthState = { + isAuthenticated: true, + user: result.data.user, + token: result.data.token, + }; + this.authState.next(state); + localStorage.setItem('auth', JSON.stringify(state)); + return { success: true }; + } + return { success: false, error: result.error }; + } + + // Demo mode - simulate login for web preview + if (username === 'Admin' && password === 'Admin') { + const state: AuthState = { + isAuthenticated: true, + user: { id: '1', username: 'Admin', fullName: 'مدير النظام', role: 'admin' }, + token: 'demo-token', + }; + this.authState.next(state); + localStorage.setItem('auth', JSON.stringify(state)); + return { success: true }; + } + + return { success: false, error: 'اسم المستخدم أو كلمة المرور غير صحيحة' }; + } catch (error: any) { + return { success: false, error: error.message || 'حدث خطأ غير متوقع' }; + } + } + + logout(): void { + this.authState.next({ isAuthenticated: false, user: null, token: null }); + localStorage.removeItem('auth'); + } + + hasRole(role: string): boolean { + return this.currentUser?.role === role || this.currentUser?.role === 'admin'; + } +} diff --git a/gym-management-system/src/app/core/services/electron.service.ts b/gym-management-system/src/app/core/services/electron.service.ts new file mode 100644 index 0000000000..4c3a29ccca --- /dev/null +++ b/gym-management-system/src/app/core/services/electron.service.ts @@ -0,0 +1,157 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ElectronService { + private api: any; + + constructor() { + this.api = (window as any).electronAPI; + } + + get isElectron(): boolean { + return !!this.api; + } + + // Members + getMembers(params?: any) { return this.invoke('getMembers', params); } + getMember(id: string) { return this.invoke('getMember', id); } + createMember(data: any) { return this.invoke('createMember', data); } + updateMember(id: string, data: any) { return this.invoke('updateMember', id, data); } + deleteMember(id: string) { return this.invoke('deleteMember', id); } + searchMembers(query: string) { return this.invoke('searchMembers', query); } + + // Subscriptions + getSubscriptions(params?: any) { return this.invoke('getSubscriptions', params); } + createSubscription(data: any) { return this.invoke('createSubscription', data); } + freezeSubscription(id: string) { return this.invoke('freezeSubscription', id); } + unfreezeSubscription(id: string) { return this.invoke('unfreezeSubscription', id); } + renewSubscription(id: string, data: any) { return this.invoke('renewSubscription', id, data); } + cancelSubscription(id: string) { return this.invoke('cancelSubscription', id); } + getExpiringSoon() { return this.invoke('getExpiringSoon'); } + + // Attendance + checkIn(memberId: string) { return this.invoke('checkIn', memberId); } + checkInByQR(qrCode: string) { return this.invoke('checkInByQR', qrCode); } + getAttendanceToday() { return this.invoke('getAttendanceToday'); } + getAttendanceHistory(params?: any) { return this.invoke('getAttendanceHistory', params); } + getCurrentAttendanceCount() { return this.invoke('getCurrentAttendanceCount'); } + + // Payments + getPayments(params?: any) { return this.invoke('getPayments', params); } + createPayment(data: any) { return this.invoke('createPayment', data); } + + // Trainers + getTrainers(params?: any) { return this.invoke('getTrainers', params); } + getTrainer(id: string) { return this.invoke('getTrainer', id); } + createTrainer(data: any) { return this.invoke('createTrainer', data); } + updateTrainer(id: string, data: any) { return this.invoke('updateTrainer', id, data); } + deleteTrainer(id: string) { return this.invoke('deleteTrainer', id); } + + // Products + getProducts(params?: any) { return this.invoke('getProducts', params); } + createProduct(data: any) { return this.invoke('createProduct', data); } + updateProduct(id: string, data: any) { return this.invoke('updateProduct', id, data); } + deleteProduct(id: string) { return this.invoke('deleteProduct', id); } + + // Dashboard + getDashboardStats() { return this.invoke('getDashboardStats'); } + getMonthlySubscriptions(year: number) { return this.invoke('getMonthlySubscriptions', year); } + getMonthlyRevenue(year: number) { return this.invoke('getMonthlyRevenue', year); } + + // Backup + createBackup() { return this.invoke('createBackup'); } + getBackups() { return this.invoke('getBackups'); } + restoreBackup(filename: string) { return this.invoke('restoreBackup', filename); } + deleteBackup(id: string) { return this.invoke('deleteBackup', id); } + + // Settings + getSetting(key: string) { return this.invoke('getSetting', key); } + setSetting(key: string, value: string) { return this.invoke('setSetting', key, value); } + getAllSettings() { return this.invoke('getAllSettings'); } + + private async invoke(method: string, ...args: any[]): Promise { + if (this.api && this.api[method]) { + return this.api[method](...args); + } + // Return demo data when not in Electron + return this.getDemoData(method, args); + } + + private getDemoData(method: string, args: any[]): any { + // Demo data for web preview + const demoData: Record = { + getDashboardStats: { + success: true, + data: { totalMembers: 156, attendanceToday: 43, expiredSubscriptions: 12, dailyRevenue: 2450 } + }, + getMonthlySubscriptions: { + success: true, + data: [12, 19, 25, 32, 28, 35, 40, 38, 45, 42, 50, 48] + }, + getMonthlyRevenue: { + success: true, + data: [15000, 22000, 28000, 35000, 30000, 42000, 48000, 45000, 52000, 49000, 58000, 55000] + }, + getMembers: { + success: true, + data: { + members: [ + { id: '1', fullName: 'أحمد محمد', phone: '0551234567', qrCode: 'GYM-001', isActive: true, createdAt: new Date(), subscriptions: [{ status: 'active', endDate: new Date(Date.now() + 30*24*60*60*1000) }] }, + { id: '2', fullName: 'محمد علي', phone: '0559876543', qrCode: 'GYM-002', isActive: true, createdAt: new Date(), subscriptions: [{ status: 'active', endDate: new Date(Date.now() + 15*24*60*60*1000) }] }, + { id: '3', fullName: 'خالد عبدالله', phone: '0553334444', qrCode: 'GYM-003', isActive: true, createdAt: new Date(), subscriptions: [{ status: 'expired', endDate: new Date(Date.now() - 5*24*60*60*1000) }] }, + { id: '4', fullName: 'عمر حسن', phone: '0557778888', qrCode: 'GYM-004', isActive: true, createdAt: new Date(), subscriptions: [{ status: 'active', endDate: new Date(Date.now() + 60*24*60*60*1000) }] }, + { id: '5', fullName: 'فهد سعيد', phone: '0551112222', qrCode: 'GYM-005', isActive: false, createdAt: new Date(), subscriptions: [] }, + ], + total: 5, + page: 1, + limit: 20, + } + }, + getAttendanceToday: { + success: true, + data: [ + { id: '1', memberId: '1', checkIn: new Date(), member: { fullName: 'أحمد محمد', phone: '0551234567' } }, + { id: '2', memberId: '2', checkIn: new Date(Date.now() - 3600000), member: { fullName: 'محمد علي', phone: '0559876543' } }, + ] + }, + getTrainers: { + success: true, + data: { trainers: [ + { id: '1', fullName: 'كابتن أحمد', phone: '0551111111', specialization: 'بناء أجسام', isActive: true, sessions: [] }, + { id: '2', fullName: 'كابتن سارة', phone: '0552222222', specialization: 'لياقة بدنية', isActive: true, sessions: [] }, + ], total: 2, page: 1, limit: 20 } + }, + getProducts: { + success: true, + data: { products: [ + { id: '1', name: 'بروتين واي', price: 180, stock: 25, category: 'مكملات' }, + { id: '2', name: 'كرياتين', price: 95, stock: 15, category: 'مكملات' }, + { id: '3', name: 'قفازات تمرين', price: 45, stock: 30, category: 'معدات' }, + ], total: 3, page: 1, limit: 20 } + }, + getPayments: { + success: true, + data: { payments: [ + { id: '1', memberId: '1', amount: 300, paymentMethod: 'cash', invoiceNumber: 'INV-000001', createdAt: new Date(), member: { fullName: 'أحمد محمد' } }, + { id: '2', memberId: '2', amount: 800, paymentMethod: 'bank_transfer', invoiceNumber: 'INV-000002', createdAt: new Date(), member: { fullName: 'محمد علي' } }, + ], total: 2, page: 1, limit: 20 } + }, + getSubscriptions: { + success: true, + data: { subscriptions: [ + { id: '1', memberId: '1', type: 'monthly', price: 300, startDate: new Date(), endDate: new Date(Date.now() + 30*24*60*60*1000), status: 'active', member: { fullName: 'أحمد محمد' } }, + { id: '2', memberId: '2', type: 'quarterly', price: 800, startDate: new Date(), endDate: new Date(Date.now() + 90*24*60*60*1000), status: 'active', member: { fullName: 'محمد علي' } }, + ], total: 2, page: 1, limit: 20 } + }, + getBackups: { + success: true, + data: [ + { id: '1', filename: 'auto-backup-2026-05-20.sqlite', size: 2048000, type: 'auto', createdAt: new Date() }, + { id: '2', filename: 'backup-2026-05-19.sqlite', size: 1980000, type: 'manual', createdAt: new Date(Date.now() - 86400000) }, + ] + }, + }; + + return demoData[method] || { success: true, data: null }; + } +} diff --git a/gym-management-system/src/app/layouts/main-layout/main-layout.component.ts b/gym-management-system/src/app/layouts/main-layout/main-layout.component.ts new file mode 100644 index 0000000000..3c1b6971cd --- /dev/null +++ b/gym-management-system/src/app/layouts/main-layout/main-layout.component.ts @@ -0,0 +1,149 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Router } from '@angular/router'; +import { AuthService } from '../../core/services/auth.service'; + +interface NavItem { + icon: string; + label: string; + route: string; + roles?: string[]; +} + +@Component({ + selector: 'app-main-layout', + standalone: true, + imports: [CommonModule, RouterModule], + template: ` +
+ + + + +
+ +
+
+

{{ getPageTitle() }}

+
+
+ {{ currentDate }} + +
+
+ + +
+ +
+
+
+ `, +}) +export class MainLayoutComponent { + navItems: NavItem[] = [ + { icon: 'dashboard', label: 'لوحة التحكم', route: '/dashboard' }, + { icon: 'people', label: 'الأعضاء', route: '/members' }, + { icon: 'card_membership', label: 'الاشتراكات', route: '/subscriptions' }, + { icon: 'qr_code_scanner', label: 'الحضور', route: '/attendance' }, + { icon: 'sports_martial_arts', label: 'المدربين', route: '/trainers' }, + { icon: 'restaurant', label: 'التغذية والتدريب', route: '/nutrition' }, + { icon: 'store', label: 'المتجر', route: '/store' }, + { icon: 'payments', label: 'المدفوعات', route: '/payments' }, + { icon: 'backup', label: 'النسخ الاحتياطي', route: '/backup' }, + { icon: 'settings', label: 'الإعدادات', route: '/settings' }, + ]; + + currentDate = new Date().toLocaleDateString('ar-SA', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + get currentUser() { + return this.authService.currentUser; + } + + constructor(private authService: AuthService, private router: Router) {} + + getRoleLabel(role?: string): string { + const roles: Record = { + admin: 'مدير النظام', + reception: 'استقبال', + trainer: 'مدرب', + accountant: 'محاسب', + }; + return roles[role || ''] || role || ''; + } + + getPageTitle(): string { + const url = this.router.url; + const titles: Record = { + '/dashboard': 'لوحة التحكم', + '/members': 'إدارة الأعضاء', + '/subscriptions': 'إدارة الاشتراكات', + '/attendance': 'تسجيل الحضور', + '/trainers': 'إدارة المدربين', + '/nutrition': 'التغذية والتدريب', + '/store': 'المتجر', + '/payments': 'المدفوعات', + '/backup': 'النسخ الاحتياطي', + '/settings': 'الإعدادات', + }; + return titles[url] || 'لوحة التحكم'; + } + + logout(): void { + this.authService.logout(); + this.router.navigate(['/login']); + } +} diff --git a/gym-management-system/src/app/pages/attendance/attendance.component.ts b/gym-management-system/src/app/pages/attendance/attendance.component.ts new file mode 100644 index 0000000000..b61ce76541 --- /dev/null +++ b/gym-management-system/src/app/pages/attendance/attendance.component.ts @@ -0,0 +1,112 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-attendance', + standalone: true, + imports: [CommonModule], + template: ` +
+ + + +
+
+
+ qr_code_scanner +
+

مسح QR Code

+

وجّه الكاميرا نحو رمز QR الخاص بالعضو

+ + + +
+ + {{ scanResult.success ? 'check_circle' : 'error' }} + +

{{ scanResult.message }}

+
+
+ + +
+

إحصائيات اليوم

+
+
+ إجمالي الحضور + {{ todayCount }} +
+
+ آخر تسجيل + {{ lastCheckIn }} +
+
+
+
+ + +
+

سجل حضور اليوم

+
+
+
+
+ person +
+
+

{{ record.member?.fullName }}

+

{{ record.member?.phone }}

+
+
+ {{ record.checkIn | date:'HH:mm' }} +
+
+ event_busy +

لا يوجد تسجيلات حضور اليوم

+
+
+
+
+ `, +}) +export class AttendanceComponent implements OnInit { + todayAttendance: any[] = []; + todayCount = 0; + lastCheckIn = '--:--'; + scanResult: { success: boolean; message: string } | null = null; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { this.loadToday(); } + + async loadToday(): Promise { + const result = await this.electronService.getAttendanceToday(); + if (result?.success) { + this.todayAttendance = result.data; + this.todayCount = result.data.length; + if (result.data.length > 0) { + this.lastCheckIn = new Date(result.data[0].checkIn).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' }); + } + } + } + + startScanner(): void { + // In Electron, this would use html5-qrcode library + // For demo, simulate a scan + this.scanResult = { success: true, message: 'تم تسجيل حضور: أحمد محمد' }; + setTimeout(() => this.scanResult = null, 3000); + } +} diff --git a/gym-management-system/src/app/pages/backup/backup.component.ts b/gym-management-system/src/app/pages/backup/backup.component.ts new file mode 100644 index 0000000000..faf21ab5ef --- /dev/null +++ b/gym-management-system/src/app/pages/backup/backup.component.ts @@ -0,0 +1,107 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-backup', + standalone: true, + imports: [CommonModule], + template: ` +
+ + + +
+
+
+ schedule +
+
+

النسخ الاحتياطي التلقائي

+

يتم إنشاء نسخة احتياطية تلقائيًا يوميًا الساعة 12:00 صباحًا

+
+
+ مفعل +
+
+
+ + +
+

النسخ الاحتياطية المتوفرة

+
+
+
+
+ + {{ backup.type === 'auto' ? 'schedule' : 'backup' }} + +
+
+

{{ backup.filename }}

+

{{ backup.createdAt | date:'yyyy/MM/dd HH:mm' }} | {{ formatSize(backup.size) }}

+
+
+
+ + +
+
+
+ cloud_off +

لا يوجد نسخ احتياطية

+
+
+
+
+ `, +}) +export class BackupComponent implements OnInit { + backups: any[] = []; + isCreating = false; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { this.loadBackups(); } + + async loadBackups(): Promise { + const result = await this.electronService.getBackups(); + if (result?.success) this.backups = result.data; + } + + async createBackup(): Promise { + this.isCreating = true; + await this.electronService.createBackup(); + this.isCreating = false; + this.loadBackups(); + } + + async restore(backup: any): Promise { + if (confirm('هل أنت متأكد من استعادة هذه النسخة الاحتياطية؟')) { + await this.electronService.restoreBackup(backup.filename); + } + } + + async deleteBackup(backup: any): Promise { + if (confirm('هل أنت متأكد من حذف هذه النسخة الاحتياطية؟')) { + await this.electronService.deleteBackup(backup.id); + this.loadBackups(); + } + } + + formatSize(bytes: number): string { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } +} diff --git a/gym-management-system/src/app/pages/dashboard/dashboard.component.ts b/gym-management-system/src/app/pages/dashboard/dashboard.component.ts new file mode 100644 index 0000000000..1b501720f9 --- /dev/null +++ b/gym-management-system/src/app/pages/dashboard/dashboard.component.ts @@ -0,0 +1,165 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+ +
+ +
+
+ groups +
+
+
{{ stats.attendanceToday }}
+
الحضور اليوم
+
+
+ + +
+
+ event_busy +
+
+
{{ stats.expiredSubscriptions }}
+
اشتراكات منتهية
+
+
+ + +
+
+ attach_money +
+
+
{{ stats.dailyRevenue | number }}
+
إيرادات اليوم (ر.س)
+
+
+ + +
+
+ person +
+
+
{{ stats.totalMembers }}
+
إجمالي الأعضاء
+
+
+
+ + +
+ +
+
+

المشتركين حسب الشهر

+ +
+
+
+ {{ value }} +
+ {{ months[i] }} +
+
+
+ + +
+
+

الإيرادات حسب الشهر

+ +
+
+
+ {{ (value / 1000) | number:'1.0-0' }}k +
+ {{ months[i] }} +
+
+
+
+
+ `, +}) +export class DashboardComponent implements OnInit { + stats = { totalMembers: 0, attendanceToday: 0, expiredSubscriptions: 0, dailyRevenue: 0 }; + monthlySubscriptions: number[] = new Array(12).fill(0); + monthlyRevenue: number[] = new Array(12).fill(0); + months = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر']; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { + this.loadStats(); + this.loadMonthlyData(new Date().getFullYear()); + } + + async loadStats(): Promise { + const result = await this.electronService.getDashboardStats(); + if (result?.success) { + this.stats = result.data; + } + } + + async loadMonthlyData(year: number): Promise { + const [subsResult, revResult] = await Promise.all([ + this.electronService.getMonthlySubscriptions(year), + this.electronService.getMonthlyRevenue(year), + ]); + + if (subsResult?.success) this.monthlySubscriptions = subsResult.data; + if (revResult?.success) this.monthlyRevenue = revResult.data; + } + + getBarHeight(value: number, data: number[]): number { + const max = Math.max(...data, 1); + return (value / max) * 100; + } + + onSubYearChange(event: Event): void { + const year = parseInt((event.target as HTMLSelectElement).value); + this.electronService.getMonthlySubscriptions(year).then(r => { + if (r?.success) this.monthlySubscriptions = r.data; + }); + } + + onRevYearChange(event: Event): void { + const year = parseInt((event.target as HTMLSelectElement).value); + this.electronService.getMonthlyRevenue(year).then(r => { + if (r?.success) this.monthlyRevenue = r.data; + }); + } +} diff --git a/gym-management-system/src/app/pages/login/login.component.ts b/gym-management-system/src/app/pages/login/login.component.ts new file mode 100644 index 0000000000..8a3afebccb --- /dev/null +++ b/gym-management-system/src/app/pages/login/login.component.ts @@ -0,0 +1,124 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AuthService } from '../../core/services/auth.service'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+
+ +
+
+ fitness_center +
+

نظام إدارة النادي

+

قم بتسجيل الدخول للمتابعة

+
+ + +
+
+ +
+ +
+ person + +
+
+ + +
+ +
+ lock + + +
+
+ + +
+ {{ error }} +
+ + + +
+ + +
+

البيانات الافتراضية: Admin / Admin

+
+
+
+
+ `, +}) +export class LoginComponent { + username = ''; + password = ''; + showPassword = false; + isLoading = false; + error = ''; + + constructor(private authService: AuthService, private router: Router) { + if (this.authService.isAuthenticated) { + this.router.navigate(['/dashboard']); + } + } + + async onSubmit(): Promise { + if (!this.username || !this.password) { + this.error = 'يرجى إدخال اسم المستخدم وكلمة المرور'; + return; + } + + this.isLoading = true; + this.error = ''; + + const result = await this.authService.login(this.username, this.password); + + if (result.success) { + this.router.navigate(['/dashboard']); + } else { + this.error = result.error || 'حدث خطأ غير متوقع'; + } + + this.isLoading = false; + } +} diff --git a/gym-management-system/src/app/pages/members/member-detail/member-detail.component.ts b/gym-management-system/src/app/pages/members/member-detail/member-detail.component.ts new file mode 100644 index 0000000000..9071418900 --- /dev/null +++ b/gym-management-system/src/app/pages/members/member-detail/member-detail.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-member-detail', + standalone: true, + imports: [CommonModule], + template: ` +
+
+ person +

صفحة تفاصيل العضو

+
+
+ `, +}) +export class MemberDetailComponent {} diff --git a/gym-management-system/src/app/pages/members/members.component.ts b/gym-management-system/src/app/pages/members/members.component.ts new file mode 100644 index 0000000000..d9ec8cdd73 --- /dev/null +++ b/gym-management-system/src/app/pages/members/members.component.ts @@ -0,0 +1,295 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-members', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+ + + + +
+
+ search + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
العضورقم الهاتفرمز QRالاشتراكالحالةالإجراءات
+
+
+ person +
+
+

{{ member.fullName }}

+

{{ member.createdAt | date:'yyyy/MM/dd' }}

+
+
+
{{ member.phone }} + {{ member.qrCode }} + + + {{ getSubscriptionLabel(member.subscriptions[0]?.status) }} + + بدون اشتراك + + + +
+ + + +
+
+
+ people_outline +

لا يوجد أعضاء

+
+
+ + +
+
+
+

{{ editingMember ? 'تعديل عضو' : 'إضافة عضو جديد' }}

+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ warning +
+

هل أنت متأكد من عملية الحذف؟

+

لا يمكن التراجع بعد الحذف.

+
+ + +
+
+
+
+ `, +}) +export class MembersComponent implements OnInit { + members: any[] = []; + searchQuery = ''; + statusFilter = 'all'; + showAddModal = false; + showDeleteConfirm = false; + editingMember: any = null; + deletingMember: any = null; + memberForm: any = { fullName: '', phone: '', emergencyPhone: '', gender: 'male', email: '', notes: '' }; + + constructor(private electronService: ElectronService, private router: Router) {} + + ngOnInit(): void { + this.loadMembers(); + } + + async loadMembers(): Promise { + const result = await this.electronService.getMembers({ search: this.searchQuery, status: this.statusFilter }); + if (result?.success) { + this.members = result.data.members; + } + } + + onSearch(): void { + this.loadMembers(); + } + + viewMember(member: any): void { + this.router.navigate(['/members', member.id]); + } + + editMember(member: any): void { + this.editingMember = member; + this.memberForm = { ...member }; + this.showAddModal = true; + } + + confirmDelete(member: any): void { + this.deletingMember = member; + this.showDeleteConfirm = true; + } + + async deleteMember(): Promise { + if (this.deletingMember) { + await this.electronService.deleteMember(this.deletingMember.id); + this.showDeleteConfirm = false; + this.loadMembers(); + } + } + + async saveMember(): Promise { + if (this.editingMember) { + await this.electronService.updateMember(this.editingMember.id, this.memberForm); + } else { + await this.electronService.createMember(this.memberForm); + } + this.closeModal(); + this.loadMembers(); + } + + closeModal(): void { + this.showAddModal = false; + this.editingMember = null; + this.memberForm = { fullName: '', phone: '', emergencyPhone: '', gender: 'male', email: '', notes: '' }; + } + + getSubscriptionClass(status: string): string { + const classes: Record = { + active: 'bg-green-500/10 text-green-500', + frozen: 'bg-blue-500/10 text-blue-500', + expired: 'bg-red-500/10 text-red-500', + cancelled: 'bg-dark-500/10 text-dark-400', + }; + return classes[status] || 'bg-dark-500/10 text-dark-400'; + } + + getSubscriptionLabel(status: string): string { + const labels: Record = { + active: 'فعال', + frozen: 'مجمد', + expired: 'منتهي', + cancelled: 'ملغي', + }; + return labels[status] || status; + } +} diff --git a/gym-management-system/src/app/pages/nutrition/nutrition.component.ts b/gym-management-system/src/app/pages/nutrition/nutrition.component.ts new file mode 100644 index 0000000000..ae44adf640 --- /dev/null +++ b/gym-management-system/src/app/pages/nutrition/nutrition.component.ts @@ -0,0 +1,86 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-nutrition', + standalone: true, + imports: [CommonModule], + template: ` +
+ + +
+ +
+
+
+ restaurant +
+
+

الخطط الغذائية

+

إدارة البرامج الغذائية للأعضاء

+
+
+ +
+ + +
+
+
+ fitness_center +
+
+

برامج التدريب

+

إدارة البرامج التدريبية للأعضاء

+
+
+ +
+ + +
+
+
+ monitor_weight +
+
+

متابعة الوزن

+

تتبع قياسات وأوزان الأعضاء

+
+
+ +
+ + +
+
+
+ photo_camera +
+
+

صور التطور

+

توثيق التقدم بالصور

+
+
+ +
+
+
+ `, +}) +export class NutritionComponent {} diff --git a/gym-management-system/src/app/pages/payments/payments.component.ts b/gym-management-system/src/app/pages/payments/payments.component.ts new file mode 100644 index 0000000000..a93c3f84fe --- /dev/null +++ b/gym-management-system/src/app/pages/payments/payments.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-payments', + standalone: true, + imports: [CommonModule], + template: ` +
+ + + +
+
+
+ payments +
+
+

نقدي

+

{{ getTotalByMethod('cash') | number }} ر.س

+
+
+
+
+ account_balance +
+
+

تحويل بنكي

+

{{ getTotalByMethod('bank_transfer') | number }} ر.س

+
+
+
+
+ account_balance_wallet +
+
+

محفظة

+

{{ getTotalByMethod('wallet') | number }} ر.س

+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
رقم الفاتورةالعضوالمبلغطريقة الدفعالتاريخالإجراءات
{{ payment.invoiceNumber }}{{ payment.member?.fullName }}{{ payment.amount | number }} ر.س{{ getMethodLabel(payment.paymentMethod) }}{{ payment.createdAt | date:'yyyy/MM/dd' }} + +
+
+ payments +

لا يوجد مدفوعات

+
+
+ + +
+
+

تسجيل دفعة جديدة

+
+

قريبًا...

+
+ +
+
+
+ `, +}) +export class PaymentsComponent implements OnInit { + payments: any[] = []; + showAddModal = false; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { this.loadPayments(); } + + async loadPayments(): Promise { + const result = await this.electronService.getPayments(); + if (result?.success) this.payments = result.data.payments; + } + + getTotalByMethod(method: string): number { + return this.payments.filter(p => p.paymentMethod === method).reduce((sum, p) => sum + p.amount, 0); + } + + getMethodLabel(method: string): string { + const labels: Record = { cash: 'نقدي', bank_transfer: 'تحويل بنكي', wallet: 'محفظة' }; + return labels[method] || method; + } +} diff --git a/gym-management-system/src/app/pages/settings/settings.component.ts b/gym-management-system/src/app/pages/settings/settings.component.ts new file mode 100644 index 0000000000..c8f2efa49a --- /dev/null +++ b/gym-management-system/src/app/pages/settings/settings.component.ts @@ -0,0 +1,136 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-settings', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+ + +
+ +
+

+ settings + إعدادات عامة +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

+ security + إعدادات الأمان +

+
+
+ + +
+
+ النسخ الاحتياطي التلقائي + +
+
+
+ + +
+

+ palette + المظهر +

+
+
+ الوضع الداكن + +
+
+
+ + +
+

+ manage_accounts + إدارة المستخدمين +

+
+
+
+
+ person +
+
+

Admin

+

مدير النظام

+
+
+ admin +
+
+ +
+
+ + +
+ +
+
+ `, +}) +export class SettingsComponent { + settings = { + gym_name: 'النادي الرياضي', + gym_phone: '', + gym_address: '', + currency: 'SAR', + session_timeout: 30, + }; + autoBackup = true; + + saveSettings(): void { + // Save settings via electron service + alert('تم حفظ الإعدادات بنجاح'); + } +} diff --git a/gym-management-system/src/app/pages/store/store.component.ts b/gym-management-system/src/app/pages/store/store.component.ts new file mode 100644 index 0000000000..4dd3f0417d --- /dev/null +++ b/gym-management-system/src/app/pages/store/store.component.ts @@ -0,0 +1,112 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-store', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+ + + +
+
+
+ inventory_2 +
+

{{ product.name }}

+

{{ product.category }}

+
+ {{ product.price }} ر.س + + المخزون: {{ product.stock }} + +
+
+ + +
+
+
+ +
+ store +

لا يوجد منتجات

+
+ + +
+
+
+

إضافة منتج جديد

+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+ `, +}) +export class StoreComponent implements OnInit { + products: any[] = []; + showAddModal = false; + productForm = { name: '', price: 0, stock: 0, category: 'مكملات' }; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { this.loadProducts(); } + + async loadProducts(): Promise { + const result = await this.electronService.getProducts(); + if (result?.success) this.products = result.data.products; + } + + async saveProduct(): Promise { + await this.electronService.createProduct(this.productForm); + this.showAddModal = false; + this.productForm = { name: '', price: 0, stock: 0, category: 'مكملات' }; + this.loadProducts(); + } +} diff --git a/gym-management-system/src/app/pages/subscriptions/subscriptions.component.ts b/gym-management-system/src/app/pages/subscriptions/subscriptions.component.ts new file mode 100644 index 0000000000..27f7f2d7b0 --- /dev/null +++ b/gym-management-system/src/app/pages/subscriptions/subscriptions.component.ts @@ -0,0 +1,163 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-subscriptions', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+ + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
العضونوع الاشتراكالسعرتاريخ البدءتاريخ الانتهاءالحالةالإجراءات
{{ sub.member?.fullName }}{{ getTypeLabel(sub.type) }}{{ sub.price | number }} ر.س{{ sub.startDate | date:'yyyy/MM/dd' }}{{ sub.endDate | date:'yyyy/MM/dd' }} + + {{ getStatusLabel(sub.status) }} + + +
+ + + + +
+
+
+ card_membership +

لا يوجد اشتراكات

+
+
+ + +
+
+
+

اشتراك جديد

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ `, +}) +export class SubscriptionsComponent implements OnInit { + subscriptions: any[] = []; + statusFilter = 'all'; + showAddModal = false; + subForm = { type: 'monthly', price: 300 }; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { this.loadSubscriptions(); } + + async loadSubscriptions(): Promise { + const result = await this.electronService.getSubscriptions({ status: this.statusFilter }); + if (result?.success) this.subscriptions = result.data.subscriptions; + } + + async freeze(sub: any): Promise { await this.electronService.freezeSubscription(sub.id); this.loadSubscriptions(); } + async unfreeze(sub: any): Promise { await this.electronService.unfreezeSubscription(sub.id); this.loadSubscriptions(); } + async cancel(sub: any): Promise { await this.electronService.cancelSubscription(sub.id); this.loadSubscriptions(); } + async renew(sub: any): Promise { /* Open renew dialog */ } + + async saveSubscription(): Promise { + this.showAddModal = false; + this.loadSubscriptions(); + } + + updatePrice(): void { + const prices: Record = { monthly: 300, quarterly: 800, semi_annual: 1400, annual: 2500 }; + this.subForm.price = prices[this.subForm.type] || 300; + } + + getTypeLabel(type: string): string { + const labels: Record = { monthly: 'شهري', quarterly: '3 أشهر', semi_annual: '6 أشهر', annual: 'سنوي' }; + return labels[type] || type; + } + + getStatusClass(status: string): string { + const classes: Record = { active: 'bg-green-500/10 text-green-500', frozen: 'bg-blue-500/10 text-blue-500', expired: 'bg-red-500/10 text-red-500', cancelled: 'bg-dark-500/10 text-dark-400' }; + return classes[status] || ''; + } + + getStatusLabel(status: string): string { + const labels: Record = { active: 'فعال', frozen: 'مجمد', expired: 'منتهي', cancelled: 'ملغي' }; + return labels[status] || status; + } +} diff --git a/gym-management-system/src/app/pages/trainers/trainers.component.ts b/gym-management-system/src/app/pages/trainers/trainers.component.ts new file mode 100644 index 0000000000..d38b5e4fd4 --- /dev/null +++ b/gym-management-system/src/app/pages/trainers/trainers.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ElectronService } from '../../core/services/electron.service'; + +@Component({ + selector: 'app-trainers', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+ + + +
+
+
+
+ sports_martial_arts +
+
+

{{ trainer.fullName }}

+

{{ trainer.specialization }}

+
+
+
+
+ phone + {{ trainer.phone }} +
+
+ + + {{ trainer.isActive ? 'نشط' : 'غير نشط' }} + +
+
+
+ + +
+
+
+ +
+ sports_martial_arts +

لا يوجد مدربين

+
+ + +
+
+
+

إضافة مدرب جديد

+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+ `, +}) +export class TrainersComponent implements OnInit { + trainers: any[] = []; + showAddModal = false; + trainerForm = { fullName: '', phone: '', specialization: '', salary: null }; + + constructor(private electronService: ElectronService) {} + + ngOnInit(): void { this.loadTrainers(); } + + async loadTrainers(): Promise { + const result = await this.electronService.getTrainers(); + if (result?.success) this.trainers = result.data.trainers; + } + + async saveTrainer(): Promise { + await this.electronService.createTrainer(this.trainerForm); + this.showAddModal = false; + this.trainerForm = { fullName: '', phone: '', specialization: '', salary: null }; + this.loadTrainers(); + } +} diff --git a/gym-management-system/src/environments/environment.development.ts b/gym-management-system/src/environments/environment.development.ts new file mode 100644 index 0000000000..a20cfe5573 --- /dev/null +++ b/gym-management-system/src/environments/environment.development.ts @@ -0,0 +1,3 @@ +export const environment = { + production: false, +}; diff --git a/gym-management-system/src/environments/environment.ts b/gym-management-system/src/environments/environment.ts new file mode 100644 index 0000000000..c9669790be --- /dev/null +++ b/gym-management-system/src/environments/environment.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/gym-management-system/src/index.html b/gym-management-system/src/index.html new file mode 100644 index 0000000000..f2d9b16224 --- /dev/null +++ b/gym-management-system/src/index.html @@ -0,0 +1,13 @@ + + + + + نظام إدارة النادي الرياضي + + + + + + + + diff --git a/gym-management-system/src/main.ts b/gym-management-system/src/main.ts new file mode 100644 index 0000000000..55df52b131 --- /dev/null +++ b/gym-management-system/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err: any) => console.error(err)); diff --git a/gym-management-system/src/styles.scss b/gym-management-system/src/styles.scss new file mode 100644 index 0000000000..ced5f11bcb --- /dev/null +++ b/gym-management-system/src/styles.scss @@ -0,0 +1,118 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + font-family: 'Segoe UI', Tahoma, Arial, sans-serif; + direction: rtl; +} + +body { + background-color: #09090B; + color: #F4F4F5; + overflow: hidden; +} + +// Scrollbar styling +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #18181B; +} + +::-webkit-scrollbar-thumb { + background: #3F3F46; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #52525B; +} + +// Angular Material Overrides for dark theme +.mat-mdc-dialog-container { + --mdc-dialog-container-color: #18181B !important; +} + +.mat-mdc-snack-bar-container { + --mdc-snackbar-container-color: #27272A !important; +} + +.mat-mdc-form-field { + --mdc-filled-text-field-container-color: #27272A !important; + --mdc-outlined-text-field-outline-color: #3F3F46 !important; +} + +// Custom utility classes +.card { + @apply bg-dark-800 rounded-xl border border-dark-700 p-6; +} + +.btn-primary { + @apply bg-primary-600 hover:bg-primary-700 text-white px-6 py-2 rounded-lg font-bold transition-colors; +} + +.btn-secondary { + @apply bg-dark-700 hover:bg-dark-600 text-white px-6 py-2 rounded-lg font-bold transition-colors; +} + +.btn-danger { + @apply bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg font-bold transition-colors; +} + +// Table styles +.data-table { + @apply w-full; + + th { + @apply bg-dark-800 text-dark-300 font-bold text-sm px-4 py-3 text-right; + } + + td { + @apply px-4 py-3 border-b border-dark-700 text-sm; + } + + tr:hover td { + @apply bg-dark-800/50; + } +} + +// Page layout +.page-container { + @apply p-6 h-full overflow-y-auto; +} + +.page-header { + @apply flex items-center justify-between mb-6; +} + +.page-title { + @apply text-2xl font-bold; +} + +// Stats card +.stat-card { + @apply bg-dark-800 rounded-xl border border-dark-700 p-5 flex items-center gap-4; +} + +.stat-icon { + @apply w-14 h-14 rounded-xl flex items-center justify-center text-2xl; +} + +.stat-value { + @apply text-3xl font-bold; +} + +.stat-label { + @apply text-dark-400 text-sm mt-1; +} diff --git a/gym-management-system/tailwind.config.js b/gym-management-system/tailwind.config.js new file mode 100644 index 0000000000..5fe0876106 --- /dev/null +++ b/gym-management-system/tailwind.config.js @@ -0,0 +1,46 @@ +// @ts-check +const tsConfig = require("./tsconfig.json"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{html,ts}", + ], + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + DEFAULT: '#DC2626', + 50: '#FEF2F2', + 100: '#FEE2E2', + 200: '#FECACA', + 300: '#FCA5A5', + 400: '#F87171', + 500: '#EF4444', + 600: '#DC2626', + 700: '#B91C1C', + 800: '#991B1B', + 900: '#7F1D1D', + }, + dark: { + DEFAULT: '#09090B', + 50: '#F4F4F5', + 100: '#E4E4E7', + 200: '#D4D4D8', + 300: '#A1A1AA', + 400: '#71717A', + 500: '#52525B', + 600: '#3F3F46', + 700: '#27272A', + 800: '#18181B', + 900: '#09090B', + }, + }, + fontFamily: { + 'cairo': ['Cairo', 'sans-serif'], + }, + }, + }, + plugins: [], +} diff --git a/gym-management-system/tsconfig.app.json b/gym-management-system/tsconfig.app.json new file mode 100644 index 0000000000..5b9d3c5ecb --- /dev/null +++ b/gym-management-system/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/gym-management-system/tsconfig.json b/gym-management-system/tsconfig.json new file mode 100644 index 0000000000..449376f476 --- /dev/null +++ b/gym-management-system/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"], + "baseUrl": "./", + "paths": { + "@core/*": ["src/app/core/*"], + "@shared/*": ["src/app/shared/*"], + "@pages/*": ["src/app/pages/*"], + "@state/*": ["src/app/state/*"], + "@env/*": ["src/environments/*"] + } + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +}