diff --git a/components/badge/Date.vue b/components/badge/Date.vue new file mode 100644 index 0000000000000000000000000000000000000000..dccb6400457d32500e8acd34b0682781aa2019a3 --- /dev/null +++ b/components/badge/Date.vue @@ -0,0 +1,24 @@ +<template> + <small> + <span + class="badge" + :class="'badge-' + $options.filters.dateStatus(date)"> + {{ $d(new Date(date * 1000), styleDate) }} + </span> + </small> +</template> + +<script> +export default { + props: { + date: { + type: Number, + required: true + }, + styleDate: { + type: String, + required: true + }, + } +} +</script> diff --git a/components/certif/List.vue b/components/certif/List.vue index 7146b8c55362581bd5881b2871f13abab403507b..03abdc2266011756a459b44f9d0c6cdab032434c 100644 --- a/components/certif/List.vue +++ b/components/certif/List.vue @@ -34,20 +34,12 @@ </div> </th> <td class="text-right py-1"> - <small - ><span - class="badge" - :class=" - 'badge-' + $options.filters.dateStatus(certif.expires_on) - " - >{{ $d(new Date(certif.expires_on * 1000), "short") }}</span - ></small - > - <small class="d-block" - ><span class="badge badge-secondary">{{ - $t("traitement") - }}</span></small - > + <BadgeDate + :date="certif.expires_on" + :styleDate="'short'" /> + <small class="d-block"> + <span class="badge badge-secondary">{{ $t("traitement")}}</span> + </small> </td> </tr> </tbody> @@ -134,15 +126,9 @@ </div> </th> <td class="text-right py-1"> - <small - ><span - class="badge" - :class=" - 'badge-' + $options.filters.dateStatus(certif.expires_on) - " - >{{ $d(new Date(certif.expires_on * 1000), "long") }}</span - ></small - > + <BadgeDate + :date="certif.expires_on" + :styleDate="'long'" /> </td> </tr> </tbody> diff --git a/components/member/List.vue b/components/member/List.vue index c6137ee28e32a2896c24ba987d4b087ea325d5f7..1ed5ac898a1c48259f0db25cbd4daa693665c91e 100644 --- a/components/member/List.vue +++ b/components/member/List.vue @@ -3,10 +3,52 @@ <table class="table table-striped table-hover"> <thead v-if="displayHead"> <tr> - <th scope="col">UID</th> - <th scope="col" class="d-none d-xl-table-cell" v-if="displayPubkey"> + <th scope="col" @click="sort('uid')"> + UID + <div class="d-inline-block position-absolute ml-2"> + <div + class="up" + :class="{ + sorted: currentSortDir == 'desc' && currentSort == 'uid', + invisible: currentSortDir == 'asc' && currentSort == 'uid' + }"> + â–² + </div> + <div + class="down" + :class="{ + sorted: currentSortDir == 'asc' && currentSort == 'uid', + invisible: currentSortDir == 'desc' && currentSort == 'uid' + }"> + â–¼ + </div> + </div> + </th> + <th scope="col" class="d-none d-xl-table-cell" v-if="displayPubkey && !displayOnlyDate"> {{ $t("cle.publique.title") }} </th> + <th scope="col" class="d-none d-xl-table-cell" + @click="sort('limit_date')" v-if="displayOnlyDate"> + {{ $t("limitDate") }} + <div class="d-inline-block position-absolute ml-2"> + <div + class="up" + :class="{ + sorted: currentSortDir == 'desc' && currentSort == 'limit_date', + invisible: currentSortDir == 'asc' && currentSort == 'limit_date' + }"> + â–² + </div> + <div + class="down" + :class="{ + sorted: currentSortDir == 'asc' && currentSort == 'limit_date', + invisible: currentSortDir == 'desc' && currentSort == 'limit_date' + }"> + â–¼ + </div> + </div> + </th> <th scope="col" class="d-none d-sm-table-cell" v-if="displayDate"> {{ $t("membre.datelimpertestatut") }} </th> @@ -14,7 +56,7 @@ </thead> <tbody> <tr - v-for="member in members" + v-for="member in membersSorted" :key="member.uid" @click="redirect(member.hash)"> <th scope="row"> @@ -23,9 +65,15 @@ :limitDate=" Math.min(member.received_certifications.limit, member.limitDate) " - :memberStatus="member.status" /> - <BadgeStatus :membre="member" /> + :memberStatus="member.status" + v-if="!displayOnlyDate" /> + <BadgeStatus :membre="member" v-if="!displayOnlyDate"/> </th> + <td class="d-none d-xl-table-cell" v-if="displayOnlyDate"> + <BadgeDate + :date="adhesion ? member.limitDate : member.received_certifications.limit" + :styleDate="'long'" /> + </td> <td class="d-none d-xl-table-cell" v-if="displayPubkey"> {{ member.pubkey.substring(0, 10) }} </td> @@ -41,6 +89,12 @@ <script> export default { + data() { + return { + currentSort: "uid", + currentSortDir: "asc" + } + }, props: { members: { type: Array, @@ -57,7 +111,15 @@ export default { displayDate: { type: Boolean, default: true - } + }, + displayOnlyDate: { + type: Boolean, + default: false + }, + adhesion: { + type: Boolean, + default: true + }, }, methods: { redirect(hash) { @@ -77,7 +139,64 @@ export default { "'>" + this.$d(new Date(date * 1000), "long") + "</span>" + }, + sort(s) { + if (s === this.currentSort) { + this.currentSortDir = this.currentSortDir === "asc" ? "desc" : "asc" + } + this.currentSort = s + } + }, + computed: { + membersSorted() { + return this.members + .slice() + .sort((a, b) => { + let modifier = this.currentSortDir === "desc" ? -1 : 1 + + if (this.currentSort == "limit_date") { + if(this.adhesion) { + if (a["limitDate"] < b["limitDate"]) return -1 * modifier + if (a["limitDate"] > b["limitDate"]) return 1 * modifier + } else { + if (a["received_certifications"]["limit"] < b["received_certifications"]["limit"]) return -1 * modifier + if (a["received_certifications"]["limit"] > b["received_certifications"]["limit"]) return 1 * modifier + } + } else { + if (a["uid"].toLowerCase() < b["uid"].toLowerCase()) + return -1 * modifier + if (a["uid"].toLowerCase() > b["uid"].toLowerCase()) + return 1 * modifier + } + + return 0 + }) } } } </script> + +<style lang="scss" scoped> +thead th { + position: relative; + cursor: pointer; + background: var(--background-color-secondary); + + &:last-child { + padding-right: 1.5rem; + text-align: right; + } +} + +.up, +.down { + line-height: 10px; + font-size: 1.1rem; + transform: scale(1.5, 1); + opacity: 0.3; +} + +.sorted { + opacity: 1; +} +</style> diff --git a/graphql/doc/graphQLschema.txt b/graphql/doc/graphQLschema.txt index a4ce243ccb6e89b62c274c3c6a4fe0943bd8558d..bfa30d15060df51340fb233c1474d26bb420f0a6 100644 --- a/graphql/doc/graphQLschema.txt +++ b/graphql/doc/graphQLschema.txt @@ -595,7 +595,7 @@ type Forecast { "Entry or exit of an identity" type EventId { - id: Identity! + member: Identity! "Entry or exit; true if entry" inOut: Boolean! diff --git a/graphql/queries.js b/graphql/queries.js index 3585f5be469180649efb8a69e8a48d8b4518cc70..1aae22226bd331751aad2f95e26e1c6329d6c4e0 100644 --- a/graphql/queries.js +++ b/graphql/queries.js @@ -206,3 +206,21 @@ export const FAVORIS = gql` } } ` + +// Pour la page index +export const NEXT_EXITS = gql` + query NextExits($group: [String!], $start: Int64, $period: Int64) { + memEnds (group: $group, startFromNow: $start, period: $period) { + __typename + pubkey + uid + status + hash + limitDate + received_certifications { + __typename + limit + } + } + } +` diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 2d2967b2de30b0f203fd033b80171ccdb3ca5c63..bcce32286f9e2dfe5e0e1d4aa055dd57726c59a5 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -62,6 +62,10 @@ "title": "Duniter" }, "expire": "Expires", + "error": { + "tooSmall": "{0} is too small. It must be between {1} and {2}. The value used is {1}", + "tooBig": "{0} is too big. It must be between {1} and {2}. The value used is {1}" + }, "favoris": { "enregistre": "Saved to favorites !", "none": "You don't have any favorites yet", @@ -69,10 +73,12 @@ "title": "My favourites" }, "futuremembers": "Future members", + "futureexits": "Future exits", "infos": "Informations", "inout": "Entries and exits of the web of trust for the last 2 days", "inpreparation": "In preparation", "lexique": "Lexicon", + "limitDate": "Deadline", "membre": { "calculant": { "desc": "Member using his private key to forge blocks thanks to Duniter installed on a node accessible on the Internet network", @@ -150,8 +156,13 @@ "previsions": { "pardate": "Forecasts by dates", "parmembre": "Forecasts by members", + "period": { + "title": "Search period", + "desc": "Select the desired number of days between 1 and 30" + }, "title": "Forecasts" }, + "pubkey": "Public key", "recherche": { "desc": "Enter the start of a nickname or public key", "title": "Your search" @@ -184,6 +195,7 @@ "title": "Relative Theory of Money (RTM)" }, "type": "Type", + "uid": "Unique identifier", "valeur": "Value", "wot": { "desc": "Set of the individuals recognized as such by their peers including the links that bind them together through certifications", diff --git a/i18n/locales/es.json b/i18n/locales/es.json index 158f517d3b89e09cabd5c032f4d7f265bbdfe0df..bc350951643454b85c70a8ddb7d11d42ad94725e 100644 --- a/i18n/locales/es.json +++ b/i18n/locales/es.json @@ -62,6 +62,10 @@ "title": "Duniter" }, "expire": "Expira el", + "error": { + "tooSmall": "{0} es demasiado pequeño. Debe estar entre {1} y {2}. El valor utilizado es {1}", + "tooBig": "{0} es demasiado grande. Debe estar entre {1} y {2}. El valor utilizado es {1}" + }, "favoris": { "enregistre": "¡Guardado en favoritos!", "none": "Aún no tienes favoritos", @@ -69,10 +73,12 @@ "title": "Mis favoritos" }, "futuremembers": "Futuros miembros", + "futureexits": "Futuras salidas", "infos": "Informaciones", "inout": "Entradas y salidas de la red de confianza en los últimos 2 dÃas", "inpreparation": "En preparación", "lexique": "Léxico", + "limitDate": "Fecha lÃmite", "membre": { "calculant": { "desc": "Miembro usando su clave privada para falsificar bloques gracias a Duniter instalado en un nodo accesible en la red de Internet", @@ -150,8 +156,13 @@ "previsions": { "pardate": "Previsiones por fecha", "parmembre": "Previsiones por miembros", + "period": { + "title": "PerÃodo de búsqueda", + "desc": "Seleccione el número de dÃas deseado entre 1 y 30" + }, "title": "Pronósticos" }, + "pubkey": "Llave pública", "recherche": { "desc": "Introduce el comienzo de un pseudónimo o llave pública", "title": "Buscar" @@ -184,6 +195,7 @@ "title": "TeorÃa relativa del dinero (TRD)" }, "type": "Tipo", + "uid": "Identificador único", "valeur": "Valor", "wot": { "desc": "Conjunto de las personas reconocidas como tales por sus pares incluyendo los vÃnculos que las unen a través de certificaciones", diff --git a/i18n/locales/fr.json b/i18n/locales/fr.json index 043e0e242e2c05130a71c39a1b7468bcced64a64..bafd341e6e9b85ec2516ca1bdf7d3f7c9421e85f 100644 --- a/i18n/locales/fr.json +++ b/i18n/locales/fr.json @@ -62,6 +62,10 @@ "title": "Duniter" }, "expire": "Expire le", + "error": { + "tooSmall": "{0} est trop petit. Il doit être compris entre {1} et {2}. La valeur utilisée est {1}", + "tooBig": "{0} est trop grand. Il doit être compris entre {1} et {2}. La valeur utilisée est {2}" + }, "favoris": { "enregistre": "Enregistré dans les favoris !", "none": "Vous n'avez pas encore de favoris", @@ -69,10 +73,12 @@ "title": "Mes favoris" }, "futuremembers": "Futurs membres", + "futureexits": "Futures sorties", "infos": "Informations", "inout": "Entrées et sorties de la toile de confiance des 2 derniers jours", "inpreparation": "En préparation", "lexique": "Lexique", + "limitDate": "Date limite", "membre": { "calculant": { "desc": "Membre utilisant sa clé privée pour forger des blocs grâce à Duniter installé sur un noeud accessible sur le réseau Internet", @@ -150,8 +156,13 @@ "previsions": { "pardate": "Prévisions par date", "parmembre": "Prévisions par membres", + "period": { + "title": "Période de recherche", + "desc": "Sélectionnez le nombre de jours souhaités entre 1 et 30" + }, "title": "Prévisions" }, + "pubkey": "Clef publique", "recherche": { "desc": "Saisissez le début d'un pseudo ou d'une clé publique", "title": "Votre recherche" @@ -184,6 +195,7 @@ "title": "Théorie Relative de la Monnaie (TRM)" }, "type": "Type", + "uid": "Identifiant unique", "valeur": "Valeur", "wot": { "desc": "Ensemble des membres et des certifications qui les relient entre eux", diff --git a/layouts/default.vue b/layouts/default.vue index 651572ffc0ca15a236d8ca5dedb4809645c36ad2..6061a0cbb5811cd2db9e76f7b7760d9697f4143c 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -21,7 +21,10 @@ export default { }, { title: "previsions.title", - items: [{ path: "/previsions", title: "futuremembers" }] + items: [ + { path: "/previsions", title: "futuremembers" }, + { path: "/previsions/futures_sorties", title: "futureexits" }, + ] }, { title: "infos", diff --git a/pages/previsions/futures_sorties.vue b/pages/previsions/futures_sorties.vue new file mode 100644 index 0000000000000000000000000000000000000000..ef8adfb4fe125b16586007ce0505547999b9b4bc --- /dev/null +++ b/pages/previsions/futures_sorties.vue @@ -0,0 +1,169 @@ +<template> + <main class="container"> + <h2 class="text-center my-5 font-weight-light">{{ $t("futureexits") }}</h2> + <div class="row mb-4"> + <div class="col-6 m-auto text-center"> + <label for="period" class="form-label">{{ $t("previsions.period.title") }}</label> + <input + type="number" + class="form-control" + :class="{ invalid:periodIsInvalid() }" + id="period" + aria-describedby="periodHelp" + v-model="period" + autocomplete="off" + min="1" max="30" + @keyup="save" /> + <small id="periodHelp" class="form-text text-muted">{{ $t("previsions.period.desc") }}</small> + </div> + </div> + <NavigationLoader :isLoading="$apollo.queries.wwResult.loading" /> + <transition name="fade"> + <div class="alert alert-danger" v-if="error && !$apollo.queries.wwResult.loading">{{ error }}</div> + </transition> + <transition name="fade"> + <div v-if="wwResult && !$apollo.queries.wwResult.loading"> + <div class="row text-center"> + <div class="col-md-6 col-lg-6"> + <h2 class="h4 text-danger">{{ $t("statut.renew") }}</h2> + <MemberList + :members="wwResult['membership']" + :displayPubkey="false" + :displayOnlyDate="true" + :displayDate="false" /> + </div> + <div class="col-md-6 col-lg-6"> + <h2 class="h4 text-danger">{{ $t("statut.manquecertif") }}</h2> + <MemberList + :members="wwResult['outOfCerts']" + :displayPubkey="false" + :displayOnlyDate="true" + :displayDate="false" + :adhesion="false" /> + </div> + </div> + </div> + </transition> + </main> +</template> + +<script> +import { NEXT_EXITS } from "@/graphql/queries.js" + +const day = 24*60*60 +const defaultPeriod = 30*day + +export default { + data() { + return { + breadcrumb: [ + { + text: this.$t('accueil'), + to: '/' + }, + { + text: this.$t('previsions.title'), + to: '/previsions' + }, + { + text: this.$t('futureexits'), + active: true + } + ], + error: null, + period: 30, + display: 'forecastsByNames' + } + }, + methods: { + save() { + this.error = null + localStorage.setItem('previsions_sorties', this.display) + localStorage.setItem('previsions_period', this.getPeriod()/day) + }, + addValue(arr, val) { + if ( + arr.filter(function (e) { + return e.uid === val.uid + }).length == 0 + ) { + arr.push(val) + } + return arr + }, + getPeriod() { + if( this.period != "") { + let tempPeriod = parseInt(this.period) + if(tempPeriod < 1) { + this.error = this.$t("error.tooSmall", [ tempPeriod, 1, 30 ]) + return day + } + if(tempPeriod > 30) { + this.error = this.$t('error.tooBig', [ tempPeriod, 1, 30 ]) + return 30 * day + } + return this.period*day + } + return defaultPeriod + }, + periodIsInvalid() { + return this.period != "" && (this.period < 1 || this.period > 30) + } + }, + apollo: { + wwResult : { + query: NEXT_EXITS, + variables() { + return { period: this.getPeriod()} + }, + update (data) { + let result = { membership: [], outOfCerts: [] } + + for (let i = 0; i < data.memEnds.length; i++) { + let identity = data.memEnds[i] + if(['danger', 'warning'].includes(this.$options.filters.dateStatus(identity.limitDate))) { + this.addValue(result["membership"], identity) + } + if(['danger', 'warning'].includes(this.$options.filters.dateStatus(identity.received_certifications.limit))) { + this.addValue(result["outOfCerts"], identity) + } + } + return result + }, + error (err) {this.error = err.message} + } + }, + nuxtI18n: { + paths: { + fr: '/previsions/futures_sorties', + en: '/forecasts/future_exits', + es: '/pronosticos/futuras_salidas' + } + }, + mounted () { + $nuxt.$emit('changeRoute',this.breadcrumb) + if (localStorage.previsions_sorties) { + this.display = localStorage.getItem('previsions_sorties') + this.period = localStorage.getItem('previsions_period') + } + } +} +</script> + +<style lang="scss" scoped> +.list-group-item { + background: transparent; + &:hover { + background: rgba(0, 0, 255, 0.075); + color: var(--text-primary-color); + } +} + +.forecast_date { + min-width: 150px; +} + +.invalid { + border: 4px solid #ec0404; +} +</style>