Descriptive message about your changes

This commit is contained in:
yznahmad 2025-07-26 18:35:09 +03:00
parent 87cb513317
commit 386241b488
35 changed files with 1759 additions and 376 deletions

View File

@ -63,18 +63,54 @@ export async function GET(req: Request) {
}); });
} }
// Calculate plan status (active/expired) // Calculate plan status based on individual services (active/expired)
const currentTime = Math.floor(Date.now() / 1000); const currentTime = Math.floor(Date.now() / 1000);
const planStatus = member.planExpAt_unix > currentTime ? 'active' : 'expired'; const activeServices = member.services?.filter(service =>
service.active && service.planExpAt_unix > currentTime
) || [];
// Prepare the response data with only the required fields const expiredServices = member.services?.filter(service =>
!service.active || service.planExpAt_unix <= currentTime
) || [];
const planStatus = activeServices.length > 0 ? 'active' : 'expired';
// Get the latest expiration date from active services
const latestExpiration = activeServices.length > 0
? Math.max(...activeServices.map(s => s.planExpAt_unix))
: member.planExpAt_unix;
// Prepare detailed services information
const servicesInfo = member.services?.map(service => ({
serviceID: service.serviceID,
serviceName: service.serviceName,
registeredAt: service.registeredAt,
registeredAt_unix: service.registeredAt_unix,
planDelay: service.planDelay,
planDelay_unix: service.planDelay_unix,
planExpAt: service.planExpAt,
planExpAt_unix: service.planExpAt_unix,
planUpdatedAt: service.planUpdatedAt,
planUpdatedAt_unix: service.planUpdatedAt_unix,
active: service.active,
status: (service.active && service.planExpAt_unix > currentTime) ? 'active' : 'expired',
daysRemaining: service.planExpAt_unix > currentTime
? Math.ceil((service.planExpAt_unix - currentTime) / (24 * 60 * 60))
: 0
})) || [];
// Prepare the response data with detailed services information
const memberInfo = { const memberInfo = {
name: `${member.firstName} ${member.lastName}`, name: `${member.firstName} ${member.lastName}`,
gender: member.gendre, gender: member.gendre,
planDelay: member.planDelay, planDelay: member.planDelay,
planStart: member.planUpdatedAt, planStart: member.planUpdatedAt,
planStatus: planStatus, planStatus: planStatus,
planExpAt: member.planExpAt planExpAt: new Date(latestExpiration * 1000).toUTCString(),
activeServicesCount: activeServices.length,
expiredServicesCount: expiredServices.length,
totalServicesCount: member.services?.length || 0,
services: servicesInfo
}; };
// Return the member information // Return the member information

View File

