Compare commits

11 Commits

Author SHA1 Message Date
7d06f51740 fix: prevent stepper markers from overflowing on mobile
Remove min-width: 44px and vertical centering override from .progress-dot
in the mobile breakpoint, which caused 9 dots to overflow the viewport
(9x44px = 396px > 374px available). Also add a 480px breakpoint with
smaller 28px dots for narrow phones like iPhone SE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 17:16:08 +09:00
cb74569c97 fix: correct checkbox_anzahl field name so Elektriker/Dübelarbeiten appear in email
The _anzahl suffix was placed outside the closing bracket in PHP array
notation (e.g. [e-herd]_anzahl), causing PHP to ignore it and overwrite
the checkbox value. Moved the suffix inside the bracket ([e-herd_anzahl])
so both checkbox and quantity values are stored as separate POST keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:01:02 +09:00
11cd74cac3 docs: update STATE.md with post-milestone work and update plugin author
- Document UI/UX modernization, email rewrite, and dev tooling in STATE.md
- Add post-milestone decisions table entries
- Update session continuity to 2026-02-13
- Update plugin author

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:43:58 +09:00
f5ca452a85 feat: add test email admin page and comprehensive form fill button
- New Umzugsliste_Test_Email class with test data generator covering all
  fields: addresses, 7 rooms with items, all additional work sections, sonstiges
- Admin page under Moving List > Test Email with inline preview iframe
  and Send Test Email button (manage_options capability)
- Replace Step 1 dev fill button with Fill All that populates every field
  across all 9 steps (furniture, additional work, sonstiges)
- Fix getFieldVal crash when select has no selection (selectedIndex=-1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:40:08 +09:00
60f82f1224 feat: rewrite email Weitere Arbeiten as single 3-column table with grand totals in last room
- Replace per-section tables with unified Weitere Arbeiten table matching legacy format
- Move grand totals row into last room table instead of standalone section
- Add get_field_key/get_field_value/get_field_anzahl helpers for field resolution
- Packarbeiten/Anfahrt use sub-headers matching legacy HTML structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:40:00 +09:00
a91425bd2d Merge pull request 'feature/ui-ux-modernization' (#1) from feature/ui-ux-modernization into main
Reviewed-on: #1
2026-02-12 14:35:22 +00:00
f6c7af7cbc fix: polish form UX and step 9 summary translations
- Add spacing below Datenschutzerklaerung text
- Fix missing Umzugstermin on step 9 (wrong field names in JS)
- Remove number-to-checkmark animation flicker on progress dots
- Add cbm unit label to furniture item values and summary
- Translate all summary field labels via l10n (Name, Strasse, etc.)
- Fix room names showing lowercase keys instead of proper titles
- Auto-check checkbox when quantity is entered on step 8
- Remove redundant Sonstiges textarea placeholder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:31:25 +09:00
f1f5c760c2 feat: add 3 switchable color palettes with Slate Blue & Amber as default
Replace generic Google blue with 3 professional palettes (Deep Teal,
Slate Blue & Amber, Rich Olive & Copper) using CSS token overrides.
Palette B (Slate Blue + amber Next button) is the default. Includes
WP_DEBUG-only purple switcher button to cycle between palettes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:02:55 +09:00
2ca1f4ff54 feat: redesign Step 8 with flat list layout matching room steps
Replace bordered card sections with flat rows using hairline dividers,
opacity dimming, and native radio controls to match the room step visual
pattern. Also includes structural refactors (step-sections, address-sections)
and running totals bar polish from the modernization branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:47:39 +09:00
89bd555dc1 feat: modernize wizard UX with smart rows, steppers, transitions, and edit links
- Hide montage toggles and dim furniture rows when quantity is 0
- Animate running totals bar with CSS transform instead of display toggle
- Add directional slide transitions (forward/backward) between steps
- Add +/- stepper buttons around quantity inputs for better affordance
- Increase mobile tap targets to 44px and show active step label
- Add "Edit" links to summary section headings for quick navigation
- Add "Step X of Y" counter below progress bar
- Add summaryEdit, stepLabel, stepOf l10n strings to both entry points

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 23:46:36 +09:00
b9ae7d707d fix: step navigation back-click bug and add shortcode lang attribute
Track highestStep so navigating backward preserves completed dots and
allows clicking forward to any previously visited step. Add [umzugsliste
lang="de|en"] shortcode attribute that switches locale via
switch_to_locale() for per-page language control.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:54:55 +09:00
12 changed files with 1536 additions and 344 deletions

View File

@@ -5,21 +5,43 @@
See: .planning/PROJECT.md (updated 2026-01-16)
**Core value:** Email format identical to legacy — office staff workflow depends on the exact HTML table structure.
**Current focus:** Post-v1.0 bug fixes (manual testing discoveries)
**Current focus:** Post-v1.0 polish — UI modernization, email format corrections, developer tooling
## Current Position
Phase: 10 of 10 (Post-Release Fixes)
Plan: 1 of 1 complete
Status: Phase complete ✓
Last activity: 2026-02-07 — Fixed form submit 404, handler timing, CPT meta box
Phase: 10 of 10 (all roadmap phases complete)
Status: Milestone v1.0 COMPLETE + post-milestone enhancements
Last activity: 2026-02-13 — Test email admin page, comprehensive form fill button
Progress: ██████████ 100% (11/11 plans)
## Post-Milestone Work (after v1.0 completion)
Work completed outside the GSD roadmap since milestone completion:
### UI/UX Modernization (feature/ui-ux-modernization branch, merged)
- Modernized wizard UX with smart rows, steppers, transitions, and edit links
- Redesigned Step 8 (additional work) with flat list layout matching room steps
- Added 3 switchable color palettes with Slate Blue & Amber as default
- Polished form UX and step 9 summary translations
- Fixed step navigation back-click bug, added shortcode lang attribute
### Email Generator Rewrite (`60f82f1`)
- Rewrote "Weitere Arbeiten" section as single 3-column table matching legacy format
- Moved grand totals into last room's table instead of standalone section
- Added Packarbeiten/Anfahrt sub-headers matching legacy HTML structure
- Added get_field_key/get_field_value/get_field_anzahl helper methods
### Developer Tooling (`f5ca452`)
- New `Umzugsliste_Test_Email` class with comprehensive test data generator
- Admin page (Moving List > Test Email) with inline email preview iframe + send button
- Upgraded dev "Fill" button to "Fill All" — populates every field across all 9 form steps
- Fixed getFieldVal crash when select has no selection (defensive coding)
## Performance Metrics
**Velocity:**
- Total plans completed: 11
- Total roadmap plans completed: 11
- Average duration: ~24 min per plan
- Total execution time: ~4.5 hours
@@ -35,13 +57,14 @@ Progress: ██████████ 100% (11/11 plans)
| 6 | 1 | Form handler, email generator, wp_mail() integration |
| 7 | 1 | Captcha verification and inline validation |
| 8 | 2/2 | Bug fixes & legacy parity (gap closure) |
| 9 | 2/2 | Internationalization (gap closure) |
| 10 | 1/1 | Post-release fixes (manual testing bugs) |
| 9 | 2/2 | Internationalization (gap closure) |
| 10 | 1/1 | Post-release fixes (manual testing bugs) |
**Overall Trend:**
- Phases 1-7 completed successfully
- Milestone audit found 4 gaps requiring phases 8-9
- Manual testing found 3 runtime bugs requiring phase 10
- Post-milestone: UI modernization, email format correction, dev tooling
- No blockers encountered
## Accumulated Context
@@ -72,6 +95,10 @@ Recent decisions affecting current work:
| 10 | Rename day/month/year fields to umzug_day/umzug_month/umzug_year | WordPress reserved query vars caused 404 on form POST |
| 10 | Move handler instantiation to init_hooks() | init callback too late for handler's own init hook |
| 10 | Add CPT submission details meta box | No way to view stored submission data in admin |
| Post | Weitere Arbeiten as single 3-column table | Matches legacy format exactly — one table with sub-section headers |
| Post | Grand totals in last room's table | Legacy places grand totals as final rows of last room table |
| Post | Test email admin page with iframe preview | Instant verification of email output without filling the full form |
| Post | Fill All dev button (WP_DEBUG only) | Populates every form field for rapid end-to-end testing |
### Deferred Issues
@@ -85,7 +112,7 @@ None.
## Session Continuity
Last session: 2026-02-07
Stopped at: Documented phase 10 post-release fixes - PHASE 10 COMPLETE ✓
Last session: 2026-02-13
Stopped at: Test email admin page + Fill All button complete and committed
Resume file: None
Next up: All phases complete! Plugin shipped and working end-to-end.
Next up: All roadmap phases complete. Plugin is functional and polished. Available for new milestone or ad-hoc work.

View File

@@ -27,6 +27,123 @@
--umzug-transition: 0.2s ease;
}
/* ===== Color Palettes ===== */
/* Palette A: Deep Teal — Grounded Confidence */
.palette-a.umzugsliste-wizard {
--umzug-primary: #1F7A7A;
--umzug-primary-light: rgba(31, 122, 122, 0.2);
--umzug-primary-dark: #155858;
--umzug-success: #4A7C59;
--umzug-success-light: rgba(74, 124, 89, 0.1);
--umzug-error: #C14E3A;
--umzug-error-light: #FCEAE6;
--umzug-bg: #FAF8F5;
--umzug-surface: #FFFFFF;
--umzug-border: #D4CEC4;
--umzug-text: #3E2E28;
--umzug-text-secondary: #6B5D56;
}
.palette-a .room-totals {
background: #F5F2ED;
}
.palette-a .wizard-btn-back:hover {
background: #F2EFEA;
color: var(--umzug-text);
}
.palette-a .wizard-btn-submit:hover {
background: #3D6A4A;
}
body.umzugsliste-standalone:has(.palette-a) {
background: #FAF8F5;
}
/* Palette B: Slate Blue & Amber — Modern Logistics */
.palette-b.umzugsliste-wizard {
--umzug-primary: #4A6FA5;
--umzug-primary-light: rgba(74, 111, 165, 0.2);
--umzug-primary-dark: #2F4668;
--umzug-success: #52A07B;
--umzug-success-light: rgba(82, 160, 123, 0.1);
--umzug-error: #D15847;
--umzug-error-light: #FDEAE8;
--umzug-bg: #F5F7FA;
--umzug-surface: #FFFFFF;
--umzug-border: #D8DFE8;
--umzug-text: #1A2332;
--umzug-text-secondary: #5F6B7A;
}
.palette-b .room-totals {
background: #EDF0F5;
}
.palette-b .wizard-btn-back:hover {
background: #ECEFF4;
color: var(--umzug-text);
}
.palette-b .wizard-btn-submit:hover {
background: #448A69;
}
.palette-b .wizard-btn-next {
background: #E8A63C;
}
.palette-b .wizard-btn-next:hover {
background: #D69529;
}
body.umzugsliste-standalone:has(.palette-b) {
background: #F5F7FA;
}
/* Palette C: Rich Olive & Copper — Heritage & Motion */
.palette-c.umzugsliste-wizard {
--umzug-primary: #5C6E54;
--umzug-primary-light: rgba(92, 110, 84, 0.2);
--umzug-primary-dark: #3D4738;
--umzug-success: #6B9F6F;
--umzug-success-light: rgba(107, 159, 111, 0.1);
--umzug-error: #B85A3D;
--umzug-error-light: #F8EDE8;
--umzug-bg: #F8F6F1;
--umzug-surface: #FEFDFB;
--umzug-border: #D9D3C8;
--umzug-text: #2F3126;
--umzug-text-secondary: #6B6E64;
}
.palette-c .room-totals {
background: #F0EDE6;
}
.palette-c .wizard-btn-back:hover {
background: #F0EDE6;
color: var(--umzug-text);
}
.palette-c .wizard-btn-submit:hover {
background: #5A8A5E;
}
.palette-c .wizard-btn-next {
background: #C17E5D;
}
.palette-c .wizard-btn-next:hover {
background: #A96A4B;
}
body.umzugsliste-standalone:has(.palette-c) {
background: #F8F6F1;
}
/* ===== Reset & Base ===== */
.umzugsliste-standalone {
margin: 0;
@@ -132,7 +249,7 @@
font-size: 0.8rem;
font-weight: 600;
color: var(--umzug-text-secondary);
transition: all var(--umzug-transition);
transition: background var(--umzug-transition), border-color var(--umzug-transition), color var(--umzug-transition);
}
.dot-label {
@@ -167,6 +284,14 @@
font-size: 0;
}
/* ===== Step Counter ===== */
.progress-counter {
text-align: center;
font-size: 0.8rem;
color: var(--umzug-text-secondary);
margin-bottom: 16px;
}
/* ===== Running Totals Bar ===== */
.running-totals {
position: sticky;
@@ -175,17 +300,24 @@
right: 0;
z-index: 100;
background: var(--umzug-surface);
border-top: 3px solid var(--umzug-primary);
border: 1px solid var(--umzug-border);
border-radius: var(--umzug-radius);
padding: 12px 20px;
margin-bottom: 12px;
text-align: center;
font-size: 0.95rem;
font-weight: 500;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
display: none;
box-shadow: var(--umzug-shadow-lg);
transform: translateY(100%);
opacity: 0;
transition: transform 0.3s, opacity 0.3s;
pointer-events: none;
}
.running-totals.visible {
display: block;
transform: translateY(0);
opacity: 1;
pointer-events: auto;
}
.running-totals-label {
@@ -219,10 +351,30 @@
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInForward {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInBackward {
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
.wizard-step.active.forward {
animation: slideInForward 0.3s ease;
}
.wizard-step.active.backward {
animation: slideInBackward 0.3s ease;
}
.step-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 20px;
margin: 0 0 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--umzug-border);
color: var(--umzug-text);
}
@@ -245,6 +397,32 @@
border-bottom: 1px solid var(--umzug-border);
}
/* ===== Step Sections (inner dividers within a step-card) ===== */
.step-section {
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid var(--umzug-border);
}
.step-section:first-of-type {
border-top: none;
padding-top: 0;
margin-top: 0;
}
/* ===== Address Sections (grid items within step-card) ===== */
.address-section {
background: var(--umzug-surface);
border: 1px solid var(--umzug-border);
border-radius: var(--umzug-radius);
padding: 16px 20px;
}
/* Summary sections inside step-card: lighter treatment */
.step-card #wizard-summary .summary-section {
box-shadow: none;
}
/* ===== Date Selector ===== */
.date-selector {
display: flex;
@@ -287,7 +465,7 @@
font-size: 0.8rem;
color: var(--umzug-text-secondary);
margin-top: 12px;
margin-bottom: 0;
margin-bottom: 16px;
}
.privacy-note a {
@@ -394,27 +572,78 @@
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
opacity: 0.55;
transition: opacity 0.2s;
}
.furniture-item.has-quantity,
.furniture-item:hover,
.furniture-item:focus-within {
opacity: 1;
}
.furniture-item .montage-toggle {
display: none;
}
.furniture-item.has-quantity .montage-toggle {
display: flex;
}
.furniture-item:last-of-type {
border-bottom: none;
}
.quantity-input {
width: 56px;
height: 36px;
padding: 0 8px;
.quantity-stepper {
display: inline-flex;
align-items: center;
border: 1px solid var(--umzug-border);
border-radius: var(--umzug-radius-sm);
border-radius: var(--umzug-radius);
overflow: hidden;
flex-shrink: 0;
}
.qty-btn {
width: 36px;
height: 36px;
border: none;
background: var(--umzug-bg);
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background var(--umzug-transition);
color: var(--umzug-text);
padding: 0;
line-height: 1;
}
.qty-btn:hover {
background: var(--umzug-border);
}
.qty-btn:active {
background: var(--umzug-primary-light);
}
.quantity-stepper .quantity-input {
border: none;
border-left: 1px solid var(--umzug-border);
border-right: 1px solid var(--umzug-border);
border-radius: 0;
width: 48px;
height: 36px;
padding: 0 4px;
font-size: 0.95rem;
text-align: center;
flex-shrink: 0;
color: var(--umzug-text);
background: var(--umzug-surface);
transition: border-color var(--umzug-transition), box-shadow var(--umzug-transition);
}
.quantity-input:focus {
.quantity-stepper .quantity-input:focus {
outline: none;
border-color: var(--umzug-primary);
box-shadow: 0 0 0 3px var(--umzug-primary-light);
@@ -432,9 +661,9 @@
}
.item-cbm {
width: 52px;
width: 75px;
text-align: right;
font-size: 0.85rem;
font-size: 13px;
color: var(--umzug-text-secondary);
flex-shrink: 0;
}
@@ -446,7 +675,14 @@
}
.montage-toggle .radio-label {
font-size: 0.8rem;
font-size: 13px;
}
.montage-label {
font-size: 13px;
font-weight: 600;
color: var(--umzug-text-secondary);
white-space: nowrap;
}
/* ===== Room Totals ===== */
@@ -547,6 +783,98 @@
box-shadow: 0 0 0 3px var(--umzug-primary-light);
}
/* ===== Step 8 Design ===== */
.additional-field input[type="checkbox"] {
accent-color: var(--umzug-primary);
}
.wizard-step[data-step="8"] .additional-field {
padding: 8px 0;
background: transparent;
border: none;
border-bottom: 1px solid #f0f0f0;
border-radius: 0;
opacity: 0.55;
transition: opacity 0.2s;
}
.wizard-step[data-step="8"] .additional-field:last-child {
border-bottom: none;
}
.wizard-step[data-step="8"] .additional-field:hover,
.wizard-step[data-step="8"] .additional-field:focus-within,
.wizard-step[data-step="8"] .additional-field:has(input:checked),
.wizard-step[data-step="8"] .additional-field:has(.qty-small:not(:placeholder-shown)) {
opacity: 1;
}
.wizard-step[data-step="8"] .additional-field-abbau .radio-group {
display: inline-flex;
gap: 16px;
background: transparent;
border: none;
border-radius: 0;
padding: 0;
}
.wizard-step[data-step="8"] .additional-field-abbau .radio-label {
padding: 0;
font-size: 0.9rem;
}
.wizard-step[data-step="8"] .additional-field-abbau .radio-label input[type="radio"] {
position: static;
opacity: 1;
width: auto;
height: auto;
margin: 0;
}
/* ===== Dev Auto-Fill ===== */
.dev-autofill-btn {
position: fixed;
bottom: 60px;
right: 16px;
z-index: 300;
background: #ff6b35;
color: #fff;
border: none;
border-radius: 20px;
padding: 6px 14px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
opacity: 0.6;
transition: opacity var(--umzug-transition);
}
.dev-autofill-btn:hover {
opacity: 1;
}
/* ===== Palette Switcher (WP_DEBUG) ===== */
.dev-palette-btn {
position: fixed;
bottom: 90px;
right: 16px;
z-index: 300;
background: #7c3aed;
color: #fff;
border: none;
border-radius: 20px;
padding: 6px 14px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
opacity: 0.6;
transition: opacity var(--umzug-transition);
}
.dev-palette-btn:hover {
opacity: 1;
}
/* ===== Sonstiges ===== */
.sonstiges-textarea {
width: 100%;
@@ -619,7 +947,7 @@
}
#wizard-summary .summary-item-cbm {
width: 60px;
width: 80px;
text-align: right;
color: var(--umzug-text-secondary);
}
@@ -630,6 +958,19 @@
font-size: 0.8rem;
}
.summary-edit {
float: right;
font-size: 0.8rem;
font-weight: 500;
color: var(--umzug-primary);
cursor: pointer;
text-decoration: none;
}
.summary-edit:hover {
text-decoration: underline;
}
#wizard-summary .summary-grand-total {
background: var(--umzug-primary);
color: #fff;
@@ -730,7 +1071,8 @@
flex-basis: calc(100% - 80px);
}
.quantity-input {
.quantity-input,
.quantity-stepper {
order: 2;
}
@@ -745,6 +1087,15 @@
padding-left: 0;
}
.montage-label {
flex-basis: 100%;
margin-bottom: -4px;
}
.wizard-step[data-step="8"] .additional-field {
padding: 6px 0;
}
.additional-field-abbau {
flex-direction: column;
align-items: flex-start;
@@ -769,18 +1120,30 @@
font-size: 0.95rem;
}
.progress-dot {
min-height: 44px;
}
.dot-label {
display: none;
}
.progress-dot.active .dot-label {
display: block;
font-size: 0.6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dot-number {
width: 28px;
height: 28px;
width: 36px;
height: 36px;
font-size: 0.7rem;
}
.progress-track {
top: 14px;
top: 18px;
}
}
@@ -789,3 +1152,19 @@
display: block;
}
}
@media (max-width: 480px) {
.progress-bar {
padding: 0 4px;
}
.dot-number {
width: 28px;
height: 28px;
font-size: 0.65rem;
}
.progress-track {
top: 14px;
}
}

