diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/StatsService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/StatsService.java new file mode 100644 index 0000000..0f08139 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/StatsService.java @@ -0,0 +1,71 @@ +package fr.titionfire.ffsaf.domain.service; + +import fr.titionfire.ffsaf.data.repository.LicenceRepository; +import fr.titionfire.ffsaf.rest.data.LicenceStats; +import fr.titionfire.ffsaf.utils.Categorie; +import fr.titionfire.ffsaf.utils.Genre; +import fr.titionfire.ffsaf.utils.Utils; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.hibernate.reactive.mutiny.Mutiny; + +import java.util.concurrent.ConcurrentHashMap; + +@WithSession +@ApplicationScoped +public class StatsService { + + @Inject + LicenceRepository licenceRepository; + + public Uni getStats() { + ConcurrentHashMap categories = new ConcurrentHashMap<>(); + LicenceStats stats = new LicenceStats(); + int currentSaison = Utils.getSaison(); + + //noinspection ReactiveStreamsUnusedPublisher + return licenceRepository.listAll() + .onItem().transformToMulti(licences -> Multi.createFrom().iterable(licences)) + .call(licence -> Mutiny.fetch(licence.getMembre())) + .invoke(licence -> { + if (!stats.getLicences().containsKey(licence.getSaison())) + stats.getLicences().put(licence.getSaison(), new LicenceStats.YearStats()); + + LicenceStats.YearStats yearStats = stats.getLicences().get(licence.getSaison()); + + System.out.println("stats: " + licence.getMembre().getFname()); + if (licence.isValidate()) { + if (licence.getMembre().getGenre() == Genre.H) + yearStats.addH(); + else if (licence.getMembre().getGenre() == Genre.F) + yearStats.addF(); + else + yearStats.addNa(); + + if (licence.getSaison() == currentSaison && licence.getMembre().getCategorie() != null) { + categories.put(licence.getMembre().getCategorie(), + categories.getOrDefault(licence.getMembre().getCategorie(), 0) + 1); + } + } else { + yearStats.addNotValid(); + } + + }) + .collect().asList() + .map(__ -> { + for (Categorie c : Categorie.values()) { + LicenceStats.CategoriesStats o = new LicenceStats.CategoriesStats(); + + o.setName(c.getName()); + o.setCount(categories.getOrDefault(c, 0)); + + stats.getCategories().add(o); + } + + return stats; + }); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/StatsEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/StatsEndpoints.java new file mode 100644 index 0000000..97806fd --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/StatsEndpoints.java @@ -0,0 +1,25 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.StatsService; +import fr.titionfire.ffsaf.rest.data.LicenceStats; +import io.smallrye.mutiny.Uni; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("api/stats") +public class StatsEndpoints { + + @Inject + StatsService service; + + @GET + @RolesAllowed("federation_admin") + @Produces(MediaType.APPLICATION_JSON) + public Uni getStats() { + return service.getStats(); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/LicenceStats.java b/src/main/java/fr/titionfire/ffsaf/rest/data/LicenceStats.java new file mode 100644 index 0000000..d8c254b --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/LicenceStats.java @@ -0,0 +1,48 @@ +package fr.titionfire.ffsaf.rest.data; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +@Data +@RegisterForReflection +public class LicenceStats { + private ConcurrentHashMap licences = new ConcurrentHashMap<>(); + private List categories = new ArrayList<>(); + + @Data + @RegisterForReflection + public static class CategoriesStats { + private String name; + private int count = 0; + } + + @Data + @RegisterForReflection + public static class YearStats { + private int h = 0; + private int f = 0; + private int na = 0; + private int not_valid = 0; + + public void addH() { + this.h++; + } + + public void addF() { + this.f++; + } + + public void addNa() { + this.na++; + } + + public void addNotValid() { + this.not_valid++; + } + + } +} diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 78674e5..b83100b 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -19,10 +19,12 @@ "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-is": "^19.0.0", "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.21.2", - "react-toastify": "^10.0.4" + "react-toastify": "^10.0.4", + "recharts": "^2.15.1" }, "devDependencies": { "@types/react": "^18.2.43", @@ -335,6 +337,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -1254,6 +1267,60 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1726,8 +1793,117 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } }, "node_modules/debug": { "version": "4.3.4", @@ -1746,6 +1922,11 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1803,6 +1984,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.630", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.630.tgz", @@ -2282,12 +2472,25 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2713,6 +2916,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3159,6 +3370,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3528,6 +3744,11 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3586,9 +3807,9 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==" }, "node_modules/react-leaflet": { "version": "4.2.1", @@ -3663,6 +3884,20 @@ "react-dom": ">=16.8" } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-toastify": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", @@ -3675,6 +3910,56 @@ "react-dom": ">=16" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -3695,6 +3980,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -4130,6 +4420,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -4292,6 +4587,27 @@ "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.0.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 32fbdf3..3384fa4 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -21,10 +21,12 @@ "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-is": "^19.0.0", "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", "react-router-dom": "^6.21.2", - "react-toastify": "^10.0.4" + "react-toastify": "^10.0.4", + "recharts": "^2.15.1" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index 5f64f86..825a6a0 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -72,6 +72,7 @@ function AdminMenu() {
  • Member
  • Club
  • +
  • Statistiques
} diff --git a/src/main/webapp/src/pages/admin/AdminRoot.jsx b/src/main/webapp/src/pages/admin/AdminRoot.jsx index 3a4ddfa..948503b 100644 --- a/src/main/webapp/src/pages/admin/AdminRoot.jsx +++ b/src/main/webapp/src/pages/admin/AdminRoot.jsx @@ -9,6 +9,8 @@ import {AffiliationReqPage} from "./affiliation/AffiliationReqPage.jsx"; import {NewClubPage} from "./club/NewClubPage.jsx"; import {ClubPage} from "./club/ClubPage.jsx"; import {AffiliationReqList} from "./affiliation/AffiliationReqList.jsx"; +import {Scale} from "leaflet/src/control/Control.Scale.js"; +import {StatsPage} from "./StatsPage.jsx"; export function AdminRoot() { return <> @@ -54,8 +56,8 @@ export function getAdminChildren() { element: }, { - path: 'b', - element:
Admin B
+ path: 'stats', + element: } ] } \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/StatsLazy.jsx b/src/main/webapp/src/pages/admin/StatsLazy.jsx new file mode 100644 index 0000000..a704b06 --- /dev/null +++ b/src/main/webapp/src/pages/admin/StatsLazy.jsx @@ -0,0 +1,170 @@ +import {Area, AreaChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Sector, Tooltip, XAxis, YAxis} from "recharts"; +import {useEffect, useState} from "react"; +import {getSaison} from "../../utils/Tools.js"; + +export default function StatsLazy({data}) { + return
+
+

Nombre de licences

+
+
+ +
+
+
+
+

Nombre de licences par catégorie pour {getSaison()}

+
+
+ +
+
+
+
+} + +function NbGraph({raw_data}) { + const [showData, setShowData] = useState([]) + + useEffect(() => { + const data = [] + for (const k in raw_data.licences) { + data.push({ + name: k + "-" + (parseInt(k) + 1), + h: raw_data.licences[k].h, + f: raw_data.licences[k].f, + na: raw_data.licences[k].na, + not_valid: raw_data.licences[k].not_valid, + }) + } + setShowData(data) + }, [raw_data]) + + const colors = ['#6e6e6e', '#1a07f8', '#f659d5', '#f80000'] + + return + + + + + + + + + + + +} + +function CatGraph({raw_data}) { + const [showData, setShowData] = useState([]) + const [activeIndex, setActiveIndex] = useState(0) + + + useEffect(() => { + const data = [] + for (const k of raw_data.categories) { + data.push({ + name: k.name, + value: k.count, + }) + } + setShowData(data) + }, [raw_data]) + + const COLORS = ['#33FF8F', '#30E393', '#2DC697', '#2AAA9B' + , '#278E9F', '#2471A3', '#2155A7', '#1E39AB' + , '#1B1CAF', '#1800B3']; + + const RADIAN = Math.PI / 180; + const renderCustomizedLabel = ({cx, cy, midAngle, innerRadius, outerRadius, index}) => { + const radius = innerRadius + (outerRadius - innerRadius) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + return <> { + showData[index]['value'] > 0 && + cx ? 'start' : 'end'} dominantBaseline="central"> + {`${showData[index]['name']}`} + + + }; + }; + + const onPieEnter = (_, index) => { + setActiveIndex(index); + }; + + const renderActiveShape = (props) => { + const RADIAN = Math.PI / 180; + const {cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value} = props; + const sin = Math.sin(-RADIAN * midAngle); + const cos = Math.cos(-RADIAN * midAngle); + const sx = cx + (outerRadius + 10) * cos; + const sy = cy + (outerRadius + 10) * sin; + const mx = cx + (outerRadius + 30) * cos; + const my = cy + (outerRadius + 30) * sin; + const ex = mx + (cos >= 0 ? 1 : -1) * 22; + const ey = my; + const textAnchor = cos >= 0 ? 'start' : 'end'; + + return + + {payload.name} + + + + + + = 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#333">{`${payload.name}`} + = 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#999"> + {`${value} (${(percent * 100).toFixed(0)}%)`} + + ; + }; + + return + + + {showData.map((entry, index) => ( + + ))} + + + +} \ No newline at end of file diff --git a/src/main/webapp/src/pages/admin/StatsPage.jsx b/src/main/webapp/src/pages/admin/StatsPage.jsx new file mode 100644 index 0000000..6a1a8ed --- /dev/null +++ b/src/main/webapp/src/pages/admin/StatsPage.jsx @@ -0,0 +1,23 @@ +import {lazy, Suspense} from "react"; +import {FallingLines} from "react-loader-spinner"; +import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; +import {useFetch} from "../../hooks/useFetch.js"; +import {AxiosError} from "../../components/AxiosError.jsx"; + +export function StatsPage() { + const StatsLazy = lazy(() => import('./StatsLazy.jsx')) + const setLoading = useLoadingSwitcher() + const {data, error} = useFetch(`/stats`, setLoading, 1) + + return
+

Statistiques

+ }> + {data + ? <> + + + : error && + } + +
+} \ No newline at end of file