-- =====================================================================
-- PRODUCTION VARIANT -- the same 22 `menu` rows and their `rolemenu`
-- grants, written for a production database where the first three of them
-- ALREADY EXIST.
--
--   Target   : PRODUCTION (bukanmain), already migrated
--   Written  : 2026-08-26
--   Sibling  : insert_missing_menu_and_rolemenu.sql -- the clean-slate
--              version, for a database where NONE of the 13 exist yet.
--              That file is unchanged and still correct for that case.
--              deploy.sh ships THIS one; the other stays in the repo.
--
-- ---------------------------------------------------------------------
-- WHY THERE ARE TWO FILES
-- ---------------------------------------------------------------------
-- Three of these menu rows and six rolemenu grants were inserted into
-- production by hand before this file existed:
--
--     menu      IDs 438, 439, 440
--     rolemenu  IDs 2782, 2783, 2784, 2785, 2786, 2787
--
-- Those nine IDs are exactly the ones the clean-slate file would have
-- taken, so running it against production now fails at the first INSERT
-- with ERROR 1062 Duplicate entry '438'. This file UPDATEs those nine rows
-- into the state the application needs and INSERTs only the remaining
-- nineteen menu rows and the remaining grants. End state is identical to
-- what the clean-slate file produces on an untouched database.
--
--
-- ---------------------------------------------------------------------
-- ALSO IN HERE: one UPDATE that is not an insert
-- ---------------------------------------------------------------------
-- Step 3 repairs menu #13 "Product Sample", a row that already exists in
-- every copy of this database but carries an EMPTY `LinkLaravel`, which
-- makes it 403 for every role including Administrator and invisible in the
-- sidebar. It is here rather than in its own file because it is the same
-- class of defect as the 22 missing rows -- a menu the application cannot
-- reach -- and an operator fixing one should not have to find the other.
-- ---------------------------------------------------------------------
-- ⚠️ THE NINE EXISTING ROWS ARE OVERWRITTEN, NOT MERGED
-- ---------------------------------------------------------------------
-- Every column of menu 438-440 and rolemenu 2782-2787 is rewritten from
-- the values below. That is deliberate: it makes the end state the same no
-- matter what those rows hold now, which is the only way to be correct
-- about rows this file cannot see.
--
-- THE COST: if you put something at those IDs that is NOT one of the 13
-- links below, it is GONE. Look before you run --
--
--     SELECT ID, Name, LinkLaravel, ParentID, SortNo, IsShow
--       FROM menu WHERE ID BETWEEN 438 AND 440 ORDER BY ID;
--     SELECT ID, RoleID, MenuID, IsDeleted
--       FROM rolemenu WHERE ID BETWEEN 2782 AND 2787 ORDER BY ID;
--
-- -- and if any row there is not one of these 22 links or a grant on them,
-- STOP and re-plan the IDs rather than running this file.
--
-- ---------------------------------------------------------------------
-- WHICH GRANTS THE SIX UPDATED ROWS BECOME
-- ---------------------------------------------------------------------
-- 2782-2787 become the Administrator (RoleID 1) grants on menus 441-446.
-- Those six pairs are in the required set no matter what, they need no
-- lookup against production's `role` table, and RoleID 1 is the one role
-- guaranteed to exist. The remaining grants are inserted normally and pick
-- up fresh AUTO_INCREMENT ids from 2788 up.
--
-- Every grant INSERT below is additionally guarded with NOT EXISTS, so a
-- grant that already exists for the same (RoleID, MenuID) is never doubled
-- -- `rolemenu` has only a plain KEY on (RoleID, MenuID), not a UNIQUE, so
-- nothing in the database itself would stop a duplicate.
--
-- ---------------------------------------------------------------------
-- IT REFUSES TO RUN IF THE PRECONDITION IS FALSE
-- ---------------------------------------------------------------------
-- Two guards run before the transaction opens. They abort with
--
--     ERROR 1054 Unknown column 'ABORT: ...' in 'field list'
--
-- which is a deliberate failure, not a bug: MySQL rejects SIGNAL inside
-- the prepared-statement protocol (ERROR 1295), so a backtick-quoted
-- identifier carries the message instead. Nothing has been changed when
-- you see it. The two conditions are:
--
--   1. menu 438, 439 and 440 must ALL exist and be undeleted. If they do
--      not, this is the wrong file -- run the clean-slate one instead.
--      Without the guard the UPDATEs would silently match zero rows and
--      leave three links missing with nothing to say so.
--   2. rolemenu 2782-2787 must ALL exist. Same reasoning: six admin
--      grants would silently go missing.
--
-- A third failure mode needs no guard because it is already loud: if
-- anything occupies menu 441-450, the INSERT fails with ERROR 1062 and the
-- whole transaction rolls back.
--
-- ---------------------------------------------------------------------
-- EVERYTHING ELSE IS AS THE CLEAN-SLATE FILE
-- ---------------------------------------------------------------------
-- The 22 links, their parents, their SortNo values and their grant sets
-- are unchanged and are documented in full in
-- insert_missing_menu_and_rolemenu.sql -- including why every live role is
-- granted all eight dashboard widgets (451-458), which is a deliberate
-- no-op rollout rather than a generous one, and why the widget rows' SortNo
-- 1-8 collide harmlessly with Pengelolaan's existing children. The short
-- version:
--
--   * A `menu` row is the ONLY place a `rolemenu` grant can hang, so until
--     these rows exist canAccessMenuLink() is false for every role,
--     Administrator included, and all 22 routes 403 for everyone.
--   * Nine of the 22 (451-459) are DashboardWidgetMenuSeeder's: eight
--     per-widget capability rows under Pengelolaan (11) and the admin page
--     that edits them. Running that seeder instead of this file is what
--     put the first nine rows in production by hand in the first place.
--   * ROOT IS SPELLED NULL, NOT 0 -- menu carries a self-referencing
--     foreign key on ParentID and there is no menu #0.
--   * Every ParentID here was read from a migrated production copy, NOT
--     from dev. The two menu trees share IDs only up to ~282.
--   * RUN THIS RATHER THAN THE SEEDERS that own these links. Several of
--     them damage a legacy database because they hardcode dev menu IDs:
--     QuotationPriceDescSeeder overwrites quotationpricedesc rows 1-3 and
--     un-deletes two, SidebarIconSeeder paints icons onto three unrelated
--     rows (355/363/392), and TestUserSeeder creates admin/admin whenever
--     APP_ENV=local.
--
-- Rolls back as a unit: START TRANSACTION ... COMMIT.
-- =====================================================================

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET NAMES utf8mb4 */;

