From 37535762c5ec379a10414ea855228a367ae64d1a Mon Sep 17 00:00:00 2001 From: yznahmad Date: Thu, 26 Jun 2025 22:22:51 +0300 Subject: [PATCH] Add v1 1220 --- webapp/-.gitignore | 35 ++ webapp/ecosystem.config.js | 14 +- webapp/next.config.js | 13 +- webapp/package-lock.json | 62 +- webapp/package.json | 6 +- .../(Auth)/dashboard/(home)/page.tsx | 65 +-- .../(GlobalWrapper)/(Auth)/layout.tsx | 8 - .../(GlobalWrapper)/(NotAuth)/layout.tsx | 1 - .../(GlobalWrapper)/(NotAuth)/login/page.tsx | 2 +- webapp/src/app/[locale]/layout.tsx | 4 +- webapp/src/app/api/auth/route.ts | 24 +- .../src/app/api/user/actions/members/route.ts | 4 +- .../app/api/user/actions/statistics/route.ts | 348 ++++++++++- .../src/app/api/user/actions/workers/route.ts | 4 +- .../equipments/parts/addNewButton.tsx | 4 +- .../dashboard/equipments/parts/addPopUp.tsx | 4 +- .../parts/equipmentsListPagination.tsx | 4 +- .../dashboard/expenses/parts/addNewButton.tsx | 4 +- .../dashboard/expenses/parts/addPopUp.tsx | 3 +- .../expenses/parts/expensesListPagination.tsx | 4 +- webapp/src/components/dashboard/header.tsx | 4 +- .../components/dashboard/home/generalLook.tsx | 233 +++----- .../dashboard/home/incomeOutcome.tsx | 144 ++--- .../dashboard/home/membersOverviewChart.tsx | 544 ++++++++---------- .../dashboard/home/servicesSubscriptions.tsx | 71 +-- .../dashboard/home/workersJobTypes.tsx | 71 +-- .../dashboard/incomes/parts/addNewButton.tsx | 4 +- .../dashboard/incomes/parts/addPopUp.tsx | 3 +- .../incomes/parts/incomesListPagination.tsx | 4 +- .../dashboard/members/membersList.tsx | 46 +- .../dashboard/members/parts/addNewButton.tsx | 4 +- .../dashboard/members/parts/addPopUp.tsx | 3 +- .../dashboard/members/parts/detailsPopUp.tsx | 3 +- .../members/parts/membersListPagination.tsx | 4 +- .../dashboard/members/parts/updatePopUp.tsx | 6 +- .../members/parts/updateSubPopUp.tsx | 6 +- .../dashboard/products/parts/addNewButton.tsx | 4 +- .../products/parts/productsListPagination.tsx | 4 +- .../dashboard/services/parts/addNewButton.tsx | 4 +- .../services/parts/servicesListPagination.tsx | 4 +- webapp/src/components/dashboard/sidebar.tsx | 27 +- .../dashboard/workers/parts/addNewButton.tsx | 4 +- .../workers/parts/servicesListPagination.tsx | 4 +- webapp/src/messages/ar.json | 83 ++- webapp/src/messages/en.json | 16 +- webapp/src/middleware/validateAuthToken.ts | 34 +- webapp/src/redux/features/members-slice.ts | 9 + webapp/src/redux/features/services-slice.ts | 7 + webapp/src/redux/features/settings-slice.ts | 18 +- webapp/src/redux/features/statistics-slice.ts | 96 ++-- webapp/src/redux/provider.tsx | 11 +- webapp/src/redux/store.ts | 82 +-- webapp/tsconfig.json | 1 - 53 files changed, 1094 insertions(+), 1077 deletions(-) create mode 100644 webapp/-.gitignore diff --git a/webapp/-.gitignore b/webapp/-.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/webapp/-.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/webapp/ecosystem.config.js b/webapp/ecosystem.config.js index 72e9ed2..a38cbaf 100644 --- a/webapp/ecosystem.config.js +++ b/webapp/ecosystem.config.js @@ -5,18 +5,18 @@ module.exports = { exec_mode: 'cluster', instances: 1, // Or a number of instances script: 'node_modules/next/dist/bin/next', - args: 'start -p 5000', + args: 'start -p 3000', env_local: { - DB_URI:"mongodb+srv://xxx", - NEXT_PUBLIC_API_BASE:"https://xxx" + DB_URI:"mongodb://root:mongodb_str_p%40ss@localhost:27017/Infinity?authSource=admin", + NEXT_PUBLIC_API_BASE:"http://localhost:3000" }, env_development: { - DB_URI:"mongodb+srv://xxx", - NEXT_PUBLIC_API_BASE:"https://xxx" + DB_URI:"mongodb://root:mongodb_str_p%40ss@localhost:27017/Infinity?authSource=admin", + NEXT_PUBLIC_API_BASE:"http://localhost:3000" }, env_production: { - DB_URI:"mongodb+srv://xxx", - NEXT_PUBLIC_API_BASE:"https://xxx" + DB_URI:"mongodb://root:mongodb_str_p%40ss@localhost:27017/Infinity?authSource=admin", + NEXT_PUBLIC_API_BASE:"http://localhost:3000" } } ] diff --git a/webapp/next.config.js b/webapp/next.config.js index 902fb60..aa397ec 100644 --- a/webapp/next.config.js +++ b/webapp/next.config.js @@ -3,14 +3,9 @@ const nextConfig = { typescript: { ignoreBuildErrors: true, }, - output: 'standalone', - experimental: { - // Disable server-side rendering for API routes during build - // to prevent database connection attempts - serverComponentsExternalPackages: ['mongoose'], + env: { + NEXT_PUBLIC_API_BASE: process.env.NEXT_PUBLIC_API_BASE, }, - // For App Router, use generateStaticParams in your page components - // instead of exportPathMap async redirects() { return [ { // we redirect '/' to '/dashboard @@ -20,10 +15,6 @@ const nextConfig = { }, ] }, - // Skip type checking during build - eslint: { - ignoreDuringBuilds: true, - }, } module.exports = nextConfig diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 7e90efa..f76bd78 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -22,9 +22,9 @@ "@tailwindcss/typography": "^0.5.9", "@tippyjs/react": "^4.2.6", "@types/bcrypt": "^5.0.0", + "@types/node": "20.4.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", - "@types/redux-persist": "^4.0.0", "apexcharts": "^3.42.0", "autoprefixer": "10.4.14", "axios": "^1.4.0", @@ -33,7 +33,6 @@ "eslint-config-next": "^14.0.1", "formik": "^2.4.2", "formik-wizard-form": "^2.1.0", - "lucide-react": "^0.519.0", "mongoose": "^7.4.1", "next": "^13.4.12", "next-intl": "^2.19.0", @@ -54,9 +53,6 @@ "typescript": "5.1.6", "universal-cookie": "^4.0.4", "yup": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^24.0.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1606,13 +1602,9 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } + "version": "20.4.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz", + "integrity": "sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -1666,27 +1658,6 @@ "@types/react": "*" } }, - "node_modules/@types/redux-persist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/redux-persist/-/redux-persist-4.0.0.tgz", - "integrity": "sha512-VyDn75G7+Xkebp5Lxowk2t7HeT4VLSy92nyoQu1dZxk+ySBBdvV0yVR/upE4z24eVFxMp6Fo3CisQpkl4v1k3w==", - "license": "MIT", - "dependencies": { - "redux": "^3.6.0" - } - }, - "node_modules/@types/redux-persist/node_modules/redux": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.2.1", - "lodash-es": "^4.2.1", - "loose-envify": "^1.1.0", - "symbol-observable": "^1.0.3" - } - }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -4680,15 +4651,6 @@ "node": ">=10" } }, - "node_modules/lucide-react": { - "version": "0.519.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.519.0.tgz", - "integrity": "sha512-cLJyjRKBJFzaZ/+1oIeQaH7XUdxKOYU3uANcGSrKdIZWElmNbRAm8RXKiTJS7AWLCBOS8b7A497Al/kCHozd+A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5941,7 +5903,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", - "license": "MIT", "peerDependencies": { "redux": ">4.0.0" } @@ -6731,15 +6692,6 @@ "node": ">= 0.8.0" } }, - "node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", @@ -7052,12 +7004,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT" - }, "node_modules/universal-cookie": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", diff --git a/webapp/package.json b/webapp/package.json index 49f7c1b..8733cff 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -23,9 +23,9 @@ "@tailwindcss/typography": "^0.5.9", "@tippyjs/react": "^4.2.6", "@types/bcrypt": "^5.0.0", + "@types/node": "20.4.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", - "@types/redux-persist": "^4.0.0", "apexcharts": "^3.42.0", "autoprefixer": "10.4.14", "axios": "^1.4.0", @@ -34,7 +34,6 @@ "eslint-config-next": "^14.0.1", "formik": "^2.4.2", "formik-wizard-form": "^2.1.0", - "lucide-react": "^0.519.0", "mongoose": "^7.4.1", "next": "^13.4.12", "next-intl": "^2.19.0", @@ -55,8 +54,5 @@ "typescript": "5.1.6", "universal-cookie": "^4.0.4", "yup": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^24.0.3" } } diff --git a/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/dashboard/(home)/page.tsx b/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/dashboard/(home)/page.tsx index ec695e2..eb0f9b8 100644 --- a/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/dashboard/(home)/page.tsx +++ b/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/dashboard/(home)/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useCallback } from 'react' +import { useEffect } from 'react' import GeneralLook from '@/components/dashboard/home/generalLook' import MembersOverviewChart from '@/components/dashboard/home/membersOverviewChart' import ServicesSubscriptions from '@/components/dashboard/home/servicesSubscriptions' @@ -12,60 +12,41 @@ import { AppDispatch } from '@/redux/store'; import { load } from '@/redux/features/statistics-slice' import CircularProgress from '@mui/material/CircularProgress'; import { useTranslations } from 'next-intl'; -import Button from '@mui/material/Button'; -import { RefreshCw } from 'lucide-react'; export default function Page() { - const dispatch = useDispatch() + + const dispatch = useDispatch() const t = useTranslations('statistics'); const report = useAppSelector((state) => state.statisticsReducer.value.report) + const loadedFirstTime = useAppSelector((state) => state.statisticsReducer.value.loadedFirstTime) const isLoading = useAppSelector((state) => state.statisticsReducer.value.isLoading) - const fetchData = useCallback(async () => { - try { - await dispatch(load({ forceRefresh: true })); - } catch (error) { - console.error('Failed to fetch dashboard data:', error); - } - }, [dispatch]); - useEffect(() => { - // Always fetch fresh data when the component mounts - fetchData(); - - // Set up interval to refresh data every 5 minutes - const intervalId = setInterval(fetchData, 5 * 60 * 1000); - - // Clean up interval on component unmount - return () => clearInterval(intervalId); - }, [fetchData]); + if(loadedFirstTime) return; + if(isLoading) return; + async function a() + { + await dispatch(load()) + } + a() + }, []) - if (isLoading && !report) { + if(isLoading) + { return ( -
- -

{t('loading')}...

-
+ <> +
+ +

{t('loading')}...

+
+ ) } return ( <> -
-
- -
- +
@@ -75,9 +56,9 @@ export default function Page() {
- {/*
+
-
*/} +
diff --git a/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx b/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx index d0f0230..1c342cc 100644 --- a/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx +++ b/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx @@ -10,11 +10,6 @@ import { ReactNode } from 'react' import FullScreenLoader from "@/components/common/fullScreenLoader"; import { load } from '@/redux/features/settings-slice' - -import Cookies from 'universal-cookie'; - -const cookies = new Cookies(); - // HOC for auth pages ( only accept auth user ) export default function LocaleLayout({children} : { children : ReactNode }) { @@ -50,9 +45,6 @@ export default function LocaleLayout({children} : { children : ReactNode }) { // if wasnt logged in this will fire function NotLoggedInCallback() { - console.log('SERVER ::: not logged in') - cookies.remove('authToken', { path: '/' }); - // reset the state to there initial value return router.push(notAuthRedirectPage) } return ( diff --git a/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/layout.tsx b/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/layout.tsx index 6046558..242c79a 100644 --- a/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/layout.tsx +++ b/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/layout.tsx @@ -34,7 +34,6 @@ export default function LocaleLayout({children} : { children : ReactNode }) { // if logged in this will fire function LoggedInCallback() { - console.log('SERVER ::: logged in') return router.push(authRedirectPage) } diff --git a/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/login/page.tsx b/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/login/page.tsx index de0c5af..819c41b 100644 --- a/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/login/page.tsx +++ b/webapp/src/app/[locale]/(GlobalWrapper)/(NotAuth)/login/page.tsx @@ -156,7 +156,7 @@ export default function Index() {
- + diff --git a/webapp/src/app/[locale]/layout.tsx b/webapp/src/app/[locale]/layout.tsx index fe2c9a6..815546f 100644 --- a/webapp/src/app/[locale]/layout.tsx +++ b/webapp/src/app/[locale]/layout.tsx @@ -25,8 +25,10 @@ export default async function RootLayout({children, params: {locale}} : {childre return ( - + + + {children} diff --git a/webapp/src/app/api/auth/route.ts b/webapp/src/app/api/auth/route.ts index c4c435d..132f739 100644 --- a/webapp/src/app/api/auth/route.ts +++ b/webapp/src/app/api/auth/route.ts @@ -65,7 +65,7 @@ export async function POST(req : Request) $set: { "token" : { value: authToken, - expiresAt: Math.floor(Date.now() / 1000) + 30 * 60 * 60, + expiresAt: Math.floor(Date.now() / 1000) + 3 * 60 * 60, } } }) @@ -100,37 +100,33 @@ export async function POST(req : Request) // revalidate every 10 seconds export const revalidate = 10; + + // handle the check authToken validation export async function GET(req: Request) { try { - // connect to the database first - await dbConnect(); - + + await dbConnect(); // Add await here, dbConnect might be async // get the request params const { searchParams } = new URL(req.url) const authToken = searchParams.get('authToken') // trow error if invalid auth token - if(!authToken) - { - throw { - specialError : 'invalidRequest' - } - } - else if(authToken == 'null') + if(!authToken || authToken == 'null') { throw { specialError : 'invalidRequest' } } + // if there was a valid authToken cookie else { // we try to get the user by the authToken let userDoc = await userModel.findOne({'token.value': authToken}) // if the authToken is invalid we go return - if(!userDoc) + if(!userDoc || !userDoc.token ) { throw { specialError : 'invalidToken' @@ -138,9 +134,8 @@ export async function GET(req: Request) } // if the authToken if valid we check if it expired or not yet // we throw error if expired - if(userDoc.token.expiresAt < (Date.now() / 1000)) //1000 for 3 hours + if(userDoc.token.expiresAt < (Date.now() / 1000)) { - console.log('SERVER ::: token expired') throw { specialError : 'expiredToken' } @@ -168,6 +163,7 @@ export async function GET(req: Request) } else { + console.error("Unhandled error in auth GET API:", e); return NextResponse.json({ success: false, message: "serverError", diff --git a/webapp/src/app/api/user/actions/members/route.ts b/webapp/src/app/api/user/actions/members/route.ts index 8446dfe..d710bb9 100644 --- a/webapp/src/app/api/user/actions/members/route.ts +++ b/webapp/src/app/api/user/actions/members/route.ts @@ -125,7 +125,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) * 20, 20]; // get the docs const docs = await memberModel.find({}).skip(range[0]).limit(range[1]), // get the size of the docs @@ -193,7 +193,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) * 20, 20]; // delete the doc await memberModel.findByIdAndRemove(_id) // get the docs by page diff --git a/webapp/src/app/api/user/actions/statistics/route.ts b/webapp/src/app/api/user/actions/statistics/route.ts index e35e4b4..c373913 100644 --- a/webapp/src/app/api/user/actions/statistics/route.ts +++ b/webapp/src/app/api/user/actions/statistics/route.ts @@ -10,10 +10,355 @@ import { IStatisticsSchema } from '@/types/IDB' // set the revalidate variable export const revalidate = 5; + +/** + * Updates the members general overview statistics for the current week, month, and year in the database. + * This function calculates total members, active/unactive subscriptions, and gender-based counts + * for each day of the current week, aggregates them into weeks for the current month, + * and then into months for the current year, storing it in the statisticsModel. + */ +async function updateMembersOverviewStatistics() { + try { + await dbConnect(); // Ensure DB connection + + // Define week days in order, starting from Saturday as per frontend + const weekDaysOrder = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri']; + + const now = new Date(); + // Set to the start of today (local time) + now.setHours(0, 0, 0, 0); + + const currentUnix = Math.floor(new Date().getTime() / 1000); + + // --- Calculate thisWeek data --- + let currentDayOfWeek = now.getDay(); // 0 (Sunday) to 6 (Saturday) + let daysToSubtract; + if (currentDayOfWeek === 6) { // If today is Saturday + daysToSubtract = 0; + } else { // For any other day, calculate how many days back to last Saturday + daysToSubtract = (currentDayOfWeek + 1); + } + + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - daysToSubtract); + startOfWeek.setHours(0, 0, 0, 0); // Ensure it's the very start of Saturday + + let thisWeekDaysData: { [key: string]: { [key: string]: number } } = {}; + + for (let i = 0; i < 7; i++) { + const day = new Date(startOfWeek); + day.setDate(startOfWeek.getDate() + i); + + const endOfDay = new Date(day); + endOfDay.setDate(day.getDate() + 1); // Move to the next day + const endOfDayUnix = Math.floor(endOfDay.getTime() / 1000); + + // Calculate cumulative totals up to this day (inclusive) + const totalMembers = await memberModel.countDocuments({ + registerAt_unix: { $lt: endOfDayUnix } + }); + + const totalActiveSubs = await memberModel.countDocuments({ + planExpAt_unix: { $gt: currentUnix }, + registerAt_unix: { $lt: endOfDayUnix } + }); + + const totalUnActiveSubs = await memberModel.countDocuments({ + planExpAt_unix: { $lte: currentUnix }, + registerAt_unix: { $lt: endOfDayUnix } + }); + + const totalMansMembers = await memberModel.countDocuments({ + gendre: 'm', + registerAt_unix: { $lt: endOfDayUnix } + }); + + const totalGirlsMembers = await memberModel.countDocuments({ + gendre: 'w', + registerAt_unix: { $lt: endOfDayUnix } + }); + + thisWeekDaysData[weekDaysOrder[i]] = { + totalMembers, + totalActiveSubs, + totalUnActiveSubs, + totalMansMembers, + totalGirlsMembers, + }; + } + + const thisWeekData = { + year: startOfWeek.getFullYear(), + month: startOfWeek.getMonth(), // 0-indexed month + week: Math.ceil(startOfWeek.getDate() / 7), // Simplified week number + days: thisWeekDaysData + }; + + // --- Calculate thisMonth data --- + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + startOfMonth.setHours(0, 0, 0, 0); + + let thisMonthWeeksData = []; + let tempDate = new Date(startOfMonth); + let weekCounter = 1; + + while (tempDate.getMonth() === now.getMonth()) { + const startOfWeekInMonth = new Date(tempDate); + startOfWeekInMonth.setDate(tempDate.getDate() - ((tempDate.getDay() + 1) % 7)); // Back to last Saturday + startOfWeekInMonth.setHours(0, 0, 0, 0); + + if (startOfWeekInMonth.getMonth() !== now.getMonth() && startOfWeekInMonth.getTime() < startOfMonth.getTime()) { + startOfWeekInMonth.setTime(startOfMonth.getTime()); + } + + let weekSum = { + totalMembers: 0, + totalActiveSubs: 0, + totalUnActiveSubs: 0, + totalMansMembers: 0, + totalGirlsMembers: 0 + }; + let daysInWeek = {}; + let lastDayOfWeek = null; + + for (let i = 0; i < 7; i++) { + const dayInWeek = new Date(startOfWeekInMonth); + dayInWeek.setDate(startOfWeekInMonth.getDate() + i); + + // Only consider days within the current month + if (dayInWeek.getMonth() === now.getMonth()) { + const endOfDay = new Date(dayInWeek); + endOfDay.setDate(dayInWeek.getDate() + 1); + const endOfDayUnix = Math.floor(endOfDay.getTime() / 1000); + + // Calculate cumulative totals up to this day (inclusive) + const totalMembers = await memberModel.countDocuments({ + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalActiveSubs = await memberModel.countDocuments({ + planExpAt_unix: { $gt: currentUnix }, + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalUnActiveSubs = await memberModel.countDocuments({ + planExpAt_unix: { $lte: currentUnix }, + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalMansMembers = await memberModel.countDocuments({ + gendre: 'm', + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalGirlsMembers = await memberModel.countDocuments({ + gendre: 'w', + registerAt_unix: { $lt: endOfDayUnix } + }); + + daysInWeek[weekDaysOrder[i]] = { + totalMembers, + totalActiveSubs, + totalUnActiveSubs, + totalMansMembers, + totalGirlsMembers, + }; + + // Keep track of the last day's totals for the week sum + lastDayOfWeek = { + totalMembers, + totalActiveSubs, + totalUnActiveSubs, + totalMansMembers, + totalGirlsMembers, + }; + } + } + + // Set week sum to the cumulative total at the end of the week + if (lastDayOfWeek) { + weekSum = lastDayOfWeek; + } + + thisMonthWeeksData.push({ + week: weekCounter, + days: daysInWeek, + thisWeekSum: weekSum // This matches the frontend's expected structure for thisMonthTotalSum + }); + + tempDate.setDate(tempDate.getDate() + 7); // Move to the next week + weekCounter++; + } + + const thisMonthData = { + year: now.getFullYear(), + month: now.getMonth(), + weeks: thisMonthWeeksData + }; + + // --- Calculate thisYear data --- + let thisYearMonthsDataForFrontendAndDB = []; // This will contain months with full weeks data and total sum + + for (let month = 0; month < 12; month++) { + const startOfMonthOfYear = new Date(now.getFullYear(), month, 1); + startOfMonthOfYear.setHours(0, 0, 0, 0); + + let monthlyWeeksData = []; // Weeks data for this specific month + let tempMonthDateForYear = new Date(startOfMonthOfYear); + let monthWeekCounterForYear = 1; + + let totalMonthSum = { // Sum for the entire month for the year view + totalMembers: 0, totalActiveSubs: 0, totalUnActiveSubs: 0, + totalMansMembers: 0, totalGirlsMembers: 0 + }; + + while (tempMonthDateForYear.getMonth() === month) { + const startOfWeekInMonthForYear = new Date(tempMonthDateForYear); + startOfWeekInMonthForYear.setDate(tempMonthDateForYear.getDate() - ((tempMonthDateForYear.getDay() + 1) % 7)); + startOfWeekInMonthForYear.setHours(0, 0, 0, 0); + + if (startOfWeekInMonthForYear.getMonth() !== month && startOfWeekInMonthForYear.getTime() < startOfMonthOfYear.getTime()) { + startOfWeekInMonthForYear.setTime(startOfMonthOfYear.getTime()); + } + + let daysInMonthlyWeekForYear = {}; + let currentWeeklySumForYear = { // Sum for the current week within the month + totalMembers: 0, totalActiveSubs: 0, totalUnActiveSubs: 0, + totalMansMembers: 0, totalGirlsMembers: 0 + }; + + for (let i = 0; i < 7; i++) { + const dayInMonthlyWeekForYear = new Date(startOfWeekInMonthForYear); + dayInMonthlyWeekForYear.setDate(startOfWeekInMonthForYear.getDate() + i); + + if (dayInMonthlyWeekForYear.getMonth() === month) { + const endOfDay = new Date(dayInMonthlyWeekForYear); + endOfDay.setDate(dayInMonthlyWeekForYear.getDate() + 1); + const endOfDayUnix = Math.floor(endOfDay.getTime() / 1000); + + // Calculate cumulative totals up to this day (inclusive) + const totalMembers = await memberModel.countDocuments({ + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalActiveSubs = await memberModel.countDocuments({ + planExpAt_unix: { $gt: currentUnix }, + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalUnActiveSubs = await memberModel.countDocuments({ + planExpAt_unix: { $lte: currentUnix }, + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalMansMembers = await memberModel.countDocuments({ + gendre: 'm', + registerAt_unix: { $lt: endOfDayUnix } + }); + const totalGirlsMembers = await memberModel.countDocuments({ + gendre: 'w', + registerAt_unix: { $lt: endOfDayUnix } + }); + + daysInMonthlyWeekForYear[weekDaysOrder[i]] = { + totalMembers, + totalActiveSubs, + totalUnActiveSubs, + totalMansMembers, + totalGirlsMembers, + }; + + // Keep the last day's totals as the week sum (cumulative total at end of week) + currentWeeklySumForYear = { + totalMembers, + totalActiveSubs, + totalUnActiveSubs, + totalMansMembers, + totalGirlsMembers, + }; + } + } + + monthlyWeeksData.push({ + week: monthWeekCounterForYear, + days: daysInMonthlyWeekForYear, + thisWeekSum: currentWeeklySumForYear // Sum for this specific week + }); + + // Keep the last week's totals as the month sum (cumulative total at end of month) + totalMonthSum = { + totalMembers: currentWeeklySumForYear.totalMembers, + totalActiveSubs: currentWeeklySumForYear.totalActiveSubs, + totalUnActiveSubs: currentWeeklySumForYear.totalUnActiveSubs, + totalMansMembers: currentWeeklySumForYear.totalMansMembers, + totalGirlsMembers: currentWeeklySumForYear.totalGirlsMembers, + }; + + tempMonthDateForYear.setDate(tempMonthDateForYear.getDate() + 7); + monthWeekCounterForYear++; + } + + thisYearMonthsDataForFrontendAndDB.push({ + month: month, + weeks: monthlyWeeksData, // This is crucial for frontend's thisYear.months[x].weeks + thisMonthWeeksSum: totalMonthSum // The rolled-up sum for the month + }); + } + + const thisYearData = { + year: now.getFullYear(), + months: thisYearMonthsDataForFrontendAndDB // Both frontend and DB will use this detailed structure + }; + + // --- Update the statistics document --- + let statistics = await statisticsModel.findOne({}); + + if (!statistics) { + statistics = new statisticsModel({ + listsTracking: { + thisWeek: thisWeekData, + thisMonth: thisMonthData, + thisYear: thisYearData, // Use the detailed data for thisYear + months: [thisMonthData], // Store current month's full data in array + years: [thisYearData] // Store current year's full detailed data in array + } + }); + await statistics.save(); + } else { + // Update thisWeek, thisMonth, thisYear directly (for current period display) + statistics.listsTracking.thisWeek = thisWeekData; + statistics.listsTracking.thisMonth = thisMonthData; + statistics.listsTracking.thisYear = thisYearData; // Update with the detailed data + + // Update or add current month in listsTracking.months array (for historical list) + const currentMonthIndex = statistics.listsTracking.months.findIndex(m => m.year === thisMonthData.year && m.month === thisMonthData.month); + if (currentMonthIndex !== -1) { + statistics.listsTracking.months[currentMonthIndex] = thisMonthData; + } else { + // To avoid too many empty entries if listsTracking.months is for *all* months in current year + // and not just a historical log. If it's a fixed size array (12 months), + // we can update by index. If it's a dynamic historical log, push is fine. + // Assuming it's for current year's months, better to update by index or push if month is not there. + // For simplicity and matching current structure, push if not found. + statistics.listsTracking.months.push(thisMonthData); + } + + // Update or add current year in listsTracking.years array (for historical list) + const currentYearIndex = statistics.listsTracking.years.findIndex(y => y.year === thisYearData.year); + if (currentYearIndex !== -1) { + statistics.listsTracking.years[currentYearIndex] = thisYearData; // Update with full detailed data + } else { + statistics.listsTracking.years.push(thisYearData); // Push full detailed data + } + + await statistics.save(); + } + + console.log("Members overview statistics updated successfully for this week, month, and year."); + + } catch (error) { + console.error("Error updating members overview statistics:", error); + } +} + // GET METHOD export async function GET(req:Request) { try{ + await updateMembersOverviewStatistics(); // Call the updater function first // connect to the db dbConnect(); // get needed docs @@ -25,8 +370,7 @@ export async function GET(req:Request) let expiredSubscriptionsCount : number = await memberModel.count({planExpAt_unix: {$lte : Math.floor(new Date().getTime() / 1000)}}) let servicesCount : number = await servicesModel.count({}) let activeServicesCount : number = await servicesModel.count({status: "active"}) - let disActiveServicesCount : number = servicesCount - activeServicesCount //await servicesModel.count({status: "inactive"}) - + let disActiveServicesCount : number = await servicesModel.count({status: "active"}) let servicesNameAndSubscribers : { _id: Types.ObjectId, diff --git a/webapp/src/app/api/user/actions/workers/route.ts b/webapp/src/app/api/user/actions/workers/route.ts index b2aed8f..0b1076b 100644 --- a/webapp/src/app/api/user/actions/workers/route.ts +++ b/webapp/src/app/api/user/actions/workers/route.ts @@ -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 workerModel.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 workerModel.findByIdAndRemove(_id) // get the docs by page diff --git a/webapp/src/components/dashboard/equipments/parts/addNewButton.tsx b/webapp/src/components/dashboard/equipments/parts/addNewButton.tsx index 047ce7f..2114fc1 100644 --- a/webapp/src/components/dashboard/equipments/parts/addNewButton.tsx +++ b/webapp/src/components/dashboard/equipments/parts/addNewButton.tsx @@ -22,8 +22,8 @@ export default function AddNewButton() flex justify-between items-center gap-3 "> - - + +

{t('addNewEquipment')}

diff --git a/webapp/src/components/dashboard/equipments/parts/addPopUp.tsx b/webapp/src/components/dashboard/equipments/parts/addPopUp.tsx index 469d960..d6dc620 100644 --- a/webapp/src/components/dashboard/equipments/parts/addPopUp.tsx +++ b/webapp/src/components/dashboard/equipments/parts/addPopUp.tsx @@ -63,7 +63,9 @@ export default function AddPopUp() // This is the title of the pop up } -

{t('addNewEquipment')}

+ +

{t('addNewEquipment')}

+ handleClose()} className="fill-primary-dark dark:fill-primary cursor-pointer"> diff --git a/webapp/src/components/dashboard/equipments/parts/equipmentsListPagination.tsx b/webapp/src/components/dashboard/equipments/parts/equipmentsListPagination.tsx index 3f6e15b..a09e4aa 100644 --- a/webapp/src/components/dashboard/equipments/parts/equipmentsListPagination.tsx +++ b/webapp/src/components/dashboard/equipments/parts/equipmentsListPagination.tsx @@ -18,11 +18,11 @@ export default function ListPagination() { // show the pagination ui return ( <> - {Math.ceil(total / 4) > 1 ? ( + {Math.ceil(total / 10) > 1 ? (
- - + +

{t('addNewExpense')}

diff --git a/webapp/src/components/dashboard/expenses/parts/addPopUp.tsx b/webapp/src/components/dashboard/expenses/parts/addPopUp.tsx index a431179..a5519fc 100644 --- a/webapp/src/components/dashboard/expenses/parts/addPopUp.tsx +++ b/webapp/src/components/dashboard/expenses/parts/addPopUp.tsx @@ -63,7 +63,8 @@ export default function AddPopUp() // This is the title of the pop up } -

{t('addNewExpense')}

+

{t('addNewExpense')}

+ handleClose()} className="fill-primary-dark dark:fill-primary cursor-pointer"> diff --git a/webapp/src/components/dashboard/expenses/parts/expensesListPagination.tsx b/webapp/src/components/dashboard/expenses/parts/expensesListPagination.tsx index ab61933..c2ef4ba 100644 --- a/webapp/src/components/dashboard/expenses/parts/expensesListPagination.tsx +++ b/webapp/src/components/dashboard/expenses/parts/expensesListPagination.tsx @@ -18,11 +18,11 @@ export default function ListPagination() { // show the pagination ui return ( <> - {Math.ceil(total / 4) > 1 ? ( + {Math.ceil(total / 10) > 1 ? (
- - + +
diff --git a/webapp/src/components/dashboard/home/generalLook.tsx b/webapp/src/components/dashboard/home/generalLook.tsx index 60a6b49..58551ee 100644 --- a/webapp/src/components/dashboard/home/generalLook.tsx +++ b/webapp/src/components/dashboard/home/generalLook.tsx @@ -1,174 +1,125 @@ import { useTranslations } from 'next-intl'; import { useAppSelector } from '@/redux/store'; +import { useEffect } from 'react'; +import { constants } from 'node:buffer'; -export default function GeneralLook() { +export default function GeneralLook() +{ const t = useTranslations('statistics'); const report = useAppSelector((state) => state.statisticsReducer.value.report) - if (!report) { - return ( -
-
-
-
-
- ); - } - - const stats = [ - { - icon: 'members', - value: report?.membersCount?.value ?? 0, - label: t('totalMembers'), - bgColor: 'bg-info' - }, - { - icon: 'activeMembers', - value: report?.activeMembersCount?.value ?? 0, - label: t('totalActiveMembers'), - bgColor: 'bg-success' - }, - { - icon: 'disActiveMembers', - value: report?.disActiveMembersCount?.value ?? 0, - label: t('totalUnActiveMembers'), - bgColor: 'bg-error' - } - ]; - - - const stats2 = [ - { - icon: 'services', - value: report?.servicesCount?.value ?? 0, - label: t('totalServices'), - bgColor: 'bg-info' - }, - { - icon: 'activeServices', - value: report?.activeServicesCount?.value ?? 0, - label: t('totalActiveServices'), - bgColor: 'bg-success' - }, - { - icon: 'disActiveServices', - value: report?.disActiveServicesCount?.value ?? 0, - label: t('totalUnActiveServices'), - bgColor: 'bg-error' - } - ]; - - - - - const stats3 = [ - { - icon: 'activeSubscriptions', - value: report?.activeSubscriptionsCount?.value ?? 0, - label: t('totalActiveSubscriptions'), - bgColor: 'bg-info' - }, - { - icon: 'expiredSoonSubscriptions', - value: report?.expiredSoonSubscriptionsCount?.value ?? 0, - label: t('expiredSoonSubscriptionsCount'), - bgColor: 'bg-warning' - }, - { - icon: 'expiredSubscriptions', - value: report?.expiredSubscriptionsCount?.value ?? 0, - label: t('totalExpiredSubscriptions'), - bgColor: 'bg-error' - } - ]; - return ( -
- - - {/* {stats.map((stat, index) => ( -
- + <> +
+
- + - + -

{stat.value}

-

{stat.label}

+

{report?.membersCount.value}

+

{t('totalMembers')}

+
+
+
+ + + + + + +

{report?.activeMembersCount.value}

+

{t('totalActiveMembers')}

+
+
+
+ + + + + + +

{report?.disActiveMembersCount.value}

+

{t('totalUnActiveMembers')}

-
- ))} */} - - -
- - {stats.map((stat, index) => ( - -
- - - - - - -

{stat.value}

-

{stat.label}

-
-
- ))} - -
- - -
- - {stats2.map((stat, index) => ( - -
- - +
+
+ + -

{stat.value}

-

{stat.label}

+

{report?.servicesCount.value}

+

{t('totalServices')}

- ))} - -
- - -
- - {stats3.map((stat, index) => ( - -
- - +
+ + + + + + +

{report?.activeServicesCount.value}

+

{t('totalActiveServices')}

+
+
+
+ + + + + + +

{ ((report?.servicesCount?.value ?? 0) - (report?.activeServicesCount?.value ?? 0))}

+

{t('totalUnActiveServices')}

+
+
+
+
+
+ + -

{stat.value}

-

{stat.label}

+

{report?.activeSubscriptionsCount.value}

+

{t('totalActiveSubscriptions')}

- ))} +
+ + + + + + +

{report?.expiredSoonSubscriptionsCount.value}

+

{t('expiredSoonSubscriptionsCount')}

+
+
+
+ + + + + + +

{report?.expiredSubscriptionsCount.value}

+

{t('totalExpiredSubscriptions')}

+
+
+
- - - - -
+ ) } \ No newline at end of file diff --git a/webapp/src/components/dashboard/home/incomeOutcome.tsx b/webapp/src/components/dashboard/home/incomeOutcome.tsx index 01a5842..658d6fe 100644 --- a/webapp/src/components/dashboard/home/incomeOutcome.tsx +++ b/webapp/src/components/dashboard/home/incomeOutcome.tsx @@ -2,7 +2,6 @@ import { useAppSelector } from '@/redux/store'; import { useTranslations } from 'next-intl'; import dynamic from 'next/dynamic'; import Cookies from 'universal-cookie'; -import { useEffect, useState } from 'react'; const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false, }); @@ -10,66 +9,45 @@ const ReactApexChart = dynamic(() => import('react-apexcharts'), { export default function IncomeOutcome() { // get needed redux state - const report = useAppSelector((state) => state.statisticsReducer.value.report); - const settings = useAppSelector((state) => state.settingsReducer.value); - const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType); - const [isClient, setIsClient] = useState(false); - + 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'; - const isDark = themeType === 'dark'; - - // Set isClient to true after component mounts - useEffect(() => { - setIsClient(true); - }, []); - - // if loading or no report or settings not loaded, show loading spinner - if (!report || !isClient || !settings?.appGeneralSettings) { - return ( -
-
-
- ); - } - - // Get currency symbol with fallback to '$' if not available - const currencySymbol = settings.appGeneralSettings.currencySymbol || '$'; - + 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 null // Return null or a loading indicator if report is not yet available // prepare chart data - const data = [report?.totalIncome || 0, report?.totalOutcome || 0]; + // Ensure totalIncome and totalOutcome are numbers, default to 0 if undefined/null + let data = [report?.totalIncome ?? 0 , report?.totalOutcome ?? 0] // prepare chart labels - const labels = [t('income'), t('outcome')]; - + let labels = [t('income') , t('outcome')] // prepare chart options - const options = { - series: [{ + var options = { + series: [{ name: currencySymbol, data: data }], - annotations: { + annotations: { points: [{ - x: t('income'), - seriesIndex: 0, - label: { - borderColor: '#775DD0', - offsetY: 0, - style: { - color: '#fff', - background: '#775DD0', - }, - } + x: t('incomes'), + seriesIndex: 0, + label: { + borderColor: '#775DD0', + offsetY: 0, + style: { + color: '#fff', + background: '#775DD0', + }, + } }] }, chart: { height: 350, type: 'bar', - toolbar: { - show: false - } }, plotOptions: { bar: { @@ -92,7 +70,6 @@ export default function IncomeOutcome() horizontal: 10, vertical: 5, }, - show: false }, fill: { type: isDark ? '' : 'gradient', @@ -104,7 +81,7 @@ export default function IncomeOutcome() stops: isDark ? [100, 100] : [45, 100], }, }, - colors: ['#0263FF', '#E3342F'], + colors: ['#0263FF' , '#E3342F'], dataLabels: { enabled: false }, @@ -114,7 +91,7 @@ export default function IncomeOutcome() }, grid: { row: { - colors: [isDark ? '#333' : '#fff', '#f2f2f2'] + colors: [isDark ? '#333' : '#fff', '#f2f2f2'] // Adjust the grid row color for dark mode }, }, xaxis: { @@ -124,68 +101,39 @@ export default function IncomeOutcome() offsetY: 5, style: { colors: isDark ? '#fff' : '#000', + fontSize: '12px', + cssClass: 'apexcharts-xaxis-title', }, - formatter: function(value: string) { - return value; - } }, categories: labels, - axisBorder: { - show: false - }, - axisTicks: { - show: false - }, + tickPlacement: 'on' }, yaxis: { + tickAmount: 7, labels: { + offsetX: isRtl ? -30 : -10, + offsetY: 0, style: { colors: isDark ? '#fff' : '#000', - } - } + fontSize: '12px', + cssClass: 'apexcharts-yaxis-title', + }, + }, }, tooltip: { theme: isDark ? 'dark' : 'light', - y: { - formatter: function(val: number) { - return `${currencySymbol} ${val}`; - } - } + x: { + show: true, + }, }, - responsive: [{ - breakpoint: 480, - options: { - legend: { - position: 'bottom', - offsetY: 10, - horizontalAlign: 'center', - fontSize: '12px', - markers: { - width: 10, - height: 10, - offsetX: isRtl ? 5 : -5, - }, - itemMargin: { - horizontal: 10, - vertical: 5, - }, - }, - } - }] }; - + // return the ui return ( -
-

{t('incomeOutcomeGeneralOverview')}

-
- + <> +
+

{t('incomeOutcomeGeneralOverview')}

+
-
- ); + + ) } \ No newline at end of file diff --git a/webapp/src/components/dashboard/home/membersOverviewChart.tsx b/webapp/src/components/dashboard/home/membersOverviewChart.tsx index 18137a3..69334db 100644 --- a/webapp/src/components/dashboard/home/membersOverviewChart.tsx +++ b/webapp/src/components/dashboard/home/membersOverviewChart.tsx @@ -6,31 +6,52 @@ import Cookies from 'universal-cookie'; const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false, }); -import { useDispatch } from 'react-redux'; -import { AppDispatch } from '@/redux/store'; +import { useAppDispatch } from '@/redux/store'; import { setCurrentMembersGeneralOverviewDuration } from '@/redux/features/statistics-slice' -export default function MembersOverviewChart() { +export default function MembersOverviewChart() +{ // get needed redux state const report = useAppSelector((state) => state.statisticsReducer.value.report) const currentMembersGeneralOverviewDuration = useAppSelector((state) => state.statisticsReducer.value.currentMembersGeneralOverviewDuration) const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType) // declare global variables - const dispatch = useDispatch() + const dispatch = useAppDispatch() 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 - const [chartSeries, setChartSeries] = useState<{}[]>([]); - const [labels, setLabels] = useState([]) + const [chartSeries , setChartSeries] = useState<{}[]>([]); + const [labels , setLabels] = useState([]) // revenue Chart // prepare chart series and handle the transaction between currentMembersGeneralOverviewDuration useEffect(() => { + + + // Optional: Re-add a robust check for initial report data, if needed as a fallback. + // For now, let's focus on debugging the data flow within the existing logic. + if (!report || !report.membersGeneralOverview || !report.membersGeneralOverview.value) { + //console.log("MembersOverviewChart - Report or general overview data is not available. Setting default."); + setChartSeries([ + { name: t('totalMembers'), data: [0,0,0,0,0,0,0] }, + { name: t('totalActiveSubs'), data: [0,0,0,0,0,0,0] }, + { name: t('totalUnActiveSubs'), data: [0,0,0,0,0,0,0] }, + { name: t('totalMansMembers'), data: [0,0,0,0,0,0,0] }, + { name: t('totalGirlsMembers'), data: [0,0,0,0,0,0,0] }, + ]); + setLabels([t('sat') , t('sun') , t('mon') , t('tue') , t('wed') , t('thu') , t('fri')]); + return; + } + + // if chart duration is set to this week - if (currentMembersGeneralOverviewDuration == 'thisWeek') { + if(currentMembersGeneralOverviewDuration == 'thisWeek') + { // set the labels - setLabels([t('sat'), t('sun'), t('mon'), t('tue'), t('wed'), t('thu'), t('fri')]) + const newLabelsThisWeek = [t('sat') , t('sun') , t('mon') , t('tue') , t('wed') , t('thu') , t('fri')]; + setLabels(newLabelsThisWeek); + // init this weekdata let data_thisWeek = { 'sat': { @@ -84,57 +105,96 @@ export default function MembersOverviewChart() { } } // if there is a data from the backend we overwrite the currentData - // if(report?.membersGeneralOverview?.value.thisWeek.days) { - // data_thisWeek = report?.membersGeneralOverview?.value.thisWeek.days; - // } - - // To this: - console.log( report?.membersGeneralOverview?.value?.thisWeek?.days) - if (report?.membersGeneralOverview?.value?.thisWeek?.days) { + if(report.membersGeneralOverview.value.thisWeek?.days) { data_thisWeek = report.membersGeneralOverview.value.thisWeek.days; } + // prepare the chart series - setChartSeries([ + const newChartSeriesThisWeek = [ { name: t('totalMembers'), - data: [data_thisWeek['sat']['totalMembers'], data_thisWeek['sun']['totalMembers'], data_thisWeek['mon']['totalMembers'], data_thisWeek['tue']['totalMembers'], data_thisWeek['wed']['totalMembers'], data_thisWeek['thu']['totalMembers'], data_thisWeek['fri']['totalMembers']], + data: [ + data_thisWeek['sat']?.['totalMembers'] || 0, + data_thisWeek['sun']?.['totalMembers'] || 0, + data_thisWeek['mon']?.['totalMembers'] || 0, + data_thisWeek['tue']?.['totalMembers'] || 0, + data_thisWeek['wed']?.['totalMembers'] || 0, + data_thisWeek['thu']?.['totalMembers'] || 0, + data_thisWeek['fri']?.['totalMembers'] || 0 + ], }, { name: t('totalActiveSubs'), - data: [data_thisWeek['sat']['totalActiveSubs'], data_thisWeek['sun']['totalActiveSubs'], data_thisWeek['mon']['totalActiveSubs'], data_thisWeek['tue']['totalActiveSubs'], data_thisWeek['wed']['totalActiveSubs'], data_thisWeek['thu']['totalActiveSubs'], data_thisWeek['fri']['totalActiveSubs']], + data: [ + data_thisWeek['sat']?.['totalActiveSubs'] || 0, + data_thisWeek['sun']?.['totalActiveSubs'] || 0, + data_thisWeek['mon']?.['totalActiveSubs'] || 0, + data_thisWeek['tue']?.['totalActiveSubs'] || 0, + data_thisWeek['wed']?.['totalActiveSubs'] || 0, + data_thisWeek['thu']?.['totalActiveSubs'] || 0, + data_thisWeek['fri']?.['totalActiveSubs'] || 0 + ], }, { name: t('totalUnActiveSubs'), - data: [data_thisWeek['sat']['totalUnActiveSubs'], data_thisWeek['sun']['totalUnActiveSubs'], data_thisWeek['mon']['totalUnActiveSubs'], data_thisWeek['tue']['totalUnActiveSubs'], data_thisWeek['wed']['totalUnActiveSubs'], data_thisWeek['thu']['totalUnActiveSubs'], data_thisWeek['fri']['totalUnActiveSubs']], + data: [ + data_thisWeek['sat']?.['totalUnActiveSubs'] || 0, + data_thisWeek['sun']?.['totalUnActiveSubs'] || 0, + data_thisWeek['mon']?.['totalUnActiveSubs'] || 0, + data_thisWeek['tue']?.['totalUnActiveSubs'] || 0, + data_thisWeek['wed']?.['totalUnActiveSubs'] || 0, + data_thisWeek['thu']?.['totalUnActiveSubs'] || 0, + data_thisWeek['fri']?.['totalUnActiveSubs'] || 0 + ], }, { name: t('totalMansMembers'), - data: [data_thisWeek['sat']['totalMansMembers'], data_thisWeek['sun']['totalMansMembers'], data_thisWeek['mon']['totalMansMembers'], data_thisWeek['tue']['totalMansMembers'], data_thisWeek['wed']['totalMansMembers'], data_thisWeek['thu']['totalMansMembers'], data_thisWeek['fri']['totalMansMembers']], + data: [ + data_thisWeek['sat']?.['totalMansMembers'] || 0, + data_thisWeek['sun']?.['totalMansMembers'] || 0, + data_thisWeek['mon']?.['totalMansMembers'] || 0, + data_thisWeek['tue']?.['totalMansMembers'] || 0, + data_thisWeek['wed']?.['totalMansMembers'] || 0, + data_thisWeek['thu']?.['totalMansMembers'] || 0, + data_thisWeek['fri']?.['totalMansMembers'] || 0 + ], }, { name: t('totalGirlsMembers'), - data: [data_thisWeek['sat']['totalGirlsMembers'], data_thisWeek['sun']['totalGirlsMembers'], data_thisWeek['mon']['totalGirlsMembers'], data_thisWeek['tue']['totalGirlsMembers'], data_thisWeek['wed']['totalGirlsMembers'], data_thisWeek['thu']['totalGirlsMembers'], data_thisWeek['fri']['totalGirlsMembers']], + data: [ + data_thisWeek['sat']?.['totalGirlsMembers'] || 0, + data_thisWeek['sun']?.['totalGirlsMembers'] || 0, + data_thisWeek['mon']?.['totalGirlsMembers'] || 0, + data_thisWeek['tue']?.['totalGirlsMembers'] || 0, + data_thisWeek['wed']?.['totalGirlsMembers'] || 0, + data_thisWeek['thu']?.['totalGirlsMembers'] || 0, + data_thisWeek['fri']?.['totalGirlsMembers'] || 0 + ], }, - ]) + ]; + + setChartSeries(newChartSeriesThisWeek); } // if chart duration is set to this month - else if (currentMembersGeneralOverviewDuration == 'thisMonth') { - let thisMonthTotalSum: { - week: number, - thisWeekSum: { + else if(currentMembersGeneralOverviewDuration == 'thisMonth') + { + let thisMonthTotalSum : { + week : number, + thisWeekSum : { "totalMembers": number, "totalActiveSubs": number, "totalUnActiveSubs": number, "totalMansMembers": number, "totalGirlsMembers": number } - }[]; - if (report?.membersGeneralOverview?.value.thisMonth) { + }[] = []; + if(report.membersGeneralOverview.value.thisMonth) + { // if we founded a data for the current month - thisMonthTotalSum = report?.membersGeneralOverview?.value.thisMonth.weeks.map((v: any, i: number): { - week: number, - thisWeekSum: { + thisMonthTotalSum = (report.membersGeneralOverview.value.thisMonth.weeks || []).map((v : any , i : number) : { + week : number, + thisWeekSum : { "totalMembers": number, "totalActiveSubs": number, "totalUnActiveSubs": number, @@ -144,118 +204,61 @@ export default function MembersOverviewChart() { } => { // we init the this week sum let thisWeekSum = { - "totalMembers": 0, - "totalActiveSubs": 0, - "totalUnActiveSubs": 0, - "totalMansMembers": 0, - "totalGirlsMembers": 0 - } - // we map through this month weeks - v.days.map((w: { - 'sat': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'sun': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'mon': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'tue': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'wed': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'thu': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'fri': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, + "totalMembers": 0, + "totalActiveSubs": 0, + "totalUnActiveSubs": 0, + "totalMansMembers": 0, + "totalGirlsMembers": 0 } - }, i: number) => { - // we loop through week days and we sum there data - Object.keys(w).forEach(function (key: "sat" | "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "_id", index: number) { - if (key == '_id') return - thisWeekSum.totalMembers = thisWeekSum.totalMembers + w[key].totalMembers - thisWeekSum.totalActiveSubs = thisWeekSum.totalActiveSubs + w[key].totalActiveSubs - thisWeekSum.totalUnActiveSubs = thisWeekSum.totalUnActiveSubs + w[key].totalUnActiveSubs - thisWeekSum.totalMansMembers = thisWeekSum.totalMansMembers + w[key].totalMansMembers - thisWeekSum.totalGirlsMembers = thisWeekSum.totalGirlsMembers + w[key].totalGirlsMembers - }) - }) + // we map through this month weeks + if (!v || !v.days) { + return { + week: v?.week || i, + thisWeekSum: thisWeekSum + }; + } + // Since backend now provides cumulative totals, use the last day's values as the week total + if (v.days && v.days.length > 0) { + const lastDay = v.days[v.days.length - 1]; + const dayKeys = ['fri', 'thu', 'wed', 'tue', 'mon', 'sun', 'sat']; + + // Find the last day with data + for (const dayKey of dayKeys) { + if (lastDay[dayKey] && Object.keys(lastDay[dayKey]).length > 0) { + thisWeekSum = { + totalMembers: lastDay[dayKey]['totalMembers'] || 0, + totalActiveSubs: lastDay[dayKey]['totalActiveSubs'] || 0, + totalUnActiveSubs: lastDay[dayKey]['totalUnActiveSubs'] || 0, + totalMansMembers: lastDay[dayKey]['totalMansMembers'] || 0, + totalGirlsMembers: lastDay[dayKey]['totalGirlsMembers'] || 0, + }; + break; + } + } + } // each week we return there sum data return { - week: v.week, + week: v?.week || i, thisWeekSum: thisWeekSum } }) - let labels: [] = []; - thisMonthTotalSum.forEach(function (v, i) { - labels.push(t('week') + ' ' + thisMonthTotalSum[i]["week"]) + const newLabelsThisMonth : string[] = []; + thisMonthTotalSum.forEach(function(v , i) { + newLabelsThisMonth.push(t('week')+ ' ' +thisMonthTotalSum[i]["week"]) }) // set the labels - setLabels(labels) + setLabels(newLabelsThisMonth) // we setup chart_series ( chart data ) - let a: { + let newChartSeriesThisMonth : { name: string, - data: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number - } - }[] = ['totalMembers', 'totalActiveSubs', 'totalUnActiveSubs', 'totalMansMembers', 'totalGirlsMembers'].map((v: string, i: number): { + data: number[] + }[] = ['totalMembers' , 'totalActiveSubs' , 'totalUnActiveSubs' , 'totalMansMembers' , 'totalGirlsMembers'].map((v : string , i : number) : { name: string, - data: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number - } + data: number[] } => { - let data = thisMonthTotalSum.map((v2: { - week: number, - thisWeekSum: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number - } - }, i2: number) => { - let index: 'totalMembers' | 'totalActiveSubs' | 'totalUnActiveSubs' | 'totalMansMembers' | 'totalGirlsMembers' = v; - return v2.thisWeekSum[index]; + let data = thisMonthTotalSum.map((v2 : any) => { + let index : 'totalMembers' | 'totalActiveSubs' | 'totalUnActiveSubs' | 'totalMansMembers' | 'totalGirlsMembers' = v as any; + return v2?.thisWeekSum?.[index] || 0; }) // return the line data return { @@ -263,36 +266,31 @@ export default function MembersOverviewChart() { data: data }; }) - setChartSeries(a) + + setChartSeries(newChartSeriesThisMonth) } // if there was no data we use default return else { setChartSeries([ - { - name: '1' + t('week'), - data: [0, 0, 0, 0, 0], - }, - { - name: '2' + t('week'), - data: [0, 0, 0, 0, 0], - }, - { - name: '3' + t('week'), - data: [0, 0, 0, 0, 0], - }, - { - name: '4' + t('week'), - data: [0, 0, 0, 0, 0], - } - ]) + { name: t('totalMembers'), data: [0,0,0,0] }, + { name: t('totalActiveSubs'), data: [0,0,0,0] }, + { name: t('totalUnActiveSubs'), data: [0,0,0,0] }, + { name: t('totalMansMembers'), data: [0,0,0,0] }, + { name: t('totalGirlsMembers'), data: [0,0,0,0] }, + ]); + setLabels([t('week')+' 1', t('week')+' 2', t('week')+' 3', t('week')+' 4']); } + } // if chart duration is set to this month - else if (currentMembersGeneralOverviewDuration == 'thisYear') { + else if(currentMembersGeneralOverviewDuration == 'thisYear') + { // set the labels - setLabels([t('1month'), t('2month'), t('3month'), t('4month'), t('5month'), t('6month'), t('7month'), t('8month'), t('9month'), t('10month'), t('11month')]) + const newLabelsThisYear = [t('1month'), t('2month'), t('3month'), t('4month'), t('5month'), t('6month'), t('7month'), t('8month'), t('9month'), t('10month'), t('11month'),t('12month') ]; + setLabels(newLabelsThisYear); + // check if there is current month report - let thisYearTotalSum: { + let thisYearTotalSum : { month: number, thisMonthWeeksSum: { "totalMembers": number, @@ -301,12 +299,13 @@ export default function MembersOverviewChart() { "totalMansMembers": number, "totalGirlsMembers": number } - }[] - if (report?.membersGeneralOverview?.value.thisYear) { + }[] = []; + if(report?.membersGeneralOverview?.value?.thisYear) + { // if we founded a data for the current month - thisYearTotalSum = report?.membersGeneralOverview?.value.thisYear.months.map((v: any, i: number): { - month: number, - thisMonthWeeksSum: { + thisYearTotalSum = (report.membersGeneralOverview.value.thisYear.months || []).map((v : any , i : number) : { + month : number, + thisMonthWeeksSum : { "totalMembers": number, "totalActiveSubs": number, "totalUnActiveSubs": number, @@ -314,186 +313,94 @@ export default function MembersOverviewChart() { "totalGirlsMembers": number } } => { - let summedWeeks = v.weeks.map((w: any, i: number) => { - // we map through this month weeks - let weekSum = w.days.map((d: { - 'sat': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'sun': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'mon': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'tue': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'wed': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'thu': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - }, - 'fri': { - 'totalMembers': number, - 'totalActiveSubs': number, - 'totalUnActiveSubs': number, - 'totalMansMembers': number, - 'totalGirlsMembers': number, - } - }, i: number) => { - // we init the this week sum - let thisWeekSum = { - "totalMembers": 0, - "totalActiveSubs": 0, - "totalUnActiveSubs": 0, - "totalMansMembers": 0, - "totalGirlsMembers": 0 - } - // we loop through week days and we sum there data - Object.keys(d).forEach(function (key: "sat" | "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "_id", index: number) { - if (key == '_id') return - thisWeekSum.totalMembers = thisWeekSum.totalMembers + d[key].totalMembers - thisWeekSum.totalActiveSubs = thisWeekSum.totalActiveSubs + d[key].totalActiveSubs - thisWeekSum.totalUnActiveSubs = thisWeekSum.totalUnActiveSubs + d[key].totalUnActiveSubs - thisWeekSum.totalMansMembers = thisWeekSum.totalMansMembers + d[key].totalMansMembers - thisWeekSum.totalGirlsMembers = thisWeekSum.totalGirlsMembers + d[key].totalGirlsMembers - }) - // each week we return there sum data - return thisWeekSum - }) - return { - week: w.week, - weekSum: weekSum - } - }) - + // Initialize thisMonthWeeksSum let thisMonthWeeksSum = { "totalMembers": 0, "totalActiveSubs": 0, "totalUnActiveSubs": 0, "totalMansMembers": 0, "totalGirlsMembers": 0 + }; + + if (!v || !v.weeks) { + return { + month: v?.month !== undefined ? (v.month + 1) : (i + 1), + thisMonthWeeksSum: thisMonthWeeksSum + }; } - summedWeeks.map((v: { - week: number, weekSum: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number - }[] - }, i: number) => { - thisMonthWeeksSum.totalMembers += v.weekSum[0].totalMembers - thisMonthWeeksSum.totalActiveSubs += v.weekSum[0].totalActiveSubs - thisMonthWeeksSum.totalUnActiveSubs += v.weekSum[0].totalUnActiveSubs - thisMonthWeeksSum.totalMansMembers += v.weekSum[0].totalMansMembers - thisMonthWeeksSum.totalGirlsMembers += v.weekSum[0].totalGirlsMembers - }) - - return { - month: v.month, - thisMonthWeeksSum: thisMonthWeeksSum - } - }) - // prepare the labels - let labels: [] = []; - thisYearTotalSum.forEach(function (v, i) { - labels.push(t(thisYearTotalSum[i]["month"] + "month")) - }) - setLabels(labels) - // we setup chart_series ( chart data ) - let a: { - name: string, - data: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number - } - }[] = ['totalMembers', 'totalActiveSubs', 'totalUnActiveSubs', 'totalMansMembers', 'totalGirlsMembers'].map((v: string, i: number): { - name: string, - data: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number - } - } => { - let data = thisYearTotalSum.map((v2: { - month: number, - thisMonthWeeksSum: { - "totalMembers": number, - "totalActiveSubs": number, - "totalUnActiveSubs": number, - "totalMansMembers": number, - "totalGirlsMembers": number + // Since backend now provides cumulative totals, use the last week's values as the month total + if (v.weeks && v.weeks.length > 0) { + const lastWeek = v.weeks[v.weeks.length - 1]; + + if (lastWeek && lastWeek.days && lastWeek.days.length > 0) { + const lastDay = lastWeek.days[lastWeek.days.length - 1]; + const dayKeys = ['fri', 'thu', 'wed', 'tue', 'mon', 'sun', 'sat']; + + // Find the last day with data in the last week + for (const dayKey of dayKeys) { + if (lastDay[dayKey] && Object.keys(lastDay[dayKey]).length > 0) { + thisMonthWeeksSum = { + totalMembers: lastDay[dayKey]['totalMembers'] || 0, + totalActiveSubs: lastDay[dayKey]['totalActiveSubs'] || 0, + totalUnActiveSubs: lastDay[dayKey]['totalUnActiveSubs'] || 0, + totalMansMembers: lastDay[dayKey]['totalMansMembers'] || 0, + totalGirlsMembers: lastDay[dayKey]['totalGirlsMembers'] || 0, + }; + break; + } + } } - }, i2: number) => { - let index: 'totalMembers' | 'totalActiveSubs' | 'totalUnActiveSubs' | 'totalMansMembers' | 'totalGirlsMembers' = v; - return v2.thisMonthWeeksSum[index]; - }) + } + return { month: v?.month !== undefined ? (v.month + 1) : (i + 1), thisMonthWeeksSum: thisMonthWeeksSum }; + }); + // prepare the labels - ensure correct month order from 1 to 12 + const newLabelsThisYear : string[] = []; + for (let monthIndex = 1; monthIndex <= 12; monthIndex++) { + newLabelsThisYear.push(t(monthIndex + "month")); + } + setLabels(newLabelsThisYear) + // we setup chart_series ( chart data ) + let newChartSeriesThisYear : { + name: string, + data: number[] + }[] = ['totalMembers' , 'totalActiveSubs' , 'totalUnActiveSubs' , 'totalMansMembers' , 'totalGirlsMembers'].map((v : string , i : number) : { + name: string, + data: number[] + } => { + let data = []; + // Ensure we have data for all 12 months in correct order + for (let monthIndex = 1; monthIndex <= 12; monthIndex++) { + const monthData = thisYearTotalSum.find(item => item.month === monthIndex); + let index : 'totalMembers' | 'totalActiveSubs' | 'totalUnActiveSubs' | 'totalMansMembers' | 'totalGirlsMembers' = v as any; + data.push(monthData?.thisMonthWeeksSum?.[index] || 0); + } // return the line data return { name: t(v), data: data }; }) - setChartSeries(a) + + setChartSeries(newChartSeriesThisYear) } // if there was no data we use default return else { + const defaultYearLabels = [t('1month'), t('2month'), t('3month'), t('4month'), t('5month'), t('6month'), t('7month'), t('8month'), t('9month'), t('10month'), t('11month'), t('12month')]; setChartSeries([ - { - name: '1' + t('week'), - data: [0, 0, 0, 0, 0], - }, - { - name: '2' + t('week'), - data: [0, 0, 0, 0, 0], - }, - { - name: '3' + t('week'), - data: [0, 0, 0, 0, 0], - }, - { - name: '4' + t('week'), - data: [0, 0, 0, 0, 0], - } - ]) + { name: t('totalMembers'), data: [0,0,0,0,0,0,0,0,0,0,0,0] }, + { name: t('totalActiveSubs'), data: [0,0,0,0,0,0,0,0,0,0,0,0] }, + { name: t('totalUnActiveSubs'), data: [0,0,0,0,0,0,0,0,0,0,0,0] }, + { name: t('totalMansMembers'), data: [0,0,0,0,0,0,0,0,0,0,0,0] }, + { name: t('totalGirlsMembers'), data: [0,0,0,0,0,0,0,0,0,0,0,0] }, + ]); + setLabels(defaultYearLabels); } + } - }, [currentMembersGeneralOverviewDuration]) + }, [report , currentMembersGeneralOverviewDuration , isRtl , isDark , t]) // prepare the chart options - const options: any = { + const options : any = { series: chartSeries, options: { chart: { @@ -523,7 +430,7 @@ export default function MembersOverviewChart() { left: -7, top: 22, }, - colors: ['#38C172', '#38C172', '#E3342F', '#0263FF', '#FF30F7'], + colors: ['#38C172', '#38C172' , '#E3342F' , '#0263FF', '#FF30F7'], markers: { //discrete: [ // { @@ -567,11 +474,12 @@ export default function MembersOverviewChart() { tickAmount: 7, labels: { formatter: (value: number) => { - if (value > 1000) { + if(value > 1000) + { return parseInt(value.toFixed(0)) + t('k') } else { - return parseInt(value.toFixed(0)) + return parseInt(value.toFixed(0)) } }, offsetX: isRtl ? -30 : -10, diff --git a/webapp/src/components/dashboard/home/servicesSubscriptions.tsx b/webapp/src/components/dashboard/home/servicesSubscriptions.tsx index b4b3dac..41f9eeb 100644 --- a/webapp/src/components/dashboard/home/servicesSubscriptions.tsx +++ b/webapp/src/components/dashboard/home/servicesSubscriptions.tsx @@ -2,7 +2,6 @@ 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, }); @@ -10,56 +9,34 @@ const ReactApexChart = dynamic(() => import('react-apexcharts'), { export default function ServicesSubscriptions() { // get redux needed state - const report = useAppSelector((state) => state.statisticsReducer.value.report); - const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType); - const [isClient, setIsClient] = useState(false); - + const report = useAppSelector((state) => state.statisticsReducer.value.report) + 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'; - const isDark = themeType === 'dark'; - - // Set isClient to true after component mounts - useEffect(() => { - setIsClient(true); - }, []); - - // if loading or no report, show loading spinner - if (!report || !isClient) { - return ( -
-
-
- ); - } - - // setup chart data with null checks - const data = report?.servicesNameAndSubscribers?.value?.map((v: { - _id: string; - name: string; - totalSubscribers: number; - }) => v?.totalSubscribers) || []; - - // setup chart labels with null checks - const labels = report?.servicesNameAndSubscribers?.value?.map((v: { - _id: string; - name: string; - totalSubscribers: number; - }) => v?.name) || []; - - // Return empty state if no data - if (data.length === 0 || labels.length === 0) { - return ( -
-

