develop #9

Merged
walamana merged 26 commits from develop into main 2026-07-14 22:47:20 +02:00
3 changed files with 470 additions and 23 deletions
Showing only changes of commit ad29d0682e - Show all commits
+345
View File
@@ -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>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import type {Gottesdienst, Mark, SimplifiedMinistrant} from "@/models/models"
import GottesdienstCard from "@/components/GottesdienstCard.vue"
defineProps<{
gottesdienste: Gottesdienst[]
ministranten: SimplifiedMinistrant[]
marks: Mark[]
editable: string[]
currentUsername: string
}>()
const emit = defineEmits<{
toggleMark: [gid: number, mid: number]
}>()
</script>
<template>
<div class="list-view">
<GottesdienstCard
v-for="godi in gottesdienste"
:key="godi.id"
:gottesdienst="godi"
:ministranten="ministranten"
:marks="marks"
:editable="editable"
:current-username="currentUsername"
@toggle-mark="(gid, mid) => emit('toggleMark', gid, mid)"
/>
</div>
</template>
<style scoped lang="less">
.list-view {
padding: 12px;
}
</style>
+66 -1
View File
@@ -5,6 +5,7 @@ import TablePlan from "@/components/TablePlan.vue";
import {API, UnauthorizedError} from "@/services/api";
import type {Gottesdienst, GottesdienstGroup, Mark, SimplifiedMinistrant} from "@/models/models";
import MobilePlan from "@/components/MobilePlan.vue";
import ListView from "@/components/ListView.vue";
import PlanActionBar from "@/components/PlanActionBar.vue";
import {Auth} from "@/services/auth";
import GroupView from "@/components/GroupView.vue";
@@ -38,6 +39,8 @@ const plan = reactive<{
groups: []
})
const mobile = ref(window.innerWidth <= MAX_WIDTH_MOBILE)
const viewMode = ref<"table" | "list">("table")
const currentUsername = ref("")
const editedMarks = reactive<Mark[]>([]);
const editPlanAdmin = ref(false)
const planId = ref(parseInt(route.params.id as string))
@@ -79,6 +82,7 @@ async function loadPlan() {
Auth.loggedInSubject.subscribe(async (loggedIn) => {
console.log("logged in " + loggedIn)
if (loggedIn) {
currentUsername.value = Auth.getUser()
plan.editable = Auth.getPrivileges()
if (Auth.getUser() == "admin") {
editPlanAdmin.value = true
@@ -86,6 +90,7 @@ Auth.loggedInSubject.subscribe(async (loggedIn) => {
console.log("Plan", plan.ministranten)
}
} else {
currentUsername.value = ""
editPlanAdmin.value = false
plan.editable = []
}
@@ -312,10 +317,39 @@ async function importZelebrationsplan() {
<template>
<main>
<div class="container">
<div class="container" :class="{
'mobile-view': mobile && viewMode === 'table'
}">
<GroupView :groups="plan.groups" :admin="editPlanAdmin"
@new="createNewPlan" @delete="deletePlan" @edit="editPlan" class="no-print"/>
<div class="view-toggle no-print" v-if="plan.gottesdienste.length > 0">
<button
class="flat"
:class="{ active: viewMode === 'table' }"
@click="viewMode = 'table'"
><i>table_rows</i> Tabelle</button>
<button
class="flat"
:class="{ active: viewMode === 'list' }"
@click="viewMode = 'list'"
><i>list</i> Liste</button>
</div>
<ListView
:gottesdienste="sortedGottesdienste"
:ministranten="plan.ministranten"
:marks="getMarks()"
:editable="plan.editable"
:current-username="currentUsername"
@toggle-mark="toggleMark"
class="plan list"
v-if="viewMode === 'list'">
</ListView>
<template v-if="viewMode === 'table'">
<TablePlan
:gottesdienste="sortedGottesdienste"
:ministranten="plan.ministranten"
@@ -346,6 +380,7 @@ async function importZelebrationsplan() {
v-else>
</MobilePlan>
</template>
<PlanActionBar
class="action-bar no-print"
@@ -385,6 +420,36 @@ async function importZelebrationsplan() {
//width: 100%;
}
.container .view-toggle {
display: flex;
gap: 4px;
padding: 8px 12px;
background: #f5f5f5;
border-bottom: 1px solid #d7d5d5;
button {
font-size: 14px;
padding: 6px 14px;
i {
font-size: 18px;
}
&.active {
background: #ffffff;
border-color: #c4c4c4;
}
}
}
.container.mobile-view {
.plan.list {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
.action-bar {
position: fixed;
bottom: 0;