mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
[admin] timesheets overview
This commit is contained in:
parent
fe1fad6f0f
commit
89e9bceac3
@ -49,6 +49,7 @@
|
|||||||
'names',
|
'names',
|
||||||
'translations',
|
'translations',
|
||||||
'code',
|
'code',
|
||||||
|
'org',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -333,6 +333,7 @@ export default {
|
|||||||
routes.push({ path: '/admin/profiles', component: resolve(__dirname, 'routes/adminProfiles.vue') });
|
routes.push({ path: '/admin/profiles', component: resolve(__dirname, 'routes/adminProfiles.vue') });
|
||||||
|
|
||||||
routes.push({ path: '/admin/timesheets', component: resolve(__dirname, 'routes/adminTimesheets.vue') });
|
routes.push({ path: '/admin/timesheets', component: resolve(__dirname, 'routes/adminTimesheets.vue') });
|
||||||
|
routes.push({ path: '/admin/timesheets/overview', component: resolve(__dirname, 'routes/adminTimesheetsOverview.vue') });
|
||||||
|
|
||||||
routes.push({ path: '/admin/moderation', component: resolve(__dirname, 'routes/adminModeration.vue') });
|
routes.push({ path: '/admin/moderation', component: resolve(__dirname, 'routes/adminModeration.vue') });
|
||||||
routes.push({ path: '/admin/abuse-reports', component: resolve(__dirname, 'routes/adminAbuseReports.vue') });
|
routes.push({ path: '/admin/abuse-reports', component: resolve(__dirname, 'routes/adminAbuseReports.vue') });
|
||||||
|
@ -107,6 +107,13 @@
|
|||||||
link="/admin/timesheets"
|
link="/admin/timesheets"
|
||||||
header="Volunteering timesheets"
|
header="Volunteering timesheets"
|
||||||
/>
|
/>
|
||||||
|
<AdminDashboardCard
|
||||||
|
v-if="$isGranted('org')"
|
||||||
|
v-show="!filterAttention"
|
||||||
|
icon="file-spreadsheet"
|
||||||
|
link="/admin/timesheets/overview"
|
||||||
|
header="Timesheets overview"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-for="({name, config: localeConfig, url, published}, locale) in allLocales" v-if="$isGranted('panel', locale) && stats[locale]">
|
<template v-for="({name, config: localeConfig, url, published}, locale) in allLocales" v-if="$isGranted('panel', locale) && stats[locale]">
|
||||||
|
@ -117,45 +117,8 @@
|
|||||||
import {head} from "../src/helpers";
|
import {head} from "../src/helpers";
|
||||||
import {DateTime} from 'luxon';
|
import {DateTime} from 'luxon';
|
||||||
import gm from 'avris-generator';
|
import gm from 'avris-generator';
|
||||||
|
import {min, max, closed, MONTHS, AREAS, TRANSFER_METHODS} from '../src/timesheets';
|
||||||
|
|
||||||
const max = DateTime.now();
|
|
||||||
const min = DateTime.local(2020, 1, 1);
|
|
||||||
const closed = DateTime.local(2020, 1, 1); // TODO DateTime.local(2023, 1, 1);
|
|
||||||
|
|
||||||
const AREAS = [
|
|
||||||
'translation',
|
|
||||||
'moderation',
|
|
||||||
'content creation',
|
|
||||||
'coding',
|
|
||||||
'devops',
|
|
||||||
'user support',
|
|
||||||
'social media',
|
|
||||||
'media interviews',
|
|
||||||
'design',
|
|
||||||
'sensitivity reviews',
|
|
||||||
'administration',
|
|
||||||
'other',
|
|
||||||
];
|
|
||||||
const MONTHS = {
|
|
||||||
1: 'Jan',
|
|
||||||
2: 'Feb',
|
|
||||||
3: 'Mar',
|
|
||||||
4: 'Apr',
|
|
||||||
5: 'May',
|
|
||||||
6: 'Jun',
|
|
||||||
7: 'Jul',
|
|
||||||
8: 'Aug',
|
|
||||||
9: 'Sep',
|
|
||||||
10: 'Oct',
|
|
||||||
11: 'Nov',
|
|
||||||
12: 'Dec',
|
|
||||||
};
|
|
||||||
const TRANSFER_METHODS = {
|
|
||||||
'bank': 'Bank transfer',
|
|
||||||
'paypal': 'PayPal',
|
|
||||||
'charity': 'Charity',
|
|
||||||
'skip': 'Skip allowance',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
142
routes/adminTimesheetsOverview.vue
Normal file
142
routes/adminTimesheetsOverview.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<Page wide>
|
||||||
|
<NotFound v-if="!$isGranted('org')"/>
|
||||||
|
<div v-else>
|
||||||
|
<p>
|
||||||
|
<nuxt-link to="/admin">
|
||||||
|
<Icon v="user-cog"/>
|
||||||
|
<T>admin.header</T>
|
||||||
|
</nuxt-link>
|
||||||
|
</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="#" @click.prevent="setPeriod(period)" class="btn btn-outline-primary">{{period}}</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 && username !== 'andrea' ? ((100 * hours / hoursSum).toFixed(1) + '%') : '—'}}</td>
|
||||||
|
<td>
|
||||||
|
<p><strong>{{timesheets[username].transfer}}</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(value, key) in timesheets[username].details" v-if="value">
|
||||||
|
<strong>{{ key }}:</strong> {{ value }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {head} from "../src/helpers";
|
||||||
|
import {DateTime} from 'luxon';
|
||||||
|
import {min, max, MONTHS, PERIODS} from '../src/timesheets';
|
||||||
|
|
||||||
|
function* range(start, end) {
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
yield i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
const period = '2020-2022';
|
||||||
|
const [startYear, startMonth, endYear, endMonth] = PERIODS[period];
|
||||||
|
return {
|
||||||
|
PERIODS,
|
||||||
|
MONTHS,
|
||||||
|
startYear, startMonth, endYear, endMonth,
|
||||||
|
years: [...range(min.year, max.year)],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async asyncData({ app }) {
|
||||||
|
return {
|
||||||
|
timesheets: await app.$axios.$get(`/admin/timesheets`),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setPeriod(period) {
|
||||||
|
[this.startYear, this.startMonth, this.endYear, this.endMonth] = PERIODS[period];
|
||||||
|
},
|
||||||
|
isInRange(year, month) {
|
||||||
|
const x = DateTime.local(year, month, 15);
|
||||||
|
return x >= DateTime.local(this.startYear, this.startMonth, 1) && x <= DateTime.local(this.endYear, this.endMonth, 30);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hoursSummary() {
|
||||||
|
const hoursSummary = {};
|
||||||
|
for (let username in this.timesheets) {
|
||||||
|
if (!this.timesheets.hasOwnProperty(username)) { continue; }
|
||||||
|
let hours = 0;
|
||||||
|
const timesheet = this.timesheets[username].timesheet;
|
||||||
|
for (let year in timesheet) {
|
||||||
|
if (!timesheet.hasOwnProperty(year)) { continue; }
|
||||||
|
for (let month in timesheet[year]) {
|
||||||
|
if (!timesheet[year].hasOwnProperty(month)) { continue; }
|
||||||
|
if (!this.isInRange(parseInt(year), parseInt(month))) { continue; }
|
||||||
|
for (let h of Object.values(timesheet[year][month])) {
|
||||||
|
hours += h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hours > 0) {
|
||||||
|
hoursSummary[username] = hours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hoursSummary;
|
||||||
|
},
|
||||||
|
hoursSum() {
|
||||||
|
let sum = 0;
|
||||||
|
for (let username in this.hoursSummary) {
|
||||||
|
if (!this.hoursSummary.hasOwnProperty(username) || username === 'andrea') { continue; }
|
||||||
|
sum += this.hoursSummary[username];
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return head({
|
||||||
|
title: this.$t('admin.header') + ' • Timesheets overview',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -15,7 +15,7 @@ const isGranted = (user, locale, area) => {
|
|||||||
}
|
}
|
||||||
const [ permissionLocale, permissionArea ] = permission.split('-');
|
const [ permissionLocale, permissionArea ] = permission.split('-');
|
||||||
if ((permissionLocale === '*' || permissionLocale === locale || locale === null)
|
if ((permissionLocale === '*' || permissionLocale === locale || locale === null)
|
||||||
&& ((permissionArea === '*' && area !== 'code') || permissionArea === area || area === '' || area === 'panel')
|
&& ((permissionArea === '*' && area !== 'code' && area !== 'org') || permissionArea === area || area === '' || area === 'panel')
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -567,4 +567,17 @@ router.post('/admin/timesheet', handleErrorAsync(async (req, res) => {
|
|||||||
return res.json('OK');
|
return res.json('OK');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
router.get('/admin/timesheets', handleErrorAsync(async (req, res) => {
|
||||||
|
if (!req.isGranted('org')) {
|
||||||
|
return res.status(401).json({error: 'Unauthorised'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timesheetsByUsername = {};
|
||||||
|
for (let {username, timesheets} of await req.db.all(SQL`SELECT username, timesheets FROM users WHERE timesheets IS NOT NULL`)) {
|
||||||
|
timesheetsByUsername[username] = JSON.parse(timesheets);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(timesheetsByUsername);
|
||||||
|
}));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -174,12 +174,12 @@ export const isGranted = (user, locale, area = '') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let permission of user.roles.split('|')) {
|
for (let permission of user.roles.split('|')) {
|
||||||
if (permission === '*' && area !== 'code') {
|
if (permission === '*' && area !== 'code' && area !== 'org') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const [ permissionLocale, permissionArea ] = permission.split('-');
|
const [ permissionLocale, permissionArea ] = permission.split('-');
|
||||||
if ((permissionLocale === '*' || permissionLocale === locale || locale === null)
|
if ((permissionLocale === '*' || permissionLocale === locale || locale === null)
|
||||||
&& ((permissionArea === '*' && area !== 'code') || permissionArea === area || area === '' || (area === 'panel' && permissionArea !== 'users'))
|
&& ((permissionArea === '*' && area !== 'code' && area !== 'org') || permissionArea === area || area === '' || (area === 'panel' && permissionArea !== 'users'))
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
49
src/timesheets.js
Normal file
49
src/timesheets.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {DateTime} from "luxon";
|
||||||
|
|
||||||
|
export const max = DateTime.now();
|
||||||
|
export const min = DateTime.local(2020, 1, 1);
|
||||||
|
export const closed = DateTime.local(2020, 1, 1); // TODO DateTime.local(2023, 1, 1);
|
||||||
|
|
||||||
|
export const AREAS = [
|
||||||
|
'translation',
|
||||||
|
'moderation',
|
||||||
|
'content creation',
|
||||||
|
'coding',
|
||||||
|
'devops',
|
||||||
|
'user support',
|
||||||
|
'social media',
|
||||||
|
'media interviews',
|
||||||
|
'design',
|
||||||
|
'sensitivity reviews',
|
||||||
|
'administration',
|
||||||
|
'blog',
|
||||||
|
'census',
|
||||||
|
'other',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MONTHS = {
|
||||||
|
1: 'Jan',
|
||||||
|
2: 'Feb',
|
||||||
|
3: 'Mar',
|
||||||
|
4: 'Apr',
|
||||||
|
5: 'May',
|
||||||
|
6: 'Jun',
|
||||||
|
7: 'Jul',
|
||||||
|
8: 'Aug',
|
||||||
|
9: 'Sep',
|
||||||
|
10: 'Oct',
|
||||||
|
11: 'Nov',
|
||||||
|
12: 'Dec',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRANSFER_METHODS = {
|
||||||
|
'bank': 'Bank transfer',
|
||||||
|
'paypal': 'PayPal',
|
||||||
|
'charity': 'Charity',
|
||||||
|
'skip': 'Skip allowance',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PERIODS = {
|
||||||
|
'2020-2022': [2020, 1, 2022, 12],
|
||||||
|
'2023': [2023, 1, 2023, 12],
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user