Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions gym-management-system/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/
dist/
dist-electron/
release/
storage/database.sqlite
storage/backups/*.sqlite
storage/uploads/*
storage/temp/*
.angular/
*.log
.env
69 changes: 69 additions & 0 deletions gym-management-system/angular.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
63 changes: 63 additions & 0 deletions gym-management-system/electron/database/init.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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 });
}
}
}
139 changes: 139 additions & 0 deletions gym-management-system/electron/ipc/attendance.handlers.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
});
}
66 changes: 66 additions & 0 deletions gym-management-system/electron/ipc/auth.handlers.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
});
}
Loading
Loading