Descriptive message about your changes
This commit is contained in:
parent
87cb513317
commit
386241b488
@ -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 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 = {
|
||||
name: `${member.firstName} ${member.lastName}`,
|
||||
gender: member.gendre,
|
||||
planDelay: member.planDelay,
|
||||
planStart: member.planUpdatedAt,
|
||||
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
|
||||
|
||||
@ -73,7 +73,7 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the page
|
||||
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
|
||||
const docs = await equipmentModel.find({}).skip(range[0]).limit(range[1]),
|
||||
// get the size of the docs
|
||||
@ -141,7 +141,7 @@ export async function DELETE(req:Request)
|
||||
const { searchParams } = new URL(req.url)
|
||||
const page = searchParams.get('page'),
|
||||
_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
|
||||
await equipmentModel.findByIdAndRemove(_id)
|
||||
// get the docs by page
|
||||
|
||||
@ -70,9 +70,9 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the page
|
||||
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
|
||||
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4];
|
||||
// get the docs
|
||||
const docs = await expenseModel.find({}).skip(range[0]).limit(range[1]),
|
||||
range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
|
||||
// get the docs (sorted by addedAt in descending order to show newest first)
|
||||
const docs = await expenseModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
|
||||
// get the size of the docs
|
||||
docs_count = await expenseModel.countDocuments({});
|
||||
// prepare the return data
|
||||
@ -90,14 +90,14 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the searchKeyword param
|
||||
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({
|
||||
$or: [
|
||||
// search by ( case insensitive search )
|
||||
{ name: { $regex: searchKeyword, $options: 'i' } },
|
||||
{ description: { $regex: searchKeyword, $options: 'i' } }
|
||||
]
|
||||
})
|
||||
}).sort({ addedAt: -1 })
|
||||
// get the docs
|
||||
let responseData = {
|
||||
docs: results,
|
||||
@ -136,7 +136,7 @@ export async function DELETE(req:Request)
|
||||
const { searchParams } = new URL(req.url)
|
||||
const page = searchParams.get('page'),
|
||||
_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
|
||||
// get the old amount
|
||||
let doc = await expenseModel.findById(_id)
|
||||
@ -148,8 +148,8 @@ export async function DELETE(req:Request)
|
||||
})
|
||||
// delete the doc
|
||||
await expenseModel.findByIdAndRemove(_id)
|
||||
// get the docs by page
|
||||
const docs = await expenseModel.find({}).skip(range[0]).limit(range[1]),
|
||||
// get the docs by page (sorted by addedAt in descending order)
|
||||
const docs = await expenseModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
|
||||
// get the size of the docs
|
||||
docs_count = await expenseModel.countDocuments({});
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -70,9 +70,10 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the page
|
||||
const page : string | null = searchParams.get('page') ? searchParams.get('page') : '0',
|
||||
range : number[] = [(parseInt(page?page : '0') - 1) * 4 , 4];
|
||||
// get the docs
|
||||
const docs = await incomesModel.find({}).skip(range[0]).limit(range[1]),
|
||||
range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
|
||||
// range : number[] = [(parseInt(page?page : '0') - 1) * 10, 10];
|
||||
// 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
|
||||
docs_count = await incomesModel.countDocuments({});
|
||||
// prepare the return data
|
||||
@ -90,14 +91,14 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the searchKeyword param
|
||||
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({
|
||||
$or: [
|
||||
// search by ( case insensitive search )
|
||||
{ name: { $regex: searchKeyword, $options: 'i' } },
|
||||
{ description: { $regex: searchKeyword, $options: 'i' } }
|
||||
]
|
||||
})
|
||||
}).sort({ addedAt: -1 })
|
||||
// get the docs
|
||||
let responseData = {
|
||||
docs: results,
|
||||
@ -136,7 +137,7 @@ export async function DELETE(req:Request)
|
||||
const { searchParams } = new URL(req.url)
|
||||
const page = searchParams.get('page'),
|
||||
_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
|
||||
// get the old amount
|
||||
let doc = await incomesModel.findById(_id)
|
||||
@ -148,8 +149,8 @@ export async function DELETE(req:Request)
|
||||
})
|
||||
// delete the doc
|
||||
await incomesModel.findByIdAndRemove(_id)
|
||||
// get the docs by page
|
||||
const docs = await incomesModel.find({}).skip(range[0]).limit(range[1]),
|
||||
// get the docs by page (sorted by addedAt in descending order)
|
||||
const docs = await incomesModel.find({}).sort({ addedAt: -1 }).skip(range[0]).limit(range[1]),
|
||||
// get the size of the docs
|
||||
docs_count = await incomesModel.countDocuments({});
|
||||
// prepare the return data
|
||||
|
||||
@ -20,9 +20,29 @@ export async function POST(req:Request)
|
||||
email : string | null = payload.email as string | null,
|
||||
phone : string | null = payload.phone 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,
|
||||
gendre : string | null = payload.gendre as string | null,
|
||||
gendre : 'm' | 'w' = payload.gendre as 'm' | 'w',
|
||||
bodyState : {
|
||||
startBodyForm: String,
|
||||
startWeight: Number,
|
||||
@ -30,38 +50,101 @@ export async function POST(req:Request)
|
||||
startBodyForm: String,
|
||||
startWeight: Number,
|
||||
} | 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) => {
|
||||
return new mongoose.Types.ObjectId(v as any)
|
||||
})
|
||||
const servicesDocs = await serviceModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
_id: { $in : ids }
|
||||
const servicesDocs = await serviceModel.find({ _id: { $in: ids } })
|
||||
|
||||
// Validate that all services exist
|
||||
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,
|
||||
totalPrice: {
|
||||
$sum: "$price"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
// inc the value of totalSubscribers for all included services in the membership
|
||||
|
||||
// Calculate total monthly pay
|
||||
const payMonth = servicesDocs.reduce((total, service) => total + service.price, 0)
|
||||
|
||||
// Declare needed variables
|
||||
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);
|
||||
|
||||
// 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}} , {
|
||||
$inc: {
|
||||
totalSubscribers: 1
|
||||
}
|
||||
})
|
||||
const payMonth : number = servicesDocs[0] ? servicesDocs[0].totalPrice : 0
|
||||
// declare needed variables
|
||||
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
|
||||
|
||||
// Save the doc with new structure
|
||||
const doc = new memberModel({
|
||||
firstName,
|
||||
lastName,
|
||||
@ -69,11 +152,12 @@ export async function POST(req:Request)
|
||||
phone,
|
||||
address,
|
||||
payMonth,
|
||||
services,
|
||||
services: servicesArray,
|
||||
gendre,
|
||||
bodyState,
|
||||
registerAt: current_date.toUTCString(),
|
||||
registerAt_unix: current_date_unix,
|
||||
// Global subscription status (derived from services)
|
||||
planUpdatedAt: current_date.toUTCString(),
|
||||
planUpdatedAt_unix: current_date_unix,
|
||||
planDelay,
|
||||
@ -82,8 +166,36 @@ export async function POST(req:Request)
|
||||
planExpAt_unix: current_date_unix + planDelay_unix,
|
||||
active: true,
|
||||
})
|
||||
|
||||
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 NextResponse.json({
|
||||
success: true,
|
||||
@ -97,10 +209,14 @@ export async function POST(req:Request)
|
||||
})
|
||||
}catch(e)
|
||||
{
|
||||
// Log the actual error for debugging
|
||||
console.error('Error adding new member:', e);
|
||||
|
||||
// catch any error and return an error response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "serverError",
|
||||
error: e instanceof Error ? e.message : 'Unknown error'
|
||||
}, {
|
||||
status: 500,
|
||||
headers: {
|
||||
@ -171,10 +287,14 @@ export async function GET(req:Request)
|
||||
}
|
||||
}catch(e)
|
||||
{
|
||||
// Log the actual error for debugging
|
||||
console.error('Error updating member:', e);
|
||||
|
||||
// catch any error and return an error response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "serverError",
|
||||
error: e instanceof Error ? e.message : 'Unknown error'
|
||||
}, {
|
||||
status: 500,
|
||||
headers: {
|
||||
@ -211,10 +331,14 @@ export async function DELETE(req:Request)
|
||||
})
|
||||
}catch(e)
|
||||
{
|
||||
// Log the actual error for debugging
|
||||
console.error('Error in GET request:', e);
|
||||
|
||||
// catch any error and return an error response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "serverError",
|
||||
error: e instanceof Error ? e.message : 'Unknown error'
|
||||
}, {
|
||||
status: 500,
|
||||
headers: {
|
||||
@ -242,7 +366,8 @@ export async function PUT(req:Request)
|
||||
email : string | null = payload.email as string | null,
|
||||
phone : string | null = payload.phone 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
|
||||
const doc = await memberModel.findByIdAndUpdate( _id , {
|
||||
firstName,
|
||||
@ -251,6 +376,7 @@ export async function PUT(req:Request)
|
||||
phone,
|
||||
address,
|
||||
active,
|
||||
gendre,
|
||||
} , {
|
||||
new: true
|
||||
})
|
||||
@ -270,7 +396,18 @@ export async function PUT(req:Request)
|
||||
{
|
||||
// declare the needed variables
|
||||
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,
|
||||
bodyState : {
|
||||
currentBodyForm: String,
|
||||
@ -279,53 +416,166 @@ export async function PUT(req:Request)
|
||||
currentBodyForm: String,
|
||||
currentWeight: Number,
|
||||
} | null;
|
||||
// calculat month pay
|
||||
const ids = services?.map((v , i) => {
|
||||
return new mongoose.Types.ObjectId(v as string)
|
||||
|
||||
// Get current member data
|
||||
const currentMember = await memberModel.findById(_id);
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "Member not found",
|
||||
}, {
|
||||
status: 404,
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
})
|
||||
const servicesDocs = await serviceModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
_id: { $in : ids }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalPrice: {
|
||||
$sum: "$price"
|
||||
|
||||
// Get service details for new services
|
||||
const newServiceIds = newServices.map(id => new mongoose.Types.ObjectId(id as string));
|
||||
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
|
||||
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
|
||||
|
||||
// Calculate total monthly pay and global subscription status
|
||||
const allServiceIds = updatedServices.map(s => new mongoose.Types.ObjectId(s.serviceID));
|
||||
const allServicesDocs = await serviceModel.find({ _id: { $in: allServiceIds } });
|
||||
const payMonth = allServicesDocs.reduce((total, service) => total + service.price, 0);
|
||||
|
||||
// Determine global subscription status (latest expiration date)
|
||||
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,
|
||||
services,
|
||||
services: plainServices,
|
||||
payMonth,
|
||||
planUpdatedAt: current_date.toUTCString(),
|
||||
planUpdatedAt_unix: current_date_unix,
|
||||
planDelay,
|
||||
planDelay_unix: planDelay_unix,
|
||||
planExpAt: planExpAt.toUTCString(),
|
||||
planExpAt_unix: current_date_unix + planDelay_unix,
|
||||
active: true,
|
||||
planDelay: globalPlanDelay,
|
||||
planDelay_unix: globalPlanDelay * 2592000,
|
||||
planExpAt: new Date(latestExpiration * 1000).toUTCString(),
|
||||
planExpAt_unix: latestExpiration,
|
||||
active: globalActive,
|
||||
}, {
|
||||
new: true
|
||||
})
|
||||
// add the subscription income
|
||||
let income = payMonth * planDelay
|
||||
});
|
||||
|
||||
// Add income entry if there's any charge
|
||||
if (totalIncome > 0) {
|
||||
await statisticsModel.findOneAndUpdate({}, {
|
||||
$inc: {
|
||||
totalIncome: income
|
||||
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 NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@ -65,7 +65,7 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the page
|
||||
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
|
||||
const docs = await productsModel.find({}).skip(range[0]).limit(range[1]),
|
||||
// get the size of the docs
|
||||
@ -131,7 +131,7 @@ export async function DELETE(req:Request)
|
||||
const { searchParams } = new URL(req.url)
|
||||
const page = searchParams.get('page'),
|
||||
_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
|
||||
await productsModel.findByIdAndRemove(_id)
|
||||
// get the docs by page
|
||||
|
||||
105
webapp/src/app/api/user/actions/services-subscriptions/route.ts
Normal file
105
webapp/src/app/api/user/actions/services-subscriptions/route.ts
Normal 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"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -64,9 +64,10 @@ export async function GET(req:Request)
|
||||
{
|
||||
// get the page
|
||||
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
|
||||
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
|
||||
docs_count = await serviceModel.countDocuments({});
|
||||
// prepare the return data
|
||||
@ -91,7 +92,7 @@ export async function GET(req:Request)
|
||||
{ name: { $regex: searchKeyword, $options: 'i' } },
|
||||
{ description: { $regex: searchKeyword, $options: 'i' } }
|
||||
]
|
||||
})
|
||||
}).sort({ addedAt: -1 })
|
||||
// get the docs
|
||||
let responseData = {
|
||||
docs: results,
|
||||
@ -130,11 +131,11 @@ export async function DELETE(req:Request)
|
||||
const { searchParams } = new URL(req.url)
|
||||
const page = searchParams.get('page'),
|
||||
_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
|
||||
await serviceModel.findByIdAndRemove(_id)
|
||||
// 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
|
||||
docs_count = await serviceModel.countDocuments({});
|
||||
// prepare the return data
|
||||
|
||||
@ -58,14 +58,33 @@ async function updateMembersOverviewStatistics() {
|
||||
registerAt_unix: { $lt: endOfDayUnix }
|
||||
});
|
||||
|
||||
// Count members with at least one active service subscription
|
||||
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({
|
||||
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({
|
||||
@ -135,13 +154,33 @@ async function updateMembersOverviewStatistics() {
|
||||
const totalMembers = await memberModel.countDocuments({
|
||||
registerAt_unix: { $lt: endOfDayUnix }
|
||||
});
|
||||
// Count members with at least one active service subscription
|
||||
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({
|
||||
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({
|
||||
gendre: 'm',
|
||||
@ -236,13 +275,33 @@ async function updateMembersOverviewStatistics() {
|
||||
const totalMembers = await memberModel.countDocuments({
|
||||
registerAt_unix: { $lt: endOfDayUnix }
|
||||
});
|
||||
// Count members with at least one active service subscription
|
||||
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({
|
||||
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({
|
||||
gendre: 'm',
|
||||
@ -365,12 +424,48 @@ export async function GET(req:Request)
|
||||
let membersCount : number = await memberModel.count({})
|
||||
let activeMembersCount : number = await memberModel.count({active: true})
|
||||
let disActiveMembersCount : number = await memberModel.count({active: false})
|
||||
let activeSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$gt : Math.floor(new Date().getTime() / 1000)}})
|
||||
let expiredSoonSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$gt : Math.floor(new Date().getTime() / 1000) + 259200}})
|
||||
let expiredSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$lte : Math.floor(new Date().getTime() / 1000)}})
|
||||
// Count members with at least one active service subscription
|
||||
let activeSubscriptionsCount : number = await memberModel.count({
|
||||
'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 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 :
|
||||
{
|
||||
_id: Types.ObjectId,
|
||||
|
||||
@ -36,12 +36,12 @@ export default function List()
|
||||
let dateObj = new Date(unix_time * 1000)
|
||||
// we got the year
|
||||
let year = dateObj.getUTCFullYear(),
|
||||
// get month
|
||||
month = dateObj.getUTCMonth(),
|
||||
// get day
|
||||
day = dateObj.getUTCDay();
|
||||
// now we format the string
|
||||
let formattedTime = year.toString()+ '-' + month.toString() + '-' + day.toString();
|
||||
// get month (add 1 because getUTCMonth returns 0-11)
|
||||
month = dateObj.getUTCMonth() + 1,
|
||||
// get day of month
|
||||
day = dateObj.getUTCDate();
|
||||
// now we format the string with zero padding
|
||||
let formattedTime = year.toString()+ '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
|
||||
@ -2,65 +2,164 @@ import { useAppSelector } from '@/redux/store';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Cookies from 'universal-cookie';
|
||||
import { useState, useEffect } from 'react';
|
||||
const ReactApexChart = dynamic(() => import('react-apexcharts'), {
|
||||
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()
|
||||
{
|
||||
// get needed redux state
|
||||
const report = useAppSelector((state) => state.statisticsReducer.value.report)
|
||||
const currencySymbol = useAppSelector((state) => state.settingsReducer.value.appGeneralSettings.currencySymbol) ?? ''
|
||||
const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType)
|
||||
|
||||
// declare global variables
|
||||
const cookies = new Cookies();
|
||||
const t = useTranslations('statistics');
|
||||
const locale = cookies.get("NEXT_LOCALE")
|
||||
const isRtl = locale == 'ar' ? 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(!report) return null // Return null or a loading indicator if report is not yet available
|
||||
// prepare chart data
|
||||
// Ensure totalIncome and totalOutcome are numbers, default to 0 if undefined/null
|
||||
let data = [report?.totalIncome ?? 0 , report?.totalOutcome ?? 0]
|
||||
// prepare chart labels
|
||||
let labels = [t('income') , t('outcome')]
|
||||
if (loading || !analyticsData) {
|
||||
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]">
|
||||
<h3 className="text-xl font-bold">{t('incomeOutcomeGeneralOverview')}</h3>
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<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
|
||||
var options = {
|
||||
series: [{
|
||||
name: currencySymbol,
|
||||
data: data
|
||||
}],
|
||||
annotations: {
|
||||
points: [{
|
||||
x: t('incomes'),
|
||||
seriesIndex: 0,
|
||||
label: {
|
||||
borderColor: '#775DD0',
|
||||
offsetY: 0,
|
||||
style: {
|
||||
color: '#fff',
|
||||
background: '#775DD0',
|
||||
series: [
|
||||
{
|
||||
name: t('income'),
|
||||
data: incomeData,
|
||||
color: '#0263FF'
|
||||
},
|
||||
{
|
||||
name: t('outcome'),
|
||||
data: expenseData,
|
||||
color: '#E3342F'
|
||||
}
|
||||
}]
|
||||
},
|
||||
],
|
||||
chart: {
|
||||
height: 350,
|
||||
type: 'bar',
|
||||
toolbar: {
|
||||
show: true,
|
||||
tools: {
|
||||
download: true,
|
||||
selection: false,
|
||||
zoom: false,
|
||||
zoomin: false,
|
||||
zoomout: false,
|
||||
pan: false,
|
||||
reset: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: '50%',
|
||||
horizontal: true,
|
||||
distributed: true,
|
||||
borderRadius: 4,
|
||||
columnWidth: '60%',
|
||||
dataLabels: {
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: function (val: number) {
|
||||
return currencySymbol + val.toLocaleString();
|
||||
},
|
||||
offsetY: -20,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
colors: [isDark ? '#fff' : '#000']
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'right',
|
||||
fontSize: '16px',
|
||||
fontSize: '14px',
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
@ -71,67 +170,140 @@ export default function IncomeOutcome()
|
||||
vertical: 5,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
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: {
|
||||
width: 2,
|
||||
curve: 'straight',
|
||||
},
|
||||
grid: {
|
||||
row: {
|
||||
colors: [isDark ? '#333' : '#fff', '#f2f2f2'] // Adjust the grid row color for dark mode
|
||||
},
|
||||
borderColor: isDark ? '#333' : '#e0e0e0',
|
||||
strokeDashArray: 3,
|
||||
},
|
||||
xaxis: {
|
||||
categories: chartCategories,
|
||||
labels: {
|
||||
rotate: -45,
|
||||
rotate: viewMode === 'monthly' ? -45 : 0,
|
||||
offsetX: isRtl ? 2 : 0,
|
||||
offsetY: 5,
|
||||
style: {
|
||||
colors: isDark ? '#fff' : '#000',
|
||||
fontSize: '12px',
|
||||
cssClass: 'apexcharts-xaxis-title',
|
||||
},
|
||||
},
|
||||
categories: labels,
|
||||
tickPlacement: 'on'
|
||||
axisBorder: {
|
||||
color: isDark ? '#333' : '#e0e0e0'
|
||||
},
|
||||
axisTicks: {
|
||||
color: isDark ? '#333' : '#e0e0e0'
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 7,
|
||||
labels: {
|
||||
offsetX: isRtl ? -30 : -10,
|
||||
offsetX: isRtl ? -10 : -10,
|
||||
offsetY: 0,
|
||||
style: {
|
||||
colors: isDark ? '#fff' : '#000',
|
||||
fontSize: '12px',
|
||||
cssClass: 'apexcharts-yaxis-title',
|
||||
},
|
||||
formatter: function (val: number) {
|
||||
return currencySymbol + val.toLocaleString();
|
||||
}
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
theme: isDark ? 'dark' : 'light',
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
shared: true,
|
||||
intersect: false,
|
||||
y: {
|
||||
formatter: function (val: number) {
|
||||
return currencySymbol + val.toLocaleString();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
// return the ui
|
||||
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]">
|
||||
{/* 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%'} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,147 +1,317 @@
|
||||
import { useAppSelector } from '@/redux/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Cookies from 'universal-cookie';
|
||||
import { useAppSelector } from '@/redux/store';
|
||||
|
||||
const ReactApexChart = dynamic(() => import('react-apexcharts'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function ServicesSubscriptions()
|
||||
{
|
||||
// get redux needed state
|
||||
const report = useAppSelector((state) => state.statisticsReducer.value.report)
|
||||
const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType)
|
||||
// declare global variables
|
||||
interface ServiceSubscriptionData {
|
||||
_id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
activeSubscriptions: number;
|
||||
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 t = useTranslations('statistics');
|
||||
const locale = cookies.get("NEXT_LOCALE")
|
||||
const isRtl = locale == 'ar' ? true : false
|
||||
const isDark = themeType == 'dark' ? true : false
|
||||
// if loading show load screen
|
||||
if(!report) return
|
||||
// setup chart data
|
||||
let data = report?.servicesNameAndSubscribers.value.map((v : {
|
||||
_id: string,
|
||||
name: string,
|
||||
totalSubscribers: number,
|
||||
} , i : number) => {
|
||||
return v?.totalSubscribers;
|
||||
})
|
||||
// setup chart labels
|
||||
let labels = report?.servicesNameAndSubscribers.value.map((v : {
|
||||
_id: string,
|
||||
name: string,
|
||||
totalSubscribers: number,
|
||||
} , i : number) => {
|
||||
return v?.name;
|
||||
})
|
||||
// setup chart options
|
||||
var options = {
|
||||
series: [{
|
||||
name: t('subscription'),
|
||||
data: data
|
||||
}],
|
||||
annotations: {
|
||||
points: [{
|
||||
x: t('services'),
|
||||
seriesIndex: 0,
|
||||
label: {
|
||||
borderColor: '#775DD0',
|
||||
offsetY: 0,
|
||||
style: {
|
||||
color: '#fff',
|
||||
background: '#775DD0',
|
||||
},
|
||||
const locale = cookies.get("NEXT_LOCALE");
|
||||
const isRtl = locale === 'ar';
|
||||
const isDark = themeType === 'dark';
|
||||
|
||||
// Fetch services subscriptions data
|
||||
useEffect(() => {
|
||||
const fetchServicesData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/user/actions/services-subscriptions');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setServicesData(result.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(result.message || 'Failed to fetch data');
|
||||
}
|
||||
}]
|
||||
} catch (err) {
|
||||
setError('Network error occurred');
|
||||
console.error('Error fetching services data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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: {
|
||||
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: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
columnWidth: '50%',
|
||||
distributed: true,
|
||||
borderRadius: 8,
|
||||
columnWidth: '60%',
|
||||
horizontal: false,
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
type: isDark ? '' : 'gradient',
|
||||
type: isDark ? 'solid' : 'gradient',
|
||||
gradient: isDark ? {} : {
|
||||
shadeIntensity: 1,
|
||||
inverseColors: !1,
|
||||
opacityFrom: isDark ? 0.19 : 0.28,
|
||||
opacityTo: 0.05,
|
||||
stops: isDark ? [100, 100] : [45, 100],
|
||||
inverseColors: false,
|
||||
opacityFrom: 0.85,
|
||||
opacityTo: 0.55,
|
||||
stops: [0, 100],
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'right',
|
||||
fontSize: '16px',
|
||||
position: 'top' as const,
|
||||
horizontalAlign: 'right' as const,
|
||||
fontSize: '14px',
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
width: 12,
|
||||
height: 12,
|
||||
offsetX: isRtl ? 5 : -5,
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
},
|
||||
labels: {
|
||||
colors: isDark ? '#fff' : '#000',
|
||||
}
|
||||
},
|
||||
colors: ['#38C172', '#38C172' , '#E3342F' , '#0263FF', '#FF30F7'],
|
||||
colors: ['#10B981', '#EF4444'], // Green for active, Red for expired
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
enabled: true,
|
||||
style: {
|
||||
colors: ['#fff']
|
||||
},
|
||||
formatter: function(val: number) {
|
||||
return val > 0 ? val.toString() : '';
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
width: 2,
|
||||
curve: 'straight',
|
||||
width: 1,
|
||||
colors: ['transparent']
|
||||
},
|
||||
grid: {
|
||||
row: {
|
||||
colors: [isDark ? '#333' : '#fff', '#f2f2f2'] // Adjust the grid row color for dark mode
|
||||
},
|
||||
borderColor: isDark ? '#374151' : '#E5E7EB',
|
||||
strokeDashArray: 3,
|
||||
},
|
||||
xaxis: {
|
||||
categories: labels,
|
||||
labels: {
|
||||
rotate: -45,
|
||||
offsetX: isRtl ? 2 : 0,
|
||||
offsetY: 5,
|
||||
style: {
|
||||
colors: isDark ? '#fff' : '#000',
|
||||
colors: isDark ? '#D1D5DB' : '#374151',
|
||||
fontSize: '12px',
|
||||
cssClass: 'apexcharts-xaxis-title',
|
||||
},
|
||||
maxHeight: 80,
|
||||
},
|
||||
categories: labels,
|
||||
axisBorder: {
|
||||
color: isDark ? '#374151' : '#E5E7EB',
|
||||
},
|
||||
axisTicks: {
|
||||
color: isDark ? '#374151' : '#E5E7EB',
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 7,
|
||||
labels: {
|
||||
offsetX: isRtl ? -30 : -10,
|
||||
offsetY: 0,
|
||||
offsetX: isRtl ? -10 : -5,
|
||||
style: {
|
||||
colors: isDark ? '#fff' : '#000',
|
||||
colors: isDark ? '#D1D5DB' : '#374151',
|
||||
fontSize: '12px',
|
||||
cssClass: 'apexcharts-yaxis-title',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
text: t('subscriptionsCount'),
|
||||
style: {
|
||||
color: isDark ? '#D1D5DB' : '#374151',
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: isDark ? 'dark' : 'light',
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
// return the ui
|
||||
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>
|
||||
<ReactApexChart series={options.series} options={options} type="bar" height={350} width={'100%'} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
shared: 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 (
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
@ -36,12 +36,12 @@ export default function List()
|
||||
let dateObj = new Date(unix_time * 1000)
|
||||
// we got the year
|
||||
let year = dateObj.getUTCFullYear(),
|
||||
// get month
|
||||
month = dateObj.getUTCMonth(),
|
||||
// get day
|
||||
day = dateObj.getUTCDay();
|
||||
// now we format the string
|
||||
let formattedTime = year.toString()+ '-' + month.toString() + '-' + day.toString();
|
||||
// get month (add 1 because getUTCMonth returns 0-11)
|
||||
month = dateObj.getUTCMonth() + 1,
|
||||
// get day of month
|
||||
day = dateObj.getUTCDate();
|
||||
// now we format the string with zero padding
|
||||
let formattedTime = year.toString()+ '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
|
||||
@ -22,8 +22,8 @@ export default function AddNewButton()
|
||||
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">
|
||||
<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" 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="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="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>
|
||||
<h3>{t('addNewIncome')}</h3>
|
||||
</button>
|
||||
|
||||
@ -59,8 +59,8 @@ export default function List()
|
||||
<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('gendre')}</span>
|
||||
<span className="w-full text-start">{t('payMonth')}</span>
|
||||
<span className="w-full text-start">{t('planDelay')}</span>
|
||||
<span className="w-full text-start">{t('phone')}</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('actions')}</span>
|
||||
</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 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">{v.payMonth} {appGeneralSettings.currencySymbol}</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.phone}</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">
|
||||
{v.planExpAt_unix - (Date.now() / 1000) > 259200 ?
|
||||
<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>
|
||||
:
|
||||
(
|
||||
v.planExpAt_unix - (Date.now() / 1000) > 0 ?
|
||||
<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>
|
||||
:
|
||||
{(() => {
|
||||
// Calculate overall subscription status based on individual services
|
||||
const currentTime = Date.now() / 1000;
|
||||
const activeServices = v.services?.filter(service =>
|
||||
service.active && service.planExpAt_unix > currentTime
|
||||
) || [];
|
||||
|
||||
if (activeServices.length === 0) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if any service expires soon (within 3 days)
|
||||
const expiringSoon = activeServices.some(service =>
|
||||
service.planExpAt_unix - currentTime <= 259200
|
||||
);
|
||||
|
||||
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 className="lg:w-full w-[100px] h-full text-start flex justify-start items-center gap-3">
|
||||
<button onClick={async() => {
|
||||
|
||||
@ -16,9 +16,9 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { FormikWizard } from 'formik-wizard-form';
|
||||
import * as Yup from "yup";
|
||||
import Select from 'react-select'
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import { components } from 'react-select';
|
||||
import searchForServices from "@/functions/requests/members/searchForServices";
|
||||
import loadAllServices from "@/functions/requests/members/loadAllServices";
|
||||
|
||||
export default function AddPopUp()
|
||||
{
|
||||
@ -49,7 +49,7 @@ export default function AddPopUp()
|
||||
email: string | null,
|
||||
phone: string | null,
|
||||
address: string | null,
|
||||
services: [string] | [],
|
||||
services: string[],
|
||||
planDelay: string | null,
|
||||
gendre: string | null, // m or w
|
||||
startBodyForm: string | null,
|
||||
@ -225,15 +225,22 @@ export default function AddPopUp()
|
||||
isSubmitting,
|
||||
setFieldValue
|
||||
} : any) => {
|
||||
async function loadServices(s : string) {
|
||||
let docs = await searchForServices(s)
|
||||
const [servicesOptions, setServicesOptions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadServices() {
|
||||
try {
|
||||
let docs = await loadAllServices();
|
||||
let options = docs.map((v, i) => {
|
||||
return {value: v._id, label: v.name}
|
||||
})
|
||||
|
||||
return options
|
||||
});
|
||||
setServicesOptions(options);
|
||||
} catch (error) {
|
||||
console.error('Error loading services:', error);
|
||||
}
|
||||
}
|
||||
loadServices();
|
||||
}, []);
|
||||
// body types options
|
||||
const bodyTypesOptions = [
|
||||
{ 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>
|
||||
</div>
|
||||
<div className="flex flex-col lg:max-w-[350px] min-h-[150px] h-auto">
|
||||
<AsyncSelect
|
||||
cacheOptions
|
||||
loadOptions={loadServices}
|
||||
<Select
|
||||
options={servicesOptions}
|
||||
components={{ NoOptionsMessage }}
|
||||
loadingMessage={() => t('search')}
|
||||
isMulti
|
||||
styles={customStyles}
|
||||
placeholder={t("services")}
|
||||
|
||||
@ -281,13 +281,96 @@ export default function ServiceDetailsPopUp()
|
||||
</p>
|
||||
</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 */}
|
||||
<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 flex-col gap-2">
|
||||
<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>
|
||||
<img
|
||||
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">
|
||||
<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>
|
||||
{t('downloadQR') || 'Download QR Code'}
|
||||
{t('downloadQR')}
|
||||
</button>
|
||||
<p className="text-xs text-gray-500">
|
||||
{t('qrDownloadNote') || 'Downloads as PNG image'}
|
||||
{t('qrDownloadNote')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -85,7 +85,7 @@ export default function UpdatePopUp()
|
||||
lastName : updatePopUpData?.lastName ? updatePopUpData?.lastName : '' ,
|
||||
email : updatePopUpData?.email ? updatePopUpData?.email : '' ,
|
||||
phone : updatePopUpData?.phone ? updatePopUpData?.phone : '' ,
|
||||
//gendre: updatePopUpData?.gendre ? updatePopUpData?.gendre : 'm' ,
|
||||
gendre: updatePopUpData?.gendre ? updatePopUpData?.gendre : 'm' ,
|
||||
address : updatePopUpData?.address ? updatePopUpData?.address : '' ,
|
||||
active: updatePopUpData?.active,
|
||||
//payMonth: updatePopUpData?.payMonth ? updatePopUpData?.payMonth : 0,
|
||||
@ -163,6 +163,41 @@ export default function UpdatePopUp()
|
||||
placeholder={t('lastName')}
|
||||
/>
|
||||
</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>
|
||||
<div className="w-full flex justify-start items-center gap-5">
|
||||
<input
|
||||
|
||||
@ -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 DialogTitle from '@mui/material/DialogTitle';
|
||||
import { useDispatch } from "react-redux";
|
||||
@ -15,18 +15,20 @@ import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Select from 'react-select'
|
||||
import { search as searchForServices , load as loadServices } from '@/redux/features/services-slice'
|
||||
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()
|
||||
{
|
||||
const router = useRouter();
|
||||
// declare the needed variables
|
||||
// redux
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
// intl-18
|
||||
const t = useTranslations('members');
|
||||
// mui
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
// get state from redux
|
||||
@ -48,6 +50,7 @@ export default function UpdateSubPopUp()
|
||||
status: false,
|
||||
data: null
|
||||
}));
|
||||
router.refresh();
|
||||
};
|
||||
// setup multi select
|
||||
// no options react select component
|
||||
@ -126,25 +129,28 @@ export default function UpdateSubPopUp()
|
||||
}),
|
||||
};
|
||||
function ServicesMultiSelect ({setFieldValue , errors , touched} : {setFieldValue : any , errors : any , touched : any}) {
|
||||
// load services
|
||||
const listContent = useAppSelector((state) => state.servicesReducer.value.listContent)
|
||||
async function loadServices (searchKeyword : string) {
|
||||
if(searchKeyword)
|
||||
{
|
||||
await dispatch(searchForServices({searchKeyword: searchKeyword}))
|
||||
}
|
||||
let options = listContent.map((v , i) => {
|
||||
const [servicesOptions, setServicesOptions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadServices() {
|
||||
try {
|
||||
let docs = await loadAllServices();
|
||||
let options = docs.map((v, i) => {
|
||||
return {value: v._id, label: v.name}
|
||||
})
|
||||
return options
|
||||
});
|
||||
setServicesOptions(options);
|
||||
} catch (error) {
|
||||
console.error('Error loading services:', error);
|
||||
}
|
||||
}
|
||||
loadServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w-[350px] min-h-[150px] h-auto">
|
||||
<AsyncSelect
|
||||
cacheOptions
|
||||
loadOptions={loadServices}
|
||||
<Select
|
||||
options={servicesOptions}
|
||||
components={{ NoOptionsMessage }}
|
||||
loadingMessage={() => t('search')}
|
||||
isMulti
|
||||
styles={customStyles}
|
||||
placeholder={t("services")}
|
||||
@ -218,6 +224,7 @@ export default function UpdateSubPopUp()
|
||||
await dispatch(updateSub(values))
|
||||
// close the popup
|
||||
handleClose();
|
||||
|
||||
}}
|
||||
>
|
||||
{({
|
||||
@ -245,6 +252,67 @@ export default function UpdateSubPopUp()
|
||||
|
||||
<div> <h1 className="text-[18px] font-tajawal">{t('memberSubscriptionRenewalText1')}</h1> </div>
|
||||
</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="
|
||||
[&_*]:fill-text/80 [&_*]:dark:fill-text-dark/80
|
||||
[&_*]:text-text/80 [&_*]:dark:text-text-dark/80
|
||||
@ -308,7 +376,10 @@ export default function UpdateSubPopUp()
|
||||
}
|
||||
<DialogActions sx={{ padding: '16px 24px' }}>
|
||||
<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 ?
|
||||
<CircularProgress color="secondary" size={24} />
|
||||
|
||||
@ -17,5 +17,10 @@ const equipmentSchema = new mongoose.Schema<IEquipmentSchema>({
|
||||
singlePrice: Number,
|
||||
}, { timestamps: true });
|
||||
// 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
|
||||
@ -13,5 +13,10 @@ const expenseSchema = new mongoose.Schema<IExpenseSchema>({
|
||||
addedAt: Number,
|
||||
}, { timestamps: true });
|
||||
// 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
|
||||
@ -13,5 +13,10 @@ const incomeSchema = new mongoose.Schema<IIncomeSchema>({
|
||||
addedAt: Number,
|
||||
}, { timestamps: true });
|
||||
// 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
|
||||
@ -16,7 +16,20 @@ const memberSchema = new mongoose.Schema<IMemberSchema>({
|
||||
registerAt: String,
|
||||
registerAt_unix: 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_unix: Number,
|
||||
planDelay: Number,
|
||||
@ -33,5 +46,10 @@ const memberSchema = new mongoose.Schema<IMemberSchema>({
|
||||
}
|
||||
}, { 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
|
||||
@ -13,5 +13,10 @@ const productsSchema = new mongoose.Schema<IProductsSchema>({
|
||||
quantity: Number,
|
||||
}, { timestamps: true });
|
||||
// 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
|
||||
@ -20,5 +20,10 @@ const servicesSchema = new mongoose.Schema<IServicesSchema>({
|
||||
}
|
||||
}, { timestamps: true });
|
||||
// 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
|
||||
@ -293,5 +293,10 @@ const statisticsSchema = new mongoose.Schema<IStatisticsSchema>({
|
||||
totalOutcome: Number,
|
||||
}, { 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
|
||||
@ -44,5 +44,10 @@ const userSchema = new mongoose.Schema<IUserSchema>({
|
||||
}
|
||||
}, { 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
|
||||
@ -17,5 +17,10 @@ const workersSchema = new mongoose.Schema<IWorkersSchema>({
|
||||
address: String,
|
||||
}, { timestamps: true });
|
||||
// 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
|
||||
12
webapp/src/functions/requests/members/loadAllServices.ts
Normal file
12
webapp/src/functions/requests/members/loadAllServices.ts
Normal 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');
|
||||
}
|
||||
@ -135,7 +135,7 @@
|
||||
"selectAtLeastOneService": "يجب اختيار خدمة واحدة على الاقل",
|
||||
"mustBeANumber": "يجب ان تكون رقما",
|
||||
"registerAt": "سجل في",
|
||||
"planStatus": "حالة الخطة",
|
||||
"planStatus": "حالة الاشتراك",
|
||||
"expireVerySoon": "تنتهي قريبا",
|
||||
"BodyStateInformations": "معلومات عن حالة الجسم",
|
||||
"planDetails": "معلومات عن الاشتراك",
|
||||
@ -144,7 +144,33 @@
|
||||
"memberQRCode": "رمز QR للعضو",
|
||||
"qrCodeDescription": "امسح رمز الاستجابة السريعة هذا للوصول السريع إلى معلومات العضو:",
|
||||
"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": "العمال",
|
||||
@ -391,6 +417,13 @@
|
||||
"services": "الخدمات",
|
||||
"workers": "العمال",
|
||||
"incomes": "الايرادات",
|
||||
"sales": "مبيعات"
|
||||
"sales": "مبيعات",
|
||||
"totalExpense": "إجمالي النفقات",
|
||||
"totalIncome": "إجمالي الإيرادات",
|
||||
"activeSubscriptions": "الاشتراكات النشطة",
|
||||
"expiredSubscriptions": "الاشتراكات المنتهية",
|
||||
"totalSubscriptions": "إجمالي الاشتراكات",
|
||||
"subscriptionsCount": "عدد الاشتراكات",
|
||||
"noServicesFound": "لم يتم العثور على خدمات"
|
||||
}
|
||||
}
|
||||
@ -148,7 +148,33 @@
|
||||
"memberQRCode": "Member QR Code",
|
||||
"qrCodeDescription": "Scan this QR code to quickly access member information:",
|
||||
"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",
|
||||
@ -395,7 +421,14 @@
|
||||
"services": "Services",
|
||||
"workers": "Workers",
|
||||
"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"
|
||||
|
||||
}
|
||||
}
|
||||
@ -60,6 +60,15 @@ type IValueState = {
|
||||
services: {
|
||||
serviceID: 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,
|
||||
payMonth: string | null,
|
||||
planUpdatedAt: string | null,
|
||||
@ -92,6 +101,15 @@ type IValueState = {
|
||||
services: {
|
||||
serviceID: 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,
|
||||
payMonth: string | null,
|
||||
planUpdatedAt: string | null,
|
||||
@ -340,6 +358,7 @@ const update = createAsyncThunk(
|
||||
phone : string | null | undefined ,
|
||||
address : string | null | undefined ,
|
||||
active: boolean | null | undefined,
|
||||
gendre: string | null | undefined,
|
||||
}, thunkAPI) => {
|
||||
try {
|
||||
let { data } = await axios.put(`/api/user/actions/${ACTION_NAME}`, {
|
||||
@ -373,7 +392,7 @@ const updateSub = createAsyncThunk(
|
||||
'services/updateSub',
|
||||
async (actionPayload : {
|
||||
_id: string | null | undefined,
|
||||
services: [string] | [] | undefined,
|
||||
services: string[] | undefined,
|
||||
planDelay: number | null | undefined,
|
||||
bodyState: {
|
||||
currentBodyForm: string | null,
|
||||
@ -434,6 +453,15 @@ export const members = createSlice({
|
||||
services: {
|
||||
serviceID: 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,
|
||||
payMonth: string | null,
|
||||
planUpdatedAt: string | null,
|
||||
|
||||
@ -324,7 +324,7 @@ export const services = createSlice({
|
||||
{
|
||||
state.value.listContent = action.payload.data.docs
|
||||
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
|
||||
}
|
||||
|
||||
@ -39,7 +39,17 @@ export interface IMemberSchema extends Document {
|
||||
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_unix: number,
|
||||
planDelay: number,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user