-- ---------------------------------------------------------------------
-- 0. PRECONDITION GUARDS (before any transaction -- nothing is open yet)
-- ---------------------------------------------------------------------

SET @g := IF(
    (SELECT COUNT(*) FROM `menu`
      WHERE `ID` BETWEEN 438 AND 440 AND `IsDeleted` = 0) = 3,
    'DO 0',
    'SELECT `ABORT: menu 438, 439 and 440 must already exist -- use insert_missing_menu_and_rolemenu.sql instead` FROM `menu`'
);
PREPARE g FROM @g; EXECUTE g; DEALLOCATE PREPARE g;

SET @g := IF(
    (SELECT COUNT(*) FROM `rolemenu` WHERE `ID` BETWEEN 2782 AND 2787) = 6,
    'DO 0',
    'SELECT `ABORT: rolemenu 2782-2787 must already exist -- use insert_missing_menu_and_rolemenu.sql instead` FROM `rolemenu`'
);
PREPARE g FROM @g; EXECUTE g; DEALLOCATE PREPARE g;

START TRANSACTION;

-- ---------------------------------------------------------------------
-- 1a. menu -- OVERWRITE the three rows that already exist
-- ---------------------------------------------------------------------

-- 438  Root-level landing page. ParentID NULL, not 0 -- see header.
--      Root SortNo already has four rows tied at 0 and a landing page
--      belongs at the top, so it takes 0 as well.
UPDATE `menu` SET
    `IsDeleted` = 0, `IsShow` = 1, `IsNotification` = 0,
    `ShowName` = 'Dashboard', `Name` = 'Dashboard',
    `Link` = '', `LinkLaravel` = '/dashboard',
    `Icon` = 'Grid', `Path` = '', `ParentID` = NULL, `SortNo` = 0,
    `NotificationQuery` = '', `IsTutorial` = 0, `LinkTutorial` = '',
    `TutorialFileName` = '', `TutorialFileType` = '',
    `TutorialFileSize` = 0, `TutorialFileContent` = ''
