Compare commits
6 Commits
39af21057c
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
| 3caf5a0c13 | |||
| 4d92b911b7 | |||
| fe76b59c34 | |||
| fb16acc984 | |||
|
|
97c6beb4e1 | ||
|
|
7d10af6ef2 |
@@ -5,7 +5,7 @@ networks:
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:16
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=minis
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
6
public/package-lock.json
generated
6
public/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": "^7.8.1",
|
||||
"underscore": "^1.13.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
125
public/src/components/SavingIndicator.vue
Normal file
125
public/src/components/SavingIndicator.vue
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user