Initial commit

This commit is contained in:
Jonas G 2023-09-16 21:39:56 +02:00
parent 2d458f08f3
commit 7972ad66a8
8 changed files with 710 additions and 26 deletions

View File

@ -5,11 +5,18 @@ import HelloWorld from './components/HelloWorld.vue'
<template> <template>
<header> <header>
Header Miniplan
</header> </header>
<RouterView /> <RouterView />
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
header{
display: flex;
padding: 20px 32px;
border-bottom: 1px solid #d7d5d5;
font-weight: 700;
}
</style> </style>

View File

@ -2,6 +2,7 @@
html, body { html, body {
margin: 0; margin: 0;
font-family: "Roboto", sans-serif;
} }
.icon, i:not(.icon) { .icon, i:not(.icon) {
@ -20,6 +21,34 @@ html, body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.filled { .icon.filled, i:not(.icon).filled {
font-variation-settings: "FILL" 1; font-variation-settings: "FILL" 1;
} }
button {
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 8px 14px 8px 10px;
margin: 0 4px;
border-radius: 4px;
font-weight: 600;
background: #d7eaf3;
color: #0e2c48;
}
button i {
margin-right: 10px;
color: #0e2c48;
padding: 0;
}
button:hover {
background: #e4eff6;
}
button:active {
background: #d0e3f1;
}

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>

View File

@ -0,0 +1,251 @@
<script setup lang="ts">
import {API} from "@/views/api";
import {onMounted, reactive, ref} from "vue";
import type {PlanModel, SimplifiedMinistrant} from "@/models/models";
const props = defineProps<PlanModel>()
defineEmits(["toggleMark"])
function getIconForMark(gid, mid) {
const mark = getMark(gid, mid).value
switch (mark) {
case -1:
return "remove";
case 0:
return "question_mark";
// return ""
case 1:
return "close"
}
return ""
}
function getClassForMark(gid, mid) {
const mark = getMark(gid, mid).value
switch (mark) {
case -1:
return "minus";
case 0:
return "neutral";
case 1:
return "cross"
}
return ""
}
function getHintForMark(gid, mid) {
const mark = getMark(gid, mid).value
switch (mark) {
case -1:
return "Ich kann nicht";
case 0:
return "Egal";
case 1:
return "Ich komme"
}
}
function two(s) {
return (s < 10 ? "0" : "") + s
}
function formatDay(time) {
let date = new Date(time)
return two(date.getDate()) + "." + two(date.getMonth()) + "."
}
function formatTime(time) {
let date = new Date(time)
return two(date.getHours()) + ":" + two(date.getMinutes())
}
function formatWeekday(time) {
let date = new Date(time)
switch (date.getDay()) {
case 1:
return "Montag";
case 2:
return "Dienstag";
case 3:
return "Mittwoch";
case 4:
return "Donnerstag";
case 5:
return "Freitag";
case 6:
return "Samstag";
case 0:
return "Sonntag"
}
}
function getMark(gid, mid) {
const mark = props.marks.find(mark => mark.mid == mid && mark.gid == gid)
return mark ? mark : {gid, mid, value: 0}
}
function getMinistrantClasses(mini: SimplifiedMinistrant) {
return {
edit: props.editable.includes(mini.id)
}
}
function getMinis() {
return props.ministranten.filter(m => props.editable.includes(m.id))
}
function getMiniName(mini) {
return mini.firstname + " " + mini.lastname
}
</script>
<template>
<table>
<thead>
<tr>
<th>Gottesdienst</th>
<th v-for="mini in getMinis()">
{{ getMiniName(mini) }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="godi in props.gottesdienste" class="gottesdienst">
<td>
<div class="gottesdienst">
<span class="name">{{godi.name !== "" ? godi.name : "Gottesdienst"}}</span>
<span class="date">am {{formatWeekday(godi.date) }} {{formatDay(godi.date) }}</span>
<span class="time">um {{formatTime(godi.date)}} Uhr</span>
<span class="attendance">Anwesenheit: {{formatTime(godi.attendance) }}</span>
</div>
</td>
<td
v-for="mini in getMinis()"
class="mark edit"
:class="getClassForMark(godi.id, mini.id)"
@click="$emit('toggleMark', godi.id, mini.id)">
<i class="icon"> {{ getIconForMark(godi.id, mini.id) }} </i><br>
<span class="hint">{{ getHintForMark(godi.id, mini.id) }}</span>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped lang="less">
table {
border-spacing: 0;
}
tr {
th {
font-weight: 400;
}
&.bold th {
font-weight: 700;
}
}
td {
background: #ffffff;
}
td:first-child, th:first-child {
padding: 6px 60px 6px 12px;
text-align: left;
}
td:nth-child(2n), th:nth-child(2n) {
background: #8ce081;
}
.gottesdienst{
span {
display: block;
}
.name {
font-weight: 700;
font-size: 18px;
}
.date, .time {
font-weight: 700;
color: #656565;
font-size: 14px;
}
.attendance{
color: #656565;
font-size: 14px;
}
}
.mark {
text-align: center;
vertical-align: center;
justify-content: center;
align-items: center;
min-width: 100px;
height: 20px;
cursor: pointer;
user-select: none;
i {
border-radius: 100%;
padding: 1px;
font-size: 22px;
margin: 2px;
}
&.minus {
background: #fdd5d5;
color: #690b0b;
i {
}
}
&.cross {
background: #d1fcd1;
color: #045b04;
i {
}
}
&.neutral i {
opacity: 0.5;
color: #9f9f9f;
font-variation-settings: "wght" 350;
mix-blend-mode: difference;
}
.hint {
margin-top: 4px;
opacity: 0.7;
font-size: 14px;
//mix-blend-mode: difference;
}
}
td, th {
--color-outline: #908888;
border-right: 1px solid var(--color-outline);
border-bottom: 1px solid var(--color-outline);
}
td:first-child, th:first-child {
border-right: 2px solid #575757;
}
thead tr:last-child th, tr:nth-child(5n) td {
border-bottom: 2px solid #575757;
}
</style>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
const props = defineProps<{
save: boolean,
plan: boolean,
godi: boolean
}>()
</script>
<template>
<div class="action-bar" :class="{save: props.save}">
<div class="other-action">
<button class="add-plan" :class="{show: props.plan}" @click="$emit('save')"> <i class="icon">add_box</i> Neuer Plan</button>
<button class="add-godi" :class="{show: props.godi}" @click="$emit('save')"> <i class="icon">add</i> Neuer Gottesdienst</button>
</div>
<button class="save" :class="{show: props.save}" @click="$emit('save')"><i class="icon">save</i> Änderungen speichern </button>
</div>
</template>
<style scoped lang="less">
.action-bar {
width: calc(100% - 32px * 2);
border-top: 1px solid #d7d5d5;
display: flex;
padding: 10px 32px;
background: #ffffff;
justify-content: flex-end;
.other-action{
transition: 200ms translate;
translate: 220px;
transition-delay: 100ms;
};
&.save .other-action {
translate: 0;
transition-delay: 0ms;
}
}
button {
&:not(.show){
display: none;
}
&.save{
&:not(.show){
display: flex;
}
width: 220px;
translate: 0 calc(100% + 20px);
transition: 200ms translate;
&.show {
translate: 0;
transition-delay: 100ms;
}
}
}
button{
}
</style>

View File

@ -0,0 +1,255 @@
<script setup lang="ts">
import {API} from "@/views/api";
import {onMounted, reactive, ref} from "vue";
import type {PlanModel, SimplifiedMinistrant} from "@/models/models";
const props = defineProps<PlanModel>()
defineEmits(["toggleMark"])
function getIconForMark(gid, mid) {
const mark = getMark(gid, mid).value
switch (mark) {
case -1:
return "remove";
case 0:
return "question_mark";
// return ""
case 1:
return "close"
}
return ""
}
function getClassForMark(gid, mid) {
const mark = getMark(gid, mid).value
switch (mark) {
case -1:
return "minus";
case 0:
return "neutral";
case 1:
return "cross"
}
return ""
}
function getHintForMark(gid, mid) {
const mark = getMark(gid, mid).value
switch (mark) {
case -1:
return "Ich kann nicht";
case 0:
return "Egal";
case 1:
return "Ich komme"
}
}
function two(s) {
return (s < 10 ? "0" : "") + s
}
function formatDay(time) {
let date = new Date(time)
return two(date.getDate()) + "." + two(date.getMonth()) + "."
}
function formatTime(time) {
let date = new Date(time)
return two(date.getHours()) + ":" + two(date.getMinutes())
}
function formatWeekday(time) {
let date = new Date(time)
switch (date.getDay()) {
case 1:
return "Mo";
case 2:
return "Di";
case 3:
return "Mi";
case 4:
return "Do";
case 5:
return "Fr";
case 6:
return "Sa";
case 0:
return "So"
}
}
function getMark(gid, mid) {
const mark = props.marks.find(mark => mark.mid == mid && mark.gid == gid)
return mark ? mark : {gid, mid, value: 0}
}
function getMinistrantClasses(mini: SimplifiedMinistrant) {
return {
edit: props.editable.includes(mini.id)
}
}
</script>
<template>
<table>
<thead>
<tr>
<th></th>
<th v-for="godi in props.gottesdienste">{{ godi.name }}</th>
</tr>
<tr class="bold">
<th>Datum</th>
<th v-for="godi in props.gottesdienste">{{ formatDay(godi.date) }}</th>
</tr>
<tr>
<th>Uhrzeit</th>
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.date) }}</th>
</tr>
<tr class="bold">
<th>Anwesenheit</th>
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.attendance) }}</th>
</tr>
<tr>
<th>Wochentag</th>
<th v-for="godi in props.gottesdienste">{{ formatWeekday(godi.date) }}</th>
</tr>
</thead>
<tbody>
<tr v-for="mini in props.ministranten" class="ministrant" :class="getMinistrantClasses(mini)">
<td class="name">{{ mini.id }} {{ mini.firstname }} {{ mini.lastname }}</td>
<td
v-for="godi in props.gottesdienste"
class="mark"
:class="getClassForMark(godi.id, mini.id)"
@click="$emit('toggleMark', godi.id, mini.id)">
<i class="icon"> {{ getIconForMark(godi.id, mini.id) }} </i><br>
<span class="hint">{{ getHintForMark(godi.id, mini.id) }}</span>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped lang="less">
table {
border-spacing: 0;
}
tr{
th{
font-weight: 400;
}
&.bold th{
font-weight: 700;
}
}
td {
background: #ffffff;
}
td:first-child, th:first-child {
padding: 6px 60px 6px 12px;
text-align: left;
}
td:nth-child(2n), th:nth-child(2n){
background: #8ce081;
}
.mark {
text-align: center;
vertical-align: center;
justify-content: center;
align-items: center;
min-width: 100px;
height: 20px;
user-select: none;
i {
border-radius: 100%;
padding: 1px;
font-size: 22px;
margin: 2px;
}
&.minus {
background: #fdd5d5;
color: #690b0b;
i {
}
}
&.cross {
background: #d1fcd1;
color: #045b04;
i {
}
}
&.neutral i {
opacity: 0;
color: #9f9f9f;
font-variation-settings: "wght" 350;
mix-blend-mode: difference;
}
.hint {
margin-top: 4px;
opacity: 0.7;
display: none;
font-size: 14px;
//mix-blend-mode: difference;
}
}
.ministrant.edit {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
z-index: 10;
position: relative;
td {
padding-top: 10px;
padding-bottom: 10px;
}
.mark{
cursor: pointer;
&.neutral i {
opacity: 0.5;
}
.hint {
display: inline-block;
}
}
}
td, th {
--color-outline: #908888;
border-right: 1px solid var(--color-outline);
border-bottom: 1px solid var(--color-outline);
}
td:first-child, th:first-child {
border-right: 2px solid #575757;
}
thead tr:last-child th, tr:nth-child(5n) td {
border-bottom: 2px solid #575757;
}
</style>

