Compare commits

7 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
10 changed files with 927 additions and 233 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

@@ -249,7 +249,7 @@ body.umzugsliste-standalone:has(.palette-c) {
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 {
@@ -465,7 +465,7 @@ body.umzugsliste-standalone:has(.palette-c) {
font-size: 0.8rem;
color: var(--umzug-text-secondary);
margin-top: 12px;
margin-bottom: 0;
margin-bottom: 16px;
}
.privacy-note a {
@@ -661,7 +661,7 @@ body.umzugsliste-standalone:has(.palette-c) {
}
.item-cbm {
width: 52px;
width: 75px;
text-align: right;
font-size: 13px;
color: var(--umzug-text-secondary);
@@ -947,7 +947,7 @@ body.umzugsliste-standalone:has(.palette-c) {
}
#wizard-summary .summary-item-cbm {
width: 60px;
width: 80px;
text-align: right;
color: var(--umzug-text-secondary);
}
@@ -1121,11 +1121,7 @@ body.umzugsliste-standalone:has(.palette-c) {
}
.progress-dot {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.dot-label {
@@ -1156,3 +1152,19 @@ body.umzugsliste-standalone:has(.palette-c) {
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

@@ -297,44 +297,44 @@
// Customer info
html += '<div class="summary-section">';
html += summaryHeading(l10n.summaryMovingDate || 'Moving Date', 1);
var day = getFieldVal('day');
var month = getFieldVal('month');
var year = getFieldVal('year');
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 += summaryHeading(l10n.summaryLoading || 'Loading Address', 1);
html += summaryRow('Name', getFieldVal('bName'));
html += summaryRow('Street', getFieldVal('bStrasse'));
html += summaryRow('ZIP/City', getFieldVal('bort'));
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 += summaryHeading(l10n.summaryUnloading || 'Unloading Address', 1);
html += summaryRow('Name', getFieldVal('eName'));
html += summaryRow('Street', getFieldVal('eStrasse'));
html += summaryRow('ZIP/City', getFieldVal('eort'));
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
@@ -359,7 +359,7 @@
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>';
}
@@ -409,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();
}
@@ -419,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;
@@ -556,6 +562,15 @@
}
});
// 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');

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

@@ -206,20 +206,99 @@ class Umzugsliste_Form_Renderer {
<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</button>
<button type="button" id="dev-autofill" class="dev-autofill-btn">&#9881; Fill All</button>
<script>
document.getElementById('dev-autofill').addEventListener('click', function() {
var fields = {
'bName':'Max Mustermann','bStrasse':'Musterstr. 12',
'bort':'10115 Berlin','bTelefon':'030 12345678',
'eName':'Erika Musterfrau','eStrasse':'Zielweg 5',
'eort':'80331 München','info[eE-Mail]':'test@example.com'
};
for (var n in fields) {
var el = document.querySelector('[name="'+n+'"]');
if (el) { el.value = fields[n]; el.dispatchEvent(new Event('input',{bubbles:true})); }
function setField(name, val) {
var el = document.querySelector('[name="'+name+'"]');
if (el) { el.value = val; el.dispatchEvent(new Event('input',{bubbles:true})); }
}
document.getElementById('wizard-next').click();
function setRadio(name, val) {
var el = document.querySelector('[name="'+name+'"][value="'+val+'"]');
if (el) el.checked = true;
}
/* 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; ?>
@@ -340,7 +419,7 @@ class Umzugsliste_Form_Renderer {
<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" placeholder="<?php echo esc_attr__( 'Additional notes or requests...', 'siegel-umzugsliste' ); ?>"></textarea>
<textarea name="sonstiges" id="sonstiges" rows="5" class="sonstiges-textarea"></textarea>
</div>
</div>
</div>
@@ -432,7 +511,7 @@ class Umzugsliste_Form_Renderer {
<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">
@@ -485,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

@@ -124,6 +124,15 @@ class Umzugsliste_Shortcode {
'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

@@ -41,6 +41,15 @@ $l10n_data = array(
'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';
}
@@ -152,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