new system prototype
This commit is contained in:
945
website/css/style.css
Normal file
945
website/css/style.css
Normal file
@ -0,0 +1,945 @@
|
||||
:root {
|
||||
--bg: #f4f6f9;
|
||||
--surface: #ffffff;
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--danger: #dc2626;
|
||||
--danger-hover: #b91c1c;
|
||||
--success: #16a34a;
|
||||
--warning: #d97706;
|
||||
--text: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);
|
||||
--shadow-lg: 0 4px 12px rgba(0,0,0,.1);
|
||||
--sidebar-width: 240px;
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.sidebar-brand {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.sidebar-brand small {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: .75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
list-style: none;
|
||||
}
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: .9rem;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.sidebar-nav a:hover,
|
||||
.sidebar-nav a.active {
|
||||
background: #f1f5f9;
|
||||
color: var(--primary);
|
||||
}
|
||||
.sidebar-nav a svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: .85rem;
|
||||
}
|
||||
.sidebar-footer .user-info {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
flex: 1;
|
||||
padding: 28px 32px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: .875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background .15s, box-shadow .15s;
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.btn:hover { background: #f8fafc; box-shadow: var(--shadow); }
|
||||
.btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||
.btn-primary:hover { background: var(--primary-hover); }
|
||||
.btn-danger { background: var(--danger); color: #fff; border-color: var(--danger); }
|
||||
.btn-danger:hover { background: var(--danger-hover); }
|
||||
.btn-sm { padding: 4px 10px; font-size: .8rem; }
|
||||
.btn-block { width: 100%; justify-content: center; }
|
||||
.btn-icon { padding: 6px; border: none; background: transparent; }
|
||||
.btn-icon:hover { background: #f1f5f9; border-radius: 4px; }
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: .85rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: .9rem;
|
||||
font-family: var(--font);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37,99,235,.12);
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.form-row .form-group { flex: 1; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
.badge-draft { background: #f1f5f9; color: #64748b; }
|
||||
.badge-active { background: #dcfce7; color: #16a34a; }
|
||||
.badge-archived { background: #fef3c7; color: #d97706; }
|
||||
.badge-completed { background: #dcfce7; color: #16a34a; }
|
||||
.badge-in_progress { background: #dbeafe; color: #2563eb; }
|
||||
.badge-pending { background: #f1f5f9; color: #64748b; }
|
||||
|
||||
/* Page header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.page-header .actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Questionnaire card grid */
|
||||
.q-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.q-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: box-shadow .2s, transform .2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.q-card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.q-card-header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.q-card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.q-card-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: var(--text-secondary);
|
||||
font-size: .85rem;
|
||||
}
|
||||
.q-card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
table.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .875rem;
|
||||
}
|
||||
table.data-table th,
|
||||
table.data-table td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
table.data-table th {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: .8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
background: #f8fafc;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
table.data-table tr:hover td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
th.sortable { cursor: pointer; user-select: none; }
|
||||
th.sortable::after { content: ' \2195'; opacity: .4; }
|
||||
th.sort-asc::after { content: ' \2191'; opacity: 1; }
|
||||
th.sort-desc::after { content: ' \2193'; opacity: 1; }
|
||||
|
||||
/* Accordion / question list */
|
||||
.question-list {
|
||||
list-style: none;
|
||||
}
|
||||
.question-item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.question-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.question-header:hover { background: #f8fafc; }
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
.question-header .q-text {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-header .q-type {
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
background: #f1f5f9;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.question-body {
|
||||
display: none;
|
||||
padding: 0 16px 16px 42px;
|
||||
}
|
||||
.question-item.open .question-body {
|
||||
display: block;
|
||||
}
|
||||
.question-item.open .chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.chevron {
|
||||
transition: transform .2s;
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Answer options inside a question */
|
||||
.option-list {
|
||||
list-style: none;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
background: #fafbfc;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.option-item .opt-text { flex: 1; }
|
||||
.option-item .opt-points {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
font-size: .8rem;
|
||||
background: #eff6ff;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Translations panel */
|
||||
.translations-panel {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.translations-panel h4 {
|
||||
font-size: .85rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.translation-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.translation-row input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.translation-row .lang-code {
|
||||
font-weight: 600;
|
||||
font-size: .8rem;
|
||||
width: 30px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Inline edit */
|
||||
.inline-edit {
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
padding: 2px 4px;
|
||||
font: inherit;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.inline-edit:hover { border-color: var(--border); }
|
||||
.inline-edit:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 0 0 3px rgba(37,99,235,.1);
|
||||
}
|
||||
|
||||
/* Login */
|
||||
.login-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.login-card h1 { font-size: 1.5rem; margin-bottom: 4px; }
|
||||
.login-card .subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: .9rem;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.error-text { color: var(--danger); font-size: .85rem; margin-top: 8px; }
|
||||
|
||||
/* Toast */
|
||||
#toastContainer {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.toast {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: .875rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
opacity: 0;
|
||||
transform: translateX(40px);
|
||||
transition: opacity .3s, transform .3s;
|
||||
max-width: 360px;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(0); }
|
||||
.toast-success { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
|
||||
.toast-error { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
|
||||
.toast-info { background: #dbeafe; color: #1e40af; border: 1px solid #bfdbfe; }
|
||||
|
||||
/* Filter bar */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-bar select,
|
||||
.filter-bar input {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: .85rem;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.empty-state svg { margin-bottom: 16px; opacity: .4; }
|
||||
.empty-state h3 { margin-bottom: 8px; color: var(--text); }
|
||||
|
||||
/* Dragging */
|
||||
.question-item.dragging {
|
||||
opacity: .5;
|
||||
border-style: dashed;
|
||||
}
|
||||
.option-item.dragging {
|
||||
opacity: .5;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin .6s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform .3s;
|
||||
}
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.main-content { margin-left: 0; padding: 16px; }
|
||||
.q-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Role badges */
|
||||
.badge-admin { background: #ede9fe; color: #6d28d9; }
|
||||
.badge-supervisor { background: #fce7f3; color: #be185d; }
|
||||
.badge-coach { background: #d1fae5; color: #065f46; }
|
||||
.badge-you { background: #dbeafe; color: #1d4ed8; font-size: .7rem; }
|
||||
|
||||
/* Assignment page layout */
|
||||
.assign-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.assign-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.assign-coaches { padding: 16px; }
|
||||
.assign-right { display: flex; flex-direction: column; gap: 16px; }
|
||||
.assign-clients-card { padding: 16px; }
|
||||
.assign-action-card { padding: 16px; }
|
||||
|
||||
/* Coach list */
|
||||
.coach-list { list-style: none; }
|
||||
.coach-list-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border: 1px solid transparent;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.coach-list-item:hover { background: #f1f5f9; }
|
||||
.coach-list-item.active {
|
||||
background: #eff6ff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.coach-name { font-weight: 600; font-size: .9rem; }
|
||||
.coach-supervisor { font-size: .8rem; color: var(--text-secondary); }
|
||||
.coach-client-count {
|
||||
font-size: .75rem;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Selection info bar */
|
||||
.selection-info {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 6px;
|
||||
font-size: .875rem;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab-bar button {
|
||||
padding: 10px 20px;
|
||||
font-size: .9rem;
|
||||
font-weight: 500;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: color .15s, border-color .15s;
|
||||
}
|
||||
.tab-bar button:hover { color: var(--text); }
|
||||
.tab-bar button.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Metadata form in editor */
|
||||
.meta-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.meta-form { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Inline form cards (add/edit question, add/edit option) */
|
||||
.inline-form-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.inline-form-card h4 {
|
||||
font-size: .9rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.inline-form-card .form-group { margin-bottom: 10px; }
|
||||
|
||||
/* Type select */
|
||||
.type-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: .9rem;
|
||||
font-family: var(--font);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.type-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37,99,235,.12);
|
||||
}
|
||||
|
||||
/* Checkbox label */
|
||||
.checkbox-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: .875rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Options builder (inside add-question form) */
|
||||
.options-builder {
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* Pending options list (before question is saved) */
|
||||
.pending-options-list {
|
||||
list-style: none;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.pending-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 10px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.pending-option-item .opt-text { flex: 1; }
|
||||
.pending-options-empty {
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 0 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Branch indicator on answer options */
|
||||
.opt-branch {
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
color: #7c3aed;
|
||||
background: #ede9fe;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Config section inside question forms */
|
||||
.config-section {
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.config-section .form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-family: var(--font);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Condition details toggle */
|
||||
.condition-details summary {
|
||||
list-style: none;
|
||||
}
|
||||
.condition-details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.condition-details summary::before {
|
||||
content: '\25B6 ';
|
||||
font-size: .7rem;
|
||||
transition: transform .2s;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.condition-details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* ── Language manager ── */
|
||||
.lang-manager {
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.lang-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.lang-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 4px 10px 4px 12px;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.lang-chip strong { color: var(--primary); }
|
||||
.lang-chip-name { color: var(--text-secondary); }
|
||||
.lang-chip-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
.lang-chip-remove:hover { color: var(--danger); }
|
||||
.lang-add-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.lang-add-code {
|
||||
width: 80px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.lang-add-name {
|
||||
width: 160px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
/* ── Translations toolbar ── */
|
||||
.trans-toolbar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.trans-search-wrap { flex: 1; min-width: 200px; }
|
||||
.trans-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: .9rem;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.trans-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37,99,235,.12);
|
||||
}
|
||||
.trans-filter-types {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Translations table ── */
|
||||
.trans-table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.trans-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.trans-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f1f5f9;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
.trans-table td { padding: 2px 4px; }
|
||||
.trans-table tbody tr { border-bottom: 1px solid #f1f5f9; }
|
||||
.trans-table tbody tr:hover { background: #f8fafc; }
|
||||
.trans-row-hidden { display: none; }
|
||||
|
||||
.col-key { min-width: 180px; }
|
||||
.col-type { width: 50px; text-align: center; }
|
||||
.col-lang { min-width: 160px; }
|
||||
|
||||
.cell-key {
|
||||
font-family: monospace;
|
||||
font-size: .82rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 280px;
|
||||
padding: 6px 10px !important;
|
||||
}
|
||||
.cell-type { text-align: center; padding: 6px 4px !important; }
|
||||
|
||||
.trans-type-badge {
|
||||
display: inline-block;
|
||||
font-size: .7rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
.badge-q { background: #dbeafe; color: #1e40af; }
|
||||
.badge-opt { background: #fef3c7; color: #92400e; }
|
||||
.badge-str { background: #ede9fe; color: #5b21b6; }
|
||||
|
||||
.cell-trans { position: relative; }
|
||||
.cell-missing { background: #fff7ed; }
|
||||
|
||||
.trans-cell-input {
|
||||
width: 100%;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: .85rem;
|
||||
font-family: var(--font);
|
||||
background: transparent;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.trans-cell-input:hover {
|
||||
border-color: var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
.trans-cell-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 0 0 2px rgba(37,99,235,.1);
|
||||
}
|
||||
.trans-cell-input::placeholder {
|
||||
color: #f59e0b;
|
||||
font-style: italic;
|
||||
font-size: .8rem;
|
||||
}
|
||||
.trans-cell-input.save-flash {
|
||||
animation: saveFlash .4s ease;
|
||||
}
|
||||
@keyframes saveFlash {
|
||||
0% { background: #dcfce7; }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
/* ── Stats bar ── */
|
||||
.trans-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
font-size: .82rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.trans-stats-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.trans-progress-bar {
|
||||
width: 120px;
|
||||
height: 6px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.trans-progress-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--success);
|
||||
border-radius: 3px;
|
||||
transition: width .3s ease;
|
||||
}
|
||||
58
website/index.html
Normal file
58
website/index.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BW Schützt - Dashboard</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="layout">
|
||||
<nav class="sidebar" id="mainNav" style="display:none">
|
||||
<div class="sidebar-brand">
|
||||
BW Schützt
|
||||
<small>Questionnaire Management</small>
|
||||
</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="#/">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li data-nav-roles="admin supervisor">
|
||||
<a href="#/users">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
|
||||
Users
|
||||
</a>
|
||||
</li>
|
||||
<li data-nav-roles="admin supervisor">
|
||||
<a href="#/assignments">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||
Assign Clients
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/export">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Export Data
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info" id="userInfo"></div>
|
||||
<a href="#" id="logoutBtn" class="btn btn-sm" style="width:100%;justify-content:center;">Log out</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content" id="app">
|
||||
<!-- Pages render here -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="toastContainer"></div>
|
||||
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
93
website/js/api.js
Normal file
93
website/js/api.js
Normal file
@ -0,0 +1,93 @@
|
||||
const API_BASE = '../api';
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('qdb_token');
|
||||
}
|
||||
|
||||
async function apiFetch(endpoint, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = options.headers || {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
if (!(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json; charset=UTF-8';
|
||||
}
|
||||
|
||||
// Strip .php suffix for clean URLs
|
||||
const cleanEndpoint = endpoint.replace(/\.php(\?|$)/, '$1');
|
||||
const res = await fetch(`${API_BASE}/${cleanEndpoint}`, { ...options, headers });
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
const json = await res.json().catch(() => ({}));
|
||||
const errMsg = json.error?.message || json.error || '';
|
||||
if (res.status === 401 || errMsg === 'Invalid or expired token') {
|
||||
localStorage.removeItem('qdb_token');
|
||||
localStorage.removeItem('qdb_user');
|
||||
localStorage.removeItem('qdb_role');
|
||||
window.location.hash = '#/login';
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
throw new Error(errMsg || 'Permission denied');
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap the unified response envelope:
|
||||
* {"ok": true, "data": ...} -> returns { success: true, ...data }
|
||||
* {"ok": false, "error": ...} -> returns { success: false, error: message }
|
||||
* Also supports legacy format for backward compat.
|
||||
*/
|
||||
function unwrap(json) {
|
||||
if (typeof json.ok === 'boolean') {
|
||||
if (json.ok) {
|
||||
const d = json.data || {};
|
||||
return { success: true, ...(Array.isArray(d) ? { items: d } : d) };
|
||||
}
|
||||
return { success: false, error: json.error?.message || 'Unknown error' };
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
export async function apiGet(endpoint) {
|
||||
const res = await apiFetch(endpoint);
|
||||
return unwrap(await res.json());
|
||||
}
|
||||
|
||||
export async function apiPost(endpoint, body) {
|
||||
const res = await apiFetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return unwrap(await res.json());
|
||||
}
|
||||
|
||||
export async function apiPut(endpoint, body) {
|
||||
const res = await apiFetch(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return unwrap(await res.json());
|
||||
}
|
||||
|
||||
export async function apiPatch(endpoint, body) {
|
||||
const res = await apiFetch(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return unwrap(await res.json());
|
||||
}
|
||||
|
||||
export async function apiDelete(endpoint, body) {
|
||||
const res = await apiFetch(endpoint, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return unwrap(await res.json());
|
||||
}
|
||||
|
||||
export function apiDownloadUrl(endpoint) {
|
||||
const token = getToken();
|
||||
const cleanEndpoint = endpoint.replace(/\.php(\?|$)/, '$1');
|
||||
return `${API_BASE}/${cleanEndpoint}${cleanEndpoint.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
103
website/js/app.js
Normal file
103
website/js/app.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { addRoute, startRouter, navigate } from './router.js';
|
||||
import { loginPage } from './pages/login.js';
|
||||
import { dashboardPage } from './pages/dashboard.js';
|
||||
import { editorPage } from './pages/editor.js';
|
||||
import { resultsPage } from './pages/results.js';
|
||||
import { exportPage } from './pages/export.js';
|
||||
import { usersPage } from './pages/users.js';
|
||||
import { assignmentsPage } from './pages/assignments.js';
|
||||
|
||||
// Auth state
|
||||
export function isLoggedIn() {
|
||||
return !!localStorage.getItem('qdb_token');
|
||||
}
|
||||
export function getRole() {
|
||||
return localStorage.getItem('qdb_role') || '';
|
||||
}
|
||||
export function getUser() {
|
||||
return localStorage.getItem('qdb_user') || '';
|
||||
}
|
||||
export function canEdit() {
|
||||
const r = getRole();
|
||||
return r === 'admin' || r === 'supervisor';
|
||||
}
|
||||
|
||||
export function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
requestAnimationFrame(() => toast.classList.add('show'));
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function authGuard(handler) {
|
||||
return (params) => {
|
||||
if (!isLoggedIn()) { navigate('#/login'); return; }
|
||||
return handler(params);
|
||||
};
|
||||
}
|
||||
|
||||
function updateNav() {
|
||||
const nav = document.getElementById('mainNav');
|
||||
const userInfo = document.getElementById('userInfo');
|
||||
if (isLoggedIn()) {
|
||||
nav.style.display = '';
|
||||
const role = getRole();
|
||||
userInfo.textContent = `${getUser()} (${role})`;
|
||||
|
||||
// Show/hide role-gated nav items
|
||||
document.querySelectorAll('[data-nav-roles]').forEach(li => {
|
||||
const allowed = li.dataset.navRoles.split(' ');
|
||||
li.style.display = allowed.includes(role) ? '' : 'none';
|
||||
});
|
||||
|
||||
// Highlight active nav link
|
||||
const hash = window.location.hash.slice(1) || '/';
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
||||
const href = a.getAttribute('href').slice(1); // strip leading #
|
||||
a.classList.toggle('active', hash === href || (href !== '/' && hash.startsWith(href)));
|
||||
});
|
||||
} else {
|
||||
nav.style.display = 'none';
|
||||
userInfo.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function roleGuard(roles, handler) {
|
||||
return (params) => {
|
||||
if (!isLoggedIn()) { navigate('#/login'); return; }
|
||||
if (!roles.includes(getRole())) { navigate('#/'); return; }
|
||||
return handler(params);
|
||||
};
|
||||
}
|
||||
|
||||
// Routes
|
||||
addRoute('/login', loginPage);
|
||||
addRoute('/', authGuard(dashboardPage));
|
||||
addRoute('/dashboard', authGuard(dashboardPage));
|
||||
addRoute('/questionnaire/new', authGuard(editorPage));
|
||||
addRoute('/questionnaire/:id/results', authGuard(resultsPage));
|
||||
addRoute('/questionnaire/:id', authGuard(editorPage));
|
||||
addRoute('/export', authGuard(exportPage));
|
||||
addRoute('/users', roleGuard(['admin', 'supervisor'], usersPage));
|
||||
addRoute('/assignments', roleGuard(['admin', 'supervisor'], assignmentsPage));
|
||||
|
||||
// Nav link handling & logout
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('logoutBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem('qdb_token');
|
||||
localStorage.removeItem('qdb_user');
|
||||
localStorage.removeItem('qdb_role');
|
||||
navigate('#/login');
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', updateNav);
|
||||
updateNav();
|
||||
startRouter();
|
||||
});
|
||||
260
website/js/pages/assignments.js
Normal file
260
website/js/pages/assignments.js
Normal file
@ -0,0 +1,260 @@
|
||||
import { apiGet, apiPost } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
|
||||
let coaches = [];
|
||||
let clients = [];
|
||||
let selectedCoachID = null;
|
||||
|
||||
export async function assignmentsPage() {
|
||||
selectedCoachID = null;
|
||||
const app = document.getElementById('app');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Assign Clients</h1>
|
||||
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
||||
</div>
|
||||
<div id="assignContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await apiGet('assignments.php');
|
||||
coaches = data.coaches || [];
|
||||
clients = data.clients || [];
|
||||
renderAssignments();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('assignContent').innerHTML =
|
||||
`<p class="error-text" style="padding:16px">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAssignments() {
|
||||
const container = document.getElementById('assignContent');
|
||||
|
||||
if (!coaches.length) {
|
||||
container.innerHTML = `
|
||||
<div class="card empty-state">
|
||||
<h3>No coaches found</h3>
|
||||
<p>Create coaches first before assigning clients.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group clients by coachID for the filter badge counts
|
||||
const clientsByCoach = {};
|
||||
const unassigned = [];
|
||||
clients.forEach(c => {
|
||||
if (c.coachID) {
|
||||
(clientsByCoach[c.coachID] = clientsByCoach[c.coachID] || []).push(c);
|
||||
} else {
|
||||
unassigned.push(c);
|
||||
}
|
||||
});
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="assign-layout">
|
||||
|
||||
<!-- Left: Coach list -->
|
||||
<div class="assign-coaches card">
|
||||
<h3 style="margin-bottom:12px">Coaches</h3>
|
||||
<ul class="coach-list" id="coachList">
|
||||
${coaches.map(c => {
|
||||
const count = (clientsByCoach[c.coachID] || []).length;
|
||||
return `
|
||||
<li class="coach-list-item" data-id="${c.coachID}">
|
||||
<div class="coach-name">${esc(c.username)}</div>
|
||||
${c.supervisorUsername
|
||||
? `<div class="coach-supervisor">${esc(c.supervisorUsername)}</div>`
|
||||
: ''}
|
||||
<span class="coach-client-count">${count} client${count === 1 ? '' : 's'}</span>
|
||||
</li>`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Right: Client list + assign panel -->
|
||||
<div class="assign-right">
|
||||
<div class="card assign-clients-card">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<h3>Clients</h3>
|
||||
<div class="filter-bar" style="margin:0">
|
||||
<select id="filterByCoach">
|
||||
<option value="">All coaches</option>
|
||||
<option value="__unassigned__">Unassigned</option>
|
||||
${coaches.map(c =>
|
||||
`<option value="${c.coachID}">${esc(c.username)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
<input type="text" id="clientSearch" placeholder="Search client code..." style="width:160px">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-sm" id="selectAllBtn">Select all</button>
|
||||
<button class="btn btn-sm" id="clearSelBtn">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper" style="max-height:420px;overflow-y:auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:36px"><input type="checkbox" id="selectAllCb" title="Toggle all visible"></th>
|
||||
<th>Client Code</th>
|
||||
<th>Current Coach</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="clientTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="selectionInfo" class="selection-info" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<!-- Assign action card -->
|
||||
<div class="card assign-action-card" id="assignActionCard" style="display:none">
|
||||
<h4>Assign selected clients to:</h4>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px">
|
||||
<select id="targetCoachSelect" style="flex:1;min-width:180px;padding:8px 12px;border:1px solid var(--border);border-radius:6px">
|
||||
<option value="">— select coach —</option>
|
||||
${coaches.map(c =>
|
||||
`<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
<button class="btn btn-primary" id="assignBtn">Assign</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Wire coach list click → filter clients
|
||||
document.querySelectorAll('.coach-list-item').forEach(li => {
|
||||
li.addEventListener('click', () => {
|
||||
document.querySelectorAll('.coach-list-item').forEach(x => x.classList.remove('active'));
|
||||
li.classList.add('active');
|
||||
selectedCoachID = li.dataset.id;
|
||||
document.getElementById('filterByCoach').value = selectedCoachID;
|
||||
renderClientRows();
|
||||
});
|
||||
});
|
||||
|
||||
// Wire filters
|
||||
document.getElementById('filterByCoach').addEventListener('change', renderClientRows);
|
||||
document.getElementById('clientSearch').addEventListener('input', renderClientRows);
|
||||
|
||||
// Wire select all / clear
|
||||
document.getElementById('selectAllBtn').addEventListener('click', () => {
|
||||
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => cb.checked = true);
|
||||
updateSelectionInfo();
|
||||
});
|
||||
document.getElementById('clearSelBtn').addEventListener('click', () => {
|
||||
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => cb.checked = false);
|
||||
updateSelectionInfo();
|
||||
});
|
||||
document.getElementById('selectAllCb').addEventListener('change', (e) => {
|
||||
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => cb.checked = e.target.checked);
|
||||
updateSelectionInfo();
|
||||
});
|
||||
|
||||
// Wire assign button
|
||||
document.getElementById('assignBtn').addEventListener('click', doAssign);
|
||||
|
||||
renderClientRows();
|
||||
}
|
||||
|
||||
function renderClientRows() {
|
||||
const body = document.getElementById('clientTableBody');
|
||||
const coachFilter = document.getElementById('filterByCoach').value;
|
||||
const search = document.getElementById('clientSearch').value.trim().toLowerCase();
|
||||
|
||||
let visible = clients;
|
||||
if (coachFilter === '__unassigned__') {
|
||||
visible = clients.filter(c => !c.coachID);
|
||||
} else if (coachFilter) {
|
||||
visible = clients.filter(c => c.coachID === coachFilter);
|
||||
}
|
||||
if (search) {
|
||||
visible = visible.filter(c => c.clientCode.toLowerCase().includes(search));
|
||||
}
|
||||
|
||||
if (!visible.length) {
|
||||
body.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--text-secondary);padding:24px">No clients match the current filter</td></tr>`;
|
||||
updateSelectionInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
body.innerHTML = visible.map(c => `
|
||||
<tr>
|
||||
<td><input type="checkbox" class="client-cb" data-code="${esc(c.clientCode)}" value="${esc(c.clientCode)}"></td>
|
||||
<td><strong>${esc(c.clientCode)}</strong></td>
|
||||
<td>${c.coachUsername ? esc(c.coachUsername) : '<span style="color:var(--text-secondary);font-style:italic">Unassigned</span>'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
body.querySelectorAll('.client-cb').forEach(cb => {
|
||||
cb.addEventListener('change', updateSelectionInfo);
|
||||
});
|
||||
|
||||
updateSelectionInfo();
|
||||
}
|
||||
|
||||
function getSelectedCodes() {
|
||||
return [...document.querySelectorAll('#clientTableBody .client-cb:checked')]
|
||||
.map(cb => cb.value);
|
||||
}
|
||||
|
||||
function updateSelectionInfo() {
|
||||
const selected = getSelectedCodes();
|
||||
const info = document.getElementById('selectionInfo');
|
||||
const card = document.getElementById('assignActionCard');
|
||||
|
||||
if (!selected.length) {
|
||||
info.style.display = 'none';
|
||||
card.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
info.style.display = '';
|
||||
info.innerHTML = `<strong>${selected.length}</strong> client${selected.length === 1 ? '' : 's'} selected`;
|
||||
card.style.display = '';
|
||||
}
|
||||
|
||||
async function doAssign() {
|
||||
const selected = getSelectedCodes();
|
||||
const coachID = document.getElementById('targetCoachSelect').value;
|
||||
|
||||
if (!selected.length) { showToast('Select at least one client', 'error'); return; }
|
||||
if (!coachID) { showToast('Select a target coach', 'error'); return; }
|
||||
|
||||
const coachName = coaches.find(c => c.coachID === coachID)?.username || coachID;
|
||||
if (!confirm(`Assign ${selected.length} client(s) to ${coachName}?`)) return;
|
||||
|
||||
const btn = document.getElementById('assignBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Assigning...';
|
||||
|
||||
try {
|
||||
const data = await apiPost('assignments.php', { clientCodes: selected, coachID });
|
||||
if (data.success) {
|
||||
// Update local data
|
||||
selected.forEach(code => {
|
||||
const c = clients.find(x => x.clientCode === code);
|
||||
if (c) {
|
||||
c.coachID = coachID;
|
||||
c.coachUsername = coachName;
|
||||
}
|
||||
});
|
||||
showToast(`${data.assigned} client(s) assigned to ${coachName}`, 'success');
|
||||
renderAssignments();
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Assign';
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
150
website/js/pages/dashboard.js
Normal file
150
website/js/pages/dashboard.js
Normal file
@ -0,0 +1,150 @@
|
||||
import { apiGet, apiPut, apiDelete } from '../api.js';
|
||||
import { canEdit, showToast, getRole } from '../app.js';
|
||||
import { navigate } from '../router.js';
|
||||
|
||||
export async function dashboardPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Questionnaires</h1>
|
||||
<div class="actions" id="headerActions"></div>
|
||||
</div>
|
||||
<div id="qGrid" class="q-grid"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
if (canEdit()) {
|
||||
document.getElementById('headerActions').innerHTML = `
|
||||
<a href="#/questionnaire/new" class="btn btn-primary">+ New Questionnaire</a>
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiGet('questionnaires.php');
|
||||
renderGrid(data.questionnaires || []);
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('qGrid').innerHTML = `<p class="error-text">${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderGrid(questionnaires) {
|
||||
const grid = document.getElementById('qGrid');
|
||||
|
||||
if (!questionnaires.length) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 12h6m-3-3v6m-7 4h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
<h3>No questionnaires yet</h3>
|
||||
<p>Create your first questionnaire to get started.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = questionnaires.map(q => {
|
||||
const stateClass = `badge-${(q.state || 'draft').toLowerCase().replace(/\s+/g, '_')}`;
|
||||
const showPts = parseInt(q.showPoints || 0, 10);
|
||||
return `
|
||||
<div class="q-card" data-id="${q.questionnaireID}" draggable="${canEdit()}">
|
||||
<div class="q-card-header">
|
||||
<div class="q-card-title">
|
||||
${canEdit() ? '<span class="drag-handle" title="Drag to reorder" style="margin-right:6px;cursor:grab;color:var(--text-secondary)">☰</span>' : ''}
|
||||
${esc(q.name)}
|
||||
</div>
|
||||
<span class="badge ${stateClass}">${esc(q.state || 'draft')}</span>
|
||||
</div>
|
||||
<div class="q-card-meta">
|
||||
<span>v${esc(q.version || '—')}</span>
|
||||
<span>${q.questionCount || 0} question${q.questionCount == 1 ? '' : 's'}</span>
|
||||
<span>#${q.orderIndex ?? '—'}</span>
|
||||
${showPts ? '<span class="badge badge-active" style="font-size:.7rem;padding:1px 6px">pts</span>' : ''}
|
||||
</div>
|
||||
<div class="q-card-actions">
|
||||
<a href="#/questionnaire/${q.questionnaireID}" class="btn btn-sm">Edit</a>
|
||||
<a href="#/questionnaire/${q.questionnaireID}/results" class="btn btn-sm">Results</a>
|
||||
${getRole() === 'admin' ? `<button class="btn btn-sm btn-danger delete-q-btn" data-id="${q.questionnaireID}">Delete</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
grid.querySelectorAll('.q-card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.q-card-actions') || e.target.closest('.drag-handle')) return;
|
||||
navigate(`#/questionnaire/${card.dataset.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
grid.querySelectorAll('.delete-q-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm('Delete this questionnaire and all its data?')) return;
|
||||
try {
|
||||
await apiDelete('questionnaires.php', { questionnaireID: btn.dataset.id });
|
||||
showToast('Questionnaire deleted', 'success');
|
||||
btn.closest('.q-card').remove();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (canEdit()) initGridDrag(questionnaires);
|
||||
}
|
||||
|
||||
function initGridDrag(questionnaires) {
|
||||
const grid = document.getElementById('qGrid');
|
||||
if (!grid) return;
|
||||
let dragItem = null;
|
||||
|
||||
grid.addEventListener('dragstart', (e) => {
|
||||
const card = e.target.closest('.q-card');
|
||||
if (!card || !e.target.closest('.drag-handle')) { e.preventDefault(); return; }
|
||||
dragItem = card;
|
||||
card.style.opacity = '0.5';
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
|
||||
grid.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragItem) return;
|
||||
const cards = [...grid.querySelectorAll('.q-card:not([style*="opacity"])')];
|
||||
const afterCard = cards.reduce((closest, child) => {
|
||||
const box = child.getBoundingClientRect();
|
||||
const offset = e.clientY - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) return { offset, element: child };
|
||||
return closest;
|
||||
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
||||
|
||||
if (afterCard) grid.insertBefore(dragItem, afterCard);
|
||||
else grid.appendChild(dragItem);
|
||||
});
|
||||
|
||||
grid.addEventListener('dragend', async () => {
|
||||
if (!dragItem) return;
|
||||
dragItem.style.opacity = '';
|
||||
const newOrder = [...grid.querySelectorAll('.q-card')].map(el => el.dataset.id);
|
||||
dragItem = null;
|
||||
|
||||
for (let i = 0; i < newOrder.length; i++) {
|
||||
const id = newOrder[i];
|
||||
const q = questionnaires.find(q => q.questionnaireID === id);
|
||||
if (q && parseInt(q.orderIndex, 10) !== i) {
|
||||
try {
|
||||
await apiPut('questionnaires.php', { questionnaireID: id, orderIndex: i });
|
||||
q.orderIndex = i;
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
showToast('Order saved', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
1272
website/js/pages/editor.js
Normal file
1272
website/js/pages/editor.js
Normal file
File diff suppressed because it is too large
Load Diff
104
website/js/pages/export.js
Normal file
104
website/js/pages/export.js
Normal file
@ -0,0 +1,104 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
|
||||
export async function exportPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Export Data</h1>
|
||||
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
||||
</div>
|
||||
<div class="card" id="exportContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await apiGet('questionnaires.php');
|
||||
renderExport(data.questionnaires || []);
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('exportContent').innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderExport(questionnaires) {
|
||||
const container = document.getElementById('exportContent');
|
||||
|
||||
if (!questionnaires.length) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No questionnaires</h3>
|
||||
<p>Create questionnaires first to export data.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<p style="margin-bottom:16px;color:var(--text-secondary)">
|
||||
Select a questionnaire and click Export to download a CSV file with all client responses (filtered by your role's visibility).
|
||||
</p>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Questionnaire</th>
|
||||
<th>Version</th>
|
||||
<th>State</th>
|
||||
<th>Questions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${questionnaires.map(q => `
|
||||
<tr>
|
||||
<td><strong>${esc(q.name)}</strong></td>
|
||||
<td>${esc(q.version || '—')}</td>
|
||||
<td><span class="badge badge-${(q.state || 'draft').toLowerCase()}">${esc(q.state || 'draft')}</span></td>
|
||||
<td>${q.questionCount || 0}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary export-btn" data-id="${q.questionnaireID}" data-name="${esc(q.name)}" data-version="${esc(q.version)}">
|
||||
Export CSV
|
||||
</button>
|
||||
<a href="#/questionnaire/${q.questionnaireID}/results" class="btn btn-sm">View Results</a>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.querySelectorAll('.export-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Downloading...';
|
||||
try {
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
const res = await fetch(`../api/export.php?questionnaireID=${encodeURIComponent(btn.dataset.id)}&format=csv`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: 'Download failed' }));
|
||||
throw new Error(err.error);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${btn.dataset.name}_v${btn.dataset.version}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('Download started', 'success');
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Export CSV';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
124
website/js/pages/login.js
Normal file
124
website/js/pages/login.js
Normal file
@ -0,0 +1,124 @@
|
||||
import { navigate } from '../router.js';
|
||||
import { isLoggedIn, showToast } from '../app.js';
|
||||
|
||||
export function loginPage() {
|
||||
if (isLoggedIn()) { navigate('#/'); return; }
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="login-wrapper">
|
||||
<div class="login-card card">
|
||||
<h1>BW Schützt</h1>
|
||||
<p class="subtitle">Questionnaire Management</p>
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Log in</button>
|
||||
<p id="loginError" class="error-text" style="display:none"></p>
|
||||
</form>
|
||||
<div id="changePwSection" style="display:none">
|
||||
<hr>
|
||||
<p>You must change your password before continuing.</p>
|
||||
<form id="changePwForm">
|
||||
<div class="form-group">
|
||||
<label for="newPw">New password</label>
|
||||
<input type="password" id="newPw" autocomplete="new-password" required minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPwConfirm">Confirm password</label>
|
||||
<input type="password" id="newPwConfirm" autocomplete="new-password" required minlength="6">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Change & continue</button>
|
||||
<p id="changePwError" class="error-text" style="display:none"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let pendingUsername = '';
|
||||
let pendingTempToken = '';
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const errEl = document.getElementById('loginError');
|
||||
errEl.style.display = 'none';
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const res = await fetch('../api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
errEl.textContent = json.error?.message || 'Login failed';
|
||||
errEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
const d = json.data;
|
||||
if (d.mustChangePassword) {
|
||||
pendingUsername = username;
|
||||
pendingTempToken = d.token;
|
||||
document.getElementById('loginForm').style.display = 'none';
|
||||
document.getElementById('changePwSection').style.display = '';
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('qdb_token', d.token);
|
||||
localStorage.setItem('qdb_user', d.user);
|
||||
localStorage.setItem('qdb_role', d.role);
|
||||
navigate('#/');
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('changePwForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const errEl = document.getElementById('changePwError');
|
||||
errEl.style.display = 'none';
|
||||
const newPw = document.getElementById('newPw').value;
|
||||
const confirm = document.getElementById('newPwConfirm').value;
|
||||
if (newPw !== confirm) {
|
||||
errEl.textContent = 'Passwords do not match';
|
||||
errEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
const oldPw = document.getElementById('password').value;
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (pendingTempToken) {
|
||||
headers['Authorization'] = `Bearer ${pendingTempToken}`;
|
||||
}
|
||||
const res = await fetch('../api/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ username: pendingUsername, old_password: oldPw, new_password: newPw }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
errEl.textContent = json.error?.message || 'Password change failed';
|
||||
errEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
const d = json.data;
|
||||
localStorage.setItem('qdb_token', d.token);
|
||||
localStorage.setItem('qdb_user', d.user);
|
||||
localStorage.setItem('qdb_role', d.role);
|
||||
showToast('Password changed successfully', 'success');
|
||||
navigate('#/');
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
264
website/js/pages/results.js
Normal file
264
website/js/pages/results.js
Normal file
@ -0,0 +1,264 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
|
||||
let sortCol = null;
|
||||
let sortDir = 'asc';
|
||||
let allClients = [];
|
||||
let questionsDef = [];
|
||||
let questionnaireMeta = null;
|
||||
let filterCoach = '';
|
||||
let filterStatus = '';
|
||||
|
||||
export async function resultsPage(params) {
|
||||
const app = document.getElementById('app');
|
||||
sortCol = null;
|
||||
sortDir = 'asc';
|
||||
filterCoach = '';
|
||||
filterStatus = '';
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 id="resultsTitle">Results</h1>
|
||||
<div class="actions">
|
||||
<a href="#/questionnaire/${params.id}" class="btn">← Back to Editor</a>
|
||||
<a href="#/" class="btn">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resultsContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await apiGet(`results.php?questionnaireID=${encodeURIComponent(params.id)}`);
|
||||
questionnaireMeta = data.questionnaire;
|
||||
questionsDef = data.questions || [];
|
||||
allClients = data.clients || [];
|
||||
document.getElementById('resultsTitle').textContent = `Results: ${questionnaireMeta.name} v${questionnaireMeta.version}`;
|
||||
renderResults();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('resultsContent').innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
const container = document.getElementById('resultsContent');
|
||||
|
||||
// Collect unique coaches and statuses for filters
|
||||
const coaches = [...new Set(allClients.map(c => c.coachID))].sort();
|
||||
const statuses = [...new Set(allClients.map(c => c.status))].filter(Boolean).sort();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="filter-bar">
|
||||
<label style="font-size:.85rem;font-weight:500">Filter:</label>
|
||||
<select id="filterCoach">
|
||||
<option value="">All coaches</option>
|
||||
${coaches.map(c => `<option value="${esc(c)}" ${filterCoach === c ? 'selected' : ''}>${esc(c)}</option>`).join('')}
|
||||
</select>
|
||||
<select id="filterStatus">
|
||||
<option value="">All statuses</option>
|
||||
${statuses.map(s => `<option value="${esc(s)}" ${filterStatus === s ? 'selected' : ''}>${esc(s)}</option>`).join('')}
|
||||
</select>
|
||||
<span style="margin-left:auto;font-size:.85rem;color:var(--text-secondary)" id="resultCount"></span>
|
||||
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" id="resultsTable">
|
||||
<thead><tr id="resultsHead"></tr></thead>
|
||||
<tbody id="resultsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('filterCoach').addEventListener('change', (e) => {
|
||||
filterCoach = e.target.value;
|
||||
renderTableRows();
|
||||
});
|
||||
document.getElementById('filterStatus').addEventListener('change', (e) => {
|
||||
filterStatus = e.target.value;
|
||||
renderTableRows();
|
||||
});
|
||||
document.getElementById('exportCsvLink').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
exportCSV();
|
||||
});
|
||||
|
||||
renderTableHead();
|
||||
renderTableRows();
|
||||
}
|
||||
|
||||
function renderTableHead() {
|
||||
const head = document.getElementById('resultsHead');
|
||||
const fixedCols = [
|
||||
{ key: 'clientCode', label: 'Client' },
|
||||
{ key: 'coachID', label: 'Coach' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'sumPoints', label: 'Score' },
|
||||
{ key: 'completedAt', label: 'Completed' },
|
||||
];
|
||||
const questionCols = questionsDef.map(q => ({
|
||||
key: `q_${q.questionID}`,
|
||||
label: q.defaultText,
|
||||
questionID: q.questionID,
|
||||
}));
|
||||
const allCols = [...fixedCols, ...questionCols];
|
||||
|
||||
head.innerHTML = allCols.map(col => {
|
||||
let cls = 'sortable';
|
||||
if (sortCol === col.key) cls += sortDir === 'asc' ? ' sort-asc' : ' sort-desc';
|
||||
return `<th class="${cls}" data-key="${col.key}" title="${esc(col.label)}">${esc(col.label)}</th>`;
|
||||
}).join('');
|
||||
|
||||
head.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const key = th.dataset.key;
|
||||
if (sortCol === key) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||
else { sortCol = key; sortDir = 'asc'; }
|
||||
renderTableHead();
|
||||
renderTableRows();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getFilteredClients() {
|
||||
let clients = allClients;
|
||||
if (filterCoach) clients = clients.filter(c => c.coachID === filterCoach);
|
||||
if (filterStatus) clients = clients.filter(c => c.status === filterStatus);
|
||||
return clients;
|
||||
}
|
||||
|
||||
function renderTableRows() {
|
||||
const body = document.getElementById('resultsBody');
|
||||
let clients = getFilteredClients();
|
||||
|
||||
// Sort
|
||||
if (sortCol) {
|
||||
clients = [...clients].sort((a, b) => {
|
||||
let va = getCellValue(a, sortCol);
|
||||
let vb = getCellValue(b, sortCol);
|
||||
if (va == null) va = '';
|
||||
if (vb == null) vb = '';
|
||||
if (typeof va === 'number' && typeof vb === 'number') {
|
||||
return sortDir === 'asc' ? va - vb : vb - va;
|
||||
}
|
||||
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
|
||||
return sortDir === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('resultCount').textContent = `${clients.length} client${clients.length === 1 ? '' : 's'}`;
|
||||
|
||||
if (!clients.length) {
|
||||
body.innerHTML = `<tr><td colspan="99" style="text-align:center;color:var(--text-secondary);padding:30px">No results found</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build option text lookup
|
||||
const optionMap = {};
|
||||
questionsDef.forEach(q => {
|
||||
(q.answerOptions || []).forEach(o => {
|
||||
optionMap[o.answerOptionID] = o.defaultText;
|
||||
});
|
||||
});
|
||||
|
||||
body.innerHTML = clients.map(c => {
|
||||
const completedDate = c.completedAt ? new Date(c.completedAt * 1000).toLocaleDateString() : '—';
|
||||
const statusBadge = c.status ? `<span class="badge badge-${c.status.toLowerCase().replace(/\s+/g,'_')}">${esc(c.status)}</span>` : '—';
|
||||
|
||||
const questionCells = questionsDef.map(q => {
|
||||
const ans = c.answers && c.answers[q.questionID];
|
||||
if (!ans) return '<td>—</td>';
|
||||
if (ans.answerOptionID && optionMap[ans.answerOptionID]) {
|
||||
return `<td>${esc(optionMap[ans.answerOptionID])}</td>`;
|
||||
}
|
||||
if (ans.freeTextValue) return `<td>${esc(ans.freeTextValue)}</td>`;
|
||||
if (ans.numericValue !== null && ans.numericValue !== undefined) return `<td>${ans.numericValue}</td>`;
|
||||
return '<td>—</td>';
|
||||
}).join('');
|
||||
|
||||
return `<tr>
|
||||
<td>${esc(c.clientCode)}</td>
|
||||
<td>${esc(c.coachID)}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${c.sumPoints !== null ? c.sumPoints : '—'}</td>
|
||||
<td>${completedDate}</td>
|
||||
${questionCells}
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getCellValue(client, key) {
|
||||
if (key === 'clientCode') return client.clientCode;
|
||||
if (key === 'coachID') return client.coachID;
|
||||
if (key === 'status') return client.status;
|
||||
if (key === 'sumPoints') return client.sumPoints;
|
||||
if (key === 'completedAt') return client.completedAt;
|
||||
if (key.startsWith('q_')) {
|
||||
const qid = key.substring(2);
|
||||
const ans = client.answers && client.answers[qid];
|
||||
if (!ans) return null;
|
||||
if (ans.numericValue !== null && ans.numericValue !== undefined) return ans.numericValue;
|
||||
if (ans.freeTextValue) return ans.freeTextValue;
|
||||
return ans.answerOptionID || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
const clients = getFilteredClients();
|
||||
const optionMap = {};
|
||||
questionsDef.forEach(q => {
|
||||
(q.answerOptions || []).forEach(o => {
|
||||
optionMap[o.answerOptionID] = o.defaultText;
|
||||
});
|
||||
});
|
||||
|
||||
const headers = ['clientCode', 'coachID', 'status', 'sumPoints', 'completedAt',
|
||||
...questionsDef.map(q => q.defaultText)];
|
||||
|
||||
const rows = clients.map(c => {
|
||||
const base = [
|
||||
c.clientCode, c.coachID, c.status, c.sumPoints ?? '',
|
||||
c.completedAt ? new Date(c.completedAt * 1000).toISOString() : ''
|
||||
];
|
||||
const answerCols = questionsDef.map(q => {
|
||||
const ans = c.answers && c.answers[q.questionID];
|
||||
if (!ans) return '';
|
||||
if (ans.answerOptionID && optionMap[ans.answerOptionID]) return optionMap[ans.answerOptionID];
|
||||
if (ans.freeTextValue) return ans.freeTextValue;
|
||||
if (ans.numericValue !== null && ans.numericValue !== undefined) return ans.numericValue;
|
||||
return '';
|
||||
});
|
||||
return [...base, ...answerCols];
|
||||
});
|
||||
|
||||
let csv = '\uFEFF'; // BOM
|
||||
csv += headers.map(csvEsc).join(',') + '\n';
|
||||
rows.forEach(r => { csv += r.map(csvEsc).join(',') + '\n'; });
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${questionnaireMeta.name}_v${questionnaireMeta.version}_results.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('CSV exported', 'success');
|
||||
}
|
||||
|
||||
function csvEsc(val) {
|
||||
const s = String(val ?? '');
|
||||
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
||||
return '"' + s.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
326
website/js/pages/users.js
Normal file
326
website/js/pages/users.js
Normal file
@ -0,0 +1,326 @@
|
||||
import { apiGet, apiPost, apiDelete } from '../api.js';
|
||||
import { getRole, getUser, showToast } from '../app.js';
|
||||
|
||||
// Roles an admin can create; supervisors can only create coaches
|
||||
const CREATEABLE_ROLES = {
|
||||
admin: ['admin', 'supervisor', 'coach'],
|
||||
supervisor: ['coach'],
|
||||
};
|
||||
|
||||
let usersList = [];
|
||||
let supervisorsList = [];
|
||||
let callerRole = '';
|
||||
|
||||
export async function usersPage() {
|
||||
callerRole = getRole();
|
||||
const app = document.getElementById('app');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Users</h1>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="showCreateFormBtn">+ Create User</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="createUserWrapper" style="display:none"></div>
|
||||
<div id="usersContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
document.getElementById('showCreateFormBtn').addEventListener('click', () => {
|
||||
const wrapper = document.getElementById('createUserWrapper');
|
||||
if (wrapper.style.display === 'none') {
|
||||
showCreateForm();
|
||||
} else {
|
||||
hideCreateForm();
|
||||
}
|
||||
});
|
||||
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const data = await apiGet('users.php');
|
||||
usersList = data.users || [];
|
||||
supervisorsList = data.supervisors || [];
|
||||
callerRole = data.callerRole || callerRole;
|
||||
renderUsers();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('usersContent').innerHTML =
|
||||
`<p class="error-text" style="padding:16px">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── User table ────────────────────────────────────────────────────────────
|
||||
|
||||
function renderUsers() {
|
||||
const container = document.getElementById('usersContent');
|
||||
const myUsername = getUser();
|
||||
|
||||
if (!usersList.length) {
|
||||
container.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="empty-state">
|
||||
<h3>No users found</h3>
|
||||
<p>${callerRole === 'supervisor' ? 'You have no coaches yet.' : 'Create the first user above.'}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by role for a cleaner display
|
||||
const byRole = { admin: [], supervisor: [], coach: [] };
|
||||
usersList.forEach(u => (byRole[u.role] || (byRole.other = byRole.other || [])).push(u));
|
||||
|
||||
// Which role groups to show
|
||||
const groups = callerRole === 'supervisor'
|
||||
? [{ label: 'Coaches', key: 'coach' }]
|
||||
: [
|
||||
{ label: 'Admins', key: 'admin' },
|
||||
{ label: 'Supervisors', key: 'supervisor' },
|
||||
{ label: 'Coaches', key: 'coach' },
|
||||
];
|
||||
|
||||
container.innerHTML = groups.map(group => {
|
||||
const users = byRole[group.key] || [];
|
||||
if (!users.length) return '';
|
||||
return `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3 style="margin-bottom:12px">${group.label} (${users.length})</h3>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
${group.key !== 'coach' ? '<th>Location</th>' : '<th>Supervisor</th>'}
|
||||
<th>Password Change Required</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${users.map(u => userRowHTML(u, myUsername)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
container.querySelectorAll('.delete-user-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteUser(btn.dataset.id, btn.dataset.name));
|
||||
});
|
||||
}
|
||||
|
||||
function userRowHTML(u, myUsername) {
|
||||
const isSelf = u.username === myUsername;
|
||||
const detail = u.role === 'coach'
|
||||
? esc(u.supervisorUsername || '—')
|
||||
: esc(u.location || '—');
|
||||
const created = u.createdAt
|
||||
? new Date(parseInt(u.createdAt, 10) * 1000).toLocaleDateString()
|
||||
: '—';
|
||||
const pwBadge = u.mustChangePassword == 1
|
||||
? '<span class="badge badge-pending">Pending</span>'
|
||||
: '<span style="color:var(--text-secondary);font-size:.8rem">No</span>';
|
||||
|
||||
const canDelete = !isSelf && (
|
||||
callerRole === 'admin' ||
|
||||
(callerRole === 'supervisor' && u.role === 'coach')
|
||||
);
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${esc(u.username)}</strong>
|
||||
${isSelf ? '<span class="badge badge-you">You</span>' : ''}
|
||||
</td>
|
||||
<td>${detail}</td>
|
||||
<td>${pwBadge}</td>
|
||||
<td>${created}</td>
|
||||
<td>
|
||||
${canDelete
|
||||
? `<button class="btn btn-sm btn-danger delete-user-btn" data-id="${u.userID}" data-name="${esc(u.username)}">Delete</button>`
|
||||
: ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
async function deleteUser(userID, username) {
|
||||
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
const data = await apiDelete('users.php', { userID });
|
||||
if (data.success) {
|
||||
usersList = usersList.filter(u => u.userID !== userID);
|
||||
showToast(`User "${username}" deleted`, 'success');
|
||||
renderUsers();
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create user form ──────────────────────────────────────────────────────
|
||||
|
||||
function showCreateForm() {
|
||||
const wrapper = document.getElementById('createUserWrapper');
|
||||
const allowedRoles = CREATEABLE_ROLES[callerRole] || ['coach'];
|
||||
const isSupervisor = callerRole === 'supervisor';
|
||||
|
||||
wrapper.style.display = '';
|
||||
wrapper.innerHTML = `
|
||||
<div class="inline-form-card" style="margin-bottom:20px">
|
||||
<h4>Create New User</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:2">
|
||||
<label>Username</label>
|
||||
<input type="text" id="cu_username" autocomplete="off" placeholder="Enter username">
|
||||
</div>
|
||||
<div class="form-group" style="flex:2">
|
||||
<label>Password</label>
|
||||
<input type="password" id="cu_password" autocomplete="new-password" placeholder="Min 6 characters">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1;min-width:160px">
|
||||
<label>Role</label>
|
||||
${isSupervisor
|
||||
? `<input type="text" value="Coach" disabled style="background:#f8fafc">`
|
||||
: `<select id="cu_role">
|
||||
${allowedRoles.map(r =>
|
||||
`<option value="${r}">${r.charAt(0).toUpperCase() + r.slice(1)}</option>`
|
||||
).join('')}
|
||||
</select>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cu_location_row" class="form-group" ${isSupervisor ? 'style="display:none"' : ''}>
|
||||
<label>Location <span style="font-weight:400;color:var(--text-secondary)">(for admin/supervisor)</span></label>
|
||||
<input type="text" id="cu_location" placeholder="e.g. Stuttgart">
|
||||
</div>
|
||||
|
||||
<div id="cu_supervisor_row" class="form-group" style="${isSupervisor ? '' : 'display:none'}">
|
||||
<label>Supervisor</label>
|
||||
${isSupervisor
|
||||
? `<input type="text" value="You (auto-assigned)" disabled style="background:#f8fafc">`
|
||||
: `<select id="cu_supervisor_select">
|
||||
<option value="">— select supervisor —</option>
|
||||
${supervisorsList.map(s =>
|
||||
`<option value="${s.supervisorID}">${esc(s.username)}${s.location ? ` (${esc(s.location)})` : ''}</option>`
|
||||
).join('')}
|
||||
</select>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<label class="checkbox-label" style="margin-bottom:14px;display:inline-flex">
|
||||
<input type="checkbox" id="cu_mustchange" checked> Require password change on first login
|
||||
</label>
|
||||
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" id="cu_submit">Create User</button>
|
||||
<button class="btn btn-sm" id="cu_cancel">Cancel</button>
|
||||
</div>
|
||||
<p id="cu_error" class="error-text" style="display:none;margin-top:8px"></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('cu_username').focus();
|
||||
|
||||
// Show/hide location vs supervisor field based on role selection (admin only)
|
||||
if (!isSupervisor) {
|
||||
document.getElementById('cu_role').addEventListener('change', (e) => {
|
||||
const r = e.target.value;
|
||||
document.getElementById('cu_location_row').style.display = r !== 'coach' ? '' : 'none';
|
||||
document.getElementById('cu_supervisor_row').style.display = r === 'coach' ? '' : 'none';
|
||||
});
|
||||
// Set initial state based on default selection
|
||||
const initRole = document.getElementById('cu_role').value;
|
||||
document.getElementById('cu_location_row').style.display = initRole !== 'coach' ? '' : 'none';
|
||||
document.getElementById('cu_supervisor_row').style.display = initRole === 'coach' ? '' : 'none';
|
||||
}
|
||||
|
||||
document.getElementById('cu_submit').addEventListener('click', submitCreateUser);
|
||||
document.getElementById('cu_cancel').addEventListener('click', hideCreateForm);
|
||||
|
||||
// Enter key on last visible field submits
|
||||
wrapper.querySelectorAll('input').forEach(inp => {
|
||||
inp.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') submitCreateUser();
|
||||
if (e.key === 'Escape') hideCreateForm();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function hideCreateForm() {
|
||||
const wrapper = document.getElementById('createUserWrapper');
|
||||
if (wrapper) { wrapper.style.display = 'none'; wrapper.innerHTML = ''; }
|
||||
}
|
||||
|
||||
async function submitCreateUser() {
|
||||
const errEl = document.getElementById('cu_error');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const isSupervisor = callerRole === 'supervisor';
|
||||
const username = document.getElementById('cu_username').value.trim();
|
||||
const password = document.getElementById('cu_password').value;
|
||||
const role = isSupervisor ? 'coach' : (document.getElementById('cu_role').value || 'coach');
|
||||
const mustChange = document.getElementById('cu_mustchange').checked ? 1 : 0;
|
||||
|
||||
let location = '';
|
||||
let supervisorID = '';
|
||||
if (!isSupervisor) {
|
||||
if (role !== 'coach') {
|
||||
location = document.getElementById('cu_location').value.trim();
|
||||
} else {
|
||||
supervisorID = document.getElementById('cu_supervisor_select').value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!username) { showError(errEl, 'Username is required'); return; }
|
||||
if (password.length < 6) { showError(errEl, 'Password must be at least 6 characters'); return; }
|
||||
if (role === 'coach' && !isSupervisor && !supervisorID) {
|
||||
showError(errEl, 'Please select a supervisor for this coach');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('cu_submit');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creating...';
|
||||
|
||||
try {
|
||||
const data = await apiPost('users.php', {
|
||||
username, password, role, location, supervisorID, mustChangePassword: mustChange,
|
||||
});
|
||||
if (!data.success) throw new Error(data.error || 'Failed to create user');
|
||||
|
||||
// Add to local list and re-render
|
||||
usersList.push({
|
||||
userID: data.userID,
|
||||
username: data.username,
|
||||
role: data.role,
|
||||
entityID: data.entityID,
|
||||
location: data.location || '',
|
||||
supervisorID: data.supervisorID || null,
|
||||
supervisorUsername: data.supervisorUsername || null,
|
||||
mustChangePassword: data.mustChangePassword,
|
||||
createdAt: data.createdAt,
|
||||
});
|
||||
showToast(`User "${data.username}" created`, 'success');
|
||||
hideCreateForm();
|
||||
renderUsers();
|
||||
} catch (e) {
|
||||
showError(errEl, e.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Create User';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(el, msg) {
|
||||
el.textContent = msg;
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
56
website/js/router.js
Normal file
56
website/js/router.js
Normal file
@ -0,0 +1,56 @@
|
||||
const routes = [];
|
||||
let currentCleanup = null;
|
||||
|
||||
export function addRoute(pattern, handler) {
|
||||
let regex;
|
||||
const paramNames = [];
|
||||
if (pattern instanceof RegExp) {
|
||||
regex = pattern;
|
||||
} else {
|
||||
const parts = pattern.replace(/\/:([^/]+)/g, (_, name) => {
|
||||
paramNames.push(name);
|
||||
return '/([^/]+)';
|
||||
});
|
||||
regex = new RegExp('^' + parts + '$');
|
||||
}
|
||||
routes.push({ regex, paramNames, handler });
|
||||
}
|
||||
|
||||
export function navigate(hash) {
|
||||
window.location.hash = hash;
|
||||
}
|
||||
|
||||
export function currentHash() {
|
||||
return window.location.hash.slice(1) || '/';
|
||||
}
|
||||
|
||||
async function resolve() {
|
||||
if (typeof currentCleanup === 'function') {
|
||||
currentCleanup();
|
||||
currentCleanup = null;
|
||||
}
|
||||
|
||||
const path = currentHash();
|
||||
for (const route of routes) {
|
||||
const match = path.match(route.regex);
|
||||
if (match) {
|
||||
const params = {};
|
||||
route.paramNames.forEach((name, i) => {
|
||||
params[name] = decodeURIComponent(match[i + 1]);
|
||||
});
|
||||
try {
|
||||
currentCleanup = await route.handler(params) || null;
|
||||
} catch (e) {
|
||||
console.error('Route handler error:', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// fallback: dashboard
|
||||
navigate('#/');
|
||||
}
|
||||
|
||||
export function startRouter() {
|
||||
window.addEventListener('hashchange', resolve);
|
||||
resolve();
|
||||
}
|
||||
136
website/temp-assets/questionnaire_0_readme.json
Normal file
136
website/temp-assets/questionnaire_0_readme.json
Normal file
@ -0,0 +1,136 @@
|
||||
{
|
||||
"meta": {
|
||||
"_comment": "Diese Zeiel erklärt im folgenden wie die einzelnen Fragetypen definiert werden können und was für features diese mit sich bringen.",
|
||||
"id": "questionnaire_0_readme"
|
||||
},
|
||||
"questions": [
|
||||
|
||||
|
||||
{
|
||||
"id": "q1",
|
||||
"_comment": "Frage mit Radio Buttons. In diesem Fragetypen kann man maximal eine Antwort wählen. Über nextQuestionId kann man die nächste Frage definieren., Über pointsMap kann man die Punkte für die Antwort der Frage festlegen.",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_instruction",
|
||||
"options": [
|
||||
{ "key": "consent_signed"},
|
||||
{ "key": "consent_not_signed",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q2",
|
||||
"_comment": "Frage nach dem Code des Coaches.",
|
||||
"layout": "client_not_signed",
|
||||
"textKey1": "no_consent_entered",
|
||||
"textKey2": "no_consent_note",
|
||||
"question": "coach_code_request",
|
||||
"hint": "coach_code"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q3",
|
||||
"_comment": "Frage mit Spinner, dessen Antwortmöglichkeiten über options selbst definiert werden können.",
|
||||
"layout": "string_spinner",
|
||||
"question": "other_accommodation",
|
||||
"options": ["Unterkunft 1", "Unterkunft 2", "Unterkunft 3"]
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q4",
|
||||
"_comment": "Frage mit Spinner zur Auswahl des Datums",
|
||||
"layout": "date_spinner",
|
||||
"question": "departure_country"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q5",
|
||||
"_comment": "Frage mit Spinner zur Auswahl des Datums. Über constraints und notBefore und notAfter, ist es möglich das Datum nicht in der Zukunft oder Vergangenheit zu wählen.",
|
||||
"layout": "date_spinner",
|
||||
"question": "since_in_germany",
|
||||
"constraints": {
|
||||
"notBefore": "departure_country",
|
||||
"notAfter": "departure_country"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q6",
|
||||
"_comment": "Frage mit Spinner, dessen Antwortmöglichkeiten über range selbst definiert werden können.",
|
||||
"layout": "value_spinner",
|
||||
"question": "number_family_members",
|
||||
"range": {
|
||||
"min": 1,
|
||||
"max": 15
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q7",
|
||||
"_comment": "Frage nach dem Code des Klienten und den Code des Coaches.",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q8",
|
||||
"_comment": "Frage mit Checkboxen, dessen Antwortmöglichkeiten über options selbst definiert werden können. Über pointsMap können die Punkte für die jeweiligen Antworten festgelegt werden. Über minSelection kann die minimal Anzahl an auszufüllenden Antworten festgelegt werden.",
|
||||
"layout": "multi_check_box_question",
|
||||
"question": "languages_spoken",
|
||||
"options": [
|
||||
{ "key": "language_albanian" },
|
||||
{ "key": "language_pashto" },
|
||||
{ "key": "language_russian" },
|
||||
{ "key": "language_other"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"language_albanian": 1,
|
||||
"language_pashto": 1,
|
||||
"language_russian": 1,
|
||||
"language_other": 1
|
||||
},
|
||||
"minSelection": 1
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "q9",
|
||||
"_comment":"Glass scale frage, bei der es für jede Frage, welche über symptoms festgelegt werden kann eine Antwort festgelegt werden muss. Die Antwort ganz links gibt immer 0 Punkte und die Antwort ganz rechts gibt immer 4 Punkte.",
|
||||
"layout": "glass_scale_question",
|
||||
"question": "glass_explanation",
|
||||
"symptoms": [
|
||||
"q1_symptom",
|
||||
"q2_symptom",
|
||||
"q3_symptom",
|
||||
"q4_symptom",
|
||||
"q5_symptom",
|
||||
"q6_symptom",
|
||||
"q7_symptom",
|
||||
"q8_symptom",
|
||||
"q9_symptom"
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "last_page",
|
||||
"_comment":"Letzte Seite, welche den Abschluss des Fragebogens markiert.",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
302
website/temp-assets/questionnaire_1_demographic_information.json
Normal file
302
website/temp-assets/questionnaire_1_demographic_information.json
Normal file
@ -0,0 +1,302 @@
|
||||
{
|
||||
"meta": {
|
||||
"id": "questionnaire_1_demographic_information"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": "q1",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_instruction",
|
||||
"options": [
|
||||
{ "key": "consent_signed",
|
||||
"nextQuestionId": "q6"},
|
||||
{ "key": "consent_not_signed",
|
||||
"nextQuestionId": "q2"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q2",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "no_consent_entered",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
},
|
||||
{
|
||||
"id": "q28",
|
||||
"layout": "radio_question",
|
||||
"question": "accommodation",
|
||||
"options": [
|
||||
{ "key": "steinstrasse",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "doerfle",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "zoll_emmishofer",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "other",
|
||||
"nextQuestionId": "q4"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q4",
|
||||
"layout": "string_spinner",
|
||||
"question": "other_accommodation",
|
||||
"options": ["Unterkunft 1", "Unterkunft 2", "Unterkunft 3"]
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
},
|
||||
{
|
||||
"id": "q6",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "q29",
|
||||
"layout": "radio_question",
|
||||
"question": "accommodation",
|
||||
"options": [
|
||||
{ "key": "steinstrasse",
|
||||
"nextQuestionId": "q9"},
|
||||
{ "key": "doerfle",
|
||||
"nextQuestionId": "q9"},
|
||||
{ "key": "zoll_emmishofer",
|
||||
"nextQuestionId": "q9"},
|
||||
{ "key": "other",
|
||||
"nextQuestionId": "q8"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q8",
|
||||
"layout": "string_spinner",
|
||||
"question": "other_accommodation",
|
||||
"options": ["Unterkunft 1", "Unterkunft 2", "Unterkunft 3"]
|
||||
},
|
||||
{
|
||||
"id": "q9",
|
||||
"layout": "value_spinner",
|
||||
"question": "age",
|
||||
"range": {
|
||||
"min": 18,
|
||||
"max": 122
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q10",
|
||||
"layout": "radio_question",
|
||||
"question": "gender",
|
||||
"options": [
|
||||
{ "key": "gender_male"},
|
||||
{ "key": "gender_female"},
|
||||
{ "key": "gender_diverse"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q11",
|
||||
"layout": "string_spinner",
|
||||
"question": "country_of_origin"
|
||||
},
|
||||
{
|
||||
"id": "q13",
|
||||
"layout": "date_spinner",
|
||||
"question": "departure_country"
|
||||
},
|
||||
{
|
||||
"id": "q14",
|
||||
"layout": "date_spinner",
|
||||
"question": "since_in_germany",
|
||||
"constraints": {
|
||||
"notBefore": "departure_country"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q16",
|
||||
"layout": "radio_question",
|
||||
"question": "living_situation",
|
||||
"options": [
|
||||
{ "key": "alone",
|
||||
"nextQuestionId": "q18"},
|
||||
{ "key": "with_family",
|
||||
"nextQuestionId": "q17"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q17",
|
||||
"layout": "value_spinner",
|
||||
"question": "number_family_members",
|
||||
"range": {
|
||||
"min": 1,
|
||||
"max": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q18",
|
||||
"layout": "multi_check_box_question",
|
||||
"question": "languages_spoken",
|
||||
"options": [
|
||||
{ "key": "language_albanian" },
|
||||
{ "key": "language_pashto" },
|
||||
{ "key": "language_russian" },
|
||||
{ "key": "language_serbo" },
|
||||
{ "key": "language_somali" },
|
||||
{ "key": "language_tamil" },
|
||||
{ "key": "language_tigrinya" },
|
||||
{ "key": "language_turkish" },
|
||||
{ "key": "language_ukrainian" },
|
||||
{ "key": "language_urdu" },
|
||||
{ "key": "language_arabic" },
|
||||
{ "key": "language_dari_farsi" },
|
||||
{ "key": "language_chinese" },
|
||||
{ "key": "language_english" },
|
||||
{ "key": "language_macedonian" },
|
||||
{ "key": "language_kurmanji" },
|
||||
{ "key": "language_hindi" },
|
||||
{ "key": "language_french"},
|
||||
{ "key": "language_other"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"language_albanian": 0,
|
||||
"language_pashto": 0,
|
||||
"language_russian": 0,
|
||||
"language_serbo": 0,
|
||||
"language_somali": 0,
|
||||
"language_tamil": 0,
|
||||
"language_tigrinya": 0,
|
||||
"language_turkish": 0,
|
||||
"language_ukrainian": 0,
|
||||
"language_urdu": 0,
|
||||
"language_arabic": 0,
|
||||
"language_dari_farsi": 0,
|
||||
"language_chinese": 0,
|
||||
"language_english": 0,
|
||||
"language_macedonian": 0,
|
||||
"language_kurmanji": 0,
|
||||
"language_hindi": 0,
|
||||
"language_french": 0,
|
||||
"language_other": 0
|
||||
},
|
||||
"minSelection": 1
|
||||
},
|
||||
{
|
||||
"id": "q19",
|
||||
"layout": "radio_question",
|
||||
"question": "german_skills",
|
||||
"options": [
|
||||
{ "key": "skill_none"},
|
||||
{ "key": "skill_basic"},
|
||||
{ "key": "skill_a1"},
|
||||
{ "key": "skill_a2"},
|
||||
{ "key": "skill_b1"},
|
||||
{ "key": "skill_b2"},
|
||||
{ "key": "skill_c1"},
|
||||
{ "key": "skill_c2"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q30",
|
||||
"layout": "value_spinner",
|
||||
"question": "school_years_total",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 15
|
||||
},
|
||||
"options": [
|
||||
{ "value": 0, "nextQuestionId": "q23" }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "q20",
|
||||
"layout": "value_spinner",
|
||||
"question": "school_years_origin",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q21",
|
||||
"layout": "value_spinner",
|
||||
"question": "school_years_transit",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q22",
|
||||
"layout": "value_spinner",
|
||||
"question": "school_years_germany",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q23",
|
||||
"layout": "radio_question",
|
||||
"question": "vocational_training",
|
||||
"options": [
|
||||
{ "key": "answer_no"},
|
||||
{ "key": "answer_started"},
|
||||
{ "key": "answer_completed"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_signed": 0,
|
||||
"consent_not_signed": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q24",
|
||||
"layout": "date_spinner",
|
||||
"question": "provisional_accommodation_since",
|
||||
"constraints": {
|
||||
"notBefore": "since_in_germany"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q25",
|
||||
"layout": "date_spinner",
|
||||
"question": "asylum_procedure_since",
|
||||
"constraints": {
|
||||
"notBefore": "since_in_germany"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
137
website/temp-assets/questionnaire_2_rhs.json
Normal file
137
website/temp-assets/questionnaire_2_rhs.json
Normal file
@ -0,0 +1,137 @@
|
||||
{
|
||||
"meta": {
|
||||
"id": "questionnaire_2_rhs"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": "q0",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "q3",
|
||||
"layout": "glass_scale_question",
|
||||
"question": "glass_explanation",
|
||||
"symptoms": [
|
||||
"q1_symptom",
|
||||
"q2_symptom",
|
||||
"q3_symptom",
|
||||
"q4_symptom",
|
||||
"q5_symptom",
|
||||
"q6_symptom",
|
||||
"q7_symptom",
|
||||
"q8_symptom",
|
||||
"q9_symptom"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q4",
|
||||
"layout": "glass_scale_question",
|
||||
"question": "how_strong_past_month",
|
||||
"symptoms": [
|
||||
"q10_reexperience_trauma",
|
||||
"q11_physical_reaction",
|
||||
"q12_emotional_numbness",
|
||||
"q13_easily_startled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q5",
|
||||
"layout": "radio_question",
|
||||
"textKey": "resilience_reflection_prompt",
|
||||
"question": "q14_intro",
|
||||
"options": [
|
||||
{ "key": "resilience_fully_capable" },
|
||||
{ "key": "resilience_mostly_capable" },
|
||||
{ "key": "resilience_some_capable_some_not" },
|
||||
{ "key": "resilience_mostly_not_capable" },
|
||||
{ "key": "resilience_not_capable" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"resilience_fully_capable": 0,
|
||||
"resilience_mostly_capable": 1,
|
||||
"resilience_some_capable_some_not": 2,
|
||||
"resilience_mostly_not_capable": 3,
|
||||
"resilience_not_capable": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q6",
|
||||
"layout": "value_spinner",
|
||||
"question": "pain_rating_instruction",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q7",
|
||||
"layout": "radio_question",
|
||||
"question": "violence_question_1",
|
||||
"options": [
|
||||
{ "key": "no",
|
||||
"nextQuestionId": "q10"},
|
||||
{ "key": "yes" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q8",
|
||||
"layout": "radio_question",
|
||||
"question": "times_happend",
|
||||
"options": [
|
||||
{ "key": "once" },
|
||||
{ "key": "multiple_times" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q9",
|
||||
"layout": "value_spinner",
|
||||
"question": "age_at_incident",
|
||||
"range": {
|
||||
"min": 1,
|
||||
"max": 122
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q10",
|
||||
"layout": "radio_question",
|
||||
"question": "conflict_since_arrival",
|
||||
"options": [
|
||||
{ "key": "no",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "yes" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q11",
|
||||
"layout": "radio_question",
|
||||
"question": "times_happend2",
|
||||
"options": [
|
||||
{ "key": "once" },
|
||||
{ "key": "multiple_times" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q12",
|
||||
"layout": "value_spinner",
|
||||
"question": "age_at_incident2",
|
||||
"range": {
|
||||
"min": 1,
|
||||
"max": 122
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q25",
|
||||
"layout": "date_spinner",
|
||||
"question": "asylum_procedure_since"
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
255
website/temp-assets/questionnaire_3_integration_index.json
Normal file
255
website/temp-assets/questionnaire_3_integration_index.json
Normal file
@ -0,0 +1,255 @@
|
||||
{
|
||||
"meta": {
|
||||
"id": "questionnaire_3_integration_index"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": "q0",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "q1",
|
||||
"layout": "radio_question",
|
||||
"textKey": "intro_life_in_germany",
|
||||
"question": "feeling_connected_to_germany_question",
|
||||
"options": [
|
||||
{ "key": "very_connected" },
|
||||
{ "key": "quite_connected" },
|
||||
{ "key": "moderately_connected" },
|
||||
{ "key": "somewhat_loose" },
|
||||
{ "key": "not_connected_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_connected": 3,
|
||||
"quite_connected": 4,
|
||||
"moderately_connected": 5,
|
||||
"somewhat_loose": 1,
|
||||
"not_connected_at_all": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q2",
|
||||
"layout": "radio_question",
|
||||
"question": "understanding_political_issues",
|
||||
"options": [
|
||||
{ "key": "very_good" },
|
||||
{ "key": "good" },
|
||||
{ "key": "fairly_good" },
|
||||
{ "key": "somewhat" },
|
||||
{ "key": "not_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_good": 1,
|
||||
"good": 2,
|
||||
"fairly_good": 3,
|
||||
"somewhat": 4,
|
||||
"not_at_all": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q3",
|
||||
"layout": "radio_question",
|
||||
"question": "unexpected_expense_question",
|
||||
"options": [
|
||||
{ "key": "expense_50" },
|
||||
{ "key": "expense_100" },
|
||||
{ "key": "expense_300" },
|
||||
{ "key": "expense_500" },
|
||||
{ "key": "expense_800" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"expense_50": 1,
|
||||
"expense_100": 2,
|
||||
"expense_300": 3,
|
||||
"expense_500": 4,
|
||||
"expense_800": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q4",
|
||||
"layout": "radio_question",
|
||||
"question": "dining_with_germans_question",
|
||||
"options": [
|
||||
{ "key": "never" },
|
||||
{ "key": "once_a_year" },
|
||||
{ "key": "once_a_month" },
|
||||
{ "key": "once_a_week" },
|
||||
{ "key": "almost_every_day" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"never": 1,
|
||||
"once_a_year": 2,
|
||||
"once_a_month": 3,
|
||||
"once_a_week": 4,
|
||||
"almost_every_day": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q5",
|
||||
"layout": "radio_question",
|
||||
"question": "reading_german_articles_question",
|
||||
"options": [
|
||||
{ "key": "very_good" },
|
||||
{ "key": "good" },
|
||||
{ "key": "moderately_good" },
|
||||
{ "key": "not_good" },
|
||||
{ "key": "not_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_good": 1,
|
||||
"good": 2,
|
||||
"moderately_good": 3,
|
||||
"not_good": 4,
|
||||
"not_at_all": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q6",
|
||||
"layout": "radio_question",
|
||||
"question": "visiting_doctor_question",
|
||||
"options": [
|
||||
{ "key": "very_difficult" },
|
||||
{ "key": "rather_difficult" },
|
||||
{ "key": "neither_nor" },
|
||||
{ "key": "rather_easy" },
|
||||
{ "key": "very_easy" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_difficult": 1,
|
||||
"rather_difficult": 2,
|
||||
"neither_nor": 3,
|
||||
"rather_easy": 4,
|
||||
"very_easy": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q7",
|
||||
"layout": "radio_question",
|
||||
"question": "feeling_as_outsider_question",
|
||||
"options": [
|
||||
{ "key": "never" },
|
||||
{ "key": "rarely" },
|
||||
{ "key": "sometimes" },
|
||||
{ "key": "often" },
|
||||
{ "key": "always" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"never": 1,
|
||||
"rarely": 2,
|
||||
"sometimes": 3,
|
||||
"often": 4,
|
||||
"always": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q8",
|
||||
"layout": "radio_question",
|
||||
"question": "recent_occupation_question",
|
||||
"options": [
|
||||
{ "key": "employed" },
|
||||
{ "key": "education" },
|
||||
{ "key": "internship" },
|
||||
{ "key": "unemployed_searching" },
|
||||
{ "key": "unemployed_not_searching" },
|
||||
{ "key": "sick_or_disabled" },
|
||||
{ "key": "unpaid_housework" },
|
||||
{ "key": "other_activity" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"employed": 1,
|
||||
"education": 2,
|
||||
"internship": 3,
|
||||
"unemployed_searching": 4,
|
||||
"unemployed_not_searching": 8,
|
||||
"sick_or_disabled": 5,
|
||||
"unpaid_housework": 6,
|
||||
"other_activity": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q9",
|
||||
"layout": "radio_question",
|
||||
"question": "discussing_politics_question",
|
||||
"options": [
|
||||
{ "key": "never" },
|
||||
{ "key": "once_a_year" },
|
||||
{ "key": "once_a_month" },
|
||||
{ "key": "once_a_week" },
|
||||
{ "key": "almost_every_day" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"never": 1,
|
||||
"once_a_year": 2,
|
||||
"once_a_month": 3,
|
||||
"once_a_week": 4,
|
||||
"almost_every_day": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q10",
|
||||
"layout": "radio_question",
|
||||
"question": "contact_with_germans_question",
|
||||
"options": [
|
||||
{ "key": "zero" },
|
||||
{ "key": "one_to_three" },
|
||||
{ "key": "three_to_six" },
|
||||
{ "key": "seven_to_fourteen" },
|
||||
{ "key": "fifteen_or_more" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"zero": 1,
|
||||
"one_to_three": 2,
|
||||
"three_to_six": 3,
|
||||
"seven_to_fourteen": 4,
|
||||
"fifteen_or_more": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q11",
|
||||
"layout": "radio_question",
|
||||
"question": "speaking_german_opinion_question",
|
||||
"options": [
|
||||
{ "key": "very_good" },
|
||||
{ "key": "good" },
|
||||
{ "key": "moderately_good" },
|
||||
{ "key": "not_good" },
|
||||
{ "key": "not_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_good": 1,
|
||||
"good": 2,
|
||||
"moderately_good": 3,
|
||||
"not_good": 4,
|
||||
"not_at_all": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q12",
|
||||
"layout": "radio_question",
|
||||
"question": "job_search_question",
|
||||
"options": [
|
||||
{ "key": "very_difficult" },
|
||||
{ "key": "rather_difficult" },
|
||||
{ "key": "neither_nor" },
|
||||
{ "key": "rather_easy" },
|
||||
{ "key": "very_easy" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_difficult": 1,
|
||||
"rather_difficult": 2,
|
||||
"neither_nor": 3,
|
||||
"rather_easy": 4,
|
||||
"very_easy": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
182
website/temp-assets/questionnaire_4_consultation_results.json
Normal file
182
website/temp-assets/questionnaire_4_consultation_results.json
Normal file
@ -0,0 +1,182 @@
|
||||
{
|
||||
"meta": {
|
||||
"id": "questionnaire_4_consultation_results"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": "q0",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "q1",
|
||||
"layout": "date_spinner",
|
||||
"question": "date_consultation_health_interview_result"
|
||||
},
|
||||
{
|
||||
"id": "q2",
|
||||
"layout": "radio_question",
|
||||
"question": "consultation_decision",
|
||||
"options": [
|
||||
{
|
||||
"key": "green",
|
||||
"nextQuestionId": "q3"},
|
||||
{"key": "yellow",
|
||||
"nextQuestionId": "q4"},
|
||||
{ "key": "red",
|
||||
"nextQuestionId": "q9"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"green": 0,
|
||||
"yellow": 0,
|
||||
"red": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q3",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_conversation_in_6_months",
|
||||
"options": [
|
||||
{ "key": "yes",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "no",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q4",
|
||||
"layout": "radio_question",
|
||||
"question": "participation_in_coaching",
|
||||
"options": [
|
||||
{"key": "yes",
|
||||
"nextQuestionId": "q5"},
|
||||
{"key": "no",
|
||||
"nextQuestionId": "q6"},
|
||||
{"key": "time_to_think_about_it",
|
||||
"nextQuestionId": "q7"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0,
|
||||
"time_to_think_about_it": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q5",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_coaching_given",
|
||||
"options": [
|
||||
{"key": "consent_yes",
|
||||
"nextQuestionId": "q3"},
|
||||
{"key": "consent_no",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"consent_yes": 0,
|
||||
"consent_no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q6",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_conversation_in_6_months",
|
||||
"options": [
|
||||
{ "key": "yes",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "no",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q7",
|
||||
"layout": "date_spinner",
|
||||
"question": "decision_after_reflection_period"
|
||||
},
|
||||
{
|
||||
"id": "q8",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_conversation_in_6_months",
|
||||
"options": [
|
||||
{ "key": "yes",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "no",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q9",
|
||||
"layout": "radio_question",
|
||||
"question": "professional_referral",
|
||||
"options": [
|
||||
{ "key": "yes"},
|
||||
{ "key": "no"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q10",
|
||||
"layout": "radio_question",
|
||||
"question": "confidentiality_agreement" ,
|
||||
"options": [
|
||||
{ "key": "available_yes"},
|
||||
{ "key": "available_no",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q11",
|
||||
"layout": "radio_question",
|
||||
"question": "health_insurance_card",
|
||||
"options": [
|
||||
{ "key": "yes"},
|
||||
{ "key": "no"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q12",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_conversation_in_6_months",
|
||||
"options": [
|
||||
{ "key": "yes",
|
||||
"nextQuestionId": "last_page"},
|
||||
{ "key": "no",
|
||||
"nextQuestionId": "last_page"}
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
84
website/temp-assets/questionnaire_5_final_interview.json
Normal file
84
website/temp-assets/questionnaire_5_final_interview.json
Normal file
@ -0,0 +1,84 @@
|
||||
{
|
||||
"meta": {
|
||||
"id": "questionnaire_5_final_interview"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": "q0",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "q1",
|
||||
"layout": "radio_question",
|
||||
"question": "consent_followup_6_months",
|
||||
"options": [
|
||||
{ "key": "yes" },
|
||||
{ "key": "no" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q2",
|
||||
"layout": "date_spinner",
|
||||
"question": "date_final_interview"
|
||||
},
|
||||
{
|
||||
"id": "q3",
|
||||
"layout": "value_spinner",
|
||||
"question": "amount_nat_appointments",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q4",
|
||||
"layout": "value_spinner",
|
||||
"question": "amount_session_flowers",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q5",
|
||||
"layout": "value_spinner",
|
||||
"question": "amount_session_stones",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q6",
|
||||
"layout": "radio_question",
|
||||
"question": "termination_nat_coaching",
|
||||
"options": [
|
||||
{"key": "termination_nat_regular",
|
||||
"nextQuestionId": "last_page"},
|
||||
{"key": "termination_nat_referral",
|
||||
"nextQuestionId": "last_page"},
|
||||
{"key": "termination_nat_dropout",
|
||||
"nextQuestionId": "q7"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q7",
|
||||
"layout": "radio_question",
|
||||
"question": "client_canceled_NAT",
|
||||
"options": [
|
||||
{"key": "after_meeting"},
|
||||
{"key": "without_giving_reasons"},
|
||||
{"key": "with_reasons_given"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
349
website/temp-assets/questionnaire_6_follow_up_survey.json
Normal file
349
website/temp-assets/questionnaire_6_follow_up_survey.json
Normal file
@ -0,0 +1,349 @@
|
||||
{
|
||||
"meta": {
|
||||
"id": "questionnaire_6_follow_up_survey"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"id": "q0",
|
||||
"layout": "client_coach_code_question",
|
||||
"question": "client_code_entry_question",
|
||||
"hint1": "client_code",
|
||||
"hint2": "coach_code"
|
||||
},
|
||||
{
|
||||
"id": "q1",
|
||||
"layout": "radio_question",
|
||||
"question": "follow_up",
|
||||
"options": [
|
||||
{
|
||||
"key": "follow_up_completed",
|
||||
"nextQuestionId": "q2"
|
||||
},
|
||||
{
|
||||
"key": "follow_up_failed_contact",
|
||||
"nextQuestionId": "last_page"
|
||||
},
|
||||
{
|
||||
"key": "follow_up_consent_withdrawn",
|
||||
"nextQuestionId": "last_page"
|
||||
}
|
||||
],
|
||||
"pointsMap": {
|
||||
"follow_up_completed": 0,
|
||||
"follow_up_failed_contact": 0,
|
||||
"follow_up_consent_withdrawn": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q2",
|
||||
"layout": "radio_question",
|
||||
"question": "special_burden_question",
|
||||
"options": [
|
||||
{ "key": "yes" },
|
||||
{ "key": "no" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"yes": 0,
|
||||
"no": 0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"id": "q3",
|
||||
"layout": "glass_scale_question",
|
||||
"question": "glass_explanation",
|
||||
"symptoms": [
|
||||
"q1_symptom",
|
||||
"q2_symptom",
|
||||
"q3_symptom",
|
||||
"q4_symptom",
|
||||
"q5_symptom",
|
||||
"q6_symptom",
|
||||
"q7_symptom",
|
||||
"q8_symptom",
|
||||
"q9_symptom"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q4",
|
||||
"layout": "glass_scale_question",
|
||||
"question": "how_strong_past_month",
|
||||
"symptoms": [
|
||||
"q10_reexperience_trauma",
|
||||
"q11_physical_reaction",
|
||||
"q12_emotional_numbness",
|
||||
"q13_easily_startled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "q5",
|
||||
"layout": "radio_question",
|
||||
"textKey": "resilience_reflection_prompt",
|
||||
"question": "q14_intro",
|
||||
"options": [
|
||||
{ "key": "resilience_fully_capable" },
|
||||
{ "key": "resilience_mostly_capable" },
|
||||
{ "key": "resilience_some_capable_some_not" },
|
||||
{ "key": "resilience_mostly_not_capable" },
|
||||
{ "key": "resilience_not_capable" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"resilience_fully_capable": 0,
|
||||
"resilience_mostly_capable": 0,
|
||||
"resilience_some_capable_some_not": 0,
|
||||
"resilience_mostly_not_capable": 0,
|
||||
"resilience_not_capable": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q6",
|
||||
"layout": "value_spinner",
|
||||
"question": "pain_rating_instruction",
|
||||
"range": {
|
||||
"min": 0,
|
||||
"max": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q7",
|
||||
"layout": "radio_question",
|
||||
"textKey": "intro_life_in_germany",
|
||||
"question": "feeling_connected_to_germany_question",
|
||||
"options": [
|
||||
{ "key": "very_connected" },
|
||||
{ "key": "quite_connected" },
|
||||
{ "key": "moderately_connected" },
|
||||
{ "key": "somewhat_loose" },
|
||||
{ "key": "not_connected_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_connected": 0,
|
||||
"quite_connected": 0,
|
||||
"moderately_connected": 0,
|
||||
"somewhat_loose": 0,
|
||||
"not_connected_at_all": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q8",
|
||||
"layout": "radio_question",
|
||||
"question": "understanding_political_issues",
|
||||
"options": [
|
||||
{ "key": "very_good" },
|
||||
{ "key": "good" },
|
||||
{ "key": "fairly_good" },
|
||||
{ "key": "somewhat" },
|
||||
{ "key": "not_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_good": 0,
|
||||
"good": 0,
|
||||
"fairly_good": 0,
|
||||
"somewhat": 0,
|
||||
"not_at_all": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q9",
|
||||
"layout": "radio_question",
|
||||
"question": "unexpected_expense_question",
|
||||
"options": [
|
||||
{ "key": "expense_50" },
|
||||
{ "key": "expense_100" },
|
||||
{ "key": "expense_300" },
|
||||
{ "key": "expense_500" },
|
||||
{ "key": "expense_800" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"expense_50": 0,
|
||||
"expense_100": 0,
|
||||
"expense_300": 0,
|
||||
"expense_500": 0,
|
||||
"expense_800": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q10",
|
||||
"layout": "radio_question",
|
||||
"question": "dining_with_germans_question",
|
||||
"options": [
|
||||
{ "key": "never" },
|
||||
{ "key": "once_a_year" },
|
||||
{ "key": "once_a_month" },
|
||||
{ "key": "once_a_week" },
|
||||
{ "key": "almost_every_day" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"never": 0,
|
||||
"once_a_year": 0,
|
||||
"once_a_month": 0,
|
||||
"once_a_week": 0,
|
||||
"almost_every_day": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q11",
|
||||
"layout": "radio_question",
|
||||
"question": "reading_german_articles_question",
|
||||
"options": [
|
||||
{ "key": "very_good" },
|
||||
{ "key": "good" },
|
||||
{ "key": "moderately_good" },
|
||||
{ "key": "not_good" },
|
||||
{ "key": "not_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_good": 0,
|
||||
"good": 0,
|
||||
"moderately_good": 0,
|
||||
"not_good": 0,
|
||||
"not_at_all": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q12",
|
||||
"layout": "radio_question",
|
||||
"question": "visiting_doctor_question",
|
||||
"options": [
|
||||
{ "key": "very_difficult" },
|
||||
{ "key": "rather_difficult" },
|
||||
{ "key": "neither_nor" },
|
||||
{ "key": "rather_easy" },
|
||||
{ "key": "very_easy" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_difficult": 0,
|
||||
"rather_difficult": 0,
|
||||
"neither_nor": 0,
|
||||
"rather_easy": 0,
|
||||
"very_easy": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q13",
|
||||
"layout": "radio_question",
|
||||
"question": "feeling_as_outsider_question",
|
||||
"options": [
|
||||
{ "key": "never" },
|
||||
{ "key": "rarely" },
|
||||
{ "key": "sometimes" },
|
||||
{ "key": "often" },
|
||||
{ "key": "always" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"never": 0,
|
||||
"rarely": 0,
|
||||
"sometimes": 0,
|
||||
"often": 0,
|
||||
"always": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q14",
|
||||
"layout": "radio_question",
|
||||
"question": "recent_occupation_question",
|
||||
"options": [
|
||||
{ "key": "employed" },
|
||||
{ "key": "education" },
|
||||
{ "key": "internship" },
|
||||
{ "key": "unemployed_searching" },
|
||||
{ "key": "unemployed_not_searching" },
|
||||
{ "key": "sick_or_disabled" },
|
||||
{ "key": "unpaid_housework" },
|
||||
{ "key": "other_activity" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"employed": 0,
|
||||
"education": 0,
|
||||
"internship": 0,
|
||||
"unemployed_searching": 0,
|
||||
"unemployed_not_searching": 0,
|
||||
"sick_or_disabled": 0,
|
||||
"unpaid_housework": 0,
|
||||
"other_activity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q15",
|
||||
"layout": "radio_question",
|
||||
"question": "discussing_politics_question",
|
||||
"options": [
|
||||
{ "key": "never" },
|
||||
{ "key": "once_a_year" },
|
||||
{ "key": "once_a_month" },
|
||||
{ "key": "once_a_week" },
|
||||
{ "key": "almost_every_day" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"never": 0,
|
||||
"once_a_year": 0,
|
||||
"once_a_month": 0,
|
||||
"once_a_week": 0,
|
||||
"almost_every_day": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q16",
|
||||
"layout": "radio_question",
|
||||
"question": "contact_with_germans_question",
|
||||
"options": [
|
||||
{ "key": "zero" },
|
||||
{ "key": "one_to_three" },
|
||||
{ "key": "three_to_six" },
|
||||
{ "key": "seven_to_fourteen" },
|
||||
{ "key": "fifteen_or_more" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"zero": 0,
|
||||
"one_to_three": 0,
|
||||
"three_to_six": 0,
|
||||
"seven_to_fourteen": 0,
|
||||
"fifteen_or_more": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q17",
|
||||
"layout": "radio_question",
|
||||
"question": "speaking_german_opinion_question",
|
||||
"options": [
|
||||
{ "key": "very_good" },
|
||||
{ "key": "good" },
|
||||
{ "key": "fairly_good" },
|
||||
{ "key": "not_good" },
|
||||
{ "key": "not_at_all" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_good": 0,
|
||||
"good": 0,
|
||||
"fairly_good": 0,
|
||||
"not_good": 0,
|
||||
"not_at_all": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "q18",
|
||||
"layout": "radio_question",
|
||||
"question": "job_search_question",
|
||||
"options": [
|
||||
{ "key": "very_difficult" },
|
||||
{ "key": "rather_difficult" },
|
||||
{ "key": "neither_nor" },
|
||||
{ "key": "rather_easy" },
|
||||
{ "key": "very_easy" }
|
||||
],
|
||||
"pointsMap": {
|
||||
"very_difficult": 0,
|
||||
"rather_difficult": 0,
|
||||
"neither_nor": 0,
|
||||
"rather_easy": 0,
|
||||
"very_easy": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "last_page",
|
||||
"layout": "last_page",
|
||||
"textKey": "finish_data_entry",
|
||||
"question": "data_final_warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
67
website/temp-assets/questionnaire_order.json
Normal file
67
website/temp-assets/questionnaire_order.json
Normal file
@ -0,0 +1,67 @@
|
||||
[
|
||||
{
|
||||
"file": "questionnaire_1_demographic_information.json",
|
||||
"showPoints": false,
|
||||
"condition": {
|
||||
"alwaysAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "questionnaire_2_rhs.json",
|
||||
"showPoints": true,
|
||||
"condition": {
|
||||
"alwaysAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "questionnaire_3_integration_index.json",
|
||||
"showPoints": true,
|
||||
"condition": {
|
||||
"alwaysAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "questionnaire_4_consultation_results.json",
|
||||
"showPoints": false,
|
||||
"condition": {
|
||||
"requiresCompleted": [
|
||||
"questionnaire_1_demographic_information",
|
||||
"questionnaire_2_rhs",
|
||||
"questionnaire_3_integration_index"
|
||||
],
|
||||
"questionnaire": "questionnaire_1_demographic_information",
|
||||
"questionId": "consent_instruction",
|
||||
"operator": "==",
|
||||
"value": "consent_signed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "questionnaire_5_final_interview.json",
|
||||
"showPoints": false,
|
||||
"condition": {
|
||||
"requiresCompleted": ["questionnaire_4_consultation_results"],
|
||||
"questionnaire": "questionnaire_4_consultation_results",
|
||||
"questionId": "consultation_decision",
|
||||
"operator": "==",
|
||||
"value": "yellow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "questionnaire_6_follow_up_survey.json",
|
||||
"showPoints": true,
|
||||
"condition": {
|
||||
"anyOf": [
|
||||
{
|
||||
"requiresCompleted": ["questionnaire_5_final_interview"]
|
||||
},
|
||||
{
|
||||
"requiresCompleted": ["questionnaire_4_consultation_results"],
|
||||
"questionnaire": "questionnaire_4_consultation_results",
|
||||
"questionId": "consultation_decision",
|
||||
"operator": "!=",
|
||||
"value": "yellow"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user