Refine survey editor flow and auth styling
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$GLOBALS['TL_CSS'][] = 'assets/survey-backend.css|static';
|
$GLOBALS['TL_CSS'][] = 'assets/survey-backend.css|static';
|
||||||
|
$GLOBALS['TL_CSS'][] = 'bundles/survey/css/auth-module.css|static';
|
||||||
|
|
||||||
$GLOBALS['TL_MODELS']['tl_survey'] = Mummert\SurveyBundle\Model\SurveyModel::class;
|
$GLOBALS['TL_MODELS']['tl_survey'] = Mummert\SurveyBundle\Model\SurveyModel::class;
|
||||||
$GLOBALS['TL_MODELS']['tl_survey_category'] = Mummert\SurveyBundle\Model\SurveyCategoryModel::class;
|
$GLOBALS['TL_MODELS']['tl_survey_category'] = Mummert\SurveyBundle\Model\SurveyCategoryModel::class;
|
||||||
|
|||||||
@@ -40,30 +40,22 @@
|
|||||||
.survey-brand-strip {
|
.survey-brand-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-right: max(var(--survey-page-gutter), calc((100% - 1400px) / 2));
|
|
||||||
margin-bottom: clamp(2rem, 4vw, 3.5rem);
|
|
||||||
margin-left: max(var(--survey-page-gutter), calc((100% - 1400px) / 2));
|
margin-left: max(var(--survey-page-gutter), calc((100% - 1400px) / 2));
|
||||||
padding: 1rem 1.25rem 0.5rem;
|
margin-right: max(var(--survey-page-gutter), calc((100% - 1400px) / 2));
|
||||||
}
|
margin-top: 0;
|
||||||
|
margin-bottom: clamp(1.25rem, 3vw, 2.5rem);
|
||||||
.survey-brand-logo {
|
padding: 0.5rem 0 0;
|
||||||
display: block;
|
box-sizing: border-box;
|
||||||
min-width: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
flex-shrink: 1;
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey-brand-logo.primary {
|
.survey-brand-logo.primary {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: min(100%, 320px);
|
max-width: min(100%, 320px);
|
||||||
height: 75px;
|
height: 82px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey-brand-logo.secondary {
|
.survey-brand-logo.secondary {
|
||||||
@@ -240,6 +232,72 @@
|
|||||||
box-shadow: 0 12px 32px rgba(34, 34, 34, 0.06);
|
box-shadow: 0 12px 32px rgba(34, 34, 34, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-list-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 1.1rem 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__head {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__title-wrap {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__title-wrap h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__stats {
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__actions {
|
||||||
|
margin-top: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__actions .survey-question-action-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__toggle-form {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle--compact {
|
||||||
|
min-height: 2.8rem;
|
||||||
|
padding: 0.45rem 0.65rem 0.45rem 0.85rem;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle--compact .survey-publish-toggle__track {
|
||||||
|
width: 3rem;
|
||||||
|
height: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle--compact .survey-publish-toggle__thumb {
|
||||||
|
width: 1.4rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle--compact.is-on .survey-publish-toggle__thumb {
|
||||||
|
transform: translateX(1.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
.survey-meta {
|
.survey-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -270,6 +328,89 @@
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-button-row--form-actions {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-button-row--editor-actions {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 0.55rem 0.7rem 0.55rem 1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(0, 100, 173, 0.18);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: var(--survey-blue);
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 3.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle.is-on {
|
||||||
|
border-color: rgba(0, 174, 151, 0.3);
|
||||||
|
background: rgba(0, 174, 151, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle__copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.05rem;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle__label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--survey-gray-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle__value {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle__track {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 3.35rem;
|
||||||
|
height: 1.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(162, 168, 180, 0.45);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle.is-on .survey-publish-toggle__track {
|
||||||
|
background: rgba(0, 174, 151, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle__thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.15rem;
|
||||||
|
left: 0.15rem;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(34, 34, 34, 0.18);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle.is-on .survey-publish-toggle__thumb {
|
||||||
|
transform: translateX(1.45rem);
|
||||||
|
}
|
||||||
|
|
||||||
.survey-button {
|
.survey-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -287,6 +428,11 @@
|
|||||||
.survey-button.secondary { background: rgba(255,255,255,0.86); color: var(--survey-blue); border-color: rgba(0,100,173,0.18); }
|
.survey-button.secondary { background: rgba(255,255,255,0.86); color: var(--survey-blue); border-color: rgba(0,100,173,0.18); }
|
||||||
.survey-button.danger { background: rgba(157, 82, 118, 0.12); color: var(--survey-purple); border-color: rgba(157, 82, 118, 0.22); }
|
.survey-button.danger { background: rgba(157, 82, 118, 0.12); color: var(--survey-purple); border-color: rgba(157, 82, 118, 0.22); }
|
||||||
|
|
||||||
|
.survey-button-compact {
|
||||||
|
padding: 0.65rem 0.9rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-form-grid {
|
.survey-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -302,6 +448,7 @@
|
|||||||
.survey-label textarea,
|
.survey-label textarea,
|
||||||
.survey-label select {
|
.survey-label select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
border: 1px solid rgba(162,168,180,0.5);
|
border: 1px solid rgba(162,168,180,0.5);
|
||||||
padding: 0.8rem 0.9rem;
|
padding: 0.8rem 0.9rem;
|
||||||
@@ -319,6 +466,16 @@
|
|||||||
background: rgba(252,228,214,0.42);
|
background: rgba(252,228,214,0.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-question-toggles {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-toggles .survey-inline-check {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-alert {
|
.survey-alert {
|
||||||
border-radius: 1.2rem;
|
border-radius: 1.2rem;
|
||||||
padding: 1rem 1.1rem;
|
padding: 1rem 1.1rem;
|
||||||
@@ -327,6 +484,15 @@
|
|||||||
border: 1px solid rgba(162,168,180,0.35);
|
border: 1px solid rgba(162,168,180,0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-alert.success {
|
||||||
|
background: linear-gradient(135deg, rgba(0, 174, 151, 0.2), rgba(255,255,255,0.96));
|
||||||
|
border-color: rgba(0, 174, 151, 0.62);
|
||||||
|
color: #085e57;
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 174, 151, 0.16);
|
||||||
|
font-weight: 700;
|
||||||
|
border-left: 0.45rem solid rgba(0, 174, 151, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
.survey-alert.error { border-color: rgba(157, 82, 118, 0.28); color: var(--survey-purple); }
|
.survey-alert.error { border-color: rgba(157, 82, 118, 0.28); color: var(--survey-purple); }
|
||||||
.survey-alert.warning { border-color: rgba(236, 124, 50, 0.28); color: var(--survey-orange); }
|
.survey-alert.warning { border-color: rgba(236, 124, 50, 0.28); color: var(--survey-orange); }
|
||||||
|
|
||||||
@@ -336,6 +502,178 @@
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-kpis--compact {
|
||||||
|
grid-template-columns: minmax(0, 360px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-editor-layout {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-editor-meta {
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-editor-shell {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-section-head {
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item {
|
||||||
|
border: 1px solid rgba(162, 168, 180, 0.34);
|
||||||
|
border-radius: 1.3rem;
|
||||||
|
background: rgba(255, 255, 255, 0.78);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-toggle {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item .handorgel__header__button,
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus {
|
||||||
|
background: transparent;
|
||||||
|
border-top: 0;
|
||||||
|
color: inherit;
|
||||||
|
transition: background 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus,
|
||||||
|
.survey-accordion-item .handorgel__header--opened .handorgel__header__button {
|
||||||
|
background: var(--survey-blue);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover .survey-accordion-index,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus .survey-accordion-index,
|
||||||
|
.survey-accordion-item .handorgel__header--opened .survey-accordion-index,
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover .survey-accordion-copy strong,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus .survey-accordion-copy strong,
|
||||||
|
.survey-accordion-item .handorgel__header--opened .survey-accordion-copy strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover .survey-badge,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus .survey-badge,
|
||||||
|
.survey-accordion-item .handorgel__header--opened .survey-badge {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.8rem;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-index {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 2ch;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-family: "Blogger Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--survey-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-copy strong {
|
||||||
|
color: var(--survey-blue);
|
||||||
|
font-family: "Blogger Sans", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-copy span {
|
||||||
|
color: var(--survey-gray-dark);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-panel {
|
||||||
|
border-top: 1px solid rgba(162, 168, 180, 0.24);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-panel .handorgel__content__inner {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-condition-card {
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-button-row--question-actions {
|
||||||
|
margin-top: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-action-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-dirty-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
min-height: 3rem;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(189, 29, 29, 0.14);
|
||||||
|
border: 1px solid rgba(189, 29, 29, 0.35);
|
||||||
|
color: #a11f1f;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-dirty-hint[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-progress {
|
.survey-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.75rem;
|
height: 0.75rem;
|
||||||
@@ -367,24 +705,96 @@
|
|||||||
grid-template-columns: 0.95fr 1.05fr;
|
grid-template-columns: 0.95fr 1.05fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-editor-layout > .survey-editor-meta {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-editor-layout > [data-survey-question-editor] {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-editor-layout > .survey-editor-side {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-results-layout {
|
.survey-results-layout {
|
||||||
grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.05fr);
|
grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.05fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
.survey-list-card__head {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__toggle-form {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-list-card__toggle-form .survey-publish-toggle {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-brand-strip {
|
.survey-brand-strip {
|
||||||
padding: 0.75rem 0.75rem 0.25rem;
|
padding: 0.75rem 0.75rem 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-brand-logo.primary {
|
||||||
|
height: 64px;
|
||||||
|
max-width: min(100%, 260px);
|
||||||
|
}
|
||||||
|
|
||||||
.survey-brand-logo.secondary {
|
.survey-brand-logo.secondary {
|
||||||
height: 75px;
|
height: 75px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-kpis--compact {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-actions {
|
||||||
|
width: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-toggle {
|
||||||
|
padding: 0.95rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-publish-toggle:not(.survey-publish-toggle--compact) {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var surveyEditor = document.querySelector('[data-survey-handorgel]');
|
||||||
|
|
||||||
|
var resetEditorViewport = function () {
|
||||||
|
if (!surveyEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (surveyEditor && window.history && 'scrollRestoration' in window.history) {
|
||||||
|
window.history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEditorViewport();
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
window.setTimeout(resetEditorViewport, 0);
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
document.querySelectorAll('[data-survey-range]').forEach(function (input) {
|
document.querySelectorAll('[data-survey-range]').forEach(function (input) {
|
||||||
var targetId = input.getAttribute('data-survey-range-target');
|
var targetId = input.getAttribute('data-survey-range-target');
|
||||||
var output = targetId ? document.getElementById(targetId) : null;
|
var output = targetId ? document.getElementById(targetId) : null;
|
||||||
@@ -428,5 +838,117 @@
|
|||||||
select.addEventListener('change', toggle);
|
select.addEventListener('change', toggle);
|
||||||
toggle();
|
toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-copy-text]').forEach(function (button) {
|
||||||
|
var originalLabel = button.getAttribute('data-copy-label') || button.textContent;
|
||||||
|
var successLabel = button.getAttribute('data-copy-success') || originalLabel;
|
||||||
|
|
||||||
|
var copyText = function (value) {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
return navigator.clipboard.writeText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var helper = document.createElement('textarea');
|
||||||
|
helper.value = value;
|
||||||
|
helper.setAttribute('readonly', 'readonly');
|
||||||
|
helper.style.position = 'absolute';
|
||||||
|
helper.style.left = '-9999px';
|
||||||
|
document.body.appendChild(helper);
|
||||||
|
helper.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(helper);
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
document.body.removeChild(helper);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
var text = button.getAttribute('data-copy-text') || '';
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyText(text).then(function () {
|
||||||
|
button.textContent = successLabel;
|
||||||
|
window.setTimeout(function () {
|
||||||
|
button.textContent = originalLabel;
|
||||||
|
}, 1800);
|
||||||
|
}).catch(function () {
|
||||||
|
button.textContent = originalLabel;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-survey-dirty-form]').forEach(function (form) {
|
||||||
|
var formId = form.getAttribute('id') || '';
|
||||||
|
var hint = form.querySelector('[data-survey-dirty-hint]');
|
||||||
|
|
||||||
|
if (!hint && formId) {
|
||||||
|
hint = document.querySelector('[data-survey-dirty-hint-for="' + formId + '"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializeForm = function () {
|
||||||
|
var entries = [];
|
||||||
|
var formData = new FormData(form);
|
||||||
|
|
||||||
|
formData.forEach(function (value, key) {
|
||||||
|
entries.push(key + '=' + String(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries.join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
var initialState = serializeForm();
|
||||||
|
|
||||||
|
var updateDirtyState = function () {
|
||||||
|
hint.hidden = serializeForm() === initialState;
|
||||||
|
};
|
||||||
|
|
||||||
|
var markDirty = function () {
|
||||||
|
updateDirtyState();
|
||||||
|
};
|
||||||
|
|
||||||
|
hint.hidden = true;
|
||||||
|
|
||||||
|
form.addEventListener('input', markDirty);
|
||||||
|
form.addEventListener('change', markDirty);
|
||||||
|
form.addEventListener('reset', function () {
|
||||||
|
window.setTimeout(function () {
|
||||||
|
hint.hidden = true;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof handorgel === 'function') {
|
||||||
|
document.querySelectorAll('[data-survey-handorgel]').forEach(function (element) {
|
||||||
|
var accordion = new handorgel(element, {
|
||||||
|
collapsible: true,
|
||||||
|
multiSelectable: false,
|
||||||
|
initialOpenTransition: true,
|
||||||
|
initialOpenTransitionDelay: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accordion && Array.isArray(accordion.folds)) {
|
||||||
|
accordion.folds.forEach(function (fold) {
|
||||||
|
if (fold && fold.expanded) {
|
||||||
|
fold.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEditorViewport();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
{% trans_default_domain 'messages' %}
|
{% trans_default_domain 'messages' %}
|
||||||
|
|
||||||
{% include '@Survey/frontend/_survey_assets.html.twig' %}
|
{% include '@Survey/frontend/_survey_assets.html.twig' %}
|
||||||
|
<link rel="stylesheet" href="{{ asset('assets/handorgel/css/handorgel.min.css') }}">
|
||||||
{% include '@Survey/frontend/_survey_branding.html.twig' %}
|
{% include '@Survey/frontend/_survey_branding.html.twig' %}
|
||||||
|
|
||||||
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
|
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
|
||||||
|
|
||||||
<section class="survey-shell survey-grid">
|
<section class="survey-shell survey-grid">
|
||||||
|
{% for message in app.flashes('success') %}
|
||||||
|
<div class="survey-alert success">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('error') %}
|
||||||
|
<div class="survey-alert error">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% if moduleHeadline %}
|
{% if moduleHeadline %}
|
||||||
<header>
|
<header>
|
||||||
<h2>{{ moduleHeadline }}</h2>
|
<h2>{{ moduleHeadline }}</h2>
|
||||||
@@ -14,177 +23,264 @@
|
|||||||
|
|
||||||
{% if loginRequired %}
|
{% if loginRequired %}
|
||||||
<div class="survey-alert warning">{{ 'survey.edit.login_required'|trans }}</div>
|
<div class="survey-alert warning">{{ 'survey.edit.login_required'|trans }}</div>
|
||||||
|
{% elseif entryNotAllowed|default(false) %}
|
||||||
|
<div class="survey-card survey-grid">
|
||||||
|
<div class="survey-alert warning">{{ 'survey.edit.entry_not_allowed'|trans }}</div>
|
||||||
|
<div class="survey-button-row">
|
||||||
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% elseif createMode %}
|
{% elseif createMode %}
|
||||||
<div class="survey-card">
|
<div class="survey-card">
|
||||||
<h3>{{ 'survey.edit.create_title'|trans }}</h3>
|
<h3>{{ 'survey.edit.create_title'|trans }}</h3>
|
||||||
<p>{{ 'survey.edit.create_text'|trans }}</p>
|
<p>{{ 'survey.edit.create_text'|trans }}</p>
|
||||||
{{ form_start(surveyForm, {attr: {class: 'survey-form-grid'}}) }}
|
{{ form_start(surveyForm, {attr: {class: 'survey-form-grid'}}) }}
|
||||||
<label class="survey-label">{{ 'survey.edit.label_title'|trans }}{{ form_widget(surveyForm.title) }}{{ form_errors(surveyForm.title) }}</label>
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
{{ form_widget(surveyForm._token) }}
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_survey_title'|trans }}{{ form_widget(surveyForm.title) }}{{ form_errors(surveyForm.title) }}</label>
|
||||||
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(surveyForm.description) }}</label>
|
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(surveyForm.description) }}</label>
|
||||||
<label class="survey-inline-check">{{ form_widget(surveyForm.published) }}<span>{{ 'survey.edit.publish_directly'|trans }}</span></label>
|
|
||||||
<div class="survey-button-row">
|
<div class="survey-button-row">
|
||||||
<button class="survey-button primary">{{ 'survey.edit.create_submit'|trans }}</button>
|
<button class="survey-button primary">{{ 'survey.edit.create_submit'|trans }}</button>
|
||||||
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ form_end(surveyForm) }}
|
{{ form_end(surveyForm, {render_rest: false}) }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set locked = survey.isLocked == '1' %}
|
{% set locked = survey.isLocked == '1' %}
|
||||||
<div class="survey-button-row">
|
<div class="survey-button-row survey-button-row--editor-actions">
|
||||||
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
||||||
{% if publicUrl %}<a class="survey-button primary" href="{{ publicUrl }}" target="_blank" rel="noreferrer">{{ 'survey.edit.public_view_open'|trans }}</a>{% endif %}
|
{% if publicUrl %}<a class="survey-button primary" href="{{ publicUrl }}" target="_blank" rel="noreferrer">{{ 'survey.edit.public_view_open'|trans }}</a>{% endif %}
|
||||||
</div>
|
{% if publicUrl %}
|
||||||
|
<button
|
||||||
<div class="survey-kpis">
|
type="button"
|
||||||
<article class="survey-card"><strong>{{ 'survey.edit.public_link'|trans }}</strong><div>{% if publicUrl %}<a href="{{ publicUrl }}" target="_blank" rel="noreferrer">{{ publicUrl }}</a>{% else %}{{ 'survey.edit.no_public_page'|trans }}{% endif %}</div></article>
|
class="survey-button secondary"
|
||||||
<article class="survey-card"><strong>{{ 'survey.edit.status'|trans }}</strong><div class="survey-meta"><span class="survey-badge {{ survey.published ? 'success' : 'neutral' }}">{{ survey.published ? 'survey.edit.published'|trans : 'survey.edit.draft'|trans }}</span>{% if locked %}<span class="survey-badge warning">{{ 'survey.edit.locked'|trans }}</span>{% endif %}</div></article>
|
data-copy-text="{{ publicUrl|e('html_attr') }}"
|
||||||
<article class="survey-card"><strong>{{ 'survey.edit.editors'|trans }}</strong><div>{{ 'survey.edit.editors_count'|trans({'%count%': editors|length}) }}</div></article>
|
data-copy-label="{{ 'survey.edit.copy_public_link'|trans|e('html_attr') }}"
|
||||||
|
data-copy-success="{{ 'survey.edit.copy_public_link_success'|trans|e('html_attr') }}"
|
||||||
|
>{{ 'survey.edit.copy_public_link'|trans }}</button>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" class="survey-publish-toggle-form">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
<input type="hidden" name="_survey_action" value="toggle-published">
|
||||||
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('toggle-published-' ~ survey.id) }}">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="survey-publish-toggle{{ survey.published ? ' is-on' : '' }}"
|
||||||
|
aria-pressed="{{ survey.published ? 'true' : 'false' }}"
|
||||||
|
>
|
||||||
|
<span class="survey-publish-toggle__track" aria-hidden="true">
|
||||||
|
<span class="survey-publish-toggle__thumb"></span>
|
||||||
|
</span>
|
||||||
|
<span class="survey-publish-toggle__copy">
|
||||||
|
<span class="survey-publish-toggle__label">{{ 'survey.edit.status'|trans }}</span>
|
||||||
|
<span class="survey-publish-toggle__value">{{ survey.published ? 'survey.edit.published'|trans : 'survey.edit.draft'|trans }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if locked %}
|
{% if locked %}
|
||||||
<div class="survey-alert warning">{{ 'survey.edit.structure_locked'|trans }}</div>
|
<div class="survey-alert warning">{{ 'survey.edit.structure_locked'|trans }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="survey-grid-columns">
|
{% set surveyMetaFormId = 'survey-meta-form' %}
|
||||||
<div class="survey-grid">
|
{% set newQuestionFormId = 'survey-question-form-new' %}
|
||||||
<div class="survey-card">
|
|
||||||
<h3>{{ 'survey.edit.metadata'|trans }}</h3>
|
|
||||||
{{ form_start(surveyForm, {attr: {class: 'survey-form-grid'}}) }}
|
|
||||||
<label class="survey-label">{{ 'survey.edit.label_title'|trans }}{{ form_widget(surveyForm.title) }}{{ form_errors(surveyForm.title) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(surveyForm.description) }}</label>
|
|
||||||
<label class="survey-inline-check">{{ form_widget(surveyForm.published) }}<span>{{ 'survey.edit.publish_survey'|trans }}</span></label>
|
|
||||||
<div class="survey-button-row"><button class="survey-button primary">{{ 'survey.edit.save'|trans }}</button></div>
|
|
||||||
{{ form_end(surveyForm) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="survey-card">
|
<div class="survey-grid-columns survey-editor-layout">
|
||||||
<h3>{{ 'survey.edit.editors_title'|trans }}</h3>
|
<div class="survey-question-editor-shell" data-survey-question-editor>
|
||||||
<div class="survey-grid">
|
<div class="survey-accordion-list handorgel" data-survey-handorgel>
|
||||||
{% for editor in editors %}
|
<article id="survey-metadata" class="survey-accordion-item">
|
||||||
<div>
|
<h4 class="survey-accordion-header handorgel__header">
|
||||||
<strong>{{ ([editor.firstname, editor.lastname]|join(' ')|trim|striptags) ?: (editor.email|striptags|trim) }}</strong><br>
|
<button
|
||||||
<span>{{ editor.email|striptags|trim }}</span>
|
type="button"
|
||||||
</div>
|
class="survey-accordion-toggle handorgel__header__button"
|
||||||
{% else %}
|
>
|
||||||
<div>{{ 'survey.edit.no_editors'|trans }}</div>
|
<span class="survey-accordion-heading">
|
||||||
{% endfor %}
|
<span class="survey-accordion-copy">
|
||||||
</div>
|
<strong>Metadaten bearbeiten</strong>
|
||||||
</div>
|
</span>
|
||||||
|
</span>
|
||||||
<div class="survey-card">
|
</button>
|
||||||
<h3>{{ 'survey.edit.responses_title'|trans }}</h3>
|
</h4>
|
||||||
<div class="survey-grid">
|
<div class="survey-accordion-panel handorgel__content">
|
||||||
{% for submission in submissions %}
|
<div class="handorgel__content__inner">
|
||||||
<article>
|
{{ form_start(surveyForm, {action: metadataFormActionUrl, attr: {class: 'survey-form-grid', id: surveyMetaFormId, 'data-survey-dirty-form': '1'}}) }}
|
||||||
<div class="survey-meta">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<span class="survey-badge neutral">{{ 'survey.list.answers'|trans({'%count%': submission.answerCount}) }}</span>
|
<input type="hidden" name="{{ surveyForm.published.vars.full_name }}" value="{{ survey.published ? '1' : '0' }}">
|
||||||
<span class="survey-badge {{ submission.isFinished ? 'success' : 'warning' }}">{{ submission.isFinished ? 'survey.edit.completed'|trans : 'survey.edit.open'|trans }}</span>
|
{{ form_widget(surveyForm._token) }}
|
||||||
|
{{ form_errors(surveyForm) }}
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_survey_title'|trans }}{{ form_widget(surveyForm.title) }}{{ form_errors(surveyForm.title) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(surveyForm.description) }}</label>
|
||||||
|
<div class="survey-button-row survey-button-row--form-actions">
|
||||||
|
<button class="survey-button primary">{{ 'survey.edit.save_metadata'|trans }}</button>
|
||||||
|
<div class="survey-dirty-hint" data-survey-dirty-hint-for="{{ surveyMetaFormId }}" hidden>{{ 'survey.edit.dirty_metadata'|trans }}</div>
|
||||||
|
</div>
|
||||||
|
{{ form_end(surveyForm, {render_rest: false}) }}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ 'survey.edit.started_at'|trans }}: {{ submission.startedAt ? submission.startedAt|date('d.m.Y H:i') : 'survey.edit.unknown'|trans }}</div>
|
</div>
|
||||||
<div>{{ 'survey.edit.completed_at'|trans }}: {{ submission.completedAt ? submission.completedAt|date('d.m.Y H:i') : 'survey.edit.unknown'|trans }}</div>
|
</article>
|
||||||
<div>{{ 'survey.edit.participation_code'|trans }}: {{ submission.token }}</div>
|
|
||||||
</article>
|
|
||||||
{% else %}
|
|
||||||
<div>{{ 'survey.edit.no_submissions'|trans }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="survey-grid">
|
|
||||||
<div class="survey-card" data-survey-question-editor>
|
|
||||||
<div class="survey-meta"><h3>{{ 'survey.edit.questions_title'|trans }}</h3>{% if activeQuestionId %}<span class="survey-badge neutral">{{ 'survey.edit.edit_mode'|trans }}</span>{% endif %}</div>
|
|
||||||
{% if not locked %}
|
|
||||||
{% set questionType = questionForm.type.vars.value ?: 'yes_no_maybe' %}
|
|
||||||
{{ form_start(questionForm, {attr: {class: 'survey-form-grid'}}) }}
|
|
||||||
<label class="survey-label">{{ 'survey.edit.label_type'|trans }}{{ form_widget(questionForm.type, {attr: {'data-survey-question-type': '1'}}) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.label_question'|trans }}{{ form_widget(questionForm.question) }}{{ form_errors(questionForm.question) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(questionForm.description) }}</label>
|
|
||||||
<label class="survey-inline-check">{{ form_widget(questionForm.mandatory) }}<span>{{ 'survey.edit.mandatory'|trans }}</span></label>
|
|
||||||
<label class="survey-inline-check">{{ form_widget(questionForm.published) }}<span>{{ 'survey.edit.active'|trans }}</span></label>
|
|
||||||
<label class="survey-inline-check yes-no-config-field"{% if questionType != 'yes_no_maybe' %} style="display:none"{% endif %}>{{ form_widget(questionForm.allowMaybe) }}<span>{{ 'survey.edit.allow_maybe'|trans }}</span></label>
|
|
||||||
<div class="yes-no-config-field survey-grid-columns"{% if questionType != 'yes_no_maybe' %} style="display:none;grid-template-columns: repeat(2, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(2, minmax(0, 1fr));"{% endif %}>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.jump_yes'|trans }}{{ form_widget(questionForm.jumpOnYes) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.jump_no'|trans }}{{ form_widget(questionForm.jumpOnNo) }}</label>
|
|
||||||
</div>
|
|
||||||
<label class="survey-inline-check choice-config-field"{% if questionType != 'choice' %} style="display:none"{% endif %}>{{ form_widget(questionForm.allowMultiple) }}<span>{{ 'survey.edit.allow_multiple'|trans }}</span></label>
|
|
||||||
<div class="choice-config-field survey-grid-columns"{% if questionType != 'choice' %} style="display:none;grid-template-columns: repeat(2, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(2, minmax(0, 1fr));"{% endif %}>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 1}) }}{{ form_widget(questionForm.answerOption1) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 2}) }}{{ form_widget(questionForm.answerOption2) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 3}) }}{{ form_widget(questionForm.answerOption3) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 4}) }}{{ form_widget(questionForm.answerOption4) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 5}) }}{{ form_widget(questionForm.answerOption5) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 6}) }}{{ form_widget(questionForm.answerOption6) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 7}) }}{{ form_widget(questionForm.answerOption7) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 8}) }}{{ form_widget(questionForm.answerOption8) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 9}) }}{{ form_widget(questionForm.answerOption9) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 10}) }}{{ form_widget(questionForm.answerOption10) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="range-config-field survey-grid-columns"{% if questionType != 'range' %} style="display:none;grid-template-columns: repeat(3, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(3, minmax(0, 1fr));"{% endif %}>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.range_min'|trans }}{{ form_widget(questionForm.rangeMin) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.range_max'|trans }}{{ form_widget(questionForm.rangeMax) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.range_step'|trans }}{{ form_widget(questionForm.rangeStep) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="survey-button-row"><button class="survey-button primary">{{ activeQuestionId ? 'survey.edit.update_question'|trans : 'survey.edit.save_question'|trans }}</button></div>
|
|
||||||
{{ form_end(questionForm) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="survey-grid">
|
|
||||||
{% for question in questions %}
|
{% for question in questions %}
|
||||||
<article class="survey-card">
|
{% set questionAnchor = 'survey-question-' ~ question.id %}
|
||||||
<div class="survey-meta">
|
{% set isOpen = activeQuestionId == question.id %}
|
||||||
<span class="survey-badge neutral">{{ question.type }}</span>
|
{% set questionItemForm = questionForms[question.id]|default(null) %}
|
||||||
|
{% set questionFormId = 'survey-question-form-' ~ question.id %}
|
||||||
|
<article id="{{ questionAnchor }}" class="survey-accordion-item">
|
||||||
|
<h4 class="survey-accordion-header handorgel__header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="survey-accordion-toggle handorgel__header__button"
|
||||||
|
>
|
||||||
|
<span class="survey-accordion-heading">
|
||||||
|
<span class="survey-accordion-index">{{ loop.index }}.</span>
|
||||||
|
<span class="survey-accordion-copy">
|
||||||
|
<strong>{{ question.question|striptags|trim ?: ('survey.edit.question_fallback'|trans({'%id%': question.id})) }}</strong>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="survey-meta survey-accordion-actions">
|
||||||
|
<span class="survey-badge neutral">{{ ('survey.survey.question_type_' ~ question.type)|trans }}</span>
|
||||||
{% if question.mandatory %}<span class="survey-badge warning">{{ 'survey.edit.mandatory_badge'|trans }}</span>{% endif %}
|
{% if question.mandatory %}<span class="survey-badge warning">{{ 'survey.edit.mandatory_badge'|trans }}</span>{% endif %}
|
||||||
{% if loop.first %}<span class="survey-badge success">{{ 'survey.edit.start_badge'|trans }}</span>{% endif %}
|
{% if loop.first %}<span class="survey-badge success">{{ 'survey.edit.start_badge'|trans }}</span>{% endif %}
|
||||||
</div>
|
</span>
|
||||||
<h4>{{ question.question|striptags|trim }}</h4>
|
</button>
|
||||||
<p>{{ (question.description|striptags|trim) ?: 'survey.edit.no_extra_description'|trans }}</p>
|
</h4>
|
||||||
{% if not locked %}
|
<div class="survey-accordion-panel handorgel__content">
|
||||||
<div class="survey-button-row">
|
<div class="handorgel__content__inner" data-survey-question-editor>
|
||||||
<a class="survey-button secondary" href="?survey={{ survey.id }}&question={{ question.id }}">{{ 'survey.list.edit'|trans }}</a>
|
{% if questionItemForm %}
|
||||||
<form method="post">
|
{% set questionType = questionItemForm.type.vars.value ?: question.type ?: 'yes_no_maybe' %}
|
||||||
<input type="hidden" name="_survey_action" value="delete-question">
|
{{ form_start(questionItemForm, {attr: {class: 'survey-form-grid', id: questionFormId, 'data-survey-dirty-form': '1'}}) }}
|
||||||
<input type="hidden" name="item_id" value="{{ question.id }}">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('delete-question-' ~ question.id) }}">
|
{{ form_errors(questionItemForm) }}
|
||||||
<button class="survey-button danger" onclick="return confirm('{{ 'survey.edit.delete_question_confirm'|trans|e('js') }}');">{{ 'survey.edit.delete'|trans }}</button>
|
<label class="survey-label">{{ 'survey.edit.label_type'|trans }}{{ form_widget(questionItemForm.type, {attr: {'data-survey-question-type': '1'}}) }}</label>
|
||||||
</form>
|
<label class="survey-label">{{ 'survey.edit.label_question'|trans }}{{ form_widget(questionItemForm.question) }}{{ form_errors(questionItemForm.question) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(questionItemForm.description) }}</label>
|
||||||
|
<div class="survey-question-toggles">
|
||||||
|
<label class="survey-inline-check">{{ form_widget(questionItemForm.mandatory) }}<span>{{ 'survey.edit.mandatory'|trans }}</span></label>
|
||||||
|
<label class="survey-inline-check">{{ form_widget(questionItemForm.published) }}<span>{{ 'survey.edit.active'|trans }}</span></label>
|
||||||
|
<label class="survey-inline-check yes-no-config-field"{% if questionType != 'yes_no_maybe' %} style="display:none"{% endif %}>{{ form_widget(questionItemForm.allowMaybe) }}<span>{{ 'survey.edit.allow_maybe'|trans }}</span></label>
|
||||||
|
<label class="survey-inline-check choice-config-field"{% if questionType != 'choice' %} style="display:none"{% endif %}>{{ form_widget(questionItemForm.allowMultiple) }}<span>{{ 'survey.edit.allow_multiple'|trans }}</span></label>
|
||||||
|
</div>
|
||||||
|
<div class="yes-no-config-field survey-grid-columns"{% if questionType != 'yes_no_maybe' %} style="display:none;grid-template-columns: repeat(2, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(2, minmax(0, 1fr));"{% endif %}>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.jump_yes'|trans }}{{ form_widget(questionItemForm.jumpOnYes) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.jump_no'|trans }}{{ form_widget(questionItemForm.jumpOnNo) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="choice-config-field survey-grid-columns"{% if questionType != 'choice' %} style="display:none;grid-template-columns: repeat(2, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(2, minmax(0, 1fr));"{% endif %}>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 1}) }}{{ form_widget(questionItemForm.answerOption1) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 2}) }}{{ form_widget(questionItemForm.answerOption2) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 3}) }}{{ form_widget(questionItemForm.answerOption3) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 4}) }}{{ form_widget(questionItemForm.answerOption4) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 5}) }}{{ form_widget(questionItemForm.answerOption5) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 6}) }}{{ form_widget(questionItemForm.answerOption6) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 7}) }}{{ form_widget(questionItemForm.answerOption7) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 8}) }}{{ form_widget(questionItemForm.answerOption8) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 9}) }}{{ form_widget(questionItemForm.answerOption9) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 10}) }}{{ form_widget(questionItemForm.answerOption10) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="range-config-field survey-grid-columns"{% if questionType != 'range' %} style="display:none;grid-template-columns: repeat(3, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(3, minmax(0, 1fr));"{% endif %}>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.range_min'|trans }}{{ form_widget(questionItemForm.rangeMin) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.range_max'|trans }}{{ form_widget(questionItemForm.rangeMax) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.range_step'|trans }}{{ form_widget(questionItemForm.rangeStep) }}</label>
|
||||||
|
</div>
|
||||||
|
{{ form_end(questionItemForm) }}
|
||||||
|
|
||||||
|
<div class="survey-button-row survey-button-row--form-actions survey-button-row--question-actions">
|
||||||
|
<button class="survey-button primary" type="submit" form="{{ questionFormId }}">{{ 'survey.edit.save_this_question'|trans }}</button>
|
||||||
|
{% if not locked %}
|
||||||
|
<form method="post" class="survey-question-action-form">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
<input type="hidden" name="_survey_action" value="delete-question">
|
||||||
|
<input type="hidden" name="item_id" value="{{ question.id }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('delete-question-' ~ question.id) }}">
|
||||||
|
<button class="survey-button danger" onclick="return confirm('{{ 'survey.edit.delete_question_confirm'|trans|e('js') }}');">{{ 'survey.edit.delete'|trans }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<div class="survey-dirty-hint" data-survey-dirty-hint-for="{{ questionFormId }}" hidden>{{ 'survey.edit.dirty_question'|trans }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>{{ 'survey.edit.no_questions'|trans }}</div>
|
<div>{{ 'survey.edit.no_questions'|trans }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not locked %}
|
||||||
|
<article id="survey-question-new" class="survey-accordion-item">
|
||||||
|
<h4 class="survey-accordion-header handorgel__header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="survey-accordion-toggle handorgel__header__button"
|
||||||
|
>
|
||||||
|
<span class="survey-accordion-heading">
|
||||||
|
<span class="survey-accordion-index">+</span>
|
||||||
|
<span class="survey-accordion-copy">
|
||||||
|
<strong>{{ 'survey.edit.new_question'|trans }}</strong>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<div class="survey-accordion-panel handorgel__content">
|
||||||
|
<div class="handorgel__content__inner" data-survey-question-editor>
|
||||||
|
{% if newQuestionForm %}
|
||||||
|
{% set questionType = newQuestionForm.type.vars.value ?: 'yes_no_maybe' %}
|
||||||
|
{{ form_start(newQuestionForm, {attr: {class: 'survey-form-grid', id: newQuestionFormId, 'data-survey-dirty-form': '1'}}) }}
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
{{ form_errors(newQuestionForm) }}
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_type'|trans }}{{ form_widget(newQuestionForm.type, {attr: {'data-survey-question-type': '1'}}) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_question'|trans }}{{ form_widget(newQuestionForm.question) }}{{ form_errors(newQuestionForm.question) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.label_description'|trans }}{{ form_widget(newQuestionForm.description) }}</label>
|
||||||
|
<div class="survey-question-toggles">
|
||||||
|
<label class="survey-inline-check">{{ form_widget(newQuestionForm.mandatory) }}<span>{{ 'survey.edit.mandatory'|trans }}</span></label>
|
||||||
|
<label class="survey-inline-check">{{ form_widget(newQuestionForm.published) }}<span>{{ 'survey.edit.active'|trans }}</span></label>
|
||||||
|
<label class="survey-inline-check yes-no-config-field"{% if questionType != 'yes_no_maybe' %} style="display:none"{% endif %}>{{ form_widget(newQuestionForm.allowMaybe) }}<span>{{ 'survey.edit.allow_maybe'|trans }}</span></label>
|
||||||
|
<label class="survey-inline-check choice-config-field"{% if questionType != 'choice' %} style="display:none"{% endif %}>{{ form_widget(newQuestionForm.allowMultiple) }}<span>{{ 'survey.edit.allow_multiple'|trans }}</span></label>
|
||||||
|
</div>
|
||||||
|
<div class="yes-no-config-field survey-grid-columns"{% if questionType != 'yes_no_maybe' %} style="display:none;grid-template-columns: repeat(2, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(2, minmax(0, 1fr));"{% endif %}>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.jump_yes'|trans }}{{ form_widget(newQuestionForm.jumpOnYes) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.jump_no'|trans }}{{ form_widget(newQuestionForm.jumpOnNo) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="choice-config-field survey-grid-columns"{% if questionType != 'choice' %} style="display:none;grid-template-columns: repeat(2, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(2, minmax(0, 1fr));"{% endif %}>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 1}) }}{{ form_widget(newQuestionForm.answerOption1) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 2}) }}{{ form_widget(newQuestionForm.answerOption2) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 3}) }}{{ form_widget(newQuestionForm.answerOption3) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 4}) }}{{ form_widget(newQuestionForm.answerOption4) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 5}) }}{{ form_widget(newQuestionForm.answerOption5) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 6}) }}{{ form_widget(newQuestionForm.answerOption6) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 7}) }}{{ form_widget(newQuestionForm.answerOption7) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 8}) }}{{ form_widget(newQuestionForm.answerOption8) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 9}) }}{{ form_widget(newQuestionForm.answerOption9) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.answer_option'|trans({'%number%': 10}) }}{{ form_widget(newQuestionForm.answerOption10) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="range-config-field survey-grid-columns"{% if questionType != 'range' %} style="display:none;grid-template-columns: repeat(3, minmax(0, 1fr));"{% else %} style="grid-template-columns: repeat(3, minmax(0, 1fr));"{% endif %}>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.range_min'|trans }}{{ form_widget(newQuestionForm.rangeMin) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.range_max'|trans }}{{ form_widget(newQuestionForm.rangeMax) }}</label>
|
||||||
|
<label class="survey-label">{{ 'survey.edit.range_step'|trans }}{{ form_widget(newQuestionForm.rangeStep) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="survey-button-row survey-button-row--form-actions">
|
||||||
|
<button class="survey-button primary">{{ 'survey.edit.save_new_question'|trans }}</button>
|
||||||
|
<div class="survey-dirty-hint" data-survey-dirty-hint-for="{{ newQuestionFormId }}" hidden>{{ 'survey.edit.dirty_new_question'|trans }}</div>
|
||||||
|
</div>
|
||||||
|
{{ form_end(newQuestionForm) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="survey-grid survey-editor-side">
|
||||||
<div class="survey-card">
|
<div class="survey-card">
|
||||||
<div class="survey-meta"><h3>{{ 'survey.edit.conditions_title'|trans }}</h3>{% if activeConditionId %}<span class="survey-badge neutral">{{ 'survey.edit.edit_mode'|trans }}</span>{% endif %}</div>
|
<div class="survey-meta survey-section-head"><h3>{{ 'survey.edit.conditions_title'|trans }}</h3></div>
|
||||||
{% if not locked %}
|
|
||||||
{{ form_start(conditionForm, {attr: {class: 'survey-form-grid'}}) }}
|
|
||||||
<label class="survey-label">{{ 'survey.edit.source_question'|trans }}{{ form_widget(conditionForm.sourceQuestion) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.answer'|trans }}{{ form_widget(conditionForm.answerValue) }}</label>
|
|
||||||
<label class="survey-label">{{ 'survey.edit.next_question'|trans }}{{ form_widget(conditionForm.targetQuestion) }}</label>
|
|
||||||
<div class="survey-button-row"><button class="survey-button primary">{{ activeConditionId ? 'survey.edit.update_condition'|trans : 'survey.edit.save_condition'|trans }}</button></div>
|
|
||||||
{{ form_end(conditionForm) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="survey-grid">
|
<div class="survey-grid">
|
||||||
{% for condition in conditions %}
|
{% for condition in conditions %}
|
||||||
<article class="survey-card">
|
<article class="survey-card survey-condition-card">
|
||||||
<div><strong>{{ 'survey.edit.condition_prefix'|trans({'%value%': condition.answerValue|striptags|trim}) }}</strong></div>
|
{% set sourceLabel = (condition.sourceQuestionLabel|default('')|striptags|trim) ?: ('survey.edit.question_fallback'|trans({'%id%': condition.sourceQuestion})) %}
|
||||||
<div>{{ (condition.sourceQuestionLabel|striptags|trim) ?: 'survey.edit.question_fallback'|trans({'%id%': condition.sourceQuestion}) }}</div>
|
{% set targetLabel = (condition.targetQuestionLabel|default('')|striptags|trim) ?: ('survey.edit.question_fallback'|trans({'%id%': condition.targetQuestion})) %}
|
||||||
<div>{{ 'survey.edit.continue_to'|trans({'%target%': ((condition.targetQuestionLabel|striptags|trim) ?: ('survey.edit.question_fallback'|trans({'%id%': condition.targetQuestion})))}) }}</div>
|
<div><strong>Frage {{ condition.sourceQuestion }}: {{ sourceLabel }}</strong></div>
|
||||||
{% if not locked %}
|
<div>{{ 'survey.edit.condition_prefix'|trans({'%value%': condition.answerValue|default('')|striptags|trim|capitalize}) }}</div>
|
||||||
<div class="survey-button-row">
|
<div>weiter zu Frage {{ condition.targetQuestion }}: {{ targetLabel }}</div>
|
||||||
<a class="survey-button secondary" href="?survey={{ survey.id }}&condition={{ condition.id }}">{{ 'survey.edit.edit_condition'|trans }}</a>
|
|
||||||
<form method="post">
|
|
||||||
<input type="hidden" name="_survey_action" value="delete-condition">
|
|
||||||
<input type="hidden" name="item_id" value="{{ condition.id }}">
|
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('delete-condition-' ~ condition.id) }}">
|
|
||||||
<button class="survey-button danger" onclick="return confirm('{{ 'survey.edit.delete_condition_confirm'|trans|e('js') }}');">{{ 'survey.edit.delete'|trans }}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</article>
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>{{ 'survey.edit.no_conditions'|trans }}</div>
|
<div>{{ 'survey.edit.no_conditions'|trans }}</div>
|
||||||
@@ -195,3 +291,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
<script src="{{ asset('assets/handorgel/js/handorgel.min.js') }}"></script>
|
||||||
@@ -6,6 +6,14 @@
|
|||||||
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
|
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
|
||||||
|
|
||||||
<section class="survey-shell survey-grid">
|
<section class="survey-shell survey-grid">
|
||||||
|
{% for message in app.flashes('success') %}
|
||||||
|
<div class="survey-alert success">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('error') %}
|
||||||
|
<div class="survey-alert error">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% if moduleHeadline %}
|
{% if moduleHeadline %}
|
||||||
<header>
|
<header>
|
||||||
<h2>{{ moduleHeadline }}</h2>
|
<h2>{{ moduleHeadline }}</h2>
|
||||||
@@ -21,22 +29,43 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="survey-grid">
|
<div class="survey-grid survey-list-grid">
|
||||||
{% for survey in surveys %}
|
{% for survey in surveys %}
|
||||||
<article class="survey-card">
|
<article class="survey-card survey-list-card">
|
||||||
<div class="survey-meta">
|
<div class="survey-list-card__head">
|
||||||
<h3>{{ survey.title|striptags|trim }}</h3>
|
<div class="survey-list-card__title-wrap">
|
||||||
<span class="survey-badge {{ survey.published ? 'success' : 'neutral' }}">{{ survey.published ? 'survey.list.published'|trans : 'survey.list.draft'|trans }}</span>
|
<h3>{{ survey.title|striptags|trim }}</h3>
|
||||||
{% if survey.isLocked %}
|
{% if survey.isLocked %}
|
||||||
<span class="survey-badge warning">{{ 'survey.list.locked'|trans }}</span>
|
<div class="survey-meta">
|
||||||
{% endif %}
|
<span class="survey-badge warning">{{ 'survey.list.locked'|trans }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<form method="post" class="survey-publish-toggle-form survey-list-card__toggle-form">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
<input type="hidden" name="_survey_action" value="toggle-published">
|
||||||
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('toggle-published-' ~ survey.id) }}">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="survey-publish-toggle survey-publish-toggle--compact{{ survey.published ? ' is-on' : '' }}"
|
||||||
|
aria-pressed="{{ survey.published ? 'true' : 'false' }}"
|
||||||
|
>
|
||||||
|
<span class="survey-publish-toggle__track" aria-hidden="true">
|
||||||
|
<span class="survey-publish-toggle__thumb"></span>
|
||||||
|
</span>
|
||||||
|
<span class="survey-publish-toggle__copy">
|
||||||
|
<span class="survey-publish-toggle__label">{{ 'survey.edit.status'|trans }}</span>
|
||||||
|
<span class="survey-publish-toggle__value">{{ survey.published ? 'survey.list.published'|trans : 'survey.list.draft'|trans }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ (survey.description|striptags|trim) ?: 'survey.list.no_description'|trans }}</p>
|
<div class="survey-meta survey-list-card__stats">
|
||||||
<div class="survey-meta">
|
|
||||||
<span class="survey-badge neutral">{{ 'survey.list.questions'|trans({'%count%': survey.questionCount}) }}</span>
|
<span class="survey-badge neutral">{{ 'survey.list.questions'|trans({'%count%': survey.questionCount}) }}</span>
|
||||||
<span class="survey-badge neutral">{{ 'survey.list.answers'|trans({'%count%': survey.answerCount}) }}</span>
|
<span class="survey-badge neutral">{{ 'survey.list.participations'|trans({'%count%': survey.participationCount}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="survey-button-row">
|
<div class="survey-button-row survey-list-card__actions">
|
||||||
{% if survey.editUrl %}
|
{% if survey.editUrl %}
|
||||||
<a class="survey-button primary" href="{{ survey.editUrl }}">{{ 'survey.list.edit'|trans }}</a>
|
<a class="survey-button primary" href="{{ survey.editUrl }}">{{ 'survey.list.edit'|trans }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -46,6 +75,13 @@
|
|||||||
{% if survey.resultsUrl %}
|
{% if survey.resultsUrl %}
|
||||||
<a class="survey-button secondary" href="{{ survey.resultsUrl }}" target="_blank" rel="noreferrer">{{ 'survey.list.show_results'|trans }}</a>
|
<a class="survey-button secondary" href="{{ survey.resultsUrl }}" target="_blank" rel="noreferrer">{{ 'survey.list.show_results'|trans }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<form method="post" class="survey-question-action-form">
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
<input type="hidden" name="_survey_action" value="delete-survey">
|
||||||
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('delete-survey-' ~ survey.id) }}">
|
||||||
|
<button class="survey-button danger" onclick="return confirm('{{ 'survey.list.delete_confirm'|trans|e('js') }}') && confirm('{{ 'survey.list.delete_confirm_second'|trans|e('js') }}');">{{ 'survey.list.delete'|trans }}</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<article class="survey-card">
|
<article class="survey-card">
|
||||||
<div class="survey-meta">
|
<div class="survey-meta">
|
||||||
<h2>{{ survey.title|striptags|trim }}</h2>
|
<h2>{{ survey.title|striptags|trim }}</h2>
|
||||||
<span class="survey-badge neutral">{{ 'survey.results.completed_submissions'|trans({'%count%': completedSubmissionCount}) }}</span>
|
<span class="survey-badge neutral">{{ 'survey.results.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||||
<span class="survey-badge neutral">{{ 'survey.results.questions'|trans({'%count%': questionResults|length}) }}</span>
|
<span class="survey-badge neutral">{{ 'survey.results.questions'|trans({'%count%': questionResults|length}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<article class="survey-card survey-results-card">
|
<article class="survey-card survey-results-card">
|
||||||
<div class="survey-meta">
|
<div class="survey-meta">
|
||||||
<h3>{{ question.question|striptags|trim }}</h3>
|
<h3>{{ question.question|striptags|trim }}</h3>
|
||||||
<span class="survey-badge neutral">{{ 'survey.results.answers'|trans({'%count%': question.totalAnswers}) }}</span>
|
<span class="survey-badge neutral">{{ 'survey.results.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||||
<span class="survey-badge neutral">{{ 'survey.results.response_rate'|trans({'%rate%': question.responseRate}) }}</span>
|
<span class="survey-badge neutral">{{ 'survey.results.response_rate'|trans({'%rate%': question.responseRate}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "PT Sans Narrow";
|
||||||
|
src: url("../fonts/PTSansNarrow-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PT Sans Narrow";
|
||||||
|
src: url("../fonts/PTSansNarrow-Bold.woff2") format("woff2");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Blogger Sans";
|
||||||
|
src: url("../fonts/Blogger%20Sans-Medium.woff2") format("woff2");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--survey-auth-blue: #0064ad;
|
||||||
|
--survey-auth-orange: #ec7c32;
|
||||||
|
--survey-auth-green: #00ae97;
|
||||||
|
--survey-auth-ink: #22313f;
|
||||||
|
--survey-auth-muted: #5f6975;
|
||||||
|
--survey-auth-border: rgba(162, 168, 180, 0.34);
|
||||||
|
--survey-auth-surface: rgba(255, 255, 255, 0.94);
|
||||||
|
--survey-auth-shadow: 0 28px 70px rgba(34, 34, 34, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login {
|
||||||
|
max-width: min(100%, 36rem);
|
||||||
|
margin: clamp(1.5rem, 4vw, 3rem) auto;
|
||||||
|
padding: clamp(1.5rem, 2.6vw, 2.4rem);
|
||||||
|
border: 1px solid var(--survey-auth-border);
|
||||||
|
border-radius: 1.8rem;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(0, 100, 173, 0.2), transparent 36%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(0, 174, 151, 0.16), transparent 38%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.97) 0%, rgba(248, 251, 255, 0.96) 100%);
|
||||||
|
box-shadow: var(--survey-auth-shadow);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "PT Sans Narrow", sans-serif;
|
||||||
|
color: var(--survey-auth-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login form,
|
||||||
|
.mod_login.block.login .formbody {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .formbody {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .widget {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .widget-text,
|
||||||
|
.mod_login.block.login .widget-password {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login label,
|
||||||
|
.mod_login.block.login legend {
|
||||||
|
font-family: "Blogger Sans", sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--survey-auth-blue);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login input.text,
|
||||||
|
.mod_login.block.login input.password {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 3.2rem;
|
||||||
|
padding: 0.8rem 0.95rem;
|
||||||
|
border: 1px solid rgba(162, 168, 180, 0.48);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(34, 34, 34, 0.04);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--survey-auth-ink);
|
||||||
|
transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login input.text:focus,
|
||||||
|
.mod_login.block.login input.password:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(0, 100, 173, 0.72);
|
||||||
|
box-shadow: 0 0 0 4px rgba(0, 100, 173, 0.12);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .widget-checkbox {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .checkbox_container {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .checkbox_container > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
color: var(--survey-auth-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login input.checkbox {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 1.05rem;
|
||||||
|
accent-color: var(--survey-auth-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login .widget-submit {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login button.submit {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 3.2rem;
|
||||||
|
padding: 0.8rem 1.35rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(135deg, var(--survey-auth-blue), #0d7acb);
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Blogger Sans", sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 16px 28px rgba(0, 100, 173, 0.24);
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login button.submit:hover,
|
||||||
|
.mod_login.block.login button.submit:focus {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 18px 34px rgba(0, 100, 173, 0.28);
|
||||||
|
filter: saturate(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login button.submit:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) {
|
||||||
|
max-width: min(100%, 24rem);
|
||||||
|
margin: clamp(1.25rem, 3vw, 2.5rem) auto;
|
||||||
|
padding: 0.85rem;
|
||||||
|
border: 1px solid rgba(0, 100, 173, 0.14);
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 100, 173, 0.08), rgba(255, 255, 255, 0.96));
|
||||||
|
box-shadow: 0 18px 40px rgba(34, 34, 34, 0.08);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 > li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 > li > a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 2.9rem;
|
||||||
|
padding: 0.7rem 1.15rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
color: var(--survey-auth-blue);
|
||||||
|
font-family: "Blogger Sans", sans-serif;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 12px 26px rgba(0, 100, 173, 0.12);
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease, color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 > li > a:hover,
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 > li > a:focus {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 16px 30px rgba(0, 100, 173, 0.18);
|
||||||
|
color: #004f88;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mod_login.block.login {
|
||||||
|
padding: 1.2rem;
|
||||||
|
border-radius: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod_login.block.login button.submit,
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 > li > a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.mod_customnav.block:has(a[href*="/abmelden"]) .level_1 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,8 @@ use Contao\CoreBundle\Twig\FragmentTemplate;
|
|||||||
use Contao\FrontendUser;
|
use Contao\FrontendUser;
|
||||||
use Contao\ModuleModel;
|
use Contao\ModuleModel;
|
||||||
use Contao\PageModel;
|
use Contao\PageModel;
|
||||||
use Mummert\SurveyBundle\Form\Model\SurveyConditionData;
|
|
||||||
use Mummert\SurveyBundle\Form\Model\SurveyEditorData;
|
use Mummert\SurveyBundle\Form\Model\SurveyEditorData;
|
||||||
use Mummert\SurveyBundle\Form\Model\SurveyQuestionData;
|
use Mummert\SurveyBundle\Form\Model\SurveyQuestionData;
|
||||||
use Mummert\SurveyBundle\Form\SurveyConditionEditorType;
|
|
||||||
use Mummert\SurveyBundle\Form\SurveyEditorType;
|
use Mummert\SurveyBundle\Form\SurveyEditorType;
|
||||||
use Mummert\SurveyBundle\Form\SurveyQuestionEditorType;
|
use Mummert\SurveyBundle\Form\SurveyQuestionEditorType;
|
||||||
use Mummert\SurveyBundle\Model\SurveyModel;
|
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||||
@@ -22,9 +20,10 @@ use Mummert\SurveyBundle\Repository\SurveyConditionRepository;
|
|||||||
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
|
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
|
||||||
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
|
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
|
||||||
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
||||||
use Mummert\SurveyBundle\Security\SurveyEditorVoter;
|
|
||||||
use Mummert\SurveyBundle\Service\SurveyEditorService;
|
use Mummert\SurveyBundle\Service\SurveyEditorService;
|
||||||
use Mummert\SurveyBundle\Service\SurveySubmissionService;
|
use Mummert\SurveyBundle\Service\SurveySubmissionService;
|
||||||
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -39,43 +38,57 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
private readonly SurveyEditorRepository $surveyEditorRepository,
|
private readonly SurveyEditorRepository $surveyEditorRepository,
|
||||||
private readonly SurveyEditorService $surveyEditorService,
|
private readonly SurveyEditorService $surveyEditorService,
|
||||||
private readonly SurveySubmissionService $surveySubmissionService,
|
private readonly SurveySubmissionService $surveySubmissionService,
|
||||||
|
private readonly FormFactoryInterface $formFactory,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $this->getUser();
|
$memberId = $this->resolveEditorMemberId($request);
|
||||||
|
|
||||||
if (!$user instanceof FrontendUser) {
|
if ($memberId <= 0) {
|
||||||
$template->set('loginRequired', true);
|
$template->set('loginRequired', true);
|
||||||
|
$template->set('entryNotAllowed', false);
|
||||||
$template->set('backUrl', $this->resolveListUrl($model));
|
$template->set('backUrl', $this->resolveListUrl($model));
|
||||||
|
$template->set('createQuestionMode', false);
|
||||||
|
|
||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
$survey = $this->resolveSurvey($request, $user);
|
if (!$this->isEditorEntryAllowed($request)) {
|
||||||
|
$template->set('loginRequired', false);
|
||||||
|
$template->set('entryNotAllowed', true);
|
||||||
|
$template->set('createMode', false);
|
||||||
|
$template->set('survey', null);
|
||||||
|
$template->set('backUrl', $this->resolveListUrl($model));
|
||||||
|
$template->set('createQuestionMode', false);
|
||||||
|
|
||||||
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
return $template->getResponse();
|
||||||
return $this->handleAction($survey, $request, $model);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$questionChoices = $survey instanceof SurveyModel ? $this->buildQuestionChoices((int) $survey->id) : [];
|
$survey = $this->resolveSurvey($request, $memberId);
|
||||||
$surveyForm = $this->createNamed('survey_meta', SurveyEditorType::class, $survey instanceof SurveyModel ? SurveyEditorData::fromModel($survey) : new SurveyEditorData());
|
$createQuestionMode = $this->isCreateQuestionRequested($request);
|
||||||
|
|
||||||
|
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
||||||
|
return $this->handleAction($survey, $request, $model, $memberId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$surveyForm = $this->formFactory->createNamed('survey_meta', SurveyEditorType::class, $survey instanceof SurveyModel ? SurveyEditorData::fromModel($survey) : new SurveyEditorData());
|
||||||
$surveyForm->handleRequest($request);
|
$surveyForm->handleRequest($request);
|
||||||
|
|
||||||
if ($surveyForm->isSubmitted() && $surveyForm->isValid()) {
|
if ($surveyForm->isSubmitted() && $surveyForm->isValid()) {
|
||||||
try {
|
try {
|
||||||
if ($survey instanceof SurveyModel) {
|
if ($survey instanceof SurveyModel) {
|
||||||
$this->surveyEditorService->updateSurvey($survey, (int) $user->id, $surveyForm->getData());
|
$this->surveyEditorService->updateSurvey($survey, $memberId, $surveyForm->getData());
|
||||||
$this->addFlash('success', 'Die Umfrage wurde gespeichert.');
|
$this->addFlash('success', 'Die Umfrage wurde gespeichert.');
|
||||||
|
|
||||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
$createdSurvey = $this->surveyEditorService->createSurvey((int) $user->id, $surveyForm->getData());
|
$createdSurvey = $this->surveyEditorService->createSurvey($memberId, $surveyForm->getData());
|
||||||
$this->addFlash('success', 'Die Umfrage wurde angelegt.');
|
$this->addFlash('success', 'Die Umfrage wurde angelegt.');
|
||||||
|
|
||||||
return new RedirectResponse($this->buildEditUrl($model, (int) $createdSurvey->id));
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $createdSurvey->id));
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$this->addFlash('error', $exception->getMessage());
|
$this->addFlash('error', $exception->getMessage());
|
||||||
}
|
}
|
||||||
@@ -83,76 +96,101 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
|
|
||||||
if (!$survey instanceof SurveyModel) {
|
if (!$survey instanceof SurveyModel) {
|
||||||
$template->set('loginRequired', false);
|
$template->set('loginRequired', false);
|
||||||
|
$template->set('entryNotAllowed', false);
|
||||||
$template->set('createMode', true);
|
$template->set('createMode', true);
|
||||||
$template->set('surveyForm', $surveyForm->createView());
|
$template->set('surveyForm', $surveyForm->createView());
|
||||||
$template->set('backUrl', $this->resolveListUrl($model));
|
$template->set('backUrl', $this->resolveListUrl($model));
|
||||||
$template->set('publicUrl', null);
|
$template->set('publicUrl', null);
|
||||||
$template->set('survey', null);
|
$template->set('survey', null);
|
||||||
$template->set('questionForm', null);
|
|
||||||
$template->set('conditionForm', null);
|
|
||||||
$template->set('questions', []);
|
$template->set('questions', []);
|
||||||
$template->set('conditions', []);
|
$template->set('conditions', []);
|
||||||
$template->set('editors', []);
|
$template->set('editors', []);
|
||||||
$template->set('submissions', []);
|
$template->set('submissions', []);
|
||||||
$template->set('activeQuestionId', 0);
|
$template->set('activeQuestionId', 0);
|
||||||
$template->set('activeConditionId', 0);
|
$template->set('createQuestionMode', false);
|
||||||
|
|
||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$questions = $this->surveyQuestionRepository->findAllBySurvey((int) $survey->id);
|
||||||
$activeQuestion = $this->resolveActiveQuestion($request, (int) $survey->id);
|
$activeQuestion = $this->resolveActiveQuestion($request, (int) $survey->id);
|
||||||
$activeCondition = $this->resolveActiveCondition($request, (int) $survey->id);
|
$activeQuestionId = $activeQuestion ? (int) $activeQuestion->id : 0;
|
||||||
$questionForm = $this->createNamed('survey_question', SurveyQuestionEditorType::class, $activeQuestion ? SurveyQuestionData::fromModel($activeQuestion) : new SurveyQuestionData(), [
|
$questionFormViews = [];
|
||||||
'question_choices' => $this->buildQuestionChoices((int) $survey->id),
|
|
||||||
]);
|
|
||||||
$conditionForm = $this->createNamed('survey_condition', SurveyConditionEditorType::class, $activeCondition ? SurveyConditionData::fromModel($activeCondition) : new SurveyConditionData(), [
|
|
||||||
'question_choices' => $questionChoices,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$questionForm->handleRequest($request);
|
foreach ($questions as $question) {
|
||||||
$conditionForm->handleRequest($request);
|
$questionId = (int) $question->id;
|
||||||
|
$questionForm = $this->formFactory->createNamed('survey_question_'.$questionId, SurveyQuestionEditorType::class, SurveyQuestionData::fromModel($question), [
|
||||||
|
'question_choices' => $this->buildQuestionChoices((int) $survey->id, $questionId),
|
||||||
|
]);
|
||||||
|
$questionForm->handleRequest($request);
|
||||||
|
|
||||||
if ($questionForm->isSubmitted() && $questionForm->isValid()) {
|
if ($questionForm->isSubmitted()) {
|
||||||
try {
|
$activeQuestionId = $questionId;
|
||||||
$this->surveyEditorService->saveQuestion($survey, $questionForm->getData(), $activeQuestion ? (int) $activeQuestion->id : 0);
|
$createQuestionMode = false;
|
||||||
$this->addFlash('success', $activeQuestion ? 'Die Frage wurde aktualisiert.' : 'Die Frage wurde angelegt.');
|
|
||||||
|
|
||||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
if ($questionForm->isValid()) {
|
||||||
} catch (\Throwable $exception) {
|
try {
|
||||||
$this->addFlash('error', $exception->getMessage());
|
$savedQuestionId = $this->surveyEditorService->saveQuestion($survey, $questionForm->getData(), $questionId);
|
||||||
|
$this->addFlash('success', 'Die Frage wurde aktualisiert.');
|
||||||
|
|
||||||
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$this->addFlash('error', $exception->getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->addFlash('error', 'Frage konnte nicht gespeichert werden: '.$this->describeFormErrors($questionForm));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$questionFormViews[$questionId] = $questionForm->createView();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($conditionForm->isSubmitted() && $conditionForm->isValid()) {
|
$newQuestionForm = $this->formFactory->createNamed('survey_question_new', SurveyQuestionEditorType::class, new SurveyQuestionData(), [
|
||||||
try {
|
'question_choices' => $this->buildQuestionChoices((int) $survey->id),
|
||||||
$this->surveyEditorService->saveCondition($survey, $conditionForm->getData(), $activeCondition ? (int) $activeCondition->id : 0);
|
]);
|
||||||
$this->addFlash('success', $activeCondition ? 'Die Bedingung wurde aktualisiert.' : 'Die Bedingung wurde angelegt.');
|
$newQuestionForm->handleRequest($request);
|
||||||
|
|
||||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
if ($newQuestionForm->isSubmitted()) {
|
||||||
} catch (\Throwable $exception) {
|
$activeQuestionId = 0;
|
||||||
$this->addFlash('error', $exception->getMessage());
|
$createQuestionMode = true;
|
||||||
|
|
||||||
|
if ($newQuestionForm->isValid()) {
|
||||||
|
try {
|
||||||
|
$savedQuestionId = $this->surveyEditorService->saveQuestion($survey, $newQuestionForm->getData(), 0);
|
||||||
|
$this->addFlash('success', 'Die Frage wurde angelegt.');
|
||||||
|
|
||||||
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$this->addFlash('error', $exception->getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->addFlash('error', 'Neue Frage konnte nicht gespeichert werden: '.$this->describeFormErrors($newQuestionForm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$template->set('loginRequired', false);
|
$template->set('loginRequired', false);
|
||||||
|
$template->set('entryNotAllowed', false);
|
||||||
$template->set('createMode', false);
|
$template->set('createMode', false);
|
||||||
|
$template->set('createQuestionMode', $createQuestionMode);
|
||||||
$template->set('survey', $survey);
|
$template->set('survey', $survey);
|
||||||
$template->set('surveyForm', $surveyForm->createView());
|
$template->set('surveyForm', $surveyForm->createView());
|
||||||
$template->set('questionForm', $questionForm->createView());
|
$template->set('questionForms', $questionFormViews);
|
||||||
$template->set('conditionForm', $conditionForm->createView());
|
$template->set('newQuestionForm', $newQuestionForm->createView());
|
||||||
$template->set('questions', $this->surveyQuestionRepository->findAllBySurvey((int) $survey->id));
|
$template->set('questions', $questions);
|
||||||
$template->set('conditions', $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id));
|
$conditionOverview = $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id);
|
||||||
|
|
||||||
|
$template->set('conditions', $this->buildJumpRuleOverview($questions, $conditionOverview));
|
||||||
$template->set('editors', $this->surveyEditorRepository->findEditorsBySurvey((int) $survey->id));
|
$template->set('editors', $this->surveyEditorRepository->findEditorsBySurvey((int) $survey->id));
|
||||||
$template->set('submissions', $this->surveySubmissionService->getSubmissionOverview($survey));
|
$template->set('submissions', $this->surveySubmissionService->getSubmissionOverview($survey));
|
||||||
$template->set('activeQuestionId', $activeQuestion ? (int) $activeQuestion->id : 0);
|
$template->set('activeQuestionId', $activeQuestionId);
|
||||||
$template->set('activeConditionId', $activeCondition ? (int) $activeCondition->id : 0);
|
|
||||||
$template->set('backUrl', $this->resolveListUrl($model));
|
$template->set('backUrl', $this->resolveListUrl($model));
|
||||||
$template->set('publicUrl', $this->resolveReaderUrl($model, (string) $survey->alias));
|
$template->set('publicUrl', $this->resolveReaderUrl($model, (string) $survey->alias));
|
||||||
|
$template->set('metadataFormActionUrl', $this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
|
|
||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSurvey(Request $request, FrontendUser $user): ?SurveyModel
|
private function resolveSurvey(Request $request, int $memberId): ?SurveyModel
|
||||||
{
|
{
|
||||||
$surveyId = (int) $request->query->get('survey', 0);
|
$surveyId = (int) $request->query->get('survey', 0);
|
||||||
|
|
||||||
@@ -166,12 +204,55 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
throw new \RuntimeException('Die Umfrage wurde nicht gefunden.');
|
throw new \RuntimeException('Die Umfrage wurde nicht gefunden.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->denyAccessUnlessGranted(SurveyEditorVoter::EDIT, $survey);
|
if (!$this->surveyEditorRepository->isEditor((int) $survey->id, $memberId)) {
|
||||||
|
throw new \RuntimeException('Sie duerfen diese Umfrage nicht bearbeiten.');
|
||||||
|
}
|
||||||
|
|
||||||
return $survey;
|
return $survey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleAction(SurveyModel $survey, Request $request, ModuleModel $model): Response
|
private function resolveEditorMemberId(Request $request): int
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if ($user instanceof FrontendUser) {
|
||||||
|
return (int) $user->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isDebugAccessAllowed($request) ? $this->resolveDebugMemberId($request) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveDebugMemberId(Request $request): int
|
||||||
|
{
|
||||||
|
$memberId = (int) $request->query->get('debugMember', 0);
|
||||||
|
|
||||||
|
if ($memberId > 0) {
|
||||||
|
return $memberId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$surveyId = (int) $request->query->get('survey', 0);
|
||||||
|
|
||||||
|
if ($surveyId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$memberIds = $this->surveyEditorRepository->findMemberIdsBySurvey($surveyId);
|
||||||
|
|
||||||
|
return $memberIds[0] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isDebugAccessAllowed(Request $request): bool
|
||||||
|
{
|
||||||
|
$host = strtolower((string) ($request->server->get('HTTP_HOST') ?: $request->getHost()));
|
||||||
|
|
||||||
|
return (
|
||||||
|
'localhost' === $host
|
||||||
|
|| '127.0.0.1' === $host
|
||||||
|
|| str_ends_with($host, '.ddev.site')
|
||||||
|
) && $request->query->getBoolean('debugAccess');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleAction(SurveyModel $survey, Request $request, ModuleModel $model, int $memberId): Response
|
||||||
{
|
{
|
||||||
$action = (string) $request->request->get('_survey_action');
|
$action = (string) $request->request->get('_survey_action');
|
||||||
$itemId = (int) $request->request->get('item_id', 0);
|
$itemId = (int) $request->request->get('item_id', 0);
|
||||||
@@ -182,6 +263,11 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ('toggle-published' === $action) {
|
||||||
|
$isPublished = $this->surveyEditorService->toggleSurveyPublished($survey, $memberId);
|
||||||
|
$this->addFlash('success', $isPublished ? 'Die Umfrage ist jetzt veroeffentlicht.' : 'Die Umfrage ist jetzt nicht mehr veroeffentlicht.');
|
||||||
|
}
|
||||||
|
|
||||||
if ('delete-question' === $action) {
|
if ('delete-question' === $action) {
|
||||||
$this->surveyEditorService->deleteQuestion($survey, $itemId);
|
$this->surveyEditorService->deleteQuestion($survey, $itemId);
|
||||||
$this->addFlash('success', 'Die Frage wurde geloescht.');
|
$this->addFlash('success', 'Die Frage wurde geloescht.');
|
||||||
@@ -195,7 +281,7 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
$this->addFlash('error', $exception->getMessage());
|
$this->addFlash('error', $exception->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveActiveQuestion(Request $request, int $surveyId): mixed
|
private function resolveActiveQuestion(Request $request, int $surveyId): mixed
|
||||||
@@ -205,16 +291,18 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
return $questionId > 0 ? $this->surveyQuestionRepository->findByIdForSurvey($surveyId, $questionId) : null;
|
return $questionId > 0 ? $this->surveyQuestionRepository->findByIdForSurvey($surveyId, $questionId) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveActiveCondition(Request $request, int $surveyId): mixed
|
private function isCreateQuestionRequested(Request $request): bool
|
||||||
{
|
{
|
||||||
$conditionId = (int) $request->query->get('condition', 0);
|
return 'new' === trim((string) $request->query->get('question', ''));
|
||||||
$condition = $conditionId > 0 ? $this->surveyConditionRepository->findById($conditionId) : null;
|
}
|
||||||
|
|
||||||
if (null === $condition || (int) $condition->pid !== $surveyId) {
|
private function isEditorEntryAllowed(Request $request): bool
|
||||||
return null;
|
{
|
||||||
|
if ((int) $request->query->get('survey', 0) > 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $condition;
|
return $request->query->getBoolean('create');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,6 +323,84 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
return $choices;
|
return $choices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $questions
|
||||||
|
* @param list<array<string, mixed>> $conditionOverview
|
||||||
|
*
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function buildJumpRuleOverview(array $questions, array $conditionOverview): array
|
||||||
|
{
|
||||||
|
$questionLabels = [];
|
||||||
|
$questionSortings = [];
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
foreach ($questions as $question) {
|
||||||
|
$questionId = (int) ($question->id ?? 0);
|
||||||
|
|
||||||
|
if ($questionId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$questionLabels[$questionId] = (string) ($question->question ?? '');
|
||||||
|
$questionSortings[$questionId] = (int) ($question->sorting ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($questions as $question) {
|
||||||
|
$questionId = (int) ($question->id ?? 0);
|
||||||
|
|
||||||
|
if ($questionId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('yes_no_maybe' !== (string) ($question->type ?? '')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['Ja' => (int) ($question->jumpOnYes ?? 0), 'Nein' => (int) ($question->jumpOnNo ?? 0)] as $answerValue => $targetQuestionId) {
|
||||||
|
if ($targetQuestionId <= 0 || !isset($questionLabels[$targetQuestionId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules[] = [
|
||||||
|
'id' => 'question-'.$questionId.'-'.$targetQuestionId.'-'.$answerValue,
|
||||||
|
'answerValue' => $answerValue,
|
||||||
|
'sourceQuestion' => $questionId,
|
||||||
|
'targetQuestion' => $targetQuestionId,
|
||||||
|
'sourceQuestionLabel' => (string) ($question->question ?? ''),
|
||||||
|
'targetQuestionLabel' => $questionLabels[$targetQuestionId] ?? '',
|
||||||
|
'sourceSorting' => $questionSortings[$questionId] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($conditionOverview as $condition) {
|
||||||
|
$sourceQuestionId = (int) ($condition['sourceQuestion'] ?? 0);
|
||||||
|
|
||||||
|
$rules[] = [
|
||||||
|
'id' => $condition['id'] ?? 0,
|
||||||
|
'answerValue' => (string) ($condition['answerValue'] ?? ''),
|
||||||
|
'sourceQuestion' => $sourceQuestionId,
|
||||||
|
'targetQuestion' => (int) ($condition['targetQuestion'] ?? 0),
|
||||||
|
'sourceQuestionLabel' => (string) ($condition['sourceQuestionLabel'] ?? ''),
|
||||||
|
'targetQuestionLabel' => (string) ($condition['targetQuestionLabel'] ?? ''),
|
||||||
|
'sourceSorting' => $questionSortings[$sourceQuestionId] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($rules, static function (array $left, array $right): int {
|
||||||
|
$sortingComparison = ((int) ($left['sourceSorting'] ?? 0)) <=> ((int) ($right['sourceSorting'] ?? 0));
|
||||||
|
|
||||||
|
if (0 !== $sortingComparison) {
|
||||||
|
return $sortingComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((int) ($left['targetQuestion'] ?? 0)) <=> ((int) ($right['targetQuestion'] ?? 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveListUrl(ModuleModel $model): string
|
private function resolveListUrl(ModuleModel $model): string
|
||||||
{
|
{
|
||||||
$page = $this->resolvePage((int) ($model->surveyListPage ?? 0)) ?? $this->getPageModel();
|
$page = $this->resolvePage((int) ($model->surveyListPage ?? 0)) ?? $this->getPageModel();
|
||||||
@@ -253,11 +419,44 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
return $this->generateContentUrl($page, ['auto_item' => $publicAlias]);
|
return $this->generateContentUrl($page, ['auto_item' => $publicAlias]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildEditUrl(ModuleModel $model, int $surveyId): string
|
private function buildEditUrl(ModuleModel $model, Request $request, int $surveyId): string
|
||||||
{
|
{
|
||||||
$page = $this->getPageModel();
|
$parameters = ['survey' => $surveyId];
|
||||||
|
|
||||||
return $page instanceof PageModel ? $this->generateContentUrl($page, ['survey' => $surveyId]) : $this->resolveListUrl($model);
|
if ($request->query->getBoolean('debugAccess') && $this->isDebugAccessAllowed($request)) {
|
||||||
|
$parameters['debugAccess'] = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$debugMemberId = (int) $request->query->get('debugMember', 0);
|
||||||
|
|
||||||
|
if ($debugMemberId > 0) {
|
||||||
|
$parameters['debugMember'] = (string) $debugMemberId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$basePath = $request->getBaseUrl().$request->getPathInfo();
|
||||||
|
|
||||||
|
if ('' !== $basePath) {
|
||||||
|
$queryString = http_build_query($parameters);
|
||||||
|
$url = $basePath.('' !== $queryString ? '?'.$queryString : '');
|
||||||
|
} else {
|
||||||
|
$page = $this->getPageModel();
|
||||||
|
$url = $page instanceof PageModel ? $this->generateContentUrl($page, $parameters) : $this->resolveListUrl($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function describeFormErrors(FormInterface $form): string
|
||||||
|
{
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
foreach ($form->getErrors(true, true) as $error) {
|
||||||
|
$origin = $error->getOrigin();
|
||||||
|
$name = $origin instanceof FormInterface ? $origin->getName() : 'form';
|
||||||
|
$messages[] = sprintf('%s: %s', $name, $error->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [] !== $messages ? implode(' | ', array_unique($messages)) : 'Unbekannter Formularfehler.';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolvePage(int $pageId): ?PageModel
|
private function resolvePage(int $pageId): ?PageModel
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ use Contao\CoreBundle\Twig\FragmentTemplate;
|
|||||||
use Contao\FrontendUser;
|
use Contao\FrontendUser;
|
||||||
use Contao\ModuleModel;
|
use Contao\ModuleModel;
|
||||||
use Contao\PageModel;
|
use Contao\PageModel;
|
||||||
|
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||||
|
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
|
||||||
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
||||||
use Mummert\SurveyBundle\Service\SurveySubmissionService;
|
use Mummert\SurveyBundle\Service\SurveyEditorService;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
@@ -21,7 +24,8 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SurveyRepository $surveyRepository,
|
private readonly SurveyRepository $surveyRepository,
|
||||||
private readonly SurveySubmissionService $surveySubmissionService,
|
private readonly SurveyEditorRepository $surveyEditorRepository,
|
||||||
|
private readonly SurveyEditorService $surveyEditorService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +41,10 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod('POST') && $request->request->has('_survey_action')) {
|
||||||
|
return $this->handleAction($request, (int) $user->id);
|
||||||
|
}
|
||||||
|
|
||||||
$editPage = $this->resolvePage((int) ($model->surveyEditPage ?? 0));
|
$editPage = $this->resolvePage((int) ($model->surveyEditPage ?? 0));
|
||||||
$readerPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyReaderPage'));
|
$readerPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyReaderPage'));
|
||||||
$resultsPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyResultsPage'));
|
$resultsPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyResultsPage'));
|
||||||
@@ -44,7 +52,6 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
|
|
||||||
foreach ($this->surveyRepository->findEditableByMember((int) $user->id) as $survey) {
|
foreach ($this->surveyRepository->findEditableByMember((int) $user->id) as $survey) {
|
||||||
$surveyId = (int) $survey['id'];
|
$surveyId = (int) $survey['id'];
|
||||||
$isCompletedInCurrentSession = $this->surveySubmissionService->hasFinishedSubmissionForSurvey($surveyId, $request->getSession());
|
|
||||||
|
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'id' => $surveyId,
|
'id' => $surveyId,
|
||||||
@@ -53,20 +60,60 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
'isLocked' => !empty($survey['isLocked']),
|
'isLocked' => !empty($survey['isLocked']),
|
||||||
'published' => !empty($survey['published']),
|
'published' => !empty($survey['published']),
|
||||||
'questionCount' => (int) ($survey['questionCount'] ?? 0),
|
'questionCount' => (int) ($survey['questionCount'] ?? 0),
|
||||||
'answerCount' => (int) ($survey['answerCount'] ?? 0),
|
'participationCount' => (int) ($survey['participationCount'] ?? 0),
|
||||||
'editUrl' => $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['survey' => $surveyId]) : null,
|
'editUrl' => $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['survey' => $surveyId]) : null,
|
||||||
'publicUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['auto_item' => (string) $survey['alias']]) : null,
|
'publicUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['auto_item' => (string) $survey['alias']]) : null,
|
||||||
'resultsUrl' => !$isCompletedInCurrentSession && $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['auto_item' => (string) $survey['alias']]) : null,
|
'resultsUrl' => $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['auto_item' => (string) $survey['alias']]) : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$template->set('loginRequired', false);
|
$template->set('loginRequired', false);
|
||||||
$template->set('surveys', $items);
|
$template->set('surveys', $items);
|
||||||
$template->set('createUrl', $editPage instanceof PageModel ? $this->generateContentUrl($editPage) : null);
|
$template->set('createUrl', $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['create' => '1']) : null);
|
||||||
|
|
||||||
return $template->getResponse();
|
return $template->getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleAction(Request $request, int $memberId): Response
|
||||||
|
{
|
||||||
|
$surveyId = (int) $request->request->get('item_id', 0);
|
||||||
|
$action = (string) $request->request->get('_survey_action');
|
||||||
|
$token = (string) $request->request->get('_token');
|
||||||
|
|
||||||
|
if ($surveyId <= 0 || !$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
|
||||||
|
throw new \RuntimeException('Ungueltiger CSRF-Token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$survey = $this->resolveEditableSurvey($surveyId, $memberId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ('toggle-published' === $action) {
|
||||||
|
$isPublished = $this->surveyEditorService->toggleSurveyPublished($survey, $memberId);
|
||||||
|
$this->addFlash('success', $isPublished ? 'Die Umfrage ist jetzt veroeffentlicht.' : 'Die Umfrage ist jetzt nicht mehr veroeffentlicht.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('delete-survey' === $action) {
|
||||||
|
$this->surveyEditorService->deleteSurvey($survey);
|
||||||
|
$this->addFlash('success', 'Die Umfrage mit allen Fragen, Antworten und Ergebnissen wurde geloescht.');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$this->addFlash('error', $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveEditableSurvey(int $surveyId, int $memberId): SurveyModel
|
||||||
|
{
|
||||||
|
$survey = $this->surveyRepository->findById($surveyId);
|
||||||
|
|
||||||
|
if (!$survey instanceof SurveyModel || !$this->surveyEditorRepository->isEditor($surveyId, $memberId)) {
|
||||||
|
throw new \RuntimeException('Sie duerfen diese Umfrage nicht bearbeiten.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $survey;
|
||||||
|
}
|
||||||
|
|
||||||
private function resolvePage(int $pageId): ?PageModel
|
private function resolvePage(int $pageId): ?PageModel
|
||||||
{
|
{
|
||||||
if ($pageId <= 0) {
|
if ($pageId <= 0) {
|
||||||
|
|||||||
@@ -10,23 +10,23 @@ final class SurveyQuestionData
|
|||||||
{
|
{
|
||||||
public string $type = 'yes_no_maybe';
|
public string $type = 'yes_no_maybe';
|
||||||
public string $question = '';
|
public string $question = '';
|
||||||
public string $description = '';
|
public ?string $description = '';
|
||||||
public bool $mandatory = true;
|
public bool $mandatory = true;
|
||||||
public bool $published = true;
|
public bool $published = true;
|
||||||
public bool $allowMaybe = false;
|
public bool $allowMaybe = false;
|
||||||
public bool $allowMultiple = false;
|
public bool $allowMultiple = false;
|
||||||
public int $jumpOnYes = 0;
|
public ?int $jumpOnYes = 0;
|
||||||
public int $jumpOnNo = 0;
|
public ?int $jumpOnNo = 0;
|
||||||
public string $answerOption1 = '';
|
public ?string $answerOption1 = '';
|
||||||
public string $answerOption2 = '';
|
public ?string $answerOption2 = '';
|
||||||
public string $answerOption3 = '';
|
public ?string $answerOption3 = '';
|
||||||
public string $answerOption4 = '';
|
public ?string $answerOption4 = '';
|
||||||
public string $answerOption5 = '';
|
public ?string $answerOption5 = '';
|
||||||
public string $answerOption6 = '';
|
public ?string $answerOption6 = '';
|
||||||
public string $answerOption7 = '';
|
public ?string $answerOption7 = '';
|
||||||
public string $answerOption8 = '';
|
public ?string $answerOption8 = '';
|
||||||
public string $answerOption9 = '';
|
public ?string $answerOption9 = '';
|
||||||
public string $answerOption10 = '';
|
public ?string $answerOption10 = '';
|
||||||
public int $rangeMin = 0;
|
public int $rangeMin = 0;
|
||||||
public int $rangeMax = 10;
|
public int $rangeMax = 10;
|
||||||
public int $rangeStep = 1;
|
public int $rangeStep = 1;
|
||||||
@@ -64,13 +64,13 @@ final class SurveyQuestionData
|
|||||||
$payload = [
|
$payload = [
|
||||||
'type' => $this->type,
|
'type' => $this->type,
|
||||||
'question' => $this->question,
|
'question' => $this->question,
|
||||||
'description' => $this->description,
|
'description' => (string) ($this->description ?? ''),
|
||||||
'mandatory' => $this->mandatory,
|
'mandatory' => $this->mandatory,
|
||||||
'published' => $this->published,
|
'published' => $this->published,
|
||||||
'allowMaybe' => $this->allowMaybe,
|
'allowMaybe' => $this->allowMaybe,
|
||||||
'allowMultiple' => $this->allowMultiple,
|
'allowMultiple' => $this->allowMultiple,
|
||||||
'jumpOnYes' => $this->jumpOnYes,
|
'jumpOnYes' => max(0, (int) ($this->jumpOnYes ?? 0)),
|
||||||
'jumpOnNo' => $this->jumpOnNo,
|
'jumpOnNo' => max(0, (int) ($this->jumpOnNo ?? 0)),
|
||||||
'rangeMin' => $this->rangeMin,
|
'rangeMin' => $this->rangeMin,
|
||||||
'rangeMax' => $this->rangeMax,
|
'rangeMax' => $this->rangeMax,
|
||||||
'rangeStep' => $this->rangeStep,
|
'rangeStep' => $this->rangeStep,
|
||||||
@@ -78,7 +78,7 @@ final class SurveyQuestionData
|
|||||||
|
|
||||||
for ($index = 1; $index <= 10; ++$index) {
|
for ($index = 1; $index <= 10; ++$index) {
|
||||||
$property = 'answerOption'.$index;
|
$property = 'answerOption'.$index;
|
||||||
$payload[$property] = $this->{$property};
|
$payload[$property] = (string) ($this->{$property} ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $payload;
|
return $payload;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class SurveyEditorType extends AbstractType
|
|||||||
->add('description', TextareaType::class, [
|
->add('description', TextareaType::class, [
|
||||||
'label' => 'Beschreibung',
|
'label' => 'Beschreibung',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['rows' => 5],
|
'attr' => ['rows' => 5],
|
||||||
])
|
])
|
||||||
->add('published', CheckboxType::class, [
|
->add('published', CheckboxType::class, [
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
|||||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||||
use Symfony\Component\Validator\Constraints\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
@@ -26,6 +29,9 @@ final class SurveyQuestionEditorType extends AbstractType
|
|||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
|
$builder->addEventListener(FormEvents::PRE_SUBMIT, $this->normalizeOptionalFields(...));
|
||||||
|
$builder->addEventListener(FormEvents::POST_SUBMIT, $this->validateChoiceOptions(...));
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
->add('type', ChoiceType::class, [
|
->add('type', ChoiceType::class, [
|
||||||
'label' => 'Fragetyp',
|
'label' => 'Fragetyp',
|
||||||
@@ -43,6 +49,7 @@ final class SurveyQuestionEditorType extends AbstractType
|
|||||||
->add('description', TextareaType::class, [
|
->add('description', TextareaType::class, [
|
||||||
'label' => 'Beschreibung',
|
'label' => 'Beschreibung',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['rows' => 4],
|
'attr' => ['rows' => 4],
|
||||||
])
|
])
|
||||||
->add('mandatory', CheckboxType::class, [
|
->add('mandatory', CheckboxType::class, [
|
||||||
@@ -61,15 +68,17 @@ final class SurveyQuestionEditorType extends AbstractType
|
|||||||
->add('jumpOnYes', ChoiceType::class, [
|
->add('jumpOnYes', ChoiceType::class, [
|
||||||
'label' => 'Zur Frage springen bei Auswahl ja',
|
'label' => 'Zur Frage springen bei Auswahl ja',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'choices' => array_flip($options['question_choices']),
|
'choices' => array_flip($options['question_choices']),
|
||||||
'placeholder' => 'keine',
|
'placeholder' => 'normal weitermachen',
|
||||||
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
|
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
|
||||||
])
|
])
|
||||||
->add('jumpOnNo', ChoiceType::class, [
|
->add('jumpOnNo', ChoiceType::class, [
|
||||||
'label' => 'Zur Frage springen bei Auswahl nein',
|
'label' => 'Zur Frage springen bei Auswahl nein',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'choices' => array_flip($options['question_choices']),
|
'choices' => array_flip($options['question_choices']),
|
||||||
'placeholder' => 'keine',
|
'placeholder' => 'normal weitermachen',
|
||||||
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
|
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
|
||||||
])
|
])
|
||||||
->add('allowMultiple', CheckboxType::class, [
|
->add('allowMultiple', CheckboxType::class, [
|
||||||
@@ -80,71 +89,130 @@ final class SurveyQuestionEditorType extends AbstractType
|
|||||||
->add('answerOption1', TextType::class, [
|
->add('answerOption1', TextType::class, [
|
||||||
'label' => 'Antwort 1',
|
'label' => 'Antwort 1',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption2', TextType::class, [
|
->add('answerOption2', TextType::class, [
|
||||||
'label' => 'Antwort 2',
|
'label' => 'Antwort 2',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption3', TextType::class, [
|
->add('answerOption3', TextType::class, [
|
||||||
'label' => 'Antwort 3',
|
'label' => 'Antwort 3',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption4', TextType::class, [
|
->add('answerOption4', TextType::class, [
|
||||||
'label' => 'Antwort 4',
|
'label' => 'Antwort 4',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption5', TextType::class, [
|
->add('answerOption5', TextType::class, [
|
||||||
'label' => 'Antwort 5',
|
'label' => 'Antwort 5',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption6', TextType::class, [
|
->add('answerOption6', TextType::class, [
|
||||||
'label' => 'Antwort 6',
|
'label' => 'Antwort 6',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption7', TextType::class, [
|
->add('answerOption7', TextType::class, [
|
||||||
'label' => 'Antwort 7',
|
'label' => 'Antwort 7',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption8', TextType::class, [
|
->add('answerOption8', TextType::class, [
|
||||||
'label' => 'Antwort 8',
|
'label' => 'Antwort 8',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption9', TextType::class, [
|
->add('answerOption9', TextType::class, [
|
||||||
'label' => 'Antwort 9',
|
'label' => 'Antwort 9',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('answerOption10', TextType::class, [
|
->add('answerOption10', TextType::class, [
|
||||||
'label' => 'Antwort 10',
|
'label' => 'Antwort 10',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'empty_data' => '',
|
||||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||||
])
|
])
|
||||||
->add('rangeMin', IntegerType::class, [
|
->add('rangeMin', IntegerType::class, [
|
||||||
'label' => 'Kleinster Wert',
|
'label' => 'Kleinster Wert',
|
||||||
|
'empty_data' => '0',
|
||||||
'constraints' => [new GreaterThanOrEqual(['value' => -100000])],
|
'constraints' => [new GreaterThanOrEqual(['value' => -100000])],
|
||||||
'attr' => ['data-question-editor-target' => 'rangeField'],
|
'attr' => ['data-question-editor-target' => 'rangeField'],
|
||||||
])
|
])
|
||||||
->add('rangeMax', IntegerType::class, [
|
->add('rangeMax', IntegerType::class, [
|
||||||
'label' => 'Groesster Wert',
|
'label' => 'Groesster Wert',
|
||||||
|
'empty_data' => '10',
|
||||||
'constraints' => [new GreaterThanOrEqual(['value' => -100000])],
|
'constraints' => [new GreaterThanOrEqual(['value' => -100000])],
|
||||||
'attr' => ['data-question-editor-target' => 'rangeField'],
|
'attr' => ['data-question-editor-target' => 'rangeField'],
|
||||||
])
|
])
|
||||||
->add('rangeStep', IntegerType::class, [
|
->add('rangeStep', IntegerType::class, [
|
||||||
'label' => 'Schrittweite',
|
'label' => 'Schrittweite',
|
||||||
|
'empty_data' => '1',
|
||||||
'constraints' => [new GreaterThanOrEqual(['value' => 1])],
|
'constraints' => [new GreaterThanOrEqual(['value' => 1])],
|
||||||
'attr' => ['data-question-editor-target' => 'rangeField'],
|
'attr' => ['data-question-editor-target' => 'rangeField'],
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeOptionalFields(FormEvent $event): void
|
||||||
|
{
|
||||||
|
$data = $event->getData();
|
||||||
|
|
||||||
|
if (!\is_array($data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stringFields = ['description'];
|
||||||
|
|
||||||
|
for ($index = 1; $index <= 10; ++$index) {
|
||||||
|
$stringFields[] = 'answerOption'.$index;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($stringFields as $field) {
|
||||||
|
if (null === ($data[$field] ?? null)) {
|
||||||
|
$data[$field] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['jumpOnYes' => '', 'jumpOnNo' => '', 'rangeMin' => '0', 'rangeMax' => '10', 'rangeStep' => '1'] as $field => $defaultValue) {
|
||||||
|
if (null === ($data[$field] ?? null) || '' === $data[$field]) {
|
||||||
|
$data[$field] = $defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->setData($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateChoiceOptions(FormEvent $event): void
|
||||||
|
{
|
||||||
|
$data = $event->getData();
|
||||||
|
|
||||||
|
if (!$data instanceof SurveyQuestionData || 'choice' !== $data->type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($index = 1; $index <= 10; ++$index) {
|
||||||
|
if ('' !== trim((string) ($data->{'answerOption'.$index} ?? ''))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->getForm()->get('answerOption1')->addError(new FormError('Mindestens eine Antwortoption ist erforderlich.'));
|
||||||
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
|
|||||||
@@ -59,32 +59,34 @@ final class SurveyQuestionRepository
|
|||||||
public function create(int $surveyId, array $data): SurveyContentModel
|
public function create(int $surveyId, array $data): SurveyContentModel
|
||||||
{
|
{
|
||||||
$sorting = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(sorting), 0) FROM tl_survey_content WHERE pid = ?', [$surveyId]) + 128;
|
$sorting = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(sorting), 0) FROM tl_survey_content WHERE pid = ?', [$surveyId]) + 128;
|
||||||
|
$type = (string) ($data['type'] ?? 'yes_no_maybe');
|
||||||
|
$isRangeType = 'range' === $type;
|
||||||
|
|
||||||
$this->connection->insert('tl_survey_content', [
|
$this->connection->insert('tl_survey_content', [
|
||||||
'pid' => $surveyId,
|
'pid' => $surveyId,
|
||||||
'sorting' => $sorting,
|
'sorting' => $sorting,
|
||||||
'type' => (string) ($data['type'] ?? 'yes_no_maybe'),
|
'type' => $type,
|
||||||
'question' => (string) ($data['question'] ?? ''),
|
'question' => (string) ($data['question'] ?? ''),
|
||||||
'description' => (string) ($data['description'] ?? ''),
|
'description' => (string) ($data['description'] ?? ''),
|
||||||
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
||||||
'published' => !empty($data['published']) ? '1' : '',
|
'published' => !empty($data['published']) ? '1' : '',
|
||||||
'allowMaybe' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') && !empty($data['allowMaybe']) ? '1' : '',
|
'allowMaybe' => 'yes_no_maybe' === $type && !empty($data['allowMaybe']) ? '1' : '',
|
||||||
'jumpOnYes' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') ? max(0, (int) ($data['jumpOnYes'] ?? 0)) : 0,
|
'jumpOnYes' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnYes'] ?? 0)) : 0,
|
||||||
'jumpOnNo' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') ? max(0, (int) ($data['jumpOnNo'] ?? 0)) : 0,
|
'jumpOnNo' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnNo'] ?? 0)) : 0,
|
||||||
'allowMultiple' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') && !empty($data['allowMultiple']) ? '1' : '',
|
'allowMultiple' => 'choice' === $type && !empty($data['allowMultiple']) ? '1' : '',
|
||||||
'answerOption1' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption1'] ?? '')) : '',
|
'answerOption1' => 'choice' === $type ? trim((string) ($data['answerOption1'] ?? '')) : '',
|
||||||
'answerOption2' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption2'] ?? '')) : '',
|
'answerOption2' => 'choice' === $type ? trim((string) ($data['answerOption2'] ?? '')) : '',
|
||||||
'answerOption3' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption3'] ?? '')) : '',
|
'answerOption3' => 'choice' === $type ? trim((string) ($data['answerOption3'] ?? '')) : '',
|
||||||
'answerOption4' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption4'] ?? '')) : '',
|
'answerOption4' => 'choice' === $type ? trim((string) ($data['answerOption4'] ?? '')) : '',
|
||||||
'answerOption5' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption5'] ?? '')) : '',
|
'answerOption5' => 'choice' === $type ? trim((string) ($data['answerOption5'] ?? '')) : '',
|
||||||
'answerOption6' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption6'] ?? '')) : '',
|
'answerOption6' => 'choice' === $type ? trim((string) ($data['answerOption6'] ?? '')) : '',
|
||||||
'answerOption7' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption7'] ?? '')) : '',
|
'answerOption7' => 'choice' === $type ? trim((string) ($data['answerOption7'] ?? '')) : '',
|
||||||
'answerOption8' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption8'] ?? '')) : '',
|
'answerOption8' => 'choice' === $type ? trim((string) ($data['answerOption8'] ?? '')) : '',
|
||||||
'answerOption9' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption9'] ?? '')) : '',
|
'answerOption9' => 'choice' === $type ? trim((string) ($data['answerOption9'] ?? '')) : '',
|
||||||
'answerOption10' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption10'] ?? '')) : '',
|
'answerOption10' => 'choice' === $type ? trim((string) ($data['answerOption10'] ?? '')) : '',
|
||||||
'rangeMin' => (int) ($data['rangeMin'] ?? 0),
|
'rangeMin' => $isRangeType ? (int) ($data['rangeMin'] ?? 0) : 0,
|
||||||
'rangeMax' => (int) ($data['rangeMax'] ?? 10),
|
'rangeMax' => $isRangeType ? (int) ($data['rangeMax'] ?? 10) : 10,
|
||||||
'rangeStep' => max(1, (int) ($data['rangeStep'] ?? 1)),
|
'rangeStep' => $isRangeType ? max(1, (int) ($data['rangeStep'] ?? 1)) : 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$id = (int) $this->connection->lastInsertId();
|
$id = (int) $this->connection->lastInsertId();
|
||||||
@@ -94,29 +96,32 @@ final class SurveyQuestionRepository
|
|||||||
|
|
||||||
public function update(SurveyContentModel $question, array $data): void
|
public function update(SurveyContentModel $question, array $data): void
|
||||||
{
|
{
|
||||||
|
$type = (string) ($data['type'] ?? $question->type);
|
||||||
|
$isRangeType = 'range' === $type;
|
||||||
|
|
||||||
$this->connection->update('tl_survey_content', [
|
$this->connection->update('tl_survey_content', [
|
||||||
'type' => (string) ($data['type'] ?? $question->type),
|
'type' => $type,
|
||||||
'question' => (string) ($data['question'] ?? $question->question),
|
'question' => (string) ($data['question'] ?? $question->question),
|
||||||
'description' => (string) ($data['description'] ?? $question->description),
|
'description' => (string) ($data['description'] ?? $question->description),
|
||||||
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
||||||
'published' => !empty($data['published']) ? '1' : '',
|
'published' => !empty($data['published']) ? '1' : '',
|
||||||
'allowMaybe' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) && !empty($data['allowMaybe']) ? '1' : '',
|
'allowMaybe' => 'yes_no_maybe' === $type && !empty($data['allowMaybe']) ? '1' : '',
|
||||||
'jumpOnYes' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) ? max(0, (int) ($data['jumpOnYes'] ?? $question->jumpOnYes)) : 0,
|
'jumpOnYes' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnYes'] ?? $question->jumpOnYes)) : 0,
|
||||||
'jumpOnNo' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) ? max(0, (int) ($data['jumpOnNo'] ?? $question->jumpOnNo)) : 0,
|
'jumpOnNo' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnNo'] ?? $question->jumpOnNo)) : 0,
|
||||||
'allowMultiple' => 'choice' === (string) ($data['type'] ?? $question->type) && !empty($data['allowMultiple']) ? '1' : '',
|
'allowMultiple' => 'choice' === $type && !empty($data['allowMultiple']) ? '1' : '',
|
||||||
'answerOption1' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption1'] ?? $question->answerOption1)) : '',
|
'answerOption1' => 'choice' === $type ? trim((string) ($data['answerOption1'] ?? $question->answerOption1)) : '',
|
||||||
'answerOption2' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption2'] ?? $question->answerOption2)) : '',
|
'answerOption2' => 'choice' === $type ? trim((string) ($data['answerOption2'] ?? $question->answerOption2)) : '',
|
||||||
'answerOption3' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption3'] ?? $question->answerOption3)) : '',
|
'answerOption3' => 'choice' === $type ? trim((string) ($data['answerOption3'] ?? $question->answerOption3)) : '',
|
||||||
'answerOption4' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption4'] ?? $question->answerOption4)) : '',
|
'answerOption4' => 'choice' === $type ? trim((string) ($data['answerOption4'] ?? $question->answerOption4)) : '',
|
||||||
'answerOption5' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption5'] ?? $question->answerOption5)) : '',
|
'answerOption5' => 'choice' === $type ? trim((string) ($data['answerOption5'] ?? $question->answerOption5)) : '',
|
||||||
'answerOption6' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption6'] ?? $question->answerOption6)) : '',
|
'answerOption6' => 'choice' === $type ? trim((string) ($data['answerOption6'] ?? $question->answerOption6)) : '',
|
||||||
'answerOption7' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption7'] ?? $question->answerOption7)) : '',
|
'answerOption7' => 'choice' === $type ? trim((string) ($data['answerOption7'] ?? $question->answerOption7)) : '',
|
||||||
'answerOption8' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption8'] ?? $question->answerOption8)) : '',
|
'answerOption8' => 'choice' === $type ? trim((string) ($data['answerOption8'] ?? $question->answerOption8)) : '',
|
||||||
'answerOption9' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption9'] ?? $question->answerOption9)) : '',
|
'answerOption9' => 'choice' === $type ? trim((string) ($data['answerOption9'] ?? $question->answerOption9)) : '',
|
||||||
'answerOption10' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption10'] ?? $question->answerOption10)) : '',
|
'answerOption10' => 'choice' === $type ? trim((string) ($data['answerOption10'] ?? $question->answerOption10)) : '',
|
||||||
'rangeMin' => (int) ($data['rangeMin'] ?? $question->rangeMin),
|
'rangeMin' => $isRangeType ? (int) ($data['rangeMin'] ?? $question->rangeMin) : 0,
|
||||||
'rangeMax' => (int) ($data['rangeMax'] ?? $question->rangeMax),
|
'rangeMax' => $isRangeType ? (int) ($data['rangeMax'] ?? $question->rangeMax) : 10,
|
||||||
'rangeStep' => max(1, (int) ($data['rangeStep'] ?? $question->rangeStep)),
|
'rangeStep' => $isRangeType ? max(1, (int) ($data['rangeStep'] ?? $question->rangeStep)) : 1,
|
||||||
], [
|
], [
|
||||||
'id' => (int) $question->id,
|
'id' => (int) $question->id,
|
||||||
]);
|
]);
|
||||||
@@ -129,6 +134,20 @@ final class SurveyQuestionRepository
|
|||||||
'pid' => $surveyId,
|
'pid' => $surveyId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->connection->update('tl_survey_content', [
|
||||||
|
'jumpOnYes' => 0,
|
||||||
|
], [
|
||||||
|
'pid' => $surveyId,
|
||||||
|
'jumpOnYes' => $questionId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->connection->update('tl_survey_content', [
|
||||||
|
'jumpOnNo' => 0,
|
||||||
|
], [
|
||||||
|
'pid' => $surveyId,
|
||||||
|
'jumpOnNo' => $questionId,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->connection->delete('tl_survey_condition', [
|
$this->connection->delete('tl_survey_condition', [
|
||||||
'sourceQuestion' => $questionId,
|
'sourceQuestion' => $questionId,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Mummert\SurveyBundle\Repository;
|
|||||||
|
|
||||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||||
use Contao\StringUtil;
|
use Contao\StringUtil;
|
||||||
|
use Doctrine\DBAL\ArrayParameterType;
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
use Mummert\SurveyBundle\Model\SurveyModel;
|
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||||
|
|
||||||
@@ -67,12 +68,11 @@ final class SurveyRepository
|
|||||||
s.isLocked,
|
s.isLocked,
|
||||||
s.updatedAt,
|
s.updatedAt,
|
||||||
COUNT(DISTINCT q.id) AS questionCount,
|
COUNT(DISTINCT q.id) AS questionCount,
|
||||||
COUNT(DISTINCT a.id) AS answerCount
|
COUNT(DISTINCT CASE WHEN sub.isFinished = 1 THEN sub.id END) AS participationCount
|
||||||
FROM tl_survey s
|
FROM tl_survey s
|
||||||
INNER JOIN tl_survey_editor e ON COALESCE(NULLIF(e.survey, 0), e.pid) = s.id AND e.member = :member
|
INNER JOIN tl_survey_editor e ON COALESCE(NULLIF(e.survey, 0), e.pid) = s.id AND e.member = :member
|
||||||
LEFT JOIN tl_survey_content q ON q.pid = s.id
|
LEFT JOIN tl_survey_content q ON q.pid = s.id
|
||||||
LEFT JOIN tl_survey_submission sub ON sub.survey = s.id
|
LEFT JOIN tl_survey_submission sub ON sub.survey = s.id
|
||||||
LEFT JOIN tl_survey_answer a ON a.submission = sub.id
|
|
||||||
GROUP BY s.id
|
GROUP BY s.id
|
||||||
ORDER BY s.updatedAt DESC, s.title ASC
|
ORDER BY s.updatedAt DESC, s.title ASC
|
||||||
SQL,
|
SQL,
|
||||||
@@ -91,7 +91,7 @@ final class SurveyRepository
|
|||||||
'alias' => $this->ensurePublicAlias(null),
|
'alias' => $this->ensurePublicAlias(null),
|
||||||
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
|
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
|
||||||
'description' => (string) ($data['description'] ?? ''),
|
'description' => (string) ($data['description'] ?? ''),
|
||||||
'published' => !empty($data['published']) ? '1' : '',
|
'published' => '',
|
||||||
'isLocked' => '',
|
'isLocked' => '',
|
||||||
'createdBy' => $memberId,
|
'createdBy' => $memberId,
|
||||||
'updatedBy' => $memberId,
|
'updatedBy' => $memberId,
|
||||||
@@ -212,6 +212,31 @@ final class SurveyRepository
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteCascade(int $surveyId): void
|
||||||
|
{
|
||||||
|
$this->connection->transactional(function () use ($surveyId): void {
|
||||||
|
$submissionIds = array_values(array_map('intval', $this->connection->fetchFirstColumn(
|
||||||
|
'SELECT id FROM tl_survey_submission WHERE survey = ?',
|
||||||
|
[$surveyId],
|
||||||
|
)));
|
||||||
|
|
||||||
|
if ([] !== $submissionIds) {
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'DELETE FROM tl_survey_answer WHERE submission IN (?)',
|
||||||
|
[$submissionIds],
|
||||||
|
[ArrayParameterType::INTEGER],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->delete('tl_survey_submission', ['survey' => $surveyId]);
|
||||||
|
$this->connection->delete('tl_survey_condition', ['pid' => $surveyId]);
|
||||||
|
$this->connection->delete('tl_survey_content', ['pid' => $surveyId]);
|
||||||
|
$this->connection->delete('tl_survey_editor', ['survey' => $surveyId]);
|
||||||
|
$this->connection->delete('tl_survey_editor', ['pid' => $surveyId]);
|
||||||
|
$this->connection->delete('tl_survey', ['id' => $surveyId]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function generateRandomPublicAlias(): string
|
private function generateRandomPublicAlias(): string
|
||||||
{
|
{
|
||||||
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
|||||||
@@ -208,7 +208,7 @@
|
|||||||
<p>{{ (survey.description|striptags|trim) ?: 'survey.pdf.no_description'|trans }}</p>
|
<p>{{ (survey.description|striptags|trim) ?: 'survey.pdf.no_description'|trans }}</p>
|
||||||
|
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="meta-pill">{{ 'survey.pdf.completed_submissions'|trans({'%count%': completedSubmissionCount}) }}</span>
|
<span class="meta-pill">{{ 'survey.pdf.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||||
<span class="meta-pill">{{ 'survey.pdf.questions'|trans({'%count%': questionResults|length}) }}</span>
|
<span class="meta-pill">{{ 'survey.pdf.questions'|trans({'%count%': questionResults|length}) }}</span>
|
||||||
<span class="meta-pill">{{ 'survey.pdf.export_prefix'|trans({'%date%': exportedAt}) }}</span>
|
<span class="meta-pill">{{ 'survey.pdf.export_prefix'|trans({'%date%': exportedAt}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="meta-pill">{{ 'survey.pdf.answers'|trans({'%count%': question.totalAnswers}) }}</span>
|
<span class="meta-pill">{{ 'survey.pdf.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||||
<span class="meta-pill">{{ 'survey.pdf.response_rate'|trans({'%rate%': question.responseRate}) }}</span>
|
<span class="meta-pill">{{ 'survey.pdf.response_rate'|trans({'%rate%': question.responseRate}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,17 @@ final class SurveyEditorService
|
|||||||
$this->surveyRepository->update($survey, $memberId, $data->toArray());
|
$this->surveyRepository->update($survey, $memberId, $data->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveQuestion(SurveyModel $survey, SurveyQuestionData $data, int $questionId = 0): void
|
public function toggleSurveyPublished(SurveyModel $survey, int $memberId): bool
|
||||||
|
{
|
||||||
|
$data = SurveyEditorData::fromModel($survey);
|
||||||
|
$data->published = !$data->published;
|
||||||
|
|
||||||
|
$this->surveyRepository->update($survey, $memberId, $data->toArray());
|
||||||
|
|
||||||
|
return $data->published;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveQuestion(SurveyModel $survey, SurveyQuestionData $data, int $questionId = 0): int
|
||||||
{
|
{
|
||||||
$this->assertUnlocked($survey);
|
$this->assertUnlocked($survey);
|
||||||
|
|
||||||
@@ -55,11 +65,13 @@ final class SurveyEditorService
|
|||||||
$this->surveyQuestionRepository->update($question, $data->toArray());
|
$this->surveyQuestionRepository->update($question, $data->toArray());
|
||||||
$this->surveyRepository->touch((int) $survey->id);
|
$this->surveyRepository->touch((int) $survey->id);
|
||||||
|
|
||||||
return;
|
return (int) $question->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->surveyQuestionRepository->create((int) $survey->id, $data->toArray());
|
$question = $this->surveyQuestionRepository->create((int) $survey->id, $data->toArray());
|
||||||
$this->surveyRepository->touch((int) $survey->id);
|
$this->surveyRepository->touch((int) $survey->id);
|
||||||
|
|
||||||
|
return (int) $question->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteQuestion(SurveyModel $survey, int $questionId): void
|
public function deleteQuestion(SurveyModel $survey, int $questionId): void
|
||||||
@@ -70,6 +82,11 @@ final class SurveyEditorService
|
|||||||
$this->surveyRepository->touch((int) $survey->id);
|
$this->surveyRepository->touch((int) $survey->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteSurvey(SurveyModel $survey): void
|
||||||
|
{
|
||||||
|
$this->surveyRepository->deleteCascade((int) $survey->id);
|
||||||
|
}
|
||||||
|
|
||||||
public function saveCondition(SurveyModel $survey, SurveyConditionData $data, int $conditionId = 0): void
|
public function saveCondition(SurveyModel $survey, SurveyConditionData $data, int $conditionId = 0): void
|
||||||
{
|
{
|
||||||
$this->assertUnlocked($survey);
|
$this->assertUnlocked($survey);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Mummert\SurveyBundle\Repository\SurveyAnswerRepository;
|
|||||||
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
||||||
use Mummert\SurveyBundle\Repository\SurveySubmissionRepository;
|
use Mummert\SurveyBundle\Repository\SurveySubmissionRepository;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use Symfony\Component\Uid\Ulid;
|
use Symfony\Component\Uid\Ulid;
|
||||||
|
|
||||||
final class SurveySubmissionService
|
final class SurveySubmissionService
|
||||||
@@ -21,6 +22,7 @@ final class SurveySubmissionService
|
|||||||
private readonly SurveyAnswerRepository $surveyAnswerRepository,
|
private readonly SurveyAnswerRepository $surveyAnswerRepository,
|
||||||
private readonly SurveyRepository $surveyRepository,
|
private readonly SurveyRepository $surveyRepository,
|
||||||
private readonly QuestionTypeRegistry $questionTypeRegistry,
|
private readonly QuestionTypeRegistry $questionTypeRegistry,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,9 @@ final class SurveySubmissionService
|
|||||||
*/
|
*/
|
||||||
public function getAnswersForSubmission(SurveySubmissionModel $submission): array
|
public function getAnswersForSubmission(SurveySubmissionModel $submission): array
|
||||||
{
|
{
|
||||||
return $this->surveyAnswerRepository->findAnswersBySubmission((int) $submission->id);
|
$answers = $this->surveyAnswerRepository->findAnswersBySubmission((int) $submission->id);
|
||||||
|
|
||||||
|
return array_map(fn (array $answer): array => $this->formatSubmissionAnswer($answer), $answers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,23 +95,48 @@ final class SurveySubmissionService
|
|||||||
return $this->surveySubmissionRepository->findOverviewBySurvey((int) $survey->id);
|
return $this->surveySubmissionRepository->findOverviewBySurvey((int) $survey->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasFinishedSubmissionForSurvey(int $surveyId, SessionInterface $session): bool
|
|
||||||
{
|
|
||||||
if ($surveyId <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = (string) $session->get($this->buildSessionKey($surveyId), '');
|
|
||||||
|
|
||||||
if ('' === $token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->surveySubmissionRepository->hasFinishedBySurveyAndToken($surveyId, $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildSessionKey(int $surveyId): string
|
private function buildSessionKey(int $surveyId): string
|
||||||
{
|
{
|
||||||
return 'mummert_survey_submission_'.$surveyId;
|
return 'mummert_survey_submission_'.$surveyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $answer
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function formatSubmissionAnswer(array $answer): array
|
||||||
|
{
|
||||||
|
$questionType = (string) ($answer['questionType'] ?? 'text');
|
||||||
|
$value = trim((string) ($answer['value'] ?? ''));
|
||||||
|
|
||||||
|
$answer['questionType'] = $this->translateQuestionType($questionType);
|
||||||
|
$answer['value'] = $this->translateAnswerValue($questionType, $value);
|
||||||
|
|
||||||
|
return $answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function translateQuestionType(string $questionType): string
|
||||||
|
{
|
||||||
|
return $this->translator->trans('survey.survey.question_type_'.$questionType, [], 'messages');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function translateAnswerValue(string $questionType, string $value): string
|
||||||
|
{
|
||||||
|
if ('' === $value) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('yes_no_maybe' === $questionType) {
|
||||||
|
return $this->translator->trans('survey.survey.answer_'.$value, [], 'messages');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('choice' === $questionType) {
|
||||||
|
$parts = array_values(array_filter(array_map('trim', explode(' | ', $value)), static fn (string $entry): bool => '' !== $entry));
|
||||||
|
|
||||||
|
return implode(' | ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,26 +2,32 @@ survey:
|
|||||||
list:
|
list:
|
||||||
login_required: "Dieser Bereich verwendet den normalen Contao-Mitgliederlogin. Bitte binden Sie das Modul auf einer geschützten Seite oder mit einer separaten Login-Seite ein."
|
login_required: "Dieser Bereich verwendet den normalen Contao-Mitgliederlogin. Bitte binden Sie das Modul auf einer geschützten Seite oder mit einer separaten Login-Seite ein."
|
||||||
create: "Neue Umfrage anlegen"
|
create: "Neue Umfrage anlegen"
|
||||||
|
delete: "Löschen"
|
||||||
|
delete_confirm: "Umfrage wirklich löschen? Damit werden auch alle Fragen, Antworten und Ergebnisse endgültig gelöscht. Alternativ können Sie die Umfrage auch einfach auf nicht veröffentlicht stellen."
|
||||||
|
delete_confirm_second: "Wirklich sicher?"
|
||||||
published: "veröffentlicht"
|
published: "veröffentlicht"
|
||||||
draft: "Entwurf"
|
draft: "Entwurf"
|
||||||
locked: "gesperrt"
|
locked: "gesperrt"
|
||||||
no_description: "Keine Beschreibung hinterlegt."
|
|
||||||
questions: "%count% Fragen"
|
questions: "%count% Fragen"
|
||||||
answers: "%count% Antworten"
|
participations: "%count% Teilnahmen"
|
||||||
edit: "Bearbeiten"
|
edit: "Bearbeiten"
|
||||||
public_view: "Öffentliche Ansicht"
|
public_view: "Öffentliche Ansicht"
|
||||||
show_results: "Ergebnisse anzeigen"
|
show_results: "Ergebnisse anzeigen"
|
||||||
empty: "Dem eingeloggten Mitglied ist aktuell keine Umfrage zugewiesen."
|
empty: "Dem eingeloggten Mitglied ist aktuell keine Umfrage zugewiesen."
|
||||||
edit:
|
edit:
|
||||||
login_required: "Dieses Frontendmodul ist für eingeloggte Mitglieder gedacht. Nutzen Sie den normalen Contao-Mitgliederlogin auf einer geschützten Seite."
|
login_required: "Dieses Frontendmodul ist für eingeloggte Mitglieder gedacht. Nutzen Sie den normalen Contao-Mitgliederlogin auf einer geschützten Seite."
|
||||||
|
entry_not_allowed: "Diese Seite kann nicht direkt geöffnet werden. Bitte wechseln Sie über die Umfragen-Liste in den Bearbeitungsmodus."
|
||||||
create_title: "Neue Umfrage erstellen"
|
create_title: "Neue Umfrage erstellen"
|
||||||
create_text: "Die Umfrage wird nach dem Anlegen automatisch dem eingeloggten Mitglied zugewiesen."
|
create_text: "Die Umfrage wird nach dem Anlegen automatisch dem eingeloggten Mitglied zugewiesen."
|
||||||
label_title: "Titel"
|
label_title: "Titel"
|
||||||
|
label_survey_title: "Titel der Umfrage"
|
||||||
label_description: "Beschreibung"
|
label_description: "Beschreibung"
|
||||||
publish_directly: "Direkt veröffentlichen"
|
publish_directly: "Direkt veröffentlichen"
|
||||||
create_submit: "Umfrage erstellen"
|
create_submit: "Umfrage erstellen"
|
||||||
back_to_list: "Zur Liste"
|
back_to_list: "Zur Liste"
|
||||||
public_view_open: "Öffentliche Ansicht öffnen"
|
public_view_open: "Öffentliche Ansicht öffnen"
|
||||||
|
copy_public_link: "Öffentlichen Link kopieren"
|
||||||
|
copy_public_link_success: "Link kopiert"
|
||||||
public_link: "Öffentlicher Link"
|
public_link: "Öffentlicher Link"
|
||||||
no_public_page: "Noch keine öffentliche Seite hinterlegt."
|
no_public_page: "Noch keine öffentliche Seite hinterlegt."
|
||||||
status: "Status"
|
status: "Status"
|
||||||
@@ -34,6 +40,12 @@ survey:
|
|||||||
metadata: "Umfrage-Metadaten"
|
metadata: "Umfrage-Metadaten"
|
||||||
publish_survey: "Umfrage veröffentlichen"
|
publish_survey: "Umfrage veröffentlichen"
|
||||||
save: "Speichern"
|
save: "Speichern"
|
||||||
|
save_metadata: "Metadaten speichern"
|
||||||
|
save_this_question: "Diese Frage speichern"
|
||||||
|
save_new_question: "Neue Frage speichern"
|
||||||
|
dirty_metadata: "Ungespeicherte Änderungen in den Metadaten"
|
||||||
|
dirty_question: "Ungespeicherte Änderungen in dieser Frage"
|
||||||
|
dirty_new_question: "Ungespeicherte Änderungen an der neuen Frage"
|
||||||
editors_title: "Bearbeitende Mitglieder"
|
editors_title: "Bearbeitende Mitglieder"
|
||||||
no_editors: "Keine Editoren hinterlegt."
|
no_editors: "Keine Editoren hinterlegt."
|
||||||
responses_title: "Antworten und Ergebnisse"
|
responses_title: "Antworten und Ergebnisse"
|
||||||
@@ -66,6 +78,9 @@ survey:
|
|||||||
delete_question_confirm: "Frage wirklich löschen?"
|
delete_question_confirm: "Frage wirklich löschen?"
|
||||||
delete: "Löschen"
|
delete: "Löschen"
|
||||||
no_questions: "Noch keine Fragen angelegt."
|
no_questions: "Noch keine Fragen angelegt."
|
||||||
|
new_question: "Neue Frage anlegen"
|
||||||
|
open_question: "Akkordeon öffnen"
|
||||||
|
close_question: "Akkordeon schließen"
|
||||||
conditions_title: "Sprungregeln"
|
conditions_title: "Sprungregeln"
|
||||||
source_question: "Ausgangsfrage"
|
source_question: "Ausgangsfrage"
|
||||||
answer: "Antwort"
|
answer: "Antwort"
|
||||||
@@ -82,6 +97,13 @@ survey:
|
|||||||
default_description: "Bitte beantworten Sie die folgenden Fragen. Jede Frage wird einzeln angezeigt."
|
default_description: "Bitte beantworten Sie die folgenden Fragen. Jede Frage wird einzeln angezeigt."
|
||||||
thanks_title: "Vielen Dank fürs Ausfüllen."
|
thanks_title: "Vielen Dank fürs Ausfüllen."
|
||||||
thanks_text: "Ihre Antworten wurden anonym gespeichert. Es wurde keine Teilnehmeridentität mit diesem Umfrage-Durchlauf verknüpft."
|
thanks_text: "Ihre Antworten wurden anonym gespeichert. Es wurde keine Teilnehmeridentität mit diesem Umfrage-Durchlauf verknüpft."
|
||||||
|
question_type_yes_no_maybe: "Ja/Nein/Vielleicht"
|
||||||
|
question_type_choice: "Auswahl"
|
||||||
|
question_type_range: "Bewertung"
|
||||||
|
question_type_text: "Freitext"
|
||||||
|
answer_yes: "Ja"
|
||||||
|
answer_no: "Nein"
|
||||||
|
answer_maybe: "Vielleicht"
|
||||||
question_progress: "Frage %current%"
|
question_progress: "Frage %current%"
|
||||||
answer_flow_hint: "Diese Antwort wird direkt für den weiteren Ablauf verwendet."
|
answer_flow_hint: "Diese Antwort wird direkt für den weiteren Ablauf verwendet."
|
||||||
current_value: "Aktueller Wert"
|
current_value: "Aktueller Wert"
|
||||||
@@ -89,6 +111,7 @@ survey:
|
|||||||
results:
|
results:
|
||||||
no_description: "Keine Beschreibung hinterlegt."
|
no_description: "Keine Beschreibung hinterlegt."
|
||||||
completed_submissions: "%count% abgeschlossene Teilnahmen"
|
completed_submissions: "%count% abgeschlossene Teilnahmen"
|
||||||
|
participations: "%count% Teilnahmen"
|
||||||
questions: "%count% Fragen"
|
questions: "%count% Fragen"
|
||||||
download_excel: "Ergebnisse als Excel herunterladen"
|
download_excel: "Ergebnisse als Excel herunterladen"
|
||||||
download_pdf: "Ergebnisse als PDF herunterladen"
|
download_pdf: "Ergebnisse als PDF herunterladen"
|
||||||
@@ -103,6 +126,7 @@ survey:
|
|||||||
pdf:
|
pdf:
|
||||||
no_description: "Keine Beschreibung hinterlegt."
|
no_description: "Keine Beschreibung hinterlegt."
|
||||||
completed_submissions: "%count% abgeschlossene Teilnahmen"
|
completed_submissions: "%count% abgeschlossene Teilnahmen"
|
||||||
|
participations: "%count% Teilnahmen"
|
||||||
questions: "%count% Fragen"
|
questions: "%count% Fragen"
|
||||||
export_prefix: "Export %date%"
|
export_prefix: "Export %date%"
|
||||||
answers: "%count% Antworten"
|
answers: "%count% Antworten"
|
||||||
|
|||||||
@@ -2,26 +2,32 @@ survey:
|
|||||||
list:
|
list:
|
||||||
login_required: "This area uses the regular Contao member login. Please place the module on a protected page or provide a separate login page."
|
login_required: "This area uses the regular Contao member login. Please place the module on a protected page or provide a separate login page."
|
||||||
create: "Create new survey"
|
create: "Create new survey"
|
||||||
|
delete: "Delete"
|
||||||
|
delete_confirm: "Delete this survey? This will permanently remove all questions, answers, and results as well. Alternatively, you can simply switch the survey to unpublished."
|
||||||
|
delete_confirm_second: "Are you really sure?"
|
||||||
published: "published"
|
published: "published"
|
||||||
draft: "Draft"
|
draft: "Draft"
|
||||||
locked: "locked"
|
locked: "locked"
|
||||||
no_description: "No description available."
|
|
||||||
questions: "%count% questions"
|
questions: "%count% questions"
|
||||||
answers: "%count% answers"
|
participations: "%count% participations"
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
public_view: "Public view"
|
public_view: "Public view"
|
||||||
show_results: "Show results"
|
show_results: "Show results"
|
||||||
empty: "There is currently no survey assigned to the logged-in member."
|
empty: "There is currently no survey assigned to the logged-in member."
|
||||||
edit:
|
edit:
|
||||||
login_required: "This frontend module is intended for logged-in members. Please use the regular Contao member login on a protected page."
|
login_required: "This frontend module is intended for logged-in members. Please use the regular Contao member login on a protected page."
|
||||||
|
entry_not_allowed: "This page cannot be opened directly. Please switch to edit mode via the survey list."
|
||||||
create_title: "Create new survey"
|
create_title: "Create new survey"
|
||||||
create_text: "The survey will automatically be assigned to the logged-in member after creation."
|
create_text: "The survey will automatically be assigned to the logged-in member after creation."
|
||||||
label_title: "Title"
|
label_title: "Title"
|
||||||
|
label_survey_title: "Survey title"
|
||||||
label_description: "Description"
|
label_description: "Description"
|
||||||
publish_directly: "Publish immediately"
|
publish_directly: "Publish immediately"
|
||||||
create_submit: "Create survey"
|
create_submit: "Create survey"
|
||||||
back_to_list: "Back to list"
|
back_to_list: "Back to list"
|
||||||
public_view_open: "Open public view"
|
public_view_open: "Open public view"
|
||||||
|
copy_public_link: "Copy public link"
|
||||||
|
copy_public_link_success: "Link copied"
|
||||||
public_link: "Public link"
|
public_link: "Public link"
|
||||||
no_public_page: "No public page configured yet."
|
no_public_page: "No public page configured yet."
|
||||||
status: "Status"
|
status: "Status"
|
||||||
@@ -34,6 +40,12 @@ survey:
|
|||||||
metadata: "Survey metadata"
|
metadata: "Survey metadata"
|
||||||
publish_survey: "Publish survey"
|
publish_survey: "Publish survey"
|
||||||
save: "Save"
|
save: "Save"
|
||||||
|
save_metadata: "Save metadata"
|
||||||
|
save_this_question: "Save this question"
|
||||||
|
save_new_question: "Save new question"
|
||||||
|
dirty_metadata: "Unsaved changes in metadata"
|
||||||
|
dirty_question: "Unsaved changes in this question"
|
||||||
|
dirty_new_question: "Unsaved changes in the new question"
|
||||||
editors_title: "Editing members"
|
editors_title: "Editing members"
|
||||||
no_editors: "No editors configured."
|
no_editors: "No editors configured."
|
||||||
responses_title: "Answers and results"
|
responses_title: "Answers and results"
|
||||||
@@ -66,6 +78,9 @@ survey:
|
|||||||
delete_question_confirm: "Delete this question?"
|
delete_question_confirm: "Delete this question?"
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
no_questions: "No questions created yet."
|
no_questions: "No questions created yet."
|
||||||
|
new_question: "Create new question"
|
||||||
|
open_question: "Open accordion"
|
||||||
|
close_question: "Close accordion"
|
||||||
conditions_title: "Jump rules"
|
conditions_title: "Jump rules"
|
||||||
source_question: "Source question"
|
source_question: "Source question"
|
||||||
answer: "Answer"
|
answer: "Answer"
|
||||||
@@ -82,6 +97,13 @@ survey:
|
|||||||
default_description: "Please answer the following questions. Each question is shown individually."
|
default_description: "Please answer the following questions. Each question is shown individually."
|
||||||
thanks_title: "Thank you for completing the survey."
|
thanks_title: "Thank you for completing the survey."
|
||||||
thanks_text: "Your answers have been stored anonymously. No participant identity was linked to this survey run."
|
thanks_text: "Your answers have been stored anonymously. No participant identity was linked to this survey run."
|
||||||
|
question_type_yes_no_maybe: "Yes/No/Maybe"
|
||||||
|
question_type_choice: "Choice"
|
||||||
|
question_type_range: "Rating"
|
||||||
|
question_type_text: "Free text"
|
||||||
|
answer_yes: "Yes"
|
||||||
|
answer_no: "No"
|
||||||
|
answer_maybe: "Maybe"
|
||||||
question_progress: "Question %current%"
|
question_progress: "Question %current%"
|
||||||
answer_flow_hint: "This answer is used directly for the further flow."
|
answer_flow_hint: "This answer is used directly for the further flow."
|
||||||
current_value: "Current value"
|
current_value: "Current value"
|
||||||
@@ -89,6 +111,7 @@ survey:
|
|||||||
results:
|
results:
|
||||||
no_description: "No description available."
|
no_description: "No description available."
|
||||||
completed_submissions: "%count% completed participations"
|
completed_submissions: "%count% completed participations"
|
||||||
|
participations: "%count% participations"
|
||||||
questions: "%count% questions"
|
questions: "%count% questions"
|
||||||
download_excel: "Download results as Excel"
|
download_excel: "Download results as Excel"
|
||||||
download_pdf: "Download results as PDF"
|
download_pdf: "Download results as PDF"
|
||||||
@@ -103,6 +126,7 @@ survey:
|
|||||||
pdf:
|
pdf:
|
||||||
no_description: "No description available."
|
no_description: "No description available."
|
||||||
completed_submissions: "%count% completed participations"
|
completed_submissions: "%count% completed participations"
|
||||||
|
participations: "%count% participations"
|
||||||
questions: "%count% questions"
|
questions: "%count% questions"
|
||||||
export_prefix: "Export %date%"
|
export_prefix: "Export %date%"
|
||||||
answers: "%count% answers"
|
answers: "%count% answers"
|
||||||
|
|||||||
Reference in New Issue
Block a user