Plugin Directory

Changeset 3695886


Ignore:
Timestamp:
09/14/2026 08:33:44 PM (7 weeks ago)
Author:
flavorstudio
Message:

Version 2.4 - Room planner (Pro), guest left, move guests, menu page as block, block editor preview styles

Location:
flavor-restaurant-menu/trunk
Files:
13 edited

Legend:

Unmodified
Added
Removed
  • flavor-restaurant-menu/trunk/assets/css/flavor-admin-app.css

    r3692870 r3695886  
    357357#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); }
    358358#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; }
    359367
    360368/* Reservation Settings — per-location switcher (Flavor pills, replaces the WP .subsubsub) */
    … …  
    912920#flavor-app .flavor-seat-pick small{ color:var(--muted); }
    913921#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; }
    914925
    915926/* walk-in fields */
  • flavor-restaurant-menu/trunk/assets/js/flavor-reservation-admin.js

    r3682810 r3695886  
    353353
    354354        // ---- 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) {
    356357                var box = modalBox();
    357358                box.innerHTML = "";
    … …  
    382383                var tablesById = {};
    383384                (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                }
    386403
    387404                // header
    388405                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")));
    390407                head.appendChild(el("p", "flavor-seat-sub", res.name || ""));
    391408                box.appendChild(head);
    … …  
    402419                body.appendChild(bc);
    403420
    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; });
    408429
    409430                function pickNode(t, selectable, tag) {
    … …  
    419440                                        p.classList.add("is-sel");
    420441                                        if (confirmBtn) { confirmBtn.disabled = false; }
     442                                        updateWarn();
    421443                                });
    422444                        }
    … …  
    424446                }
    425447
     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                }
    426454                if (fitFree.length) {
    427455                        body.appendChild(el("p", "flavor-seat-sublabel", i18n.freeFit || ""));
    … …  
    433461                        body.appendChild(el("p", "flavor-seat-sublabel", i18n.tooSmall || ""));
    434462                        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, "")); });
    436464                        body.appendChild(g2);
    437465                }
    … …  
    439467                        body.appendChild(el("p", "flavor-seat-sublabel", i18n.taken || ""));
    440468                        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, "")); });
    442470                        body.appendChild(g3);
    443471                }
     472                warnEl = el("p", "flavor-seat-warn");
     473                warnEl.setAttribute("role", "status");
     474                body.appendChild(warnEl);
     475                updateWarn();
    444476                box.appendChild(body);
    445477
    … …  
    471503                }
    472504
    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"));
    474506                confirmBtn.type = "button";
    475507                confirmBtn.disabled = !sel;
    … …  
    480512                                if (j && j.success) {
    481513                                        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]));
    483515                                        close();
    484516                                        if (reloadsAfterSeating()) {
    … …  
    641673                if (!btn) { return; }
    642674                e.preventDefault();
     675                var menu = btn.closest("details");
     676                if (menu) { menu.open = false; } // opened from the row's "⋯" menu
    643677                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) {
    645682                open();
    646683                modalBox().innerHTML = '<div class="flavor-seat-loading">' + (i18n.loading || "…") + "</div>";
    647684                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); }
    649686                        else { modalBox().innerHTML = '<div class="flavor-seat-loading">' + ((j && j.data && j.data.message) || i18n.loadErr || "") + "</div>"; }
    650687                }).catch(function () {
    651688                        modalBox().innerHTML = '<div class="flavor-seat-loading">' + (i18n.loadErr || "") + "</div>";
    652689                });
     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") || "");
    653738        });
    654739})();
  • flavor-restaurant-menu/trunk/flavor-restaurant-menu.php

    r3693109 r3695886  
    44 * Plugin URI: https://flavor-menu.com
    55 * 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.2
     6 * Version: 2.4
    77 * Author: Flavor Studio
    88 * Requires at least: 6.0
    … …  
    1818}
    1919
    20 define( 'FLAVOR_MENU_VERSION', '2.3.2' );
     20define( 'FLAVOR_MENU_VERSION', '2.4' );
    2121define( 'FLAVOR_MENU_PATH', plugin_dir_path( __FILE__ ) );
    2222define( 'FLAVOR_MENU_URL', plugin_dir_url( __FILE__ ) );
  • flavor-restaurant-menu/trunk/includes/class-flavor-admin.php

    r3692870 r3695886  
    103103                        // from the dishes list). Excluded from subnav, collapse count and landing.
    104104                        '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,
    105108                        // Optional subnav badge: a callable returning an int; a positive count is
    106109                        // shown as a bubble on the subnav link (e.g. open reservation requests).
    … …  
    341344                $group        = $screen['group'];
    342345                $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' : '' );
    346350                // Painted by PHP rather than by JavaScript after load — otherwise every page
    347351                // change flashes the light theme first.
    … …  
    15561560                        $minute = ( (int) $m[1] * 60 ) + (int) $m[2];
    15571561                        // 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                        }
    15611572                        $booked[] = array( (int) Flavor_Reservations::business_min( $minute, $open ), $party, $hhmm, $turn );
    15621573                }
    … …  
    16681679        }
    16691680
     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
    16701689        /** Success notice after a post-redirect-get from the reservation handlers. */
    16711690        private static function res_notice() {
    … …  
    16811700                        'arrived' => __( 'Marked as arrived.', 'flavor-restaurant-menu' ),
    16821701                        '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' ),
    16831704                        'deleted' => __( 'Reservation deleted.', 'flavor-restaurant-menu' ),
    16841705                        'added'   => __( 'Reservation added.', 'flavor-restaurant-menu' ),
    … …  
    22392260                                                                                <span class="flv-pill flv-pill--overdue"><?php esc_html_e( 'Overdue', 'flavor-restaurant-menu' ); ?></span>
    22402261                                                                        <?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; ?>
    22412265                                                                </td>
    22422266                                                                <?php if ( $seating ) : ?>
    … …  
    22742298                $is_active = in_array( $r->status, array( Flavor_Reservations::STATUS_CONFIRMED, Flavor_Reservations::STATUS_ARRIVED ), true );
    22752299                $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' );
    22762305                ?>
    22772306                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="flv-resactions">
    … …  
    22832312                        <?php if ( $is_open ) : ?>
    22842313                                <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 ) : ?>
    22862317                                <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 ); ?>
    22882319                                </button>
    22892320                        <?php endif; ?>
    … …  
    22942325                                        <?php if ( $is_open ) : ?>
    22952326                                                <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>
    22962333                                        <?php endif; ?>
    22972334                                        <?php if ( Flavor_Reservations::STATUS_CONFIRMED === $r->status ) : ?>
  • flavor-restaurant-menu/trunk/includes/class-flavor-block.php

    r3692870 r3695886  
    1919        add_action( 'init',                        array( __CLASS__, 'register_block' ) );
    2020        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' );
    2139    }
    2240
  • flavor-restaurant-menu/trunk/includes/class-flavor-onboarding.php

    r3587609 r3695886  
    102102    }
    103103
    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. */
    105121    public static function handle_create_page() {
    106122        if ( ! current_user_can( 'manage_options' ) ) {
    … …  
    121137            'post_title'   => __( 'Our Menu', 'flavor-restaurant-menu' ),
    122138            'post_status'  => 'publish',
    123             'post_content' => '[flavor_menu]',
     139            'post_content' => self::menu_page_content(),
    124140        ) );
    125141
  • flavor-restaurant-menu/trunk/includes/class-flavor-reservation-block.php

    r3692870 r3695886  
    2020        public static function init() {
    2121                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' );
    2238        }
    2339
  • flavor-restaurant-menu/trunk/includes/class-flavor-reservation-form.php

    r3693109 r3695886  
    787787                                Flavor_Reservations::set_status( $id, Flavor_Reservations::STATUS_NO_SHOW );
    788788                                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 );
    789806                }
    790807
  • flavor-restaurant-menu/trunk/includes/class-flavor-reservations.php

    r3692870 r3695886  
    3535
    3636        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
    3838        const DB_OPT     = 'flavor_reservations_db_version';
    3939        const OPTION_KEY = 'flavor_reservation_settings';
    … …  
    253253                        table_id      VARCHAR(60) NOT NULL DEFAULT '',
    254254                        is_walkin     TINYINT(1) NOT NULL DEFAULT 0,
     255                        left_at       DATETIME NULL DEFAULT NULL,
    255256                        token         VARCHAR(64) NOT NULL DEFAULT '',
    256257                        token_expires DATETIME NULL,
    … …  
    271272                // Add is_walkin (DB v2) explicitly if it is still missing.
    272273                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' );
    273276
    274277                update_option( self::DB_OPT, self::DB_VERSION );
    … …  
    487490
    488491        /**
     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        /**
    489522         * The turn-time ladder in words — "1-4: 120 min - 5-8: 150 min - 9-12: 180 min".
    490523         *
    … …  
    703736                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    704737                $rows = $wpdb->get_results( $wpdb->prepare(
    705                         "SELECT res_time, party_size FROM {$table}
     738                        "SELECT res_date, res_time, party_size, left_at FROM {$table}
    706739                         WHERE res_date = %s AND status IN ( %s, %s ) AND location_id = %d",
    707740                        $date, self::STATUS_CONFIRMED, self::STATUS_ARRIVED, (int) $location_id
    … …  
    715748                                continue;
    716749                        }
    717                         $e = $s + self::turn_for( (int) $r->party_size, $location_id );
     750                        $e = $s + self::hold_for( $r, $location_id );
    718751                        if ( $s < $win_e && $win_s < $e ) { // intervals overlap
    719752                                $covers += (int) $r->party_size;
    … …  
    741774                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    742775                $rows = $wpdb->get_results( $wpdb->prepare(
    743                         "SELECT res_time, party_size FROM {$table}
     776                        "SELECT res_date, res_time, party_size, left_at FROM {$table}
    744777                         WHERE res_date = %s AND status IN ( %s, %s ) AND location_id = %d",
    745778                        $date, self::STATUS_CONFIRMED, self::STATUS_ARRIVED, (int) $location_id
    … …  
    753786                                continue;
    754787                        }
    755                         if ( $s <= $t && $t < $s + self::turn_for( (int) $r->party_size, $location_id ) ) { // active at this instant
     788                        if ( $s <= $t && $t < $s + self::hold_for( $r, $location_id ) ) { // active at this instant
    756789                                $covers += (int) $r->party_size;
    757790                        }
    … …  
    9801013                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Update internal plugin table, no caching needed.
    9811014                $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                );
    9821033                return false !== $res;
    9831034        }
    … …  
    22632314                        'arrived' => __( 'Marked as arrived.', 'flavor-restaurant-menu' ),
    22642315                        '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' ),
    22652318                        'deleted' => __( 'Reservation deleted.', 'flavor-restaurant-menu' ),
    22662319                        'added'   => __( 'Reservation added.', 'flavor-restaurant-menu' ),
    … …  
    25592612                do_action( 'flavor_reservation_created', $id, $data );
    25602613
    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 ) );
    25622621                exit;
    25632622        }
  • flavor-restaurant-menu/trunk/includes/class-flavor-upsell.php

    r3692870 r3695886  
    159159            __( 'Draw your dining room once, then seat guests at real tables instead of guessing from a list.', 'flavor-restaurant-menu' ),
    160160            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' ),
    162162                __( 'Live room view: free, reserved and occupied at any time of day', 'flavor-restaurant-menu' ),
    163163                __( '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  
    66msgid ""
    77msgstr ""
    8 "Project-Id-Version: Flavor Restaurant Menu 2.3.2\n"
     8"Project-Id-Version: Flavor Restaurant Menu 2.4\n"
    99"Report-Msgid-Bugs-To: https://flavor-menu.com/knowledge-base\n"
    1010"POT-Creation-Date: 2026-05-21T00:00:00+00:00\n"
    … …  
    13621362msgstr "Entwurf"
    13631363
    1364 msgid "Drag-and-drop editor for tables and zones"
    1365 msgstr "Drag-and-drop-Editor für Tische und Zonen"
     1364msgid "Drag-and-drop editor for rooms, walls, tables and decor"
     1365msgstr "Drag-and-drop-Editor für Räume, Wände, Tische und Deko"
    13661366
    13671367msgid "Draw your dining room once, then seat guests at real tables instead of guessing from a list."
    … …  
    17851785msgid "Category deleted"
    17861786msgstr "Kategorie gelöscht"
     1787
     1788msgid "Back at the table"
     1789msgstr "Doch noch da"
     1790
     1791msgid "Guest left"
     1792msgstr "Gast gegangen"
     1793
     1794msgid "Left at %s"
     1795msgstr "Gegangen %s"
     1796
     1797msgid "Table freed — the guest has left."
     1798msgstr "Tisch freigegeben – der Gast ist gegangen."
     1799
     1800msgid "The guest is marked as back at the table."
     1801msgstr "Der Gast ist wieder als anwesend markiert."
     1802
     1803msgctxt "move a guest to another table"
     1804msgid "Move"
     1805msgstr "Umsetzen"
  • flavor-restaurant-menu/trunk/readme.txt

    r3693109 r3695886  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 2.3.2
     7Stable tag: 2.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    5050* **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
    5151* **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
    5253* **Data protection built in** — Consent checkbox, and old bookings are deleted automatically after a number of days you choose
    5354
    … …  
    6263* **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)
    6364* **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 timeline
     65* **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
    6566* **Reservation analytics** — Covers per day and per hour, no-show rate and average party size over 7, 30 or 90 days
    6667* **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
    … …  
    188189== Changelog ==
    189190
     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
    190198= 2.3.2 =
    191199* Changed: the plugin is now called "Flavor – Restaurant Menu & Table Reservations", so the name says what it does.
    … …  
    268276== Upgrade Notice ==
    269277
     278= 2.4 =
     279Adds "Guest left" to free a table early and shows the right day after adding a reservation.
     280
    270281= 2.3.2 =
    271282Renames 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.