- New GottesdienstCard component shows each service with its info - Shows the currently logged-in user's mark as a toggle checkbox - Lists ministrants who have responded with their mark status - Admin view hides the checkbox, shows responses only - New ListView wrapper renders all cards - PlanView gets a Tabelle/Liste toggle between table and card views
346 lines
8.6 KiB
Vue
346 lines
8.6 KiB
Vue
<script setup lang="ts">
|
|
import type {Gottesdienst, Mark, SimplifiedMinistrant} from "@/models/models"
|
|
import {computed} from "vue"
|
|
|
|
const props = defineProps<{
|
|
gottesdienst: Gottesdienst
|
|
ministranten: SimplifiedMinistrant[]
|
|
marks: Mark[]
|
|
editable: string[]
|
|
currentUsername: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
toggleMark: [gid: number, mid: number]
|
|
}>()
|
|
|
|
const isAdmin = computed(() => props.currentUsername === "admin")
|
|
const currentMini = computed(() => props.ministranten.find(m => m.username === props.currentUsername))
|
|
const showCheckbox = computed(() => !!currentMini.value && !isAdmin.value)
|
|
|
|
const relevantMarks = computed(() =>
|
|
props.marks.filter(m => m.gid === props.gottesdienst.id && m.value !== 0)
|
|
)
|
|
|
|
const sortedMinistrants = computed(() => {
|
|
const miniMap = new Map(props.ministranten.map(m => [m.id, m]))
|
|
return relevantMarks.value
|
|
.map(m => ({ministrant: miniMap.get(m.mid), mark: m}))
|
|
.filter((entry): entry is {ministrant: SimplifiedMinistrant; mark: Mark} => !!entry.ministrant)
|
|
})
|
|
|
|
function getMark(gid: number, mid: number): Mark {
|
|
const mark = props.marks.find(m => m.mid === mid && m.gid === gid)
|
|
return mark ?? {gid, mid, value: 0}
|
|
}
|
|
|
|
function getIconForMark(gid: number, mid: number): string {
|
|
const mark = getMark(gid, mid).value
|
|
switch (mark) {
|
|
case -1: return "remove"
|
|
case 0: return "question_mark"
|
|
case 1: return "close"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function getMarkClass(gid: number, mid: number): Record<string, boolean> {
|
|
const mark = getMark(gid, mid).value
|
|
return {
|
|
minus: mark === -1,
|
|
neutral: mark === 0,
|
|
cross: mark === 1
|
|
}
|
|
}
|
|
|
|
function getHintForMark(gid: number, mid: number): string {
|
|
const mark = getMark(gid, mid).value
|
|
switch (mark) {
|
|
case -1: return "Ich kann nicht"
|
|
case 0: return "Egal"
|
|
case 1: return "Ich komme"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function two(s: number): string {
|
|
return (s < 10 ? "0" : "") + s
|
|
}
|
|
|
|
function formatDay(time: number | Date): string {
|
|
const date = new Date(time)
|
|
return two(date.getDate()) + "." + two(date.getMonth() + 1) + "."
|
|
}
|
|
|
|
function formatTime(time: number | Date): string {
|
|
const date = new Date(time)
|
|
return two(date.getHours()) + ":" + two(date.getMinutes())
|
|
}
|
|
|
|
function formatWeekday(time: number | Date): string {
|
|
const 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"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function toggleOwnMark() {
|
|
if (!currentMini.value) return
|
|
emit("toggleMark", props.gottesdienst.id, currentMini.value.id)
|
|
}
|
|
|
|
function getOwnMarkIcon(): string {
|
|
if (!currentMini.value) return ""
|
|
const mark = getMark(props.gottesdienst.id, currentMini.value.id).value
|
|
switch (mark) {
|
|
case -1: return "close"
|
|
case 0: return ""
|
|
case 1: return "check"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function getOwnMarkClass(): Record<string, boolean> {
|
|
if (!currentMini.value) return {}
|
|
const mark = getMark(props.gottesdienst.id, currentMini.value.id).value
|
|
return {
|
|
minus: mark === -1,
|
|
neutral: mark === 0,
|
|
cross: mark === 1
|
|
}
|
|
}
|
|
|
|
function getOwnMarkHint(): string {
|
|
if (!currentMini.value) return ""
|
|
const mark = getMark(props.gottesdienst.id, currentMini.value.id).value
|
|
switch (mark) {
|
|
case -1: return "Ich kann nicht"
|
|
case 0: return "keine Angabe"
|
|
case 1: return "Ich komme"
|
|
}
|
|
return ""
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="title-row">
|
|
<button
|
|
v-if="showCheckbox"
|
|
class="own-mark"
|
|
:class="getOwnMarkClass()"
|
|
@click="toggleOwnMark"
|
|
:title="getOwnMarkHint()"
|
|
>
|
|
<i v-if="getOwnMarkIcon()" class="icon">{{ getOwnMarkIcon() }}</i>
|
|
</button>
|
|
<span class="name">{{ gottesdienst.name !== "" ? gottesdienst.name : "Gottesdienst" }}</span>
|
|
</div>
|
|
<div class="info">
|
|
<span class="date">am {{ formatWeekday(gottesdienst.date) }} {{ formatDay(gottesdienst.date) }}</span>
|
|
<span class="time">um {{ formatTime(gottesdienst.date) }} Uhr</span>
|
|
<span class="attendance">Anwesenheit: {{ formatTime(gottesdienst.attendance) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body" v-if="sortedMinistrants.length > 0">
|
|
<div
|
|
v-for="{ministrant, mark} in sortedMinistrants"
|
|
:key="ministrant.id"
|
|
class="ministrant-row"
|
|
:class="getMarkClass(gottesdienst.id, ministrant.id)"
|
|
>
|
|
<span class="mini-name">{{ ministrant.firstname }} {{ ministrant.lastname }}</span>
|
|
<span class="mark-badge">
|
|
<i class="icon">{{ getIconForMark(gottesdienst.id, ministrant.id) }}</i>
|
|
<span class="hint">{{ getHintForMark(gottesdienst.id, ministrant.id) }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body empty" v-else>
|
|
<span class="no-marks">Keine Rückmeldungen</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="less">
|
|
.card {
|
|
background: #ffffff;
|
|
border: 1px solid #d7d5d5;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-header {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
|
|
.title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 4px;
|
|
|
|
.name {
|
|
font-weight: 700;
|
|
font-size: 18px;
|
|
}
|
|
}
|
|
|
|
.own-mark {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px;
|
|
height: 36px;
|
|
border: 2px solid #cecece;
|
|
border-radius: 6px;
|
|
background: #ffffff;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
padding: 0;
|
|
margin: 0;
|
|
transition: 100ms;
|
|
|
|
i {
|
|
margin: 0;
|
|
}
|
|
|
|
&:hover {
|
|
border-color: #919191;
|
|
}
|
|
|
|
&.minus {
|
|
border-color: #690b0b;
|
|
background: #fdd5d5;
|
|
color: #690b0b;
|
|
}
|
|
|
|
&.cross {
|
|
border-color: #045b04;
|
|
background: #d1fcd1;
|
|
color: #045b04;
|
|
}
|
|
|
|
&.neutral {
|
|
border-color: #9f9f9f;
|
|
background: #f0f0f0;
|
|
color: #9f9f9f;
|
|
}
|
|
|
|
i {
|
|
font-size: 20px;
|
|
font-variation-settings: "wght" 600;
|
|
}
|
|
}
|
|
|
|
.info {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 16px;
|
|
font-size: 14px;
|
|
color: #656565;
|
|
|
|
span {
|
|
font-weight: 700;
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-body {
|
|
padding: 8px 16px;
|
|
|
|
&.empty {
|
|
padding: 16px;
|
|
text-align: center;
|
|
color: #9f9f9f;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.ministrant-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.mini-name {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.mark-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
|
|
i {
|
|
border-radius: 100%;
|
|
padding: 2px;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.hint {
|
|
font-size: 13px;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
&.minus {
|
|
.mark-badge {
|
|
color: #690b0b;
|
|
|
|
i {
|
|
background: #fdd5d5;
|
|
}
|
|
|
|
.hint {
|
|
color: #690b0b;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.cross {
|
|
.mark-badge {
|
|
color: #045b04;
|
|
|
|
i {
|
|
background: #d1fcd1;
|
|
}
|
|
|
|
.hint {
|
|
color: #045b04;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.neutral {
|
|
.mark-badge {
|
|
color: #9f9f9f;
|
|
|
|
i {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.hint {
|
|
color: #9f9f9f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|