WHERE `ID` = 438;

-- 439  Under Company (8). Siblings occupy SortNo 0-14; appended at 15.
UPDATE `menu` SET
    `IsDeleted` = 0, `IsShow` = 1, `IsNotification` = 0,
    `ShowName` = 'Create Company', `Name` = 'Create Company',
    `Link` = '', `LinkLaravel` = '/companies/create',
    `Icon` = 'Plus', `Path` = '', `ParentID` = 8, `SortNo` = 15,
    `NotificationQuery` = '', `IsTutorial` = 0, `LinkTutorial` = '',
    `TutorialFileName` = '', `TutorialFileType` = '',
    `TutorialFileSize` = 0, `TutorialFileContent` = ''
WHERE `ID` = 439;

-- 440  Under Company Project (353). Siblings occupy 0-7; appended at 8.
--      Icon NULL to match all eight existing siblings.
UPDATE `menu` SET
    `IsDeleted` = 0, `IsShow` = 1, `IsNotification` = 0,
    `ShowName` = 'View Head - Company Project',
    `Name` = 'View Head - Company Project',
    `Link` = 'listprojecthead.php', `LinkLaravel` = '/company-projects/head',
    `Icon` = NULL, `Path` = '', `ParentID` = 353, `SortNo` = 8,
    `NotificationQuery` = '', `IsTutorial` = 0, `LinkTutorial` = '',
    `TutorialFileName` = '', `TutorialFileType` = '',
    `TutorialFileSize` = 0, `TutorialFileContent` = ''
WHERE `ID` = 440;

-- ---------------------------------------------------------------------
-- 1b. menu -- INSERT the remaining nineteen, IDs 441-459
-- ---------------------------------------------------------------------

INSERT INTO `menu`
  (`ID`, `IsDeleted`, `IsShow`, `IsNotification`, `ShowName`, `Name`, `Link`, `LinkLaravel`,
   `Icon`, `Path`, `ParentID`, `SortNo`,
   `NotificationQuery`, `IsTutorial`, `LinkTutorial`, `TutorialFileName`, `TutorialFileType`,
   `TutorialFileSize`, `TutorialFileContent`)
VALUES

-- --- navigable pages (IsShow = 1) -------------------------------------

-- Under Principal (220). Siblings occupy 1-3; appended at 4.
-- No icon on the leaf, deliberately (PrincipalMenuSeeder).
(441, 0, 1, 0, 'Principals', 'Principals', '', '/principals',
 '', '', 220, 4, '', 0, '', '', '', 0, ''),

-- Under Company (8), sibling of #399. ShowName keeps legacy wording for the
-- sidebar; Name is qualified so the Role-Menu admin screen can tell it
-- apart (SalesRebateMenuSeeder).
(442, 0, 1, 0, 'Export Company ex-Sales', 'Export Company ex-Sales - Sales Rebate',
 'printinvoicecompanyexsales.php', '/sales-rebates/export-company-ex-sales',
 '', '', 8, 16, '', 0, '', '', '', 0, ''),

