Skip to content

Commit

Permalink
feat(attendance): draft attendance page
Browse files Browse the repository at this point in the history
  • Loading branch information
mtthp committed Jan 5, 2025
1 parent fc4478b commit ea641f1
Show file tree
Hide file tree
Showing 19 changed files with 841 additions and 25 deletions.
Binary file added src/assets/animations/empty-office.lottie
Binary file not shown.
Binary file added src/assets/animations/select-calendar-date.lottie
Binary file not shown.
27 changes: 27 additions & 0 deletions src/components/LoadingProgressBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!-- eslint-disable tailwindcss/no-custom-classname -->
<template>
<div class="flex overflow-hidden bg-transparent">
<!-- https://github.com/tailwindlabs/tailwindcss/discussions/3921#discussioncomment-5258971 -->
<progress
class="progress left-right size-full [&::-moz-progress-bar]:bg-indigo-500 [&::-webkit-progress-bar]:rounded-lg [&::-webkit-progress-bar]:bg-indigo-500 [&::-webkit-progress-value]:rounded-lg [&::-webkit-progress-value]:bg-indigo-500" />
</div>
</template>

<style scoped>
@keyframes indeterminateAnimation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}

.progress {
animation: indeterminateAnimation 1s infinite linear;
transform-origin: 0% 50%;
}
</style>
10 changes: 9 additions & 1 deletion src/components/layout/NavigationDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { doesRouteBelongsTo } from '@/router/helpers';
import { ROUTE_NAMES } from '@/router/names';
import { useAuthStore } from '@/store/auth';
import MembersThumbnail from '@/views/Private/Members/MembersThumbnail.vue';
import { mdiAccountGroup, mdiFinance, mdiHistory } from '@mdi/js';
import { mdiAccountGroup, mdiCalendarMultiselect, mdiFinance, mdiHistory } from '@mdi/js';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { RouteLocationRaw, useRoute } from 'vue-router';
Expand Down Expand Up @@ -88,5 +88,13 @@ const sidebarNavigation = computed<NavigationItem[]>(() => [
icon: mdiHistory,
active: doesRouteBelongsTo(route, ROUTE_NAMES.HISTORY),
},
{
label: i18n.t('navigation.attendance'),
to: {
name: ROUTE_NAMES.ATTENDANCE,
},
icon: mdiCalendarMultiselect,
active: doesRouteBelongsTo(route, ROUTE_NAMES.ATTENDANCE),
},
]);
</script>
4 changes: 4 additions & 0 deletions src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { LOCALE_STORAGE_KEY } from '@/store/settings';
import { createI18nMessage } from '@vuelidate/validators';
import dayjs from 'dayjs';
import calendar from 'dayjs/plugin/calendar.js';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import duration from 'dayjs/plugin/duration.js';
import isBetween from 'dayjs/plugin/isBetween.js';
import LocalizedFormat from 'dayjs/plugin/localizedFormat.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
import updateLocale from 'dayjs/plugin/updateLocale.js';
import weekday from 'dayjs/plugin/weekday.js';
import { createI18n, IntlDateTimeFormats, IntlNumberFormats, PluralizationRule } from 'vue-i18n';
import 'dayjs/locale/fr.js';
import 'dayjs/locale/en-gb.js';
Expand All @@ -16,6 +18,8 @@ dayjs.extend(relativeTime);
dayjs.extend(LocalizedFormat);
dayjs.extend(duration);
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
dayjs.extend(weekday);

