Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • clients/wotwizard-ui
  • manutopik/wotwizard-ui
  • wellno1/wotwizard-ui
3 results
Show changes
Showing
with 1737 additions and 249 deletions
<template>
<span class="badge" :class="displayStatus(membre).class">
{{ this.displayStatus(membre).str }}
</span>
</template>
<script>
export default {
props: {
membre: {
type: Object,
required: true
}
},
methods: {
displayStatus: function (member) {
switch (member.status) {
case "NEWCOMER":
return { str: this.$i18n.t("statut.newcomer"), class: "bg-info" }
case "MISSING":
return { str: this.$i18n.t("statut.missing"), class: "bg-danger" }
case "MEMBER":
return {
str: this.$i18n.t("statut.member"),
class: "bg-success"
}
case "RENEW":
return { str: this.$i18n.t("statut.renew"), class: "bg-warning" }
case "REVOKED":
return {
str: this.$i18n.t("statut.revoked"),
class: "bg-secondary"
}
default:
return "N/A"
}
}
}
}
</script>
<template>
<div class="clipboard input-group mb-3 mx-auto">
<button
id="btncopy"
class="btn btn-secondary px-4 py-1"
type="button"
v-tooltip-click="$t('copie') + ' !'"
@click="copyText">
<solid-share-icon class="icon" aria-hidden="true" />
<span class="visually-hidden">{{ $t("aria.clipboard") }}</span>
</button>
<input
type="text"
class="form-control text-truncate"
:value="textContent"
disabled />
</div>
</template>
<script>
export default {
props: {
textContent: {
type: String,
required: true
}
},
methods: {
copyText() {
navigator.clipboard.writeText(this.textContent)
}
}
}
</script>
<style lang="scss">
.clipboard {
max-width: 500px;
input {
user-select: none;
}
}
</style>
<template>
<button
class="btn btn-secondary"
v-tooltip-click="
$favourites.list.includes(uid)
? $t('suivis.enregistre')
: $t('suivis.supprime')
"
@click="$favourites.toggleFavourite(uid, $event)">
<span class="visually-hidden">{{
$favourites.list.includes(uid)
? $t("suivis.supprimer")
: $t("suivis.ajouter")
}}</span>
<solid-user-add-icon
aria-hidden="true"
style="width: 2rem"
v-if="!$favourites.list.includes(uid)" />
<solid-user-remove-icon
aria-hidden="true"
style="width: 2rem"
v-if="$favourites.list.includes(uid)" />
</button>
</template>
<script>
export default {
props: {
uid: {
type: String,
required: true
}
}
}
</script>
<template>
<button
v-if="membersToAdd.length != 0"
class="btn btn-secondary position-relative"
v-tooltip="{ title: $t('suivis.ajouter'), placement: 'left' }"
@click="$favourites.addFavorisArray(membersToAdd, $event)">
<solid-user-group-icon aria-hidden="true" class="icon" />
<span
aria-hidden="true"
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
+ {{ membersToAdd.length }}
</span>
</button>
</template>
<script>
export default {
props: {
listUID: {
type: Array,
required: true
}
},
computed: {
membersToAdd() {
return this.listUID.filter(
(el) => !this.$favourites.list.includes(el.uid)
)
}
}
}
</script>
<template>
<button type="submit" class="btn btn-primary" :disabled="isWaiting || disabled">
<span v-if="isWaiting">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Chargement...
</span>
<span v-else>Go !</span>
</button>
<button
type="submit"
class="btn btn-primary"
:disabled="isWaiting || disabled">
<span v-if="isWaiting">
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"></span>
{{ $t("chargement") }}...
</span>
<span v-else>Go !</span>
</button>
</template>
<script>
export default {
props: {
isWaiting: Boolean,
disabled: Boolean
}
props: {
isWaiting: Boolean,
disabled: Boolean
}
}
</script>
\ No newline at end of file
</script>
<template>
<div>
<div class="input-group mb-3">
<span class="input-group-text"
><solid-search-icon class="icon" aria-hidden="true"
/></span>
<input
type="text"
class="form-control"
:value="value"
autocomplete="off"
@input="$emit('input', $event.target.value)"
@keyup="$emit('keyup', $event.keyCode)"
:placeholder="$t('recherche.title')"
:aria-label="$t('recherche.title')"
:aria-describedby="help ? 'rechHelp' : null" />
<button
v-if="value != ''"
:title="$t('recherche.effacer')"
class="btn"
type="button"
@click="$emit('erase')">
<solid-x-icon class="icon" aria-hidden="true" />
</button>
</div>
<div
v-if="help"
id="rechHelp"
class="small form-text text-muted text-center">
{{ help }}
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: String
},
help: {
type: String
}
}
}
</script>
<style lang="scss" scoped>
.input-group > * {
border-color: var(--border-color);
}
.btn {
background: var(--bg-menu-color);
color: var(--txt-secondary-color);
&:hover {
background: var(--bg-secondary-color);
}
}
</style>
<template>
<div
class="btn-sort pointer px-2"
tabindex="0"
:title="$t('tri.action')"
@click="sort(fieldName)"
@keyup.enter="sort(fieldName)">
<span class="text-truncate">{{ title }}</span>
<solid-sort-ascending-icon
aria-hidden="true"
class="ms-2 icon flex-shrink-0"
v-if="currentSortDir == 'desc' && currentSort == fieldName" />
<solid-sort-descending-icon
aria-hidden="true"
class="ms-2 icon flex-shrink-0"
v-if="currentSortDir == 'asc' && currentSort == fieldName" />
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true
},
fieldName: {
type: String,
required: true
},
tableName: {
type: String,
required: true
},
currentSort: {
type: String,
required: true
},
currentSortDir: {
type: String,
required: true,
validator: function (value) {
return ["asc", "desc"].indexOf(value) !== -1
}
}
},
methods: {
sort(s) {
if (s === this.currentSort) {
this.$parent.currentSortDir =
this.currentSortDir === "asc" ? "desc" : "asc"
} else {
this.$parent.currentSortDir = "asc"
}
this.$parent.currentSort = s
let query = this.$route.query
let newQuery = {}
let same_array = false
if (Object.keys(query).length !== 0) {
for (const param in query) {
if (param.includes(this.tableName + "_")) {
same_array = true
newQuery[this.tableName + "_" + this.fieldName] =
this.$parent.currentSortDir
} else {
newQuery[param] = query[param]
}
}
}
if (!same_array) {
newQuery[this.tableName + "_" + this.fieldName] =
this.$parent.currentSortDir
}
this.$router.push({
hash: this.$route.hash,
query: newQuery
})
},
retrieveQuery() {
let query = this.$route.query
if (Object.keys(query).length !== 0) {
for (const param in query) {
if (param == this.tableName + "_" + this.fieldName) {
this.$parent.currentSort = this.fieldName
this.$parent.currentSortDir = query[param]
}
}
}
}
},
mounted() {
this.retrieveQuery()
},
watch: {
$route(n, o) {
this.retrieveQuery()
}
}
}
</script>
<style lang="scss">
.btn-sort {
display: flex;
justify-content: center;
align-items: center;
min-height: 50px;
background: var(--bg-secondary-color);
color: var(--txt-secondary-color);
&:focus,
&:hover {
filter: brightness(90%);
}
}
</style>
<template>
<div>
<input @change="toggleTheme" id="checkbox" type="checkbox" class="switch-checkbox" />
<label for="checkbox" class="switch-label d-flex align-items-center justify-content-between position-relative mb-0 ">
<span>🌙</span>
<span>☀️</span>
<div class="switch-toggle position-absolute rounded-circle" :class="{ 'switch-toggle-checked': userTheme === 'dark-theme' }"></div>
</label>
</div>
<div>
<input
@change="toggleTheme"
id="checkbox"
type="checkbox"
class="switch-checkbox" />
<label
for="checkbox"
class="switch-label pointer d-flex align-items-center justify-content-between position-relative mb-0 form-control"
tabindex="0">
<span class="visually-hidden">
{{
this.userTheme === "light-theme"
? $t("aria.themedark")
: $t("aria.themelight")
}}
</span>
<span>🌙</span>
<span>☀️</span>
<div
class="switch-toggle position-absolute rounded-circle"
:class="{ 'switch-toggle-checked': userTheme === 'dark-theme' }"></div>
</label>
</div>
</template>
<script>
export default {
mounted() {
const initUserTheme = this.getMediaPreference();
this.setTheme(initUserTheme);
},
mounted() {
this.setTheme(localStorage.getItem("user-theme"))
},
data() {
return {
userTheme: "light-theme",
};
},
data() {
return {
userTheme: "light-theme"
}
},
methods: {
toggleTheme() {
const activeTheme = localStorage.getItem("user-theme");
if (activeTheme === "light-theme") {
this.setTheme("dark-theme");
} else {
this.setTheme("light-theme");
}
},
methods: {
toggleTheme() {
if (this.userTheme === "light-theme") {
this.setTheme("dark-theme")
} else {
this.setTheme("light-theme")
}
},
setTheme(theme) {
localStorage.setItem("user-theme", theme);
this.userTheme = theme;
document.documentElement.className = theme;
},
getMediaPreference() {
const hasDarkPreference = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (hasDarkPreference) {
return "dark-theme";
} else {
return "light-theme";
}
},
},
};
setTheme(theme) {
if (theme == null) {
theme = "light-theme"
}
localStorage.setItem("user-theme", theme)
this.userTheme = theme
document.documentElement.className = theme
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
*, ::before, ::after {
box-sizing: initial;
*,
::before,
::after {
box-sizing: initial;
}
.switch-checkbox {
display: none;
display: none;
}
.switch-label {
background: var(--text-primary-color);
border-radius: var(--element-size);
cursor: pointer;
font-size: calc(var(--element-size) * 0.3);
height: calc(var(--element-size) * 0.35);
padding: calc(var(--element-size) * 0.1);
transition: background 0.5s ease;
width: var(--element-size);
z-index: 1;
--element-size: 4rem;
background: var(--txt-primary-color);
border-radius: var(--element-size);
font-size: calc(var(--element-size) * 0.3);
height: calc(var(--element-size) * 0.35);
padding: calc(var(--element-size) * 0.1);
transition: background 0.5s ease;
width: var(--element-size);
z-index: 1;
}
.switch-toggle {
background-color: var(--background-color-primary);
top: calc(var(--element-size) * 0.07);
left: calc(var(--element-size) * 0.07);
height: calc(var(--element-size) * 0.4);
width: calc(var(--element-size) * 0.4);
transform: translateX(0);
transition: transform 0.3s ease, background-color 0.5s ease;
background-color: var(--bg-primary-color);
top: calc(var(--element-size) * 0.07);
left: calc(var(--element-size) * 0.07);
height: calc(var(--element-size) * 0.4);
width: calc(var(--element-size) * 0.4);
transform: translateX(0);
transition: transform 0.3s ease, background-color 0.5s ease;
}
.switch-toggle-checked {
transform: translateX(calc(var(--element-size) * 0.6)) !important;
transform: translateX(calc(var(--element-size) * 0.6)) !important;
}
</style>
<template>
<div class="container-lg" v-if="idFromHash.status != 'REVOKED'">
<div class="row mt-3">
<div class="col-sm-10 col-md-8 col-lg-5 mx-auto">
<CertifGroup
:limitDate="idFromHash.certsLimit"
:memberStatus="idFromHash.status"
:certifs="idFromHash.received_certifications"
type="received"
:certifStatus="$options.filters.dateStatus(idFromHash.certsLimit)" />
</div>
<hr class="d-lg-none mt-4" style="height: 10px" />
<div
class="col-1 d-none d-lg-flex"
v-if="['MISSING', 'MEMBER'].includes(idFromHash.status)">
<div class="vr mx-auto"></div>
</div>
<div
class="col-sm-10 col-md-8 col-lg-5 mx-auto"
v-if="['MISSING', 'MEMBER'].includes(idFromHash.status)">
<CertifGroup :certifs="idFromHash.sent_certifications" type="sent" />
</div>
</div>
</div>
</template>
<script>
export default {
props: {
idFromHash: {
type: Object,
required: true
}
}
}
</script>
<template>
<div>
<div
class="d-flex align-items-center justify-content-between flex-column flex-sm-row mb-4">
<h3
class="h4 text-center d-flex"
style="min-width: 0"
:class="{
'text-success': certifStatus == 'success',
'text-warning': certifStatus == 'warning',
'text-danger': certifStatus == 'danger',
'text-info': type == 'sent'
}">
<span class="text-truncate d-block">
{{
type == "sent"
? $t("certification.envoyees")
: $t("certification.recues")
}}
</span>
&nbsp;<BadgeDanger
v-if="type == 'received'"
class="flex-shrink-0"
style="width: 2rem"
:limitDate="limitDate"
:memberStatus="memberStatus" />
</h3>
<BtnFavoriArray
:listUID="certifsNotPending"
v-if="$parent.$parent.registeredAccount.hash != $route.query.hash" />
</div>
<CertifList
:title="$t('certification.enattente')"
:certifs="certifsPending"
:collapseId="type + '-entraitement'" />
<hr v-if="certifsPending.length > 0 && certifsNotPending.length > 0" />
<CertifList
:certifStatus="certifStatus"
:openDefault="true"
:title="$t('certification.encours')"
:certifs="certifsNotPending"
:collapseId="type + '-encours'" />
<hr v-if="certifsExpired.length > 0" />
<CertifList
:title="$t('certification.perimees')"
:certifs="certifsExpired"
:collapseId="type + '-perimees'" />
</div>
</template>
<script>
export default {
props: {
limitDate: Number,
memberStatus: String,
certifs: Array,
certifStatus: {
type: String,
default: ""
},
type: {
type: String,
required: true,
validator: function (value) {
const types = ["received", "sent"]
return types.indexOf(value) !== -1
}
}
},
computed: {
certifsNotPending() {
return this.certifs.filter((el) => el.pending == false)
},
certifsPending() {
return this.certifs.filter((el) => el.pending == true)
},
certifsExpired() {
return this.certifs.filter((el) => el.expired == true)
}
}
}
</script>
<template>
<div class="certifList" v-if="certifs.length > 0">
<button
:title="
isOpen ? $t('certification.masquer') : $t('certification.afficher')
"
@click="isOpen = !isOpen"
class="btn w-100 m-auto d-block rounded-0"
:class="btnClass"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#' + collapseId"
aria-expanded="false"
:aria-controls="collapseId">
<span v-if="!isOpen">
<solid-eye-icon class="icon" aria-hidden="true" />
</span>
<span v-else>
<solid-eye-off-icon class="icon" aria-hidden="true" />
</span>
<h4 class="d-inline align-middle">
{{ title }}&nbsp;&nbsp;
<span class="badge rounded-pill bg-white opacity-75 text-dark">{{
certifs.length
}}</span>
</h4>
</button>
<div
:id="collapseId"
class="table-responsive collapse p-3"
:class="collapseClass">
<BtnSearch
@erase="search = ''"
v-model="search"
class="px-2"
v-if="certifs.length > 5" />
<table
class="table table-striped table-hover table-fixed sortable border m-0">
<thead class="thead-light">
<tr>
<th class="p-0">
<BtnSort
:title="$t('membre.title')"
fieldName="uid"
:tableName="collapseId"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th class="p-0 col-4">
<BtnSort
:title="$t('expire')"
fieldName="expires_on"
:tableName="collapseId"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
</tr>
</thead>
<tbody>
<tr
v-for="certif in certifsTriees"
:key="certif.uid"
tabindex="0"
:title="$t('membre.voirinfos')"
@click="
$router.push(
localePath({
name: 'membres-profil',
query: { hash: certif.hash }
})
)
"
@keyup.enter="
$router.push(
localePath({
name: 'membres-profil',
query: { hash: certif.hash }
})
)
">
<td class="py-1">
<div class="d-flex">
<span v-if="$favourites.list.includes(certif.uid)"
>&nbsp;</span
>
<div class="text-truncate">{{ certif.uid }}</div>
&nbsp;
<BadgeDanger
style="width: 1.2rem"
:limitDate="certif.certsLimit"
:memberStatus="certif.status" />
</div>
<BadgeStatus :membre="certif" />
<BadgeQuality
:quality="certif.quality.ratio"
v-if="!['REVOKED', 'NEWCOMER'].includes(certif.status)" />
<BadgeDispo
:isDispo="certif.minDatePassed"
:dateDispo="certif.minDate"
:certifs="certif.sent_certifications"
v-if="certif.status == 'MEMBER'" />
</td>
<td class="p-0 text-center col-4">
<div class="d-inline-flex flex-column gap-1">
<BadgeDate :date="certif.expires_on" />
<span
class="badge bg-secondary text-truncate d-block"
v-if="certif.pending"
>{{ $t("traitement") }}</span
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
data() {
return {
search: "",
currentSort: "expires_on",
currentSortDir: "asc",
isOpen: this.openDefault
}
},
props: {
certifs: Array,
certifStatus: String,
collapseId: String,
title: String,
openDefault: {
type: Boolean,
default: false
}
},
computed: {
certifsFiltrees() {
return this.certifs.filter((row, index) => {
return (
this.search == "" ||
row.uid.toLowerCase().includes(this.search.toLowerCase())
)
})
},
certifsTriees() {
return this.certifsFiltrees
.map((el) => {
el.status =
this.$options.filters.dateStatus(el.limitDate) == "warning"
? "RENEW"
: el.status
return el
})
.sort((a, b) => {
let modifier = this.currentSortDir === "desc" ? -1 : 1
if (this.currentSort == "expires_on") {
if (a["expires_on"] < b["expires_on"]) return -1 * modifier
if (a["expires_on"] > b["expires_on"]) 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
})
},
collapseClass() {
if (this.collapseId.includes("entraitement")) return "bg-secondary"
if (this.collapseId.includes("perimees")) return "bg-warning"
if (this.collapseId == "sent-encours") return "bg-info"
return {
"bg-success": this.certifStatus == "success",
"bg-warning": this.certifStatus == "warning",
"bg-danger": this.certifStatus == "danger"
}
},
btnClass() {
if (this.collapseId.includes("entraitement")) return "btn-secondary"
if (this.collapseId.includes("perimees")) return "btn-warning"
if (this.collapseId == "sent-encours") return "btn-info"
return {
"btn-success": this.certifStatus == "success",
"btn-warning": this.certifStatus == "warning",
"btn-danger": this.certifStatus == "danger"
}
}
},
mounted() {
if (this.openDefault && this.certifs.length > 0) {
document.querySelector("#" + this.collapseId).classList.add("show")
}
},
watch: {
certifs: {
handler(n, o) {
this.search = ""
}
}
}
}
</script>
<style lang="scss">
.certifList {
.table-responsive tbody {
max-height: 456px;
}
tbody tr {
height: 80px;
}
@media (min-width: 576px) {
button {
font-size: 1.3rem;
}
tbody tr {
height: initial;
}
}
}
</style>
<template>
<div class="card member">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-4">
<h2
class="h1 card-title text-center d-flex align-items-center flex-column m-0">
<span class="text-truncate d-inline-block mw-100">
{{ hash.uid }}
</span>
<small><BadgeStatus class="ms-2" :membre="hash" /></small>
</h2>
<div class="btn-group" role="group">
<button
v-if="
$parent.registeredAccount.uid == '' ||
$parent.registeredAccount.uid == hash.uid
"
class="btn iam"
:class="{
'btn-success': $parent.registeredAccount.uid != hash.uid,
'btn-warning': $parent.registeredAccount.uid == hash.uid
}"
@click="updateCurrentHash($event)">
<span v-if="$parent.registeredAccount.uid != hash.uid">{{
$t("suivis.iam")
}}</span>
<span v-else>{{ $t("suivis.iamnot") }}</span>
</button>
<BtnFavori
:uid="hash.uid"
v-if="$parent.registeredAccount.uid != hash.uid" />
</div>
</div>
<BtnClipboard :textContent="hash.pubkey" />
<AlertMember :hash="hash" />
<div class="table-responsive">
<table
class="table table-sm table-borderless"
v-if="hash.status != 'REVOKED'">
<tbody>
<MemberProp
v-if="hash.status == 'MEMBER'"
:title="$t('membre.referent.title')"
:tooltip="$t('membre.referent.desc')"
:classClor="{
'table-success': hash.sentry,
'table-warning': !hash.sentry
}">
{{ hash.sentry ? $t("oui") : $t("non") }}
</MemberProp>
<MemberProp
v-if="hash.status != 'NEWCOMER'"
:title="$t('membre.qualite.title')"
:tooltip="$t('membre.qualite.desc')"
:classClor="{
'table-success': hash.quality.ratio >= 80,
'table-warning': hash.quality.ratio < 80
}">
{{ Math.round(hash.quality.ratio * 100) / 100 }}</MemberProp
>
<MemberProp
:title="$t('membre.distance.title')"
:tooltip="$t('membre.distance.desc')"
:classClor="{
'table-success':
hash.status != 'NEWCOMER'
? hash.distanceE.dist_ok
: hash.distanceE.dist_ok,
'table-danger':
hash.status != 'NEWCOMER'
? !hash.distanceE.dist_ok
: !hash.distanceE.dist_ok
}">
{{ Math.round(hash.distanceE.value.ratio * 100) / 100 }}
</MemberProp>
<MemberProp
:title="
hash.status != 'MISSING'
? $t('membre.datelimadhesion.title')
: $t('membre.datelimrevoc.title')
"
:tooltip="
hash.status != 'MISSING'
? $t('membre.datelimadhesion.desc')
: $t('membre.datelimrevoc.desc')
"
:classClor="
hash.status != 'MISSING'
? 'table-' + $options.filters.dateStatus(hash.limitDate)
: 'table-danger'
">
{{ $d(new Date(hash.limitDate * 1000)).toLocaleString() }}
</MemberProp>
<MemberProp
v-if="hash.status == 'MEMBER'"
:title="$t('membre.datemanquecertifs.title')"
:tooltip="$t('membre.datemanquecertifs.desc')"
:classClor="
'table-' + $options.filters.dateStatus(hash.certsLimit)
">
{{ $d(new Date(hash.certsLimit * 1000)).toLocaleString() }}
</MemberProp>
<MemberProp
v-if="hash.status == 'MEMBER'"
:title="$t('membre.dispocertif.title')"
:tooltip="$t('membre.dispocertif.desc')"
:classClor="{
'table-success': hash.minDatePassed,
'table-danger': !hash.minDatePassed
}">
{{ hash.minDatePassed ? $t("oui") : $t("non") }}
<small v-if="!hash.minDatePassed"
>( > {{ $d(new Date(hash.minDate * 1000)).toLocaleString() }} )</small
>
</MemberProp>
<MemberProp
v-if="hash.status == 'MEMBER'"
:title="$t('membre.nb_certifs.title')"
:tooltip="$t('membre.nb_certifs.desc')"
:classClor="{
'table-success': sentCertNotExpired.length <= 80,
'table-warning': sentCertNotExpired.length > 80,
'table-danger': sentCertNotExpired.length > 90
}">
{{ 100 - sentCertNotExpired.length }}
</MemberProp>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
hash: Object
},
computed: {
sentCertNotExpired() {
return this.hash.sent_certifications.filter((el) => !el.expired)
}
},
methods: {
updateCurrentHash(e) {
if (this.$parent.registeredAccount.uid == this.hash.uid) {
this.$parent.registeredAccount = { hash: "", uid: "" }
} else {
this.$parent.registeredAccount = {
hash: this.hash.hash,
uid: this.hash.uid
}
if (this.$favourites.list.includes(this.hash.uid)) {
this.$favourites.toggleFavourite(this.hash.uid, e)
}
}
}
}
}
</script>
<style lang="scss">
.btn.iam:not(.btn-warning) {
z-index: 10;
animation: zoom-in-zoom-out 1s ease infinite;
}
@keyframes zoom-in-zoom-out {
50% {
opacity: 0.3;
}
}
.member {
h2 {
min-width: 0;
}
.table {
text-align: center;
width: auto;
margin: auto;
}
}
@media (min-width: 576px) {
.member .table {
th {
text-align: right;
}
td {
text-align: left;
}
}
}
</style>
<template>
<div class="text-muted">
<p id="filterStatutTitle">{{ $t("filter.statut") }}&nbsp;:</p>
<ul class="p-0 m-0" aria-labelledby="filterStatutTitle" role="group">
<li
class="form-check form-check-inline mb-3"
v-if="type != 'certificateurs'">
<input
class="btn-check"
v-model="checkedStatus"
type="checkbox"
:id="'check-newcomer-' + _uid"
value="NEWCOMER"
@change="fixColumns" />
<label class="btn btn-outline-info" :for="'check-newcomer-' + _uid">{{
$t("statut.newcomer")
}}</label>
</li>
<li class="form-check form-check-inline mb-3">
<input
class="btn-check"
v-model="checkedStatus"
type="checkbox"
:id="'check-member-' + _uid"
value="MEMBER"
@change="fixColumns" />
<label class="btn btn-outline-success" :for="'check-member-' + _uid">{{
$t("statut.member")
}}</label>
</li>
<li class="form-check form-check-inline mb-3">
<input
class="btn-check"
v-model="checkedStatus"
type="checkbox"
:id="'check-renew-' + _uid"
value="RENEW"
@change="fixColumns" />
<label class="btn btn-outline-warning" :for="'check-renew-' + _uid">{{
$t("statut.renew")
}}</label>
</li>
<li class="form-check form-check-inline mb-3">
<input
class="btn-check"
v-model="checkedStatus"
type="checkbox"
:id="'check-missing-' + _uid"
value="MISSING"
@change="fixColumns" />
<label class="btn btn-outline-danger" :for="'check-missing-' + _uid">{{
$t("statut.missing")
}}</label>
</li>
<li class="form-check form-check-inline mb-3" v-if="type == 'favoris'">
<input
class="btn-check"
v-model="checkedStatus"
type="checkbox"
:id="'check-revoked-' + _uid"
value="REVOKED"
@change="fixColumns" />
<label
class="btn btn-outline-secondary"
:for="'check-revoked-' + _uid"
>{{ $t("statut.revoked") }}</label
>
</li>
</ul>
<div v-if="type != 'favoris'">
<p>{{ $t("filter.certif") }}&nbsp;:</p>
<input
class="btn-check"
v-model="certifStatus"
type="radio"
:id="'radio-current-' + _uid"
value="current"
autocomplete="off" />
<label
class="btn btn-outline-success me-3 mb-3"
:for="'radio-current-' + _uid">
{{ $t("certification.title") + " " + $t("certification.encours") }}
</label>
<input
class="btn-check"
v-model="certifStatus"
type="radio"
:id="'radio-outdated-' + _uid"
value="outdated"
autocomplete="off" />
<label
class="btn btn-outline-danger mb-3"
:for="'radio-outdated-' + _uid">
{{ $t("certification.title") + " " + $t("certification.perimees") }}
</label>
</div>
</div>
</template>
<script>
export default {
data() {
return {
checkedStatus: ["NEWCOMER", "MEMBER", "RENEW", "MISSING", ""],
certifStatus: "current"
}
},
props: {
type: {
type: String,
default: ""
}
},
methods: {
fixColumns(e) {
setTimeout(() => {
this.$favourites.fixColumns()
}, 5)
}
},
mounted() {
this.$emit("update:selectedStatus", this.checkedStatus)
this.$emit("update:selectedCertifStatus", this.certifStatus)
},
watch: {
checkedStatus(n, o) {
this.$emit("update:selectedStatus", n)
},
certifStatus(n, o) {
this.$emit("update:selectedCertifStatus", n)
}
}
}
</script>
<template>
<div class="table-responsive pb-3">
<table
class="table table-striped table-hover table-fixed sortable border text-center">
<thead class="thead-light">
<tr>
<th class="p-0" scope="col">
<BtnSort
fieldName="uid"
:tableName="id"
title="UID"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th
scope="col"
class="d-none d-lg-table-cell p-0"
v-if="id != 'default'">
<BtnSort
fieldName="statut"
:tableName="id"
:title="$t('statut.title')"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th
scope="col"
class="td-quality d-none d-lg-table-cell p-0"
v-if="
['favoris', 'search', 'certificateurs', 'certifies'].includes(id)
">
<BtnSort
fieldName="quality"
:tableName="id"
:title="$t('membre.qualite.title')"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th
scope="col"
class="d-none d-xl-table-cell p-0"
v-if="
['favoris', 'search', 'certificateurs', 'certifies'].includes(id)
">
<BtnSort
fieldName="dispo"
:tableName="id"
:title="$t('membre.disponibilite')"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th
scope="col"
class="td-date d-none d-sm-table-cell p-0"
v-if="
[
'adhesion',
'favoris',
'search',
'certificateurs',
'certifies'
].includes(id)
">
<BtnSort
fieldName="date_membership"
:tableName="id"
:title="
['certif', 'adhesion'].includes(id)
? $t('date')
: $t('membre.datelimpertestatut')
"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th
scope="col"
class="td-date d-none p-0"
:class="{
'd-sm-table-cell': id == 'certif',
'd-md-table-cell': id != 'certif'
}"
v-if="
[
'certif',
'favoris',
'search',
'certificateurs',
'certifies'
].includes(id)
">
<BtnSort
fieldName="date_certs"
:tableName="id"
:title="
['certif', 'adhesion'].includes(id)
? $t('date')
: $t('membre.datemanquecertifs.title')
"
:currentSort="currentSort"
:currentSortDir="currentSortDir" />
</th>
<th v-if="id == 'favoris'" style="width: 60px"></th>
</tr>
</thead>
<tbody>
<tr
v-for="member in membersSorted"
:key="member.uid"
tabindex="0"
:title="$t('membre.voirinfos')"
@click="redirect(member.hash)"
@keyup.enter="redirect(member.hash)">
<td>
<div class="d-flex">
<div
class="d-flex flex-column align-items-center justify-content-evenly flex-grow-1 mw-100">
<div class="d-flex justify-content-center mw-100">
<span v-if="$favourites.list.includes(member.uid)"
>&nbsp;</span
>
<div class="text-truncate">{{ member.uid }}</div>
&nbsp;
<BadgeDanger
style="width: 1.2rem"
:limitDate="member.certsLimit"
:memberStatus="member.status" />
</div>
<div class="text-muted small">
{{ member.pubkey.substring(0, 10) }}
</div>
<div
v-if="['adhesion', 'certif'].includes(id)"
class="d-sm-none">
<BadgeDate :date="getDate(member)" />
</div>
</div>
<div
class="w-50 d-flex flex-column align-items-center justify-content-evenly gap-1 d-lg-none"
v-if="
['favoris', 'search', 'certificateurs', 'certifies'].includes(
id
)
">
<BadgeStatus :membre="member" class="mw-100 text-truncate" />
<BadgeQuality :quality="member.quality.ratio" />
<BadgeDispo
:isDispo="member.minDatePassed"
:dateDispo="member.minDate"
:certifs="member.sent_certifications"
class="mw-100 text-truncate" />
</div>
</div>
</td>
<td class="d-none d-lg-table-cell" v-if="id != 'default'">
<BadgeStatus :membre="member" />
</td>
<td
class="d-none d-lg-table-cell"
v-if="
['favoris', 'search', 'certificateurs', 'certifies'].includes(id)
">
<BadgeQuality :quality="member.quality.ratio" />
</td>
<td
class="d-none d-xl-table-cell"
v-if="
['favoris', 'search', 'certificateurs', 'certifies'].includes(id)
">
<BadgeDispo
:isDispo="member.minDatePassed"
:dateDispo="member.minDate"
:certifs="member.sent_certifications" />
</td>
<td
class="d-none d-sm-table-cell"
v-if="
[
'adhesion',
'favoris',
'search',
'certificateurs',
'certifies'
].includes(id)
">
<BadgeDate :date="member.limitDate" />
</td>
<td
class="d-none"
:class="{
'd-sm-table-cell': id == 'certif',
'd-md-table-cell': id != 'certif'
}"
v-if="
[
'certif',
'favoris',
'search',
'certificateurs',
'certifies'
].includes(id)
">
<BadgeDate :date="member.certsLimit" />
</td>
<td class="py-1" v-if="id == 'favoris'" style="width: 60px">
<button
class="btn btn-danger"
v-if="$favourites.list.includes(member.uid)"
@click="$favourites.toggleFavourite(member.uid, $event)"
:title="$t('suivis.supprimer')">
<solid-trash-icon class="icon" aria-hidden="true" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data() {
return {
currentSort: this.defaultSort,
currentSortDir: this.defaultSortDir
}
},
props: {
members: {
type: Array,
required: true
},
id: {
type: String,
required: true
},
defaultSortDir: {
type: String,
default: "asc"
},
defaultSort: {
type: String,
default: "uid"
}
},
methods: {
redirect(hash) {
this.$router.push(
this.localePath({ name: "membres-profil", query: { hash } })
)
},
getDate(member) {
if (this.id == "adhesion") return member.limitDate
if (this.id == "certif") return member.certsLimit
return Math.min(member.limitDate, member.certsLimit)
},
getOrder(a, b, order) {
if (a < b) return -1 * order
if (a > b) return 1 * order
return 0
}
},
computed: {
membersSorted() {
return this.members.sort((a, b) => {
let modifier = this.currentSortDir === "desc" ? -1 : 1
if (this.currentSort == "uid") {
return this.getOrder(
a["uid"].toLowerCase(),
b["uid"].toLowerCase(),
modifier
)
} else if (this.currentSort == "dispo") {
if (a.minDate == null) return 1 * modifier
if (b.minDate == null) return -1 * modifier
return this.getOrder(a.minDate, b.minDate, modifier)
} else if (this.currentSort == "quality") {
return this.getOrder(a.quality.ratio, b.quality.ratio, modifier)
} else if (this.currentSort == "statut") {
return this.getOrder(a["status"], b["status"], modifier)
} else if (this.currentSort == "date_membership") {
return this.getOrder(a["limitDate"], b["limitDate"], modifier)
} else if (this.currentSort == "date_certs") {
return this.getOrder(a["certsLimit"], b["certsLimit"], modifier)
}
return 0
})
}
},
mounted() {
this.$favourites.fixColumns()
}
}
</script>
<template>
<tr class="help" v-tooltip="{ title: tooltip, placement: 'right' }">
<th scope="row" class="fw-normal">{{ title }}&nbsp;:</th>
<td :class="classClor">
<slot></slot>
</td>
</tr>
</template>
<script>
export default {
props: {
title: String,
tooltip: {
type: String,
default: ""
},
classClor: [String, Object]
}
}
</script>
<style lang="scss" scoped>
tr {
display: flex;
flex-direction: column;
user-select: none;
}
@media (min-width: 576px) {
tr {
display: table-row;
}
}
</style>
<template>
<header>
<div class="position-relative">
<button class="toggle btn border-secondary position-absolute p-1 m-1 ml-3" @click="toggleMenu"><span></span></button>
<NavigationBreadcrumb :breadcrumb="breadcrumb" class="breadcrumb p-1" />
</div>
<NavigationMenuSidebar @toggleMenu="toggleMenu" :menus="menus" />
<div class="bg_overlay position-fixed" @click="toggleMenu"></div>
</header>
<header class="header position-fixed">
<div class="position-relative">
<button
:title="isOpen ? $t('aria.closemenu') : $t('aria.openmenu')"
class="toggle btn border-secondary position-absolute p-1 ms-4"
@click="toggleMenu">
<span></span>
</button>
<NavigationBreadcrumb :breadcrumb="breadcrumb" />
</div>
<NavigationMenuSidebar @toggleMenu="toggleMenu" :menus="menus" />
<div class="bg_overlay position-fixed" @click="toggleMenu"></div>
</header>
</template>
<script>
export default {
props: {
breadcrumb: Array,
menus: Array
},
methods: {
toggleMenu() {
document.querySelector('.app').classList.toggle('open')
}
}
data() {
return {
isOpen: false
}
},
props: {
breadcrumb: Array,
menus: Array
},
methods: {
toggleMenu() {
document.querySelector(".app").classList.toggle("open")
this.isOpen = !this.isOpen
localStorage.setItem("menu-open", this.isOpen)
}
},
mounted() {
this.isOpen = localStorage.getItem("menu-open") == "true"
if (this.isOpen) {
document.querySelector(".app").classList.add("open")
}
}
}
</script>
<style lang="scss">
$btn-width: 50px;
nav.breadcrumb {
margin: .5rem .5rem .5rem 5rem;
.header {
--menu-width: 0px;
width: 100%;
z-index: 100;
background: var(--bg-primary-color);
transition: width 0.5s ease-in-out;
a {color: var(--text-primary-color)}
.open & {
--menu-width: 320px;
}
.breadcrumb-item.active {
opacity: .7;
}
@media (min-width: 1200px) {
width: calc(99vw - var(--menu-width));
}
}
nav.breadcrumb-wrapper {
min-height: 80px;
margin: 8px 15px;
padding: 1rem 1rem 1rem 4.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
background: var(--bg-menu-color);
a {
color: var(--txt-primary-color);
}
.breadcrumb-item.active {
opacity: 0.7;
}
@media (min-width: 768px) {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
%hamburger-line {
display: block;
height: 4px;
width: .8 * $btn-width;
background: var(--text-primary-color);
content: "";
position: absolute;
transition-property: transform;
border-radius: 4px;
display: block;
height: 4px;
width: 0.8 * $btn-width;
background: var(--txt-primary-color);
content: "";
position: absolute;
transition-property: transform;
border-radius: 4px;
}
.toggle {
height: $btn-width;
width: $btn-width;
line-height: $btn-width;
span {
@extend %hamburger-line;
top: 50%;
transform: translateY(-50%);
transition-timing-function: cubic-bezier(.55,.055,.675,.19);
transition-duration: 75ms;
.open & {
transform: rotate(45deg);
display: block;
margin-top: -2px;
}
&::before {
transition: top 75ms ease .12s,opacity 75ms ease;
@extend %hamburger-line;
top: -10px;
.open & {
opacity: 0;
}
}
&::after {
transition: bottom 75ms ease .12s,transform 75ms cubic-bezier(.55,.055,.675,.19);
@extend %hamburger-line;
bottom: -10px;
.open & {
top: 0;
transform: rotate(-90deg);
}
}
}
height: $btn-width;
width: $btn-width;
line-height: $btn-width;
top: 1rem;
span {
@extend %hamburger-line;
top: 50%;
transform: translateY(-50%);
transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
transition-duration: 75ms;
.open & {
transform: rotate(45deg);
display: block;
margin-top: -2px;
}
&::before {
transition: top 75ms ease 0.12s, opacity 75ms ease;
@extend %hamburger-line;
top: -10px;
.open & {
opacity: 0;
}
}
&::after {
transition: bottom 75ms ease 0.12s,
transform 75ms cubic-bezier(0.55, 0.055, 0.675, 0.19);
@extend %hamburger-line;
bottom: -10px;
.open & {
top: 0;
transform: rotate(-90deg);
}
}
}
}
.menu {
background: var(--background-color-primary);
width: var(--menu-size);
top: 0;
z-index: 1200;
height: 100vh;
padding: 1.1rem 0.5rem;
overflow-y: scroll;
scrollbar-color: #6969dd #e0e0e0;
scrollbar-width: thin;
transition: left .5s ease-in-out;
left: -400px;
h1 {color: var(--text-primary-color);}
.list-group-item div {
transition: left .3s ease-in-out;
left: 0;
&::before {
content: "›";
position: relative;
left: -.5em;
}
&:hover {
left: .5em;
}
}
.open & {
left: 0;
}
.close {
--size: 50px;
width: var(--size);
height: var(--size);
top: .8rem;
right: 0;
font-size: 2rem;
}
h2 {
cursor: default;
}
width: var(--menu-size);
top: 0;
z-index: 1200;
height: 100%;
transition: left 0.5s ease-in-out;
left: -400px;
h1 {
color: var(--txt-primary-color);
}
.open & {
left: 0;
}
.btn-close {
--size: 30px;
width: var(--size);
height: var(--size);
top: 0.8rem;
right: 0.8rem;
.dark-theme & {
filter: invert(1) grayscale(100%) brightness(200%);
}
}
h2 {
cursor: default;
}
}
.bg_overlay {
top: 0;
left: 0;
right: 0;
height: 120vh;
z-index: 1100;
visibility: hidden;
opacity: 0;
transition: all .5s ease;
background-color: rgba(34,41,47,.5);
.open & {
opacity: 1;
visibility: visible;
}
top: 0;
left: 0;
right: 0;
height: 120vh;
z-index: 1100;
visibility: hidden;
opacity: 0;
transition: all 0.5s ease;
background-color: rgba(34, 41, 47, 0.5);
.open & {
opacity: 1;
visibility: visible;
}
}
.logo {
max-width: 75px;
&:hover {
text-decoration: none;
}
img {
max-width: 75px;
}
}
@media (min-width:1200px) {
.open {
&.app {
margin-left: var(--menu-size);
}
.menu {
left: 0;
}
.bg_overlay {
visibility: hidden;
opacity: 0;
}
}
@media (min-width: 1200px) {
.open {
&.app {
margin-left: var(--menu-size);
}
.menu {
left: 0;
}
.bg_overlay {
visibility: hidden;
opacity: 0;
}
}
}
</style>
<template>
<nav aria-label="Fil d'Ariane" class="justify-content-between align-items-center">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item" :class="{ 'active': item.active }" :aria-current="item.active ? 'page' : null" v-for="item in breadcrumb" :key="item.text">
<NuxtLink :to="item.to" v-if="item.to">{{ item.text }}</NuxtLink>
<span v-else>{{ item.text }}</span>
</li>
</ol>
<BtnTheme />
</nav>
<nav
:aria-label="$t('aria.ariane')"
class="breadcrumb-wrapper rounded border">
<ol class="breadcrumb m-0 p-0 d-none d-sm-flex">
<li
class="breadcrumb-item"
:class="{ active: item.active }"
:aria-current="item.active ? 'page' : null"
v-for="item in breadcrumb"
:key="item.text">
<NuxtLink :to="localePath(item.to)" v-if="item.to">{{
item.text
}}</NuxtLink>
<span v-else>{{ item.text }}</span>
</li>
</ol>
<div class="d-flex justify-content-between align-items-center">
<NavigationLanguage class="me-3" />
<BtnTheme />
</div>
</nav>
</template>
<script>
export default {
props: {
breadcrumb: Array
}
props: {
breadcrumb: Array
}
}
</script>
\ No newline at end of file
</script>
<template>
<div>
<select
class="form-select"
:aria-label="$t('lang')"
@change="saveLocale($event)"
v-model="activeLang">
<option
v-for="lang in $i18n.locales"
:key="lang.code"
:value="lang.code"
:selected="lang.code === activeLang">
{{ lang.name }}
</option>
</select>
</div>
</template>
<script>
export default {
data() {
return {
activeLang: "en"
}
},
methods: {
saveLocale(e) {
// this.$i18n.locale = e.target.value
this.$i18n.setLocale(e.target.value)
// this.$i18n.setLocaleCookie(e.target.value)
// this.$router.push(this.switchLocalePath(e.target.value))
// this.$router.replace(this.switchLocalePath(e))
}
},
mounted() {
this.activeLang = this.$i18n.locale
}
}
</script>
<template>
<div class="spinner-border text-primary mx-auto" role="status" v-if="isLoading">
<span class="sr-only">Chargement...</span>
</div>
<transition name="fade">
<div
class="loader overflow-hidden text-center position-absolute"
v-if="isLoading">
<img src="~@/assets/img/loader.gif" />
<div class="text-center font-weight-bold my-3">
{{ $t("chargement") }}...
</div>
</div>
</transition>
</template>
<script>
export default {
props: {isLoading: Boolean}
props: { isLoading: Boolean }
}
</script>
\ No newline at end of file
</script>
<style lang="scss">
.loader {
z-index: 50;
left: 50%;
transform: translateX(-50%);
--color: #391855;
color: var(--txt-primary-color);
}
</style>
<template>
<div class="mb-4">
<h2 class="small text-muted text-uppercase ml-4 mb-3">{{ menu.title }}</h2>
<div class="nav navbar-nav list-group list-group-flush">
<NuxtLink class="list-group-item list-group-item-action" :to="item.path" v-for="item in menu.items" :key="item.path"><div class="position-relative">{{ item.title }}</div></NuxtLink>
</div>
</div>
<div class="mb-4">
<h2 class="small text-muted text-uppercase ms-5 mb-0 pb-2">
{{ $t(menu.title) }}
</h2>
<div class="nav navbar-nav list-group list-group-flush">
<NuxtLink
class="list-group-item list-group-item-action p-0 ps-3 border-0"
:to="localePath(item.path)"
v-for="item in menu.items"
:key="item.path"
@click.native="toggleMenu()">
<div class="menu-item position-relative py-2">
<component
aria-hidden="true"
:is="'solid-' + item.icon + '-icon'"
class="icon" />&nbsp;{{ $t(item.title) }}
</div>
</NuxtLink>
</div>
</div>
</template>
<script>
export default {
props: {
menu: Object
}
props: {
menu: Object
},
methods: {
toggleMenu() {
if (window.innerWidth < 1200) {
this.$emit("toggleMenu")
}
}
}
}
</script>
\ No newline at end of file
</script>
<style lang="scss" scoped>
h2 {
letter-spacing: 0.02rem;
}
</style>