-- --- capability-gate rows (IsShow = 0, never rendered) ----------------

-- Under Company Rebate (255). Siblings occupy 0-11; appended at 12.
-- Link duplicates #373 deliberately: #373 is the visible page
-- (/company-rebates/create), 443 is the hidden own-scope capability read
-- by StoreCompanyRebateRequest. 443 is IsShow=0, so legacy never renders it.
(443, 0, 0, 0, 'Create Request Company Rebate - Own', 'Create Request Company Rebate - Own',
 'createcompanyrebate.php', '/company-rebates/create-own',
 '', '', 255, 12, '', 0, '', '', '', 0, ''),

-- Under Customer Data (154). Siblings occupy 1-7; appended at 8.
(444, 0, 0, 0, 'Change Status - Credit Ceiling', 'Change Status - Credit Ceiling',
 '', '/credit-ceilings/change-status',
 '', '', 154, 8, '', 0, '', '', '', 0, ''),

-- Under Quotation (34). Siblings occupy 1-17; appended at 18 and 19.
(445, 0, 0, 0, 'Change Status', 'Change Status', '', '/quotations/change-status',
 '', '', 34, 18, '', 0, '', '', '', 0, ''),
(446, 0, 0, 0, 'Cancel Quotation', 'Cancel Quotation', '', '/quotations/cancel',
 '', '', 34, 19, '', 0, '', '', '', 0, ''),

-- Under Lab Work Request (103). Siblings occupy 0-17; appended at 18 and 19.
(447, 0, 0, 0, 'Change Status LWR', 'Change Status LWR', '', '/lwrs/change-status',
 'RefreshCw', '', 103, 18, '', 0, '', '', '', 0, ''),
(448, 0, 0, 0, 'Cancel LWR', 'Cancel LWR', '', '/lwrs/cancel',
 'Ban', '', 103, 19, '', 0, '', '', '', 0, ''),

-- Under Employee (127). Siblings occupy 0,1,2,3,4,6; appended at 7.
(449, 0, 0, 0, 'Login as User', 'Login as User', '', '/user-logins/impersonate',
 '', '', 127, 7, '', 0, '', '', '', 0, ''),

-- Under Visit Report (79). Siblings occupy 0-15; appended at 16.
(450, 0, 0, 0, 'Create Visit Report (From Project)', 'Visit Plans Create From Project',
 '', '/visit-plans/create-from-project',
 '', '', 79, 16, '', 0, '', '', '', 0, ''),

-- --- dashboard widgets, under Pengelolaan (11) ------------------------
-- Eight capability rows, IsShow=0 and no icon, exactly as
-- DashboardWidgetMenuSeeder writes them. SortNo 1-8 duplicates existing
-- siblings deliberately: the rows are never rendered.

(451, 0, 0, 0, 'Ringkasan KPI', 'Ringkasan KPI', '', '/dashboard/widgets/kpis',
 '', '', 11, 1, '', 0, '', '', '', 0, ''),
(452, 0, 0, 0, 'Podium Penjualan', 'Podium Penjualan', '', '/dashboard/widgets/podium',
 '', '', 11, 2, '', 0, '', '', '', 0, ''),
(453, 0, 0, 0, 'Top Principal', 'Top Principal', '', '/dashboard/widgets/principal',
 '', '', 11, 3, '', 0, '', '', '', 0, ''),
(454, 0, 0, 0, 'Omset 12 Bulan', 'Omset 12 Bulan', '', '/dashboard/widgets/trend',
 '', '', 11, 4, '', 0, '', '', '', 0, ''),
(455, 0, 0, 0, 'Approval', 'Approval', '', '/dashboard/widgets/approvals',
 '', '', 11, 5, '', 0, '', '', '', 0, ''),
(456, 0, 0, 0, 'Transaksi Terbaru', 'Transaksi Terbaru', '', '/dashboard/widgets/recent',
 '', '', 11, 6, '', 0, '', '', '', 0, ''),