dayjs.updateLocale('fr', {
calendar: {
Expand Down
54 changes: 54 additions & 0 deletions src/i18n/locales/fr-FR/attendance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"calendar": {
"tile": {
"attending": "Personne | {count} présent | {count} présents",
"debt": "Aucune dette | {count} présent | {count} présents"
}
},
"description": "Qui était présent ? Qui était absent ? Qui doit de l'argent ?",
"detail": {
"activity": {
"value": {
"FULL": "1 journée complète",
"HALF": "1 demi-journée",
"NONE": "absent"
}
},
"attending": "Aucun présent | 1 seul présent | {count} présents",
"empty": {
"description": "Apparemment, personne ne s'est présenté ce jour-ci.",
"title": "Personne"
},
"search": {
"empty": {
"title": "Aucun résultat"
},
"label": "Rechercher un membre",
"placeholder": "@:attendance.detail.search.label"
},
"select": {
"description": "Pour visualiser les présences et autres informations d'un jour précis.",
"title": "Sélectionner une date"
},
"sort": {
"label": "Trier {suffix}",
"value": {
"activity": "Par activité",
"debt": "Par dette",
"name": "Par nom"
}
}
},
"head": {
"title": "@:attendance.title"
},
"navigation": {
"nextMonth": "Mois suivant",
"previousMonth": "Mois précédent",
"today": "Aujourd'hui"
},
"onFetch": {
"fail": "Impossible de récupérer les présences pour la période du {start} au {end}"
},
"title": "Présence"
}
1 change: 1 addition & 0 deletions src/i18n/locales/fr-FR/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { default as tickets } from './tickets.json';
export { default as activity } from './activity.json';
export { default as errors } from './errors.json';
export { default as audit } from './audit.json';
export { default as attendance } from './attendance.json';
1 change: 1 addition & 0 deletions src/i18n/locales/fr-FR/navigation.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"attendance": "@:attendance.title",
"close": "Fermer le menu",
"history": "@:audit.list.title",
"members": "@:members.list.title",
Expand Down
1 change: 1 addition & 0 deletions src/router/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { flatMapDeep, isEqual } from 'lodash';
const RAW_ROUTE_NAMES = {
LOGIN: 'LOGIN',
HISTORY: 'HISTORY',
ATTENDANCE: 'ATTENDANCE',
STATS: {
INDEX: 'STATS.INDEX',
INCOMES: {
Expand Down
11 changes: 11 additions & 0 deletions src/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ export const routes: RouteRecordRaw[] = [
to: route.query.to,
}),
},
{
path: 'attendance/:date?',
name: ROUTE_NAMES.ATTENDANCE,
component: () => import('@/views/Private/Attendance/AttendancePage.vue'),
props: (route) => ({
month: route.query.month,
date: route.params.date,
search: route.query.search,
sort: route.query.sort,
}),
},
{
path: 'profile',
name: ROUTE_NAMES.USER.PROFILE,
Expand Down
2 changes: 1 addition & 1 deletion src/services/api/activity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import HTTP from '../http';

export const MAX_ATTENDANCE = 28;
export const MAX_ATTENDANCE = 40;

export type ActivityPeriod<PeriodType extends 'year' | 'month' | 'week' | 'day'> = {
date: string;
Expand Down
42 changes: 42 additions & 0 deletions src/services/api/attendance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MemberListItem } from './members';
import HTTP from '../http';

export const MAX_ATTENDANCE = 28;

export type AttendingMember = MemberListItem & {
attendance: {
tickets: {
count: number; // tickets count consumed
amount: number; // amount in euro
debt: {
count: number; // tickets count consumed when not paid yet
amount: number; // debt in euro
};
};
subscriptions: {
count: number; // subscriptions count
amount: number; // amount in euro
};
};
};

export type AttendancePeriod<PeriodType extends 'year' | 'month' | 'week' | 'day'> = {
date: string;
type: PeriodType;
data: {
members: AttendingMember[];
};
};

export const getAttendancePerDay = (
from?: string,
to?: string,
): Promise<AttendancePeriod<'day'>[]> => {
return HTTP.get('/stats/attendance/day', {
params: {
...(from && { from }),
...(to && { to }),
},
timeout: 30_000,
}).then(({ data }) => data);
};
2 changes: 1 addition & 1 deletion src/services/api/members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import HTTP from '../http';
import dayjs from 'dayjs';

export type AttendanceType = 'subscription' | 'ticket';
export type MemberLocation = 'poulailler' | 'pti-poulailler' | 'racine';
export type MemberLocation = 'poulailler' | 'pti-poulailler' | 'racine' | 'cantina';

export interface Attendance {
date: string;
Expand Down
86 changes: 86 additions & 0 deletions src/views/Private/Attendance/AttendanceCalendarTile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<div
:class="[
'flex h-20 flex-col py-2 focus:z-10',
selected
? 'bg-amber-500 text-white shadow-inner hover:bg-amber-700 active:bg-amber-800'
: isCurrentMonth
? 'bg-white hover:bg-gray-100'
: 'bg-gray-50 hover:bg-gray-100',
]">
<time
:class="['ml-auto mr-2 text-sm', selected ? 'font-bold' : 'font-medium']"
:datetime="date">
{{ dayjs(date).format('D') }}
</time>

<div class="ml-2 mt-auto flex flex-row lg:flex-col">
<template v-if="attendance?.data.members.length">
<span
:class="['rounded-md px-1.5 py-0.5 text-xs font-medium lg:hidden', attendingBadgeColor]">
{{ attendance?.data.members.length }}
</span>
<i18n-t
class="inline truncate max-lg:hidden"
keypath="attendance.calendar.tile.attending"
:plural="attendance?.data.members.length"
scope="global"
tag="span">
<template #count>
<span
:class="['inline rounded-md px-1.5 py-0.5 text-xs font-medium', attendingBadgeColor]">
{{ attendance?.data.members.length }}
</span>
</template>
</i18n-t>
</template>
</div>
</div>
</template>

<script setup lang="ts">
import { AttendancePeriod } from '@/services/api/attendance';
import dayjs from 'dayjs';
import { PropType, computed } from 'vue';
const props = defineProps({
date: {
type: String,
default: null,
},
selected: {
type: Boolean,
default: false,
},
selectedMonth: {
type: String,
default: null,
},
attendance: {
type: Object as PropType<AttendancePeriod<'day'>>,
default: null,
},
});
const isToday = computed(() => props.date === dayjs().format('YYYY-MM-DD'));

Check failure on line 65 in src/views/Private/Attendance/AttendanceCalendarTile.vue

View workflow job for this annotation

GitHub Actions / Check for lint and typescript error

'isToday' is declared but its value is never read.
const isCurrentMonth = computed(() => {
return props.selectedMonth && dayjs(props.date).isSame(props.selectedMonth, 'month');
});
const attendingBadgeColor = computed(() => {
if (props.attendance?.data.members.length >= 30) {
return 'text-amber-50 bg-amber-600';
}
if (props.attendance?.data.members.length >= 20) {
return 'text-amber-800 bg-amber-200';
}
if (props.attendance?.data.members.length >= 10) {
return 'text-amber-600 bg-amber-50';
}
return 'text-gray-600 bg-gray-100';
});
</script>
Loading

0 comments on commit ea641f1

Please sign in to comment.