replace + in email helpers with spaces; configs for title edit, multi edit table & confetti on endupload; empty student projektarbeiten check; only show dropdown labels on large displays; new phrasen & phrasen fixes;

This commit is contained in:
Johann Hoffmann
2026-05-26 17:06:39 +02:00
parent 22eed92c86
commit 2eb2c36d5b
9 changed files with 218 additions and 29 deletions
@@ -347,3 +347,52 @@
}
/*confetti celebration on endupload - impossible to miss*/
#confetti-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.confetti-piece {
position: absolute;
opacity: 0;
will-change: top, transform, opacity;
}
/* Background Rain */
@keyframes fallAndSpin {
0% {
top: var(--start-y);
transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg);
opacity: 1;
}
100% {
top: 105vh;
transform: translate3d(var(--drift), 0, 0) rotateX(720deg) rotateY(360deg);
opacity: 0.3;
}
}
/* Corner Cannons*/
@keyframes cannonBlast {
0% {
transform: translate3d(0, 0, 0) scale(0.3) rotate(0deg);
opacity: 1;
animation-timing-function: cubic-bezier(0.1, 0.8, 0.2, 1);
}
30% {
transform: translate3d(var(--blast-x), var(--blast-y), 0) scale(1.2) rotate(270deg);
opacity: 1;
animation-timing-function: linear;
}
100% {
transform: translate3d(calc(var(--blast-x) * 1.4), 15vh, 0) scale(0.4) rotate(630deg);
opacity: 0;
}
}
@@ -21,7 +21,7 @@ export const AbgabeStudentDetail = {
VueDatePicker,
FhcOverlay
},
inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link'],
inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link', 'confetti_on_endupload', 'title_edit_allowed'],
props: {
projektarbeit: {
type: Object,
@@ -52,6 +52,67 @@ export const AbgabeStudentDetail = {
}
},
methods: {
confettiCannons() {
const container = document.getElementById('confetti-container');
if (!container) return;
const colors = ['#FFC107', '#FF5722', '#E91E63', '#00BCD4', '#4CAF50', '#9C27B0'];
const shapes = ['50%', '0%'];
const fragment = document.createDocumentFragment();
// Corner Cannons - Slowed Down)
const cannonCount = 150;
for (let i = 0; i < cannonCount; i++) {
const leftConfetti = document.createElement('div');
leftConfetti.classList.add('confetti-piece');
leftConfetti.style.left = '0px';
leftConfetti.style.top = '100%';
const rightConfetti = document.createElement('div');
rightConfetti.classList.add('confetti-piece');
rightConfetti.style.left = '100vw';
rightConfetti.style.top = '100%';
const colorL = colors[Math.floor(Math.random() * colors.length)];
const colorR = colors[Math.floor(Math.random() * colors.length)];
const shapeL = shapes[Math.floor(Math.random() * shapes.length)];
const shapeR = shapes[Math.floor(Math.random() * shapes.length)];
// Left Styles
leftConfetti.style.background = colorL;
leftConfetti.style.borderRadius = shapeL;
leftConfetti.style.width = `${Math.random() * 10 + 6}px`;
leftConfetti.style.height = `${Math.random() * 14 + 6}px`;
leftConfetti.style.setProperty('--blast-x', `${Math.random() * 50 + 10}vw`);
leftConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
// Right Styles
rightConfetti.style.background = colorR;
rightConfetti.style.borderRadius = shapeR;
rightConfetti.style.width = `${Math.random() * 10 + 6}px`;
rightConfetti.style.height = `${Math.random() * 14 + 6}px`;
rightConfetti.style.setProperty('--blast-x', `-${Math.random() * 50 + 10}vw`);
rightConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
// Increased durations to 3s - 5s for a floating gravity effect
const durationL = Math.random() * 2 + 3;
const durationR = Math.random() * 2 + 3;
const delayL = Math.random() * 0.2;
const delayR = Math.random() * 0.2;
leftConfetti.style.animation = `cannonBlast ${durationL}s linear ${delayL}s both`;
rightConfetti.style.animation = `cannonBlast ${durationR}s linear ${delayR}s both`;
fragment.appendChild(leftConfetti);
fragment.appendChild(rightConfetti);
setTimeout(() => leftConfetti.remove(), (delayL + durationL) * 1000);
setTimeout(() => rightConfetti.remove(), (delayR + durationR) * 1000);
}
container.appendChild(fragment);
},
openTitelEdit() {
this.editingTitel = this.projektarbeit.titel ?? '';
this.$refs.modalTitelEdit.show();
@@ -155,6 +216,9 @@ export const AbgabeStudentDetail = {
this.$api.call(ApiAbgabe.postStudentProjektarbeitEndupload(formData))
.then(res => {
this.handleUploadRes(res, this.enduploadTermin)
if(this.confetti_on_endupload && res.meta.status == "success") {
this.confettiCannons()
}
}).finally(()=> {
this.loading = false
})
@@ -274,8 +338,7 @@ export const AbgabeStudentDetail = {
return qgatefound
},
isTitelEditAllowed() {
// blocked once the projektarbeit has a note (finished) - mirrors backend guard
return !this.isViewMode && !this.projektarbeit?.note;
return this.title_edit_allowed && !this.isViewMode && !this.projektarbeit?.note;
},
getTooltipVerspaetet() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" }
@@ -433,7 +496,7 @@ export const AbgabeStudentDetail = {
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9">
@@ -544,6 +607,8 @@ export const AbgabeStudentDetail = {
</div>
<div v-if="confetti_on_endupload" id="confetti-container"></div>
<bs-modal
ref="modalTitelEdit"
class="bootstrap-prompt"
@@ -562,7 +627,7 @@ export const AbgabeStudentDetail = {
rows="10"
maxlength="1024"
class="form-control w-100"
@keyup.enter="saveTitel"
@keydown.enter.prevent="saveTitel"
/>
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
</div>
@@ -674,6 +739,7 @@ export const AbgabeStudentDetail = {
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
`,
};
@@ -95,6 +95,7 @@ export const AbgabetoolAssistenz = {
old_abgabe_beurteilung_link: null,
ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT: null,
ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER: null,
MULTIEDIT_TABLE: false,
saving: false,
loading: false,
abgabeTypeOptions: null,
@@ -2275,6 +2276,7 @@ export const AbgabetoolAssistenz = {
this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link;
this.ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = res.data?.ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT;
this.ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = res.data?.ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER;
this.MULTIEDIT_TABLE = res.data?.MULTIEDIT_TABLE;
}
// 2. Studiengänge
@@ -2659,7 +2661,7 @@ export const AbgabetoolAssistenz = {
<div class="col-auto me-auto">
<h2 tabindex="1">{{$p.t('abgabetool/abgabetoolTitleAdmin')}}</h2>
</div>
<div class="col-auto">
<div class="col-auto d-none d-xxl-flex">
<label class="col-form-label">{{$capitalize($p.t('lehre/studiengang'))}}:</label>
</div>
<div class="col-3">
@@ -2676,10 +2678,10 @@ export const AbgabetoolAssistenz = {
</template>
</Dropdown>
</div>
<div class="col-auto">
<div class="col-auto d-none d-xxl-flex">
<label class="col-form-label">{{$capitalize($p.t('lehre/note'))}}:</label>
</div>
<div class="col-3">
<div class="col-2">
<Dropdown
:placeholder="$p.t('lehre/note')"
:style="{'width': '100%', 'scroll-behavior': 'auto !important'}"
@@ -2738,7 +2740,7 @@ export const AbgabetoolAssistenz = {
</button>
<tiered-menu ref="menu" :model="emailItems" popup :autoZIndex="false" />
<button @click="switchMode" class="btn btn-secondary">
<button v-if="MULTIEDIT_TABLE" @click="switchMode" class="btn btn-secondary">
{{ $p.t('abgabetool/c4terminansicht') }}
</button>
@@ -18,7 +18,9 @@ export const AbgabetoolStudent = {
return {
notenOptions: Vue.computed(() => this.notenOptions),
isViewMode: Vue.computed(() => this.isViewMode),
moodle_link: Vue.computed(() => this.moodle_link)
moodle_link: Vue.computed(() => this.moodle_link),
title_edit_allowed: Vue.computed(() => this.title_edit_allowed),
confetti_on_endupload: Vue.computed(() => this.confetti_on_endupload)
}
},
props: {
@@ -46,6 +48,8 @@ export const AbgabetoolStudent = {
projektarbeiten: null,
selectedProjektarbeit: null,
moodle_link: null,
title_edit_allowed: null,
confetti_on_endupload: null,
editingTitel: '',
editingProjektarbeit: null,
};
@@ -335,6 +339,8 @@ export const AbgabetoolStudent = {
this.$api.call(ApiAbgabe.getConfigStudent()).then(res => {
this.moodle_link = res.data?.moodle_link
this.title_edit_allowed = res.data?.title_edit_allowed
this.confetti_on_endupload = res.data?.confetti_on_endupload
}).catch(e => {
this.loading = false
})
@@ -381,7 +387,7 @@ export const AbgabetoolStudent = {
rows="10"
maxlength="1024"
class="form-control w-100"
@keyup.enter="saveTitel"
@keydown.enter.prevent="saveTitel"
/>
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
</div>
@@ -407,7 +413,7 @@ export const AbgabetoolStudent = {
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
<hr>
<div v-if="projektarbeiten === null">
<div v-if="projektarbeiten === null || projektarbeiten?.length == 0">
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
</div>
@@ -497,7 +503,7 @@ export const AbgabetoolStudent = {
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>{{ projektarbeit.titel }}</span>
<button
v-if="!isViewMode && projektarbeit.note == null"
v-if="title_edit_allowed && !isViewMode && projektarbeit.note == null"
class="btn btn-sm btn-outline-secondary border-0 p-1"
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
@click="openTitelEdit(projektarbeit, $event)"
+3 -3
View File
@@ -1,6 +1,6 @@
export async function splitMailsHelper(mails, event, subject, body, alertPluginRef, phrasenPluginRef) {
await phrasenPluginRef.loadCategory('ui');
debugger
let splititem = ",";
let maillist = mails.join(splititem);
let useBcc = event?.ctrlKey || event?.metaKey;
@@ -16,13 +16,13 @@ export async function splitMailsHelper(mails, event, subject, body, alertPluginR
// initial overhead: "mailto:?bcc=" -> 12 chars, "mailto:" -> 7 chars
const baseOverhead = useBcc ? 12 : 7;
let queryString = urlParams.toString();
let queryString = urlParams.toString().replace(/\+/g, '%20');;
let overhead = baseOverhead + (queryString ? 1 + queryString.length : 0); // +1 accounts for '?' or '&'
// calculate overhead with body to exceed the limit
if (overhead > 2024) {
await alertPluginRef.alertWarning(phrasenPluginRef.t('ui', 'bodyZuLang'));
urlParams.delete('body');
urlParams.delete('body').replace(/\+/g, '%20');;
queryString = urlParams.toString();
overhead = baseOverhead + (queryString ? 1 + queryString.length : 0);
}