View File

@ -19,7 +19,8 @@ export interface Mark {
} }
export interface PlanModel { export interface PlanModel {
gottesdienste: Array<Gottesdienst>, gottesdienste: Gottesdienst[],
ministranten: Array<SimplifiedMinistrant>, ministranten: SimplifiedMinistrant[],
marks: Array<Mark> marks: Mark[],
editable: number[]
} }

View File

@ -1,5 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, reactive, ref, toRaw} from "vue";
import TablePlan from "@/components/TablePlan.vue";
import {API} from "@/views/api";
import type {Mark, PlanModel} from "@/models/models";
import MobilePlan from "@/components/MobilePlan.vue";
import PlanActionBar from "@/components/PlanActionBar.vue";
import {computed, onMounted, reactive, ref} from "vue"; import {computed, onMounted, reactive, ref} from "vue";
import Plan from "@/components/Plan.vue"; import Plan from "@/components/Plan.vue";
import {API} from "@/views/api"; import {API} from "@/views/api";
@ -10,6 +17,8 @@ const plan = reactive<PlanModel>({
ministranten: [], ministranten: [],
marks: [] marks: []
}) })
const mobile = ref(false)
const editedMarks = reactive<Mark[]>([]);
const sortedGottesdienste = computed(() => { const sortedGottesdienste = computed(() => {
return plan.gottesdienste.sort((a, b) => { return plan.gottesdienste.sort((a, b) => {
@ -44,6 +53,13 @@ async function deleteGottedienst(id) {
} }
async function addGodi() {
let time = 1692104646066 + ((1000 * 60 * 60 * 24) * Math.random() * 6)
let newGodi = await API.addGottesdienst("Godi", new Date(time), new Date(time - 1000 * 60 * 30), 0)
plan.gottesdienste.push(newGodi)
}
onMounted(async () => { onMounted(async () => {
let fetchedPlan = await API.getPlan(0) let fetchedPlan = await API.getPlan(0)
plan.gottesdienste = fetchedPlan.gottesdienste plan.gottesdienste = fetchedPlan.gottesdienste
@ -51,18 +67,36 @@ onMounted(async () => {
plan.marks = fetchedPlan.marks plan.marks = fetchedPlan.marks
}) })
function getMarks(): Mark[]{
return plan.marks.filter((mark: Mark) => {
let difMark = editedMarks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid)
return !difMark
}).concat(editedMarks);
}
function getDif(): Mark[]{
return editedMarks.filter((mark: Mark) => {
let sameMark = plan.marks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid)
return (!sameMark && mark.value != 0) || (sameMark && mark.value != sameMark.value)
})
}
function canEdit(mid) {
return plan.editable.includes(mid)
}
function toggleMark(gid, mid) { function toggleMark(gid, mid) {
// TODO: track changes // TODO: track changes
let existingMark = plan.marks.find(mark => mark.mid == mid && mark.gid == gid); if(!canEdit(mid)) return;
if(existingMark) {
existingMark.value = ((existingMark.value + 2) % 3) - 1 let mark = editedMarks.find(m => m.mid == mid && m.gid == gid);
console.log(existingMark.value.valueOf()) if (mark) {
}else{ mark.value = ((mark.value + 2) % 3) - 1
const mark = { } else {
gid, mid, value: 1 mark = {
} gid, mid, value: 1
plan.marks.push(mark)
} }
editedMarks.push(mark)
}
} }
@ -72,22 +106,54 @@ function toggleMark(gid, mid) {
<template> <template>
<main> <main>
<Plan <TablePlan
:gottesdienste="sortedGottesdienste" :gottesdienste="plan.gottesdienste"
:ministranten="plan.ministranten" :ministranten="plan.ministranten"
:marks="plan.marks" :marks="getMarks()"
:edit="false" :editable="plan.editable"
@added="addGodi" @added="addGodi"
@delete="deleteGottedienst" @delete="deleteGottesdienst"
@toggle-mark="toggleMark"> @toggle-mark="toggleMark"
class="plan table"
v-if="!mobile">
</Plan> </TablePlan>
<MobilePlan
:gottesdienste="plan.gottesdienste"
:ministranten="plan.ministranten"
:marks="getMarks()"
:editable="plan.editable"
@toggle-mark="toggleMark"
class="plan mobile"
v-else>
</MobilePlan>
<PlanActionBar
class="action-bar"
:save="getDif().length > 0"
:plan="false"
:godi="false"
/>
<button @click="addGodi">Add random Gottesdienst</button>
</main> </main>
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.plan {
padding-bottom: 100px;
}
.plan.table {
width: 100%;
}
.action-bar {
position: fixed;
bottom: 0;
z-index: 100;
}
</style> </style>