(457, 0, 0, 0, 'Ulang Tahun', 'Ulang Tahun', '', '/dashboard/widgets/birthdays',
 '', '', 11, 7, '', 0, '', '', '', 0, ''),
(458, 0, 0, 0, 'Lihat Angka Uang', 'Lihat Angka Uang', '', '/dashboard/widgets/money',
 '', '', 11, 8, '', 0, '', '', '', 0, ''),

-- The editor itself: visible in the sidebar, ordinary canAccessMenuLink
-- gate, SortNo 99 so it sorts last under Pengelolaan.
(459, 0, 1, 0, 'Pengaturan Dashboard', 'Pengaturan Dashboard',
 '', '/pengelolaan/dashboard-widgets',
 'Settings', '', 11, 99, '', 0, '', '', '', 0, '');

-- ---------------------------------------------------------------------
-- 2a. rolemenu -- OVERWRITE the six grants that already exist
-- ---------------------------------------------------------------------
-- Administrator on 441-446. Menus 441-446 were created in 1b, so the
-- rolemenu_MenuID foreign key resolves.

UPDATE `rolemenu` SET `IsDeleted` = 0, `RoleID` = 1, `MenuID` = 441 WHERE `ID` = 2782;
UPDATE `rolemenu` SET `IsDeleted` = 0, `RoleID` = 1, `MenuID` = 442 WHERE `ID` = 2783;
UPDATE `rolemenu` SET `IsDeleted` = 0, `RoleID` = 1, `MenuID` = 443 WHERE `ID` = 2784;
UPDATE `rolemenu` SET `IsDeleted` = 0, `RoleID` = 1, `MenuID` = 444 WHERE `ID` = 2785;
UPDATE `rolemenu` SET `IsDeleted` = 0, `RoleID` = 1, `MenuID` = 445 WHERE `ID` = 2786;
UPDATE `rolemenu` SET `IsDeleted` = 0, `RoleID` = 1, `MenuID` = 446 WHERE `ID` = 2787;

-- ---------------------------------------------------------------------
-- 2b. rolemenu -- INSERT the rest
-- ---------------------------------------------------------------------

-- 438 /dashboard -- every active role. DashboardController deliberately
-- does not call canAccessMenuLink('/dashboard'), so this grant governs
-- sidebar visibility only, not access.
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT 0, r.`ID`, 438
  FROM `role` r
 WHERE r.`IsDeleted` = 0
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = r.`ID` AND x.`MenuID` = 438
                      AND x.`IsDeleted` = 0);

-- 439 /companies/create -- union of the company-list grants
-- (CompanyMenuSeeder::mirrorCreateGrants). Computed from production's own
-- grants: a role list read off dev would be wrong here.
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT DISTINCT 0, rm.`RoleID`, 439
  FROM `rolemenu` rm
  JOIN `menu` m ON m.`ID` = rm.`MenuID`
  JOIN `role` r ON r.`ID` = rm.`RoleID`
 WHERE rm.`IsDeleted` = 0
   AND m.`IsDeleted`  = 0
   AND r.`IsDeleted`  = 0
   AND m.`LinkLaravel` IN ('/companies', '/companies/others', '/companies/all',
                           '/companies/head-dept', '/companies/sm')
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = rm.`RoleID` AND x.`MenuID` = 439
                      AND x.`IsDeleted` = 0);

-- 440 /company-projects/head -- mirror whoever holds /company-projects/all.
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT DISTINCT 0, rm.`RoleID`, 440
  FROM `rolemenu` rm
  JOIN `menu` m ON m.`ID` = rm.`MenuID`
  JOIN `role` r ON r.`ID` = rm.`RoleID`
 WHERE rm.`IsDeleted` = 0
   AND m.`IsDeleted`  = 0
   AND r.`IsDeleted`  = 0
   AND m.`LinkLaravel` = '/company-projects/all'
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = rm.`RoleID` AND x.`MenuID` = 440
                      AND x.`IsDeleted` = 0);

