Changeset 3695886
- Timestamp:
- 09/14/2026 08:33:44 PM (7 weeks ago)
- Location:
- flavor-restaurant-menu/trunk
- Files:
-
- 13 edited
-
assets/css/flavor-admin-app.css (modified) (2 diffs)
-
assets/js/flavor-reservation-admin.js (modified) (10 diffs)
-
flavor-restaurant-menu.php (modified) (2 diffs)
-
includes/class-flavor-admin.php (modified) (9 diffs)
-
includes/class-flavor-block.php (modified) (1 diff)
-
includes/class-flavor-onboarding.php (modified) (2 diffs)
-
includes/class-flavor-reservation-block.php (modified) (1 diff)
-
includes/class-flavor-reservation-form.php (modified) (1 diff)
-
includes/class-flavor-reservations.php (modified) (11 diffs)
-
includes/class-flavor-upsell.php (modified) (1 diff)
-
languages/flavor-restaurant-menu-de_DE.mo (modified) ( previous)
-
languages/flavor-restaurant-menu-de_DE.po (modified) (3 diffs)
-
readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
flavor-restaurant-menu/trunk/assets/css/flavor-admin-app.css
r3692870 r3695886 357 357 #flavor-app .flv-tl__bar{ position:absolute; top:9px; height:28px; border-radius:7px; display:flex; align-items:center; padding:0 9px; font-size:11.5px; font-weight:500; color:var(--ink); white-space:nowrap; overflow:hidden; background:var(--good-bg); border-left:3px solid var(--good); box-shadow:var(--shadow); } 358 358 #flavor-app .flv-tl__bar.arrived{ background:var(--accent-soft); border-color:var(--accent); } 359 /* The guest has gone — the bar ends where they left, greyed out and no longer movable. */ 360 #flavor-app .flv-tl__bar.left{ background:var(--surface-2); border-color:var(--line-strong); color:var(--muted); box-shadow:none; } 361 /* Movable bars: click opens the move dialog, drag onto another row. */ 362 #flavor-app .flv-tl__bar[data-seat-res]{ cursor:grab; } 363 #flavor-app .flv-tl__bar[data-seat-res]:hover, 364 #flavor-app .flv-tl__bar[data-seat-res]:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; z-index:2; } 365 #flavor-app .flv-tl__bar.is-dragging{ opacity:.45; } 366 #flavor-app .flv-tl__lane.is-drop{ background:var(--accent-soft); border-radius:8px; } 359 367 360 368 /* Reservation Settings — per-location switcher (Flavor pills, replaces the WP .subsubsub) */ … … 912 920 #flavor-app .flavor-seat-pick small{ color:var(--muted); } 913 921 #flavor-app .flavor-seat-tag{ color:var(--accent); font-family:var(--font-mono); font-size:10px; } 922 /* Moving onto a taken or too-small table is allowed — this says what it means. */ 923 #flavor-app .flavor-seat-warn{ margin:12px 0 0; padding:7px 10px; border-radius:8px; font-size:12.5px; line-height:1.4; font-weight:600; color:var(--bad); background:var(--bad-bg); } 924 #flavor-app .flavor-seat-intro{ margin:10px 0 0; color:var(--ink-2); font-size:13px; } 914 925 915 926 /* walk-in fields */ -
flavor-restaurant-menu/trunk/assets/js/flavor-reservation-admin.js
r3682810 r3695886 353 353 354 354 // ---- render the modal for a reservation ---- 355 function renderModal(data) { 355 // preselect: a table id to start on (a bar dropped onto a timeline row). 356 function renderModal(data, preselect) { 356 357 var box = modalBox(); 357 358 box.innerHTML = ""; … … 382 383 var tablesById = {}; 383 384 (data.tables || []).forEach(function (t) { tablesById[t.id] = t; }); 384 var sel = data.best || (res.tableId || ""); 385 var confirmBtn; 385 // Moving a guest who already sits somewhere. Nothing is preselected unless a 386 // drop on the timeline named the target, and every table can be picked — the 387 // guest may well have sat down at a taken or too-small one already, and the 388 // board has to record what really happened. Conflicts are spelled out under the 389 // grid instead of blocked. Plain seating keeps its stricter chooser. 390 var moving = !!res.tableId; 391 var pre = (preselect && preselect !== res.tableId && tablesById[preselect]) ? preselect : ""; 392 var sel = moving ? pre : (data.best || ""); 393 var confirmBtn, warnEl; 394 395 function updateWarn() { 396 if (!warnEl) { return; } 397 var t = tablesById[sel], bits = []; 398 if (t && t.taken) { bits.push(i18n.warnTaken || ""); } 399 if (t && !t.fits) { bits.push(i18n.warnSmall || ""); } 400 warnEl.textContent = bits.join(" "); 401 warnEl.hidden = !bits.length; 402 } 386 403 387 404 // header 388 405 var head = el("div", "flavor-seat-head"); 389 head.appendChild(el("h3", "flavor-seat-title", i18n.seatTitle || "Seat"));406 head.appendChild(el("h3", "flavor-seat-title", moving ? (i18n.moveTitle || "Move guest") : (i18n.seatTitle || "Seat"))); 390 407 head.appendChild(el("p", "flavor-seat-sub", res.name || "")); 391 408 box.appendChild(head); … … 402 419 body.appendChild(bc); 403 420 404 // classify tables 405 var fitFree = (data.suggested || []).map(function (id) { return tablesById[id]; }).filter(Boolean); 406 var tooSmall = (data.tables || []).filter(function (t) { return !t.taken && !t.fits; }); 407 var taken = (data.tables || []).filter(function (t) { return t.taken; }); 421 if (moving && i18n.moveIntro) { body.appendChild(el("p", "flavor-seat-intro", i18n.moveIntro)); } 422 423 // classify tables (the guest's current table gets its own row when moving) 424 function notCurrent(id) { return !moving || id !== res.tableId; } 425 var others = (data.tables || []).filter(function (t) { return notCurrent(t.id); }); 426 var fitFree = (data.suggested || []).filter(notCurrent).map(function (id) { return tablesById[id]; }).filter(Boolean); 427 var tooSmall = others.filter(function (t) { return !t.taken && !t.fits; }); 428 var taken = others.filter(function (t) { return t.taken; }); 408 429 409 430 function pickNode(t, selectable, tag) { … … 419 440 p.classList.add("is-sel"); 420 441 if (confirmBtn) { confirmBtn.disabled = false; } 442 updateWarn(); 421 443 }); 422 444 } … … 424 446 } 425 447 448 if (moving && tablesById[res.tableId]) { 449 body.appendChild(el("p", "flavor-seat-sublabel", i18n.current || "")); 450 var g0 = el("div", "flavor-seat-grid"); 451 g0.appendChild(pickNode(tablesById[res.tableId], false, "")); 452 body.appendChild(g0); 453 } 426 454 if (fitFree.length) { 427 455 body.appendChild(el("p", "flavor-seat-sublabel", i18n.freeFit || "")); … … 433 461 body.appendChild(el("p", "flavor-seat-sublabel", i18n.tooSmall || "")); 434 462 var g2 = el("div", "flavor-seat-grid"); 435 tooSmall.forEach(function (t) { g2.appendChild(pickNode(t, false, "")); });463 tooSmall.forEach(function (t) { g2.appendChild(pickNode(t, moving, "")); }); 436 464 body.appendChild(g2); 437 465 } … … 439 467 body.appendChild(el("p", "flavor-seat-sublabel", i18n.taken || "")); 440 468 var g3 = el("div", "flavor-seat-grid"); 441 taken.forEach(function (t) { g3.appendChild(pickNode(t, false, "")); });469 taken.forEach(function (t) { g3.appendChild(pickNode(t, moving, "")); }); 442 470 body.appendChild(g3); 443 471 } 472 warnEl = el("p", "flavor-seat-warn"); 473 warnEl.setAttribute("role", "status"); 474 body.appendChild(warnEl); 475 updateWarn(); 444 476 box.appendChild(body); 445 477 … … 471 503 } 472 504 473 confirmBtn = el("button", "button button-primary", i18n.confirm || "Seat");505 confirmBtn = el("button", "button button-primary", moving ? (i18n.moveBtn || "Move here") : (i18n.confirm || "Seat")); 474 506 confirmBtn.type = "button"; 475 507 confirmBtn.disabled = !sel; … … 480 512 if (j && j.success) { 481 513 var label = (tablesById[sel] && tablesById[sel].label) || sel; 482 toast(fmt( i18n.seatedAt|| "%s", [label]));514 toast(fmt((moving ? i18n.movedTo : i18n.seatedAt) || "%s", [label])); 483 515 close(); 484 516 if (reloadsAfterSeating()) { … … 641 673 if (!btn) { return; } 642 674 e.preventDefault(); 675 var menu = btn.closest("details"); 676 if (menu) { menu.open = false; } // opened from the row's "⋯" menu 643 677 activeCell = btn.closest("td"); 644 var resId = btn.getAttribute("data-seat-res"); 678 openSeat(btn.getAttribute("data-seat-res"), ""); 679 }); 680 681 function openSeat(resId, preselect) { 645 682 open(); 646 683 modalBox().innerHTML = '<div class="flavor-seat-loading">' + (i18n.loading || "…") + "</div>"; 647 684 post("flavor_reservation_suggest", { res_id: resId }).then(function (j) { 648 if (j && j.success) { renderModal(j.data ); }685 if (j && j.success) { renderModal(j.data, preselect); } 649 686 else { modalBox().innerHTML = '<div class="flavor-seat-loading">' + ((j && j.data && j.data.message) || i18n.loadErr || "") + "</div>"; } 650 687 }).catch(function () { 651 688 modalBox().innerHTML = '<div class="flavor-seat-loading">' + (i18n.loadErr || "") + "</div>"; 652 689 }); 690 } 691 692 // ---- Timeline: bars are keyboard-reachable and can be dragged to another row ---- 693 document.addEventListener("keydown", function (e) { 694 if (e.key !== "Enter" && e.key !== " ") { return; } 695 var bar = e.target.closest ? e.target.closest(".flv-tl__bar[data-seat-res]") : null; 696 if (!bar) { return; } 697 e.preventDefault(); 698 activeCell = null; 699 openSeat(bar.getAttribute("data-seat-res"), ""); 700 }); 701 702 var dragRes = ""; 703 function laneOf(t) { return t && t.closest ? t.closest(".flv-tl__lane[data-flv-lane]") : null; } 704 function clearDrop() { 705 Array.prototype.forEach.call(document.querySelectorAll(".flv-tl__lane.is-drop"), function (l) { l.classList.remove("is-drop"); }); 706 } 707 document.addEventListener("dragstart", function (e) { 708 var bar = e.target.closest ? e.target.closest(".flv-tl__bar[data-seat-res]") : null; 709 if (!bar) { return; } 710 dragRes = bar.getAttribute("data-seat-res"); 711 bar.classList.add("is-dragging"); 712 if (e.dataTransfer) { e.dataTransfer.setData("text/plain", dragRes); e.dataTransfer.effectAllowed = "move"; } 713 }); 714 document.addEventListener("dragend", function (e) { 715 var bar = e.target.closest ? e.target.closest(".flv-tl__bar") : null; 716 if (bar) { bar.classList.remove("is-dragging"); } 717 clearDrop(); 718 dragRes = ""; 719 }); 720 document.addEventListener("dragover", function (e) { 721 var lane = dragRes ? laneOf(e.target) : null; 722 if (!lane) { return; } 723 e.preventDefault(); 724 if (e.dataTransfer) { e.dataTransfer.dropEffect = "move"; } 725 if (!lane.classList.contains("is-drop")) { clearDrop(); lane.classList.add("is-drop"); } 726 }); 727 document.addEventListener("drop", function (e) { 728 var lane = dragRes ? laneOf(e.target) : null; 729 if (!lane) { return; } 730 e.preventDefault(); 731 var resId = dragRes; 732 clearDrop(); 733 dragRes = ""; 734 activeCell = null; 735 // The dialog opens on the row it was dropped on, showing any clash before 736 // anything is saved — the same check as picking the table by hand. 737 openSeat(resId, lane.getAttribute("data-flv-lane") || ""); 653 738 }); 654 739 })(); -
flavor-restaurant-menu/trunk/flavor-restaurant-menu.php
r3693109 r3695886 4 4 * Plugin URI: https://flavor-menu.com 5 5 * Description: Restaurant menu and online table reservations for WordPress. Elegant menu layouts, a booking form with approval by e-mail, and support for Gutenberg, Elementor, WPBakery and the [flavor_menu] shortcode. 6 * Version: 2. 3.26 * Version: 2.4 7 7 * Author: Flavor Studio 8 8 * Requires at least: 6.0 … … 18 18 } 19 19 20 define( 'FLAVOR_MENU_VERSION', '2. 3.2' );20 define( 'FLAVOR_MENU_VERSION', '2.4' ); 21 21 define( 'FLAVOR_MENU_PATH', plugin_dir_path( __FILE__ ) ); 22 22 define( 'FLAVOR_MENU_URL', plugin_dir_url( __FILE__ ) ); -
flavor-restaurant-menu/trunk/includes/class-flavor-admin.php
r3692870 r3695886 103 103 // from the dishes list). Excluded from subnav, collapse count and landing. 104 104 'hidden' => false, 105 // Wide screens (the room editor) hand the subnav column and the content 106 // max-width over to their own tools. 107 'wide' => false, 105 108 // Optional subnav badge: a callable returning an int; a positive count is 106 109 // shown as a bubble on the subnav link (e.g. open reservation requests). … … 341 344 $group = $screen['group']; 342 345 $group_screens = self::screens_in( $group ); 343 $collapse = count( $group_screens ) <= 1; 344 345 $root_class = 'flv-app' . ( $collapse ? ' flv-app--no-subnav' : '' ); 346 $wide = ! empty( $screen['wide'] ); 347 $collapse = $wide || count( $group_screens ) <= 1; 348 349 $root_class = 'flv-app' . ( $collapse ? ' flv-app--no-subnav' : '' ) . ( $wide ? ' flv-app--wide' : '' ); 346 350 // Painted by PHP rather than by JavaScript after load — otherwise every page 347 351 // change flashes the light theme first. … … 1556 1560 $minute = ( (int) $m[1] * 60 ) + (int) $m[2]; 1557 1561 // Each booking holds its table for its own turn time (staggered by party 1558 // size), so the peak is summed over per-booking windows, not one flat one. 1559 // Clamped to 1: a zero turn would empty every window and hide the peak. 1560 $turn = max( 1, (int) Flavor_Reservations::turn_for( $party, $loc ) ); 1562 // size), or until the guest left, so the peak is summed over per-booking 1563 // windows, not one flat one. Clamped to 1: a zero turn setting would empty 1564 // every window and hide the peak — but a guest who left right away really 1565 // holds nothing. 1566 $turn = (int) Flavor_Reservations::hold_for( $r, $loc ); 1567 if ( empty( $r->left_at ) ) { 1568 $turn = max( 1, $turn ); 1569 } elseif ( $turn < 1 ) { 1570 continue; 1571 } 1561 1572 $booked[] = array( (int) Flavor_Reservations::business_min( $minute, $open ), $party, $hhmm, $turn ); 1562 1573 } … … 1668 1679 } 1669 1680 1681 /** 'H:i' when a seated guest has already left (table freed early), '' otherwise. */ 1682 private static function res_left_time( $r ) { 1683 if ( Flavor_Reservations::STATUS_ARRIVED !== $r->status || empty( $r->left_at ) ) { 1684 return ''; 1685 } 1686 return substr( (string) $r->left_at, 11, 5 ); 1687 } 1688 1670 1689 /** Success notice after a post-redirect-get from the reservation handlers. */ 1671 1690 private static function res_notice() { … … 1681 1700 'arrived' => __( 'Marked as arrived.', 'flavor-restaurant-menu' ), 1682 1701 'no_show' => __( 'Marked as no-show.', 'flavor-restaurant-menu' ), 1702 'left' => __( 'Table freed — the guest has left.', 'flavor-restaurant-menu' ), 1703 'back' => __( 'The guest is marked as back at the table.', 'flavor-restaurant-menu' ), 1683 1704 'deleted' => __( 'Reservation deleted.', 'flavor-restaurant-menu' ), 1684 1705 'added' => __( 'Reservation added.', 'flavor-restaurant-menu' ), … … 2239 2260 <span class="flv-pill flv-pill--overdue"><?php esc_html_e( 'Overdue', 'flavor-restaurant-menu' ); ?></span> 2240 2261 <?php endif; ?> 2262 <?php if ( '' !== self::res_left_time( $r ) ) : ?> 2263 <span class="flv-pill flv-pill--mute"><?php echo esc_html( sprintf( /* translators: %s: time the guest left, e.g. 21:40 */ __( 'Left at %s', 'flavor-restaurant-menu' ), self::res_left_time( $r ) ) ); ?></span> 2264 <?php endif; ?> 2241 2265 </td> 2242 2266 <?php if ( $seating ) : ?> … … 2274 2298 $is_active = in_array( $r->status, array( Flavor_Reservations::STATUS_CONFIRMED, Flavor_Reservations::STATUS_ARRIVED ), true ); 2275 2299 $tbl = self::res_table_label( $r ); 2300 $has_left = ( '' !== self::res_left_time( $r ) ); 2301 // Guest at the table right now: the next thing that happens is them leaving, 2302 // so that is the primary action; seating / moving moves into the menu. 2303 $is_here = ( Flavor_Reservations::STATUS_ARRIVED === $r->status && ! $has_left ); 2304 $seat_lbl = ( '' === $tbl ) ? __( 'Seat', 'flavor-restaurant-menu' ) : _x( 'Move', 'move a guest to another table', 'flavor-restaurant-menu' ); 2276 2305 ?> 2277 2306 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="flv-resactions"> … … 2283 2312 <?php if ( $is_open ) : ?> 2284 2313 <button type="submit" name="res_action" value="confirm" class="flv-btn flv-btn--primary flv-btn--small"><?php esc_html_e( 'Approve', 'flavor-restaurant-menu' ); ?></button> 2285 <?php elseif ( $is_active && $seating && 'list' === $where ) : ?> 2314 <?php elseif ( $is_here && 'list' === $where ) : ?> 2315 <button type="submit" name="res_action" value="left" class="flv-btn flv-btn--small"><?php esc_html_e( 'Guest left', 'flavor-restaurant-menu' ); ?></button> 2316 <?php elseif ( $is_active && ! $has_left && $seating && 'list' === $where ) : ?> 2286 2317 <button type="button" class="flv-btn flv-btn--small<?php echo ( '' === $tbl ) ? ' flv-btn--primary' : ''; ?> flavor-seat-btn" data-seat-res="<?php echo (int) $r->id; ?>"> 2287 <?php echo ( '' === $tbl ) ? esc_html__( 'Seat', 'flavor-restaurant-menu' ) : esc_html__( 'Change', 'flavor-restaurant-menu'); ?>2318 <?php echo esc_html( $seat_lbl ); ?> 2288 2319 </button> 2289 2320 <?php endif; ?> … … 2294 2325 <?php if ( $is_open ) : ?> 2295 2326 <button type="submit" name="res_action" value="reject"><?php esc_html_e( 'Decline', 'flavor-restaurant-menu' ); ?></button> 2327 <?php endif; ?> 2328 <?php if ( $is_here && $seating && 'list' === $where ) : ?> 2329 <button type="button" class="flavor-seat-btn" data-seat-res="<?php echo (int) $r->id; ?>"><?php echo esc_html( $seat_lbl ); ?></button> 2330 <?php endif; ?> 2331 <?php if ( $has_left ) : ?> 2332 <button type="submit" name="res_action" value="back"><?php esc_html_e( 'Back at the table', 'flavor-restaurant-menu' ); ?></button> 2296 2333 <?php endif; ?> 2297 2334 <?php if ( Flavor_Reservations::STATUS_CONFIRMED === $r->status ) : ?> -
flavor-restaurant-menu/trunk/includes/class-flavor-block.php
r3692870 r3695886 19 19 add_action( 'init', array( __CLASS__, 'register_block' ) ); 20 20 add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'localize_editor_data' ) ); 21 add_action( 'enqueue_block_assets', array( __CLASS__, 'enqueue_editor_styles' ) ); 22 } 23 24 /** 25 * Styles for the menu preview inside the block editor. The preview comes from 26 * the REST block renderer, which returns markup only, so without this the card 27 * grid showed up unstyled (placeholder icon at full column width). Admin only: 28 * on the frontend the shortcode keeps enqueueing the stylesheet just where a 29 * menu is rendered. enqueue_block_assets also reaches an iframed editor. 30 */ 31 public static function enqueue_editor_styles() { 32 if ( ! is_admin() ) { 33 return; 34 } 35 if ( ! wp_style_is( 'flavor-menu-style', 'registered' ) && function_exists( 'flavor_menu_enqueue_assets' ) ) { 36 flavor_menu_enqueue_assets(); 37 } 38 wp_enqueue_style( 'flavor-menu-style' ); 21 39 } 22 40 -
flavor-restaurant-menu/trunk/includes/class-flavor-onboarding.php
r3587609 r3695886 102 102 } 103 103 104 /** One-click: create a published "Our Menu" page containing the shortcode. */ 104 /** 105 * Content for the one-click menu page. 106 * 107 * In the block editor this is a real "Menu Card" block, so its settings sit 108 * in the block sidebar. As a bare shortcode it showed up as an easily 109 * overlooked Classic block, and people added a second shortcode below it 110 * expecting to change the first — which left two menus on the page. Where 111 * pages are edited classically (Classic Editor, WPBakery with Gutenberg off) 112 * a block comment would be invisible, so the shortcode stays. 113 */ 114 private static function menu_page_content() { 115 $block_editor = function_exists( 'use_block_editor_for_post_type' ) && use_block_editor_for_post_type( 'page' ); 116 $registered = class_exists( 'WP_Block_Type_Registry' ) && WP_Block_Type_Registry::get_instance()->is_registered( 'flavor-menu/menu' ); 117 return ( $block_editor && $registered ) ? '<!-- wp:flavor-menu/menu /-->' : '[flavor_menu]'; 118 } 119 120 /** One-click: create a published "Our Menu" page containing the menu. */ 105 121 public static function handle_create_page() { 106 122 if ( ! current_user_can( 'manage_options' ) ) { … … 121 137 'post_title' => __( 'Our Menu', 'flavor-restaurant-menu' ), 122 138 'post_status' => 'publish', 123 'post_content' => '[flavor_menu]',139 'post_content' => self::menu_page_content(), 124 140 ) ); 125 141 -
flavor-restaurant-menu/trunk/includes/class-flavor-reservation-block.php
r3692870 r3695886 20 20 public static function init() { 21 21 add_action( 'init', array( __CLASS__, 'register_block' ) ); 22 add_action( 'enqueue_block_assets', array( __CLASS__, 'enqueue_editor_styles' ) ); 23 } 24 25 /** 26 * Form styles for the preview inside the block editor — the REST block renderer 27 * returns markup only. Admin only; on the frontend the shortcode enqueues the 28 * stylesheet itself. enqueue_block_assets also reaches an iframed editor. 29 */ 30 public static function enqueue_editor_styles() { 31 if ( ! is_admin() ) { 32 return; 33 } 34 if ( ! wp_style_is( 'flavor-reservation-style', 'registered' ) ) { 35 Flavor_Reservation_Form::register_assets(); 36 } 37 wp_enqueue_style( 'flavor-reservation-style' ); 22 38 } 23 39 -
flavor-restaurant-menu/trunk/includes/class-flavor-reservation-form.php
r3693109 r3695886 787 787 Flavor_Reservations::set_status( $id, Flavor_Reservations::STATUS_NO_SHOW ); 788 788 return array( 'ok' => true, 'status' => Flavor_Reservations::STATUS_NO_SHOW ); 789 790 // The guest got up before their turn time was over — free the table now. 791 // Status stays "arrived": they were here, and the analytics must count them. 792 case 'left': 793 if ( Flavor_Reservations::STATUS_ARRIVED !== $status || ! empty( $res->left_at ) ) { 794 return array( 'ok' => false, 'reason' => 'badstate', 'status' => $status ); 795 } 796 Flavor_Reservations::set_left( $id, current_time( 'mysql' ) ); 797 return array( 'ok' => true, 'status' => $status ); 798 799 // Undo for a "left" pressed on the wrong row. 800 case 'back': 801 if ( Flavor_Reservations::STATUS_ARRIVED !== $status || empty( $res->left_at ) ) { 802 return array( 'ok' => false, 'reason' => 'badstate', 'status' => $status ); 803 } 804 Flavor_Reservations::set_left( $id, null ); 805 return array( 'ok' => true, 'status' => $status ); 789 806 } 790 807 -
flavor-restaurant-menu/trunk/includes/class-flavor-reservations.php
r3692870 r3695886 35 35 36 36 const TABLE = 'flavor_reservations'; 37 const DB_VERSION = ' 3'; // 2: + is_walkin column; 3: self-heal that column (dbDelta skipped it on some hosts)37 const DB_VERSION = '4'; // 2: + is_walkin column; 3: self-heal that column (dbDelta skipped it on some hosts); 4: + left_at 38 38 const DB_OPT = 'flavor_reservations_db_version'; 39 39 const OPTION_KEY = 'flavor_reservation_settings'; … … 253 253 table_id VARCHAR(60) NOT NULL DEFAULT '', 254 254 is_walkin TINYINT(1) NOT NULL DEFAULT 0, 255 left_at DATETIME NULL DEFAULT NULL, 255 256 token VARCHAR(64) NOT NULL DEFAULT '', 256 257 token_expires DATETIME NULL, … … 271 272 // Add is_walkin (DB v2) explicitly if it is still missing. 272 273 self::ensure_column( 'is_walkin', 'TINYINT(1) NOT NULL DEFAULT 0' ); 274 // DB v4: when a seated guest got up — frees the table before the turn time ends. 275 self::ensure_column( 'left_at', 'DATETIME NULL DEFAULT NULL' ); 273 276 274 277 update_option( self::DB_OPT, self::DB_VERSION ); … … 487 490 488 491 /** 492 * How long one booking actually holds its table: its turn time, cut short when the 493 * guest has already left (operator pressed "Guest left"). 494 * 495 * Every occupancy calculation goes through this — the add-form hint, the covers 496 * read-out, table suggestions, the timeline bars and the room colours — so a table 497 * is free again the moment the guest is gone instead of when the clock says so. 498 * Never longer than the turn time: a "left" pressed late (or on yesterday's row) 499 * must not stretch a booking over the next seating. 500 * 501 * @param object $r Row with res_date, res_time, party_size and (optionally) left_at. 502 * @param int $location_id Pro: resolves that location's settings. 503 * @return int Minutes, 0 when the guest left before their slot had even begun. 504 */ 505 public static function hold_for( $r, $location_id = 0 ) { 506 $turn = self::turn_for( (int) $r->party_size, $location_id ); 507 $left = isset( $r->left_at ) ? (string) $r->left_at : ''; 508 if ( '' === $left || 0 === strpos( $left, '0000' ) ) { 509 return $turn; 510 } 511 $start = self::datetime_ts( $r->res_date, substr( (string) $r->res_time, 0, 5 ), $location_id ); 512 $gone = strtotime( $left ); 513 if ( ! $start || ! $gone ) { 514 return $turn; 515 } 516 // Both sides are site-local wall-clock strings read under WordPress's UTC 517 // default timezone, so the difference is exact. 518 return max( 0, min( $turn, (int) floor( ( $gone - $start ) / MINUTE_IN_SECONDS ) ) ); 519 } 520 521 /** 489 522 * The turn-time ladder in words — "1-4: 120 min - 5-8: 150 min - 9-12: 180 min". 490 523 * … … 703 736 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 704 737 $rows = $wpdb->get_results( $wpdb->prepare( 705 "SELECT res_ time, party_sizeFROM {$table}738 "SELECT res_date, res_time, party_size, left_at FROM {$table} 706 739 WHERE res_date = %s AND status IN ( %s, %s ) AND location_id = %d", 707 740 $date, self::STATUS_CONFIRMED, self::STATUS_ARRIVED, (int) $location_id … … 715 748 continue; 716 749 } 717 $e = $s + self:: turn_for( (int) $r->party_size, $location_id );750 $e = $s + self::hold_for( $r, $location_id ); 718 751 if ( $s < $win_e && $win_s < $e ) { // intervals overlap 719 752 $covers += (int) $r->party_size; … … 741 774 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 742 775 $rows = $wpdb->get_results( $wpdb->prepare( 743 "SELECT res_ time, party_sizeFROM {$table}776 "SELECT res_date, res_time, party_size, left_at FROM {$table} 744 777 WHERE res_date = %s AND status IN ( %s, %s ) AND location_id = %d", 745 778 $date, self::STATUS_CONFIRMED, self::STATUS_ARRIVED, (int) $location_id … … 753 786 continue; 754 787 } 755 if ( $s <= $t && $t < $s + self:: turn_for( (int) $r->party_size, $location_id ) ) { // active at this instant788 if ( $s <= $t && $t < $s + self::hold_for( $r, $location_id ) ) { // active at this instant 756 789 $covers += (int) $r->party_size; 757 790 } … … 980 1013 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Update internal plugin table, no caching needed. 981 1014 $res = $wpdb->update( self::table(), $fields, array( 'id' => (int) $id ), $formats, array( '%d' ) ); 1015 return false !== $res; 1016 } 1017 1018 /** 1019 * Records when a seated guest got up ('Y-m-d H:i:s', site time) — or clears it with 1020 * null. Status is left alone: the guest did arrive. The transition rules live in 1021 * Flavor_Reservation_Form::operator_apply() ('left' / 'back'). 1022 */ 1023 public static function set_left( $id, $left_at ) { 1024 global $wpdb; 1025 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Update internal plugin table. 1026 $res = $wpdb->update( 1027 self::table(), 1028 array( 'left_at' => ( null === $left_at ) ? null : (string) $left_at, 'updated_at' => current_time( 'mysql' ) ), 1029 array( 'id' => (int) $id ), 1030 array( '%s', '%s' ), 1031 array( '%d' ) 1032 ); 982 1033 return false !== $res; 983 1034 } … … 2263 2314 'arrived' => __( 'Marked as arrived.', 'flavor-restaurant-menu' ), 2264 2315 'no_show' => __( 'Marked as no-show.', 'flavor-restaurant-menu' ), 2316 'left' => __( 'Table freed — the guest has left.', 'flavor-restaurant-menu' ), 2317 'back' => __( 'The guest is marked as back at the table.', 'flavor-restaurant-menu' ), 2265 2318 'deleted' => __( 'Reservation deleted.', 'flavor-restaurant-menu' ), 2266 2319 'added' => __( 'Reservation added.', 'flavor-restaurant-menu' ), … … 2559 2612 do_action( 'flavor_reservation_created', $id, $data ); 2560 2613 2561 wp_safe_redirect( add_query_arg( array( 'scope' => 'upcoming', 'notice' => 'added' ), $base ) ); 2614 // Land on the booking's own day (and branch) — otherwise a booking made for next 2615 // Saturday "disappears" because the board stays on the day it was opened on. 2616 $back = array( 'scope' => 'upcoming', 'notice' => 'added', 'fdate' => $date ); 2617 if ( $location_id > 0 ) { 2618 $back['floc'] = $location_id; 2619 } 2620 wp_safe_redirect( add_query_arg( $back, $base ) ); 2562 2621 exit; 2563 2622 } -
flavor-restaurant-menu/trunk/includes/class-flavor-upsell.php
r3692870 r3695886 159 159 __( 'Draw your dining room once, then seat guests at real tables instead of guessing from a list.', 'flavor-restaurant-menu' ), 160 160 array( 161 __( 'Drag-and-drop editor for tables and zones', 'flavor-restaurant-menu' ),161 __( 'Drag-and-drop editor for rooms, walls, tables and decor', 'flavor-restaurant-menu' ), 162 162 __( 'Live room view: free, reserved and occupied at any time of day', 'flavor-restaurant-menu' ), 163 163 __( 'Seat guests with table suggestions, plus walk-ins and a per-table timeline', 'flavor-restaurant-menu' ), -
flavor-restaurant-menu/trunk/languages/flavor-restaurant-menu-de_DE.po
r3693109 r3695886 6 6 msgid "" 7 7 msgstr "" 8 "Project-Id-Version: Flavor Restaurant Menu 2. 3.2\n"8 "Project-Id-Version: Flavor Restaurant Menu 2.4\n" 9 9 "Report-Msgid-Bugs-To: https://flavor-menu.com/knowledge-base\n" 10 10 "POT-Creation-Date: 2026-05-21T00:00:00+00:00\n" … … 1362 1362 msgstr "Entwurf" 1363 1363 1364 msgid "Drag-and-drop editor for tables and zones"1365 msgstr "Drag-and-drop-Editor für Tische und Zonen"1364 msgid "Drag-and-drop editor for rooms, walls, tables and decor" 1365 msgstr "Drag-and-drop-Editor für Räume, Wände, Tische und Deko" 1366 1366 1367 1367 msgid "Draw your dining room once, then seat guests at real tables instead of guessing from a list." … … 1785 1785 msgid "Category deleted" 1786 1786 msgstr "Kategorie gelöscht" 1787 1788 msgid "Back at the table" 1789 msgstr "Doch noch da" 1790 1791 msgid "Guest left" 1792 msgstr "Gast gegangen" 1793 1794 msgid "Left at %s" 1795 msgstr "Gegangen %s" 1796 1797 msgid "Table freed — the guest has left." 1798 msgstr "Tisch freigegeben – der Gast ist gegangen." 1799 1800 msgid "The guest is marked as back at the table." 1801 msgstr "Der Gast ist wieder als anwesend markiert." 1802 1803 msgctxt "move a guest to another table" 1804 msgid "Move" 1805 msgstr "Umsetzen" -
flavor-restaurant-menu/trunk/readme.txt
r3693109 r3695886 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 2. 3.27 Stable tag: 2.4 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 50 50 * **E-mails that do the work** — Confirmation with a calendar file and a one-click cancellation link for the guest, a notification with occupancy context for you 51 51 * **Daily service board** — Today's bookings with the day's covers and the busiest moment, an approval inbox, search by name, phone or e-mail, an overdue marker and a nudge for past bookings nobody closed 52 * **Tables free up when guests leave** — One click on "Guest left" and the seats count as free again straight away, instead of when the usual time is up 52 53 * **Data protection built in** — Consent checkbox, and old bookings are deleted automatically after a number of days you choose 53 54 … … 62 63 * **Allergens & Nutrition** — 14 EU mandatory allergens (Regulation 1169/2011) + nutritional values per 100 g. Layout-specific display: 180° flip (cards), floating popup (lists), abbreviation chips (table) 63 64 * **Guest Ratings** — 1–5 star ratings in the Card Grid layout. No login required. Duplicate protection via localStorage + server-side IP hash 64 * ** Floor plan & seating** — Draw your dining room once, then seat guests at real tables instead of guessing from a list: drag & drop editor, live room view, table suggestions, walk-ins and a per-table timeline65 * **Room planner & seating** — Draw your restaurant once — rooms, walls, doors, decor and tables to scale — then seat guests at real tables instead of guessing from a list: live room view, table suggestions, moving guests between tables, walk-ins and a per-table timeline 65 66 * **Reservation analytics** — Covers per day and per hour, no-show rate and average party size over 7, 30 or 90 days 66 67 * **Multiple Locations** — Assign dishes to one or more branches and filter per location in the shortcode. Each branch keeps its own opening hours, capacity, floor plan and reservation inbox … … 188 189 == Changelog == 189 190 191 = 2.4 = 192 * New: "Guest left" frees the seats the moment a guest leaves, so availability is right again straight away. Pressed by mistake? "Back at the table" undoes it. 193 * Improved: "Create menu page for me" in the setup checklist now adds a Menu Card block, so the menu's settings are right there in the block sidebar. 194 * Fixed: the menu and reservation blocks looked unstyled in the block editor preview. Published pages were not affected. 195 * Fixed: after adding a reservation, the list now shows the day of that booking. 196 * **In Pro:** a new room editor with rooms, walls, decor and tables to scale, moving guests between tables, and a warning when a walk-in's table is needed again soon. 197 190 198 = 2.3.2 = 191 199 * Changed: the plugin is now called "Flavor – Restaurant Menu & Table Reservations", so the name says what it does. … … 268 276 == Upgrade Notice == 269 277 278 = 2.4 = 279 Adds "Guest left" to free a table early and shows the right day after adding a reservation. 280 270 281 = 2.3.2 = 271 282 Renames the plugin and fixes the booking form and the Elementor preview. Nothing changes for your menu or bookings.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)