feat: add list view with GottesdienstCard components
- 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
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user