-- 450 /visit-plans/create-from-project -- VisitPlanMenuSeeder::ROLE_IDS.
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT 0, r.`ID`, 450
  FROM `role` r
 WHERE r.`IsDeleted` = 0
   AND r.`ID` IN (1, 4, 5, 6, 15, 18, 19, 20, 33)
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = r.`ID` AND x.`MenuID` = 450
                      AND x.`IsDeleted` = 0);

-- 447, 448, 449 -- Administrator baseline. (441-446 came from 2a.)
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT 0, 1, m.`ID`
  FROM `menu` m
 WHERE m.`ID` IN (447, 448, 449)
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = 1 AND x.`MenuID` = m.`ID`
                      AND x.`IsDeleted` = 0);

-- 451-458 the eight dashboard widgets -- every active role. Deliberately a
-- no-op on screen; see the clean-slate file's header.
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT 0, r.`ID`, m.`ID`
  FROM `role` r
  JOIN `menu` m ON m.`ID` BETWEEN 451 AND 458
 WHERE r.`IsDeleted` = 0
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = r.`ID` AND x.`MenuID` = m.`ID`
                      AND x.`IsDeleted` = 0);

-- 459 the widget-permission page -- Administrator only.
INSERT INTO `rolemenu` (`IsDeleted`, `RoleID`, `MenuID`)
SELECT 0, 1, m.`ID`
  FROM `menu` m
 WHERE m.`ID` = 459
   AND NOT EXISTS (SELECT 1 FROM `rolemenu` x
                    WHERE x.`RoleID` = 1 AND x.`MenuID` = 459
                      AND x.`IsDeleted` = 0);

-- ---------------------------------------------------------------------
-- 3. Repair menu #13 -- the one existing row that is unreachable
-- ---------------------------------------------------------------------
-- Menu #13 "Product Sample" (Pengelolaan > Product, ParentID 128, SortNo 1)
-- is a LEGACY row, not a missing one: it exists, it is IsShow = 1, it is
-- not soft-deleted, and it carries active `rolemenu` grants. Its
-- `LinkLaravel` is the EMPTY STRING, and that single blank is what makes it
-- unreachable in the Laravel app:
--
--   * User::allowedMenuLinks() filters `LinkLaravel <> ''`, so
--     canAccessMenuLink('/product-samples') is FALSE for every role --
--     Administrator included. The page answers 403 to everyone.
--   * Menu::navTreeForRole() requires filled(LinkLaravel) on a leaf, so the
--     sidebar entry never renders either. Nothing anywhere reports a
--     problem; the feature simply is not there.
--
-- The route and the gate have existed all along and agree on the link:
--   routes    product-samples.index / .create / .store / .edit / .update /
--             .destroy / .restore / .export-data
--   policy    App\Policies\BarangPolicy::MENU_LINK = '/product-samples'
--
-- So this is the whole fix -- write the link the code already expects. The
-- legacy `Link` column ('listbarang.php') is left ALONE: legacy CC reads it
-- and both apps share this table.
--
-- Grants are NOT touched. On production_testing (read 2026-08-27) menu 13
-- already has four active grants -- roles 1 (Administrator), 2 (Sample
-- Order Supervisor), 26 (Logistic Manager), 37 (Logistic - GA Manager) --
-- plus three soft-deleted ones. Those roles get the page back the moment
-- the link is filled in, which is exactly the intent legacy recorded.
--
-- Idempotent: re-running writes the same value. The `Name` guard is there
-- so this cannot land on an unrelated row if ID 13 ever means something
-- else in the database you are pointing at.
UPDATE `menu`
   SET `LinkLaravel` = '/product-samples'
 WHERE `ID`   = 13
   AND `Name` = 'Product Sample';

