Compare commits

...

6 Commits

Author SHA1 Message Date
3caf5a0c13 feat: enable updating gottesdienste 2025-03-21 21:18:25 +01:00
4d92b911b7 update dependencies 2025-03-21 21:17:14 +01:00
fe76b59c34 fix: specific version for database
All checks were successful
Deploy Miniplan / build (push) Successful in 25s
2024-11-28 20:08:55 +01:00
fb16acc984 feat: show number of checks
All checks were successful
Deploy Miniplan / build (push) Successful in 1m24s
2024-11-28 20:04:01 +01:00
walamana
97c6beb4e1 fix: iterate toggle from existing marks
All checks were successful
Deploy Miniplan / build (push) Successful in 1m54s
2024-08-18 18:00:06 +02:00
walamana
7d10af6ef2 feat: automatically save 2024-08-18 17:54:56 +02:00
12 changed files with 257 additions and 45 deletions

View File

@@ -5,7 +5,7 @@ networks:
services:
db:
image: postgres
image: postgres:16
restart: always
environment:
- POSTGRES_PASSWORD=minis

View File

@@ -5,9 +5,9 @@ val logback_version: String by project
val exposed_version: String by project
val h2_version: String by project
plugins {
kotlin("jvm") version "1.9.0"
id("io.ktor.plugin") version "2.3.3"
kotlin("plugin.serialization") version "1.9.0"
kotlin("jvm") version "2.1.20"
id("io.ktor.plugin") version "3.1.1"
kotlin("plugin.serialization") version "2.1.20"
id("com.palantir.docker") version "0.35.0"
}
@@ -45,7 +45,7 @@ dependencies {
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
implementation("at.favre.lib:bcrypt:0.10.2")
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("io.ktor:ktor-server-test-host")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"rxjs": "^7.8.1",
"underscore": "^1.13.7",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
@@ -2209,6 +2210,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"rxjs": "^7.8.1",
"underscore": "^1.13.7",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},

View File