View File

@@ -13,6 +13,7 @@
var l10n = typeof umzugslisteL10n !== 'undefined' ? umzugslisteL10n : {};
var TOTAL_STEPS = 9;
var currentStep = 1;
var highestStep = 1;
// ===== Utility Helpers =====
@@ -47,16 +48,23 @@
function showStep(n) {
if (n < 1 || n > TOTAL_STEPS) return;
// Hide all steps
// Determine direction
var direction = n > currentStep ? 'forward' : 'backward';
// Hide all steps and remove direction classes
qsa('.wizard-step').forEach(function(el) {
el.classList.remove('active');
el.classList.remove('active', 'forward', 'backward');
});
// Show target step
// Show target step with direction
var target = qs('.wizard-step[data-step="' + n + '"]');
if (target) target.classList.add('active');
if (target) {
target.classList.add(direction);
target.classList.add('active');
}
currentStep = n;
if (n > highestStep) highestStep = n;
updateProgressBar();
updateNavButtons();
updateRunningTotalsVisibility();
@@ -90,7 +98,7 @@
dot.classList.remove('active', 'completed');
if (step === currentStep) {
dot.classList.add('active');
} else if (step < currentStep) {
} else if (step <= highestStep) {
dot.classList.add('completed');
}
});
@@ -98,9 +106,15 @@
// Update progress fill
var fill = qs('#progress-fill');
if (fill) {
var pct = ((currentStep - 1) / (TOTAL_STEPS - 1)) * 100;
var pct = ((highestStep - 1) / (TOTAL_STEPS - 1)) * 100;
fill.style.width = pct + '%';
}
// Update step counter
var counter = qs('#progress-counter');
if (counter) {
counter.textContent = (l10n.stepLabel || 'Step') + ' ' + currentStep + ' ' + (l10n.stepOf || 'of') + ' ' + TOTAL_STEPS;
}
}
function updateNavButtons() {
@@ -269,6 +283,11 @@
// ===== Summary Generation =====
function summaryHeading(text, gotoStep) {
var editLabel = escHtml(l10n.summaryEdit || 'Edit');
return '<h3>' + escHtml(text) + ' <a class="summary-edit" data-goto="' + gotoStep + '" role="button">' + editLabel + '</a></h3>';
}
function generateSummary() {
var container = qs('#wizard-summary');
if (!container) return;
@@ -277,56 +296,56 @@
// Customer info
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryMovingDate || 'Moving Date') + '</h3>';
var day = getFieldVal('day');
var month = getFieldVal('month');
var year = getFieldVal('year');
html += summaryHeading(l10n.summaryMovingDate || 'Moving Date', 1);
var day = getFieldVal('umzug_day');
var month = getFieldVal('umzug_month');
var year = getFieldVal('umzug_year');
html += summaryRow(l10n.summaryMovingDate || 'Moving Date', day + '.' + month + '.' + year);
html += '</div>';
// Loading address
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryLoading || 'Loading Address') + '</h3>';
html += summaryRow('Name', getFieldVal('bName'));
html += summaryRow('Street', getFieldVal('bStrasse'));
html += summaryRow('ZIP/City', getFieldVal('bort'));
html += summaryHeading(l10n.summaryLoading || 'Loading Address', 1);
html += summaryRow(l10n.summaryName || 'Name', getFieldVal('bName'));
html += summaryRow(l10n.summaryStreet || 'Street', getFieldVal('bStrasse'));
html += summaryRow(l10n.summaryZipCity || 'ZIP/City', getFieldVal('bort'));
var bGeschoss = getFieldVal('info[bGeschoss]');
if (bGeschoss) html += summaryRow('Floor', bGeschoss);
html += summaryRow('Elevator', getRadioVal('info[bLift]'));
html += summaryRow('Phone', getFieldVal('bTelefon'));
if (bGeschoss) html += summaryRow(l10n.summaryFloor || 'Floor', bGeschoss);
html += summaryRow(l10n.summaryElevator || 'Elevator', getRadioVal('info[bLift]'));
html += summaryRow(l10n.summaryPhone || 'Phone', getFieldVal('bTelefon'));
var bFax = getFieldVal('info[bTelefax]');
if (bFax) html += summaryRow('Fax', bFax);
if (bFax) html += summaryRow(l10n.summaryFax || 'Fax', bFax);
var bMobil = getFieldVal('info[bMobil]');
if (bMobil) html += summaryRow('Mobile', bMobil);
html += summaryRow('Email', getFieldVal('info[eE-Mail]'));
if (bMobil) html += summaryRow(l10n.summaryMobile || 'Mobile', bMobil);
html += summaryRow(l10n.summaryEmail || 'Email', getFieldVal('info[eE-Mail]'));
html += '</div>';
// Unloading address
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryUnloading || 'Unloading Address') + '</h3>';
html += summaryRow('Name', getFieldVal('eName'));
html += summaryRow('Street', getFieldVal('eStrasse'));
html += summaryRow('ZIP/City', getFieldVal('eort'));
html += summaryHeading(l10n.summaryUnloading || 'Unloading Address', 1);
html += summaryRow(l10n.summaryName || 'Name', getFieldVal('eName'));
html += summaryRow(l10n.summaryStreet || 'Street', getFieldVal('eStrasse'));
html += summaryRow(l10n.summaryZipCity || 'ZIP/City', getFieldVal('eort'));
var eGeschoss = getFieldVal('info[eGeschoss]');
if (eGeschoss) html += summaryRow('Floor', eGeschoss);
html += summaryRow('Elevator', getRadioVal('info[eLift]'));
if (eGeschoss) html += summaryRow(l10n.summaryFloor || 'Floor', eGeschoss);
html += summaryRow(l10n.summaryElevator || 'Elevator', getRadioVal('info[eLift]'));
var eTel = getFieldVal('eTelefon');
if (eTel) html += summaryRow('Phone', eTel);
if (eTel) html += summaryRow(l10n.summaryPhone || 'Phone', eTel);
var eFax = getFieldVal('info[eTelefax]');
if (eFax) html += summaryRow('Fax', eFax);
if (eFax) html += summaryRow(l10n.summaryFax || 'Fax', eFax);
var eMobil = getFieldVal('info[eMobil]');
if (eMobil) html += summaryRow('Mobile', eMobil);
if (eMobil) html += summaryRow(l10n.summaryMobile || 'Mobile', eMobil);
html += '</div>';
// Room summaries
var roomMap = [
{ key: 'wohnzimmer', name: 'Wohnzimmer' },
{ key: 'schlafzimmer', name: 'Schlafzimmer' },
{ key: 'arbeitszimmer', name: 'Arbeitszimmer' },
{ key: 'bad', name: 'Bad' },
{ key: 'kueche_esszimmer', name: 'Kueche_Esszimmer' },
{ key: 'kinderzimmer', name: 'Kinderzimmer' },
{ key: 'keller', name: 'Keller' }
{ key: 'wohnzimmer', name: 'Wohnzimmer', step: 2 },
{ key: 'schlafzimmer', name: 'Schlafzimmer', step: 3 },
{ key: 'arbeitszimmer', name: 'Arbeitszimmer', step: 4 },
{ key: 'bad', name: 'Bad', step: 5 },
{ key: 'kueche_esszimmer', name: 'Kueche_Esszimmer', step: 5 },
{ key: 'kinderzimmer', name: 'Kinderzimmer', step: 6 },
{ key: 'keller', name: 'Keller', step: 7 }
];
roomMap.forEach(function(room) {
@@ -335,12 +354,12 @@
var total = calculateRoomTotal(room.key);
html += '<div class="summary-section">';
html += '<h3>' + escHtml(getRoomDisplayName(room.key)) + '</h3>';
html += summaryHeading(getRoomDisplayName(room.key), room.step);
roomItems.forEach(function(item) {
html += '<div class="summary-item">';
html += '<span class="summary-item-name">' + escHtml(item.name) + '</span>';
html += '<span class="summary-item-qty">' + item.qty + '</span>';
html += '<span class="summary-item-cbm">' + formatGermanDecimal(item.cbm) + '</span>';
html += '<span class="summary-item-cbm">' + formatGermanDecimal(item.cbm) + ' ' + escHtml(l10n.summaryCbm || 'cbm') + '</span>';
if (item.montage !== null) {
html += '<span class="summary-item-montage">' + escHtml(item.montage === 'ja' ? (l10n.summaryYes || 'Yes') : (l10n.summaryNo || 'No')) + '</span>';
}
@@ -366,7 +385,7 @@
var additionalHtml = getAdditionalWorkSummary();
if (additionalHtml) {
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryAdditional || 'Additional Work') + '</h3>';
html += summaryHeading(l10n.summaryAdditional || 'Additional Work', 8);
html += additionalHtml;
html += '</div>';
}
@@ -375,7 +394,7 @@
var sonstiges = getFieldVal('sonstiges');
if (sonstiges) {
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryOther || 'Other') + '</h3>';
html += summaryHeading(l10n.summaryOther || 'Other', 8);
html += '<p>' + escHtml(sonstiges) + '</p>';
html += '</div>';
}
@@ -390,7 +409,7 @@
function getFieldVal(name) {
var el = qs('[name="' + name + '"]');
if (!el) return '';
if (el.tagName === 'SELECT') return el.options[el.selectedIndex].value;
if (el.tagName === 'SELECT') return el.selectedIndex >= 0 ? el.options[el.selectedIndex].value : '';
return el.value.trim();
}
@@ -400,13 +419,19 @@
}
function getRoomDisplayName(roomKey) {
// Read from the step title or the furniture list heading
var list = qs('.furniture-list[data-room="' + roomKey + '"]');
if (list) {
var card = list.closest('.step-card');
if (card) {
var h3 = qs('h3', card);
if (h3) return h3.textContent;
// For combined steps (step 5), use the h3 section heading
var section = list.closest('.step-section');
if (section) {
var h3 = qs('h3', section);
if (h3) return h3.textContent;
}
// For single-room steps, use the h2 step title
var h2 = qs('h2.step-title', card);
if (h2) return h2.textContent;
}
}
return roomKey;
@@ -510,11 +535,11 @@
});
}
// Progress dot click (backward navigation only)
// Progress dot click (navigate to any visited step)
qsa('.progress-dot').forEach(function(dot) {
dot.addEventListener('click', function() {
var step = parseInt(this.getAttribute('data-step'), 10);
if (step < currentStep) {
if (step <= highestStep && step !== currentStep) {
showStep(step);
}
});
@@ -524,15 +549,47 @@
document.addEventListener('input', function(e) {
if (e.target.classList.contains('quantity-input')) {
handleQuantityChange();
var hasQty = parseGermanDecimal(e.target.value) > 0;
// Toggle has-value class
if (parseGermanDecimal(e.target.value) > 0) {
if (hasQty) {
e.target.classList.add('has-value');
} else {
e.target.classList.remove('has-value');
}
// Toggle has-quantity on parent row for dimming/montage visibility
var row = e.target.closest('.furniture-item');
if (row) row.classList.toggle('has-quantity', hasQty);
}
});
// Auto-check checkbox when qty-small gets a value
document.addEventListener('input', function(e) {
if (!e.target.classList.contains('qty-small')) return;
var field = e.target.closest('.additional-field');
if (!field) return;
var cb = qs('input[type="checkbox"]', field);
if (cb) cb.checked = e.target.value.trim() !== '';
});
// Stepper button click handlers
document.addEventListener('click', function(e) {
var btn = e.target.closest('.qty-btn');
if (!btn) return;
var input = btn.parentNode.querySelector('.quantity-input');
if (!input) return;
var val = parseGermanDecimal(input.value);
if (btn.classList.contains('qty-plus')) val++;
else if (btn.classList.contains('qty-minus') && val > 0) val--;
input.value = val > 0 ? val : '';
input.dispatchEvent(new Event('input', { bubbles: true }));
});
// Summary edit link click handler
document.addEventListener('click', function(e) {
var el = e.target.closest('.summary-edit');
if (el) showStep(parseInt(el.dataset.goto, 10));
});
// Clear field errors on input
document.addEventListener('input', function(e) {
if (e.target.classList.contains('field-error')) {

View File

@@ -69,6 +69,16 @@ class Umzugsliste_Admin_Menu {
array( $this, 'settings_page' ) // Callback
);
// Add Test Email submenu
add_submenu_page(
'umzugsliste',
'Test Email',
'Test Email',
'manage_options',
'umzugsliste-test-email',
array( Umzugsliste_Test_Email::get_instance(), 'render_admin_page' )
);
// Remove duplicate top-level menu item
remove_submenu_page( 'umzugsliste', 'umzugsliste' );
}

View File

@@ -35,20 +35,17 @@ class Umzugsliste_Email_Generator {
// Customer info
$content .= self::generate_customer_info_section( $data );
// All rooms
// All rooms with grand totals in last room's table
$content .= self::generate_all_rooms( $data );
// Additional work sections
$content .= self::generate_additional_work_sections( $data );
// Weitere Arbeiten (single 3-column table)
$content .= self::generate_weitere_arbeiten( $data );
// Sonstiges
if ( ! empty( $data['sonstiges'] ) ) {
$content .= self::generate_sonstiges_section( $data['sonstiges'] );
}
// Grand totals
$content .= self::generate_grand_totals( $data );
// Wrap in HTML document
return self::wrap_html( $content );
}
@@ -135,7 +132,7 @@ class Umzugsliste_Email_Generator {
}
/**
* Generate all room sections
* Generate all room sections with grand totals in last room's table
*
* @param array $data Form data
* @return string HTML
@@ -144,22 +141,66 @@ class Umzugsliste_Email_Generator {
$html = '';
$rooms = Umzugsliste_Furniture_Data::get_rooms();
// Collect rooms that have items
$rooms_with_items = array();
foreach ( $rooms as $room_key => $room_label ) {
// Get post array name for this room
$post_array_name = ucfirst( $room_key );
if ( 'kueche_esszimmer' === $room_key ) {
$post_array_name = 'Kueche_Esszimmer';
}
// Get room data from submission
$room_data = $data[ $post_array_name ] ?? array();
// Only include room if it has items with quantities
if ( self::has_items_with_quantities( $room_data ) ) {
$html .= self::generate_room_section( $room_key, $room_label, $room_data );
$rooms_with_items[ $room_key ] = array(
'label' => $room_label,
'data' => $room_data,
);
}
}
if ( empty( $rooms_with_items ) ) {
return '';
}
$room_keys = array_keys( $rooms_with_items );
$last_room_key = end( $room_keys );
// Grand totals accumulators
$grand_total_quantity = 0;
$grand_total_cbm = 0;
foreach ( $rooms_with_items as $room_key => $room_info ) {
$is_last = ( $room_key === $last_room_key );
$html .= self::generate_room_section(
$room_key,
$room_info['label'],
$room_info['data'],
$is_last
);
// Accumulate grand totals
foreach ( $room_info['data'] as $key => $value ) {
if ( substr( $key, 0, 1 ) === 'v' && ! empty( $value ) && floatval( $value ) > 0 ) {
$item_name = substr( $key, 1 );
$quantity = floatval( str_replace( ',', '.', trim( $value ) ) );
$cbm = isset( $room_info['data'][ 'q' . $item_name ] ) ? floatval( $room_info['data'][ 'q' . $item_name ] ) : 0;
$grand_total_quantity += $quantity;
$grand_total_cbm += ( $quantity * $cbm );
}
}
}
// Grand totals row inside last room's table
$grand_total_display = str_replace( '.', ',', number_format( $grand_total_cbm, 2, '.', '' ) );
$html .= "<tr><th>&nbsp;</th></tr>
<tr>
<th bgcolor='CCCCCC' align='right'>" . $grand_total_quantity . "</th>
<th bgcolor='CCCCCC' align='left'>Gesamtsummen</th>
<th bgcolor='CCCCCC' colspan='2' align='right'>" . esc_html( $grand_total_display ) . "</th>
<th bgcolor='CCCCCC'>&nbsp;</th>
</tr></tbody></table></div></div>";
return $html;
}
@@ -184,9 +225,10 @@ class Umzugsliste_Email_Generator {
* @param string $room_key Room key
* @param string $room_label Room label
* @param array $room_data Room submission data
* @param bool $is_last Whether this is the last room (keeps table open for grand totals)
* @return string HTML
*/
private static function generate_room_section( $room_key, $room_label, $room_data ) {
private static function generate_room_section( $room_key, $room_label, $room_data, $is_last = false ) {
$html = "<div class='row'>
<div class='large-12 columns' style='margin: 10px 0px; overflow-x: auto;'>
<table width='100%'>
@@ -212,9 +254,6 @@ class Umzugsliste_Email_Generator {
$room_total_quantity = 0;
$room_total_cbm = 0;
// Process items in groups of v, q, m
$processed_items = array();
foreach ( $room_data as $key => $value ) {
if ( substr( $key, 0, 1 ) === 'v' ) {
$item_name = substr( $key, 1 );
@@ -238,8 +277,6 @@ class Umzugsliste_Email_Generator {
$html .= "<td align='right'>" . esc_html( $total_display ) . '</td>';
$html .= '<td>&nbsp;' . esc_html( $montage ) . '</td>';
$html .= '</tr>';
$processed_items[] = $item_name;
}
}
}
@@ -254,165 +291,353 @@ class Umzugsliste_Email_Generator {
<th bgcolor='CCCCCC'>&nbsp;</th>
</tr>";
// Only close table if NOT the last room (last room stays open for grand totals)
if ( ! $is_last ) {
$html .= '</tbody></table></div></div>';
}
return $html;
}
/**
* Get field key matching how form handler stores it
*
* @param array $field Field definition
* @return string Field key
*/
private static function get_field_key( $field ) {
if ( ! empty( $field['key'] ) ) {
return sanitize_key( $field['key'] );
}
return sanitize_title( $field['name'] );
}
/**
* Get field value from submitted data
*
* @param array $section_data Submitted data for section
* @param array $field Field definition
* @return string Field value
*/
private static function get_field_value( $section_data, $field ) {
$key = self::get_field_key( $field );
return $section_data[ $key ] ?? '';
}
/**
* Get _anzahl value for checkbox_anzahl fields
*
* @param array $section_data Submitted data for section
* @param array $field Field definition
* @return string Anzahl value
*/
private static function get_field_anzahl( $section_data, $field ) {
$key = self::get_field_key( $field );
return $section_data[ $key . '_anzahl' ] ?? '';
}
/**
* Generate Weitere Arbeiten section as single 3-column table
*
* @param array $data Form data
* @return string HTML
*/
private static function generate_weitere_arbeiten( $data ) {
$additional_data = $data['additional_work'] ?? array();
$sections = Umzugsliste_Furniture_Data::get_additional_work();
$html = "<div class='row'>
<div class='large-12 columns' style='margin: 10px 0px; overflow-x: auto;'>
<table width='100%'>
<thead>
<tr>
<th align='left' bgcolor='#CCCCCC'>Weitere Arbeiten (bitte ankreuzen)</th>
<th bgcolor='#CCCCCC'>&nbsp;</th>
<th bgcolor='#CCCCCC'>&nbsp;</th>
</tr>
</thead>
<tbody>";
$html .= self::generate_montage_rows( $additional_data['montage'] ?? array(), $sections['montage'] );
$html .= self::generate_schrank_rows( $additional_data['schrank'] ?? array(), $sections['schrank'] );
$html .= self::generate_elektriker_rows( $additional_data['elektriker'] ?? array(), $sections['elektriker'] );
$html .= self::generate_duebelarbeiten_rows( $additional_data['duebelarbeiten'] ?? array(), $sections['duebelarbeiten'] );
$html .= self::generate_packarbeiten_rows( $additional_data['packarbeiten'] ?? array(), $sections['packarbeiten'] );
$html .= self::generate_anfahrt_rows( $additional_data['anfahrt'] ?? array(), $sections['anfahrt'] );
// anfahrt_rows closes the table
return $html;
}
/**
* Generate Montagearbeiten rows
*
* @param array $section_data Submitted data
* @param array $section_def Section field definitions
* @return string HTML rows
*/
private static function generate_montage_rows( $section_data, $section_def ) {
$html = "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Montagearbeiten</th>
</tr>";
foreach ( $section_def['fields'] as $field ) {
$value = self::get_field_value( $section_data, $field );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . $checked . '</td>';
$html .= '<td>&nbsp;</td>';
$html .= '</tr>';
}
return $html;
}
/**
* Generate Schrank rows with Abbau/Aufbau columns
*
* @param array $section_data Submitted data
* @param array $section_def Section field definitions
* @return string HTML rows
*/
private static function generate_schrank_rows( $section_data, $section_def ) {
$html = "<tr>
<th bgcolor='#CCCCCC'>Schrank</th>
<th bgcolor='#CCCCCC'>Abbau</th>
<th bgcolor='#CCCCCC'>Aufbau</th>
</tr>";
foreach ( $section_def['fields'] as $field ) {
$value = self::get_field_value( $section_data, $field );
$abbau = '&nbsp;';
$aufbau = '&nbsp;';
if ( 'Abbau' === $value ) {
$abbau = 'X';
} elseif ( 'Aufbau' === $value ) {
$aufbau = 'X';
} elseif ( 'Beides' === $value ) {
$abbau = 'X';
$aufbau = 'X';
}
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . $abbau . '</td>';
$html .= '<td>' . $aufbau . '</td>';
$html .= '</tr>';
}
return $html;
}
/**
* Generate Elektriker/Installateur rows
*
* @param array $section_data Submitted data
* @param array $section_def Section field definitions
* @return string HTML rows
*/
private static function generate_elektriker_rows( $section_data, $section_def ) {
$html = "<tr>
<th bgcolor='#CCCCCC'>Elektriker/Installateur</th>
<th bgcolor='#CCCCCC'>&nbsp;</th>
<th bgcolor='#CCCCCC'>Anzahl</th>
</tr>";
foreach ( $section_def['fields'] as $field ) {
$value = self::get_field_value( $section_data, $field );
$anzahl = self::get_field_anzahl( $section_data, $field );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$anzahl_display = ! empty( $anzahl ) ? esc_html( $anzahl ) : '&nbsp;';
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . $checked . '</td>';
$html .= '<td>' . $anzahl_display . '</td>';
$html .= '</tr>';
}
return $html;
}
/**
* Generate Dübelarbeiten rows
*
* @param array $section_data Submitted data
* @param array $section_def Section field definitions
* @return string HTML rows
*/
private static function generate_duebelarbeiten_rows( $section_data, $section_def ) {
$html = "<tr>
<th bgcolor='#CCCCCC'>D&uuml;belarbeiten</th>
<th bgcolor='#CCCCCC'>&nbsp;</th>
<th bgcolor='#CCCCCC'>Anzahl</th>
</tr>";
foreach ( $section_def['fields'] as $field ) {
$value = self::get_field_value( $section_data, $field );
$anzahl = self::get_field_anzahl( $section_data, $field );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$anzahl_display = ! empty( $anzahl ) ? esc_html( $anzahl ) : '&nbsp;';
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . $checked . '</td>';
$html .= '<td>' . $anzahl_display . '</td>';
$html .= '</tr>';
}
return $html;
}
/**
* Generate Packarbeiten rows with sub-headers
*
* @param array $section_data Submitted data
* @param array $section_def Section field definitions
* @return string HTML rows
*/
private static function generate_packarbeiten_rows( $section_data, $section_def ) {
$fields = $section_def['fields'];
// Packarbeiten header + first 2 checkbox rows
$html = "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Packarbeiten</th>
</tr>";
for ( $i = 0; $i < 2 && $i < count( $fields ); $i++ ) {
$value = self::get_field_value( $section_data, $fields[ $i ] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr>';
$html .= '<td>' . esc_html( $fields[ $i ]['name'] ) . '</td>';
$html .= '<td>' . $checked . '</td>';
$html .= '<td>&nbsp;</td>';
$html .= '</tr>';
}
// "Wir haben spezielle Packwünsche:" sub-header + next 2 checkbox rows
$html .= "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Wir haben spezielle Packw&uuml;nsche:</th>
</tr>";
for ( $i = 2; $i < 4 && $i < count( $fields ); $i++ ) {
$value = self::get_field_value( $section_data, $fields[ $i ] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr>';
$html .= '<td>' . esc_html( $fields[ $i ]['name'] ) . '</td>';
$html .= '<td>' . $checked . '</td>';
$html .= '<td>&nbsp;</td>';
$html .= '</tr>';
}
// "Packmaterial" sub-header + 2 text quantity rows
$html .= "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Packmaterial</th>
</tr>";
for ( $i = 4; $i < 6 && $i < count( $fields ); $i++ ) {
$value = self::get_field_value( $section_data, $fields[ $i ] );
$value_display = ! empty( $value ) ? esc_html( $value ) : '&nbsp;';
$html .= '<tr>';
$html .= '<td>' . esc_html( $fields[ $i ]['name'] ) . '</td>';
$html .= '<td>&nbsp;</td>';
$html .= '<td>' . $value_display . '</td>';
$html .= '</tr>';
}
return $html;
}
/**
* Generate Anfahrt rows with nested sub-headers (also closes the table)
*
* @param array $section_data Submitted data
* @param array $section_def Section field definitions
* @return string HTML rows including table close
*/
private static function generate_anfahrt_rows( $section_data, $section_def ) {
$fields = $section_def['fields'];
$html = "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Anfahrt</th>
</tr>";
// "LKW kann direkt vor den Eingang fahren" sub-header
$html .= "<tr>
<th bgcolor='#CCCCCC' colspan='3'>LKW kann direkt vor den Eingang fahren</th>
</tr>";
// Beladestelle (field index 0)
$value = self::get_field_value( $section_data, $fields[0] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr><td>Beladestelle</td><td>' . $checked . '</td><td>&nbsp;</td></tr>';
// Entladestelle (field index 1)
$value = self::get_field_value( $section_data, $fields[1] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr><td>Entladestelle</td><td>' . $checked . '</td><td>&nbsp;</td></tr>';
// "Parkverbotsschilder aufstellen" sub-header
$html .= "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Parkverbotsschilder aufstellen</th>
</tr>";
// Beladestelle (field index 2)
$value = self::get_field_value( $section_data, $fields[2] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr><td>Beladestelle</td><td>' . $checked . '</td><td>&nbsp;</td></tr>';
// Entladestelle (field index 3)
$value = self::get_field_value( $section_data, $fields[3] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr><td>Entladestelle</td><td>' . $checked . '</td><td>&nbsp;</td></tr>';
// "Die Anfahrt ist eng bzw. nicht möglich" sub-header
$html .= "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Die Anfahrt ist eng bzw. nicht m&ouml;glich</th>
</tr>";
// Beladestelle (field index 4)
$value = self::get_field_value( $section_data, $fields[4] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr><td>Beladestelle</td><td>' . $checked . '</td><td>&nbsp;</td></tr>';
// Entladestelle (field index 5)
$value = self::get_field_value( $section_data, $fields[5] );
$checked = ( 'ja' === $value ) ? 'X' : '&nbsp;';
$html .= '<tr><td>Entladestelle</td><td>' . $checked . '</td><td>&nbsp;</td></tr>';
// "Abtrageweg" sub-header
$html .= "<tr>
<th bgcolor='#CCCCCC' colspan='3'>Abtrageweg</th>
</tr>";
// Beladestelle distance (field index 6) - value in col3
$value = self::get_field_value( $section_data, $fields[6] );
$value_display = ! empty( $value ) ? esc_html( $value ) : '&nbsp;';
$html .= '<tr><td>Beladestelle Wegstrecke Haus-LKW in Meter</td><td>&nbsp;</td><td>' . $value_display . '</td></tr>';
// Entladestelle distance (field index 7) - value in col3, also closes table
$value = self::get_field_value( $section_data, $fields[7] );
$value_display = ! empty( $value ) ? esc_html( $value ) : '&nbsp;';
$html .= '<tr><td>Entladestelle Wegstrecke LKW-Haus in Meter</td><td>&nbsp;</td><td>' . $value_display . '</td></tr>';
// Close the Weitere Arbeiten table
$html .= '</tbody></table></div></div>';
return $html;
}
/**
* Generate grand totals section
*
* @param array $data Form data
* @return string HTML
*/
private static function generate_grand_totals( $data ) {
$grand_total_quantity = 0;
$grand_total_cbm = 0;
$rooms = Umzugsliste_Furniture_Data::get_rooms();
foreach ( $rooms as $room_key => $room_label ) {
$post_array_name = ucfirst( $room_key );
if ( 'kueche_esszimmer' === $room_key ) {
$post_array_name = 'Kueche_Esszimmer';
}
$room_data = $data[ $post_array_name ] ?? array();
foreach ( $room_data as $key => $value ) {
if ( substr( $key, 0, 1 ) === 'v' && ! empty( $value ) && floatval( $value ) > 0 ) {
$item_name = substr( $key, 1 );
$quantity = floatval( str_replace( ',', '.', trim( $value ) ) );
$cbm = isset( $room_data[ 'q' . $item_name ] ) ? floatval( $room_data[ 'q' . $item_name ] ) : 0;
$grand_total_quantity += $quantity;
$grand_total_cbm += ( $quantity * $cbm );
}
}
}
$grand_total_display = str_replace( '.', ',', number_format( $grand_total_cbm, 2, '.', '' ) );
return "<tr><th>&nbsp;</th></tr>
<tr>
<th bgcolor='CCCCCC' align='right'>" . $grand_total_quantity . "</th>
<th bgcolor='CCCCCC' align='left'>Gesamtsummen</th>
<th bgcolor='CCCCCC' colspan='2' align='right'>" . esc_html( $grand_total_display ) . "</th>
<th bgcolor='CCCCCC'>&nbsp;</th>
</tr></tbody></table></div></div>";
}
/**
* Generate additional work sections
*
* @param array $data Form data
* @return string HTML
*/
private static function generate_additional_work_sections( $data ) {
$html = '';
$sections = Umzugsliste_Furniture_Data::get_additional_work();
foreach ( $sections as $section_key => $section_data ) {
// Only include section if it has data
if ( self::has_additional_work_data( $data, $section_key ) ) {
$html .= "<div class='row'>
<div class='large-12 columns' style='margin: 10px 0px; overflow-x: auto;'>
<table width='100%'>
<thead>
<tr>
<th align='left' bgcolor='#CCCCCC' colspan='2'>" . esc_html( $section_data['label'] ) . "</th>
</tr>
</thead>
<tbody>";
$section_submitted_data = $data['additional_work'][ $section_key ] ?? array();
foreach ( $section_data['fields'] as $field ) {
// Get field key
$field_key = ! empty( $field['key'] ) ? $field['key'] : sanitize_title( $field['name'] );
// Get field value
$field_value = $section_submitted_data[ $field_key ] ?? '';
// Render based on field type
switch ( $field['type'] ) {
case 'checkbox':
if ( 'ja' === $field_value ) {
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>Ja</td>';
$html .= '</tr>';
}
break;
case 'abbau_aufbau':
if ( ! empty( $field_value ) ) {
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . esc_html( $field_value ) . '</td>';
$html .= '</tr>';
}
break;
case 'checkbox_anzahl':
if ( 'ja' === $field_value ) {
$anzahl_value = $section_submitted_data[ $field_key . '_anzahl' ] ?? '';
$display_value = 'Ja';
if ( ! empty( $anzahl_value ) ) {
$display_value .= ' (Anzahl: ' . esc_html( $anzahl_value ) . ')';
}
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . $display_value . '</td>';
$html .= '</tr>';
}
break;
case 'text':
if ( ! empty( $field_value ) ) {
$html .= '<tr>';
$html .= '<td>' . esc_html( $field['name'] ) . '</td>';
$html .= '<td>' . esc_html( $field_value ) . '</td>';
$html .= '</tr>';
}
break;
}
}
$html .= '</tbody></table></div></div>';
}
}
return $html;
}
/**
* Check if section has any data
*
* @param array $data Form data
* @param string $section_key Section key
* @return bool True if has data
*/
private static function has_additional_work_data( $data, $section_key ) {
if ( empty( $data['additional_work'][ $section_key ] ) ) {
return false;
}
$section_data = $data['additional_work'][ $section_key ];
if ( ! is_array( $section_data ) ) {
return false;
}
// Check if any value is non-empty
foreach ( $section_data as $value ) {
if ( ! empty( trim( $value ) ) ) {
return true;
}
}
return false;
}
/**
* Generate Sonstiges section
*
@@ -448,7 +673,7 @@ class Umzugsliste_Email_Generator {
return "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'>
<html>
<head>
<title>Siegel Umzüge - Internetanfrage</title>
<title>Siegel-Umzug</title>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
</head>
<body>" . $content . "</body>

View File

@@ -46,7 +46,7 @@ class Umzugsliste_Form_Renderer {
ob_start();
?>
<div class="umzugsliste-wizard">
<div class="umzugsliste-wizard palette-b">
<?php self::render_validation_errors(); ?>
<?php self::render_progress_bar( $steps ); ?>
<div class="running-totals" id="running-totals">
@@ -82,6 +82,20 @@ class Umzugsliste_Form_Renderer {
<button type="submit" class="wizard-btn wizard-btn-submit" id="wizard-submit" style="display:none;"><?php echo esc_html__( 'Submit Request', 'siegel-umzugsliste' ); ?></button>
</div>
</form>
<?php if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) : ?>
<button type="button" id="dev-palette-switch" class="dev-palette-btn">&#127912; B</button>
<script>
document.getElementById('dev-palette-switch').addEventListener('click', function() {
var w = document.querySelector('.umzugsliste-wizard');
var p = ['palette-a', 'palette-b', 'palette-c'];
var c = p.findIndex(function(x) { return w.classList.contains(x); });
var n = (c + 1) % p.length;
w.classList.remove(p[c]);
w.classList.add(p[n]);
this.textContent = '\u{1F3A8} ' + p[n].split('-')[1].toUpperCase();
});
</script>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
@@ -134,6 +148,7 @@ class Umzugsliste_Form_Renderer {
<?php endforeach; ?>
</div>
</div>
<div class="progress-counter" id="progress-counter"></div>
<?php
}
@@ -143,9 +158,8 @@ class Umzugsliste_Form_Renderer {
private static function render_step_1() {
?>
<div class="wizard-step active" data-step="1">
<h2 class="step-title"><?php echo esc_html__( 'Moving Date & Addresses', 'siegel-umzugsliste' ); ?></h2>
<div class="step-card">
<h2 class="step-title"><?php echo esc_html__( 'Moving Date & Addresses', 'siegel-umzugsliste' ); ?></h2>
<h3><?php echo esc_html__( 'Expected Moving Date', 'siegel-umzugsliste' ); ?></h3>
<div class="date-selector">
<?php
@@ -160,38 +174,134 @@ class Umzugsliste_Form_Renderer {
'<a href="http://siegel-umzug.de/datenschutz.html" target="_blank" rel="noopener">' . esc_html__( 'Privacy Policy', 'siegel-umzugsliste' ) . '</a>'
);
?></p>
<div class="address-grid">
<div class="address-section">
<h3><?php echo esc_html__( 'Loading Address', 'siegel-umzugsliste' ); ?></h3>
<?php
self::render_address_field( __( 'Name', 'siegel-umzugsliste' ), 'bName', true );
self::render_address_field( __( 'Street', 'siegel-umzugsliste' ), 'bStrasse', true );
self::render_address_field( __( 'ZIP/City', 'siegel-umzugsliste' ), 'bort', true );
self::render_address_field( __( 'Floor', 'siegel-umzugsliste' ), 'info[bGeschoss]' );
self::render_lift_field( 'info[bLift]' );
self::render_address_field( __( 'Phone', 'siegel-umzugsliste' ), 'bTelefon', true );
self::render_address_field( __( 'Fax', 'siegel-umzugsliste' ), 'info[bTelefax]' );
self::render_address_field( __( 'Mobile', 'siegel-umzugsliste' ), 'info[bMobil]' );
self::render_address_field( __( 'Email', 'siegel-umzugsliste' ), 'info[eE-Mail]', true, 'email' );
?>
</div>
<div class="address-section">
<h3><?php echo esc_html__( 'Unloading Address', 'siegel-umzugsliste' ); ?></h3>
<?php
self::render_address_field( __( 'Name', 'siegel-umzugsliste' ), 'eName', true );
self::render_address_field( __( 'Street', 'siegel-umzugsliste' ), 'eStrasse', true );
self::render_address_field( __( 'ZIP/City', 'siegel-umzugsliste' ), 'eort', true );
self::render_address_field( __( 'Floor', 'siegel-umzugsliste' ), 'info[eGeschoss]' );
self::render_lift_field( 'info[eLift]' );
self::render_address_field( __( 'Phone', 'siegel-umzugsliste' ), 'eTelefon' );
self::render_address_field( __( 'Fax', 'siegel-umzugsliste' ), 'info[eTelefax]' );
self::render_address_field( __( 'Mobile', 'siegel-umzugsliste' ), 'info[eMobil]' );
?>
</div>
</div>
<p class="required-note"><?php echo esc_html__( '* Required fields', 'siegel-umzugsliste' ); ?></p>
</div>
<?php if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) : ?>
<button type="button" id="dev-autofill" class="dev-autofill-btn">&#9881; Fill All</button>
<script>
document.getElementById('dev-autofill').addEventListener('click', function() {
function setField(name, val) {
var el = document.querySelector('[name="'+name+'"]');
if (el) { el.value = val; el.dispatchEvent(new Event('input',{bubbles:true})); }
}
function setRadio(name, val) {
var el = document.querySelector('[name="'+name+'"][value="'+val+'"]');
if (el) el.checked = true;
}
<div class="address-grid">
<div class="step-card">
<h3><?php echo esc_html__( 'Loading Address', 'siegel-umzugsliste' ); ?></h3>
<?php
self::render_address_field( __( 'Name', 'siegel-umzugsliste' ), 'bName', true );
self::render_address_field( __( 'Street', 'siegel-umzugsliste' ), 'bStrasse', true );
self::render_address_field( __( 'ZIP/City', 'siegel-umzugsliste' ), 'bort', true );
self::render_address_field( __( 'Floor', 'siegel-umzugsliste' ), 'info[bGeschoss]' );
self::render_lift_field( 'info[bLift]' );
self::render_address_field( __( 'Phone', 'siegel-umzugsliste' ), 'bTelefon', true );
self::render_address_field( __( 'Fax', 'siegel-umzugsliste' ), 'info[bTelefax]' );
self::render_address_field( __( 'Mobile', 'siegel-umzugsliste' ), 'info[bMobil]' );
self::render_address_field( __( 'Email', 'siegel-umzugsliste' ), 'info[eE-Mail]', true, 'email' );
?>
</div>
<div class="step-card">
<h3><?php echo esc_html__( 'Unloading Address', 'siegel-umzugsliste' ); ?></h3>
<?php
self::render_address_field( __( 'Name', 'siegel-umzugsliste' ), 'eName', true );
self::render_address_field( __( 'Street', 'siegel-umzugsliste' ), 'eStrasse', true );
self::render_address_field( __( 'ZIP/City', 'siegel-umzugsliste' ), 'eort', true );
self::render_address_field( __( 'Floor', 'siegel-umzugsliste' ), 'info[eGeschoss]' );
self::render_lift_field( 'info[eLift]' );
self::render_address_field( __( 'Phone', 'siegel-umzugsliste' ), 'eTelefon' );
self::render_address_field( __( 'Fax', 'siegel-umzugsliste' ), 'info[eTelefax]' );
self::render_address_field( __( 'Mobile', 'siegel-umzugsliste' ), 'info[eMobil]' );
?>
</div>
</div>
<p class="required-note"><?php echo esc_html__( '* Required fields', 'siegel-umzugsliste' ); ?></p>
/* Step 1: Date — option values are plain numbers (no zero-pad) */
var d = new Date();
setField('umzug_day', String(d.getDate()));
setField('umzug_month', String(d.getMonth()+1));
setField('umzug_year', String(d.getFullYear()));
/* Step 1: Addresses */
var addr = {
'bName':'Max Mustermann','bStrasse':'Musterstraße 42',
'bort':'65197 Wiesbaden','bTelefon':'0611 123456',
'eName':'Erika Musterfrau','eStrasse':'Beispielweg 7',
'eort':'55116 Mainz','eTelefon':'06131 654321',
'info[bGeschoss]':'2. OG','info[eGeschoss]':'EG',
'info[bTelefax]':'0611 123457','info[eTelefax]':'06131 654322',
'info[bMobil]':'0170 1234567','info[eMobil]':'0171 7654321',
'info[eE-Mail]':'test@example.com'
};
for (var n in addr) setField(n, addr[n]);
setRadio('info[bLift]','ja');
/* Steps 2-7: Furniture — ALL items in every room */
document.querySelectorAll('.furniture-list').forEach(function(list) {
var items = list.querySelectorAll('.furniture-item');
for (var i = 0; i < items.length; i++) {
var qty = (i % 3) + 1;
var inp = items[i].querySelector('.quantity-input');
if (inp) {
inp.value = String(qty);
inp.classList.add('has-value');
items[i].classList.add('has-quantity');
inp.dispatchEvent(new Event('input',{bubbles:true}));
}
if (i % 2 === 0) {
var mj = items[i].querySelector('.montage-toggle input[value="ja"]');
if (mj) mj.checked = true;
}
}
});
/* Step 8: Additional work — Montage checkboxes */
document.querySelectorAll('[data-section="montage"] input[type="checkbox"]').forEach(function(cb) { cb.checked = true; });
/* Schrank radios — cycle Abbau/Aufbau/Beides */
var abbauVals = ['Abbau','Aufbau','Beides'], ai = 0;
document.querySelectorAll('[data-section="schrank"] .additional-field-abbau').forEach(function(f) {
var r = f.querySelector('input[value="'+abbauVals[ai%3]+'"]');
if (r) r.checked = true;
ai++;
});
/* Elektriker — check + anzahl */
var ez = 1;
document.querySelectorAll('[data-section="elektriker"] .additional-field-qty').forEach(function(f) {
var cb = f.querySelector('input[type="checkbox"]'); if (cb) cb.checked = true;
var q = f.querySelector('.qty-small'); if (q) q.value = String(ez++);
});
/* Dübelarbeiten — check + anzahl */
var dz = 2;
document.querySelectorAll('[data-section="duebelarbeiten"] .additional-field-qty').forEach(function(f) {
var cb = f.querySelector('input[type="checkbox"]'); if (cb) cb.checked = true;
var q = f.querySelector('.qty-small'); if (q) { q.value = String(dz); dz += 2; }
});
/* Packarbeiten — all checkboxes + text quantities */
document.querySelectorAll('[data-section="packarbeiten"] input[type="checkbox"]').forEach(function(cb) { cb.checked = true; });
var pv = [25, 5], pi = 0;
document.querySelectorAll('[data-section="packarbeiten"] .additional-field-text .qty-small').forEach(function(inp) {
inp.value = String(pv[pi] || 5); pi++;
});
/* Anfahrt — all checkboxes + distances */
document.querySelectorAll('[data-section="anfahrt"] input[type="checkbox"]').forEach(function(cb) { cb.checked = true; });
var av = [15, 25], avi = 0;
document.querySelectorAll('[data-section="anfahrt"] .additional-field-text .qty-small').forEach(function(inp) {
inp.value = String(av[avi] || 25); avi++;
});
/* Sonstiges */
var s = document.querySelector('[name="sonstiges"]');
if (s) s.value = 'Bitte vorsichtig mit dem antiken Schrank im Wohnzimmer.\nDas Klavier muss besonders geschützt werden.';
});
</script>
<?php endif; ?>
</div>
<?php
}
@@ -213,8 +323,8 @@ class Umzugsliste_Form_Renderer {
}
?>
<div class="wizard-step" data-step="<?php echo esc_attr( $step_num ); ?>">
<h2 class="step-title"><?php echo esc_html( $room_label ); ?></h2>
<div class="step-card">
<h2 class="step-title"><?php echo esc_html( $room_label ); ?></h2>
<div class="furniture-list" data-room="<?php echo esc_attr( $room_key ); ?>">
<?php
foreach ( $items as $item ) {
@@ -240,10 +350,10 @@ class Umzugsliste_Form_Renderer {
$rooms = Umzugsliste_Furniture_Data::get_rooms();
?>
<div class="wizard-step" data-step="5">
<h2 class="step-title"><?php echo esc_html( $rooms['bad'] ); ?> &amp; <?php echo esc_html( $rooms['kueche_esszimmer'] ); ?></h2>
<div class="step-card">
<h3><?php echo esc_html( $rooms['bad'] ); ?></h3>
<h2 class="step-title"><?php echo esc_html( $rooms['bad'] ); ?> &amp; <?php echo esc_html( $rooms['kueche_esszimmer'] ); ?></h2>
<div class="step-section">
<h3><?php echo esc_html( $rooms['bad'] ); ?></h3>
<div class="furniture-list" data-room="bad">
<?php
$bad_items = Umzugsliste_Furniture_Data::get_furniture_items( 'bad' );
@@ -257,23 +367,23 @@ class Umzugsliste_Form_Renderer {
<span class="room-totals-sep">&middot;</span>
<span class="room-total-cbm">0,00</span> <?php echo esc_html__( 'cbm', 'siegel-umzugsliste' ); ?>
</div>
</div>
</div>
</div>
<div class="step-card">
<h3><?php echo esc_html( $rooms['kueche_esszimmer'] ); ?></h3>
<div class="furniture-list" data-room="kueche_esszimmer">
<?php
$kueche_items = Umzugsliste_Furniture_Data::get_furniture_items( 'kueche_esszimmer' );
foreach ( $kueche_items as $item ) {
self::render_furniture_item( 'Kueche_Esszimmer', 'kueche_esszimmer', $item );
}
?>
<div class="room-totals" data-room="kueche_esszimmer">
<span class="room-total-label"><?php echo esc_html__( 'Total', 'siegel-umzugsliste' ) . ' ' . esc_html( $rooms['kueche_esszimmer'] ); ?>:</span>
<span class="room-total-quantity">0</span> <?php echo esc_html__( 'Items', 'siegel-umzugsliste' ); ?>
<span class="room-totals-sep">&middot;</span>
<span class="room-total-cbm">0,00</span> <?php echo esc_html__( 'cbm', 'siegel-umzugsliste' ); ?>
<div class="step-section">
<h3><?php echo esc_html( $rooms['kueche_esszimmer'] ); ?></h3>
<div class="furniture-list" data-room="kueche_esszimmer">
<?php
$kueche_items = Umzugsliste_Furniture_Data::get_furniture_items( 'kueche_esszimmer' );
foreach ( $kueche_items as $item ) {
self::render_furniture_item( 'Kueche_Esszimmer', 'kueche_esszimmer', $item );
}
?>
<div class="room-totals" data-room="kueche_esszimmer">
<span class="room-total-label"><?php echo esc_html__( 'Total', 'siegel-umzugsliste' ) . ' ' . esc_html( $rooms['kueche_esszimmer'] ); ?>:</span>
<span class="room-total-quantity">0</span> <?php echo esc_html__( 'Items', 'siegel-umzugsliste' ); ?>
<span class="room-totals-sep">&middot;</span>
<span class="room-total-cbm">0,00</span> <?php echo esc_html__( 'cbm', 'siegel-umzugsliste' ); ?>
</div>
</div>
</div>
</div>
@@ -288,27 +398,29 @@ class Umzugsliste_Form_Renderer {
$sections = Umzugsliste_Furniture_Data::get_additional_work();
?>
<div class="wizard-step" data-step="8">
<h2 class="step-title"><?php echo esc_html__( 'Additional Work', 'siegel-umzugsliste' ); ?></h2>
<?php foreach ( $sections as $section_key => $section_data ) : ?>
<div class="step-card">
<h3><?php echo esc_html( $section_data['label'] ); ?></h3>
<div class="additional-work-section" data-section="<?php echo esc_attr( $section_key ); ?>">
<?php
foreach ( $section_data['fields'] as $field ) {
$field_key = self::get_field_key( $field );
$field_name = 'additional_work[' . $section_key . '][' . $field_key . ']';
self::render_additional_field( $field, $field_name, $field_key );
}
?>
</div>
</div>
<?php endforeach; ?>
<div class="step-card">
<h3><?php echo esc_html__( 'Other', 'siegel-umzugsliste' ); ?></h3>
<label for="sonstiges"><?php echo esc_html__( 'Additional notes or requests:', 'siegel-umzugsliste' ); ?></label>
<textarea name="sonstiges" id="sonstiges" rows="5" class="sonstiges-textarea" placeholder="<?php echo esc_attr__( 'Additional notes or requests...', 'siegel-umzugsliste' ); ?>"></textarea>
<h2 class="step-title"><?php echo esc_html__( 'Additional Work', 'siegel-umzugsliste' ); ?></h2>
<?php foreach ( $sections as $section_key => $section_data ) : ?>
<div class="step-section">
<h3><?php echo esc_html( $section_data['label'] ); ?></h3>
<div class="additional-work-section" data-section="<?php echo esc_attr( $section_key ); ?>">
<?php
foreach ( $section_data['fields'] as $field ) {
$field_key = self::get_field_key( $field );
$field_name = 'additional_work[' . $section_key . '][' . $field_key . ']';
self::render_additional_field( $field, $field_name, $field_key );
}
?>
</div>
</div>
<?php endforeach; ?>
<div class="step-section">
<h3><?php echo esc_html__( 'Other', 'siegel-umzugsliste' ); ?></h3>
<label for="sonstiges"><?php echo esc_html__( 'Additional notes or requests:', 'siegel-umzugsliste' ); ?></label>
<textarea name="sonstiges" id="sonstiges" rows="5" class="sonstiges-textarea"></textarea>
</div>
</div>
</div>
<?php
@@ -323,15 +435,17 @@ class Umzugsliste_Form_Renderer {
$captcha = Umzugsliste_Captcha::get_instance();
?>
<div class="wizard-step" data-step="9">
<h2 class="step-title"><?php echo esc_html__( 'Summary', 'siegel-umzugsliste' ); ?></h2>
<div id="wizard-summary"></div>
<?php
if ( $captcha->is_enabled() ) {
echo '<div class="step-card">';
echo $captcha->render_widget();
echo '</div>';
}
?>
<div class="step-card">
<h2 class="step-title"><?php echo esc_html__( 'Summary', 'siegel-umzugsliste' ); ?></h2>
<div id="wizard-summary"></div>
<?php
if ( $captcha->is_enabled() ) {
echo '<div class="captcha-section">';
echo $captcha->render_widget();
echo '</div>';
}
?>
</div>
<?php wp_nonce_field( 'umzugsliste_submit', 'umzugsliste_nonce' ); ?>
<input type="hidden" name="umzugsliste_submit" value="1">
<input type="hidden" name="umzugsliste_form_id" value="<?php echo esc_attr( $form_id ); ?>">
@@ -391,12 +505,17 @@ class Umzugsliste_Form_Renderer {
$montage_name = $room_name . '[m' . $item_name . ']';
?>
<div class="furniture-item" data-room="<?php echo esc_attr( $room_key ); ?>" data-cbm="<?php echo esc_attr( $cbm ); ?>">
<input type="text" name="<?php echo esc_attr( $quantity_name ); ?>" class="quantity-input" inputmode="decimal" placeholder="0" maxlength="3">
<div class="quantity-stepper">
<button type="button" class="qty-btn qty-minus" aria-label="<?php echo esc_attr__( 'Decrease', 'siegel-umzugsliste' ); ?>">-</button>
<input type="text" name="<?php echo esc_attr( $quantity_name ); ?>" class="quantity-input" inputmode="numeric" placeholder="0" maxlength="3">
<button type="button" class="qty-btn qty-plus" aria-label="<?php echo esc_attr__( 'Increase', 'siegel-umzugsliste' ); ?>">+</button>
</div>
<span class="item-name"><?php echo esc_html( $item_name ); ?></span>
<span class="item-cbm"><?php echo esc_html( str_replace( '.', ',', (string) $cbm ) ); ?></span>
<span class="item-cbm"><?php echo esc_html( str_replace( '.', ',', (string) $cbm ) ); ?> <?php echo esc_html__( 'cbm', 'siegel-umzugsliste' ); ?></span>
<input type="hidden" name="<?php echo esc_attr( $cbm_name ); ?>" value="<?php echo esc_attr( $cbm ); ?>">
<?php if ( $has_montage ) : ?>
<div class="montage-toggle">
<span class="montage-label"><?php echo esc_html__( 'Montage?', 'siegel-umzugsliste' ); ?></span>
<label class="radio-label"><input type="radio" name="<?php echo esc_attr( $montage_name ); ?>" value="nein" checked> <?php echo esc_html__( 'No', 'siegel-umzugsliste' ); ?></label>
<label class="radio-label"><input type="radio" name="<?php echo esc_attr( $montage_name ); ?>" value="ja"> <?php echo esc_html__( 'Yes', 'siegel-umzugsliste' ); ?></label>
</div>
@@ -445,7 +564,7 @@ class Umzugsliste_Form_Renderer {
<input type="checkbox" name="<?php echo esc_attr( $field_name ); ?>" value="ja">
<?php echo esc_html( $field['name'] ); ?>
</label>
<input type="text" name="<?php echo esc_attr( $field_name . '_anzahl' ); ?>" class="qty-small" placeholder="<?php echo esc_attr__( 'Qty.', 'siegel-umzugsliste' ); ?>">
<input type="text" name="<?php echo esc_attr( substr( $field_name, 0, -1 ) . '_anzahl]' ); ?>" class="qty-small" placeholder="<?php echo esc_attr__( 'Qty.', 'siegel-umzugsliste' ); ?>">
</div>
<?php
break;

View File

@@ -51,11 +51,26 @@ class Umzugsliste_Shortcode {
* @return string Form HTML
*/
public function render_form( $atts ) {
// Ensure assets are enqueued
$this->enqueue_assets();
$atts = shortcode_atts( array( 'lang' => '' ), $atts, 'umzugsliste' );
$switched = false;
// Render the form
return Umzugsliste_Form_Renderer::render();
if ( ! empty( $atts['lang'] ) ) {
$locale_map = array( 'de' => 'de_DE', 'en' => 'en_US' );
$locale = isset( $locale_map[ $atts['lang'] ] ) ? $locale_map[ $atts['lang'] ] : '';
if ( $locale && $locale !== get_locale() ) {
switch_to_locale( $locale );
$switched = true;
}
}
$this->enqueue_assets();
$html = Umzugsliste_Form_Renderer::render();
if ( $switched ) {
restore_previous_locale();
}
return $html;
}
/**
@@ -108,6 +123,18 @@ class Umzugsliste_Shortcode {
'grandTotalLabel' => __( 'Grand Total', 'siegel-umzugsliste' ),
'quantityLabel' => __( 'Qty', 'siegel-umzugsliste' ),
'cbmLabel' => __( 'cbm', 'siegel-umzugsliste' ),
'summaryEdit' => __( 'Edit', 'siegel-umzugsliste' ),
'summaryName' => __( 'Name', 'siegel-umzugsliste' ),
'summaryStreet' => __( 'Street', 'siegel-umzugsliste' ),
'summaryZipCity' => __( 'ZIP/City', 'siegel-umzugsliste' ),
'summaryFloor' => __( 'Floor', 'siegel-umzugsliste' ),
'summaryElevator' => __( 'Elevator', 'siegel-umzugsliste' ),
'summaryPhone' => __( 'Phone', 'siegel-umzugsliste' ),
'summaryFax' => __( 'Fax', 'siegel-umzugsliste' ),
'summaryMobile' => __( 'Mobile', 'siegel-umzugsliste' ),
'summaryEmail' => __( 'Email', 'siegel-umzugsliste' ),
'stepLabel' => __( 'Step', 'siegel-umzugsliste' ),
'stepOf' => __( 'of', 'siegel-umzugsliste' ),
) );
}
}

View File

@@ -0,0 +1,306 @@
<?php
/**
* Test Email
*
* Admin tool for generating and previewing test emails with all fields populated
*
* @package Umzugsliste
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Test email class
*/
class Umzugsliste_Test_Email {
/**
* Single instance
*
* @var Umzugsliste_Test_Email
*/
private static $instance = null;
/**
* Get instance
*
* @return Umzugsliste_Test_Email
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action( 'admin_init', array( $this, 'maybe_output_preview' ) );
}
/**
* Intercept preview request before admin page header is output
*/
public function maybe_output_preview() {
if ( ! isset( $_GET['page'] ) || 'umzugsliste-test-email' !== $_GET['page'] ) {
return;
}
if ( ! isset( $_GET['action'] ) || 'preview' !== $_GET['action'] ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Unauthorized' );
}
switch_to_locale( 'de_DE' );
$data = self::generate_test_data();
$html = Umzugsliste_Email_Generator::generate( $data );
restore_previous_locale();
echo $html;
exit;
}
/**
* Generate comprehensive test data array with every field populated
*
* Must be called within de_DE locale context so __() returns German strings
*
* @return array Complete form submission data
*/
public static function generate_test_data() {
$data = array();
// Date - today
$data['umzug_day'] = date( 'd' );
$data['umzug_month'] = date( 'm' );
$data['umzug_year'] = date( 'Y' );
// Address fields - realistic German test values
$data['bName'] = 'Max Mustermann';
$data['eName'] = 'Erika Musterfrau';
$data['bStrasse'] = 'Musterstraße 42';
$data['eStrasse'] = 'Beispielweg 7';
$data['bort'] = '65197 Wiesbaden';
$data['eort'] = '55116 Mainz';
$data['bTelefon'] = '0611 123456';
$data['eTelefon'] = '06131 654321';
// Info array
$data['info'] = array(
'bGeschoss' => '2. OG',
'eGeschoss' => 'EG',
'bLift' => 'ja',
'eLift' => 'nein',
'bTelefax' => '0611 123457',
'eTelefax' => '06131 654322',
'bMobil' => '0170 1234567',
'eMobil' => '0171 7654321',
'eE-Mail' => 'test@example.com',
);
// Room data - pick 2-3 items per room with fixed quantities
$room_picks = array(
'wohnzimmer' => array( array( 0, 2 ), array( 4, 4 ), array( 8, 1 ) ),
'schlafzimmer' => array( array( 2, 1 ), array( 3, 2 ), array( 5, 2 ) ),
'arbeitszimmer' => array( array( 1, 1 ), array( 3, 2 ), array( 8, 1 ) ),
'bad' => array( array( 0, 1 ), array( 1, 1 ) ),
'kueche_esszimmer' => array( array( 4, 1 ), array( 7, 4 ), array( 9, 1 ) ),
'kinderzimmer' => array( array( 2, 1 ), array( 3, 1 ), array( 6, 1 ) ),
'keller' => array( array( 0, 2 ), array( 4, 4 ), array( 8, 1 ) ),
);
$rooms = Umzugsliste_Furniture_Data::get_rooms();
foreach ( $rooms as $room_key => $room_label ) {
$post_array_name = ucfirst( $room_key );
if ( 'kueche_esszimmer' === $room_key ) {
$post_array_name = 'Kueche_Esszimmer';
}
$furniture_items = Umzugsliste_Furniture_Data::get_furniture_items( $room_key );
$picks = $room_picks[ $room_key ];
$room_data = array();
foreach ( $picks as $pick ) {
$idx = $pick[0];
$quantity = $pick[1];
if ( isset( $furniture_items[ $idx ] ) ) {
$item = $furniture_items[ $idx ];
$name = $item['name'];
$room_data[ 'v' . $name ] = (string) $quantity;
$room_data[ 'q' . $name ] = (string) $item['cbm'];
$room_data[ 'm' . $name ] = ( $quantity > 2 ) ? 'ja' : 'nein';
}
}
$data[ $post_array_name ] = $room_data;
}
// Additional work
$sections = Umzugsliste_Furniture_Data::get_additional_work();
$additional_work = array();
// Montage - both checkboxes checked
$montage_data = array();
foreach ( $sections['montage']['fields'] as $field ) {
$key = self::get_field_key( $field );
$montage_data[ $key ] = 'ja';
}
$additional_work['montage'] = $montage_data;
// Schrank - mix of Abbau, Aufbau, Beides
$schrank_values = array( 'Abbau', 'Aufbau', 'Beides', 'Abbau', 'Aufbau', 'Beides' );
$schrank_data = array();
$i = 0;
foreach ( $sections['schrank']['fields'] as $field ) {
$key = self::get_field_key( $field );
$schrank_data[ $key ] = $schrank_values[ $i % count( $schrank_values ) ];
$i++;
}
$additional_work['schrank'] = $schrank_data;
// Elektriker - all checked with varied _anzahl values
$elektriker_data = array();
$anzahl = 1;
foreach ( $sections['elektriker']['fields'] as $field ) {
$key = self::get_field_key( $field );
$elektriker_data[ $key ] = 'ja';
$elektriker_data[ $key . '_anzahl' ] = (string) $anzahl;
$anzahl++;
}
$additional_work['elektriker'] = $elektriker_data;
// Duebelarbeiten - all checked with varied _anzahl values
$duebel_data = array();
$anzahl = 2;
foreach ( $sections['duebelarbeiten']['fields'] as $field ) {
$key = self::get_field_key( $field );
$duebel_data[ $key ] = 'ja';
$duebel_data[ $key . '_anzahl' ] = (string) $anzahl;
$anzahl += 2;
}
$additional_work['duebelarbeiten'] = $duebel_data;
// Packarbeiten - all 4 checkboxes checked, text quantities filled
$pack_data = array();
$pack_fields = $sections['packarbeiten']['fields'];
for ( $i = 0; $i < 4 && $i < count( $pack_fields ); $i++ ) {
$key = self::get_field_key( $pack_fields[ $i ] );
$pack_data[ $key ] = 'ja';
}
for ( $i = 4; $i < 6 && $i < count( $pack_fields ); $i++ ) {
$key = self::get_field_key( $pack_fields[ $i ] );
$pack_data[ $key ] = ( $i === 4 ) ? '25' : '5';
}
$additional_work['packarbeiten'] = $pack_data;
// Anfahrt - all checkboxes checked + distance text values
$anfahrt_data = array();
$anfahrt_fields = $sections['anfahrt']['fields'];
foreach ( $anfahrt_fields as $i => $field ) {
$key = self::get_field_key( $field );
if ( 'checkbox' === $field['type'] ) {
$anfahrt_data[ $key ] = 'ja';
} else {
$anfahrt_data[ $key ] = ( $i === 6 ) ? '15' : '25';
}
}
$additional_work['anfahrt'] = $anfahrt_data;
$data['additional_work'] = $additional_work;
// Sonstiges
$data['sonstiges'] = "Bitte vorsichtig mit dem antiken Schrank im Wohnzimmer.\nDas Klavier muss besonders geschützt werden.";
return $data;
}
/**
* Get field key matching email generator logic
*
* @param array $field Field definition
* @return string Field key
*/
private static function get_field_key( $field ) {
if ( ! empty( $field['key'] ) ) {
return sanitize_key( $field['key'] );
}
return sanitize_title( $field['name'] );
}
/**
* Render admin page
*/
public function render_admin_page() {
// Handle send test email
$notice = '';
if ( isset( $_POST['send_test_email'] ) && check_admin_referer( 'umzugsliste_send_test_email' ) ) {
$notice = $this->send_test_email();
}
$preview_url = add_query_arg(
array(
'page' => 'umzugsliste-test-email',
'action' => 'preview',
),
admin_url( 'admin.php' )
);
?>
<div class="wrap">
<h1>Test Email</h1>
<?php if ( $notice ) : ?>
<?php echo $notice; ?>
<?php endif; ?>
<form method="post" style="margin-bottom: 20px;">
<?php wp_nonce_field( 'umzugsliste_send_test_email' ); ?>
<p>
<?php $to = get_option( 'umzugsliste_receiver_email', get_option( 'admin_email' ) ); ?>
<strong>Recipient:</strong> <?php echo esc_html( $to ); ?>
</p>
<p>
<?php submit_button( 'Send Test Email', 'primary', 'send_test_email', false ); ?>
</p>
</form>
<h2>Email Preview</h2>
<iframe
src="<?php echo esc_url( $preview_url ); ?>"
style="width: 100%; height: 800px; border: 1px solid #ccd0d4; background: #fff;"
></iframe>
</div>
<?php
}
/**
* Send test email
*
* @return string Notice HTML
*/
private function send_test_email() {
switch_to_locale( 'de_DE' );
$data = self::generate_test_data();
$html = Umzugsliste_Email_Generator::generate( $data );
restore_previous_locale();
$to = get_option( 'umzugsliste_receiver_email', get_option( 'admin_email' ) );
$subject = 'TEST - Internetanfrage - Anfrage vom ' . date( 'd.m.Y H:i' );
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
$sent = wp_mail( $to, $subject, $html, $headers );
if ( $sent ) {
return '<div class="notice notice-success is-dismissible"><p>Test email sent to <strong>' . esc_html( $to ) . '</strong>.</p></div>';
}
return '<div class="notice notice-error is-dismissible"><p>Failed to send test email. Check your mail configuration.</p></div>';
}
}

View File

@@ -1137,3 +1137,22 @@ msgstr "Mobil (Beladeadresse)"
#: includes/class-cpt.php
msgid "Mobile (Unloading)"
msgstr "Mobil (Entladeadresse)"
#: includes/class-shortcode.php
#: templates/form-page.php
msgid "Step"
msgstr "Schritt"
#: includes/class-shortcode.php
#: templates/form-page.php
msgid "of"
msgstr "von"
#: includes/class-shortcode.php
#: templates/form-page.php
msgid "Edit"
msgstr "Bearbeiten"
#: includes/class-form-renderer.php
msgid "Montage?"
msgstr "Montage?"

View File

@@ -40,6 +40,18 @@ $l10n_data = array(
'grandTotalLabel' => __( 'Grand Total', 'siegel-umzugsliste' ),
'quantityLabel' => __( 'Qty', 'siegel-umzugsliste' ),
'cbmLabel' => __( 'cbm', 'siegel-umzugsliste' ),
'summaryEdit' => __( 'Edit', 'siegel-umzugsliste' ),
'summaryName' => __( 'Name', 'siegel-umzugsliste' ),
'summaryStreet' => __( 'Street', 'siegel-umzugsliste' ),
'summaryZipCity' => __( 'ZIP/City', 'siegel-umzugsliste' ),
'summaryFloor' => __( 'Floor', 'siegel-umzugsliste' ),
'summaryElevator' => __( 'Elevator', 'siegel-umzugsliste' ),
'summaryPhone' => __( 'Phone', 'siegel-umzugsliste' ),
'summaryFax' => __( 'Fax', 'siegel-umzugsliste' ),
'summaryMobile' => __( 'Mobile', 'siegel-umzugsliste' ),
'summaryEmail' => __( 'Email', 'siegel-umzugsliste' ),
'stepLabel' => __( 'Step', 'siegel-umzugsliste' ),
'stepOf' => __( 'of', 'siegel-umzugsliste' ),
'nonce' => wp_create_nonce( 'umzugsliste_submit' ),
);
?>

View File

@@ -3,7 +3,7 @@
* Plugin Name: Umzugsliste
* Description: Email-basiertes Möbelauswahlsystem für Siegel Umzüge
* Version: 1.0.0
* Author: Siegel Umzüge
* Author: Viktor Miller
* Text Domain: siegel-umzugsliste
* Domain Path: /languages
*/
@@ -84,6 +84,7 @@ class Umzugsliste {
require_once UMZUGSLISTE_PLUGIN_DIR . 'includes/class-form-renderer.php';
require_once UMZUGSLISTE_PLUGIN_DIR . 'includes/class-shortcode.php';
require_once UMZUGSLISTE_PLUGIN_DIR . 'includes/class-email-generator.php';
require_once UMZUGSLISTE_PLUGIN_DIR . 'includes/class-test-email.php';
require_once UMZUGSLISTE_PLUGIN_DIR . 'includes/class-form-handler.php';
}
@@ -126,6 +127,15 @@ class Umzugsliste {
}
if ( $use_standalone ) {
// Extract lang from shortcode if present and switch locale before template loads
$post = get_queried_object();
if ( $post && isset( $post->post_content ) && preg_match( '/\[umzugsliste[^\]]*lang=["\'](\w+)["\']/', $post->post_content, $m ) ) {
$locale_map = array( 'de' => 'de_DE', 'en' => 'en_US' );
if ( isset( $locale_map[ $m[1] ] ) && $locale_map[ $m[1] ] !== get_locale() ) {
switch_to_locale( $locale_map[ $m[1] ] );
}
}
$custom_template = UMZUGSLISTE_PLUGIN_DIR . 'templates/form-page.php';
if ( file_exists( $custom_template ) ) {
return $custom_template;
@@ -143,9 +153,10 @@ class Umzugsliste {
$cpt = Umzugsliste_CPT::get_instance();
$cpt->register_post_type();
// Initialize admin menu
// Initialize admin menu and test email
if ( is_admin() ) {
Umzugsliste_Admin_Menu::get_instance();
Umzugsliste_Test_Email::get_instance();
}
// Initialize settings