-- ---------------------------------------------------------------------
-- 4. Keep AUTO_INCREMENT ahead of the explicit IDs
-- ---------------------------------------------------------------------
ALTER TABLE `menu` AUTO_INCREMENT = 460;

COMMIT;

-- =====================================================================
-- VERIFY (run after COMMIT)
-- =====================================================================
--   -- the 22 rows, three overwritten and nineteen new:
--   SELECT `ID`,`Name`,`LinkLaravel`,`ParentID`,`SortNo`,`IsShow`
--     FROM `menu` WHERE `ID` BETWEEN 438 AND 459 ORDER BY `ID`;
--   -- expect 22 rows, 438 with ParentID NULL, no other NULL parent
--
--   -- every new row must resolve to a real parent (NULL only for 438):
--   SELECT c.`ID`, c.`Name`, c.`ParentID`
--     FROM `menu` c LEFT JOIN `menu` p ON p.`ID` = c.`ParentID`
--    WHERE c.`ID` BETWEEN 438 AND 459
--      AND c.`ParentID` IS NOT NULL AND p.`ID` IS NULL;
--   -- expect 0 rows
--
--   -- no new row may be left ungranted (that is what made them unreachable):
--   SELECT m.`ID`, m.`LinkLaravel`
--     FROM `menu` m
--     LEFT JOIN `rolemenu` rm ON rm.`MenuID` = m.`ID` AND rm.`IsDeleted` = 0
--    WHERE m.`ID` BETWEEN 438 AND 459 AND rm.`ID` IS NULL;
--   -- expect 0 rows
--
--   -- no link may be granted twice to the same role:
--   SELECT `RoleID`, `MenuID`, COUNT(*) FROM `rolemenu`
--    WHERE `MenuID` BETWEEN 438 AND 459 AND `IsDeleted` = 0
--    GROUP BY `RoleID`, `MenuID` HAVING COUNT(*) > 1;
--   -- expect 0 rows
--
--   -- the six reused ids must now be the 441-446 admin grants:
--   SELECT `ID`, `RoleID`, `MenuID` FROM `rolemenu`
--    WHERE `ID` BETWEEN 2782 AND 2787 ORDER BY `ID`;
--   -- expect RoleID 1 on MenuID 441, 442, 443, 444, 445, 446
--
--   -- grant fan-out per row:
--   SELECT m.`ID`, m.`LinkLaravel`, COUNT(rm.`ID`) AS grants
--     FROM `menu` m
--     LEFT JOIN `rolemenu` rm ON rm.`MenuID` = m.`ID` AND rm.`IsDeleted` = 0
--    WHERE m.`ID` BETWEEN 438 AND 459
--    GROUP BY m.`ID`, m.`LinkLaravel` ORDER BY m.`ID`;
--   -- 441..449 and 459 are 1 each. 438, 439, 440, 450 and the eight
--   -- widgets 451-458 are computed from production's own `role` and
--   -- `rolemenu` rows, so their counts depend on production, not on this
--   -- file. On the migrated copy read 2026-08-26 they came out
--   -- 50 / 15 / 1 / 9 and 50 each, for 485 grants in total.
--
--   SELECT AUTO_INCREMENT FROM `information_schema`.`TABLES`
--    WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'menu';
--   -- expect 460. ⚠️ information_schema caches this value; if it looks
--   -- stale, SHOW CREATE TABLE `menu` reads the live one.
--
--   -- menu 13 must now be reachable -- expect one row,
--   -- LinkLaravel = '/product-samples', IsShow = 1, IsDeleted = 0:
--   SELECT `ID`, `Name`, `Link`, `LinkLaravel`, `ParentID`, `IsShow`, `IsDeleted`
--     FROM `menu` WHERE `ID` = 13;
--
--   -- and someone must hold it, or the repair changes nothing visible:
--   SELECT COUNT(*) FROM `rolemenu`
--    WHERE `MenuID` = 13 AND `IsDeleted` = 0;   -- expect >= 1
-- =====================================================================