@@ -7,7 +7,7 @@ const props = withDefaults(defineProps<{
label?: string,
disabled?: boolean,
type?: string,
dateFormat?: "string" | "number",
dateFormat?: "string" | "number" | "date",
focus?: boolean
}>(), {
dateFormat: "string"
@@ -45,10 +45,12 @@ function leading(val) {
function getValue(){
if(props.type == "date" && props.dateFormat == "number") {
console.log(props.value)
const date = new Date(props.value)
return leading(date.getFullYear()) + "-" + zeros(date.getMonth() + 1) + "-" + zeros(date.getDate())
}else{
} if(props.type == "date" && props.dateFormat == "date") {
const date = new Date(props.value)
return leading(date.getFullYear()) + "-" + zeros(date.getMonth() + 1) + "-" + zeros(date.getDate())
} else {
return props.value
}
}

View File

@@ -9,15 +9,16 @@ const props = defineProps<{
<template>
<div class="action-bar" :class="{save: props.save}">
<div class="action-bar" :class="{save: true}">
<div class="other-action">
<button class="add-plan" :class="{show: props.plan}" @click="$emit('addPlan')"> <i class="icon">add_box</i> Neuer Plan</button>
<button class="add-godi" :class="{show: props.godi}" @click="$emit('addGodi')"> <i class="icon">add</i> Gottesdienst</button>
<button class="add-mini" :class="{show: props.godi}" @click="$emit('addMini')"> <i class="icon">add</i> Ministrant</button>
</div>
<button class="save" :class="{show: props.save}" @click="$emit('save')"><i class="icon">save</i> Änderungen speichern </button>
<!-- <button class="save" :class="{show: props.save}" @click="$emit('save')"><i class="icon">save</i> Änderungen speichern </button>-->
</div>
</template>
<style scoped lang="less">

View File

@@ -0,0 +1,125 @@
<script setup lang="ts">
import {ref, watch} from "vue";
const props = defineProps<{
visible: boolean
}>()
const up = ref(false)
const showText = ref(false)
const saved = ref(false)
const timeout = ref(undefined)
const timeoutShowText = ref(0)
watch(props, (isSaving, wasBeingSaved) => {
console.log("Is saving", props.visible, wasBeingSaved)
if(props.visible) {
up.value = true
saved.value = false
clearTimeout(timeout.value)
timeoutShowText.value = setTimeout(() => {
showText.value = true
}, 3000)
}else if(wasBeingSaved){
clearTimeout(timeoutShowText.value)
if(showText.value) {
saved.value = true
timeout.value = setTimeout(() => {
up.value = false
showText.value = false
}, 1000)
}else{
up.value = false
}
}
}, {immediate: true})
</script>
<template>
<div class="indicator" :class="{up, saved, showText}">
<span class="loader" :class="{saved}"></span>
<span class="text">{{saved ? "Gespeichert" : "Wird gespeichert"}}</span>
<i :class="{saved}">check</i>
</div>
</template>
<style scoped lang="less">
.indicator {
position: fixed;
right: 16px;
bottom: -60px;
background: white;
border-radius: 22px;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
padding: 10px 10px 10px 10px;
height: 22px;
width: 22px;
display: flex;
align-items: center;
overflow: hidden;
transition: width 300ms, bottom 300ms;
color: #000000;
i {
color: #6cc361;
}
&.up{
bottom: 16px;
&.showText{
width: 154px;
&.saved{
width: 150px;
}
}
}
}
.loader {
width: 22px;
height: 22px;
flex-shrink: 0;
margin-right: 10px;
border: 3px solid #6cc361;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
transition: opacity 200ms, width 200ms;
&.saved{
width: 0;
opacity: 0;
}
}
.text{
flex-shrink: 0;
opacity: 0.5;
}
i {
opacity: 0;
transition: opacity 200ms;
margin-left: 15px;
&.saved{
opacity: 1;
}
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -15,7 +15,7 @@ const props = defineProps<{
edit: boolean,
smallMode: boolean
}>()
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit", "resetPassword", "deleteMinistrant", "createMinistrant", "editMinistrant"])
const emit = defineEmits(["toggleMark", "added", "delete", "edit", "endEdit", "resetPassword", "deleteMinistrant", "createMinistrant", "editMinistrant"])
const openEditUser = ref<number>(-1)
const miniCopy = reactive<{ data?: SimplifiedMinistrant }>({})
const data = reactive({
@@ -115,6 +115,10 @@ function toggleEditMinistrant(mini: SimplifiedMinistrant) {
}
}
function getAmount(mid: number, value: number): number {
return props.marks.filter(m => m.mid == mid && m.value == value).length
}
</script>
<template>
@@ -127,7 +131,12 @@ function toggleEditMinistrant(mini: SimplifiedMinistrant) {
<tr v-if="props.edit" class="no-print">
<th></th>
<th v-for="godi in props.gottesdienste"><i @click="$emit('delete', godi.id)">delete</i></th>
<th v-for="godi in props.gottesdienste"><i @click="$emit('delete', godi.id)" style="cursor: pointer">delete</i></th>
</tr>
<tr v-if="props.edit" class="no-print">
<th></th>
<th v-for="godi in props.gottesdienste"><i @click="$emit('edit', godi.id)" style="cursor: pointer">edit</i></th>
</tr>
<tr>
@@ -157,12 +166,18 @@ function toggleEditMinistrant(mini: SimplifiedMinistrant) {
<tr v-for="mini in props.ministranten" class="ministrant" :class="getMinistrantClasses(mini)">
<td class="name">
<div class="center">
<i class="edit-button no-print"
v-if="edit"
style="margin-right: 10px; font-size: 18px"
@click="$emit('editMinistrant', mini.id)">edit</i>
{{ mini.firstname }}
{{ mini.lastname }}
<div style="width: 100%;">
<i class="edit-button no-print"
v-if="edit"
style="margin-right: 10px; font-size: 18px"
@click="$emit('editMinistrant', mini.id)">edit</i>
{{ mini.firstname }}
{{ mini.lastname }}
</div>
<div style="flex-shrink: 0" v-if="edit">
{{getAmount(mini.id, 1)}}
</div>
</div>
</td>
<td

View File

@@ -7,8 +7,8 @@ import {onMounted, ref, toRaw} from "vue";
import type {Gottesdienst, GottesdienstGroup} from "@/models/models";
import {onKeyPress} from "@/composables/enter";
interface CreateGottesdienstDialogProps extends DialogControls {
onCreate: (Gottesdienst) => (Promise<any> | undefined),
godi?: Gottesdienst,
onCreate: (arg0: Gottesdienst) => (Promise<any> | undefined),
gottesdienst?: Gottesdienst,
planId: number
}
@@ -16,19 +16,34 @@ onKeyPress("Enter", create)
const props = defineProps<CreateGottesdienstDialogProps>()
const date = ref("")
const time = ref("")
const godi = ref(formatGottesdienst(props.gottesdienst))
const godi = ref(props.godi ?? {
planId: props.planId,
date: "",
attendance: "",
name: "",
id: -1
})
const date = ref(props.gottesdienst ? formatDateString(props.gottesdienst.date) : "")
const time = ref(props.gottesdienst ? formatTimeString(props.gottesdienst.date) : "")
let submitted = false
function formatGottesdienst(gottesdienst: Gottesdienst) {
return gottesdienst ? {
...gottesdienst,
attendance: formatTimeString(gottesdienst.attendance)
} : {
planId: props.planId,
date: "",
attendance: "",
name: "",
id: -1
}
}
function formatDateString(date: Date) {
return (new Date(date)).toISOString().split('T')[0];
}
function formatTimeString(date: Date) {
return (new Date(date)).toTimeString().slice(0, 5);
}
async function create(){
if(submitted) return;
@@ -44,6 +59,7 @@ async function create(){
submitted = false
}
</script>
<template>
@@ -51,8 +67,8 @@ async function create(){
<Dialog class="dialog">
<h3>Gottesdienst {{ godi.id == -1 ? "erstellen" : "bearbeiten"}}</h3>
<Input class="input" v-model:value="godi.name" label="Name" focus/>
<Input class="input" v-model:value="date" type="date" label="Datum"/>
<Input class="input" v-model:value="time" type="time" label="Um"/>
<Input class="input" v-model:value="date" date-format="string" type="date" label="Datum"/>
<Input class="input" v-model:value="time" date-format="string" type="time" label="Um"/>
<Input class="input" v-model:value="godi.attendance" type="time" label="Anwesenheit"/>
<div class="buttons" style="display: flex; justify-content: end; margin-top: 20px;">
<button @click="onDismiss">Abbrechen</button>

View File

@@ -87,6 +87,22 @@ export namespace API {
})
}
export async function addGottesdienstNew(gottesdienst: Gottesdienst) {
return api("/gottesdienste", "PUT", {
...gottesdienst,
date: gottesdienst.date.getTime(),
attendance: gottesdienst.attendance.getTime()
}).then(data => data.json())
}
export async function updateGottesdienst(gottesdienst: Gottesdienst) {
return api("/gottesdienste", "PATCH", {
...gottesdienst,
date: gottesdienst.date.getTime(),
attendance: gottesdienst.attendance.getTime()
}).then(res => res.status == 200)
}
export async function getMinistranten() {
return api("/ministranten", "GET").then(res => res.json())
}

View File

@@ -14,7 +14,8 @@ import CreatePlanDialog from "@/components/dialog/CreatePlanDialog.vue";
import CreateGottesdienstDialog from "@/components/dialog/CreateGottesdienstDialog.vue";
import CreateMinistrantDialog from "@/components/dialog/CreateMinistrantDialog.vue";
import {useRoute, useRouter} from "vue-router";
import {min} from "rxjs";
import debounce from "underscore/modules/debounce.js"
import SavingIndicator from "@/components/SavingIndicator.vue";
const MAX_WIDTH_MOBILE = 600;
@@ -38,6 +39,7 @@ const mobile = ref(window.innerWidth <= MAX_WIDTH_MOBILE)
const editedMarks = reactive<Mark[]>([]);
const editPlanAdmin = ref(false)
const planId = ref(parseInt(route.params.id as string))
const isSaving = ref(false)
onMounted(async () => {
@@ -86,21 +88,27 @@ const sortedGottesdienste = computed(() => {
})
})
async function createGottesdienst(){
async function createGottesdienst(gottesdienstId?: number){
let gottesdienstRef = plan.gottesdienste.find(gottesdienst => gottesdienst.id == gottesdienstId)
let gottesdienst = gottesdienstRef ? Object.assign({}, toRaw(gottesdienstRef)) : null;
Dialogs.createDialog(CreateGottesdienstDialog, {
onPositive() {},
onNegative() {},
onDismiss() {}
}, {
planId: parseInt(planId.value as string),
gottesdienst,
planId: parseInt(planId.value as unknown),
async onCreate(godi: Gottesdienst) {
const newGodi = await API.addGottesdienst(
godi.name,
godi.date,
godi.attendance,
godi.planId
)
plan.gottesdienste.push(newGodi)
if(godi.id == -1) {
const newGodi = await API.addGottesdienstNew(godi)
godi.id = newGodi.id
plan.gottesdienste.push(godi)
} else {
await API.updateGottesdienst(godi)
const index = plan.gottesdienste.findIndex(g => g.id == godi.id)
plan.gottesdienste.splice(index, 1)
plan.gottesdienste.push(godi)
}
}
})
}
@@ -147,11 +155,13 @@ function getDif(): Mark[] {
})
}
async function saveChanges() {
const saveChanges = debounce(saveChangesFunc, 600)
async function saveChangesFunc() {
const saved = await API.setMarks(getDif())
if (saved) {
plan.marks = getMarks()
editedMarks.length = 0
isSaving.value = false
}
}
@@ -165,12 +175,23 @@ function canEdit(username: string) {
return plan.editable.includes(username)
}
function toggleMark(gid, mid) {
// TODO: track changes
const username = plan.ministranten.find(m => m.id == mid)?.username
if (!canEdit(username)) return;
let mark = editedMarks.find(m => m.mid == mid && m.gid == gid);
if(!mark) {
let markFromPlan = plan.marks.find(m => m.gid == gid && m.mid == mid)
if(markFromPlan) {
mark = {
...markFromPlan
}
editedMarks.push(mark)
}
}
console.log(mark, editedMarks, plan.marks)
if (mark) {
mark.value = ((mark.value + 2) % 3) - 1
} else {
@@ -179,6 +200,9 @@ function toggleMark(gid, mid) {
}
editedMarks.push(mark)
}
isSaving.value = true
saveChanges()
}
@@ -275,6 +299,7 @@ async function createMinistrant(ministrantId?: number) {
:small-mode="editPlanAdmin"
@added="addGodi"
@delete="deleteGottedienst"
@edit="createGottesdienst"
@toggle-mark="toggleMark"
@end-edit="editPlanAdmin = false"
@reset-password="resetPassword"
@@ -306,6 +331,9 @@ async function createMinistrant(ministrantId?: number) {
@add-mini="createMinistrant()"
v-if="editPlanAdmin"
/>
<SavingIndicator :visible="isSaving"/>
</div>
</main>
</template>