diff --git a/docs/WORKSPACE-CATALOGS-20260917.md b/docs/WORKSPACE-CATALOGS-20260917.md new file mode 100644 index 0000000..0f827b7 --- /dev/null +++ b/docs/WORKSPACE-CATALOGS-20260917.md @@ -0,0 +1,7 @@ +# Empty company catalogs + +Catalog records and photos now come solely from each company's stored data. Removed the bundled starter and legacy catalog arrays, automatic catalog merges, fresh-workspace seeding and demo-data onboarding. Developer-created companies also receive an empty catalog. Existing workspace catalogs and manually added items are preserved. + +The Solnce composition refresh is scoped to its existing workspace. The earlier authorized import retained that workspace's catalog in the database; other companies no longer receive its boxes or premium items by default. + +Validation covers an empty first launch, preservation of a company's custom catalog after reload, returning to an empty catalog after changing local workspace state, server onboarding, and the existing permissions/branding/pricing suites. Customer data is not part of the repository. diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 1ec8f32..5619823 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v85-20260917-order-import", + "pwaCache": "v86-20260917-empty-catalog", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/public/app-runtime.js b/public/app-runtime.js index b1dd43e..17ef6d6 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -1,5 +1,5 @@ /* ===== MODULE: legacy-catalog-v175.js ===== */ -window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс № 1 Канапе мясные","category":0,"price":3690,"photo":"catalog/001.jpg","weight":"600 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. канапе с мортаделлой и дроблённой фисташкой","5 шт. канапе с салями мадеро","5 шт. канапе с салями парма","5 шт. канапе с прошуто","5 шт. канапе с шейками капоколло с вялеными томатами"],"ingredients":[["канапе с мортаделлой и дроблённой фисташкой",5,"шт."],["канапе с салями мадеро",5,"шт."],["канапе с салями парма",5,"шт."],["канапе с прошуто",5,"шт."],["канапе с шейками капоколло с вялеными томатами",5,"шт."]],"catalogOrder":1},{"id":"2","name":"Фуршетный бокс № 2 Канапе рыбные","category":0,"price":4590,"photo":"catalog/002.jpg","weight":"600 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. тартинка с красной икрой и кремчизом","5 шт. кростини с лососем и кремчизом","5 шт. тартинка с креветкой и соусом цезарь","5 шт. кростини с креветкой и соусом чили","5 шт. кростини с форелью и сливочным соусом"],"ingredients":[["тартинка с красной икрой и кремчизом",5,"шт."],["кростини с лососем и кремчизом",5,"шт."],["тартинка с креветкой и соусом цезарь",5,"шт."],["кростини с креветкой и соусом чили",5,"шт."],["кростини с форелью и сливочным соусом",5,"шт."]],"catalogOrder":2},{"id":"3","name":"Фуршетный бокс № 3 Микс тарталеток","category":0,"price":4590,"photo":"catalog/003.jpg","weight":"790 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. тартинка с креветкой и соусом цезарь","5 шт. тартинка с щучьей икрой и мягким сыром","5 шт. тартинка с красной икрой и взбитым маслом","5 шт. тартинка с прошуто и грушей","5 шт. тартинка с мортаделлой"],"ingredients":[["тартинка с креветкой и соусом цезарь",5,"шт."],["тартинка с щучьей икрой и мягким сыром",5,"шт."],["тартинка с красной икрой и взбитым маслом",5,"шт."],["тартинка с прошуто и грушей",5,"шт."],["тартинка с мортаделлой",5,"шт."]],"catalogOrder":3},{"id":"4","name":"Фуршетный бокс № 4 Премиум морепродукты","category":0,"price":6990,"photo":"catalog/004.jpg","weight":"625 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. тартинка с тунцом","5 шт. канапе с тигровой креветкой с чили","5 шт. ролл из шпината с кремчизом и красной икрой","5 шт. ролл с чернилами каракатицы, лососем и сливочным сыром","5 шт. тартинка с тигровой креветкой и соусом из тунца"],"ingredients":[["тартинка с тунцом",5,"шт."],["канапе с тигровой креветкой с чили",5,"шт."],["ролл из шпината с кремчизом и красной икрой",5,"шт."],["ролл с чернилами каракатицы, лососем и сливочным сыром",5,"шт."],["тартинка с тигровой креветкой и соусом из тунца",5,"шт."]],"catalogOrder":4},{"id":"5","name":"Фуршетный бокс № 5 Брускетты мясные","category":0,"price":3990,"photo":"catalog/005.jpg","weight":"750 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. говядина с печёным перцем и горчицей","5 шт. салями мадеро с оливкой","5 шт. салями парма с маслинами и огурцом","5 шт. прошуто с грушой и кремчизом","5 шт. шейка капоколло с вяленым томатом"],"ingredients":[["говядина с печёным перцем и горчицей",5,"шт."],["салями мадеро с оливкой",5,"шт."],["салями парма с маслинами и огурцом",5,"шт."],["прошуто с грушой и кремчизом",5,"шт."],["шейка капоколло с вяленым томатом",5,"шт."]],"catalogOrder":5},{"id":"6","name":"Фуршетный бокс № 6 Сыры в шотах","category":0,"price":5490,"photo":"catalog/006.jpg","weight":"900 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. моцарелла с томатами и песто","5 шт. дорблю с грушей и малиной","5 шт. бри с миндалём и медовыми сотами","5 шт. пармезан с голубикой и пеканом","5 шт. чеддер с виноградом"],"ingredients":[["моцарелла с томатами и песто",5,"шт."],["дорблю с грушей и малиной",5,"шт."],["бри с миндалём и медовыми сотами",5,"шт."],["пармезан с голубикой и пеканом",5,"шт."],["чеддер с виноградом",5,"шт."]],"catalogOrder":6},{"id":"catalog-007","name":"Фуршетный бокс № 7 Сыр канапе","category":0,"price":3510,"photo":"catalog/007.jpg","weight":"450 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. бри с клубникой","5 шт. эдам с курагой","5 шт. пармезан с инжиром и голубикой","10 шт. чеддер с виноградом","5 шт. камамбер с голубикой"],"ingredients":[["бри с клубникой",5,"шт."],["эдам с курагой",5,"шт."],["пармезан с инжиром и голубикой",5,"шт."],["чеддер с виноградом",10,"шт."],["камамбер с голубикой",5,"шт."]],"catalogOrder":7},{"id":"catalog-008","name":"Фуршетный бокс № 50 Мини-эклеры","category":0,"price":4490,"photo":"catalog/008.jpg","weight":"500 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. мини-эклер с паштетом из куриных жареных печёнок — 20 г","5 шт. мини-эклер с риетом из тунца — 20 г","5 шт. мини-эклер с красной икрой и крем-чизом — 20 г","5 шт. мини-эклер с чёрной структурированной икрой и крем-чизом — 20 г","5 шт. мини-эклер с форелью и крем-чизом — 20 г"],"ingredients":[["мини-эклер с паштетом из куриных жареных печёнок — 20 г",5,"шт."],["мини-эклер с риетом из тунца — 20 г",5,"шт."],["мини-эклер с красной икрой и крем-чизом — 20 г",5,"шт."],["мини-эклер с чёрной структурированной икрой и крем-чизом — 20 г",5,"шт."],["мини-эклер с форелью и крем-чизом — 20 г",5,"шт."]],"catalogOrder":8},{"id":"catalog-009","name":"Фуршетный бокс № 21 Антипасти","category":0,"price":5590,"photo":"catalog/009.jpg","weight":"675 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. перчики греко с мягким сыром","5 шт. канапе с салями мадеро","5 шт. антипасти с оливками, томатами и виноградом","5 шт. ассорти сыров","5 шт. артишоки, вяленые томаты и печёный перец"],"ingredients":[["перчики греко с мягким сыром",5,"шт."],["канапе с салями мадеро",5,"шт."],["антипасти с оливками, томатами и виноградом",5,"шт."],["ассорти сыров",5,"шт."],["артишоки, вяленые томаты и печёный перец",5,"шт."]],"catalogOrder":9},{"id":"catalog-010","name":"Фуршетный бокс № 48 Мини-эклеры","category":0,"price":3790,"photo":"catalog/010.jpg","weight":"500 г","catalogSection":"Канапе и фуршетные закуски","composition":["15 шт. мини-эклер с паштетом из куриных жареных печёнок — 20 г","10 шт. мини-эклер с красной икрой и крем-чизом — 20 г"],"ingredients":[["мини-эклер с паштетом из куриных жареных печёнок — 20 г",15,"шт."],["мини-эклер с красной икрой и крем-чизом — 20 г",10,"шт."]],"catalogOrder":10},{"id":"catalog-011","name":"Фуршетный бокс № 15 Канапе рыбные","category":0,"price":5490,"photo":"catalog/011.jpg","weight":"700 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. крости с тар-таром из креветок","5 шт. канапе с тунцом и юдзу","5 шт. канапе с креветкой в панко и чили-соусом","5 шт. канапе с лососем и мягким сыром","5 шт. канапе с креветкой и соусом Цезарь"],"ingredients":[["крости с тар-таром из креветок",5,"шт."],["канапе с тунцом и юдзу",5,"шт."],["канапе с креветкой в панко и чили-соусом",5,"шт."],["канапе с лососем и мягким сыром",5,"шт."],["канапе с креветкой и соусом Цезарь",5,"шт."]],"catalogOrder":11},{"id":"catalog-012","name":"Фуршетный бокс № 16 Вегетарианский","category":0,"price":3790,"photo":"catalog/012.jpg","weight":"700 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. крости с моцареллой, черри и песто","5 шт. крости с баклажановой икрой и фетой","5 шт. крости с грибной икрой и трюфельной пастой","5 шт. крости с печёным перцем и фетой","5 шт. крости с вялеными томатами"],"ingredients":[["крости с моцареллой, черри и песто",5,"шт."],["крости с баклажановой икрой и фетой",5,"шт."],["крости с грибной икрой и трюфельной пастой",5,"шт."],["крости с печёным перцем и фетой",5,"шт."],["крости с вялеными томатами",5,"шт."]],"catalogOrder":12},{"id":"catalog-013","name":"Фуршетный бокс № 18 Тосты ассорти","category":0,"price":3190,"photo":"catalog/013.jpg","weight":"820 г","catalogSection":"Канапе и фуршетные закуски","composition":["25 шт. тостов ассорти","Тосты с селёдкой на свекольном муссе","Тосты с салом и солёным огурчиком"],"ingredients":[["тостов ассорти",25,"шт."],["Тосты с селёдкой на свекольном муссе",1,"поз."],["Тосты с салом и солёным огурчиком",1,"поз."]],"catalogOrder":13},{"id":"catalog-014","name":"Фуршетный бокс № 43 Тарталетки с икрой","category":0,"price":4890,"photo":"catalog/014.jpg","weight":"625 г","catalogSection":"Канапе и фуршетные закуски","composition":["15 шт. тарталетка с красной икрой и крем-чизом","10 шт. тарталетка с чёрной структурированной икрой и крем-чизом"],"ingredients":[["тарталетка с красной икрой и крем-чизом",15,"шт."],["тарталетка с чёрной структурированной икрой и крем-чизом",10,"шт."]],"catalogOrder":14},{"id":"catalog-015","name":"Фуршетный бокс № 49 Мини-эклеры","category":0,"price":3490,"photo":"catalog/015.jpg","weight":"500 г","catalogSection":"Канапе и фуршетные закуски","composition":["15 шт. мини-эклер с паштетом из куриных жареных печёнок — 20 г","10 шт. мини-эклер с риетом из тунца — 20 г"],"ingredients":[["мини-эклер с паштетом из куриных жареных печёнок — 20 г",15,"шт."],["мини-эклер с риетом из тунца — 20 г",10,"шт."]],"catalogOrder":15},{"id":"catalog-016","name":"Фуршетный бокс № 19 Рулетики ветчина-сыр","category":0,"price":3690,"photo":"catalog/016.jpg","weight":"1000 г","catalogSection":"Канапе и фуршетные закуски","composition":["50 шт. рулетики из ветчины с сыром"],"ingredients":[["рулетики из ветчины с сыром",50,"шт."]],"catalogOrder":16},{"id":"catalog-017","name":"Фуршетный бокс № 51 Брускетты с говядиной и томаты капрезе","category":0,"price":3990,"photo":"catalog/017.jpg","weight":"840 г","catalogSection":"Канапе и фуршетные закуски","composition":["12 шт. брускетта с пастрами из говядины — 35 г","12 шт. брускетта с моцареллой и томатами черри — 35 г"],"ingredients":[["брускетта с пастрами из говядины — 35 г",12,"шт."],["брускетта с моцареллой и томатами черри — 35 г",12,"шт."]],"catalogOrder":17},{"id":"catalog-018","name":"Фуршетный бокс № 27 Брускетты с тунцом и лососем","category":0,"price":4290,"photo":"catalog/018.jpg","weight":"840 г","catalogSection":"Канапе и фуршетные закуски","composition":["12 шт. брускетта с тунцом и апельсином — 35 г","12 шт. брускетта с лососем и крем-чизом — 35 г"],"ingredients":[["брускетта с тунцом и апельсином — 35 г",12,"шт."],["брускетта с лососем и крем-чизом — 35 г",12,"шт."]],"catalogOrder":18},{"id":"catalog-019","name":"Фуршетный бокс № 20 Профитроли","category":0,"price":4890,"photo":"catalog/019.jpg","weight":"700 г","catalogSection":"Канапе и фуршетные закуски","composition":["5 шт. профитроли с говядиной и соусом вителло тонато","5 шт. профитроли с красной икрой и сливочным маслом","5 шт. профитроли с риетом из копчёной форели","5 шт. профитроли с лососем и мягким сыром","5 шт. профитроли с паштетом из жареных куриных печёнок"],"ingredients":[["профитроли с говядиной и соусом вителло тонато",5,"шт."],["профитроли с красной икрой и сливочным маслом",5,"шт."],["профитроли с риетом из копчёной форели",5,"шт."],["профитроли с лососем и мягким сыром",5,"шт."],["профитроли с паштетом из жареных куриных печёнок",5,"шт."]],"catalogOrder":19},{"id":"catalog-020","name":"Фуршетный бокс № 26 Брускетты с креветками и муссом из лосося","category":0,"price":3690,"photo":"catalog/020.jpg","weight":"780 г","catalogSection":"Канапе и фуршетные закуски","composition":["12 шт. брускетта с креветкой и чили-соусом — 35 г","12 шт. брускетта с муссом из копчёного лосося — 30 г"],"ingredients":[["брускетта с креветкой и чили-соусом — 35 г",12,"шт."],["брускетта с муссом из копчёного лосося — 30 г",12,"шт."]],"catalogOrder":20},{"id":"catalog-021","name":"Фуршетный бокс № 28 Брускетты с говядиной и индейкой","category":0,"price":4290,"photo":"catalog/021.jpg","weight":"780 г","catalogSection":"Канапе и фуршетные закуски","composition":["12 шт. брускетта с говядиной — 35 г","12 шт. брускетта с пастрами из индейки — 30 г"],"ingredients":[["брускетта с говядиной — 35 г",12,"шт."],["брускетта с пастрами из индейки — 30 г",12,"шт."]],"catalogOrder":21},{"id":"catalog-022","name":"Фуршетный бокс № 29 Брускетты с мортаделлой и салями мадера","category":0,"price":3690,"photo":"catalog/022.jpg","weight":"720 г","catalogSection":"Канапе и фуршетные закуски","composition":["12 шт. брускетта с мортаделлой — 25 г","12 шт. брускетта с салями мадера — 35 г"],"ingredients":[["брускетта с мортаделлой — 25 г",12,"шт."],["брускетта с салями мадера — 35 г",12,"шт."]],"catalogOrder":22},{"id":"catalog-023","name":"Фуршетный бокс № 9 Салаты Цезарь микс в шотах","category":0,"price":4390,"photo":"catalog/023.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["8 пор. цезарь с цыплёнком в хрустящей корочке — 75 г","8 пор. цезарь с креветкой в темпуре — 75 г"],"ingredients":[["цезарь с цыплёнком в хрустящей корочке — 75 г",8,"пор."],["цезарь с креветкой в темпуре — 75 г",8,"пор."]],"catalogOrder":23},{"id":"catalog-024","name":"Фуршетный бокс № 8 Салат Цезарь с цыплёнком в панировке","category":0,"price":3460,"photo":"catalog/024.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["16 пор. салата Цезарь: ромэн, черри, пармезан, цыплёнок в панировке, соус Цезарь, кростини — 75 г"],"ingredients":[["салата Цезарь: ромэн, черри, пармезан, цыплёнок в панировке, соус Цезарь, кростини — 75 г",16,"пор."]],"catalogOrder":24},{"id":"catalog-025","name":"Фуршетный бокс № 8 Салат Цезарь с креветкой в темпуре","category":0,"price":3790,"photo":"catalog/025.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["16 пор. салата Цезарь: ромэн, черри, пармезан, креветка в темпуре, соус Цезарь, кростини — 75 г"],"ingredients":[["салата Цезарь: ромэн, черри, пармезан, креветка в темпуре, соус Цезарь, кростини — 75 г",16,"пор."]],"catalogOrder":25},{"id":"catalog-026","name":"Фуршетный бокс № 8 Салат Мимоза с лососем г/к","category":0,"price":4250,"photo":"catalog/026.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["16 пор. салата Мимоза с лососем горячего копчения"],"ingredients":[["салата Мимоза с лососем горячего копчения",16,"пор."]],"catalogOrder":26},{"id":"catalog-027","name":"Фуршетный бокс № 8 Салат Греческий","category":0,"price":3290,"photo":"catalog/027.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["16 пор. салата Греческий — 75 г"],"ingredients":[["салата Греческий — 75 г",16,"пор."]],"catalogOrder":27},{"id":"catalog-028","name":"Фуршетный бокс № 8 Салат Оливье с говядиной","category":0,"price":4690,"photo":"catalog/028.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["16 пор. Оливье с говядиной — 75 г"],"ingredients":[["Оливье с говядиной — 75 г",16,"пор."]],"catalogOrder":28},{"id":"catalog-029","name":"Фуршетный бокс № 8 Салат Столичный с цыплёнком","category":0,"price":3890,"photo":"catalog/029.jpg","weight":"1200 г","catalogSection":"Салаты","composition":["16 пор. салата Столичный с цыплёнком в креманке — 75 г"],"ingredients":[["салата Столичный с цыплёнком в креманке — 75 г",16,"пор."]],"catalogOrder":29},{"id":"catalog-030","name":"Фуршетный бокс № 45 Салат Столичный в тарталетках","category":0,"price":3190,"photo":"catalog/030.jpg","weight":"900 г","catalogSection":"Салаты","composition":["20 шт. тарталеток","Салат Столичный с цыплёнком в тарталетках из песочного теста — 45 г"],"ingredients":[["тарталеток",20,"шт."],["Салат Столичный с цыплёнком в тарталетках из песочного теста",45,"г"]],"catalogOrder":30},{"id":"catalog-031","name":"Фуршетный бокс № 46 Салат Крабовый в тарталетках","category":0,"price":2790,"photo":"catalog/031.jpg","weight":"900 г","catalogSection":"Салаты","composition":["20 шт. тарталеток","Салат Крабовый классический в тарталетках из песочного теста — 45 г"],"ingredients":[["тарталеток",20,"шт."],["Салат Крабовый классический в тарталетках из песочного теста",45,"г"]],"catalogOrder":31},{"id":"catalog-032","name":"Фуршетный бокс № 47 Салат Оливье в тарталетках","category":0,"price":3890,"photo":"catalog/032.jpg","weight":"900 г","catalogSection":"Салаты","composition":["20 шт. тарталеток","Салат Оливье с говядиной в тарталетках из песочного теста — 45 г"],"ingredients":[["тарталеток",20,"шт."],["Салат Оливье с говядиной в тарталетках из песочного теста",45,"г"]],"catalogOrder":32},{"id":"catalog-033","name":"Фуршетный бокс № 47 Салат Оливье и Крабовый в тарталетках","category":0,"price":4290,"photo":"catalog/033.jpg","weight":"900 г","catalogSection":"Салаты","composition":["10 шт. тарталеток с Оливье и говядиной — 45 г","10 шт. тарталеток с классическим Крабовым салатом — 45 г"],"ingredients":[["тарталеток с Оливье и говядиной — 45 г",10,"шт."],["тарталеток с классическим Крабовым салатом — 45 г",10,"шт."]],"catalogOrder":33},{"id":"catalog-034","name":"Фуршетный бокс № 17 Круассаны с говядиной пастрами","category":0,"price":3500,"photo":"catalog/034.jpg","weight":"1050 г","catalogSection":"Бургеры, сэндвичи, круассаны","composition":["20 шт. круассаны с говядиной, помидором и огурцом"],"ingredients":[["круассаны с говядиной, помидором и огурцом",20,"шт."]],"catalogOrder":34},{"id":"catalog-035","name":"Фуршетный бокс № 17 Круассаны с лососем","category":0,"price":4390,"photo":"catalog/035.jpg","weight":"1200 г","catalogSection":"Бургеры, сэндвичи, круассаны","composition":["20 шт. круассаны с лососем с/с, салатом, свежим огурцом и сливочным соусом"],"ingredients":[["круассаны с лососем с/с, салатом, свежим огурцом и сливочным соусом",20,"шт."]],"catalogOrder":35},{"id":"catalog-036","name":"Фуршетный бокс № 10 Мини-бургеры классика","category":0,"price":4590,"photo":"catalog/036.jpg","weight":"1000 г","catalogSection":"Бургеры, сэндвичи, круассаны","composition":["12 шт. бургеры с говяжьей котлетой","13 шт. бургеры с куриным стрипсом"],"ingredients":[["бургеры с говяжьей котлетой",12,"шт."],["бургеры с куриным стрипсом",13,"шт."]],"catalogOrder":36},{"id":"catalog-037","name":"Фуршетный бокс № 11 Сэндвичи с ветчиной и сыром","category":0,"price":2890,"photo":"catalog/037.jpg","weight":"1200 г","catalogSection":"Бургеры, сэндвичи, круассаны","composition":["25 шт. сэндвич с ветчиной и сыром — 45 г"],"ingredients":[["сэндвич с ветчиной и сыром — 45 г",25,"шт."]],"catalogOrder":37},{"id":"catalog-038","name":"Фуршетный бокс № 12 Сэндвичи с лососем","category":0,"price":3990,"photo":"catalog/038.jpg","weight":"1200 г","catalogSection":"Бургеры, сэндвичи, круассаны","composition":["25 шт. сэндвич с огурцом и лососем — 45 г"],"ingredients":[["сэндвич с огурцом и лососем — 45 г",25,"шт."]],"catalogOrder":38},{"id":"catalog-039","name":"Фуршетный бокс № 13 Мини-бургеры ассорти","category":0,"price":5990,"photo":"catalog/039.jpg","weight":"1000 г","catalogSection":"Бургеры, сэндвичи, круассаны","composition":["5 шт. бургер с говяжьей котлетой","5 шт. чизбургер с говяжьей котлетой","5 шт. бургер с креветкой","5 шт. бургер с куриной котлетой","5 шт. чикенбургер"],"ingredients":[["бургер с говяжьей котлетой",5,"шт."],["чизбургер с говяжьей котлетой",5,"шт."],["бургер с креветкой",5,"шт."],["бургер с куриной котлетой",5,"шт."],["чикенбургер",5,"шт."]],"catalogOrder":39},{"id":"catalog-040","name":"Фуршетный бокс № 14 Жюльен","category":0,"price":3290,"photo":"catalog/040.jpg","weight":"625 г","catalogSection":"Тёплые блюда и выпечка","composition":["25 шт. грибной жюльен с сыром пармезан в тарталетках — 25 г"],"ingredients":[["грибной жюльен с сыром пармезан в тарталетках — 25 г",25,"шт."]],"catalogOrder":40},{"id":"catalog-041","name":"Фуршетный бокс № 14 Жюльен с белыми грибами","category":0,"price":5290,"photo":"catalog/041.jpg","weight":"625 г","catalogSection":"Тёплые блюда и выпечка","composition":["25 шт. жюльен: белые грибы 70%, шампиньоны 30%, пармезан, тарталетки — 25 г"],"ingredients":[["жюльен: белые грибы 70%, шампиньоны 30%, пармезан, тарталетки — 25 г",25,"шт."]],"catalogOrder":41},{"id":"catalog-042","name":"Фуршетный бокс № 44 Пирог с сыром","category":0,"price":2690,"photo":"catalog/042.jpg","weight":"450 г","catalogSection":"Тёплые блюда и выпечка","composition":["16 пор. закрытого пирога с сыром"],"ingredients":[["закрытого пирога с сыром",16,"пор."]],"catalogOrder":42},{"id":"catalog-043","name":"Фуршетный бокс № 42 Блинчики с печенью","category":0,"price":2430,"photo":"catalog/043.jpg","weight":"875 г","catalogSection":"Тёплые блюда и выпечка","composition":["25 пор. блины с куриной печенью, луком и крем-чизом"],"ingredients":[["блины с куриной печенью, луком и крем-чизом",25,"пор."]],"catalogOrder":43},{"id":"catalog-044","name":"Фуршетный бокс № 24 Кулебяка и пирожки","category":0,"price":3890,"photo":"catalog/044.jpg","weight":"1070 г","catalogSection":"Тёплые блюда и выпечка","composition":["6 шт. пирожков с картошкой — 60 г","6 шт. пирожков с мясом — 60 г","1 шт. кулебяка с капустой — 350 г"],"ingredients":[["пирожков с картошкой — 60 г",6,"шт."],["пирожков с мясом — 60 г",6,"шт."],["кулебяка с капустой — 350 г",1,"шт."]],"catalogOrder":44},{"id":"catalog-045","name":"Фуршетный бокс № 33 Пирожки","category":0,"price":3690,"photo":"catalog/045.jpg","weight":"1080 г","catalogSection":"Тёплые блюда и выпечка","composition":["6 шт. пирожков с картошкой — 60 г","6 шт. пирожков с мясом — 60 г","6 шт. пирожков с капустой — 60 г"],"ingredients":[["пирожков с картошкой — 60 г",6,"шт."],["пирожков с мясом — 60 г",6,"шт."],["пирожков с капустой — 60 г",6,"шт."]],"catalogOrder":45},{"id":"catalog-046","name":"Фуршетный бокс № 25 Детский","category":0,"price":3690,"photo":"catalog/046.jpg","weight":"1150 г","catalogSection":"Тёплые блюда и выпечка","composition":["10 шт. чизбургеров — 500 г","Наггетсы — 300 г","Картофель фри — 200 г","Сырный соус — 50 г","Кетчуп — 50 г","Соус барбекю — 50 г"],"ingredients":[["чизбургеров — 500 г",10,"шт."],["Наггетсы",300,"г"],["Картофель фри",200,"г"],["Сырный соус",50,"г"],["Кетчуп",50,"г"],["Соус барбекю",50,"г"]],"catalogOrder":46},{"id":"catalog-047","name":"Фуршетный бокс № 42 Блинчики с мясом","category":0,"price":2590,"photo":"catalog/047.jpg","weight":"875 г","catalogSection":"Тёплые блюда и выпечка","composition":["25 пор. блины с мясным фаршем и крем-чизом"],"ingredients":[["блины с мясным фаршем и крем-чизом",25,"пор."]],"catalogOrder":47},{"id":"catalog-048","name":"Фуршетный набор № 30 Куриные шашлычки","category":0,"price":3790,"photo":"catalog/048.jpg","weight":"1120 г","catalogSection":"Тёплые блюда и выпечка","composition":["Куриные шашлычки — 560 г","Картофель бейби — 500 г","Соус — 60 г"],"ingredients":[["Куриные шашлычки",560,"г"],["Картофель бейби",500,"г"],["Соус",60,"г"]],"catalogOrder":48},{"id":"catalog-049","name":"Фуршетный бокс № 31 Свиной шашлычок","category":0,"price":3990,"photo":"catalog/049.jpg","weight":"1120 г","catalogSection":"Тёплые блюда и выпечка","composition":["Свиной шашлык — 560 г","Картофель бейби — 500 г","Соус — 60 г"],"ingredients":[["Свиной шашлык",560,"г"],["Картофель бейби",500,"г"],["Соус",60,"г"]],"catalogOrder":49},{"id":"catalog-050","name":"Фуршетный бокс № 32 Лосось на шпажке","category":0,"price":5690,"photo":"catalog/050.jpg","weight":"1120 г","catalogSection":"Тёплые блюда и выпечка","composition":["Шашлычки из лосося — 560 г","Картофель бейби — 500 г","Соус — 60 г"],"ingredients":[["Шашлычки из лосося",560,"г"],["Картофель бейби",500,"г"],["Соус",60,"г"]],"catalogOrder":50},{"id":"catalog-051","name":"Фуршетный бокс № 22 Фрукты","category":0,"price":3890,"photo":"catalog/051.jpg","weight":"1150 г","catalogSection":"Десерты и фрукты","composition":["16 шт. фрукты в ассортименте в креманке"],"ingredients":[["фрукты в ассортименте в креманке",16,"шт."]],"catalogOrder":51},{"id":"catalog-052","name":"Фуршетный бокс № 23 Слоёные десерты","category":0,"price":5690,"photo":"catalog/052.jpg","weight":"750 г","catalogSection":"Десерты и фрукты","composition":["12 шт. трайфл со сливочным бисквитом «Клубника со сливками»","13 шт. трайфл с шоколадным бисквитом «Сникерс» с орехами и карамелью"],"ingredients":[["трайфл со сливочным бисквитом «Клубника со сливками»",12,"шт."],["трайфл с шоколадным бисквитом «Сникерс» с орехами и карамелью",13,"шт."]],"catalogOrder":52},{"id":"catalog-053","name":"Фуршетный бокс № 35 Десерты","category":0,"price":3500,"photo":"catalog/053.jpg","weight":"750 г","catalogSection":"Десерты и фрукты","composition":["25 десертных порций ассорти","15 шт. одного вида","10 шт. второго вида"],"ingredients":[["25 десертных порций ассорти",1,"поз."],["одного вида",15,"шт."],["второго вида",10,"шт."]],"catalogOrder":53},{"id":"catalog-054","name":"Фуршетный бокс № 41 Сырники","category":0,"price":2690,"photo":"catalog/054.jpg","weight":"750 г","catalogSection":"Десерты и фрукты","composition":["25 шт. творожных сырников"],"ingredients":[["творожных сырников",25,"шт."]],"catalogOrder":54},{"id":"catalog-055","name":"Фуршетный бокс № 36 Десерты","category":0,"price":2990,"photo":"catalog/055.jpg","weight":"750 г","catalogSection":"Десерты и фрукты","composition":["25 десертных порций ассорти","10 шт. первого вида","10 шт. второго вида","5 шт. третьего вида"],"ingredients":[["25 десертных порций ассорти",1,"поз."],["первого вида",10,"шт."],["второго вида",10,"шт."],["третьего вида",5,"шт."]],"catalogOrder":55},{"id":"catalog-056","name":"Фуршетный бокс № 34 Десерты микс","category":0,"price":4690,"photo":"catalog/056.jpg","weight":"750 г","catalogSection":"Десерты и фрукты","composition":["Мафины","Бисквитный рулетик","Шоколадный вафельный торт","Бисквит с орехами и карамелью"],"ingredients":[["Мафины",1,"поз."],["Бисквитный рулетик",1,"поз."],["Шоколадный вафельный торт",1,"поз."],["Бисквит с орехами и карамелью",1,"поз."]],"catalogOrder":56},{"id":"catalog-057","name":"Морс","category":3,"price":450,"photo":"catalog/057.jpg","weight":"1 л","catalogSection":"Напитки","composition":["Морс ягодный"],"ingredients":[["Морс ягодный",1,"поз."]],"catalogOrder":57},{"id":"catalog-058","name":"Компот","category":3,"price":450,"photo":"catalog/058.jpg","weight":"1 л","catalogSection":"Напитки","composition":["Компот фруктовый"],"ingredients":[["Компот фруктовый",1,"поз."]],"catalogOrder":58},{"id":"catalog-059","name":"Готовое меню «Сытный» 15–20 гостей","category":0,"price":19900,"photo":"catalog/059.jpg","weight":"5700 г","catalogSection":"Готовые меню","composition":["Бокс № 5: 25 мясных брускетт","Бокс № 8: 16 пор. Оливье с говядиной","Бокс № 10: 12 бургеров с говядиной и 13 с курицей","Бокс № 11: 25 сэндвичей с ветчиной и сыром","Бокс № 24: 10 пирожков с картошкой, 10 с мясом и кулебяка с капустой"],"ingredients":[["Бокс № 5: 25 мясных брускетт",1,"поз."],["Бокс № 8: 16 пор. Оливье с говядиной",1,"поз."],["Бокс № 10: 12 бургеров с говядиной и 13 с курицей",1,"поз."],["Бокс № 11: 25 сэндвичей с ветчиной и сыром",1,"поз."],["Бокс № 24: 10 пирожков с картошкой, 10 с мясом и кулебяка с капустой",1,"поз."]],"catalogOrder":59},{"id":"catalog-060","name":"Готовое меню «Корпоратив» 50–70 гостей","category":0,"price":71640,"photo":"catalog/060.jpg","weight":"18540 г","catalogSection":"Готовые меню","composition":["Бокс № 1: 50 мясных канапе","Бокс № 2: 50 рыбных канапе","Бокс № 6: 50 сырных закусок в шотах","Бокс № 9: 32 пор. салата Цезарь","Бокс № 8: 32 пор. Мимозы с лососем","Бокс № 8: 32 пор. Оливье с говядиной","Бокс № 11: 25 сэндвичей с ветчиной и сыром","Бокс № 22: 32 пор. фруктов в креманке","Бокс № 23: 50 десертов ассорти"],"ingredients":[["Бокс № 1: 50 мясных канапе",1,"поз."],["Бокс № 2: 50 рыбных канапе",1,"поз."],["Бокс № 6: 50 сырных закусок в шотах",1,"поз."],["Бокс № 9: 32 пор. салата Цезарь",1,"поз."],["Бокс № 8: 32 пор. Мимозы с лососем",1,"поз."],["Бокс № 8: 32 пор. Оливье с говядиной",1,"поз."],["Бокс № 11: 25 сэндвичей с ветчиной и сыром",1,"поз."],["Бокс № 22: 32 пор. фруктов в креманке",1,"поз."],["Бокс № 23: 50 десертов ассорти",1,"поз."]],"catalogOrder":60}]; +window.SUN_LEGACY_CATALOG_V175=[]; ; /* ===== MODULE: pdf-viewer-patch.js ===== */ @@ -2421,14 +2421,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс return true; } - function seedOfficialCatalogForFreshWorkspace() { - clearTenantLocal(); - let seed=[]; - try{if(Array.isArray(window.SUN_OFFICIAL_CATALOG))seed=clone(window.SUN_OFFICIAL_CATALOG)}catch(_){} - localStorage.setItem('sunBoxes',JSON.stringify(Array.isArray(seed)?seed:[])); - localStorage.setItem('sunOrders','[]'); - if(window.SUN_OFFICIAL_CATALOG_VERSION)localStorage.setItem('sunOfficialCatalogVersion',String(window.SUN_OFFICIAL_CATALOG_VERSION)); - } + function seedOfficialCatalogForFreshWorkspace(){clearTenantLocal();localStorage.setItem('sunBoxes','[]');localStorage.setItem('sunOrders','[]');} let signOutInProgress=false; @@ -4680,27 +4673,12 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const preserve=new Set(['sunCloudV2Config','sunCloudClientIdV2','sunStaticMapGeocodeCacheV2']); const keys=[];for(let i=0;ilocalStorage.removeItem(k)); } - function seedWorkspace(mode){ - clearWorkspaceLocalData(); - if(mode==='demo'){ - const demoBoxes=[ - {id:'demo-box-1',name:'Демо · Рыбные канапе',category:0,catalogSection:'Боксы',price:4500,weight:520,guests:10,photo:'catalog/001.jpg',composition:['лосось','креветка','крем-чиз'],ingredients:[['Канапе рыбные',30,'шт.']]}, - {id:'demo-box-2',name:'Демо · Мясные закуски',category:0,catalogSection:'Боксы',price:4200,weight:500,guests:10,photo:'catalog/002.jpg',composition:['ростбиф','пастрами','овощи'],ingredients:[['Канапе мясные',30,'шт.']]}, - {id:'demo-box-3',name:'Демо · Десертный бокс',category:0,catalogSection:'Боксы',price:3900,weight:430,guests:10,photo:'catalog/003.jpg',composition:['мини-десерты','ягоды'],ingredients:[['Мини-десерты',20,'шт.']]} - ]; - const demoOrders=[{id:1,event:'Демо · День рождения',date:new Date(Date.now()+7*86400000).toISOString().slice(0,10),time:'18:00',contact:'Анна',phone:'+7 900 000-00-00',address:'Демо-адрес',status:'Новый',guestsCount:20,prepayment:0,lines:[{id:'demo-box-1',qty:2},{id:'demo-box-2',qty:2}]}]; - localStorage.setItem('sunBoxes',JSON.stringify(demoBoxes));localStorage.setItem('sunOrders',JSON.stringify(demoOrders)); - }else{ - let starter=[];try{if(Array.isArray(window.SUN_OFFICIAL_CATALOG))starter=structuredClone(window.SUN_OFFICIAL_CATALOG)}catch(_){} - localStorage.setItem('sunBoxes',JSON.stringify(starter));localStorage.setItem('sunOrders','[]'); - if(window.SUN_OFFICIAL_CATALOG_VERSION)localStorage.setItem('sunOfficialCatalogVersion',String(window.SUN_OFFICIAL_CATALOG_VERSION)); - } - } + function seedWorkspace(){clearWorkspaceLocalData();localStorage.setItem('sunBoxes','[]');localStorage.setItem('sunOrders','[]');} function enhanceOnboardingGate(){ const gate=$('sunCloudAuthGateV3'),ss=session(),ws=workspace(),st=cloud()?.status?.();if(window.SunDeveloperV22?.isPlatformAdmin?.())return;if(!gate||!ss?.user||ws||st?.membershipsLoading||st?.membershipsLoaded===false||gate.dataset.saasEnhanced==='1')return; gate.dataset.saasEnhanced='1';const card=gate.querySelector('.sun-cloud-auth-card');if(!card)return; - card.innerHTML=`