{t('noDataAvailable')}

-
- ); - } - + 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 - const options = { + var options = { series: [{ name: t('subscription'), data: data diff --git a/webapp/src/components/dashboard/home/workersJobTypes.tsx b/webapp/src/components/dashboard/home/workersJobTypes.tsx index e1710b1..e810a8d 100644 --- a/webapp/src/components/dashboard/home/workersJobTypes.tsx +++ b/webapp/src/components/dashboard/home/workersJobTypes.tsx @@ -2,7 +2,6 @@ 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, }); @@ -10,56 +9,34 @@ const ReactApexChart = dynamic(() => import('react-apexcharts'), { export default function WorkersJobTypes() { // get needed redux state - const report = useAppSelector((state) => state.statisticsReducer.value.report); - const themeType = useAppSelector((state) => state.themeTypeReducer.value.themeType); - const [isClient, setIsClient] = useState(false); - + const report = useAppSelector((state) => state.statisticsReducer.value.report) + 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'; - const isDark = themeType === 'dark'; - - // Set isClient to true after component mounts - useEffect(() => { - setIsClient(true); - }, []); - - // if loading or no report, show loading spinner - if (!report || !isClient) { - return ( -
-
-
- ); - } - - // prepare chart data with null checks - const data = report?.workersNameAndJobType?.value?.map((v: { - _id: string; - jobType: string; - count: number; - }) => v?.count) || []; - - // prepare chart labels with null checks - const labels = report?.workersNameAndJobType?.value?.map((v: { - _id: string; - jobType: string; - count: number; - }) => v?.jobType) || []; - - // Return empty state if no data - if (data.length === 0 || labels.length === 0) { - return ( -
-

{t('noDataAvailable')}

-
- ); - } - + 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 + // prepare chart data + let data = report?.workersNameAndJobType.value.map((v : { + _id: string, + jobType: string, + count: number, + } , i : number) => { + return v?.count; + }) + // prepare chart labels + let labels = report?.workersNameAndJobType.value.map((v : { + _id: string, + jobType: string, + count: number, + } , i : number) => { + return v?.jobType; + }) // prepare chart options - const options = { + var options = { series: [{ name: t('worker'), data: data diff --git a/webapp/src/components/dashboard/incomes/parts/addNewButton.tsx b/webapp/src/components/dashboard/incomes/parts/addNewButton.tsx index 9aea81a..f07bc4f 100644 --- a/webapp/src/components/dashboard/incomes/parts/addNewButton.tsx +++ b/webapp/src/components/dashboard/incomes/parts/addNewButton.tsx @@ -22,8 +22,8 @@ export default function AddNewButton() flex justify-between items-center gap-3 "> - - + +

{t('addNewIncome')}

diff --git a/webapp/src/components/dashboard/incomes/parts/addPopUp.tsx b/webapp/src/components/dashboard/incomes/parts/addPopUp.tsx index e498895..02bdc6f 100644 --- a/webapp/src/components/dashboard/incomes/parts/addPopUp.tsx +++ b/webapp/src/components/dashboard/incomes/parts/addPopUp.tsx @@ -63,7 +63,8 @@ export default function AddPopUp() // This is the title of the pop up } -

{t('addNewIncome')}

+ +

{t('addNewIncome')}

handleClose()} className="fill-primary-dark dark:fill-primary cursor-pointer"> diff --git a/webapp/src/components/dashboard/incomes/parts/incomesListPagination.tsx b/webapp/src/components/dashboard/incomes/parts/incomesListPagination.tsx index 43446fc..2747b1a 100644 --- a/webapp/src/components/dashboard/incomes/parts/incomesListPagination.tsx +++ b/webapp/src/components/dashboard/incomes/parts/incomesListPagination.tsx @@ -18,11 +18,11 @@ export default function ListPagination() { // show the pagination ui return ( <> - {Math.ceil(total / 4) > 1 ? ( + {Math.ceil(total / 10) > 1 ? (
- - { if(e.target.value.length == 0) @@ -127,38 +125,30 @@ export default function List() {v.payMonth} {appGeneralSettings.currencySymbol} {v.planDelay + ' ' + t('months')} - {v.active ? - (v.planExpAt_unix - (Date.now() / 1000) > 259200 ? - - - - - {t('trueActive')} - - : - (v.planExpAt_unix - (Date.now() / 1000) > 0 ? - - - - - {t('expireVerySoon')} - - : - - - - - {t('falseActive')} - - ) - ) + {v.planExpAt_unix - (Date.now() / 1000) > 259200 ? + + + + + {t('trueActive')} + + : + ( + v.planExpAt_unix - (Date.now() / 1000) > 0 ? + + + + + {t('expireVerySoon')} + : - {t('inactive')} + {t('falseActive')} + ) } diff --git a/webapp/src/components/dashboard/members/parts/addNewButton.tsx b/webapp/src/components/dashboard/members/parts/addNewButton.tsx index 9bad784..d3e6dc4 100644 --- a/webapp/src/components/dashboard/members/parts/addNewButton.tsx +++ b/webapp/src/components/dashboard/members/parts/addNewButton.tsx @@ -22,8 +22,8 @@ export default function AddNewButton() flex justify-between items-center gap-3 "> - - + +

{t('addNewMember')}

diff --git a/webapp/src/components/dashboard/members/parts/addPopUp.tsx b/webapp/src/components/dashboard/members/parts/addPopUp.tsx index 07bbdb1..ad2d137 100644 --- a/webapp/src/components/dashboard/members/parts/addPopUp.tsx +++ b/webapp/src/components/dashboard/members/parts/addPopUp.tsx @@ -83,7 +83,8 @@ export default function AddPopUp() // dialog text content contain a small description to the current form part } -

{t('memberAddText1')}

+ +

{t('memberAddText1')}

{t('memberDetails')} + +

{t('memberDetails')}

handleClose()} className="fill-primary-dark dark:fill-primary cursor-pointer"> diff --git a/webapp/src/components/dashboard/members/parts/membersListPagination.tsx b/webapp/src/components/dashboard/members/parts/membersListPagination.tsx index 95fea1e..0061550 100644 --- a/webapp/src/components/dashboard/members/parts/membersListPagination.tsx +++ b/webapp/src/components/dashboard/members/parts/membersListPagination.tsx @@ -18,11 +18,11 @@ export default function ListPagination() { // show the pagination ui return ( <> - {Math.ceil(total / 4) > 1 ? ( + {Math.ceil(total / 20) > 1 ? (
-

{t('updateMember')}

+ +

{t('updateMember')}

handleClose()} className="fill-primary-dark dark:fill-primary cursor-pointer"> @@ -121,7 +122,8 @@ export default function UpdatePopUp() // dialog text content contain a small description to the current form part } -

{t('memberUpdateText1')}

+ +

{t('memberUpdateText1')}

{t('memberSubscriptionRenewal')} + +

{t('memberSubscriptionRenewal')}

handleClose()} className="fill-primary-dark dark:fill-primary cursor-pointer"> @@ -241,7 +242,8 @@ export default function UpdateSubPopUp() // dialog text content contain a small description to the current form part } -

{t('memberSubscriptionRenewalText1')}

+ +

{t('memberSubscriptionRenewalText1')}

- - + +

{t('addNewProduct')}

diff --git a/webapp/src/components/dashboard/products/parts/productsListPagination.tsx b/webapp/src/components/dashboard/products/parts/productsListPagination.tsx index a066d69..d70ef3c 100644 --- a/webapp/src/components/dashboard/products/parts/productsListPagination.tsx +++ b/webapp/src/components/dashboard/products/parts/productsListPagination.tsx @@ -18,11 +18,11 @@ export default function ListPagination() { // show the pagination ui return ( <> - {Math.ceil(total / 4) > 1 ? ( + {Math.ceil(total / 10) > 1 ? (
- - + +

{t('addNewService')}

diff --git a/webapp/src/components/dashboard/services/parts/servicesListPagination.tsx b/webapp/src/components/dashboard/services/parts/servicesListPagination.tsx index 57d4416..82cd87f 100644 --- a/webapp/src/components/dashboard/services/parts/servicesListPagination.tsx +++ b/webapp/src/components/dashboard/services/parts/servicesListPagination.tsx @@ -18,11 +18,11 @@ export default function ListPagination() { // show the pagination ui return ( <> - {Math.ceil(total / 4) > 1 ? ( + {Math.ceil(total / 10) > 1 ? (
(); + const dispatch = useAppDispatch(); const t = useTranslations('sidebar'); const pathname = usePathname() const prevPathName = useRef(pathname) @@ -23,12 +23,10 @@ export default function Sidebar () { // get states const sidebarState = useAppSelector((state) => state.sidebarReducer.value.state); const sidebarItems = useAppSelector((state) => state.sidebarReducer.value.items); - const appGeneralSettings = useAppSelector((state) => state.settingsReducer.value.appGeneralSettings) || { - showLogo: true, - logo: '', - appName: '', - appNameEN: '' - }; + const appGeneralSettings = useAppSelector((state) => { + //console.log("Redux state:", state.settingsReducer.value); + return state.settingsReducer.value.appGeneralSettings; + }); // handle sidebar menus states const [currentMenu, setCurrentMenu] = useState(''); const toggleMenu = (value: string) => { @@ -48,18 +46,13 @@ export default function Sidebar () {