@ -73,7 +73,7 @@ export async function GET(req:Request)
{ {
// get the page // get the page
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0', const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// get the docs // get the docs
const docs = await equipmentModel.find({}).skip(range[0]).limit(range[1]), const docs = await equipmentModel.find({}).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
@ -141,7 +141,7 @@ export async function DELETE(req:Request)
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const page = searchParams.get('page'), const page = searchParams.get('page'),
_id : string | null = searchParams.get('_id'), _id : string | null = searchParams.get('_id'),
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// delete the doc // delete the doc
await equipmentModel.findByIdAndRemove(_id) await equipmentModel.findByIdAndRemove(_id)
// get the docs by page // get the docs by page

View File

@ -70,9 +70,9 @@ export async function GET(req:Request)
{ {
// get the page // get the page
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0', const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// get the docs // get the docs (sorted by addedAt in descending order to show newest first)
const docs = await expenseModel.find({}).skip(range[0]).limit(range[1]), const docs = await expenseModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
docs_count = await expenseModel.countDocuments({}); docs_count = await expenseModel.countDocuments({});
// prepare the return data // prepare the return data
@ -90,14 +90,14 @@ export async function GET(req:Request)
{ {
// get the searchKeyword param // get the searchKeyword param
let searchKeyword = searchParams.get('searchKeyword') let searchKeyword = searchParams.get('searchKeyword')
// get the search results docs // get the search results docs (sorted by addedAt in descending order)
let results = await expenseModel.find({ let results = await expenseModel.find({
$or: [ $or: [
// search by ( case insensitive search ) // search by ( case insensitive search )
{ name: { $regex: searchKeyword, $options: 'i' } }, { name: { $regex: searchKeyword, $options: 'i' } },
{ description: { $regex: searchKeyword, $options: 'i' } } { description: { $regex: searchKeyword, $options: 'i' } }
] ]
}) }).sort({ addedAt: -1 })
// get the docs // get the docs
let responseData = { let responseData = {
docs: results, docs: results,
@ -136,7 +136,7 @@ export async function DELETE(req:Request)
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const page = searchParams.get('page'), const page = searchParams.get('page'),
_id : string | null = searchParams.get('_id'), _id : string | null = searchParams.get('_id'),
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// update the outcome // update the outcome
// get the old amount // get the old amount
let doc = await expenseModel.findById(_id) let doc = await expenseModel.findById(_id)
@ -148,8 +148,8 @@ export async function DELETE(req:Request)
}) })
// delete the doc // delete the doc
await expenseModel.findByIdAndRemove(_id) await expenseModel.findByIdAndRemove(_id)
// get the docs by page // get the docs by page (sorted by addedAt in descending order)
const docs = await expenseModel.find({}).skip(range[0]).limit(range[1]), const docs = await expenseModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
docs_count = await expenseModel.countDocuments({}); docs_count = await expenseModel.countDocuments({});

View File

@ -0,0 +1,167 @@
import dbConnect from "@/database/dbConnect";
import { NextResponse } from "next/server";
import incomeModel from "@/database/models/incomeModel";
import expenseModel from "@/database/models/expenseModel";
// GET METHOD - Fetch monthly and yearly income/expense analytics
export async function GET(req: Request) {
try {
await dbConnect();
const url = new URL(req.url);
const view = url.searchParams.get('view') || 'monthly'; // 'monthly' or 'yearly'
const year = parseInt(url.searchParams.get('year') || new Date().getFullYear().toString());
const month = parseInt(url.searchParams.get('month') || (new Date().getMonth() + 1).toString());
let data = {};
if (view === 'monthly') {
// Get monthly data for the specified year
const monthlyData = [];
for (let m = 1; m <= 12; m++) {
const startOfMonth = new Date(year, m - 1, 1);
const endOfMonth = new Date(year, m, 0, 23, 59, 59, 999);
const startUnix = Math.floor(startOfMonth.getTime() / 1000);
const endUnix = Math.floor(endOfMonth.getTime() / 1000);
// Aggregate income for this month
const incomeResult = await incomeModel.aggregate([
{
$match: {
addedAt: { $gte: startUnix, $lte: endUnix }
}
},
{
$group: {
_id: null,
totalIncome: { $sum: "$amount" },
count: { $sum: 1 }
}
}
]);
// Aggregate expenses for this month
const expenseResult = await expenseModel.aggregate([
{
$match: {
addedAt: { $gte: startUnix, $lte: endUnix }
}
},
{
$group: {
_id: null,
totalExpense: { $sum: "$amount" },
count: { $sum: 1 }
}
}
]);
monthlyData.push({
month: m,
monthName: new Date(year, m - 1).toLocaleString('default', { month: 'long' }),
income: incomeResult[0]?.totalIncome || 0,
expense: expenseResult[0]?.totalExpense || 0,
incomeCount: incomeResult[0]?.count || 0,
expenseCount: expenseResult[0]?.count || 0,
net: (incomeResult[0]?.totalIncome || 0) - (expenseResult[0]?.totalExpense || 0)
});
}
data = {
view: 'monthly',
year,
months: monthlyData,
totalIncome: monthlyData.reduce((sum, month) => sum + month.income, 0),
totalExpense: monthlyData.reduce((sum, month) => sum + month.expense, 0),
totalNet: monthlyData.reduce((sum, month) => sum + month.net, 0)
};
} else if (view === 'yearly') {
// Get yearly data for the last 5 years
const currentYear = new Date().getFullYear();
const yearlyData = [];
for (let y = currentYear - 4; y <= currentYear; y++) {
const startOfYear = new Date(y, 0, 1);
const endOfYear = new Date(y, 11, 31, 23, 59, 59, 999);
const startUnix = Math.floor(startOfYear.getTime() / 1000);
const endUnix = Math.floor(endOfYear.getTime() / 1000);
// Aggregate income for this year
const incomeResult = await incomeModel.aggregate([
{
$match: {
addedAt: { $gte: startUnix, $lte: endUnix }
}
},
{
$group: {
_id: null,
totalIncome: { $sum: "$amount" },
count: { $sum: 1 }
}
}
]);
// Aggregate expenses for this year
const expenseResult = await expenseModel.aggregate([
{
$match: {
addedAt: { $gte: startUnix, $lte: endUnix }
}
},
{
$group: {
_id: null,
totalExpense: { $sum: "$amount" },
count: { $sum: 1 }
}
}
]);
yearlyData.push({
year: y,
income: incomeResult[0]?.totalIncome || 0,
expense: expenseResult[0]?.totalExpense || 0,
incomeCount: incomeResult[0]?.count || 0,
expenseCount: expenseResult[0]?.count || 0,
net: (incomeResult[0]?.totalIncome || 0) - (expenseResult[0]?.totalExpense || 0)
});
}
data = {
view: 'yearly',
years: yearlyData,
totalIncome: yearlyData.reduce((sum, year) => sum + year.income, 0),
totalExpense: yearlyData.reduce((sum, year) => sum + year.expense, 0),
totalNet: yearlyData.reduce((sum, year) => sum + year.net, 0)
};
}
return NextResponse.json({
success: true,
message: "Analytics data retrieved successfully",
data
}, {
status: 200,
headers: {
"content-type": "application/json"
}
});
} catch (error) {
console.error("Error fetching income/expense analytics:", error);
return NextResponse.json({
success: false,
message: "Server error"
}, {
status: 500,
headers: {
"content-type": "application/json"
}
});
}
}

View File

@ -70,9 +70,10 @@ export async function GET(req:Request)
{ {
// get the page // get the page
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0', const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// get the docs // range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
const docs = await incomesModel.find({}).skip(range[0]).limit(range[1]), // get the docs (sorted by addedAt in descending order to show newest first)
const docs = await incomesModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
docs_count = await incomesModel.countDocuments({}); docs_count = await incomesModel.countDocuments({});
// prepare the return data // prepare the return data
@ -90,14 +91,14 @@ export async function GET(req:Request)
{ {
// get the searchKeyword param // get the searchKeyword param
let searchKeyword = searchParams.get('searchKeyword') let searchKeyword = searchParams.get('searchKeyword')
// get the search results docs // get the search results docs (sorted by addedAt in descending order)
let results = await incomesModel.find({ let results = await incomesModel.find({
$or: [ $or: [
// search by ( case insensitive search ) // search by ( case insensitive search )
{ name: { $regex: searchKeyword, $options: 'i' } }, { name: { $regex: searchKeyword, $options: 'i' } },
{ description: { $regex: searchKeyword, $options: 'i' } } { description: { $regex: searchKeyword, $options: 'i' } }
] ]
}) }).sort({ addedAt: -1 })
// get the docs // get the docs
let responseData = { let responseData = {
docs: results, docs: results,
@ -136,7 +137,7 @@ export async function DELETE(req:Request)
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const page = searchParams.get('page'), const page = searchParams.get('page'),
_id : string | null = searchParams.get('_id'), _id : string | null = searchParams.get('_id'),
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// update the outcome // update the outcome
// get the old amount // get the old amount
let doc = await incomesModel.findById(_id) let doc = await incomesModel.findById(_id)
@ -148,8 +149,8 @@ export async function DELETE(req:Request)
}) })
// delete the doc // delete the doc
await incomesModel.findByIdAndRemove(_id) await incomesModel.findByIdAndRemove(_id)
// get the docs by page // get the docs by page (sorted by addedAt in descending order)
const docs = await incomesModel.find({}).skip(range[0]).limit(range[1]), const docs = await incomesModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
docs_count = await incomesModel.countDocuments({}); docs_count = await incomesModel.countDocuments({});
// prepare the return data // prepare the return data

View File

@ -20,9 +20,29 @@ export async function POST(req:Request)
email : string | null = payload.email as string | null, email : string | null = payload.email as string | null,
phone : string | null = payload.phone as string | null, phone : string | null = payload.phone as string | null,
address : string | null = payload.address as string | null, address : string | null = payload.address as string | null,
services : [string] | [] = payload.services as [string] | [], // Parse and validate services
services : string[] = (() => {
try {
if (typeof payload.services === 'string') {
const parsed = JSON.parse(payload.services);
if (!Array.isArray(parsed)) {
throw new Error('Services must be an array');
}
return parsed;
} else if (Array.isArray(payload.services)) {
return payload.services;
} else {
throw new Error('Services must be an array or stringified array');
}
} catch (parseError) {
console.error('Error parsing services:', parseError);
console.error('Services value:', payload.services);
console.error('Services type:', typeof payload.services);
return [];
}
})(),
planDelay : number = payload.planDelay ? payload.planDelay : 1 as number, planDelay : number = payload.planDelay ? payload.planDelay : 1 as number,
gendre : string | null = payload.gendre as string | null, gendre : 'm' | 'w' = payload.gendre as 'm' | 'w',
bodyState : { bodyState : {
startBodyForm: String, startBodyForm: String,
startWeight: Number, startWeight: Number,
@ -30,38 +50,101 @@ export async function POST(req:Request)
startBodyForm: String, startBodyForm: String,
startWeight: Number, startWeight: Number,
} | null; } | null;
// calculat month pay
// Validate required fields
if (!firstName || !lastName || !phone) {
return NextResponse.json({
success: false,
message: "Missing required fields: firstName, lastName, and phone are required",
}, {
status: 400,
headers: {
"content-type": "application/json"
}
})
}
// Validate gendre field
if (gendre && gendre !== 'm' && gendre !== 'w') {
return NextResponse.json({
success: false,
message: "Invalid gender value. Must be 'm' or 'w'",
}, {
status: 400,
headers: {
"content-type": "application/json"
}
})
}
// Validate services array
if (!services || services.length === 0) {
return NextResponse.json({
success: false,
message: "At least one service must be selected",
}, {
status: 400,
headers: {
"content-type": "application/json"
}
})
}
// Get service details and create per-service subscription data
const ids = services?.map((v , i) => { const ids = services?.map((v , i) => {
return new mongoose.Types.ObjectId(v as any) return new mongoose.Types.ObjectId(v as any)
}) })
const servicesDocs = await serviceModel.aggregate([ const servicesDocs = await serviceModel.find({ _id: { $in: ids } })
{
$match: { // Validate that all services exist
_id: { $in : ids } if (servicesDocs.length !== services.length) {
return NextResponse.json({
success: false,
message: "One or more selected services do not exist",
}, {
status: 400,
headers: {
"content-type": "application/json"
} }
}, })
{ }
$group: {
_id: null, // Calculate total monthly pay
totalPrice: { const payMonth = servicesDocs.reduce((total, service) => total + service.price, 0)
$sum: "$price"
} // Declare needed variables
} let current_date = new Date(),
} current_date_unix = Math.floor(Date.now() / 1000),
]) planDelay_unix = planDelay ? planDelay * 2592000: 0 * 2592000,
// inc the value of totalSubscribers for all included services in the membership planExpAt = new Date((current_date_unix + planDelay_unix) * 1000);
// Create services array with per-service subscription details
const servicesArray = servicesDocs.map(service => ({
serviceID: service._id.toString(),
serviceName: service.name,
registeredAt: current_date.toUTCString(),
registeredAt_unix: current_date_unix,
planDelay: planDelay,
planDelay_unix: planDelay_unix,
planExpAt: planExpAt.toUTCString(),
planExpAt_unix: current_date_unix + planDelay_unix,
planUpdatedAt: current_date.toUTCString(),
planUpdatedAt_unix: current_date_unix,
active: true,
}))
// Debug logging
// Debug: Log services parsing
console.log('Original services from payload:', payload.services);
console.log('Parsed services:', services);
// Increment the value of totalSubscribers for all included services
await serviceModel.updateMany({_id : {$in : ids}} , { await serviceModel.updateMany({_id : {$in : ids}} , {
$inc: { $inc: {
totalSubscribers: 1 totalSubscribers: 1
} }
}) })
const payMonth : number = servicesDocs[0] ? servicesDocs[0].totalPrice : 0
// declare needed variables // Save the doc with new structure
let current_date = new Date(),
current_date_unix = Math.floor(Date.now() / 1000),
planDelay_unix = planDelay ? planDelay * 2592000: 0 * 2592000,
planExpAt = new Date((current_date_unix + planDelay_unix) * 1000);
// save the doc
const doc = new memberModel({ const doc = new memberModel({
firstName, firstName,
lastName, lastName,
@ -69,11 +152,12 @@ export async function POST(req:Request)
phone, phone,
address, address,
payMonth, payMonth,
services, services: servicesArray,
gendre, gendre,
bodyState, bodyState,
registerAt: current_date.toUTCString(), registerAt: current_date.toUTCString(),
registerAt_unix: current_date_unix, registerAt_unix: current_date_unix,
// Global subscription status (derived from services)
planUpdatedAt: current_date.toUTCString(), planUpdatedAt: current_date.toUTCString(),
planUpdatedAt_unix: current_date_unix, planUpdatedAt_unix: current_date_unix,
planDelay, planDelay,
@ -82,8 +166,36 @@ export async function POST(req:Request)
planExpAt_unix: current_date_unix + planDelay_unix, planExpAt_unix: current_date_unix + planDelay_unix,
active: true, active: true,
}) })
await doc.save() await doc.save()
// Create income entry for new member subscription
if (payMonth > 0) {
try {
const serviceNames = await serviceModel.find({ _id: { $in: ids } }, 'name');
const serviceNamesList = serviceNames.map(service => service.name).join(', ');
const totalAmount = payMonth * planDelay;
const incomeEntry = new incomeModel({
name: `New Member Subscription - ${firstName} ${lastName}`,
description: `New member subscription for services: ${serviceNamesList} (${planDelay} month${planDelay > 1 ? 's' : ''})`,
amount: totalAmount,
addedAt: current_date_unix
});
await incomeEntry.save();
// Update total income in statistics
await statisticsModel.findOneAndUpdate({}, {
$inc: {
totalIncome: totalAmount
}
}, { upsert: true });
} catch (incomeError) {
console.error('Error creating income entry:', incomeError);
// Don't fail the member creation if income entry fails
}
}
// return the success response with the new added doc // return the success response with the new added doc
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@ -97,10 +209,14 @@ export async function POST(req:Request)
}) })
}catch(e) }catch(e)
{ {
// Log the actual error for debugging
console.error('Error adding new member:', e);
// catch any error and return an error response // catch any error and return an error response
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: "serverError", message: "serverError",
error: e instanceof Error ? e.message : 'Unknown error'
}, { }, {
status: 500, status: 500,
headers: { headers: {
@ -171,10 +287,14 @@ export async function GET(req:Request)
} }
}catch(e) }catch(e)
{ {
// Log the actual error for debugging
console.error('Error updating member:', e);
// catch any error and return an error response // catch any error and return an error response
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: "serverError", message: "serverError",
error: e instanceof Error ? e.message : 'Unknown error'
}, { }, {
status: 500, status: 500,
headers: { headers: {
@ -211,10 +331,14 @@ export async function DELETE(req:Request)
}) })
}catch(e) }catch(e)
{ {
// Log the actual error for debugging
console.error('Error in GET request:', e);
// catch any error and return an error response // catch any error and return an error response
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: "serverError", message: "serverError",
error: e instanceof Error ? e.message : 'Unknown error'
}, { }, {
status: 500, status: 500,
headers: { headers: {
@ -242,7 +366,8 @@ export async function PUT(req:Request)
email : string | null = payload.email as string | null, email : string | null = payload.email as string | null,
phone : string | null = payload.phone as string | null, phone : string | null = payload.phone as string | null,
address : string | null = payload.address as string | null, address : string | null = payload.address as string | null,
active : boolean | null = payload.active as boolean | null; active : boolean | null = payload.active as boolean | null,
gendre : 'm' | 'w' | null = payload.gendre as 'm' | 'w' | null;
// save the doc // save the doc
const doc = await memberModel.findByIdAndUpdate( _id , { const doc = await memberModel.findByIdAndUpdate( _id , {
firstName, firstName,
@ -251,6 +376,7 @@ export async function PUT(req:Request)
phone, phone,
address, address,
active, active,
gendre,
} , { } , {
new: true new: true
}) })
@ -270,7 +396,18 @@ export async function PUT(req:Request)
{ {
// declare the needed variables // declare the needed variables
const _id : string | null = payload._id as string | null, const _id : string | null = payload._id as string | null,
services : [string] | [] = payload.services as [string] | [], // Parse services if it's a string (handle both string and array cases)
newServices : string[] = (() => {
if (typeof payload.services === 'string') {
try {
return JSON.parse(payload.services);
} catch (parseError) {
console.error('Error parsing services JSON in PUT:', parseError);
return [];
}
}
return payload.services as string[];
})(),
planDelay : number = payload.planDelay ? payload.planDelay : 1 as number, planDelay : number = payload.planDelay ? payload.planDelay : 1 as number,
bodyState : { bodyState : {
currentBodyForm: String, currentBodyForm: String,
@ -279,53 +416,166 @@ export async function PUT(req:Request)
currentBodyForm: String, currentBodyForm: String,
currentWeight: Number, currentWeight: Number,
} | null; } | null;
// calculat month pay
const ids = services?.map((v , i) => { // Get current member data
return new mongoose.Types.ObjectId(v as string) const currentMember = await memberModel.findById(_id);
}) if (!currentMember) {
const servicesDocs = await serviceModel.aggregate([ return NextResponse.json({
{ success: false,
$match: { message: "Member not found",
_id: { $in : ids } }, {
status: 404,
headers: {
"content-type": "application/json"
} }
}, })
{ }
$group: {
_id: null, // Get service details for new services
totalPrice: { const newServiceIds = newServices.map(id => new mongoose.Types.ObjectId(id as string));
$sum: "$price" const newServicesDocs = await serviceModel.find({ _id: { $in: newServiceIds } });
}
// Current time variables
const current_date = new Date();
const current_date_unix = Math.floor(Date.now() / 1000);
const planDelay_unix = planDelay * 2592000;
// Process current services and new services
const currentServices = currentMember.services || [];
const updatedServices = [...currentServices];
let totalIncome = 0;
const incomeServiceNames = [];
// Handle each new service
for (const newServiceDoc of newServicesDocs) {
const serviceId = newServiceDoc._id.toString();
const existingServiceIndex = updatedServices.findIndex(s => s.serviceID === serviceId);
if (existingServiceIndex >= 0) {
// Service exists - extend or replace based on current status
const existingService = updatedServices[existingServiceIndex];
const isServiceActive = existingService.planExpAt_unix > current_date_unix;
if (isServiceActive) {
// Extend active service
const newExpiration = existingService.planExpAt_unix + planDelay_unix;
updatedServices[existingServiceIndex] = {
serviceID: existingService.serviceID,
serviceName: existingService.serviceName,
registeredAt: existingService.registeredAt,
registeredAt_unix: existingService.registeredAt_unix,
planDelay: existingService.planDelay + planDelay,
planDelay_unix: existingService.planDelay_unix + planDelay_unix,
planExpAt: new Date(newExpiration * 1000).toUTCString(),
planExpAt_unix: newExpiration,
planUpdatedAt: current_date.toUTCString(),
planUpdatedAt_unix: current_date_unix,
active: true,
};
// Charge for extension
totalIncome += newServiceDoc.price * planDelay;
incomeServiceNames.push(`${newServiceDoc.name} (extended)`);
} else {
// Replace expired service
const newExpiration = current_date_unix + planDelay_unix;
updatedServices[existingServiceIndex] = {
serviceID: existingService.serviceID,
serviceName: existingService.serviceName,
registeredAt: current_date.toUTCString(),
registeredAt_unix: current_date_unix,
planDelay: planDelay,
planDelay_unix: planDelay_unix,
planExpAt: new Date(newExpiration * 1000).toUTCString(),
planExpAt_unix: newExpiration,
planUpdatedAt: current_date.toUTCString(),
planUpdatedAt_unix: current_date_unix,
active: true,
};
// Charge for replacement
totalIncome += newServiceDoc.price * planDelay;
incomeServiceNames.push(`${newServiceDoc.name} (renewed)`);
} }
} else {
// New service - add it
const newExpiration = current_date_unix + planDelay_unix;
updatedServices.push({
serviceID: serviceId,
serviceName: newServiceDoc.name,
registeredAt: current_date.toUTCString(),
registeredAt_unix: current_date_unix,
planDelay: planDelay,
planDelay_unix: planDelay_unix,
planExpAt: new Date(newExpiration * 1000).toUTCString(),
planExpAt_unix: newExpiration,
planUpdatedAt: current_date.toUTCString(),
planUpdatedAt_unix: current_date_unix,
active: true,
});
// Charge for new service
totalIncome += newServiceDoc.price * planDelay;
incomeServiceNames.push(`${newServiceDoc.name} (new)`);
} }
]) }
const payMonth : number = servicesDocs[0] ? servicesDocs[0].totalPrice : 0
// declare needed variables // Calculate total monthly pay and global subscription status
let current_date = new Date(), const allServiceIds = updatedServices.map(s => new mongoose.Types.ObjectId(s.serviceID));
current_date_unix = Math.floor(Date.now() / 1000), const allServicesDocs = await serviceModel.find({ _id: { $in: allServiceIds } });
planDelay_unix = planDelay ? planDelay * 2592000: 0 * 2592000, const payMonth = allServicesDocs.reduce((total, service) => total + service.price, 0);
planExpAt = new Date((current_date_unix + planDelay_unix) * 1000);
// save the doc // Determine global subscription status (latest expiration date)
const doc = await memberModel.findByIdAndUpdate( _id , { const latestExpiration = Math.max(...updatedServices.map(s => s.planExpAt_unix));
const globalActive = latestExpiration > current_date_unix;
const globalPlanDelay = Math.max(...updatedServices.map(s => s.planDelay));
// Convert services to plain objects to avoid Mongoose document nesting issues
const plainServices = updatedServices.map(service => ({
serviceID: service.serviceID,
serviceName: service.serviceName,
registeredAt: service.registeredAt,
registeredAt_unix: service.registeredAt_unix,
planDelay: service.planDelay,
planDelay_unix: service.planDelay_unix,
planExpAt: service.planExpAt,
planExpAt_unix: service.planExpAt_unix,
planUpdatedAt: service.planUpdatedAt,
planUpdatedAt_unix: service.planUpdatedAt_unix,
active: service.active,
}));
// Update member document
const doc = await memberModel.findByIdAndUpdate(_id, {
bodyState, bodyState,
services, services: plainServices,
payMonth, payMonth,
planUpdatedAt: current_date.toUTCString(), planUpdatedAt: current_date.toUTCString(),
planUpdatedAt_unix: current_date_unix, planUpdatedAt_unix: current_date_unix,
planDelay, planDelay: globalPlanDelay,
planDelay_unix: planDelay_unix, planDelay_unix: globalPlanDelay * 2592000,
planExpAt: planExpAt.toUTCString(), planExpAt: new Date(latestExpiration * 1000).toUTCString(),
planExpAt_unix: current_date_unix + planDelay_unix, planExpAt_unix: latestExpiration,
active: true, active: globalActive,
} , { }, {
new: true new: true
}) });
// add the subscription income
let income = payMonth * planDelay // Add income entry if there's any charge
await statisticsModel.findOneAndUpdate({} , { if (totalIncome > 0) {
$inc: { await statisticsModel.findOneAndUpdate({}, {
totalIncome: income $inc: {
} totalIncome: totalIncome
}) }
});
const member = await memberModel.findById(_id, 'firstName lastName');
const incomeEntry = new incomeModel({
name: `Subscription Update - ${member?.firstName} ${member?.lastName}`,
description: `Services updated: ${incomeServiceNames.join(', ')}`,
amount: totalIncome,
addedAt: current_date_unix
});
await incomeEntry.save();
}
// return the success response // return the success response
return NextResponse.json({ return NextResponse.json({
success: true, success: true,

View File

@ -65,7 +65,7 @@ export async function GET(req:Request)
{ {
// get the page // get the page
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0', const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// get the docs // get the docs
const docs = await productsModel.find({}).skip(range[0]).limit(range[1]), const docs = await productsModel.find({}).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
@ -131,7 +131,7 @@ export async function DELETE(req:Request)
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const page = searchParams.get('page'), const page = searchParams.get('page'),
_id : string | null = searchParams.get('_id'), _id : string | null = searchParams.get('_id'),
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// delete the doc // delete the doc
await productsModel.findByIdAndRemove(_id) await productsModel.findByIdAndRemove(_id)
// get the docs by page // get the docs by page

View File

@ -0,0 +1,105 @@
import dbConnect from "@/database/dbConnect";
import { NextResponse } from "next/server";
import memberModel from "@/database/models/memberModel";
import serviceModel from "@/database/models/serviceModel";
import { Types } from 'mongoose';
// set the revalidate variable
export const revalidate = 5;
/**
* GET METHOD - Fetch services with their active subscription counts
* This endpoint calculates real-time active subscriptions for each service
* by counting members who have active subscriptions to each service
*/
export async function GET(req: Request) {
try {
await dbConnect();
const currentUnix = Math.floor(new Date().getTime() / 1000);
// Get all services
const services = await serviceModel.find({}, { _id: 1, name: 1, status: 1 });
// Calculate active subscriptions for each service
const servicesWithSubscriptions = await Promise.all(
services.map(async (service) => {
// Count members with active subscriptions to this specific service
const activeSubscriptionsCount = await memberModel.countDocuments({
'services': {
$elemMatch: {
'serviceID': service._id.toString(),
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
});
// Count total subscriptions (including expired) for this service
const totalSubscriptionsCount = await memberModel.countDocuments({
'services': {
$elemMatch: {
'serviceID': service._id.toString()
}
}
});
return {
_id: service._id,
name: service.name,
status: service.status,
activeSubscriptions: activeSubscriptionsCount,
totalSubscriptions: totalSubscriptionsCount,
expiredSubscriptions: totalSubscriptionsCount - activeSubscriptionsCount
};
})
);
// Calculate summary statistics
const totalActiveSubscriptions = servicesWithSubscriptions.reduce(
(sum, service) => sum + service.activeSubscriptions, 0
);
const totalExpiredSubscriptions = servicesWithSubscriptions.reduce(
(sum, service) => sum + service.expiredSubscriptions, 0
);
const totalSubscriptions = servicesWithSubscriptions.reduce(
(sum, service) => sum + service.totalSubscriptions, 0
);
// Prepare response data
const responseData = {
services: servicesWithSubscriptions,
summary: {
totalServices: services.length,
activeServices: services.filter(s => s.status === 'active').length,
inactiveServices: services.filter(s => s.status === 'inactive').length,
totalActiveSubscriptions,
totalExpiredSubscriptions,
totalSubscriptions
}
};
return NextResponse.json({
success: true,
message: "Services subscriptions data retrieved successfully",
data: responseData,
}, {
status: 200,
headers: {
"content-type": "application/json"
}
});
} catch (error) {
console.error("Error fetching services subscriptions data:", error);
return NextResponse.json({
success: false,
message: "Server error",
}, {
status: 500,
headers: {
"content-type": "application/json"
}
});
}
}

View File

@ -64,9 +64,10 @@ export async function GET(req:Request)
{ {
// get the page // get the page
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0', const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// get the docs // get the docs
const docs = await serviceModel.find({}).skip(range[0]).limit(range[1]), const docs = await serviceModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
docs_count = await serviceModel.countDocuments({}); docs_count = await serviceModel.countDocuments({});
// prepare the return data // prepare the return data
@ -91,7 +92,7 @@ export async function GET(req:Request)
{ name: { $regex: searchKeyword, $options: 'i' } }, { name: { $regex: searchKeyword, $options: 'i' } },
{ description: { $regex: searchKeyword, $options: 'i' } } { description: { $regex: searchKeyword, $options: 'i' } }
] ]
}) }).sort({ addedAt: -1 })
// get the docs // get the docs
let responseData = { let responseData = {
docs: results, docs: results,
@ -130,11 +131,11 @@ export async function DELETE(req:Request)
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const page = searchParams.get('page'), const page = searchParams.get('page'),
_id : string | null = searchParams.get('_id'), _id : string | null = searchParams.get('_id'),
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4]; range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
// delete the doc // delete the doc
await serviceModel.findByIdAndRemove(_id) await serviceModel.findByIdAndRemove(_id)
// get the docs by page // get the docs by page
const docs = await serviceModel.find({}).skip(range[0]).limit(range[1]), const docs = await serviceModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
// get the size of the docs // get the size of the docs
docs_count = await serviceModel.countDocuments({}); docs_count = await serviceModel.countDocuments({});
// prepare the return data // prepare the return data

View File

@ -58,15 +58,34 @@ async function updateMembersOverviewStatistics() {
registerAt_unix: { $lt: endOfDayUnix } registerAt_unix: { $lt: endOfDayUnix }
}); });
const totalActiveSubs = await memberModel.countDocuments({ // Count members with at least one active service subscription
planExpAt_unix: { $gt: currentUnix }, const totalActiveSubs = await memberModel.countDocuments({
registerAt_unix: { $lt: endOfDayUnix } registerAt_unix: { $lt: endOfDayUnix },
}); 'services': {
$elemMatch: {
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
});
const totalUnActiveSubs = await memberModel.countDocuments({ // Count members with no active service subscriptions
planExpAt_unix: { $lte: currentUnix }, const totalUnActiveSubs = await memberModel.countDocuments({
registerAt_unix: { $lt: endOfDayUnix } registerAt_unix: { $lt: endOfDayUnix },
}); $or: [
{ 'services': { $size: 0 } },
{
'services': {
$not: {
$elemMatch: {
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
}
}
]
});
const totalMansMembers = await memberModel.countDocuments({ const totalMansMembers = await memberModel.countDocuments({
gendre: 'm', gendre: 'm',
@ -135,13 +154,33 @@ async function updateMembersOverviewStatistics() {
const totalMembers = await memberModel.countDocuments({ const totalMembers = await memberModel.countDocuments({
registerAt_unix: { $lt: endOfDayUnix } registerAt_unix: { $lt: endOfDayUnix }
}); });
// Count members with at least one active service subscription
const totalActiveSubs = await memberModel.countDocuments({ const totalActiveSubs = await memberModel.countDocuments({
planExpAt_unix: { $gt: currentUnix }, registerAt_unix: { $lt: endOfDayUnix },
registerAt_unix: { $lt: endOfDayUnix } 'services': {
$elemMatch: {
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
}); });
// Count members with no active service subscriptions
const totalUnActiveSubs = await memberModel.countDocuments({ const totalUnActiveSubs = await memberModel.countDocuments({
planExpAt_unix: { $lte: currentUnix }, registerAt_unix: { $lt: endOfDayUnix },
registerAt_unix: { $lt: endOfDayUnix } $or: [
{ 'services': { $size: 0 } },
{
'services': {
$not: {
$elemMatch: {
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
}
}
]
}); });
const totalMansMembers = await memberModel.countDocuments({ const totalMansMembers = await memberModel.countDocuments({
gendre: 'm', gendre: 'm',
@ -236,13 +275,33 @@ async function updateMembersOverviewStatistics() {
const totalMembers = await memberModel.countDocuments({ const totalMembers = await memberModel.countDocuments({
registerAt_unix: { $lt: endOfDayUnix } registerAt_unix: { $lt: endOfDayUnix }
}); });
// Count members with at least one active service subscription
const totalActiveSubs = await memberModel.countDocuments({ const totalActiveSubs = await memberModel.countDocuments({
planExpAt_unix: { $gt: currentUnix }, registerAt_unix: { $lt: endOfDayUnix },
registerAt_unix: { $lt: endOfDayUnix } 'services': {
$elemMatch: {
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
}); });
// Count members with no active service subscriptions
const totalUnActiveSubs = await memberModel.countDocuments({ const totalUnActiveSubs = await memberModel.countDocuments({
planExpAt_unix: { $lte: currentUnix }, registerAt_unix: { $lt: endOfDayUnix },
registerAt_unix: { $lt: endOfDayUnix } $or: [
{ 'services': { $size: 0 } },
{
'services': {
$not: {
$elemMatch: {
'planExpAt_unix': { $gt: currentUnix },
'active': true
}
}
}
}
]
}); });
const totalMansMembers = await memberModel.countDocuments({ const totalMansMembers = await memberModel.countDocuments({
gendre: 'm', gendre: 'm',
@ -365,12 +424,48 @@ export async function GET(req:Request)
let membersCount : number = await memberModel.count({}) let membersCount : number = await memberModel.count({})
let activeMembersCount : number = await memberModel.count({active: true}) let activeMembersCount : number = await memberModel.count({active: true})
let disActiveMembersCount : number = await memberModel.count({active: false}) let disActiveMembersCount : number = await memberModel.count({active: false})
let activeSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$gt : Math.floor(new Date().getTime() / 1000)}}) // Count members with at least one active service subscription
let expiredSoonSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$gt : Math.floor(new Date().getTime() / 1000) + 259200}}) let activeSubscriptionsCount : number = await memberModel.count({
let expiredSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$lte : Math.floor(new Date().getTime() / 1000)}}) 'services': {
$elemMatch: {
'planExpAt_unix': { $gt: Math.floor(new Date().getTime() / 1000) },
'active': true
}
}
});
// Count members with subscriptions expiring soon (within 3 days)
let expiredSoonSubscriptionsCount : number = await memberModel.count({
'services': {
$elemMatch: {
'planExpAt_unix': {
$gt: Math.floor(new Date().getTime() / 1000),
$lte: Math.floor(new Date().getTime() / 1000) + 259200
},
'active': true
}
}
});
// Count members with no active service subscriptions
let expiredSubscriptionsCount : number = await memberModel.count({
$or: [
{ 'services': { $size: 0 } },
{
'services': {
$not: {
$elemMatch: {
'planExpAt_unix': { $gt: Math.floor(new Date().getTime() / 1000) },
'active': true
}
}
}
}
]
});
let servicesCount : number = await servicesModel.count({}) let servicesCount : number = await servicesModel.count({})
let activeServicesCount : number = await servicesModel.count({status: "active"}) let activeServicesCount : number = await servicesModel.count({status: "active"})
let disActiveServicesCount : number = await servicesModel.count({status: "active"}) let disActiveServicesCount : number = await servicesModel.count({status: "inactive"})
let servicesNameAndSubscribers : let servicesNameAndSubscribers :
{ {
_id: Types.ObjectId, _id: Types.ObjectId,

View File

@ -36,12 +36,12 @@ export default function List()
let dateObj = new Date(unix_time * 1000) let dateObj = new Date(unix_time * 1000)
// we got the year // we got the year
let year = dateObj.getUTCFullYear(), let year = dateObj.getUTCFullYear(),
// get month // get month (add 1 because getUTCMonth returns 0-11)
month = dateObj.getUTCMonth(), month = dateObj.getUTCMonth() + 1,
// get day // get day of month
day = dateObj.getUTCDay(); day = dateObj.getUTCDate();
// now we format the string // now we format the string with zero padding
let formattedTime = year.toString()+ '-' + month.toString() + '-' + day.toString(); let formattedTime = year.toString()+ '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
return formattedTime; return formattedTime;
} }

View File

@ -2,65 +2,164 @@ import { useAppSelector } from '@/redux/store';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
import { useState, useEffect } from 'react';
const ReactApexChart = dynamic(() => import('react-apexcharts'), { const ReactApexChart = dynamic(() => import('react-apexcharts'), {
ssr: false, ssr: false,
}); });
interface AnalyticsData {
view: 'monthly' | 'yearly';
year?: number;
months?: Array<{
month: number;
monthName: string;
income: number;
expense: number;
net: number;
}>;
years?: Array<{
year: number;
income: number;
expense: number;
net: number;
}>;
totalIncome: number;
totalExpense: number;
totalNet: number;
}
export default function IncomeOutcome() export default function IncomeOutcome()
{ {
// get needed redux state // get needed redux state
const report = useAppSelector((state) => state.statisticsReducer.value.report) const report = useAppSelector((state) => state.statisticsReducer.value.report)
const currencySymbol = useAppSelector((state) => state.settingsReducer.value.appGeneralSettings.currencySymbol) ?? '' const currencySymbol = useAppSelector((state) => state.settingsReducer.value.appGeneralSettings.currencySymbol) ?? ''
const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType) const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType)
// declare global variables // declare global variables
const cookies = new Cookies(); const cookies = new Cookies();
const t = useTranslations('statistics'); const t = useTranslations('statistics');
const locale = cookies.get("NEXT_LOCALE") const locale = cookies.get("NEXT_LOCALE")
const isRtl = locale == 'ar' ? true : false const isRtl = locale == 'ar' ? true : false
const isDark = themeType == 'dark' ? true : false const isDark = themeType == 'dark' ? true : false
// state for analytics data and view mode
const [analyticsData, setAnalyticsData] = useState<AnalyticsData | null>(null);
const [viewMode, setViewMode] = useState<'monthly' | 'yearly'>('monthly');
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
const [loading, setLoading] = useState(true);
// fetch analytics data
useEffect(() => {
const fetchAnalyticsData = async () => {
try {
setLoading(true);
const response = await fetch(`/api/user/actions/income-expense-analytics?view=${viewMode}&year=${selectedYear}`);
const result = await response.json();
if (result.success) {
setAnalyticsData(result.data);
}
} catch (error) {
console.error('Error fetching analytics data:', error);
} finally {
setLoading(false);
}
};
fetchAnalyticsData();
}, [viewMode, selectedYear]);
// if loading show load screen // if loading show load screen
if(!report) return null // Return null or a loading indicator if report is not yet available if (loading || !analyticsData) {
// prepare chart data return (
// Ensure totalIncome and totalOutcome are numbers, default to 0 if undefined/null <div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]">
let data = [report?.totalIncome ?? 0 , report?.totalOutcome ?? 0] <h3 className="text-xl font-bold">{t('incomeOutcomeGeneralOverview')}</h3>
// prepare chart labels <div className="flex justify-center items-center h-64">
let labels = [t('income') , t('outcome')] <div className="text-lg">{t('loading')}</div>
</div>
</div>
);
}
// prepare chart data based on view mode
let data: number[] = [];
let labels: string[] = [];
let categories: string[] = [];
if (viewMode === 'monthly' && analyticsData.months) {
data = analyticsData.months.flatMap(month => [month.income, month.expense]);
labels = [t('income'), t('outcome')];
categories = analyticsData.months.flatMap(month => [month.monthName, month.monthName]);
} else if (viewMode === 'yearly' && analyticsData.years) {
data = analyticsData.years.flatMap(year => [year.income, year.expense]);
labels = [t('income'), t('outcome')];
categories = analyticsData.years.flatMap(year => [year.year.toString(), year.year.toString()]);
}
// prepare series data for grouped chart
const incomeData = viewMode === 'monthly'
? analyticsData.months?.map(month => month.income) || []
: analyticsData.years?.map(year => year.income) || [];
const expenseData = viewMode === 'monthly'
? analyticsData.months?.map(month => month.expense) || []
: analyticsData.years?.map(year => year.expense) || [];
const chartCategories = viewMode === 'monthly'
? analyticsData.months?.map(month => month.monthName) || []
: analyticsData.years?.map(year => year.year.toString()) || [];
// prepare chart options // prepare chart options
var options = { var options = {
series: [{ series: [
name: currencySymbol, {
data: data name: t('income'),
}], data: incomeData,
annotations: { color: '#0263FF'
points: [{ },
x: t('incomes'), {
seriesIndex: 0, name: t('outcome'),
label: { data: expenseData,
borderColor: '#775DD0', color: '#E3342F'
offsetY: 0,
style: {
color: '#fff',
background: '#775DD0',
},
} }
}] ],
},
chart: { chart: {
height: 350, height: 350,
type: 'bar', type: 'bar',
toolbar: {
show: true,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
}
}
}, },
plotOptions: { plotOptions: {
bar: { bar: {
borderRadius: 10, borderRadius: 4,
columnWidth: '50%', columnWidth: '60%',
horizontal: true, dataLabels: {
distributed: true, position: 'top'
}
}
},
dataLabels: {
enabled: true,
formatter: function (val: number) {
return currencySymbol + val.toLocaleString();
},
offsetY: -20,
style: {
fontSize: '12px',
colors: [isDark ? '#fff' : '#000']
} }
}, },
legend: { legend: {
position: 'top', position: 'top',
horizontalAlign: 'right', horizontalAlign: 'right',
fontSize: '16px', fontSize: '14px',
markers: { markers: {
width: 10, width: 10,
height: 10, height: 10,
@ -71,67 +170,140 @@ export default function IncomeOutcome()
vertical: 5, vertical: 5,
}, },
}, },
fill: { colors: ['#0263FF', '#E3342F'],
type: isDark ? '' : 'gradient',
gradient: isDark ? {} : {
shadeIntensity: 1,
inverseColors: !1,
opacityFrom: isDark ? 0.19 : 0.28,
opacityTo: 0.05,
stops: isDark ? [100, 100] : [45, 100],
},
},
colors: ['#0263FF' , '#E3342F'],
dataLabels: {
enabled: false
},
stroke: { stroke: {
width: 2, width: 2,
curve: 'straight', curve: 'straight',
}, },
grid: { grid: {
row: { borderColor: isDark ? '#333' : '#e0e0e0',
colors: [isDark ? '#333' : '#fff', '#f2f2f2'] // Adjust the grid row color for dark mode strokeDashArray: 3,
},
}, },
xaxis: { xaxis: {
categories: chartCategories,
labels: { labels: {
rotate: -45, rotate: viewMode === 'monthly' ? -45 : 0,
offsetX: isRtl ? 2 : 0, offsetX: isRtl ? 2 : 0,
offsetY: 5, offsetY: 5,
style: { style: {
colors: isDark ? '#fff' : '#000', colors: isDark ? '#fff' : '#000',
fontSize: '12px', fontSize: '12px',
cssClass: 'apexcharts-xaxis-title',
}, },
}, },
categories: labels, axisBorder: {
tickPlacement: 'on' color: isDark ? '#333' : '#e0e0e0'
},
axisTicks: {
color: isDark ? '#333' : '#e0e0e0'
}
}, },
yaxis: { yaxis: {
tickAmount: 7,
labels: { labels: {
offsetX: isRtl ? -30 : -10, offsetX: isRtl ? -10 : -10,
offsetY: 0, offsetY: 0,
style: { style: {
colors: isDark ? '#fff' : '#000', colors: isDark ? '#fff' : '#000',
fontSize: '12px', fontSize: '12px',
cssClass: 'apexcharts-yaxis-title',
}, },
formatter: function (val: number) {
return currencySymbol + val.toLocaleString();
}
}, },
}, },
tooltip: { tooltip: {
theme: isDark ? 'dark' : 'light', theme: isDark ? 'dark' : 'light',
x: { shared: true,
show: true, intersect: false,
}, y: {
formatter: function (val: number) {
return currencySymbol + val.toLocaleString();
}
}
}, },
}; };
// return the ui // return the ui
return ( return (
<> <>
<div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]"> <div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]">
<h3 className="text-xl font-bold">{t('incomeOutcomeGeneralOverview')}</h3> {/* Header with controls */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 className="text-xl font-bold">{t('incomeOutcomeGeneralOverview')}</h3>
<div className="flex flex-col sm:flex-row gap-3">
{/* View Mode Toggle */}
<div className="flex bg-secondary-light dark:bg-secondary-dark rounded-lg p-1">
<button
onClick={() => setViewMode('monthly')}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
viewMode === 'monthly'
? 'bg-primary dark:bg-primary-dark text-text dark:text-text-dark shadow-sm'
: 'text-text-secondary dark:text-text-secondary-dark hover:text-text dark:hover:text-text-dark'
}`}
>
{t('monthly')}
</button>
<button
onClick={() => setViewMode('yearly')}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
viewMode === 'yearly'
? 'bg-primary dark:bg-primary-dark text-text dark:text-text-dark shadow-sm'
: 'text-text-secondary dark:text-text-secondary-dark hover:text-text dark:hover:text-text-dark'
}`}
>
{t('yearly')}
</button>
</div>
{/* Year Selector for Monthly View */}
{viewMode === 'monthly' && (
<select
value={selectedYear}
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
className="px-3 py-1 rounded-md border border-secondary-light dark:border-secondary-dark bg-primary dark:bg-primary-dark text-text dark:text-text-dark text-sm"
>
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map(year => (
<option key={year} value={year}>{year}</option>
))}
</select>
)}
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="text-sm text-blue-600 dark:text-blue-400 font-medium">{t('totalIncome')}</div>
<div className="text-2xl font-bold text-blue-700 dark:text-blue-300">
{currencySymbol}{analyticsData.totalIncome.toLocaleString()}
</div>
</div>
<div className="bg-red-50 dark:bg-red-900/20 p-4 rounded-lg border border-red-200 dark:border-red-800">
<div className="text-sm text-red-600 dark:text-red-400 font-medium">{t('totalExpense')}</div>
<div className="text-2xl font-bold text-red-700 dark:text-red-300">
{currencySymbol}{analyticsData.totalExpense.toLocaleString()}
</div>
</div>
<div className={`p-4 rounded-lg border ${
analyticsData.totalNet >= 0
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
}`}>
<div className={`text-sm font-medium ${
analyticsData.totalNet >= 0
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>{t('netProfit')}</div>
<div className={`text-2xl font-bold ${
analyticsData.totalNet >= 0
? 'text-green-700 dark:text-green-300'
: 'text-red-700 dark:text-red-300'
}`}>
{currencySymbol}{analyticsData.totalNet.toLocaleString()}
</div>
</div>
</div>
{/* Chart */}
<ReactApexChart series={options.series} options={options} type="bar" height={350} width={'100%'} /> <ReactApexChart series={options.series} options={options} type="bar" height={350} width={'100%'} />
</div> </div>
</> </>

View File

@ -1,147 +1,317 @@
import { useAppSelector } from '@/redux/store'; import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
import { useAppSelector } from '@/redux/store';
const ReactApexChart = dynamic(() => import('react-apexcharts'), { const ReactApexChart = dynamic(() => import('react-apexcharts'), {
ssr: false, ssr: false,
}); });
export default function ServicesSubscriptions() interface ServiceSubscriptionData {
{ _id: string;
// get redux needed state name: string;
const report = useAppSelector((state) => state.statisticsReducer.value.report) status: string;
const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType) activeSubscriptions: number;
// declare global variables totalSubscriptions: number;
expiredSubscriptions: number;
}
interface ServicesSubscriptionsResponse {
services: ServiceSubscriptionData[];
summary: {
totalServices: number;
activeServices: number;
inactiveServices: number;
totalActiveSubscriptions: number;
totalExpiredSubscriptions: number;
totalSubscriptions: number;
};
}
export default function ServicesSubscriptions() {
const [servicesData, setServicesData] = useState<ServicesSubscriptionsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Get theme state from Redux
const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType);
// Declare global variables
const cookies = new Cookies(); const cookies = new Cookies();
const t = useTranslations('statistics'); const t = useTranslations('statistics');
const locale = cookies.get("NEXT_LOCALE") const locale = cookies.get("NEXT_LOCALE");
const isRtl = locale == 'ar' ? true : false const isRtl = locale === 'ar';
const isDark = themeType == 'dark' ? true : false const isDark = themeType === 'dark';
// if loading show load screen
if(!report) return // Fetch services subscriptions data
// setup chart data useEffect(() => {
let data = report?.servicesNameAndSubscribers.value.map((v : { const fetchServicesData = async () => {
_id: string, try {
name: string, setLoading(true);
totalSubscribers: number, const response = await fetch('/api/user/actions/services-subscriptions');
} , i : number) => { const result = await response.json();
return v?.totalSubscribers;
}) if (result.success) {
// setup chart labels setServicesData(result.data);
let labels = report?.servicesNameAndSubscribers.value.map((v : { setError(null);
_id: string, } else {
name: string, setError(result.message || 'Failed to fetch data');
totalSubscribers: number, }
} , i : number) => { } catch (err) {
return v?.name; setError('Network error occurred');
}) console.error('Error fetching services data:', err);
// setup chart options } finally {
var options = { setLoading(false);
series: [{
name: t('subscription'),
data: data
}],
annotations: {
points: [{
x: t('services'),
seriesIndex: 0,
label: {
borderColor: '#775DD0',
offsetY: 0,
style: {
color: '#fff',
background: '#775DD0',
},
} }
}] };
},
fetchServicesData();
}, []);
// Show loading state
if (loading) {
return (
<div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 lg:w-1/2 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]">
<h3 className="text-xl font-bold">{t('sevicesGeneralOverview')}</h3>
<div className="flex items-center justify-center h-[350px]">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p>{t('loading')}</p>
</div>
</div>
</div>
);
}
// Show error state
if (error || !servicesData) {
return (
<div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 lg:w-1/2 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]">
<h3 className="text-xl font-bold">{t('sevicesGeneralOverview')}</h3>
<div className="flex items-center justify-center h-[350px]">
<div className="text-center text-red-500">
<p>{error || 'No data available'}</p>
</div>
</div>
</div>
);
}
// Prepare chart data
const activeData = servicesData.services.map(service => service.activeSubscriptions);
const expiredData = servicesData.services.map(service => service.expiredSubscriptions);
const labels = servicesData.services.map(service => service.name);
// Setup chart options
const options = {
series: [
{
name: t('activeSubscriptions'),
data: activeData
},
{
name: t('expiredSubscriptions'),
data: expiredData
}
],
chart: { chart: {
height: 350, height: 350,
type: 'bar', type: 'bar' as const,
stacked: true,
toolbar: {
show: true,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
}
}
}, },
plotOptions: { plotOptions: {
bar: { bar: {
borderRadius: 10, borderRadius: 8,
columnWidth: '50%', columnWidth: '60%',
distributed: true, horizontal: false,
} }
}, },
fill: { fill: {
type: isDark ? '' : 'gradient', type: isDark ? 'solid' : 'gradient',
gradient: isDark ? {} : { gradient: isDark ? {} : {
shadeIntensity: 1, shadeIntensity: 1,
inverseColors: !1, inverseColors: false,
opacityFrom: isDark ? 0.19 : 0.28, opacityFrom: 0.85,
opacityTo: 0.05, opacityTo: 0.55,
stops: isDark ? [100, 100] : [45, 100], stops: [0, 100],
}, },
}, },
legend: { legend: {
position: 'top', position: 'top' as const,
horizontalAlign: 'right', horizontalAlign: 'right' as const,
fontSize: '16px', fontSize: '14px',
markers: { markers: {
width: 10, width: 12,
height: 10, height: 12,
offsetX: isRtl ? 5 : -5, offsetX: isRtl ? 5 : -5,
}, },
itemMargin: { itemMargin: {
horizontal: 10, horizontal: 10,
vertical: 5, vertical: 5,
}, },
labels: {
colors: isDark ? '#fff' : '#000',
}
}, },
colors: ['#38C172', '#38C172' , '#E3342F' , '#0263FF', '#FF30F7'], colors: ['#10B981', '#EF4444'], // Green for active, Red for expired
dataLabels: { dataLabels: {
enabled: false enabled: true,
style: {
colors: ['#fff']
},
formatter: function(val: number) {
return val > 0 ? val.toString() : '';
}
}, },
stroke: { stroke: {
width: 2, width: 1,
curve: 'straight', colors: ['transparent']
}, },
grid: { grid: {
row: { borderColor: isDark ? '#374151' : '#E5E7EB',
colors: [isDark ? '#333' : '#fff', '#f2f2f2'] // Adjust the grid row color for dark mode strokeDashArray: 3,
},
}, },
xaxis: { xaxis: {
categories: labels,
labels: { labels: {
rotate: -45, rotate: -45,
offsetX: isRtl ? 2 : 0, offsetX: isRtl ? 2 : 0,
offsetY: 5, offsetY: 5,
style: { style: {
colors: isDark ? '#fff' : '#000', colors: isDark ? '#D1D5DB' : '#374151',
fontSize: '12px', fontSize: '12px',
cssClass: 'apexcharts-xaxis-title',
}, },
maxHeight: 80,
}, },
categories: labels, axisBorder: {
color: isDark ? '#374151' : '#E5E7EB',
},
axisTicks: {
color: isDark ? '#374151' : '#E5E7EB',
}
}, },
yaxis: { yaxis: {
tickAmount: 7,
labels: { labels: {
offsetX: isRtl ? -30 : -10, offsetX: isRtl ? -10 : -5,
offsetY: 0,
style: { style: {
colors: isDark ? '#fff' : '#000', colors: isDark ? '#D1D5DB' : '#374151',
fontSize: '12px', fontSize: '12px',
cssClass: 'apexcharts-yaxis-title',
}, },
}, },
title: {
text: t('subscriptionsCount'),
style: {
color: isDark ? '#D1D5DB' : '#374151',
fontSize: '14px',
fontWeight: 600,
}
}
}, },
tooltip: { tooltip: {
theme: isDark ? 'dark' : 'light', theme: isDark ? 'dark' : 'light',
x: { shared: true,
show: true, intersect: false,
}, y: {
formatter: function(val: number, opts: any) {
const seriesName = opts.seriesIndex === 0 ?
t('activeSubscriptions') :
t('expiredSubscriptions');
return `${seriesName}: ${val}`;
}
}
}, },
responsive: [
{
breakpoint: 768,
options: {
chart: {
height: 300
},
plotOptions: {
bar: {
columnWidth: '80%'
}
}
}
}
]
}; };
// return the ui
return ( return (
<> <div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 lg:w-1/2 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]">
<div className="flex flex-col gap-5 [&_*]:dark:!text-text-dark p-5 lg:w-1/2 w-full shadow border border-secondary-light bg-primary dark:bg-primary-dark rounded-[5px]"> {/* Header */}
<div className="flex flex-col gap-2">
<h3 className="text-xl font-bold">{t('sevicesGeneralOverview')}</h3> <h3 className="text-xl font-bold">{t('sevicesGeneralOverview')}</h3>
<ReactApexChart series={options.series} options={options} type="bar" height={350} width={'100%'} />
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
<p className="text-blue-600 dark:text-blue-400 font-medium">
{t('totalServices')}
</p>
<p className="text-xl font-bold text-blue-700 dark:text-blue-300">
{servicesData.summary.totalServices}
</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg">
<p className="text-green-600 dark:text-green-400 font-medium">
{t('activeSubscriptions')}
</p>
<p className="text-xl font-bold text-green-700 dark:text-green-300">
{servicesData.summary.totalActiveSubscriptions}
</p>
</div>
<div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg">
<p className="text-red-600 dark:text-red-400 font-medium">
{t('expiredSubscriptions')}
</p>
<p className="text-xl font-bold text-red-700 dark:text-red-300">
{servicesData.summary.totalExpiredSubscriptions}
</p>
</div>
<div className="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-lg">
<p className="text-purple-600 dark:text-purple-400 font-medium">
{t('totalSubscriptions')}
</p>
<p className="text-xl font-bold text-purple-700 dark:text-purple-300">
{servicesData.summary.totalSubscriptions}
</p>
</div>
</div>
</div> </div>
</>
) {/* Chart */}
<div className="w-full">
{servicesData.services.length > 0 ? (
<ReactApexChart
series={options.series}
options={options}
type="bar"
height={350}
width={'100%'}
/>
) : (
<div className="flex items-center justify-center h-[350px] text-gray-500 dark:text-gray-400">
<p>{t('noServicesFound')}</p>
</div>
)}
</div>
</div>
);
} }

View File

@ -36,12 +36,12 @@ export default function List()
let dateObj = new Date(unix_time * 1000) let dateObj = new Date(unix_time * 1000)
// we got the year // we got the year
let year = dateObj.getUTCFullYear(), let year = dateObj.getUTCFullYear(),
// get month // get month (add 1 because getUTCMonth returns 0-11)
month = dateObj.getUTCMonth(), month = dateObj.getUTCMonth() + 1,
// get day // get day of month
day = dateObj.getUTCDay(); day = dateObj.getUTCDate();
// now we format the string // now we format the string with zero padding
let formattedTime = year.toString()+ '-' + month.toString() + '-' + day.toString(); let formattedTime = year.toString()+ '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
return formattedTime; return formattedTime;
} }

View File

@ -22,8 +22,8 @@ export default function AddNewButton()
flex justify-between items-center gap-3 flex justify-between items-center gap-3
"> ">
<svg width="18" height="18" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clip-rule="evenodd" d="M25 0.5C26.3807 0.5 27.5 1.61929 27.5 3L27.5 46C27.5 47.3807 26.3807 48.5 25 48.5C23.6193 48.5 22.5 47.3807 22.5 46L22.5 3C22.5 1.61929 23.6193 0.5 25 0.5Z" fill="currentColor"/> <path fillRule="evenodd" clipRule="evenodd" d="M25 0.5C26.3807 0.5 27.5 1.61929 27.5 3L27.5 46C27.5 47.3807 26.3807 48.5 25 48.5C23.6193 48.5 22.5 47.3807 22.5 46L22.5 3C22.5 1.61929 23.6193 0.5 25 0.5Z" fill="currentColor"/>
<path fillRule="evenodd" clip-rule="evenodd" d="M48.5 25C48.5 26.3807 47.3807 27.5 46 27.5L3 27.5C1.61929 27.5 0.5 26.3807 0.5 25C0.5 23.6193 1.61929 22.5 3 22.5L46 22.5C47.3807 22.5 48.5 23.6193 48.5 25Z" fill="currentColor"/> <path fillRule="evenodd" clipRule="evenodd" d="M48.5 25C48.5 26.3807 47.3807 27.5 46 27.5L3 27.5C1.61929 27.5 0.5 26.3807 0.5 25C0.5 23.6193 1.61929 22.5 3 22.5L46 22.5C47.3807 22.5 48.5 23.6193 48.5 25Z" fill="currentColor"/>
</svg> </svg>
<h3>{t('addNewIncome')}</h3> <h3>{t('addNewIncome')}</h3>
</button> </button>

View File

@ -59,8 +59,8 @@ export default function List()
<span className="w-full text-start">{t('firstName')}</span> <span className="w-full text-start">{t('firstName')}</span>
<span className="w-full text-start">{t('lastName')}</span> <span className="w-full text-start">{t('lastName')}</span>
<span className="w-full text-start">{t('gendre')}</span> <span className="w-full text-start">{t('gendre')}</span>
<span className="w-full text-start">{t('payMonth')}</span> <span className="w-full text-start">{t('phone')}</span>
<span className="w-full text-start">{t('planDelay')}</span> <span className="w-full text-start">{t('activeServices')}</span>
<span className="w-full text-start">{t('planStatus')}</span> <span className="w-full text-start">{t('planStatus')}</span>
<span className="w-full text-start">{t('actions')}</span> <span className="w-full text-start">{t('actions')}</span>
</header> </header>
@ -122,34 +122,52 @@ export default function List()
<span className="w-full text-start whitespace-nowrap text-ellipsis overflow-hidden">{v.firstName}</span> <span className="w-full text-start whitespace-nowrap text-ellipsis overflow-hidden">{v.firstName}</span>
<span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{v.lastName}</span> <span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{v.lastName}</span>
<span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{t(v.gendre+'Gendre')}</span> <span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{t(v.gendre+'Gendre')}</span>
<span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{v.payMonth} {appGeneralSettings.currencySymbol}</span> <span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{v.phone}</span>
<span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{v.planDelay + ' ' + t('months')}</span> <span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden lg:block hidden">{v.services ? v.services.length : 0}</span>
<span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden"> <span className="w-full text-start flex gap-2 whitespace-nowrap text-ellipsis overflow-hidden">
{v.planExpAt_unix - (Date.now() / 1000) > 259200 ? {(() => {
<span className="flex gap-1 text-success"> // Calculate overall subscription status based on individual services
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg"> const currentTime = Date.now() / 1000;
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/> const activeServices = v.services?.filter(service =>
</svg> service.active && service.planExpAt_unix > currentTime
{t('trueActive')} ) || [];
</span>
: if (activeServices.length === 0) {
( return (
v.planExpAt_unix - (Date.now() / 1000) > 0 ? <span className="flex gap-1 text-error">
<span className="flex gap-1 text-warning"> <svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/>
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/> </svg>
</svg> {t('falseActive')}
{t('expireVerySoon')} </span>
</span> );
: }
<span className="flex gap-1 text-error">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg"> // Check if any service expires soon (within 3 days)
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/> const expiringSoon = activeServices.some(service =>
</svg> service.planExpAt_unix - currentTime <= 259200
{t('falseActive')} );
</span>
) if (expiringSoon) {
} return (
<span className="flex gap-1 text-warning">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/>
</svg>
{t('expireVerySoon')}
</span>
);
}
return (
<span className="flex gap-1 text-success">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/>
</svg>
{t('trueActive')}
</span>
);
})()}
</span> </span>
<span className="lg:w-full w-[100px] h-full text-start flex justify-start items-center gap-3"> <span className="lg:w-full w-[100px] h-full text-start flex justify-start items-center gap-3">
<button onClick={async() => { <button onClick={async() => {

View File

@ -16,9 +16,9 @@ import CircularProgress from '@mui/material/CircularProgress';
import { FormikWizard } from 'formik-wizard-form'; import { FormikWizard } from 'formik-wizard-form';
import * as Yup from "yup"; import * as Yup from "yup";
import Select from 'react-select' import Select from 'react-select'
import AsyncSelect from 'react-select/async';
import { components } from 'react-select'; import { components } from 'react-select';
import searchForServices from "@/functions/requests/members/searchForServices"; import searchForServices from "@/functions/requests/members/searchForServices";
import loadAllServices from "@/functions/requests/members/loadAllServices";
export default function AddPopUp() export default function AddPopUp()
{ {
@ -49,7 +49,7 @@ export default function AddPopUp()
email: string | null, email: string | null,
phone: string | null, phone: string | null,
address: string | null, address: string | null,
services: [string] | [], services: string[],
planDelay: string | null, planDelay: string | null,
gendre: string | null, // m or w gendre: string | null, // m or w
startBodyForm: string | null, startBodyForm: string | null,
@ -225,15 +225,22 @@ export default function AddPopUp()
isSubmitting, isSubmitting,
setFieldValue setFieldValue
} : any) => { } : any) => {
async function loadServices(s : string) { const [servicesOptions, setServicesOptions] = useState([]);
let docs = await searchForServices(s)
let options = docs.map((v , i) => { useEffect(() => {
return {value: v._id , label: v.name} async function loadServices() {
}) try {
let docs = await loadAllServices();
return options let options = docs.map((v, i) => {
} return {value: v._id, label: v.name}
});
setServicesOptions(options);
} catch (error) {
console.error('Error loading services:', error);
}
}
loadServices();
}, []);
// body types options // body types options
const bodyTypesOptions = [ const bodyTypesOptions = [
{ value: 'weak', label: t('weak') }, { value: 'weak', label: t('weak') },
@ -346,11 +353,9 @@ export default function AddPopUp()
<p className="!text-error text-sm">{errors.planDelay && touched.planDelay && t(errors.planDelay)}</p> <p className="!text-error text-sm">{errors.planDelay && touched.planDelay && t(errors.planDelay)}</p>
</div> </div>
<div className="flex flex-col lg:max-w-[350px] min-h-[150px] h-auto"> <div className="flex flex-col lg:max-w-[350px] min-h-[150px] h-auto">
<AsyncSelect <Select
cacheOptions options={servicesOptions}
loadOptions={loadServices}
components={{ NoOptionsMessage }} components={{ NoOptionsMessage }}
loadingMessage={() => t('search')}
isMulti isMulti
styles={customStyles} styles={customStyles}
placeholder={t("services")} placeholder={t("services")}

View File

@ -281,13 +281,96 @@ export default function ServiceDetailsPopUp()
</p> </p>
</div> </div>
{/* Services Subscription Section */}
<div className="w-full flex flex-col gap-3 mt-7 border-t pt-7">
<h3 className="font-bold">{t('memberServices')}</h3>
<div className="w-full flex flex-col gap-3">
{detailsPopUpData?.services && detailsPopUpData.services.length > 0 ? (
<div className="w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{detailsPopUpData.services.map((service: any, index: number) => {
const currentTime = Date.now() / 1000;
const isActive = service.planExpAt_unix > currentTime;
const isExpiringSoon = service.planExpAt_unix - currentTime <= 259200 && service.planExpAt_unix > currentTime;
return (
<div key={index} className="border border-secondary-light rounded-lg p-4 bg-primary-dark/5 dark:bg-primary/5">
<div className="flex flex-col gap-2">
<h4 className="font-semibold text-lg">{service.serviceName}</h4>
<div className="flex flex-col gap-1 text-sm">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">{t('planStatus')}:</span>
<span className="flex gap-1 items-center">
{isActive && !isExpiringSoon ?
<span className="flex gap-1 text-success">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/>
</svg>
{t('trueActive')}
</span>
:
(
isExpiringSoon ?
<span className="flex gap-1 text-warning">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/>
</svg>
{t('expireVerySoon')}
</span>
:
<span className="flex gap-1 text-error">
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.79895" width="3.95833" height="3.95833" rx="1" transform="rotate(-45 0 2.79895)" fill="currentColor"/>
</svg>
{t('falseActive')}
</span>
)
}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">{t('registeredAt')}:</span>
<span>{new Date(service.registeredAt).toLocaleDateString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">{t('planDelay')}:</span>
<span>{service.planDelay} {t('months')}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">{t('planExpAt')}:</span>
<span>{new Date(service.planExpAt).toLocaleDateString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">{t('planUpdatedAt')}:</span>
<span>{new Date(service.planUpdatedAt).toLocaleDateString()}</span>
</div>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>{t('note')}:</strong> {t('perServiceSubscriptionNote')}
</p>
</div>
</div>
) : (
<div className="p-4 text-center text-gray-500 dark:text-gray-400">
<p>{t('noServicesSubscribed')}</p>
</div>
)}
</div>
</div>
{/* QR Code Section */} {/* QR Code Section */}
<div className="w-full flex flex-col gap-3 mt-7 border-t pt-7"> <div className="w-full flex flex-col gap-3 mt-7 border-t pt-7">
<h3 className="font-bold">{t('memberQRCode') || 'Member QR Code'}</h3> <h3 className="font-bold">{t('memberQRCode')}</h3>
<div className="flex lg:flex-row flex-col lg:items-center gap-4"> <div className="flex lg:flex-row flex-col lg:items-center gap-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
{t('qrCodeDescription') || 'Scan this QR code to quickly access member information'} {t('qrCodeDescription')}
</p> </p>
<img <img
src={generateQRCode(detailsPopUpData?._id || '')} src={generateQRCode(detailsPopUpData?._id || '')}
@ -308,10 +391,10 @@ export default function ServiceDetailsPopUp()
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 16L7 11L8.4 9.6L11 12.2V4H13V12.2L15.6 9.6L17 11L12 16ZM6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6Z" fill="currentColor"/> <path d="M12 16L7 11L8.4 9.6L11 12.2V4H13V12.2L15.6 9.6L17 11L12 16ZM6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6Z" fill="currentColor"/>
</svg> </svg>
{t('downloadQR') || 'Download QR Code'} {t('downloadQR')}
</button> </button>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
{t('qrDownloadNote') || 'Downloads as PNG image'} {t('qrDownloadNote')}
</p> </p>
</div> </div>
</div> </div>

View File

@ -85,7 +85,7 @@ export default function UpdatePopUp()
lastName : updatePopUpData?.lastName ? updatePopUpData?.lastName : '' , lastName : updatePopUpData?.lastName ? updatePopUpData?.lastName : '' ,
email : updatePopUpData?.email ? updatePopUpData?.email : '' , email : updatePopUpData?.email ? updatePopUpData?.email : '' ,
phone : updatePopUpData?.phone ? updatePopUpData?.phone : '' , phone : updatePopUpData?.phone ? updatePopUpData?.phone : '' ,
//gendre: updatePopUpData?.gendre ? updatePopUpData?.gendre : 'm' , gendre: updatePopUpData?.gendre ? updatePopUpData?.gendre : 'm' ,
address : updatePopUpData?.address ? updatePopUpData?.address : '' , address : updatePopUpData?.address ? updatePopUpData?.address : '' ,
active: updatePopUpData?.active, active: updatePopUpData?.active,
//payMonth: updatePopUpData?.payMonth ? updatePopUpData?.payMonth : 0, //payMonth: updatePopUpData?.payMonth ? updatePopUpData?.payMonth : 0,
@ -163,6 +163,41 @@ export default function UpdatePopUp()
placeholder={t('lastName')} placeholder={t('lastName')}
/> />
</div> </div>
<div>
<h4 className="font-bold mb-3">{t('gender')}</h4>
<div className="flex gap-6">
<div className="flex items-center gap-2">
<input
id="gender-male"
type="radio"
name="gendre"
value="m"
checked={values.gendre === 'm'}
onChange={handleChange}
onBlur={handleBlur}
className="form-radio text-success-light focus:ring-success-light"
/>
<label htmlFor="gender-male" className="cursor-pointer">
{t('male')}
</label>
</div>
<div className="flex items-center gap-2">
<input
id="gender-female"
type="radio"
name="gendre"
value="w"
checked={values.gendre === 'w'}
onChange={handleChange}
onBlur={handleBlur}
className="form-radio text-success-light focus:ring-success-light"
/>
<label htmlFor="gender-female" className="cursor-pointer">
{t('female')}
</label>
</div>
</div>
</div>
<h3 className="font-bold">{t('status')}</h3> <h3 className="font-bold">{t('status')}</h3>
<div className="w-full flex justify-start items-center gap-5"> <div className="w-full flex justify-start items-center gap-5">
<input <input

View File

@ -1,4 +1,4 @@
import { forwardRef , ReactElement , Ref, useEffect } from "react" import { forwardRef , ReactElement , Ref, useEffect, useState } from "react"
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
@ -15,18 +15,20 @@ import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Select from 'react-select' import Select from 'react-select'
import { search as searchForServices , load as loadServices } from '@/redux/features/services-slice'
import { components } from 'react-select'; import { components } from 'react-select';
import AsyncSelect from 'react-select/async'; import loadAllServices from "@/functions/requests/members/loadAllServices";
import { useRouter } from 'next/navigation';
export default function UpdateSubPopUp() export default function UpdateSubPopUp()
{ {
const router = useRouter();
// declare the needed variables // declare the needed variables
// redux // redux
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
// intl-18 // intl-18
const t = useTranslations('members'); const t = useTranslations('members');
// mui // mui
const theme = useTheme(); const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
// get state from redux // get state from redux
@ -48,6 +50,7 @@ export default function UpdateSubPopUp()
status: false, status: false,
data: null data: null
})); }));
router.refresh();
}; };
// setup multi select // setup multi select
// no options react select component // no options react select component
@ -126,25 +129,28 @@ export default function UpdateSubPopUp()
}), }),
}; };
function ServicesMultiSelect ({setFieldValue , errors , touched} : {setFieldValue : any , errors : any , touched : any}) { function ServicesMultiSelect ({setFieldValue , errors , touched} : {setFieldValue : any , errors : any , touched : any}) {
// load services const [servicesOptions, setServicesOptions] = useState([]);
const listContent = useAppSelector((state) => state.servicesReducer.value.listContent)
async function loadServices (searchKeyword : string) { useEffect(() => {
if(searchKeyword) async function loadServices() {
{ try {
await dispatch(searchForServices({searchKeyword: searchKeyword})) let docs = await loadAllServices();
let options = docs.map((v, i) => {
return {value: v._id, label: v.name}
});
setServicesOptions(options);
} catch (error) {
console.error('Error loading services:', error);
}
} }
let options = listContent.map((v , i) => { loadServices();
return {value: v._id , label: v.name} }, []);
})
return options
}
return ( return (
<div className="flex flex-col max-w-[350px] min-h-[150px] h-auto"> <div className="flex flex-col max-w-[350px] min-h-[150px] h-auto">
<AsyncSelect <Select
cacheOptions options={servicesOptions}
loadOptions={loadServices}
components={{ NoOptionsMessage }} components={{ NoOptionsMessage }}
loadingMessage={() => t('search')}
isMulti isMulti
styles={customStyles} styles={customStyles}
placeholder={t("services")} placeholder={t("services")}
@ -218,6 +224,7 @@ export default function UpdateSubPopUp()
await dispatch(updateSub(values)) await dispatch(updateSub(values))
// close the popup // close the popup
handleClose(); handleClose();
}} }}
> >
{({ {({
@ -245,6 +252,67 @@ export default function UpdateSubPopUp()
<div> <h1 className="text-[18px] font-tajawal">{t('memberSubscriptionRenewalText1')}</h1> </div> <div> <h1 className="text-[18px] font-tajawal">{t('memberSubscriptionRenewalText1')}</h1> </div>
</DialogContentText> </DialogContentText>
{/* Current Services Subscription Status */}
{updateSubPopUpData && updateSubPopUpData.services && (
<div className="mb-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
<h3 className="font-bold mb-3 text-lg">{t('currentServicesStatus')}</h3>
{updateSubPopUpData.services.length > 0 ? (
<div className="space-y-4">
{updateSubPopUpData.services.map((service, index) => {
const currentTime = Math.floor(Date.now() / 1000);
const isActive = service.planExpAt_unix && service.planExpAt_unix > currentTime;
return (
<div key={index} className="p-3 border border-gray-300 dark:border-gray-600 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<p className="text-sm font-medium">{t('serviceName')}:</p>
<p className="text-sm font-bold">{service.serviceName}</p>
</div>
<div>
<p className="text-sm font-medium">{t('subscriptionStatus')}:</p>
<p className="text-sm">
{isActive
? <span className="text-green-600 font-medium">{t('active')}</span>
: <span className="text-red-600 font-medium">{t('expired')}</span>
}
</p>
</div>
<div>
<p className="text-sm font-medium">{t('expiresAt')}:</p>
<p className="text-sm">{service.planExpAt || t('notSet')}</p>
</div>
<div>
<p className="text-sm font-medium">{t('registeredAt')}:</p>
<p className="text-sm">{service.registeredAt || t('notSet')}</p>
</div>
<div>
<p className="text-sm font-medium">{t('planDelay')}:</p>
<p className="text-sm">{service.planDelay || 0} {t('months')}</p>
</div>
<div>
<p className="text-sm font-medium">{t('lastUpdated')}:</p>
<p className="text-sm">{service.planUpdatedAt || t('notSet')}</p>
</div>
</div>
</div>
);
})}
</div>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">{t('noServicesSubscribed')}</p>
)}
{/* Explanation of update behavior */}
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded border-l-4 border-blue-400">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>{t('note')}:</strong> {t('subscriptionUpdateNote')}
</p>
</div>
</div>
)}
<div className=" <div className="
[&_*]:fill-text/80 [&_*]:dark:fill-text-dark/80 [&_*]:fill-text/80 [&_*]:dark:fill-text-dark/80
[&_*]:text-text/80 [&_*]:dark:text-text-dark/80 [&_*]:text-text/80 [&_*]:dark:text-text-dark/80
@ -308,7 +376,10 @@ export default function UpdateSubPopUp()
} }
<DialogActions sx={{ padding: '16px 24px' }}> <DialogActions sx={{ padding: '16px 24px' }}>
<div className="flex gap-3"> <div className="flex gap-3">
<button type="submit" className="btn font-semibold"> <button onClick={async () => {
window.location.reload();
//router.push('/dashboard/members');
}} type="submit" className="btn font-semibold">
{ {
isSubmitting ? isSubmitting ?
<CircularProgress color="secondary" size={24} /> <CircularProgress color="secondary" size={24} />

View File

@ -17,5 +17,10 @@ const equipmentSchema = new mongoose.Schema<IEquipmentSchema>({
singlePrice: Number, singlePrice: Number,
}, { timestamps: true }); }, { timestamps: true });
// export the model // export the model
const equipmentModel: Model<IEquipmentSchema> = mongoose.models.equipments || mongoose.model<IEquipmentSchema>('equipments', equipmentSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.equipments) {
delete mongoose.models.equipments;
}
const equipmentModel: Model<IEquipmentSchema> = mongoose.model<IEquipmentSchema>('equipments', equipmentSchema);
export default equipmentModel export default equipmentModel

View File

@ -13,5 +13,10 @@ const expenseSchema = new mongoose.Schema<IExpenseSchema>({
addedAt: Number, addedAt: Number,
}, { timestamps: true }); }, { timestamps: true });
// export the model // export the model
const expenseModel: Model<IExpenseSchema> = mongoose.models.expenses || mongoose.model<IExpenseSchema>('expenses', expenseSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.expenses) {
delete mongoose.models.expenses;
}
const expenseModel: Model<IExpenseSchema> = mongoose.model<IExpenseSchema>('expenses', expenseSchema);
export default expenseModel export default expenseModel

View File

@ -13,5 +13,10 @@ const incomeSchema = new mongoose.Schema<IIncomeSchema>({
addedAt: Number, addedAt: Number,
}, { timestamps: true }); }, { timestamps: true });
// export the model // export the model
const incomeModel: Model<IIncomeSchema> = mongoose.models.incomes || mongoose.model<IIncomeSchema>('incomes', incomeSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.incomes) {
delete mongoose.models.incomes;
}
const incomeModel: Model<IIncomeSchema> = mongoose.model<IIncomeSchema>('incomes', incomeSchema);
export default incomeModel export default incomeModel

View File

@ -16,7 +16,20 @@ const memberSchema = new mongoose.Schema<IMemberSchema>({
registerAt: String, registerAt: String,
registerAt_unix: Number, registerAt_unix: Number,
payMonth: Number, payMonth: Number,
services: [String], services: [{
serviceID: String,
serviceName: String,
registeredAt: String,
registeredAt_unix: Number,
planDelay: Number,
planDelay_unix: Number,
planExpAt: String,
planExpAt_unix: Number,
planUpdatedAt: String,
planUpdatedAt_unix: Number,
active: Boolean,
}],
// Global subscription status (derived from services)
planUpdatedAt: String, planUpdatedAt: String,
planUpdatedAt_unix: Number, planUpdatedAt_unix: Number,
planDelay: Number, planDelay: Number,
@ -33,5 +46,10 @@ const memberSchema = new mongoose.Schema<IMemberSchema>({
} }
}, { timestamps: true }); }, { timestamps: true });
const memberModel: Model<IMemberSchema> = mongoose.models.members || mongoose.model<IMemberSchema>('members', memberSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.members) {
delete mongoose.models.members;
}
const memberModel: Model<IMemberSchema> = mongoose.model<IMemberSchema>('members', memberSchema);
export default memberModel export default memberModel

View File

@ -13,5 +13,10 @@ const productsSchema = new mongoose.Schema<IProductsSchema>({
quantity: Number, quantity: Number,
}, { timestamps: true }); }, { timestamps: true });
// export the model // export the model
const productsModel: Model<IProductsSchema> = mongoose.models.products || mongoose.model<IProductsSchema>('products', productsSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.products) {
delete mongoose.models.products;
}
const productsModel: Model<IProductsSchema> = mongoose.model<IProductsSchema>('products', productsSchema);
export default productsModel export default productsModel

View File

@ -20,5 +20,10 @@ const servicesSchema = new mongoose.Schema<IServicesSchema>({
} }
}, { timestamps: true }); }, { timestamps: true });
// export the model // export the model
const serviceModel: Model<IServicesSchema> = mongoose.models.services || mongoose.model<IServicesSchema>('services', servicesSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.services) {
delete mongoose.models.services;
}
const serviceModel: Model<IServicesSchema> = mongoose.model<IServicesSchema>('services', servicesSchema);
export default serviceModel export default serviceModel

View File

@ -293,5 +293,10 @@ const statisticsSchema = new mongoose.Schema<IStatisticsSchema>({
totalOutcome: Number, totalOutcome: Number,
}, { timestamps: true }); }, { timestamps: true });
const statisticsModel: Model<IStatisticsSchema> = mongoose.models.statistics || mongoose.model<IStatisticsSchema>('statistics', statisticsSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.statistics) {
delete mongoose.models.statistics;
}
const statisticsModel: Model<IStatisticsSchema> = mongoose.model<IStatisticsSchema>('statistics', statisticsSchema);
export default statisticsModel export default statisticsModel

View File

@ -44,5 +44,10 @@ const userSchema = new mongoose.Schema<IUserSchema>({
} }
}, { timestamps: true }); }, { timestamps: true });
const userModel: Model<IUserSchema> = mongoose.models.users || mongoose.model<IUserSchema>('users', userSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.users) {
delete mongoose.models.users;
}
const userModel: Model<IUserSchema> = mongoose.model<IUserSchema>('users', userSchema);
export default userModel export default userModel

View File

@ -17,5 +17,10 @@ const workersSchema = new mongoose.Schema<IWorkersSchema>({
address: String, address: String,
}, { timestamps: true }); }, { timestamps: true });
// export the model // export the model
const workerModel: Model<IWorkersSchema> = mongoose.models.workers || mongoose.model<IWorkersSchema>('workers', workersSchema); // Clear any existing model to avoid schema conflicts
if (mongoose.models.workers) {
delete mongoose.models.workers;
}
const workerModel: Model<IWorkersSchema> = mongoose.model<IWorkersSchema>('workers', workersSchema);
export default workerModel export default workerModel

View File

@ -0,0 +1,12 @@
import axios from 'axios'
import { IServicesSchema } from '@/types/IDB'
const ACTION_NAME = 'services'
export default async function loadAllServices()
{
let { data } = await axios.get(`/api/user/actions/${ACTION_NAME}?type=page&page=1`)
let docs : IServicesSchema[] = data.data.docs;
// Filter only active services
return docs.filter(service => service.status === 'active');
}

View File

@ -135,7 +135,7 @@
"selectAtLeastOneService": "يجب اختيار خدمة واحدة على الاقل", "selectAtLeastOneService": "يجب اختيار خدمة واحدة على الاقل",
"mustBeANumber": "يجب ان تكون رقما", "mustBeANumber": "يجب ان تكون رقما",
"registerAt": "سجل في", "registerAt": "سجل في",
"planStatus": "حالة الخطة", "planStatus": "حالة الاشتراك",
"expireVerySoon": "تنتهي قريبا", "expireVerySoon": "تنتهي قريبا",
"BodyStateInformations": "معلومات عن حالة الجسم", "BodyStateInformations": "معلومات عن حالة الجسم",
"planDetails": "معلومات عن الاشتراك", "planDetails": "معلومات عن الاشتراك",
@ -144,7 +144,33 @@
"memberQRCode": "رمز QR للعضو", "memberQRCode": "رمز QR للعضو",
"qrCodeDescription": "امسح رمز الاستجابة السريعة هذا للوصول السريع إلى معلومات العضو:", "qrCodeDescription": "امسح رمز الاستجابة السريعة هذا للوصول السريع إلى معلومات العضو:",
"downloadQR": "تحميل رمز QR", "downloadQR": "تحميل رمز QR",
"qrDownloadNote": "يتم التحميل كصورة PNG" "qrDownloadNote": "يتم التحميل كصورة PNG",
"memberServices": "خدمات العضو",
"expiresAt": "تنتهي في",
"updatedAt": "محدث في",
"note": "ملاحظة",
"subscriptionStatusNote": "حالة الاشتراك والتواريخ المعروضة تنطبق على جميع الخدمات لهذا العضو.",
"noServicesSubscribed": "لا توجد خدمات مشترك بها",
"activeServices": "الخدمات النشطة",
"currentSubscriptionStatus": "حالة الاشتراك الحالية",
"currentServices": "الخدمات الحالية",
"subscriptionStatus": "حالة الاشتراك",
"active": "نشط",
"expired": "منتهي الصلاحية",
"currentPlanDelay": "مدة الخطة الحالية",
"months": "أشهر",
"notSet": "غير محدد",
"subscriptionUpdateNote": "عند التحديث: سيتم تمديد الخطط النشطة، واستبدال الخطط المنتهية الصلاحية، وإضافة خدمات جديدة إلى اشتراكك.",
"currentServicesStatus": "حالة الخدمات الحالية",
"serviceName": "اسم الخدمة",
"registeredAt": "تاريخ التسجيل",
"lastUpdated": "آخر تحديث",
"gender": "الجنس",
"female": "أنثى",
"male": "ذكر",
"planExpAt": "تنتهي الخطة في",
"planUpdatedAt": "تم تحديث الخطة في",
"perServiceSubscriptionNote": "كل خدمة لها حالة اشتراك وتاريخ انتهاء منفصل."
}, },
"workers": { "workers": {
"workers": "العمال", "workers": "العمال",
@ -391,6 +417,13 @@
"services": "الخدمات", "services": "الخدمات",
"workers": "العمال", "workers": "العمال",
"incomes": "الايرادات", "incomes": "الايرادات",
"sales": "مبيعات" "sales": "مبيعات",
"totalExpense": "إجمالي النفقات",
"totalIncome": "إجمالي الإيرادات",
"activeSubscriptions": "الاشتراكات النشطة",
"expiredSubscriptions": "الاشتراكات المنتهية",
"totalSubscriptions": "إجمالي الاشتراكات",
"subscriptionsCount": "عدد الاشتراكات",
"noServicesFound": "لم يتم العثور على خدمات"
} }
} }

View File

@ -148,7 +148,33 @@
"memberQRCode": "Member QR Code", "memberQRCode": "Member QR Code",
"qrCodeDescription": "Scan this QR code to quickly access member information:", "qrCodeDescription": "Scan this QR code to quickly access member information:",
"downloadQR": "Download QR Code", "downloadQR": "Download QR Code",
"qrDownloadNote": "Downloads as PNG image" "qrDownloadNote": "Downloads as PNG image",
"memberServices": "Member Services",
"planExpAt": "Expires At",
"planUpdatedAt": "Updated At",
"note": "Note",
"subscriptionStatusNote": "The subscription status and dates shown apply to all services for this member.",
"noServicesSubscribed": "No services subscribed",
"activeServices": "Active Services",
"currentSubscriptionStatus": "Current Subscription Status",
"currentServices": "Current Services",
"subscriptionStatus": "Subscription Status",
"active": "Active",
"expired": "Expired",
"currentPlanDelay": "Current Plan Duration",
"months": "months",
"notSet": "Not Set",
"subscriptionUpdateNote": "When updating: Active plans will be extended, expired plans will be replaced, and new services will be added to your subscription.",
"currentServicesStatus": "Current Services Status",
"serviceName": "Service Name",
"registeredAt": "Registered At",
"lastUpdated": "Last Updated",
"gender": "Gender",
"female": "Female",
"male": "Male",
"planExpAt": "Plan Expires At",
"planUpdatedAt": "Plan Updated At",
"perServiceSubscriptionNote": "Each service has its own subscription status and expiration date."
}, },
"workers": { "workers": {
"workers": "Workers", "workers": "Workers",
@ -395,7 +421,14 @@
"services": "Services", "services": "Services",
"workers": "Workers", "workers": "Workers",
"incomes": "Incomes", "incomes": "Incomes",
"sales": "Sales" "sales": "Sales",
"totalExpense": "Total Expense",
"totalIncome": "Total Income",
"activeSubscriptions": "Active Subscriptions",
"expiredSubscriptions": "Expired Subscriptions",
"totalSubscriptions": "Total Subscriptions",
"subscriptionsCount": "Subscriptions Count",
"noServicesFound": "No services found"
} }
} }

View File

@ -60,6 +60,15 @@ type IValueState = {
services: { services: {
serviceID: string | null, serviceID: string | null,
serviceName: string | null, serviceName: string | null,
registeredAt: string | null,
registeredAt_unix: number | null,
planDelay: number | null,
planDelay_unix: number | null,
planExpAt: string | null,
planExpAt_unix: number | null,
planUpdatedAt: string | null,
planUpdatedAt_unix: number | null,
active: boolean | null,
}[] | null, }[] | null,
payMonth: string | null, payMonth: string | null,
planUpdatedAt: string | null, planUpdatedAt: string | null,
@ -92,6 +101,15 @@ type IValueState = {
services: { services: {
serviceID: string | null, serviceID: string | null,
serviceName: string | null, serviceName: string | null,
registeredAt: string | null,
registeredAt_unix: number | null,
planDelay: number | null,
planDelay_unix: number | null,
planExpAt: string | null,
planExpAt_unix: number | null,
planUpdatedAt: string | null,
planUpdatedAt_unix: number | null,
active: boolean | null,
}[] | null, }[] | null,
payMonth: string | null, payMonth: string | null,
planUpdatedAt: string | null, planUpdatedAt: string | null,
@ -340,6 +358,7 @@ const update = createAsyncThunk(
phone : string | null | undefined , phone : string | null | undefined ,
address : string | null | undefined , address : string | null | undefined ,
active: boolean | null | undefined, active: boolean | null | undefined,
gendre: string | null | undefined,
}, thunkAPI) => { }, thunkAPI) => {
try { try {
let { data } = await axios.put(`/api/user/actions/${ACTION_NAME}`, { let { data } = await axios.put(`/api/user/actions/${ACTION_NAME}`, {
@ -373,7 +392,7 @@ const updateSub = createAsyncThunk(
'services/updateSub', 'services/updateSub',
async (actionPayload : { async (actionPayload : {
_id: string | null | undefined, _id: string | null | undefined,
services: [string] | [] | undefined, services: string[] | undefined,
planDelay: number | null | undefined, planDelay: number | null | undefined,
bodyState: { bodyState: {
currentBodyForm: string | null, currentBodyForm: string | null,
@ -432,18 +451,27 @@ export const members = createSlice({
registerAt: string, registerAt: string,
registerAt_unix: string | null, registerAt_unix: string | null,
services: { services: {
serviceID: string | null, serviceID: string | null,
serviceName: string | null, serviceName: string | null,
}[] | null, registeredAt: string | null,
payMonth: string | null, registeredAt_unix: number | null,
planUpdatedAt: string | null, planDelay: number | null,
planUpdatedAt_unix: number | null, planDelay_unix: number | null,
planDelay: number | null, planExpAt: string | null,
planDelay_unix: number | null, planExpAt_unix: number | null,
planExpAt: string | null, planUpdatedAt: string | null,
planExpAt_unix: number | null, planUpdatedAt_unix: number | null,
active: boolean | null, active: boolean | null,
gendre: string | null, // m or w }[] | null,
payMonth: string | null,
planUpdatedAt: string | null,
planUpdatedAt_unix: number | null,
planDelay: number | null,
planDelay_unix: number | null,
planExpAt: string | null,
planExpAt_unix: number | null,
active: boolean | null,
gendre: string | null, // m or w
bodyState: { bodyState: {
startBodyForm: string | null, startBodyForm: string | null,
currentBodyForm: string | null, currentBodyForm: string | null,

View File

@ -324,7 +324,7 @@ export const services = createSlice({
{ {
state.value.listContent = action.payload.data.docs state.value.listContent = action.payload.data.docs
state.value.total = action.payload.data.docs_count state.value.total = action.payload.data.docs_count
if(Math.ceil(action.payload.data.totalArrLength / 4) < state.value.currentPage) if(Math.ceil(action.payload.data.totalArrLength / 10) < state.value.currentPage)
{ {
state.value.currentPage -= 1 state.value.currentPage -= 1
} }

View File

@ -39,7 +39,17 @@ export interface IMemberSchema extends Document {
services: { services: {
serviceID: string, serviceID: string,
serviceName: string, serviceName: string,
registeredAt: string,
registeredAt_unix: number,
planDelay: number,
planDelay_unix: number,
planExpAt: string,
planExpAt_unix: number,
planUpdatedAt: string,
planUpdatedAt_unix: number,
active: boolean,
}[], }[],
// Global subscription status (derived from services)
planUpdatedAt: string, planUpdatedAt: string,
planUpdatedAt_unix: number, planUpdatedAt_unix: number,
planDelay: number, planDelay: number,