mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-27 15:05:38 -04:00
205 lines
7.8 KiB
Vue
205 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
import { DateTime, type DurationInput } from 'luxon';
|
|
import { useNuxtApp } from 'nuxt/app';
|
|
|
|
import useSimpleHead from '~/composables/useSimpleHead.ts';
|
|
import { min, max, MONTHS, PERIODS, type TimesheetData } from '~/src/timesheets.ts';
|
|
|
|
function* range(start: number, end: number) {
|
|
for (let i = start; i <= end; i++) {
|
|
yield i;
|
|
}
|
|
}
|
|
|
|
const { $translator: translator } = useNuxtApp();
|
|
useSimpleHead({
|
|
title: `${translator.translate('admin.header')} • Timesheets overview`,
|
|
}, translator);
|
|
|
|
const { data: timesheets } = await useFetch<Record<string, TimesheetData>>(
|
|
'/api/admin/timesheets',
|
|
{ default: () => ({}) },
|
|
);
|
|
|
|
const firstPeriod = '2020-2022';
|
|
const startYear = ref(PERIODS[firstPeriod][0]);
|
|
const startMonth = ref(PERIODS[firstPeriod][1]);
|
|
const endYear = ref(PERIODS[firstPeriod][2]);
|
|
const endMonth = ref(PERIODS[firstPeriod][3]);
|
|
|
|
const years = [...range(min.year, max.year)];
|
|
|
|
const hoursSummary = computed((): Record<string, number> => {
|
|
const hoursSummary: Record<string, number> = {};
|
|
if (!timesheets.value) {
|
|
return hoursSummary;
|
|
}
|
|
|
|
for (const [username, timesheetData] of Object.entries(timesheets.value)) {
|
|
const timesheet = timesheetData.timesheet;
|
|
let hours = 0;
|
|
for (const year of Object.keys(timesheet)) {
|
|
for (const month of Object.keys(timesheet[parseInt(year)])) {
|
|
if (!isInRange(parseInt(year), parseInt(month))) {
|
|
continue;
|
|
}
|
|
for (const h of Object.values(timesheet[parseInt(year)][month])) {
|
|
hours += h;
|
|
}
|
|
}
|
|
}
|
|
if (hours > 0) {
|
|
hoursSummary[username] = hours;
|
|
}
|
|
}
|
|
return hoursSummary;
|
|
});
|
|
|
|
const hoursSum = computed(() => {
|
|
let sum = 0;
|
|
for (const username in hoursSummary.value) {
|
|
if (!Object.hasOwn(hoursSummary.value, username) || timesheets.value[username].transfer === 'skip') {
|
|
continue;
|
|
}
|
|
sum += hoursSummary.value[username];
|
|
}
|
|
return sum;
|
|
});
|
|
const hoursSumTotal = computed(() => {
|
|
let sum = 0;
|
|
for (const username in hoursSummary.value) {
|
|
sum += hoursSummary.value[username];
|
|
}
|
|
return sum;
|
|
});
|
|
|
|
const setPeriod = (period: keyof typeof PERIODS) => {
|
|
startYear.value = PERIODS[period][0];
|
|
startMonth.value = PERIODS[period][1];
|
|
endYear.value = PERIODS[period][2];
|
|
endMonth.value = PERIODS[period][3];
|
|
};
|
|
|
|
const addPeriod = (offset: DurationInput) => {
|
|
const startTime = DateTime.fromObject({ year: startYear.value, month: startMonth.value }).plus(offset);
|
|
const endTime = DateTime.fromObject({ year: endYear.value, month: endMonth.value }).plus(offset);
|
|
|
|
startYear.value = startTime.year;
|
|
startMonth.value = startTime.month;
|
|
endYear.value = endTime.year;
|
|
endMonth.value = endTime.month;
|
|
};
|
|
const isInRange = (year: number, month: number): boolean => {
|
|
const x = DateTime.local(year, month, 15);
|
|
const begin = DateTime.local(startYear.value, startMonth.value, 1).startOf('month');
|
|
const end = DateTime.local(endYear.value, endMonth.value, 1).endOf('month');
|
|
// @ts-expect-error: DateTime.ts: number not in typings but in code (probably version mismatch)
|
|
return x.ts >= begin.ts && x.ts <= end.ts;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Page wide>
|
|
<NotFound v-if="!$isGranted('panel')" />
|
|
<div v-else>
|
|
<p class="d-flex justify-content-between">
|
|
<nuxt-link to="/admin">
|
|
<Icon v="user-cog" />
|
|
<T>admin.header</T>
|
|
</nuxt-link>
|
|
<span>
|
|
<nuxt-link to="/admin/timesheets">
|
|
<Icon v="file-spreadsheet" />
|
|
Fill out your timesheet
|
|
</nuxt-link>
|
|
<nuxt-link to="/admin/timesheets/expenses">
|
|
<Icon v="file-spreadsheet" />
|
|
Declare expenses
|
|
</nuxt-link>
|
|
</span>
|
|
</p>
|
|
<h2>
|
|
<Icon v="file-spreadsheet" />
|
|
Timesheets overview
|
|
</h2>
|
|
<div class="row my-4">
|
|
<ul class="list-inline">
|
|
<li class="list-inline-item">
|
|
Period:
|
|
</li>
|
|
<li v-for="(_, period) in PERIODS" class="list-inline-item">
|
|
<a href="#" class="btn btn-outline-primary" @click.prevent="setPeriod(period)">{{ period }}</a>
|
|
</li>
|
|
<li class="list-inline-item">
|
|
<a href="#" class="btn btn-outline-primary" @click.prevent="addPeriod({ months: -1 })"><</a>
|
|
</li>
|
|
<li class="list-inline-item">
|
|
<a href="#" class="btn btn-outline-primary" @click.prevent="addPeriod({ months: 1 })">></a>
|
|
</li>
|
|
</ul>
|
|
<div class="input-group mb-3">
|
|
<span class="input-group-text">Start:</span>
|
|
<select v-model.number="startYear" class="form-control">
|
|
<option v-for="year in years" :value="year">
|
|
{{ year }}
|
|
</option>
|
|
</select>
|
|
<select v-model.number="startMonth" class="form-control">
|
|
<option v-for="(month, m) in MONTHS" :value="m">
|
|
{{ month }}
|
|
</option>
|
|
</select>
|
|
<span class="input-group-text">End:</span>
|
|
<select v-model.number="endYear" class="form-control">
|
|
<option v-for="year in years" :value="year">
|
|
{{ year }}
|
|
</option>
|
|
</select>
|
|
<select v-model.number="endMonth" class="form-control">
|
|
<option v-for="(month, m) in MONTHS" :value="m">
|
|
{{ month }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<table class="table table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Hours</th>
|
|
<th>Percentage</th>
|
|
<th>Transfer info</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(hours, username) in hoursSummary">
|
|
<th>
|
|
<LocaleLink locale="_" :link="`/@${username}`">
|
|
@{{ username }}
|
|
</LocaleLink>
|
|
</th>
|
|
<td>{{ hours }}</td>
|
|
<td>{{ hoursSum && timesheets[username].transfer !== 'skip' ? `${(100 * hours / hoursSum).toFixed(1)}%` : '—' }}</td>
|
|
<td>
|
|
<p><strong>{{ timesheets[username].transfer }}</strong></p>
|
|
<ul v-if="timesheets[username]">
|
|
<template v-for="(value, key) in timesheets[username].details">
|
|
<li v-if="value" :key="key">
|
|
<strong>{{ key }}:</strong> {{ value }}
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Sum</th>
|
|
<td>{{ hoursSumTotal }}</td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Page>
|
|
</template>
|