Создайте компанию

${esc(ss.user.email||'Аккаунт авторизован')} · 14 дней Полного тарифа бесплатно

У этого аккаунта пока нет рабочей базы. Приглашения временно отключены на период локального тестирования.

`; + card.innerHTML=`

Создайте компанию

${esc(ss.user.email||'Аккаунт авторизован')} · 14 дней Полного тарифа бесплатно

У этого аккаунта пока нет рабочей базы. Приглашения временно отключены на период локального тестирования.

`; const err=$('sunSaaSGateErrorV16'); qa('[data-create]',card).forEach(b=>b.onclick=async()=>{ const name=String($('sunSaaSCompanyNameV16')?.value||'').trim();if(!name){err.textContent='Введите название компании.';return;}b.disabled=true;err.textContent='Создаю рабочую базу…'; @@ -5347,7 +5325,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс function companyOptions(rows,selected=''){return `${rows.map(r=>``).join('')}`} async function renderCompanies(body){const rows=await getCompanies();body.innerHTML=`
КомпанияВладелецТарифДоступСотрудникиСозданаДействия
`;const draw=()=>{const q=String($('#sunDevCompanySearch').value||'').trim().toLowerCase();const filtered=rows.filter(r=>`${r.workspace_name||''} ${r.owner_email||''}`.toLowerCase().includes(q));$('#sunDevCompaniesTbody').innerHTML=filtered.map(r=>`${esc(r.workspace_name)}
${esc(r.workspace_id)}
${esc(r.owner_email||r.pending_owner_email||'—')}${r.pending_owner_email?'
ожидает регистрации
':''}${esc(PLANS[r.plan_id]||r.plan_name||r.plan_id||'—')}
до ${fmtDay(r.trial_ends_at||r.current_period_end)}
${esc(r.access_mode||r.status||'—')}${Number(r.member_count||0)} / ${r.max_members==null?'∞':r.max_members}${fmtDay(r.created_at)}
`).join('')||'Компаний не найдено.';bindCompanyRows()};draw();$('#sunDevCompanySearch').oninput=draw;$('#sunDevCreateCompany').onclick=openCreateCompany} function bindCompanyRows(){qa('#sunDevCompaniesTbody tr[data-ws]').forEach(tr=>{const ws=tr.dataset.ws,row=lastCompanies.find(x=>String(x.workspace_id)===ws);tr.querySelector('[data-support]').onclick=()=>enterSupport(row);tr.querySelector('[data-diag]').onclick=()=>showDiagnostics(row);tr.querySelector('[data-plan]').onclick=()=>showCompanyPlan(row);tr.querySelector('[data-block]').onclick=async()=>{const blocked=row.access_mode==='blocked';if(!blocked&&!confirmDanger(`Заблокировать компанию «${row.workspace_name}»? Данные не удалятся.`))return;await rpc('sun_dev_set_subscription',{p_workspace:ws,p_plan:row.plan_id||'full',p_days:blocked?30:0,p_status:blocked?'active':'expired'});await log(blocked?'company.unblock':'company.block',ws,null,{company:row.workspace_name});toast(blocked?'Компания разблокирована.':'Компания заблокирована.','success');activateTab('companies',true)}})} - function openCreateCompany(){const ui=modal('Создать компанию',`

Компания получит текущий стартовый каталог (${Number(window.SUN_OFFICIAL_CATALOG?.length||0)} позиций), а заказы и клиенты будут пустыми.

`);$('#sunDevNewCreate').onclick=async()=>{const name=String($('#sunDevNewCompanyName').value||'').trim(),email=String($('#sunDevNewOwnerEmail').value||'').trim(),plan=$('#sunDevNewPlan').value,days=Number($('#sunDevNewDays').value||30);if(!name)return $('#sunDevNewError').textContent='Введите название.';const btn=$('#sunDevNewCreate');btn.disabled=true;$('#sunDevNewError').textContent='Создаю…';try{const data=await rpc('sun_dev_create_company',{p_name:name,p_owner_email:email||null,p_plan:plan,p_days:days,p_boxes:Array.isArray(window.SUN_OFFICIAL_CATALOG)?window.SUN_OFFICIAL_CATALOG:[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||'')});ui.body.innerHTML=`

Компания создана: ${esc(data.workspace_name||name)}

Каталог: ${Number(data.catalog_count||0)} позиций.

${data.owner_status==='invite'?`

Владелец ещё не зарегистрирован. Код приглашения сохранён на сервере.

`:''}
`;setTimeout(()=>activateTab('companies',true),400)}catch(e){btn.disabled=false;$('#sunDevNewError').textContent=e?.message||String(e)}}} + function openCreateCompany(){const ui=modal('Создать компанию',`

Компания начнёт с пустого каталога, без заказов и клиентов.

`);$('#sunDevNewCreate').onclick=async()=>{const name=String($('#sunDevNewCompanyName').value||'').trim(),email=String($('#sunDevNewOwnerEmail').value||'').trim(),plan=$('#sunDevNewPlan').value,days=Number($('#sunDevNewDays').value||30);if(!name)return $('#sunDevNewError').textContent='Введите название.';const btn=$('#sunDevNewCreate');btn.disabled=true;$('#sunDevNewError').textContent='Создаю…';try{const data=await rpc('sun_dev_create_company',{p_name:name,p_owner_email:email||null,p_plan:plan,p_days:days,p_boxes:[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||'')});ui.body.innerHTML=`

Компания создана: ${esc(data.workspace_name||name)}

Каталог: ${Number(data.catalog_count||0)} позиций.

${data.owner_status==='invite'?`

Владелец ещё не зарегистрирован. Код приглашения сохранён на сервере.

`:''}
`;setTimeout(()=>activateTab('companies',true),400)}catch(e){btn.disabled=false;$('#sunDevNewError').textContent=e?.message||String(e)}}} function showCompanyPlan(row){const ui=modal(`Тариф · ${row.workspace_name}`,`
`);$('#sunDevPlanSave').onclick=async()=>{await rpc('sun_dev_set_subscription',{p_workspace:row.workspace_id,p_plan:$('#sunDevPlanSelect').value,p_days:Number($('#sunDevPlanDays').value),p_status:'active'});await log('company.subscription.set',row.workspace_id,null,{plan:$('#sunDevPlanSelect').value,days:Number($('#sunDevPlanDays').value)});ui.close();toast('Тариф обновлён.','success');activateTab('companies',true)}} async function showDiagnostics(row){const d=await rpc('sun_dev_workspace_diagnostics',{p_workspace:row.workspace_id});modal(`Диагностика · ${row.workspace_name}`,`
Заказы${Number(d.orders||0)}
Клиенты${Number(d.clients||0)}
Каталог${Number(d.catalog_items||0)}
Ошибки 24 ч.${Number(d.errors_24h||0)}

Revision: ${Number(d.revision||0)}

Обновлено: ${fmtDate(d.state_updated_at)}

Последняя копия: ${fmtDate(d.latest_backup)}

Режим доступа: ${esc(d.access_mode||'—')}

`)} diff --git a/public/core/developer-console-v1768.js b/public/core/developer-console-v1768.js index ccb0c74..7959fd9 100644 --- a/public/core/developer-console-v1768.js +++ b/public/core/developer-console-v1768.js @@ -71,7 +71,7 @@ function createCompany(){ const ui=modal('Создать компанию',`

Номер присвоится автоматически и больше не изменится, даже если другие компании будут удалены.

`); - $('sunDevNewCreateV1768').onclick=async()=>{const name=String($('sunDevNewCompanyNameV1768')?.value||'').trim(),email=String($('sunDevNewOwnerEmailV1768')?.value||'').trim(),plan=$('sunDevNewPlanV1768')?.value||'full',days=Math.max(1,Number($('sunDevNewDaysV1768')?.value||30));if(!name){$('sunDevNewErrorV1768').textContent='Введите название.';return}const btn=$('sunDevNewCreateV1768');btn.disabled=true;$('sunDevNewErrorV1768').textContent='Создаю…';try{await rpc('sun_dev_create_company',{p_name:name,p_owner_email:email||null,p_plan:plan,p_days:days,p_boxes:Array.isArray(window.SUN_OFFICIAL_CATALOG)?window.SUN_OFFICIAL_CATALOG:[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||'')});ui.close();toast('Компания создана и получила следующий номер.','success');renderEnhancedTab('companies')}catch(e){btn.disabled=false;$('sunDevNewErrorV1768').textContent=e?.message||String(e)}}; + $('sunDevNewCreateV1768').onclick=async()=>{const name=String($('sunDevNewCompanyNameV1768')?.value||'').trim(),email=String($('sunDevNewOwnerEmailV1768')?.value||'').trim(),plan=$('sunDevNewPlanV1768')?.value||'full',days=Math.max(1,Number($('sunDevNewDaysV1768')?.value||30));if(!name){$('sunDevNewErrorV1768').textContent='Введите название.';return}const btn=$('sunDevNewCreateV1768');btn.disabled=true;$('sunDevNewErrorV1768').textContent='Создаю…';try{await rpc('sun_dev_create_company',{p_name:name,p_owner_email:email||null,p_plan:plan,p_days:days,p_boxes:[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||'')});ui.close();toast('Компания создана и получила следующий номер.','success');renderEnhancedTab('companies')}catch(e){btn.disabled=false;$('sunDevNewErrorV1768').textContent=e?.message||String(e)}}; } async function deleteCompany(row){ if(!row)return;const name=String(row.workspace_name||''); diff --git a/public/core/performance.js b/public/core/performance.js index 4666ce9..ef9f89b 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; const VERSION='17.7.3'; - const RELEASE='20260917-order-import'; + const RELEASE='20260917-empty-catalog'; const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}}; function installAuthBoot(){ diff --git a/public/index.html b/public/index.html index 8530e23..47a42c3 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - - + diff --git a/public/legacy/bootstrap.js b/public/legacy/bootstrap.js index 69888cb..1c662f8 100644 --- a/public/legacy/bootstrap.js +++ b/public/legacy/bootstrap.js @@ -1 +1 @@ -const sunEsc=window.SunSafe.escapeHTML,sunAttr=window.SunSafe.escapeAttr,sunId=window.SunSafe.idToken,sunImg=window.SunSafe.safeImageSrc,sunInt=v=>Number.isFinite(Number(v))?Math.trunc(Number(v)):0;let boxes=JSON.parse(localStorage.sunBoxes||'null')||[{id:'1',name:'Фуршетный бокс №1 — канапе мясные',ingredients:[['Хлеб',25,'шт.'],['Салями модеро',10,'кус.'],['Мортаделла',10,'кус.'],['Шейка коппа',10,'кус.'],['Ветчина пармская',10,'кус.'],['Салями в сыре',10,'кус.'],['Фрилис',.33,'пачки'],['Томаты черри жёлтые',2.5,'шт.'],['Томаты черри красные',2.5,'шт.'],['Огурец консервированный',5,'кус.'],['Маслины',2.5,'шт.'],['Слайсы огурца свежего',5,'шт.'],['Фисташка дроблёная',1,'г']]},{id:'2',name:'Фуршетный бокс №2 — канапе рыбные',ingredients:[['Хлеб',15,'шт.'],['Креветка',10,'шт.'],['Тарталетка',10,'шт.'],['Икра красная',20,'г'],['Крем-чиз',30,'г'],['Лосось кубиком',35,'г']]},{id:'3',name:'Фуршетный бокс №3 — микс тарталеток',ingredients:[['Тарталетка',25,'шт.'],['Креветка',5,'шт.'],['Икра щучья',25,'г'],['Икра красная',25,'г'],['Мортаделла',5,'кус.'],['Груша',5,'кус.'],['Крем-чиз',25,'г']]},{id:'4',name:'Фуршетный бокс №4 — премиум морепродукты',ingredients:[['Тарталетка',10,'шт.'],['Ролл из зелёного блинчика',5,'шт.'],['Ролл из чёрного блинчика',5,'шт.'],['Тунец обожжённый',5,'шт.'],['Креветка',5,'шт.'],['Слайс лосося',10,'шт.'],['Икра красная',25,'г']]},{id:'5',name:'Фуршетный бокс №5 — брускетты мясные',ingredients:[['Хлеб',25,'шт.'],['Салями в сыре',10,'шт.'],['Ростбиф',10,'шт.'],['Шейка коппа',10,'шт.'],['Прошутто',10,'шт.'],['Салями модеро',10,'шт.']]},{id:'6',name:'Фуршетный бокс №6 — сыры в шотах',ingredients:[['Моцарелла',15,'шариков'],['Дорблю',15,'кубиков'],['Камамбер',10,'кус.'],['Пармезан',15,'кус.'],['Чеддер',15,'кус.'],['Груша',5,'кус.'],['Виноград',5,'шт.']]}],orders=JSON.parse(localStorage.sunOrders||'[]'),draft={id:0,lines:[]},edited=null;date.value=new Date().toISOString().slice(0,10);for(let i=0;i<48;i++){let t=String(i>>1).padStart(2,'0')+':'+(i%2?'30':'00');time.add(new Option(t,t));}time.value='12:00';function persist(){localStorage.sunBoxes=JSON.stringify(boxes);localStorage.sunOrders=JSON.stringify(orders)}function dataLayer(){return window.CateriumDataV1773||window.CateriumDataV1772||window.CateriumDataV1771||window.CateriumData||window.CateriumDataV1770||null}function persistOrders(reason){let d=dataLayer();if(d?.orders?.replace){d.orders.replace(orders,{persistLocal:true,reason:reason||'legacy.orders'});return}persist()}function persistCatalog(reason){let d=dataLayer();if(d?.catalog?.replace){d.catalog.replace(boxes,{persistLocal:true,reason:reason||'legacy.catalog'});return}persist()}function show(id,b){document.querySelectorAll('.view').forEach(x=>x.classList.remove('on'));document.getElementById(id).classList.add('on');document.querySelectorAll('nav button').forEach(x=>x.classList.remove('on'));b.classList.add('on');renderOrders();renderStats()}function money(n){return Number(n||0).toLocaleString('ru-RU')+' ₽'}function calcOrderTotal(){return draft.lines.reduce((sum,l)=>{let b=boxes.find(x=>x.id==l.id);return sum+(Number(b?.price||0)*Number(l.qty||0))},0)}function updateOrderSummary(){let total=calcOrderTotal(),paid=Math.max(0,Number(prepayment.value||0)),rest=Math.max(0,total-paid);orderTotal.value=total;balance.value=rest;summaryTotal.textContent=money(total);summaryPrepayment.textContent=money(paid);summaryBalance.textContent=money(rest)}function render(){tiles.innerHTML=''+boxes.map(b=>``).join('');lines.innerHTML=draft.lines.length?draft.lines.map(l=>{let b=boxes.find(x=>x.id==l.id);return `
${sunEsc(b.name)}${money(b.price||0)} × ${l.qty} = ${money(Number(b.price||0)*l.qty)}
`}).join(''):'

Выберите боксы слева.

';updateOrderSummary()}function add(id){let l=draft.lines.find(x=>x.id==id);l?l.qty++:draft.lines.push({id,qty:1});render()}function qty(id,n){if(n<1)draft.lines=draft.lines.filter(x=>x.id!=id);else draft.lines.find(x=>x.id==id).qty=n;render()}function details(on){orderForm.style.display=on?'none':'block';const detailsPanel=document.getElementById('details');detailsPanel.style.display=on?'block':'none';orderTab.classList.toggle('on',!on);detailTab.classList.toggle('on',on)}function saveOrder(){if(!draft.lines.length)return alert('Добавьте хотя бы один бокс.');draft.event=document.getElementById('event').value;draft.date=date.value;draft.time=time.value;draft.contact=contact.value;draft.phone=phone.value;draft.address=address.value;draft.note=note.value;draft.total=calcOrderTotal();draft.prepayment=Math.max(0,Number(prepayment.value||0));draft.balance=Math.max(0,draft.total-draft.prepayment);if(!draft.status)draft.status='Новый';if(!draft.id)draft.id=Math.max(0,...orders.map(x=>x.id))+1;let n=orders.findIndex(x=>x.id==draft.id);n<0?orders.push(structuredClone(draft)):orders[n]=structuredClone(draft);persistOrders('order.save');deleteOrderBtn.style.display='inline-block';renderOrders();updateOrderSummary();alert('Заказ сохранён.');}function orderTotalValue(o){if(Number.isFinite(Number(o.total)))return Number(o.total);return (o.lines||[]).reduce((sum,l)=>{let b=boxes.find(x=>x.id==l.id);return sum+Number(b?.price||0)*Number(l.qty||0)},0)}function renderOrders(){ordersList.innerHTML=orders.length?orders.map(o=>`
№ ${sunInt(o.id)}${sunEsc(o.event)}${sunEsc(o.date)} · ${sunEsc(o.time)}${sunEsc(o.address||'—')}${money(orderTotalValue(o))}
`).join(''):'

Сохранённых заказов пока нет.

'}function status(id,s){orders.find(x=>x.id==id).status=s;persistOrders('order.status')}function openOrder(id){draft=structuredClone(orders.find(x=>x.id==id));document.getElementById('event').value=draft.event;date.value=draft.date;time.value=draft.time;contact.value=draft.contact||'';phone.value=draft.phone||'';address.value=draft.address||'';note.value=draft.note||'';prepayment.value=Number(draft.prepayment||0);deleteOrderBtn.style.display='inline-block';show('new',[...document.querySelectorAll('nav button')].find(x=>x.textContent.trim()==='Новый заказ')||document.querySelector('nav button'));render()}function resetDraft(){draft={id:0,lines:[]};document.getElementById('event').value='';date.value=new Date().toISOString().slice(0,10);time.value='12:00';contact.value='';phone.value='';address.value='';note.value='';prepayment.value=0;deleteOrderBtn.style.display='none';render()}function deleteOrderById(id){let o=orders.find(x=>x.id==id);if(!o||!confirm(`Удалить заказ №${id} «${o.event}»?`))return;orders=orders.filter(x=>x.id!=id);persistOrders('order.delete');renderOrders();if(draft.id==id)resetDraft()}function deleteOrder(){if(!draft.id)return;deleteOrderById(draft.id);show('orders',[...document.querySelectorAll('nav button')].find(x=>x.textContent.trim()==='Заказы')||document.querySelectorAll('nav button')[1])}function renderStats(){statsList.innerHTML=boxes.map(b=>`

${sunEsc(b.name)} — ${orders.filter(o=>o.status!='Отменён').reduce((n,o)=>n+(o.lines.find(l=>l.id==b.id)?.qty||0),0)} заказано

`).join('')}function modal(x,on=true){document.getElementById(x).classList.toggle('on',on)}function closeModal(x){modal(x,false)}function openManager(){modal('manager');managerList.innerHTML=boxes.map(b=>``).join('')}function editBox(id){edited=id?structuredClone(boxes.find(x=>x.id==id)):{id:'',name:'Новый бокс',price:0,ingredients:[]};editTitle.textContent=edited.id?'Изменить бокс':'Новый бокс';boxName.value=edited.name;boxPrice.value=Number(edited.price||0);editPhoto.style.display=edited.photo?'block':'none';editPhoto.src=sunImg(edited.photo)||'';deleteBox.style.display=edited.id?'block':'none';modal('editor');renderIngredients()}function readPhoto(x){let r=new FileReader();r.onload=()=>{edited.photo=sunImg(r.result);editPhoto.src=edited.photo||'';editPhoto.style.display='block'};r.readAsDataURL(x.files[0])}function renderIngredients(){ingredients.innerHTML=edited.ingredients.map((x,i)=>`
`).join('')}function addIngredient(){edited.ingredients.push(['Новый продукт',1,'шт.']);renderIngredients()}function saveBox(){edited.name=boxName.value.trim();edited.price=Math.max(0,Number(boxPrice.value||0));if(!edited.name)return alert('Введите название.');if(!edited.id){edited.id=sunUUID();boxes.push(edited)}else boxes[boxes.findIndex(x=>x.id==edited.id)]=edited;persistCatalog('catalog.save');closeModal('editor');closeModal('manager');render();renderOrders();window.sunClientOfferCatalogChanged?.(edited.id)}function removeBox(){if(confirm('Удалить бокс?')){boxes=boxes.filter(x=>x.id!=edited.id);persistCatalog('catalog.delete');closeModal('editor');closeModal('manager');render()}}function openPreview(){let m={};draft.lines.forEach(l=>boxes.find(b=>b.id==l.id).ingredients.forEach(x=>{let k=x[0]+'|'+x[2];m[k]=(m[k]||0)+x[1]*l.qty}));prepTitle.textContent='Заготовки для: '+document.getElementById('event').value;prep.innerHTML=Object.entries(m).map(([k,v])=>{let [p,u]=k.split('|');return `

${sunEsc(p)}${sunEsc(v)} ${sunEsc(u)}

`}).join('');modal('preview')}render();updateOrderSummary(); \ No newline at end of file +const sunEsc=window.SunSafe.escapeHTML,sunAttr=window.SunSafe.escapeAttr,sunId=window.SunSafe.idToken,sunImg=window.SunSafe.safeImageSrc,sunInt=v=>Number.isFinite(Number(v))?Math.trunc(Number(v)):0;let boxes=JSON.parse(localStorage.sunBoxes||'null')||[],orders=JSON.parse(localStorage.sunOrders||'[]'),draft={id:0,lines:[]},edited=null;date.value=new Date().toISOString().slice(0,10);for(let i=0;i<48;i++){let t=String(i>>1).padStart(2,'0')+':'+(i%2?'30':'00');time.add(new Option(t,t));}time.value='12:00';function persist(){localStorage.sunBoxes=JSON.stringify(boxes);localStorage.sunOrders=JSON.stringify(orders)}function dataLayer(){return window.CateriumDataV1773||window.CateriumDataV1772||window.CateriumDataV1771||window.CateriumData||window.CateriumDataV1770||null}function persistOrders(reason){let d=dataLayer();if(d?.orders?.replace){d.orders.replace(orders,{persistLocal:true,reason:reason||'legacy.orders'});return}persist()}function persistCatalog(reason){let d=dataLayer();if(d?.catalog?.replace){d.catalog.replace(boxes,{persistLocal:true,reason:reason||'legacy.catalog'});return}persist()}function show(id,b){document.querySelectorAll('.view').forEach(x=>x.classList.remove('on'));document.getElementById(id).classList.add('on');document.querySelectorAll('nav button').forEach(x=>x.classList.remove('on'));b.classList.add('on');renderOrders();renderStats()}function money(n){return Number(n||0).toLocaleString('ru-RU')+' ₽'}function calcOrderTotal(){return draft.lines.reduce((sum,l)=>{let b=boxes.find(x=>x.id==l.id);return sum+(Number(b?.price||0)*Number(l.qty||0))},0)}function updateOrderSummary(){let total=calcOrderTotal(),paid=Math.max(0,Number(prepayment.value||0)),rest=Math.max(0,total-paid);orderTotal.value=total;balance.value=rest;summaryTotal.textContent=money(total);summaryPrepayment.textContent=money(paid);summaryBalance.textContent=money(rest)}function render(){tiles.innerHTML=''+boxes.map(b=>``).join('');lines.innerHTML=draft.lines.length?draft.lines.map(l=>{let b=boxes.find(x=>x.id==l.id);return `
${sunEsc(b.name)}${money(b.price||0)} × ${l.qty} = ${money(Number(b.price||0)*l.qty)}
`}).join(''):'

Выберите боксы слева.

';updateOrderSummary()}function add(id){let l=draft.lines.find(x=>x.id==id);l?l.qty++:draft.lines.push({id,qty:1});render()}function qty(id,n){if(n<1)draft.lines=draft.lines.filter(x=>x.id!=id);else draft.lines.find(x=>x.id==id).qty=n;render()}function details(on){orderForm.style.display=on?'none':'block';const detailsPanel=document.getElementById('details');detailsPanel.style.display=on?'block':'none';orderTab.classList.toggle('on',!on);detailTab.classList.toggle('on',on)}function saveOrder(){if(!draft.lines.length)return alert('Добавьте хотя бы один бокс.');draft.event=document.getElementById('event').value;draft.date=date.value;draft.time=time.value;draft.contact=contact.value;draft.phone=phone.value;draft.address=address.value;draft.note=note.value;draft.total=calcOrderTotal();draft.prepayment=Math.max(0,Number(prepayment.value||0));draft.balance=Math.max(0,draft.total-draft.prepayment);if(!draft.status)draft.status='Новый';if(!draft.id)draft.id=Math.max(0,...orders.map(x=>x.id))+1;let n=orders.findIndex(x=>x.id==draft.id);n<0?orders.push(structuredClone(draft)):orders[n]=structuredClone(draft);persistOrders('order.save');deleteOrderBtn.style.display='inline-block';renderOrders();updateOrderSummary();alert('Заказ сохранён.');}function orderTotalValue(o){if(Number.isFinite(Number(o.total)))return Number(o.total);return (o.lines||[]).reduce((sum,l)=>{let b=boxes.find(x=>x.id==l.id);return sum+Number(b?.price||0)*Number(l.qty||0)},0)}function renderOrders(){ordersList.innerHTML=orders.length?orders.map(o=>`
№ ${sunInt(o.id)}${sunEsc(o.event)}${sunEsc(o.date)} · ${sunEsc(o.time)}${sunEsc(o.address||'—')}${money(orderTotalValue(o))}
`).join(''):'

Сохранённых заказов пока нет.

'}function status(id,s){orders.find(x=>x.id==id).status=s;persistOrders('order.status')}function openOrder(id){draft=structuredClone(orders.find(x=>x.id==id));document.getElementById('event').value=draft.event;date.value=draft.date;time.value=draft.time;contact.value=draft.contact||'';phone.value=draft.phone||'';address.value=draft.address||'';note.value=draft.note||'';prepayment.value=Number(draft.prepayment||0);deleteOrderBtn.style.display='inline-block';show('new',[...document.querySelectorAll('nav button')].find(x=>x.textContent.trim()==='Новый заказ')||document.querySelector('nav button'));render()}function resetDraft(){draft={id:0,lines:[]};document.getElementById('event').value='';date.value=new Date().toISOString().slice(0,10);time.value='12:00';contact.value='';phone.value='';address.value='';note.value='';prepayment.value=0;deleteOrderBtn.style.display='none';render()}function deleteOrderById(id){let o=orders.find(x=>x.id==id);if(!o||!confirm(`Удалить заказ №${id} «${o.event}»?`))return;orders=orders.filter(x=>x.id!=id);persistOrders('order.delete');renderOrders();if(draft.id==id)resetDraft()}function deleteOrder(){if(!draft.id)return;deleteOrderById(draft.id);show('orders',[...document.querySelectorAll('nav button')].find(x=>x.textContent.trim()==='Заказы')||document.querySelectorAll('nav button')[1])}function renderStats(){statsList.innerHTML=boxes.map(b=>`

${sunEsc(b.name)} — ${orders.filter(o=>o.status!='Отменён').reduce((n,o)=>n+(o.lines.find(l=>l.id==b.id)?.qty||0),0)} заказано

`).join('')}function modal(x,on=true){document.getElementById(x).classList.toggle('on',on)}function closeModal(x){modal(x,false)}function openManager(){modal('manager');managerList.innerHTML=boxes.map(b=>``).join('')}function editBox(id){edited=id?structuredClone(boxes.find(x=>x.id==id)):{id:'',name:'Новый бокс',price:0,ingredients:[]};editTitle.textContent=edited.id?'Изменить бокс':'Новый бокс';boxName.value=edited.name;boxPrice.value=Number(edited.price||0);editPhoto.style.display=edited.photo?'block':'none';editPhoto.src=sunImg(edited.photo)||'';deleteBox.style.display=edited.id?'block':'none';modal('editor');renderIngredients()}function readPhoto(x){let r=new FileReader();r.onload=()=>{edited.photo=sunImg(r.result);editPhoto.src=edited.photo||'';editPhoto.style.display='block'};r.readAsDataURL(x.files[0])}function renderIngredients(){ingredients.innerHTML=edited.ingredients.map((x,i)=>`
`).join('')}function addIngredient(){edited.ingredients.push(['Новый продукт',1,'шт.']);renderIngredients()}function saveBox(){edited.name=boxName.value.trim();edited.price=Math.max(0,Number(boxPrice.value||0));if(!edited.name)return alert('Введите название.');if(!edited.id){edited.id=sunUUID();boxes.push(edited)}else boxes[boxes.findIndex(x=>x.id==edited.id)]=edited;persistCatalog('catalog.save');closeModal('editor');closeModal('manager');render();renderOrders();window.sunClientOfferCatalogChanged?.(edited.id)}function removeBox(){if(confirm('Удалить бокс?')){boxes=boxes.filter(x=>x.id!=edited.id);persistCatalog('catalog.delete');closeModal('editor');closeModal('manager');render()}}function openPreview(){let m={};draft.lines.forEach(l=>boxes.find(b=>b.id==l.id).ingredients.forEach(x=>{let k=x[0]+'|'+x[2];m[k]=(m[k]||0)+x[1]*l.qty}));prepTitle.textContent='Заготовки для: '+document.getElementById('event').value;prep.innerHTML=Object.entries(m).map(([k,v])=>{let [p,u]=k.split('|');return `

${sunEsc(p)}${sunEsc(v)} ${sunEsc(u)}

`}).join('');modal('preview')}render();updateOrderSummary(); \ No newline at end of file diff --git a/public/service-worker.js b/public/service-worker.js index 5c7f0fd..08305ee 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,5 +1,5 @@ -const CACHE='sun-catering-pwa-v85-20260917-order-import'; -const VERSION='20260917-order-import'; +const CACHE='sun-catering-pwa-v86-20260917-empty-catalog'; +const VERSION='20260917-empty-catalog'; const CORE=[ './','./index.html',`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`, `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, diff --git a/tests/company-branding.spec.mjs b/tests/company-branding.spec.mjs index 548f00a..7a25f0d 100644 --- a/tests/company-branding.spec.mjs +++ b/tests/company-branding.spec.mjs @@ -96,6 +96,7 @@ test('catalog, route sheet and full PDF pages retain the company brand',async({p const data=await logo(page); const result=await page.evaluate(async data=>{ const brand=window.CateriumBranding;brand.saveName('Лист');window.print=()=>{}; + boxes.push({id:'brand-catalog-fixture',name:'Бокс компании Лист',price:1200,category:0,photo:'',ingredients:[],composition:[]}); const texts=[],images=[],proto=CanvasRenderingContext2D.prototype,fill=proto.fillText,draw=proto.drawImage; proto.fillText=function(t,...a){texts.push(String(t));return fill.call(this,t,...a)}; proto.drawImage=function(i,...a){images.push(i.src);return draw.call(this,i,...a)}; diff --git a/tests/order-import.spec.mjs b/tests/order-import.spec.mjs index fe08926..21cd221 100644 --- a/tests/order-import.spec.mjs +++ b/tests/order-import.spec.mjs @@ -1,5 +1,16 @@ import {test,expect} from '@playwright/test'; +test('new companies start empty and workspace catalogs survive reload without shared seeds',async({page})=>{ + await page.goto('/index.html',{waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>window.CateriumDataV1773); + expect(await page.evaluate(()=>({count:boxes.length,starter:window.SUN_OFFICIAL_CATALOG.length,legacy:window.SUN_LEGACY_CATALOG_V175.length}))).toEqual({count:0,starter:0,legacy:0}); + await page.evaluate(()=>{boxes.push({id:'own-menu',name:'Свой бокс',category:0,price:1234,photo:''});persist()}); + await page.reload();await page.waitForFunction(()=>window.CateriumDataV1773); + expect(await page.evaluate(()=>boxes.map(b=>b.id))).toEqual(['own-menu']); + await page.evaluate(()=>localStorage.clear());await page.reload();await page.waitForFunction(()=>window.CateriumDataV1773); + expect(await page.evaluate(()=>boxes.length)).toBe(0); +}); + test('imported history keeps partial payments and delivery is charged once after save',async({page})=>{ page.on('dialog',d=>d.accept()); await page.goto('/index.html',{waitUntil:'domcontentloaded'}); diff --git a/tests/recovery/smoke.mjs b/tests/recovery/smoke.mjs index 0c5d044..4e279a1 100644 --- a/tests/recovery/smoke.mjs +++ b/tests/recovery/smoke.mjs @@ -28,6 +28,7 @@ await actor(ids[1]);const ws=(await one('select public.sun_create_workspace() as assert.equal((await q('select * from public.sun_my_workspaces()')).rows.length,1); await denied('select public.sun_create_workspace()',[],/уже относится/); let row=await one('select * from public.sun_fetch_app_state($1)',[ws]); +assert.deepEqual(row.payload.storage.sunBoxes?.v||[],[]); let payload=row.payload;payload.storage.sunOrders={t:'j',v:[{id:1,client:'Test customer',date:'2099-01-01',time:'12:00',total:5000,status:'Новый'}]};payload.storage.sunBoxes={t:'j',v:[{id:'box-1',name:'Test box',price:500}]}; row=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'recovery-test',row.revision]); await denied('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'stale-client',row.revision-1],/SUN_CONFLICT/); diff --git a/tests/release-check.mjs b/tests/release-check.mjs index b976ce0..64324d3 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent') check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox'); check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4'); -check(sw.includes('v85-20260917-order-import')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); -check(index.includes('20260917-order-import')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); +check(sw.includes('v86-20260917-empty-catalog')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); +check(index.includes('20260917-empty-catalog')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned'); check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned'); check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded'); @@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30 check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match'); check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json'); check(releaseManifest.channel==='production','release manifest channel is production'); -check(String(releaseManifest.pwaCache||'').includes('v85-20260917-order-import'),'release manifest points to current PWA cache'); +check(String(releaseManifest.pwaCache||'').includes('v86-20260917-empty-catalog'),'release manifest points to current PWA cache'); check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3'); check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=12000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards'); @@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer'); check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes'); check(pkg.version==='17.7.3','package version is v17.7.3'); -check(index.includes('20260917-order-import'),'index cache bust is v17.7.3'); -check(sw.includes('v85-20260917-order-import')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); +check(index.includes('20260917-empty-catalog'),'index cache bust is v17.7.3'); +check(sw.includes('v86-20260917-empty-catalog')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist'); check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index 699d357..12d3e31 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x)); if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed'); -if(!sw.includes('20260917-order-import')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); -if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260917-order-import')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); +if(!sw.includes('20260917-empty-catalog')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); +if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260917-empty-catalog')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present'); if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present'); if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');