mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-30 00:28:02 -04:00
289 lines
11 KiB
Vue
289 lines
11 KiB
Vue
<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>
|
||
<nuxt-link to="/admin/timesheets/overview">
|
||
<Icon v="file-spreadsheet" />
|
||
Overview
|
||
</nuxt-link>
|
||
</p>
|
||
<div v-html="moderation.timesheets"></div>
|
||
|
||
<hr>
|
||
|
||
<h3>
|
||
Year:
|
||
<button
|
||
v-for="y in years"
|
||
:key="y"
|
||
:class="['btn', y === year ? 'btn-primary' : 'btn-outline-primary', 'mx-2']"
|
||
@click="year = y"
|
||
>
|
||
{{ y }}
|
||
</button>
|
||
</h3>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-striped">
|
||
<thead>
|
||
<tr>
|
||
<th>Area</th>
|
||
<th v-for="month in months">
|
||
{{ month }}
|
||
</th>
|
||
<th>Sum</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="area in areas">
|
||
<th>{{ area }}</th>
|
||
<td v-for="(month, m) in months">
|
||
<input
|
||
:ref="`cell-${m}-${areas.indexOf(area)}`"
|
||
v-model.number="timesheet[year][m][area]"
|
||
type="number"
|
||
min="0"
|
||
max="160"
|
||
step="0.5"
|
||
class="form-control form-control-sm"
|
||
style="min-width: 3rem"
|
||
:disabled="dt(year, m) < closed || dt(year, m) > max"
|
||
@focus="focusMonth = parseInt(m);focusArea = areas.indexOf(area)"
|
||
@keydown="cellKeydown"
|
||
>
|
||
</td>
|
||
<td>
|
||
<strong>
|
||
{{ sumCells(area, undefined) }}
|
||
</strong>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>Sum</th>
|
||
<td v-for="(month, m) in months">
|
||
<strong>
|
||
{{ sumCells(undefined, m) }}
|
||
</strong>
|
||
</td>
|
||
<td>
|
||
<strong>
|
||
{{ sumCells(undefined, undefined) }}
|
||
</strong>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<section>
|
||
<p>
|
||
<strong>Transfer method:</strong>
|
||
<span v-for="(label, value) in transferMethods" class="form-check form-check-inline">
|
||
<input
|
||
:id="`method_${value}`"
|
||
v-model="transferMethod"
|
||
class="form-check-input"
|
||
type="radio"
|
||
name="inlineRadioOptions"
|
||
:value="value"
|
||
>
|
||
<label class="form-check-label" :for="`method_${value}`">{{ label }}</label>
|
||
</span>
|
||
</p>
|
||
<div v-if="transferMethod === 'bank'" class="mb-3">
|
||
<label for="bank_name" class="form-label">Owner name</label>
|
||
<input id="bank_name" v-model="transferDetails.bank_name" type="text" class="form-control" placeholder="Jay Doe">
|
||
</div>
|
||
<div v-if="transferMethod === 'bank'" class="mb-3">
|
||
<label for="bank_iban" class="form-label">IBAN</label>
|
||
<input id="bank_iban" v-model="transferDetails.bank_iban" type="text" :class="['form-control', validateIBAN(transferDetails.bank_iban)]" placeholder="NL12 ABCD 1234 5678 90">
|
||
</div>
|
||
<div v-if="transferMethod === 'bank'" class="mb-3">
|
||
<label for="bank_bic" class="form-label">BIC</label>
|
||
<input id="bank_bic" v-model="transferDetails.bank_bic" type="text" :class="['form-control', validateBIC(transferDetails.bank_bic)]" placeholder="BUNQNL2A">
|
||
</div>
|
||
<div v-if="transferMethod === 'paypal'" class="mb-3">
|
||
<label for="paypal_email" class="form-label">Email</label>
|
||
<input id="paypal_email" v-model="transferDetails.paypal_email" type="email" class="form-control" placeholder="paypal-user@email.com">
|
||
</div>
|
||
<div v-if="transferMethod !== 'skip' && transferMethod !== 'charity'" class="mb-3">
|
||
<p><em>There's a legal limit of how much we can send as volunteer allowance, so please also pick a charity in case it's exceeded.</em></p>
|
||
</div>
|
||
<div v-if="transferMethod !== 'skip'" class="mb-3">
|
||
<p><em>You can also leave the charity details empty – in that case we'll set aside that share and pick a charity together at the end of year, or whenever need arises.</em></p>
|
||
</div>
|
||
<div v-if="transferMethod !== 'skip'" class="mb-3">
|
||
<label for="charity_name" class="form-label">Charity name</label>
|
||
<input id="charity_name" v-model="transferDetails.charity_name" type="text" class="form-control" placeholder="Trevor Project">
|
||
</div>
|
||
<div v-if="transferMethod !== 'skip'" class="mb-3">
|
||
<label for="charity_url" class="form-label">Link</label>
|
||
<input id="charity_url" v-model="transferDetails.charity_url" type="email" class="form-control" placeholder="https://www.thetrevorproject.org/">
|
||
</div>
|
||
<div v-if="transferMethod" class="mb-3">
|
||
<label for="notes" class="form-label">Notes</label>
|
||
<input id="notes" v-model="transferDetails.notes" type="text" class="form-control" placeholder="">
|
||
</div>
|
||
</section>
|
||
<button class="btn btn-primary" :disabled="!transferMethod" @click="save">
|
||
<Icon v="save" />
|
||
Save
|
||
</button>
|
||
</div>
|
||
</Page>
|
||
</template>
|
||
|
||
<script>
|
||
import { head } from '../src/helpers.ts';
|
||
import { DateTime } from 'luxon';
|
||
import gm from 'avris-generator';
|
||
import { min, max, closed, MONTHS, AREAS, TRANSFER_METHODS } from '../src/timesheets.js';
|
||
|
||
|
||
export default {
|
||
async asyncData({ app }) {
|
||
const persistent = await app.$axios.$get('/admin/timesheet');
|
||
|
||
const timesheet = {};
|
||
for (let y = min.year; y <= max.year; y++) {
|
||
timesheet[y] = {};
|
||
for (const m in MONTHS) {
|
||
if (!MONTHS.hasOwnProperty(m)) {
|
||
continue;
|
||
}
|
||
timesheet[y][m] = {};
|
||
for (const area of AREAS) {
|
||
timesheet[y][m][area] = persistent?.timesheet?.[y]?.[m]?.[area] || 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
moderation: await app.$axios.$get('/admin/moderation'),
|
||
timesheet,
|
||
transferMethod: persistent?.transfer || '',
|
||
transferDetails: {
|
||
bank_name: persistent?.details?.bank_name || '',
|
||
bank_iban: persistent?.details?.bank_iban || '',
|
||
bank_bic: persistent?.details?.bank_bic || '',
|
||
paypal_email: persistent?.details?.paypal_email || '',
|
||
charity_name: persistent?.details?.charity_name || '',
|
||
charity_url: persistent?.details?.charity_url || '',
|
||
notes: persistent?.details?.notes || '',
|
||
},
|
||
};
|
||
},
|
||
data() {
|
||
const years = [];
|
||
for (let y = min.year; y <= max.year; y++) {
|
||
years.push(y);
|
||
}
|
||
|
||
return {
|
||
year: max.year,
|
||
years,
|
||
min,
|
||
max,
|
||
closed,
|
||
areas: AREAS,
|
||
months: MONTHS,
|
||
transferMethods: TRANSFER_METHODS,
|
||
|
||
focusMonth: null,
|
||
focusArea: null,
|
||
};
|
||
},
|
||
head() {
|
||
return head({
|
||
title: `${this.$t('admin.header')} • Volunteering timesheets`,
|
||
}, this.$translator);
|
||
},
|
||
watch: {
|
||
year() {
|
||
this.$refs['cell-1-0'][0].focus();
|
||
},
|
||
},
|
||
methods: {
|
||
dt(year, month) {
|
||
return DateTime.local(parseInt(year), parseInt(month), 1);
|
||
},
|
||
validateIBAN(iban) {
|
||
if (!iban.trim()) {
|
||
return '';
|
||
}
|
||
try {
|
||
gm.validate('_', 'iban', iban);
|
||
return 'is-valid';
|
||
} catch (e) {
|
||
return 'is-invalid';
|
||
}
|
||
},
|
||
validateBIC(bic) {
|
||
if (!bic.trim()) {
|
||
return '';
|
||
}
|
||
return bic.replace(/ /g, '').match(/^[A-Z0-9]{8,11}$/)
|
||
? 'is-valid'
|
||
: 'is-invalid';
|
||
},
|
||
async save() {
|
||
await this.$post('/admin/timesheet', {
|
||
timesheets: {
|
||
timesheet: this.timesheet,
|
||
transfer: this.transferMethod,
|
||
details: this.transferDetails,
|
||
},
|
||
});
|
||
await this.$alert('Saved successfully', 'success');
|
||
},
|
||
cellKeydown(e) {
|
||
let newFocusMonth = this.focusMonth;
|
||
let newFocusArea = this.focusArea;
|
||
switch (e.key) {
|
||
case 'ArrowLeft':
|
||
newFocusMonth--;
|
||
break;
|
||
case 'ArrowRight':
|
||
newFocusMonth++;
|
||
break;
|
||
case 'ArrowUp':
|
||
newFocusArea--;
|
||
break;
|
||
case 'ArrowDown':
|
||
newFocusArea++;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
const $ref = this.$refs[`cell-${newFocusMonth}-${newFocusArea}`];
|
||
if (!$ref || !$ref.length || $ref[0].getAttribute('disabled')) {
|
||
return;
|
||
}
|
||
$ref[0].focus();
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
},
|
||
sumCells(area, month) {
|
||
let sum = 0;
|
||
for (const m in this.timesheet[this.year]) {
|
||
if (month !== undefined && parseInt(m) !== parseInt(month)) {
|
||
continue;
|
||
}
|
||
for (const a in this.timesheet[this.year][m]) {
|
||
if (area !== undefined && a !== area) {
|
||
continue;
|
||
}
|
||
sum += this.timesheet[this.year][m][a] || 0;
|
||
}
|
||
}
|
||
return sum;
|
||
},
|
||
},
|
||
};
|
||
</script>
|