Lets users download the local database as a JSON file and restore it from a file, independent of cloud sync — needed while Supabase is unreachable to move data between computers or recover after an outage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5729 lines
845 KiB
JavaScript
5729 lines
845 KiB
JavaScript
/* ===== 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}];
|
||
;
|
||
|
||
/* ===== MODULE: pdf-viewer-patch.js ===== */
|
||
(function(){
|
||
'use strict';
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const IMAGE_DATA=window.SUN_CATALOG_IMAGE_DATA||{};
|
||
const LOGO_DATA=window.SUN_LOGO_DATA||'';
|
||
let lastPdfUrl='';
|
||
|
||
function toast(message,type){
|
||
try{ if(window.SunEnterprise?.toast) return window.SunEnterprise.toast(message,type); }catch(_){}
|
||
console[type==='warn'?'warn':'log'](message);
|
||
}
|
||
function currentCategory(){
|
||
const buttons=qa('#new .sun-catalog-top-toolbar .cats button,#new .cats button');
|
||
const active=buttons.find(b=>b.style.outline&&b.style.outline!=='none')||buttons.find(b=>b.getAttribute('aria-current')==='true');
|
||
if(active?.dataset?.category!=null)return Number(active.dataset.category);
|
||
const title=document.querySelector('#new .catalog h1')?.textContent?.trim()||'';
|
||
const names=[['Боксы',0],['Премиум',5],['Посуда',1],['Дополнения',2],['Напитки',3],['Доставка',4]];
|
||
return names.find(([name])=>title.startsWith(name))?.[1]??0;
|
||
}
|
||
const categoryName=cat=>{try{if(typeof window.sunCatalogCategoryName==='function')return window.sunCatalogCategoryName(cat)}catch(_){}return ({0:'Боксы',1:'Посуда',2:'Дополнения',3:'Напитки',4:'Доставка',5:'Премиум'})[cat]||'Каталог'};
|
||
function allBoxes(){
|
||
try{if(Array.isArray(boxes))return boxes}catch(_){}
|
||
try{const saved=JSON.parse(localStorage.getItem('sunBoxes')||'[]');return Array.isArray(saved)?saved:[]}catch(_){return[]}
|
||
}
|
||
function ingredientWeight(row){if(!Array.isArray(row))return 0;const direct=Math.max(0,Number(row[3]||0));if(direct)return direct;const m=String(row[0]||'').match(/[—–-]\s*(\d+(?:[.,]\d+)?)\s*г(?:\s*\/\s*шт\.?)?\s*$/i);return m?Math.max(0,Number(String(m[1]).replace(',','.'))):0}
|
||
function cleanIngredientName(name){return String(name||'').replace(/\s*[—–-]\s*\d+(?:[.,]\d+)?\s*г(?:\s*\/\s*шт\.?)?\s*$/i,'').trim()}
|
||
function composition(item){
|
||
if(Array.isArray(item?.composition)&&item.composition.length)return item.composition.map(x=>String(x||'').trim()).filter(Boolean);
|
||
if(Array.isArray(item?.ingredients)&&item.ingredients.some(x=>Array.isArray(x)&&x[0]))return item.ingredients.filter(x=>Array.isArray(x)&&x[0]).map(x=>{const q=Math.max(0,Number(x[1]||0)),u=String(x[2]||'').trim(),name=cleanIngredientName(x[0]),w=ingredientWeight(x),shown=Number.isInteger(q)?String(q):q.toLocaleString('ru-RU',{maximumFractionDigits:2});if(/^поз\.?$/i.test(u)&&q<=1)return `${name}${w?` — ${w.toLocaleString('ru-RU')} г/шт.`:''}`;return `${shown} ${u} ${name}${w?` — ${w.toLocaleString('ru-RU')} г/шт.`:''}`.replace(/\s+/g,' ').trim()});
|
||
return [];
|
||
}
|
||
const money=value=>`${Number(value||0).toLocaleString('ru-RU')} ₽`;
|
||
const mm=v=>v*5;
|
||
|
||
function roundedRect(ctx,x,y,w,h,r){
|
||
const rr=Math.min(r,w/2,h/2);
|
||
ctx.beginPath();ctx.moveTo(x+rr,y);ctx.arcTo(x+w,y,x+w,y+h,rr);ctx.arcTo(x+w,y+h,x,y+h,rr);ctx.arcTo(x,y+h,x,y,rr);ctx.arcTo(x,y,x+w,y,rr);ctx.closePath();
|
||
}
|
||
function wrapText(ctx,text,maxWidth,maxLines=20){
|
||
const words=String(text||'').replace(/\s+/g,' ').trim().split(' ').filter(Boolean), lines=[];
|
||
let line='';
|
||
for(const word of words){
|
||
const test=line?line+' '+word:word;
|
||
if(ctx.measureText(test).width<=maxWidth||!line) line=test;
|
||
else {lines.push(line);line=word;if(lines.length>=maxLines-1)break;}
|
||
}
|
||
if(line&&lines.length<maxLines)lines.push(line);
|
||
if(words.length&&lines.length===maxLines){
|
||
let last=lines[maxLines-1];
|
||
while(last.length>2&&ctx.measureText(last+'…').width>maxWidth)last=last.slice(0,-1);
|
||
lines[maxLines-1]=last.replace(/[\s,.;:-]+$/,'')+'…';
|
||
}
|
||
return lines;
|
||
}
|
||
function drawTextLines(ctx,lines,x,y,lineHeight){lines.forEach((line,i)=>ctx.fillText(line,x,y+i*lineHeight));}
|
||
|
||
function imageSource(src){
|
||
src=String(src||'').trim();
|
||
if(!src)return LOGO_DATA;
|
||
if(/^data:image\//i.test(src))return src;
|
||
return IMAGE_DATA[src]||IMAGE_DATA[src.replace(/^\.\//,'')]||(/^sun-logo\.png$/i.test(src)?LOGO_DATA:src);
|
||
}
|
||
function loadImage(src){
|
||
return new Promise(resolve=>{
|
||
const raw=imageSource(src),img=new Image();let done=false;
|
||
const finish=value=>{if(done)return;done=true;clearTimeout(timer);resolve(value)};
|
||
try{const url=new URL(raw,document.baseURI||location.href);if(/^https?:$/i.test(url.protocol)&&url.origin!==location.origin)img.crossOrigin='anonymous'}catch(_){}
|
||
img.onload=()=>finish(img);
|
||
img.onerror=()=>finish(null);
|
||
const timer=setTimeout(()=>finish(null),15000);
|
||
img.src=raw;
|
||
});
|
||
}
|
||
function drawContain(ctx,img,x,y,w,h,pad=0){
|
||
if(!img)return;
|
||
const iw=img.naturalWidth||img.width, ih=img.naturalHeight||img.height;
|
||
if(!iw||!ih)return;
|
||
const aw=Math.max(1,w-pad*2),ah=Math.max(1,h-pad*2),scale=Math.min(aw/iw,ah/ih);
|
||
const dw=iw*scale,dh=ih*scale;
|
||
ctx.drawImage(img,x+(w-dw)/2,y+(h-dh)/2,dw,dh);
|
||
}
|
||
|
||
async function renderPageJpeg(item,cat,index,total,dateText){
|
||
const W=1050,H=1485,canvas=document.createElement('canvas');canvas.width=W;canvas.height=H;
|
||
const ctx=canvas.getContext('2d',{alpha:false});
|
||
ctx.fillStyle='#f8f4eb';ctx.fillRect(0,0,W,H);
|
||
ctx.textBaseline='alphabetic';ctx.textAlign='left';
|
||
|
||
const logo=await loadImage('sun-logo.png');
|
||
if(logo)drawContain(ctx,logo,85,50,72,72,1);
|
||
ctx.fillStyle='#1f1f1d';ctx.font='800 36px Arial, sans-serif';ctx.fillText('СОЛНЦЕ КЕЙТЕРИНГ',177,83);
|
||
const subtitle=cat===0?'ПОЛНЫЙ КАТАЛОГ ФУРШЕТНЫХ БОКСОВ':`КАТАЛОГ ${categoryName(cat).toUpperCase()}`;
|
||
ctx.fillStyle='#88827a';ctx.font='600 16px Arial, sans-serif';ctx.fillText(subtitle,178,112);
|
||
ctx.textAlign='right';ctx.font='500 15px Arial, sans-serif';ctx.fillText(`${String(index+1).padStart(2,'0')} / ${String(total).padStart(2,'0')}`,965,69);ctx.textAlign='left';
|
||
ctx.fillStyle='#c99a32';ctx.fillRect(85,143,880,2);
|
||
|
||
const vx=85,vy=178,vw=880,vh=480;
|
||
ctx.fillStyle='#f0ebdf';roundedRect(ctx,vx,vy,vw,vh,20);ctx.fill();
|
||
const photo=await loadImage(item.photo||'sun-logo.png');
|
||
if(photo)drawContain(ctx,photo,vx,vy,vw,vh,item.photo?14:165);
|
||
|
||
const section=String(item.catalogSection||categoryName(cat)).toUpperCase();
|
||
ctx.font='800 17px Arial, sans-serif';
|
||
const sectionW=Math.min(720,Math.max(175,ctx.measureText(section).width+54));
|
||
ctx.fillStyle='#191919';roundedRect(ctx,115,201,sectionW,43,22);ctx.fill();
|
||
ctx.fillStyle='#fff';ctx.fillText(section,142,229);
|
||
|
||
const titleY=717;
|
||
ctx.fillStyle='#1f1f1d';ctx.font='800 36px Arial, sans-serif';
|
||
const nameLines=wrapText(ctx,item.name||'',650,2);drawTextLines(ctx,nameLines,85,titleY,41);
|
||
ctx.textAlign='right';ctx.fillStyle='#c99a32';ctx.font='800 37px Arial, sans-serif';ctx.fillText(money(item.price||0),965,titleY);ctx.textAlign='left';
|
||
|
||
const factsY=820;
|
||
ctx.fillStyle='#77736d';ctx.font='800 14px Arial, sans-serif';ctx.fillText('ВЕС',85,factsY);ctx.fillText('КОЛ-ВО',525,factsY);
|
||
ctx.fillStyle='#232321';ctx.font='800 19px Arial, sans-serif';ctx.fillText(String(item.weight||'—').toUpperCase(),85,factsY+31);ctx.fillText(Number(item.pieces||0)>0?`${Math.round(Number(item.pieces))} ШТ.`:'—',525,factsY+31);
|
||
ctx.fillStyle='#d9d0c2';ctx.fillRect(85,889,880,2);
|
||
|
||
ctx.fillStyle='#1f1f1d';ctx.font='800 23px Arial, sans-serif';ctx.fillText('СОСТАВ',85,939);
|
||
let lines=composition(item),rendered=[];
|
||
ctx.font='400 19px Arial, sans-serif';
|
||
for(const raw of lines){
|
||
const wrapped=wrapText(ctx,raw,795,2);
|
||
for(const line of wrapped){if(rendered.length<9)rendered.push(line)}
|
||
if(rendered.length>=9)break;
|
||
}
|
||
if(!rendered.length){ctx.fillStyle='#77736d';ctx.font='400 18px Arial, sans-serif';ctx.fillText('Состав не указан.',85,985)}
|
||
else{
|
||
let y=985;ctx.font='400 19px Arial, sans-serif';
|
||
for(const line of rendered){
|
||
ctx.fillStyle='#c99a32';ctx.beginPath();ctx.arc(93,y-6,5,0,Math.PI*2);ctx.fill();
|
||
ctx.fillStyle='#33312e';ctx.fillText(line,113,y);y+=32;
|
||
}
|
||
}
|
||
|
||
ctx.fillStyle='#d9d0c2';ctx.fillRect(85,1400,880,2);
|
||
ctx.fillStyle='#88827a';ctx.font='400 14px Arial, sans-serif';ctx.fillText('+7 (921) 954-04-40 • solnce-keytering.ru',85,1435);
|
||
ctx.textAlign='right';ctx.fillText(`Цены актуальны на ${dateText}`,965,1435);ctx.textAlign='left';
|
||
|
||
let dataUrl;
|
||
try{dataUrl=canvas.toDataURL('image/jpeg',0.88)}catch(_){dataUrl=''}
|
||
if(!dataUrl)throw new Error('Не удалось сформировать страницу PDF.');
|
||
const binary=atob(dataUrl.split(',')[1]),bytes=new Uint8Array(binary.length);
|
||
for(let i=0;i<binary.length;i++)bytes[i]=binary.charCodeAt(i);
|
||
return {bytes,width:W,height:H};
|
||
}
|
||
|
||
const buildPdf=pages=>window.SunPdfEngine.fromJpegs(pages);
|
||
|
||
async function createCatalogPdfBlob(cat,progress){
|
||
const items=allBoxes().filter(item=>Number(item.category||0)===Number(cat));
|
||
if(!items.length)throw new Error('В этом разделе нет позиций.');
|
||
const pages=[],dateText=new Date().toLocaleDateString('ru-RU');
|
||
for(let i=0;i<items.length;i++){
|
||
progress?.(i+1,items.length);
|
||
pages.push(await renderPageJpeg(items[i],cat,i,items.length,dateText));
|
||
await new Promise(resolve=>setTimeout(resolve,0));
|
||
}
|
||
return buildPdf(pages);
|
||
}
|
||
|
||
function loadingPage(win,title){
|
||
try{
|
||
win.document.open();
|
||
win.document.write(`<!doctype html><meta charset="utf-8"><title>${title}</title><style>body{margin:0;display:grid;place-items:center;min-height:100vh;background:#f5f2eb;font:16px Arial;color:#15364c}.box{text-align:center;background:#fff;padding:28px 34px;border-radius:14px;box-shadow:0 10px 35px #0002}.sun{font-size:34px;color:#c99a32}.p{margin-top:10px;color:#68717a}</style><div class="box"><div class="sun">☼</div><b>Формируем актуальный PDF-каталог</b><div class="p" id="p">Подготовка…</div></div>`);
|
||
win.document.close();
|
||
}catch(_){}
|
||
}
|
||
function updateProgress(win,current,total){try{const p=win.document.getElementById('p');if(p)p.textContent=`Страница ${current} из ${total}`;}catch(_){} }
|
||
|
||
async function openCatalogPdfViewer(){
|
||
const cat=currentCategory();
|
||
if(cat===4)return;
|
||
const viewer=window.open('about:blank','_blank');
|
||
if(!viewer){toast('Браузер заблокировал новую вкладку. Разрешите всплывающие окна для приложения.','warn');return}
|
||
loadingPage(viewer,'Каталог — Солнце Кейтеринг');
|
||
try{
|
||
const blob=await createCatalogPdfBlob(cat,(n,t)=>updateProgress(viewer,n,t));
|
||
if(lastPdfUrl)URL.revokeObjectURL(lastPdfUrl);
|
||
lastPdfUrl=URL.createObjectURL(blob);
|
||
viewer.location.replace(lastPdfUrl);
|
||
}catch(err){
|
||
try{viewer.document.body.innerHTML=`<div style="font:16px Arial;padding:30px;color:#7a2e2e"><b>Не удалось открыть PDF.</b><p>${String(err?.message||err)}</p></div>`}catch(_){}
|
||
toast(err?.message||'Не удалось сформировать PDF.','warn');
|
||
}
|
||
}
|
||
|
||
function installButton(){
|
||
const head=document.querySelector('#new .catalog .catalog-head');if(!head)return;
|
||
const configure=qa('button',head).find(b=>(b.textContent||'').trim()==='Настроить');if(!configure)return;
|
||
let old=$('sunLiveCatalogPdfButton'),btn;
|
||
if(old){btn=old.cloneNode(true);old.replaceWith(btn)}
|
||
else{btn=document.createElement('button');btn.id='sunLiveCatalogPdfButton';btn.type='button';btn.className='outline';configure.insertAdjacentElement('beforebegin',btn)}
|
||
btn.textContent='PDF';
|
||
btn.title='Открыть актуальный PDF-каталог. В просмотрщике браузера можно скачать или распечатать.';
|
||
btn.onclick=openCatalogPdfViewer;
|
||
const hide=currentCategory()===4;btn.hidden=hide;btn.style.display=hide?'none':'inline-flex';
|
||
}
|
||
|
||
window.sunOpenCatalogPdfViewer=openCatalogPdfViewer;
|
||
window.sunBuildCatalogPdfBlob=()=>createCatalogPdfBlob(currentCategory());
|
||
window.sunCatalogDebugPdfPages=async(cat=currentCategory(),limit=3)=>{const items=allBoxes().filter(item=>Number(item.category||0)===Number(cat)).slice(0,Math.max(1,Number(limit||3)));const dateText=new Date().toLocaleDateString('ru-RU'),pages=[];for(let i=0;i<items.length;i++)pages.push(await renderPageJpeg(items[i],cat,i,items.length,dateText));return pages};
|
||
installButton();setTimeout(installButton,150);setTimeout(installButton,800);
|
||
document.addEventListener('click',e=>{if(e.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(installButton,30)},true);
|
||
const catalog=document.querySelector('#new .catalog');if(catalog&&'MutationObserver'in window)new MutationObserver(()=>{const b=$('sunLiveCatalogPdfButton');if(!b||b.textContent!=='PDF'||b.onclick!==openCatalogPdfViewer)queueMicrotask(installButton)}).observe(catalog,{childList:true,subtree:true});
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: feature-pack-v1.js ===== */
|
||
(function(){
|
||
'use strict';
|
||
if(window.__sunFeaturePackV1)return; window.__sunFeaturePackV1=true;
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const money=v=>`${Math.round(Number(v||0)).toLocaleString('ru-RU')} ₽`;
|
||
const toast=(m,t='success')=>{try{return window.SunEnterprise?.toast?.(m,t)}catch(_){}; if(t==='warn'||t==='error')console.warn(m);};
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-feature-pack-v1-style';
|
||
style.textContent=`
|
||
#new .catalog-head{gap:8px;flex-wrap:wrap}
|
||
#new .sun-catalog-search{height:35px;min-width:180px;width:min(235px,28vw);padding:6px 10px;border:1px solid #d9dde0;border-radius:8px;background:#fff;color:#203040}
|
||
#new .sun-catalog-tabs-button{white-space:nowrap}
|
||
#manager .sun-manager-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px}
|
||
#manager .sun-manager-toolbar .primary{margin:0}
|
||
#manager .sun-manager-search{flex:1 1 220px;min-width:180px}
|
||
.sun-category-list{display:grid;gap:8px;margin-top:12px}
|
||
.sun-category-row{display:grid;grid-template-columns:minmax(180px,1fr) 58px auto auto;gap:8px;align-items:center;padding:9px;border:1px solid #e0e4e8;border-radius:10px;background:#fff}
|
||
.sun-category-row.is-hidden{opacity:.6;background:#f6f7f8}
|
||
.sun-category-row input{width:100%}.sun-category-color{display:grid;place-items:center;margin:0}.sun-category-color input{width:42px;height:34px;padding:2px;border:1px solid #d9dde0;border-radius:8px;background:#fff;cursor:pointer}
|
||
.sun-category-row small{color:#7b858d;white-space:nowrap}
|
||
.sun-category-row-actions{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}
|
||
.sun-category-add{display:grid;grid-template-columns:1fr auto;gap:8px;margin-top:14px;padding-top:12px;border-top:1px solid #e6e8eb}
|
||
.sun-order-requisites{display:none;margin-top:4px}
|
||
.sun-order-requisites-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
|
||
.sun-order-requisites-grid .wide{grid-column:1/-1}
|
||
.sun-order-requisites h3{margin:6px 0 12px;color:#15364c}
|
||
.sun-order-qr-box{display:grid;grid-template-columns:110px minmax(0,1fr);gap:14px;align-items:center;margin-top:16px;padding:12px;border:1px solid #e1e4e7;border-radius:12px;background:#fafbfc}
|
||
.sun-order-qr-preview{width:100px;height:100px;border:1px dashed #c7cdd2;border-radius:8px;background:#fff;display:grid;place-items:center;overflow:hidden;color:#8a949b;font-size:11px;text-align:center;padding:5px}
|
||
.sun-order-qr-preview img{width:100%;height:100%;object-fit:contain}
|
||
.sun-order-qr-actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:8px}
|
||
.sun-discount-panel{display:grid;grid-template-columns:1fr;gap:10px;margin-top:10px;padding:11px;border:1px solid #e3e6e9;border-radius:10px;background:#fafbfc}
|
||
.sun-discount-combo{display:grid;grid-template-columns:minmax(0,1fr) 78px;gap:6px}
|
||
.sun-promo-select-wrap{display:grid;gap:5px}
|
||
.sun-promo-select-wrap select{width:100%}
|
||
.sun-promo-meta{font-size:12px;color:#68717a;line-height:1.35;min-height:16px}
|
||
.sun-discount-panel label>span{display:block;color:#68717a;font-size:12px;margin-bottom:5px}
|
||
.sun-order-summary-discount{color:#8a5b00}
|
||
.sun-order-summary-net{font-weight:800;color:#15364c}
|
||
.sun-order-summary-promo small{color:#7b858d;margin-left:5px}
|
||
.sun-doc-qr{width:92px;height:92px;object-fit:contain;border:1px solid #e0e3e5;border-radius:7px;padding:4px;background:#fff}
|
||
.sun-doc-title-with-qr{display:flex;align-items:flex-start;gap:14px;justify-content:flex-end}
|
||
.sun-doc-title-with-qr .sun-doc-title{text-align:right}
|
||
.sun-doc-qr-wrap{display:flex;align-items:center;gap:9px;max-width:295px}
|
||
.sun-doc-qr-note{min-width:115px;max-width:180px;padding:7px 8px;border:1px dashed #c8cdd2;border-radius:8px;background:#fff;color:#4f5962;font-size:11px;line-height:1.35;text-align:left;white-space:pre-wrap;overflow-wrap:anywhere}
|
||
.sun-doc-qr-note:empty:before{content:attr(data-placeholder);color:#9aa1a8}
|
||
.sun-doc-qr-note:focus{outline:2px solid #d8b766;outline-offset:1px;border-color:transparent}
|
||
@media print{.sun-doc-qr-note{border:0!important;padding:0!important;background:transparent!important;outline:0!important}.sun-doc-qr-note:empty{display:none!important}}
|
||
.sun-receipt .sun-receipt-party{margin:4px 0;padding:6px 8px;border-radius:6px}
|
||
.sun-receipt .sun-receipt-party h3{font-size:11px;margin:0 0 4px}
|
||
.sun-receipt .sun-receipt-party-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:2px 10px;font-size:9.5px}
|
||
.sun-receipt .sun-receipt-party-grid div{padding:1px 0;border-bottom:0;line-height:1.15}
|
||
.sun-receipt .sun-receipt-party-grid span{font-size:7.5px;margin-bottom:1px}
|
||
.sun-receipt .sun-receipt-party-grid b{font-size:9.5px;line-height:1.15}
|
||
#sunReceiptSettingsCard .sun-buyer-settings-hidden{display:none!important}
|
||
.sun-default-qr-setting{display:grid;grid-template-columns:100px minmax(0,1fr);gap:14px;align-items:center}
|
||
.sun-default-qr-setting .preview{width:92px;height:92px;border:1px dashed #cbd1d6;border-radius:8px;background:#fff;display:grid;place-items:center;overflow:hidden;color:#8a949b;font-size:11px;text-align:center;padding:4px}
|
||
.sun-default-qr-setting .preview img{width:100%;height:100%;object-fit:contain}
|
||
.sun-default-qr-setting .actions{justify-content:flex-start;margin-top:8px}
|
||
.sun-promo-stats{margin-top:18px;border:1px solid #e0e4e8;border-radius:12px;background:#fff;padding:14px}
|
||
.sun-promo-stats h2{margin:0 0 4px;color:#15364c}
|
||
.sun-promo-stats table{width:100%;border-collapse:collapse;margin-top:12px}
|
||
.sun-promo-stats th,.sun-promo-stats td{padding:8px;border-bottom:1px solid #e8eaec;text-align:left}
|
||
.sun-promo-stats th{font-size:12px;color:#68717a;background:#fafbfc}.sun-promo-stats td.num,.sun-promo-stats th.num{text-align:right}
|
||
.sun-box-weight-field,.sun-box-pieces-field{margin-top:8px}.sun-box-weight-field input,.sun-box-pieces-field input{max-width:220px}.sun-box-pieces-field small{display:block;margin-top:3px;color:#7b858d;line-height:1.3}.sun-ingredient-head,.ingredient.sun-ingredient-v175{display:grid;grid-template-columns:minmax(220px,1fr) 88px 78px 112px 34px;gap:6px;align-items:center}.sun-ingredient-head{margin:8px 0 4px;color:#68717a;font-size:11px;font-weight:700}.sun-ingredient-head span:nth-child(n+2){text-align:center}.ingredient.sun-ingredient-v175 input{text-align:left;min-width:0}.ingredient.sun-ingredient-v175 input[type=number]{text-align:center}.sun-order-line-name small{display:block;margin-top:3px;color:#7b858d;font-size:10px;font-weight:600;line-height:1.25}.sun-order-line-name small b{color:#53616d}.sun-doc .doc-sub.sun-doc-weight{margin-top:2px;color:#66727c;font-weight:600}
|
||
@media(max-width:850px){.sun-receipt .sun-receipt-party-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||
@media(max-width:650px){#new .sun-catalog-search{width:100%;flex:1 1 100%}.sun-order-requisites-grid{grid-template-columns:1fr}.sun-order-qr-box,.sun-default-qr-setting{grid-template-columns:1fr}.sun-category-row{grid-template-columns:1fr}.sun-category-row-actions{justify-content:flex-start}.sun-ingredient-head{display:none}.ingredient.sun-ingredient-v175{grid-template-columns:minmax(0,1fr) 64px 58px 84px 32px}.ingredient.sun-ingredient-v175 input{padding:7px 5px;font-size:12px}}
|
||
@media print{.sun-doc-title-with-qr{gap:8mm}.sun-doc-qr{width:25mm;height:25mm}.sun-receipt .sun-receipt-party{padding:1.5mm 2mm;margin:1mm 0}.sun-receipt .sun-receipt-party-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:.5mm 2mm}}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
// ---------------- Catalog categories + search ----------------
|
||
const CAT_KEY='sunCatalogCategoriesV2';
|
||
const defaultCats=[
|
||
{id:0,name:'Боксы',accent:'#d49a2a',prep:true,hidden:false,order:0},
|
||
{id:5,name:'Премиум',accent:'#7a5aa6',prep:true,hidden:false,order:1},
|
||
{id:3,name:'Напитки',accent:'#4f9bd8',prep:false,hidden:false,order:2},
|
||
{id:2,name:'Дополнения',accent:'#56a889',prep:false,hidden:false,order:3},
|
||
{id:1,name:'Посуда',accent:'#a78bfa',prep:false,hidden:false,order:4},
|
||
{id:4,name:'Доставка',accent:'#cf9f83',prep:false,hidden:false,order:5}
|
||
];
|
||
function readCats(){
|
||
let saved=[];try{saved=JSON.parse(localStorage.getItem(CAT_KEY)||'[]');if(!Array.isArray(saved))saved=[]}catch(_){saved=[]}
|
||
const map=new Map(saved.filter(x=>x&&Number.isFinite(Number(x.id))).map(x=>[Number(x.id),x]));
|
||
defaultCats.forEach(d=>{if(!map.has(d.id))map.set(d.id,d);else map.set(d.id,{...d,...map.get(d.id),id:d.id})});
|
||
try{(Array.isArray(boxes)?boxes:[]).forEach(item=>{const id=Number(item?.category||0);if(Number.isFinite(id)&&!map.has(id))map.set(id,{id,name:`Вкладка ${id}`,accent:'#708090',prep:true,hidden:false,order:map.size})})}catch(_){}
|
||
const arr=[...map.values()].map((c,i)=>({id:Number(c.id),name:String(c.name||`Вкладка ${i+1}`).trim()||`Вкладка ${i+1}`,accent:c.accent||'#708090',prep:c.prep!==false,hidden:Boolean(c.hidden),order:Number.isFinite(Number(c.order))?Number(c.order):i}));
|
||
arr.sort((a,b)=>a.order-b.order||a.id-b.id);arr.forEach((c,i)=>c.order=i);return arr;
|
||
}
|
||
let catalogCats=readCats();
|
||
const CAT_LAYOUT_VERSION_KEY='sunCatalogCategoryLayoutVersion';
|
||
const CAT_LAYOUT_VERSION='2026-09-02-premium-between-boxes-and-drinks';
|
||
if(localStorage.getItem(CAT_LAYOUT_VERSION_KEY)!==CAT_LAYOUT_VERSION){const desired=[0,5,3,2,1,4];const rank=new Map(desired.map((id,i)=>[id,i]));catalogCats.sort((a,b)=>(rank.has(a.id)?rank.get(a.id):100+a.order)-(rank.has(b.id)?rank.get(b.id):100+b.order));catalogCats.forEach((c,i)=>c.order=i);try{localStorage.setItem(CAT_KEY,JSON.stringify(catalogCats));localStorage.setItem(CAT_LAYOUT_VERSION_KEY,CAT_LAYOUT_VERSION)}catch(_){}}
|
||
function saveCats(){catalogCats.sort((a,b)=>a.order-b.order||a.id-b.id).forEach((c,i)=>c.order=i);try{localStorage.setItem(CAT_KEY,JSON.stringify(catalogCats))}catch(_){}window.dispatchEvent(new CustomEvent('sun:catalog-categories-changed'));}
|
||
function tabTextColor(value){const hex=String(value||'').trim();const m=/^#([0-9a-f]{6})$/i.exec(hex);if(!m)return '#fff';const n=parseInt(m[1],16),r=(n>>16)&255,g=(n>>8)&255,b=n&255;return (r*299+g*587+b*114)>160000?'#15364c':'#fff'}
|
||
const catById=id=>catalogCats.find(c=>Number(c.id)===Number(id));
|
||
const catName=id=>catById(id)?.name||'Каталог';
|
||
const visibleCats=()=>catalogCats.filter(c=>!c.hidden).sort((a,b)=>a.order-b.order);
|
||
let activeCat=visibleCats()[0]?.id??0;
|
||
let catalogQuery='';
|
||
let managerQuery='';
|
||
window.sunCatalogCategoryName=catName;
|
||
window.sunCatalogCategories=()=>catalogCats.map(c=>({...c}));
|
||
window.sunCurrentCatalogCategory=()=>activeCat;
|
||
|
||
function ensureCategoryModal(){
|
||
if($('sunCategoryManager'))return;
|
||
const modal=document.createElement('div');modal.id='sunCategoryManager';modal.className='modal';
|
||
modal.innerHTML=`<div class="dialog"><div class="dialog-head"><div><h2>Вкладки каталога</h2><p class="hint" style="margin:4px 0 0">Скрытая вкладка и её товары не удаляются — её можно вернуть.</p></div><button type="button" onclick="closeModal('sunCategoryManager')">×</button></div><div id="sunCategoryRows" class="sun-category-list"></div><div class="sun-category-add"><input id="sunNewCategoryName" placeholder="Название новой вкладки"><button id="sunAddCategoryBtn" class="primary" type="button">Добавить вкладку</button></div></div>`;
|
||
document.body.appendChild(modal);
|
||
$('sunAddCategoryBtn').onclick=()=>{
|
||
const name=$('sunNewCategoryName').value.trim();if(!name)return;
|
||
const nextId=Math.max(4,...catalogCats.map(c=>Number(c.id)||0))+1;
|
||
catalogCats.push({id:nextId,name,accent:'#708090',prep:true,hidden:false,order:catalogCats.length});saveCats();$('sunNewCategoryName').value='';renderCategoryManager();renderCategoryTabs();syncEditorCategoryOptions();
|
||
};
|
||
}
|
||
function renderCategoryManager(){
|
||
ensureCategoryModal();const root=$('sunCategoryRows');if(!root)return;
|
||
root.innerHTML=catalogCats.slice().sort((a,b)=>a.order-b.order).map(c=>{
|
||
const count=(typeof boxes!=='undefined'?boxes:[]).filter(b=>Number(b.category||0)===Number(c.id)).length;
|
||
return `<div class="sun-category-row ${c.hidden?'is-hidden':''}" data-cat-row="${c.id}"><input value="${esc(c.name)}" data-cat-name="${c.id}" aria-label="Название вкладки"><label class="sun-category-color" title="Цвет вкладки"><input type="color" value="${esc(c.accent||'#708090')}" data-cat-color="${c.id}" aria-label="Цвет вкладки"></label><small>${count} поз.</small><div class="sun-category-row-actions"><button class="outline" type="button" data-cat-up="${c.id}" title="Выше">↑</button><button class="outline" type="button" data-cat-down="${c.id}" title="Ниже">↓</button><button class="outline" type="button" data-cat-toggle="${c.id}">${c.hidden?'Восстановить':'Скрыть'}</button></div></div>`;
|
||
}).join('');
|
||
qa('[data-cat-name]',root).forEach(input=>input.onchange=()=>{const c=catById(input.dataset.catName);if(!c)return;c.name=input.value.trim()||c.name;saveCats();renderCategoryTabs();syncEditorCategoryOptions();renderCatalogV5();renderCategoryManager();});
|
||
qa('[data-cat-color]',root).forEach(input=>{input.oninput=()=>{const c=catById(input.dataset.catColor);if(!c)return;c.accent=input.value||c.accent;renderCategoryTabs();};input.onchange=()=>{const c=catById(input.dataset.catColor);if(!c)return;c.accent=input.value||c.accent;saveCats();renderCategoryTabs();};});
|
||
qa('[data-cat-toggle]',root).forEach(btn=>btn.onclick=()=>{const c=catById(btn.dataset.catToggle);if(!c)return;c.hidden=!c.hidden;if(c.hidden&&Number(activeCat)===Number(c.id))activeCat=visibleCats().find(x=>Number(x.id)!==Number(c.id))?.id??0;saveCats();renderCategoryTabs();syncEditorCategoryOptions();renderCatalogV5();renderCategoryManager();});
|
||
const move=(id,dir)=>{const arr=catalogCats.slice().sort((a,b)=>a.order-b.order),i=arr.findIndex(c=>Number(c.id)===Number(id)),j=i+dir;if(i<0||j<0||j>=arr.length)return;[arr[i],arr[j]]=[arr[j],arr[i]];arr.forEach((c,k)=>c.order=k);catalogCats=arr;saveCats();renderCategoryTabs();renderCategoryManager();};
|
||
qa('[data-cat-up]',root).forEach(btn=>btn.onclick=()=>move(btn.dataset.catUp,-1));qa('[data-cat-down]',root).forEach(btn=>btn.onclick=()=>move(btn.dataset.catDown,1));
|
||
}
|
||
window.sunOpenCategoryManager=()=>{renderCategoryManager();window.modal?.('sunCategoryManager');};
|
||
|
||
function renderCategoryTabs(){
|
||
const panel=document.querySelector('#new .cats');if(!panel)return;
|
||
const visible=visibleCats();if(!visible.some(c=>Number(c.id)===Number(activeCat)))activeCat=visible[0]?.id??0;
|
||
panel.innerHTML=visible.map(c=>`<button type="button" data-category="${c.id}" aria-current="${Number(c.id)===Number(activeCat)?'true':'false'}">${esc(c.name)}</button>`).join('')+'<p>Выберите раздел. Любую позицию можно добавить, изменить или удалить.</p>';
|
||
qa('button',panel).forEach(btn=>{const c=catById(btn.dataset.category),on=Number(btn.dataset.category)===Number(activeCat);btn.style.background=c?.accent||'#708090';btn.style.color=tabTextColor(c?.accent||'#708090');btn.style.opacity=on?'1':'.72';btn.style.outline=on?'2px solid #15364c':'none';btn.style.outlineOffset=on?'2px':'0';btn.onclick=()=>{activeCat=Number(btn.dataset.category);catalogQuery='';const search=$('sunCatalogSearch');if(search)search.value='';renderCategoryTabs();renderCatalogV5();};});
|
||
}
|
||
function syncEditorCategoryOptions(){
|
||
const select=$('itemCategory');if(!select)return;const current=select.value;
|
||
select.innerHTML=catalogCats.slice().sort((a,b)=>a.order-b.order).map(c=>`<option value="${c.id}">${esc(c.name)}${c.hidden?' (скрыта)':''}</option>`).join('');
|
||
if([...select.options].some(o=>o.value===current))select.value=current;
|
||
}
|
||
function ensureCatalogTools(){
|
||
const head=document.querySelector('#new .catalog .catalog-head');if(!head)return;
|
||
const configure=qa('button',head).find(b=>(b.textContent||'').trim()==='Настроить');if(!configure)return;
|
||
let search=$('sunCatalogSearch');if(!search){search=document.createElement('input');search.id='sunCatalogSearch';search.className='sun-catalog-search';search.type='search';search.placeholder='Поиск по № бокса';search.inputMode='numeric';search.oninput=()=>{catalogQuery=search.value;renderCatalogV5();};configure.insertAdjacentElement('beforebegin',search)}
|
||
let tabs=$('sunCatalogTabsButton');if(!tabs){tabs=document.createElement('button');tabs.id='sunCatalogTabsButton';tabs.className='outline sun-catalog-tabs-button';tabs.type='button';tabs.textContent='Вкладки';tabs.onclick=window.sunOpenCategoryManager;configure.insertAdjacentElement('beforebegin',tabs)}
|
||
}
|
||
function lineUnitPrice(line){const direct=Number(line?.price);if(Number.isFinite(direct)&&direct>=0)return direct;const item=boxes.find(x=>String(x.id)===String(line?.id));return Math.max(0,Number(item?.price||0));}
|
||
function baseTotal(order){return (order?.lines||[]).reduce((s,l)=>s+lineUnitPrice(l)*Math.max(0,Number(l.qty||0)),0)}
|
||
window.sunBaseOrderTotal=baseTotal;
|
||
function parseWeightGrams(value){const raw=String(value??'').trim().toLowerCase().replace(',','.');if(!raw)return 0;const m=raw.match(/(\d+(?:\.\d+)?)/);if(!m)return 0;let n=Math.max(0,Number(m[1]||0));if(/кг/.test(raw))n*=1000;return Math.round(n)}
|
||
const pieceUnitRe=/^(?:шт\.?|пор\.?|кус\.?|шарик(?:а|ов|и)?|креманк(?:а|и|е)?)$/i;
|
||
function ingredientUnitWeight(row){if(!Array.isArray(row))return 0;const direct=Math.max(0,Number(row[3]||0));if(direct)return direct;const m=String(row[0]||'').match(/[—–-]\s*(\d+(?:[.,]\d+)?)\s*г(?:\s*\/\s*шт\.?)?\s*$/i);return m?Math.max(0,Number(String(m[1]).replace(',','.'))):0}
|
||
function cleanIngredientName(name){return String(name||'').replace(/\s*[—–-]\s*\d+(?:[.,]\d+)?\s*г(?:\s*\/\s*шт\.?)?\s*$/i,'').trim()}
|
||
function inferBoxPieces(item){const explicit=Math.max(0,Number(item?.pieces||0));if(explicit)return Math.round(explicit);let sum=0;for(const row of item?.ingredients||[]){if(!Array.isArray(row))continue;const unit=String(row[2]||'').trim().toLowerCase();if(pieceUnitRe.test(unit))sum+=Math.max(0,Number(row[1]||0));}return Math.round(sum)}
|
||
function ingredientDisplayLine(row){if(!Array.isArray(row)||!String(row[0]||'').trim())return'';const name=cleanIngredientName(row[0]),qty=Math.max(0,Number(row[1]||0)),unit=String(row[2]||'').trim(),w=ingredientUnitWeight(row);if(/^поз\.?$/i.test(unit)&&qty<=1)return `${name}${w?` — ${w.toLocaleString('ru-RU')} г/шт.`:''}`;const q=Number.isInteger(qty)?String(qty):qty.toLocaleString('ru-RU',{maximumFractionDigits:2});return `${q} ${unit} ${name}${w?` — ${w.toLocaleString('ru-RU')} г/шт.`:''}`.replace(/\s+/g,' ').trim()}
|
||
function normalizeBoxItem(item,{cleanup=false}={}){if(!item||![0,5].includes(Number(item.category||0)))return item;const legacyComposition=Array.isArray(item.composition)?item.composition.slice():[];item.ingredients=Array.isArray(item.ingredients)?item.ingredients.filter(r=>Array.isArray(r)&&String(r[0]||'').trim()).map((r,index)=>{const row=[String(r[0]||'').trim(),Math.max(0,Number(r[1]||0)),String(r[2]||'').trim()||'шт.',Math.max(0,Number(r[3]||0))];if(!row[3])row[3]=ingredientUnitWeight(r)||ingredientUnitWeight([legacyComposition[index]||'',0,'',0]);return row;}):[];
|
||
// Box №18 had a total row "25 шт. тостов ассорти" mixed into two real varieties. The total belongs in pieces, not in composition.
|
||
if(String(item.id)==='catalog-013'||/№\s*18\b.*тосты\s+ассорти/i.test(String(item.name||''))){const idx=item.ingredients.findIndex(r=>/тостов?\s+ассорти/i.test(String(r[0]||''))&&pieceUnitRe.test(String(r[2]||''))&&Number(r[1]||0)>1);if(idx>=0){item.pieces=Math.max(Number(item.pieces||0),Number(item.ingredients[idx][1]||0));item.ingredients.splice(idx,1);}}
|
||
if(!Number(item.pieces||0))item.pieces=inferBoxPieces(item);
|
||
if((!Array.isArray(item.composition)||!item.composition.filter(Boolean).length)&&item.ingredients.length)item.composition=item.ingredients.map(ingredientDisplayLine).filter(Boolean);
|
||
return item;}
|
||
function weightText(item,prefix=true){const raw=String(item?.weight||'').trim();if(!raw)return'';return prefix?`Вес бокса: ${raw}`:raw}
|
||
function piecesText(item,prefix=true){const n=inferBoxPieces(item);if(!n)return'';return prefix?`Количество: ${n.toLocaleString('ru-RU')} шт.`:`${n.toLocaleString('ru-RU')} шт.`}
|
||
function ensureBoxWeightEditor(){
|
||
const price=$('boxPrice'),dialog=document.querySelector('#editor .dialog');if(!price||!dialog)return null;
|
||
let label=$('sunBoxWeightLabel');
|
||
if(!label){label=document.createElement('label');label.id='sunBoxWeightLabel';label.className='sun-box-weight-field';label.innerHTML='<span>Вес бокса, г</span><input id="boxWeightGrams" type="number" min="0" step="1" inputmode="numeric" placeholder="Например, 600">';price.closest('label')?.insertAdjacentElement('afterend',label)}
|
||
return label;
|
||
}
|
||
function ensureBoxPiecesEditor(){const weight=ensureBoxWeightEditor(),dialog=document.querySelector('#editor .dialog');if(!dialog)return null;let label=$('sunBoxPiecesLabel');if(!label){label=document.createElement('label');label.id='sunBoxPiecesLabel';label.className='sun-box-pieces-field';label.innerHTML='<span>Количество канапе / шт. в боксе</span><input id="boxPiecesCount" type="number" min="0" step="1" inputmode="numeric" placeholder="Например, 30"><small>Это общее количество готовых единиц в одном боксе. Не добавляйте его отдельной строкой в «Состав / ТТК».</small>';weight?.insertAdjacentElement('afterend',label)}return label}
|
||
function syncBoxWeightEditor(category,item){const label=ensureBoxWeightEditor(),input=$('boxWeightGrams'),piecesLabel=ensureBoxPiecesEditor(),piecesInput=$('boxPiecesCount');if(!label||!input)return;const isBox=[0,5].includes(Number(category));label.style.display=isBox?'flex':'none';if(piecesLabel)piecesLabel.style.display=isBox?'flex':'none';if(isBox){input.value=parseWeightGrams(item?.weight)||'';if(piecesInput)piecesInput.value=inferBoxPieces(item)||''}}
|
||
function installIngredientEditorV175(){window.renderIngredients=()=>{if(!edited)return;normalizeBoxItem(edited);const root=$('ingredients');if(!root)return;const head='<div class="sun-ingredient-head"><span>Вид / название</span><span>Кол-во</span><span>Ед.</span><span>Вес 1 шт., г</span><span></span></div>';root.innerHTML=head+edited.ingredients.map((row,i)=>{const w=ingredientUnitWeight(row);return `<div class="ingredient sun-ingredient-v175"><input value="${esc(cleanIngredientName(row[0]))}" onchange="edited.ingredients[${i}][0]=this.value"><input type="number" min="0" step="0.01" value="${Number(row[1]||0)}" onchange="edited.ingredients[${i}][1]=+this.value"><input value="${esc(row[2]||'шт.')}" onchange="edited.ingredients[${i}][2]=this.value"><input type="number" min="0" step="0.1" inputmode="decimal" placeholder="г" value="${w||''}" onchange="edited.ingredients[${i}][3]=Math.max(0,+this.value||0)"><button type="button" onclick="edited.ingredients.splice(${i},1);renderIngredients()">×</button></div>`}).join('')};window.addIngredient=()=>{if(!edited)return;if(!Array.isArray(edited.ingredients))edited.ingredients=[];edited.ingredients.push(['Новая позиция',1,'шт.',0]);window.renderIngredients();};}
|
||
installIngredientEditorV175();
|
||
function renderOrderLines(){
|
||
if(!$('lines'))return;
|
||
if(!(draft.lines||[]).length){$('lines').innerHTML='<p class="empty">Выберите позиции из каталога слева.</p>';return}
|
||
const rows=draft.lines.map((line,index)=>{const item=boxes.find(x=>String(x.id)===String(line.id));if(!item)return'';const qty=Math.max(1,Number(line.qty||1)),price=lineUnitPrice(line),isBox=[0,5].includes(Number(item.category||0));return `<div class="sun-order-line-row" data-line-index="${index}"><div class="sun-order-line-name" title="${esc(item.name)}">${esc(String(item.name||'').replace(/^(?:Фуршетный\s+)?бокс\s*№\s*(\d+)\s*(?:[—–-]\s*)?/i,'№ $1 '))}${isBox&&(item.weight||inferBoxPieces(item))?`<small>${item.weight?`<b>Вес:</b> ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`<b>Количество:</b> ${inferBoxPieces(item)} шт.`:''}</small>`:''}</div><div class="sun-order-line-qty"><button type="button" onclick="qty('${esc(line.id)}',${qty-1})">−</button><input type="number" min="1" value="${qty}" onchange="qty('${esc(line.id)}',+this.value)"><button type="button" onclick="qty('${esc(line.id)}',${qty+1})">+</button></div><div class="sun-order-line-price"><input type="text" inputmode="numeric" pattern="[0-9]*" autocomplete="off" value="${Math.round(price)}" onchange="sunOrderLinePriceChanged(${index},this)"></div><div class="sun-order-line-sum"><b>${money(price*qty)}</b></div></div>`}).join('');
|
||
$('lines').innerHTML=`<div class="sun-order-lines-table"><div class="sun-order-line-head"><span>Наименование</span><span>Кол.</span><span>Цена</span><span>Стоимость</span></div><div class="sun-order-line-body">${rows}</div><div class="sun-order-line-total"><span>Стоимость позиций</span><b>${money(baseTotal(draft))}</b></div></div>`;
|
||
}
|
||
window.sunOrderLinePriceChanged=(index,input)=>{const line=draft.lines?.[index];if(!line)return;line.price=Math.max(0,Math.round(Number(input.value||0)));const row=input.closest('.sun-order-line-row'),sum=row?.querySelector('.sun-order-line-sum b');if(sum)sum.textContent=money(line.price*Math.max(0,Number(line.qty||0)));const total=$('lines')?.querySelector('.sun-order-line-total b');if(total)total.textContent=money(baseTotal(draft));updateOrderSummary();};
|
||
function boxNumber(item){const explicit=Number(item?.boxNumber);if(Number.isInteger(explicit)&&explicit>=0)return explicit;const name=String(item?.name||'');const match=name.match(/(?:бокс\s*)?№\s*0*(\d+)/i)||name.match(/^\s*№\s*0*(\d+)/i);return match?Number(match[1]):null}
|
||
function wantedBoxNumber(query){const raw=String(query||'').trim();if(!raw)return null;const match=raw.match(/^(?:бокс\s*)?(?:№\s*)?0*(\d+)\s*$/i);return match?Number(match[1]):null}
|
||
function itemMatches(item,query){if(!query)return true;if(Number(activeCat)===0){const wanted=wantedBoxNumber(query);return wanted!==null&&boxNumber(item)===wanted}const q=query.trim().toLowerCase();const parts=[item.name,item.catalogSection,item.weight,...(Array.isArray(item.composition)?item.composition:[]),...(Array.isArray(item.ingredients)?item.ingredients.map(x=>x?.[0]):[])];return parts.join(' ').toLowerCase().includes(q)}
|
||
function renderCatalogV5(){
|
||
renderCategoryTabs();ensureCatalogTools();const catalogSearch=$('sunCatalogSearch');if(catalogSearch){const boxMode=[0,5].includes(Number(activeCat));catalogSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';catalogSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#new .catalog h1');if(title)title.textContent=catName(activeCat);
|
||
const all=boxes.filter(item=>Number(item.category||0)===Number(activeCat)),items=all.filter(item=>itemMatches(item,catalogQuery));const cat=catById(activeCat)||{name:'Каталог',prep:true};
|
||
const cards=items.map(item=>`<button class="tile" type="button" onclick="add('${esc(item.id)}')" title="Добавить в заказ">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='sun-logo.png'">`:'<div class="ph"><img src="sun-logo.png" class="sun-ph-logo" alt="Логотип Солнце Кейтеринг"></div>'}<span>${esc(item.name)}</span>${item.catalogSection?`<small class="tile-section">${esc(item.catalogSection)}</small>`:''}${(item.weight||inferBoxPieces(item))?`<small class="tile-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}<span class="tile-price-wrap"><small class="tile-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${Number(item.oldPrice||0)>Number(item.price||0)?`<span class="sale-badge">АКЦИЯ</span>`:''}</button>`).join('');
|
||
const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');
|
||
const empty=items.length?'':`<div class="catalog-empty">${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}</div>`;
|
||
$('tiles').innerHTML=`<button class="tile add" type="button" onclick="editBox(null)">+<br>${addText}</button>${cards}${empty}`;
|
||
renderOrderLines();updateOrderSummary();
|
||
}
|
||
window.render=renderCatalogV5;
|
||
window.add=id=>{const item=boxes.find(x=>String(x.id)===String(id));if(!item)return;let line=(draft.lines||[]).find(x=>String(x.id)===String(id));if(line)line.qty=Math.max(0,Number(line.qty||0))+1;else{if(!Array.isArray(draft.lines))draft.lines=[];draft.lines.push({id:item.id,qty:1,price:Math.max(0,Number(item.price||0))})}renderCatalogV5();};
|
||
window.qty=(id,n)=>{if(!Array.isArray(draft.lines))draft.lines=[];const next=Math.max(0,Number(n||0));if(next<1)draft.lines=draft.lines.filter(x=>String(x.id)!==String(id));else{const line=draft.lines.find(x=>String(x.id)===String(id));if(line)line.qty=next}renderCatalogV5();};
|
||
|
||
function ensureManagerToolbar(){
|
||
const dialog=document.querySelector('#manager .dialog');if(!dialog)return;let bar=dialog.querySelector('.sun-manager-toolbar');const add=qa(':scope > .primary',dialog)[0]||qa('.primary',dialog).find(b=>(b.textContent||'').includes('Добавить'));
|
||
if(!bar&&add){bar=document.createElement('div');bar.className='sun-manager-toolbar';add.parentNode.insertBefore(bar,add);bar.appendChild(add);const input=document.createElement('input');input.id='sunManagerSearch';input.className='sun-manager-search';input.type='search';input.placeholder=Number(activeCat)===0?'Поиск по № бокса':'Поиск по позиции';input.inputMode=Number(activeCat)===0?'numeric':'search';input.oninput=()=>{managerQuery=input.value;renderManagerV5();};bar.appendChild(input);}
|
||
}
|
||
function renderManagerV5(){
|
||
ensureManagerToolbar();const managerSearch=$('sunManagerSearch');if(managerSearch){const boxMode=[0,5].includes(Number(activeCat));managerSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';managerSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#manager .dialog-head h2'),add=document.querySelector('#manager .sun-manager-toolbar .primary')||document.querySelector('#manager .primary');if(title)title.textContent=catName(activeCat);if(add){add.textContent=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');add.onclick=()=>window.editBox(null)}
|
||
const items=boxes.filter(i=>Number(i.category||0)===Number(activeCat)&&itemMatches(i,managerQuery)),cat=catById(activeCat)||{prep:true};
|
||
$('managerList').innerHTML=items.length?items.map(item=>`<button type="button" onclick="editBox('${esc(item.id)}')">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='sun-logo.png'">`:'<div class="ph"><img src="sun-logo.png" class="sun-ph-logo" alt="Логотип Солнце Кейтеринг"></div>'}<b>${esc(item.name)}</b><span class="manager-price-wrap"><small class="manager-card-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${(item.weight||inferBoxPieces(item))?`<small class="manager-card-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}${cat.prep?`<small>${(item.ingredients||[]).length} позиций в составе</small>`:''}</button>`).join(''):'<p class="empty">Ничего не найдено.</p>';
|
||
}
|
||
window.openManager=()=>{managerQuery='';ensureManagerToolbar();if($('sunManagerSearch'))$('sunManagerSearch').value='';renderManagerV5();window.modal?.('manager');};
|
||
window.editBox=id=>{
|
||
edited=id?structuredClone(boxes.find(x=>String(x.id)===String(id))):{id:'',name:Number(activeCat)===0?'Новый бокс':(Number(activeCat)===5?'Новая премиум позиция':'Новая позиция'),category:Number(activeCat),price:0,oldPrice:0,ingredients:[]};if(!edited)return;
|
||
if(!Array.isArray(edited.ingredients))edited.ingredients=[];normalizeBoxItem(edited);syncEditorCategoryOptions();$('itemCategory').value=String(edited.category??activeCat);$('boxName').value=edited.name||'';$('boxPrice').value=Number(edited.price||0);if($('boxOldPrice'))$('boxOldPrice').value=Number(edited.oldPrice||0);if($('boxCompositionText'))$('boxCompositionText').value=(Array.isArray(edited.composition)?edited.composition:[]).join('\n');syncBoxWeightEditor(edited.category??activeCat,edited);$('editPhoto').style.display=edited.photo?'block':'none';$('editPhoto').src=edited.photo||'';$('deleteBox').style.display=edited.id?'block':'none';
|
||
const cat=catById(edited.category)||{name:'Каталог',prep:true};$('editTitle').textContent=edited.id?`Изменить: ${cat.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=cat.prep?'block':'none';window.renderIngredients?.();window.modal?.('editor');
|
||
};
|
||
if($('itemCategory'))$('itemCategory').onchange=function(){if(!edited)return;edited.category=Number(this.value);const c=catById(edited.category)||{name:'Каталог',prep:true};const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=c.prep?'block':'none';syncBoxWeightEditor(edited.category,edited);$('editTitle').textContent=edited.id?`Изменить: ${c.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));};
|
||
window.saveBox=()=>{if(!edited)return;edited.name=$('boxName').value.trim();edited.price=Math.max(0,Number($('boxPrice').value||0));edited.oldPrice=Math.max(0,Number($('boxOldPrice')?.value||0));if(!(edited.oldPrice>edited.price))delete edited.oldPrice;edited.sale=Boolean(edited.oldPrice&&edited.oldPrice>edited.price);edited.category=Number($('itemCategory').value);if([0,5].includes(edited.category)){const grams=Math.max(0,Math.round(Number($('boxWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';edited.pieces=Math.max(0,Math.round(Number($('boxPiecesCount')?.value||0)));}edited.ingredients=Array.isArray(edited.ingredients)?edited.ingredients:[];if([0,5].includes(edited.category)&&$('boxCompositionText')){const next=$('boxCompositionText').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);const before=JSON.stringify(Array.isArray(edited.composition)?edited.composition:[]);edited.composition=next;if(JSON.stringify(next)!==before)edited.compositionSource='manual';}normalizeBoxItem(edited,{cleanup:true});if(!edited.name)return alert('Введите название.');if(!edited.id){edited.id=(typeof sunUUID==='function'?sunUUID():`item-${Date.now()}`);boxes.push(edited)}else{const i=boxes.findIndex(x=>String(x.id)===String(edited.id));if(i>=0)boxes[i]=edited}if(!catById(edited.category)?.hidden)activeCat=edited.category;persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();window.sunClientOfferCatalogChanged?.(edited.id);};
|
||
window.removeBox=()=>{if(!edited?.id||!confirm(`Удалить «${edited.name}» из каталога?`))return;boxes=boxes.filter(x=>String(x.id)!==String(edited.id));draft.lines=(draft.lines||[]).filter(x=>String(x.id)!==String(edited.id));persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();};
|
||
|
||
function fileAsDataUrl(file){return new Promise((resolve,reject)=>{const reader=new FileReader();reader.onerror=()=>reject(reader.error||new Error('FileReader error'));reader.onload=()=>resolve(String(reader.result||''));reader.readAsDataURL(file)})}
|
||
function compactCatalogPhotoData(dataUrl,{maxSide=900,maxChars=130000,quality=.78,force=false}={}){return new Promise(resolve=>{if(!/^data:image\//i.test(String(dataUrl||'')))return resolve(dataUrl);const image=new Image();image.onerror=()=>resolve(dataUrl);image.onload=()=>{const iw=Math.max(1,image.naturalWidth||image.width||1),ih=Math.max(1,image.naturalHeight||image.height||1);if(!force&&String(dataUrl).length<=maxChars&&Math.max(iw,ih)<=maxSide)return resolve(dataUrl);let scale=Math.min(1,maxSide/Math.max(iw,ih)),q=quality,best=dataUrl;for(let attempt=0;attempt<5;attempt++){const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(iw*scale));canvas.height=Math.max(1,Math.round(ih*scale));const ctx=canvas.getContext('2d');if(!ctx)break;ctx.fillStyle='#fff';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.drawImage(image,0,0,canvas.width,canvas.height);try{best=canvas.toDataURL('image/jpeg',q)||best}catch(_){break}if(best.length<=maxChars)break;scale*=.78;q=Math.max(.52,q-.07)}resolve(best)};image.src=dataUrl})}
|
||
window.sunCompactCatalogPhotoData=compactCatalogPhotoData;
|
||
window.readPhoto=async input=>{const file=input?.files?.[0];if(!file||!edited)return;try{const original=await fileAsDataUrl(file);const optimized=await compactCatalogPhotoData(original);edited.photo=optimized;$('editPhoto').src=optimized;$('editPhoto').style.display='block';if(optimized.length+5000<original.length)toast('Фото оптимизировано для стабильной синхронизации.','success')}catch(err){console.warn('Catalog photo optimization failed',err);toast('Не удалось обработать фото.','warn')}};
|
||
|
||
// ---------------- Order requisites ----------------
|
||
const buyerDefaults=()=>({name:'',inn:'',kpp:'',ogrn:'',legalAddress:'',phone:'',email:'',bank:'',bik:'',account:'',corrAccount:'',contact:''});
|
||
function legacyBuyer(){try{const s=JSON.parse(localStorage.getItem('sunReceiptSettingsV2')||'{}');return {name:s.buyerName||'',inn:s.buyerInn||'',kpp:s.buyerKpp||'',ogrn:s.buyerOgrn||'',legalAddress:s.buyerLegalAddress||'',phone:s.buyerPhone||'',email:s.buyerEmail||'',bank:s.buyerBank||'',bik:s.buyerBik||'',account:s.buyerAccount||'',corrAccount:s.buyerCorrAccount||'',contact:s.buyerContact||''}}catch(_){return buyerDefaults()}}
|
||
function ensureRequisitesTab(){
|
||
const sub=document.querySelector('#new .subnav');if(!sub)return;
|
||
let btn=$('requisitesTab');if(!btn){btn=document.createElement('button');btn.id='requisitesTab';btn.type='button';btn.textContent='Реквизиты';sub.appendChild(btn)}
|
||
let panel=$('orderRequisites');if(!panel){panel=document.createElement('div');panel.id='orderRequisites';panel.className='sun-order-requisites';panel.innerHTML=`<h3>Реквизиты покупателя</h3><div class="sun-order-requisites-grid"><label>Компания / ИП<input id="orderBuyerName"></label><label>Контактное лицо<input id="orderBuyerContact"></label><label>ИНН<input id="orderBuyerInn"></label><label>КПП<input id="orderBuyerKpp"></label><label>ОГРН / ОГРНИП<input id="orderBuyerOgrn"></label><label>Телефон<input id="orderBuyerPhone"></label><label class="wide">Юридический адрес<input id="orderBuyerLegalAddress"></label><label class="wide">E-mail<input id="orderBuyerEmail"></label><label class="wide">Банк<input id="orderBuyerBank"></label><label>БИК<input id="orderBuyerBik"></label><label>Расчётный счёт<input id="orderBuyerAccount"></label><label class="wide">Корреспондентский счёт<input id="orderBuyerCorrAccount"></label></div>`;$('details').insertAdjacentElement('afterend',panel)}
|
||
$('orderTab').onclick=()=>setOrderPane('order');$('detailTab').onclick=()=>setOrderPane('details');btn.onclick=()=>setOrderPane('requisites');
|
||
qa('#orderRequisites input:not([type=file])').forEach(input=>input.addEventListener('input',captureBuyerFromFields));
|
||
}
|
||
function setOrderPane(tab){ensureRequisitesTab();$('orderForm').style.display=tab==='order'?'block':'none';$('details').style.display=tab==='details'?'block':'none';$('orderRequisites').style.display=tab==='requisites'?'block':'none';$('orderTab').classList.toggle('on',tab==='order');$('detailTab').classList.toggle('on',tab==='details');$('requisitesTab').classList.toggle('on',tab==='requisites');if(tab==='requisites')syncBuyerFields();}
|
||
window.details=on=>setOrderPane(on?'details':'order');
|
||
function buyerObject(){return {name:$('orderBuyerName')?.value.trim()||'',contact:$('orderBuyerContact')?.value.trim()||'',inn:$('orderBuyerInn')?.value.trim()||'',kpp:$('orderBuyerKpp')?.value.trim()||'',ogrn:$('orderBuyerOgrn')?.value.trim()||'',phone:$('orderBuyerPhone')?.value.trim()||'',legalAddress:$('orderBuyerLegalAddress')?.value.trim()||'',email:$('orderBuyerEmail')?.value.trim()||'',bank:$('orderBuyerBank')?.value.trim()||'',bik:$('orderBuyerBik')?.value.trim()||'',account:$('orderBuyerAccount')?.value.trim()||'',corrAccount:$('orderBuyerCorrAccount')?.value.trim()||''}}
|
||
function captureBuyerFromFields(){if(typeof draft==='undefined'||!draft)return;draft.buyerRequisites=buyerObject();}
|
||
function syncBuyerFields(){ensureRequisitesTab();let b=draft?.buyerRequisites;if(!b){const legacy=legacyBuyer(),hasLegacy=Object.values(legacy).some(Boolean);b=hasLegacy?legacy:buyerDefaults();draft.buyerRequisites={...b}};const map={orderBuyerName:'name',orderBuyerContact:'contact',orderBuyerInn:'inn',orderBuyerKpp:'kpp',orderBuyerOgrn:'ogrn',orderBuyerPhone:'phone',orderBuyerLegalAddress:'legalAddress',orderBuyerEmail:'email',orderBuyerBank:'bank',orderBuyerBik:'bik',orderBuyerAccount:'account',orderBuyerCorrAccount:'corrAccount'};Object.entries(map).forEach(([id,key])=>{if($(id))$(id).value=b?.[key]||''});}
|
||
|
||
// ---------------- QR default + image handling ----------------
|
||
const QR_KEY='sunDefaultOrderQrV1';
|
||
const defaultQr=()=>{try{return localStorage.getItem(QR_KEY)||''}catch(_){return''}};
|
||
function imageData(file,max=512){return new Promise((resolve,reject)=>{const r=new FileReader();r.onerror=reject;r.onload=()=>{const img=new Image();img.onerror=reject;img.onload=()=>{const scale=Math.min(1,max/Math.max(img.width||1,img.height||1)),c=document.createElement('canvas');c.width=Math.max(1,Math.round(img.width*scale));c.height=Math.max(1,Math.round(img.height*scale));const ctx=c.getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,c.width,c.height);ctx.drawImage(img,0,0,c.width,c.height);resolve(c.toDataURL('image/png'))};img.src=r.result};r.readAsDataURL(file)})}
|
||
function ensureDefaultQrSetting(){
|
||
const grid=document.querySelector('#enterprise-settings .enterprise-grid');if(!grid)return;let card=$('sunDefaultQrCard');if(!card){card=document.createElement('section');card.id='sunDefaultQrCard';card.className='enterprise-card wide';card.innerHTML=`<h2>QR-код для бланка заказа</h2><p class="hint">Загрузите QR-код один раз. Он будет автоматически использоваться во всех бланках заказов, пока вы не загрузите другой.</p><div class="sun-default-qr-setting"><div id="sunDefaultQrPreview" class="preview">QR не загружен</div><div><input id="sunDefaultQrFile" type="file" accept="image/*" hidden><div class="actions"><button class="outline" id="sunDefaultQrUpload" type="button">Загрузить / заменить QR</button></div></div></div>`;const audit=qa(':scope > .enterprise-card',grid).find(x=>(x.querySelector('h2')?.textContent||'').trim()==='История изменений');window.SunSafe.insertBefore(grid,card,audit||null);$('sunDefaultQrUpload').onclick=()=>$('sunDefaultQrFile').click();$('sunDefaultQrFile').onchange=async()=>{const f=$('sunDefaultQrFile').files?.[0];if(!f)return;try{localStorage.setItem(QR_KEY,await imageData(f));renderDefaultQr();toast('QR-код для бланков сохранён.')}catch(_){toast('Не удалось загрузить QR-код.','warn')}};}
|
||
renderDefaultQr();const audit=qa(':scope > .enterprise-card',grid).find(x=>(x.querySelector('h2')?.textContent||'').trim()==='История изменений');if(audit&&grid.lastElementChild!==audit)grid.appendChild(audit);
|
||
}
|
||
function renderDefaultQr(){const root=$('sunDefaultQrPreview');if(!root)return;const src=defaultQr();root.innerHTML=src?`<img src="${src}" alt="QR">`:'QR не загружен';}
|
||
|
||
// ---------------- Backup to/from JSON file ----------------
|
||
function backupFileState(){const storage={};for(let i=0;i<localStorage.length;i++){const key=localStorage.key(i);if(!key||!key.startsWith('sun'))continue;storage[key]=localStorage.getItem(key);}return {format:'sun-catering-backup',version:1,createdAt:new Date().toISOString(),storage};}
|
||
function downloadBackupFile(){try{const state=backupFileState();const blob=new Blob([JSON.stringify(state)],{type:'application/json'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=`caterium-backup-${new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')}.json`;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2000);toast('Файл резервной копии скачан.','success');}catch(_){toast('Не удалось создать файл резервной копии.','warn')}}
|
||
async function restoreBackupFile(file){
|
||
let state;
|
||
try{state=JSON.parse(await file.text());}catch(_){toast('Не удалось прочитать файл — это не JSON.','warn');return;}
|
||
if(!state||state.format!=='sun-catering-backup'||!state.storage||typeof state.storage!=='object'){toast('Неверный формат файла резервной копии.','warn');return;}
|
||
if(!confirm('Загрузить базу из файла? Текущие локальные данные будут заменены содержимым файла.'))return;
|
||
Object.entries(state.storage).forEach(([key,value])=>{if(key.startsWith('sun')){try{localStorage.setItem(key,String(value));}catch(_){}}});
|
||
toast('База восстановлена из файла. Перезагрузка…','success');
|
||
setTimeout(()=>location.reload(),600);
|
||
}
|
||
function ensureBackupFileCard(){
|
||
const grid=document.querySelector('#enterprise-settings .enterprise-grid');if(!grid)return;
|
||
let card=$('sunBackupFileCard');
|
||
if(!card){
|
||
card=document.createElement('section');card.id='sunBackupFileCard';card.className='enterprise-card wide';
|
||
card.innerHTML=`<h2>Резервная копия базы (файл)</h2><p class="hint">Скачайте базу в файл на компьютер или загрузите ранее сохранённый файл — например, для переноса на другой компьютер или работы без интернета.</p><input id="sunBackupFileInput" type="file" accept=".json,application/json" hidden><div class="actions"><button class="outline" id="sunBackupFileDownload" type="button">Скачать базу в файл</button><button class="outline" id="sunBackupFileUpload" type="button">Загрузить базу из файла</button></div>`;
|
||
const audit=qa(':scope > .enterprise-card',grid).find(x=>(x.querySelector('h2')?.textContent||'').trim()==='История изменений');
|
||
window.SunSafe.insertBefore(grid,card,audit||null);
|
||
$('sunBackupFileDownload').onclick=downloadBackupFile;
|
||
$('sunBackupFileUpload').onclick=()=>$('sunBackupFileInput').click();
|
||
$('sunBackupFileInput').onchange=async()=>{const f=$('sunBackupFileInput').files?.[0];$('sunBackupFileInput').value='';if(f)await restoreBackupFile(f);};
|
||
}
|
||
const audit=qa(':scope > .enterprise-card',grid).find(x=>(x.querySelector('h2')?.textContent||'').trim()==='История изменений');if(audit&&grid.lastElementChild!==audit)grid.appendChild(audit);
|
||
}
|
||
|
||
// ---------------- Discounts + promo tracking ----------------
|
||
const PROMO_KEY='sunPromoCodesV1';
|
||
const localDateIso=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`};
|
||
function promoCodes(){try{const list=JSON.parse(localStorage.getItem(PROMO_KEY)||'[]');return Array.isArray(list)?list:[]}catch(_){return[]}}
|
||
function promoActive(p,date=localDateIso()){if(!p||p.enabled===false)return false;const start=String(p.startDate||''),end=String(p.endDate||'');return (!start||start<=date)&&(!end||end>=date)}
|
||
function promoDiscountText(p){const value=Math.max(0,Number(p?.discountValue||0));return p?.discountType==='amount'?money(value):`${value.toLocaleString('ru-RU',{maximumFractionDigits:2})}%`}
|
||
function promoById(id){return promoCodes().find(p=>String(p.id)===String(id))||null}
|
||
function promoByCode(code){const key=String(code||'').trim().toLowerCase();return promoCodes().find(p=>String(p.code||'').trim().toLowerCase()===key)||null}
|
||
function ensureDiscountUI(){
|
||
const form=document.querySelector('#new .money-form');if(!form)return;
|
||
if(!$('deliveryCost')){
|
||
const label=document.createElement('label');label.id='deliveryCostLabel';label.innerHTML='<span>Доставка, ₽</span><input id="deliveryCost" type="number" min="0" step="100" inputmode="decimal" value="'+Math.max(0,Number(draft?.deliveryCost||0))+'">';
|
||
const prepay=$('prepayment')?.closest('label');if(prepay)prepay.insertAdjacentElement('beforebegin',label);else form.appendChild(label);
|
||
$('deliveryCost')?.addEventListener('input',()=>{captureDelivery();updateOrderSummary()});
|
||
}
|
||
let panel=$('sunDiscountPanel');
|
||
const desired=`<label><span>Скидка</span><div class="sun-discount-combo"><input id="manualDiscountValue" type="number" min="0" step="1" value="0"><select id="manualDiscountType"><option value="percent">%</option><option value="amount">₽</option></select></div></label><label class="sun-promo-select-wrap"><span>Промокод</span><select id="promoCodeSelect"><option value="">Без промокода</option></select><div id="promoCodeMeta" class="sun-promo-meta">Выберите активный промокод из раздела «Рассылки».</div></label>`;
|
||
if(!panel){panel=document.createElement('div');panel.id='sunDiscountPanel';panel.className='sun-discount-panel';panel.innerHTML=desired;form.insertAdjacentElement('afterend',panel)}
|
||
else if(!$('promoCodeSelect'))panel.innerHTML=desired;
|
||
if(panel.dataset.promoBound!=='1'){
|
||
panel.dataset.promoBound='1';
|
||
$('manualDiscountValue')?.addEventListener('input',()=>{captureDiscounts();updateOrderSummary()});
|
||
$('manualDiscountType')?.addEventListener('change',()=>{captureDiscounts();updateOrderSummary()});
|
||
$('promoCodeSelect')?.addEventListener('change',()=>{captureDiscounts();updateOrderSummary()});
|
||
}
|
||
const summary=document.querySelector('#new .order-summary');if(summary&&!$('summaryNetTotal'))summary.innerHTML=`<p><span>Стоимость позиций</span><b id="summaryTotal">0 ₽</b></p><p class="sun-order-summary-discount"><span>Скидка</span><b id="summaryManualDiscount">− 0 ₽</b></p><p class="sun-order-summary-discount sun-order-summary-promo"><span>Промокод <small id="summaryPromoCode"></small></span><b id="summaryPromoDiscount">− 0 ₽</b></p><p><span>Доставка</span><b id="summaryDelivery">0 ₽</b></p><p class="sun-order-summary-net"><span>Итого</span><b id="summaryNetTotal">0 ₽</b></p><p><span>Предоплата</span><b id="summaryPrepayment">0 ₽</b></p><p class="balance"><span>К оплате</span><b id="summaryBalance">0 ₽</b></p>`;
|
||
}
|
||
function renderPromoSelect(){
|
||
ensureDiscountUI();const select=$('promoCodeSelect');if(!select)return;
|
||
const active=promoCodes().filter(p=>promoActive(p)).sort((a,b)=>String(a.code||'').localeCompare(String(b.code||''),'ru'));
|
||
const currentId=String(draft?.promoId||''),currentCode=String(draft?.promoCode||'').trim();
|
||
let selected='';
|
||
const opts=['<option value="">Без промокода</option>'];
|
||
active.forEach(p=>{opts.push(`<option value="${esc(p.id)}">${esc(p.code||'Промокод')} · ${esc(promoDiscountText(p))} · до ${esc(p.endDate||'без срока')}</option>`);if(currentId&&String(p.id)===currentId)selected=String(p.id)});
|
||
if(currentCode&&!selected){
|
||
const saved=promoById(currentId)||promoByCode(currentCode),label=saved?(promoActive(saved)?'активен':'срок истёк / отключён'):'сохранён в заказе';
|
||
opts.push(`<option value="__snapshot__">${esc(currentCode)} · ${esc(draft?.promoDiscountType==='amount'?money(draft?.promoDiscountValue||0):`${Number(draft?.promoDiscountValue||0).toLocaleString('ru-RU',{maximumFractionDigits:2})}%`)} · ${esc(label)}</option>`);selected='__snapshot__';
|
||
}
|
||
select.innerHTML=opts.join('');select.value=selected;
|
||
updatePromoMeta();
|
||
}
|
||
function updatePromoMeta(){const meta=$('promoCodeMeta'),select=$('promoCodeSelect');if(!meta||!select)return;const id=select.value;if(!id){meta.textContent='Выберите активный промокод из раздела «Рассылки».';return}if(id==='__snapshot__'){meta.textContent=`Условия сохранены в заказе: ${draft?.promoCode||''} · ${draft?.promoDiscountType==='amount'?money(draft?.promoDiscountValue||0):`${Number(draft?.promoDiscountValue||0).toLocaleString('ru-RU',{maximumFractionDigits:2})}%`}.`;return}const p=promoById(id);meta.textContent=p?`${p.code} · скидка ${promoDiscountText(p)} · действует ${p.startDate||'без даты'} — ${p.endDate||'без даты'}`:'Промокод недоступен.';}
|
||
function captureDiscounts(){
|
||
if(!draft)return;draft.manualDiscountValue=Math.max(0,Number($('manualDiscountValue')?.value||0));draft.manualDiscountType=$('manualDiscountType')?.value==='amount'?'amount':'percent';
|
||
const select=$('promoCodeSelect');if(!select)return;const id=String(select.value||'');
|
||
if(!id){draft.promoId='';draft.promoCode='';draft.promoDiscountValue=0;draft.promoDiscountType='percent';draft.promoStartDate='';draft.promoEndDate='';return}
|
||
if(id==='__snapshot__'){updatePromoMeta();return}
|
||
const p=promoById(id);if(!p||!promoActive(p)){renderPromoSelect();return}
|
||
draft.promoId=String(p.id||'');draft.promoCode=String(p.code||'').trim();draft.promoDiscountValue=Math.max(0,Number(p.discountValue||0));draft.promoDiscountType=p.discountType==='amount'?'amount':'percent';draft.promoStartDate=String(p.startDate||'');draft.promoEndDate=String(p.endDate||'');updatePromoMeta();
|
||
}
|
||
function captureDelivery(){if(!draft)return;draft.deliveryCost=Math.max(0,Number($('deliveryCost')?.value||0));}
|
||
function syncDeliveryField(){ensureDiscountUI();if($('deliveryCost'))$('deliveryCost').value=Math.max(0,Number(draft?.deliveryCost||0));}
|
||
function discountAmount(value,type,base){const n=Math.max(0,Number(value||0));return Math.min(base,type==='amount'?n:base*Math.min(100,n)/100)}
|
||
function pricing(order=draft){const base=baseTotal(order),manual=discountAmount(order?.manualDiscountValue,order?.manualDiscountType,base),afterManual=Math.max(0,base-manual),promo=discountAmount(order?.promoDiscountValue,order?.promoDiscountType,afterManual),itemsTotal=Math.max(0,afterManual-promo),delivery=Math.max(0,Number(order?.deliveryCost||0)),total=itemsTotal+delivery,paid=Math.max(0,Number(order===draft?($('prepayment')?.value||order?.prepayment||0):(order?.prepayment||0))),balance=Math.max(0,total-paid);return {base,manual,promo,itemsTotal,delivery,total,paid,balance,discount:manual+promo}}
|
||
window.sunOrderPricing=pricing;
|
||
window.calcOrderTotal=()=>pricing(draft).total;
|
||
window.orderTotalValue=order=>pricing(order).total;
|
||
window.updateOrderSummary=()=>{ensureDiscountUI();captureDiscounts();const p=pricing(draft);if($('orderTotal'))$('orderTotal').value=Math.round(p.total);if($('balance'))$('balance').value=Math.round(p.balance);if($('summaryTotal'))$('summaryTotal').textContent=money(p.base);if($('summaryManualDiscount'))$('summaryManualDiscount').textContent=`− ${money(p.manual)}`;if($('summaryPromoDiscount'))$('summaryPromoDiscount').textContent=`− ${money(p.promo)}`;if($('summaryPromoCode'))$('summaryPromoCode').textContent=draft.promoCode?`(${draft.promoCode})`:'';if($('summaryDelivery'))$('summaryDelivery').textContent=money(p.delivery);if($('summaryNetTotal'))$('summaryNetTotal').textContent=money(p.total);if($('summaryPrepayment'))$('summaryPrepayment').textContent=money(p.paid);if($('summaryBalance'))$('summaryBalance').textContent=money(p.balance);updatePromoMeta();};
|
||
function syncDiscountFields(){ensureDiscountUI();$('manualDiscountValue').value=Number(draft?.manualDiscountValue||0);$('manualDiscountType').value=draft?.manualDiscountType==='amount'?'amount':'percent';syncDeliveryField();renderPromoSelect();updateOrderSummary();}
|
||
window.sunRefreshOrderPromos=()=>{renderPromoSelect();updateOrderSummary();};
|
||
|
||
// ---------------- Seller settings only ----------------
|
||
function simplifySellerSettings(){
|
||
const card=$('sunReceiptSettingsCard');if(!card||card.dataset.sunSellerOnly==='1')return;
|
||
const sections=qa('.sun-receipt-settings-section',card);sections.forEach(s=>{if((s.querySelector('h3')?.textContent||'').includes('Покупатель'))s.classList.add('sun-buyer-settings-hidden')});
|
||
const buyerCheck=$('receiptIncludeBuyer');buyerCheck?.closest('label')?.classList.add('sun-buyer-settings-hidden');
|
||
const h=card.querySelector('h2');if(h&&(h.textContent||'').trim()!=='Реквизиты компании')h.textContent='Реквизиты компании';
|
||
const hint=card.querySelector(':scope > .hint'),hintText='Реквизиты продавца для товарного чека. Реквизиты покупателя вводятся отдельно в каждом заказе во вкладке «Реквизиты».';if(hint&&hint.textContent!==hintText)hint.textContent=hintText;
|
||
card.dataset.sunSellerOnly='1';
|
||
}
|
||
function readSeller(){const d={includeSeller:true,sellerName:'Солнце Кейтеринг',sellerLegalName:'',sellerInn:'',sellerKpp:'',sellerOgrn:'',sellerLegalAddress:'',sellerPhone:'',sellerEmail:'',sellerBank:'',sellerBik:'',sellerAccount:'',sellerCorrAccount:''};try{return {...d,...JSON.parse(localStorage.getItem('sunReceiptSettingsV2')||'{}')}}catch(_){return d}}
|
||
function party(title,rows){const list=rows.filter(([,v])=>String(v||'').trim());if(!list.length)return'';return `<section class="sun-receipt-party"><h3>${esc(title)}</h3><div class="sun-receipt-party-grid">${list.map(([k,v])=>`<div><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join('')}</div></section>`}
|
||
|
||
// ---------------- Order blank + compact receipt ----------------
|
||
function currentRows(){return (draft.lines||[]).map((line,index)=>{const item=boxes.find(x=>String(x.id)===String(line.id)),price=lineUnitPrice(line),qty=Math.max(0,Number(line.qty||0)),categoryId=Number(item?.category??line.category??0);return{index:index+1,name:item?.name||line.name||`Позиция ${line.id||''}`,category:catName(categoryId),categoryId,weight:[0,5].includes(categoryId)?String(item?.weight||''):'',pieces:[0,5].includes(categoryId)?inferBoxPieces(item):0,price,qty,sum:price*qty}}).filter(r=>r.qty>0)}
|
||
const docDate=raw=>{if(!raw)return'—';const d=new Date(`${raw}T00:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU')};
|
||
const docActions=()=>`<div class="sun-doc-actions"><button class="primary" type="button" onclick="window.print()">Печать / PDF</button><button class="outline" type="button" onclick="closeModal('preview')">Закрыть</button></div>`;
|
||
function showOrderBlank(){
|
||
captureBuyerFromFields();captureDiscounts();const rows=currentRows(),p=pricing(draft),qr=defaultQr(),b=draft.buyerRequisites||buyerDefaults(),dialog=document.querySelector('#preview .dialog');if(!dialog)return;const number=draft.id||'черновик',qrNote=String(draft.blankQrText||'').trim();
|
||
dialog.innerHTML=`<div class="sun-doc"><div class="sun-doc-head"><div class="sun-doc-brand"><img src="sun-logo.png" alt="Солнце Кейтеринг"><div><strong>Солнце Кейтеринг</strong><small>Санкт-Петербург · кейтеринг и выездные мероприятия</small></div></div><div class="sun-doc-title-with-qr"><div class="sun-doc-title"><h2>Заказ № ${esc(number)}</h2><p>${esc($('event')?.value||'Новое мероприятие')}</p>${draft.status?`<span class="sun-doc-badge">${esc(draft.status)}</span>`:''}</div>${qr?`<div class="sun-doc-qr-wrap"><img class="sun-doc-qr" src="${qr}" alt="QR-код"><div id="sunBlankQrText" class="sun-doc-qr-note" contenteditable="true" spellcheck="true" data-placeholder="Нажмите и добавьте текст рядом с QR">${esc(qrNote)}</div></div>`:''}</div></div><div class="sun-doc-grid"><div class="sun-doc-field"><span>Клиент</span><b>${esc($('contact')?.value||'—')}</b></div><div class="sun-doc-field"><span>Телефон</span><b>${esc($('phone')?.value||'—')}</b></div><div class="sun-doc-field"><span>Дата и время выдачи</span><b>${esc(docDate($('date')?.value))} · ${esc($('time')?.value||'—')}</b></div><div class="sun-doc-field"><span>Адрес доставки</span><b>${esc($('address')?.value||'—')}</b></div>${b.name?`<div class="sun-doc-field"><span>Компания покупателя</span><b>${esc(b.name)}</b></div>`:''}</div><table><thead><tr><th class="doc-num">№</th><th class="doc-name">Позиция</th><th>Цена</th><th>Кол.</th><th>Сумма</th></tr></thead><tbody>${rows.length?rows.map(r=>`<tr><td class="doc-num">${r.index}</td><td class="doc-name"><b>${esc(r.name)}</b><span class="doc-sub">${esc(r.category)}</span>${(r.weight||r.pieces)?`<span class="doc-sub sun-doc-weight">${r.weight?`Вес бокса: ${esc(r.weight)}`:''}${r.weight&&r.pieces?' · ':''}${r.pieces?`Количество: ${esc(r.pieces)} шт.`:''}</span>`:''}</td><td>${money(r.price)}</td><td>${r.qty}</td><td><b>${money(r.sum)}</b></td></tr>`).join(''):'<tr><td colspan="5">В заказе пока нет позиций.</td></tr>'}</tbody></table><div class="sun-doc-summary"><p><span>Стоимость позиций</span><b>${money(p.base)}</b></p>${p.manual?`<p><span>Скидка</span><b>− ${money(p.manual)}</b></p>`:''}${p.promo?`<p><span>Промокод ${esc(draft.promoCode||'')}</span><b>− ${money(p.promo)}</b></p>`:''}<p><span>Доставка</span><b>${money(p.delivery)}</b></p><p><span>Итого</span><b>${money(p.total)}</b></p><p><span>Предоплата</span><b>${money(p.paid)}</b></p><p class="grand"><span>Остаток к оплате</span><span>${money(p.balance)}</span></p></div>${$('note')?.value?`<div class="sun-doc-note"><b>Комментарий к заказу</b>${esc($('note').value)}</div>`:''}<div class="sun-doc-footer"><div><b>Солнце Кейтеринг</b><br>Спасибо, что выбрали нас.<div class="sun-doc-sign">Ответственный / подпись</div></div><div>Заказчик подтверждает состав заказа, дату, время и адрес выдачи.<div class="sun-doc-sign">Заказчик / подпись</div></div></div>${docActions()}</div>`;window.modal?.('preview');const noteEl=$('sunBlankQrText');if(noteEl){const save=()=>{const text=String(noteEl.innerText||noteEl.textContent||'').replace(/\u00a0/g,' ').trim();draft.blankQrText=text;if(draft.id){const saved=(orders||[]).find(o=>String(o.id)===String(draft.id));if(saved){saved.blankQrText=text;try{persist()}catch(_){}}}};noteEl.addEventListener('input',save);noteEl.addEventListener('blur',save);}
|
||
}
|
||
function showReceipt(){
|
||
captureBuyerFromFields();captureDiscounts();const rows=currentRows(),p=pricing(draft),s=readSeller(),b=draft.buyerRequisites||buyerDefaults(),dialog=document.querySelector('#preview .dialog');if(!dialog)return;const seller=s.includeSeller!==false?party('Продавец',[['Наименование',s.sellerLegalName||s.sellerName],['ИНН',s.sellerInn],['КПП',s.sellerKpp],['ОГРН / ОГРНИП',s.sellerOgrn],['Адрес',s.sellerLegalAddress],['Телефон',s.sellerPhone],['E-mail',s.sellerEmail],['Банк',s.sellerBank],['БИК',s.sellerBik],['Расч. счёт',s.sellerAccount],['Корр. счёт',s.sellerCorrAccount]]):'';const buyer=party('Покупатель',[['Компания / ИП',b.name],['ИНН',b.inn],['КПП',b.kpp],['ОГРН / ОГРНИП',b.ogrn],['Адрес',b.legalAddress],['Контакт',b.contact],['Телефон',b.phone],['E-mail',b.email],['Банк',b.bank],['БИК',b.bik],['Расч. счёт',b.account],['Корр. счёт',b.corrAccount]]);
|
||
dialog.innerHTML=`<div class="sun-doc sun-receipt"><div class="sun-doc-head"><div class="sun-doc-brand"><img src="sun-logo.png" alt="Солнце Кейтеринг"><div><strong>${esc(s.sellerName||'Солнце Кейтеринг')}</strong><small>${esc(s.sellerLegalName||'')}</small></div></div><div class="sun-doc-title"><h2>Товарный чек № ${esc(draft.id||'—')}</h2><p>${esc(docDate($('date')?.value||draft.date))} · ${esc($('time')?.value||draft.time||'')}</p></div></div>${seller}${buyer}<table><thead><tr><th class="doc-num">№</th><th class="doc-name">Наименование</th><th>Цена</th><th>Кол.</th><th>Сумма</th></tr></thead><tbody>${rows.length?rows.map(r=>`<tr><td class="doc-num">${r.index}</td><td class="doc-name">${esc(r.name)}${(r.weight||r.pieces)?`<span class="doc-sub sun-doc-weight">${r.weight?`Вес бокса: ${esc(r.weight)}`:''}${r.weight&&r.pieces?' · ':''}${r.pieces?`Количество: ${esc(r.pieces)} шт.`:''}</span>`:''}</td><td>${money(r.price)}</td><td>${r.qty}</td><td>${money(r.sum)}</td></tr>`).join(''):'<tr><td colspan="5">В заказе нет позиций.</td></tr>'}</tbody></table><div class="sun-doc-summary"><p><span>Стоимость позиций</span><b>${money(p.base)}</b></p>${p.manual?`<p><span>Скидка</span><b>− ${money(p.manual)}</b></p>`:''}${p.promo?`<p><span>Промокод ${esc(draft.promoCode||'')}</span><b>− ${money(p.promo)}</b></p>`:''}<p><span>Доставка</span><b>${money(p.delivery)}</b></p><p><span>Итого</span><b>${money(p.total)}</b></p><p><span>Оплачено</span><b>${money(p.paid)}</b></p><p class="grand"><span>К оплате</span><span>${money(p.balance)}</span></p></div>${$('note')?.value?`<div class="sun-doc-note"><b>Примечание</b>${esc($('note').value)}</div>`:''}<div class="sun-doc-footer"><div><div class="sun-doc-sign">Продавец / подпись</div></div><div><div class="sun-doc-sign">Покупатель / подпись</div></div></div>${docActions()}</div>`;window.modal?.('preview');
|
||
}
|
||
window.sunShowConfiguredReceipt=showReceipt;window.sunShowOrderBlankV5=showOrderBlank;
|
||
function patchDocButtons(){qa('#new .actions button').forEach(b=>{const text=(b.textContent||'').trim();if(text==='Товарный чек')b.onclick=showReceipt;if(text==='Бланк заказа')b.onclick=showOrderBlank;});}
|
||
|
||
// ---------------- Promo statistics ----------------
|
||
function renderPromoStats(){
|
||
const view=$('stats');if(!view)return;let root=$('sunPromoStats');if(!root){root=document.createElement('section');root.id='sunPromoStats';root.className='sun-promo-stats';view.appendChild(root)}
|
||
const groups=new Map();(orders||[]).filter(o=>o.status!=='Отменён'&&String(o.promoCode||'').trim()).forEach(o=>{const code=String(o.promoCode).trim(),p=pricing(o),key=code.toLowerCase(),client=(String(o.phone||'').replace(/\D/g,'')||String(o.contact||'').trim().toLowerCase()||`order:${o.id}`);if(!groups.has(key))groups.set(key,{code,orders:0,clients:new Set(),sales:0,discount:0});const g=groups.get(key);g.orders++;g.clients.add(client);g.sales+=p.total;g.discount+=p.promo;});const list=[...groups.values()].sort((a,b)=>b.sales-a.sales);
|
||
root.innerHTML=`<h2>Промокоды</h2><p class="hint" style="margin:0">Отслеживание заказов, клиентов, продаж и предоставленной скидки по промокодам.</p>${list.length?`<div style="overflow:auto"><table><thead><tr><th>Промокод</th><th class="num">Заказов</th><th class="num">Клиентов</th><th class="num">Продажи</th><th class="num">Скидка</th></tr></thead><tbody>${list.map(g=>`<tr><td><b>${esc(g.code)}</b></td><td class="num">${g.orders}</td><td class="num">${g.clients.size}</td><td class="num">${money(g.sales)}</td><td class="num">${money(g.discount)}</td></tr>`).join('')}</tbody></table></div>`:'<p class="empty">Заказов с промокодами пока нет.</p>'}`;
|
||
}
|
||
|
||
// ---------------- Lifecycle wrappers ----------------
|
||
const previousSave=window.saveOrder;
|
||
if(typeof previousSave==='function')window.saveOrder=function(...args){captureBuyerFromFields();captureDiscounts();captureDelivery();const result=previousSave.apply(this,args);renderPromoStats();return result};
|
||
const previousOpen=window.openOrder;
|
||
if(typeof previousOpen==='function')window.openOrder=function(...args){const r=previousOpen.apply(this,args);setTimeout(()=>{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');patchDocButtons();renderCatalogV5();},0);return r};
|
||
const previousReset=window.resetDraft;
|
||
if(typeof previousReset==='function')window.resetDraft=function(...args){const r=previousReset.apply(this,args);draft.buyerRequisites=buyerDefaults();draft.manualDiscountValue=0;draft.manualDiscountType='percent';draft.promoId='';draft.promoCode='';draft.promoDiscountValue=0;draft.promoDiscountType='percent';draft.promoStartDate='';draft.promoEndDate='';draft.deliveryCost=0;delete draft.qrImage;setTimeout(()=>{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');renderCatalogV5();},0);return r};
|
||
const previousStats=window.renderStats;
|
||
window.renderStats=function(...args){const r=typeof previousStats==='function'?previousStats.apply(this,args):undefined;setTimeout(renderPromoStats,0);return r};
|
||
|
||
function onSettings(){setTimeout(()=>{simplifySellerSettings();ensureDefaultQrSetting();ensureBackupFileCard();},80);setTimeout(()=>{simplifySellerSettings();ensureDefaultQrSetting();ensureBackupFileCard();},250)}
|
||
document.addEventListener('click',e=>{const nav=e.target.closest('header nav button');if(nav&&String(nav.dataset.navLabel||nav.textContent||'').trim()==='Настройки')onSettings();},true);
|
||
(()=>{let queued=false;const run=()=>{if(queued)return;queued=true;setTimeout(()=>{queued=false;if($('sunReceiptSettingsCard'))simplifySellerSettings();patchDocButtons();},50);};[$('new'),$('enterprise-settings')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
||
|
||
// Initial install. Populate the new piece-count field without overwriting manually edited prices/weights.
|
||
try{let changed=false;(Array.isArray(boxes)?boxes:[]).forEach(item=>{if(![0,5].includes(Number(item?.category||0)))return;const before=JSON.stringify([item.pieces,item.ingredients,item.composition]);normalizeBoxItem(item,{cleanup:true});if(before!==JSON.stringify([item.pieces,item.ingredients,item.composition]))changed=true;});if(changed)persist();}catch(err){console.warn('v17.5 catalog normalization skipped',err)}
|
||
ensureCategoryModal();renderCategoryTabs();syncEditorCategoryOptions();ensureCatalogTools();ensureBoxWeightEditor();ensureBoxPiecesEditor();ensureRequisitesTab();ensureDiscountUI();syncBuyerFields();syncDiscountFields();syncDeliveryField();patchDocButtons();renderCatalogV5();renderPromoStats();if($('enterprise-settings')?.classList.contains('on'))onSettings();
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: client-offer-settings.js ===== */
|
||
/* Sun Catering: editable client-offer content and metric settings */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunClientOfferSettingsV1)return;
|
||
window.__sunClientOfferSettingsV1=true;
|
||
|
||
const KEY='sunClientOfferSettingsV1';
|
||
const DEFAULTS={
|
||
version:3,
|
||
metricMode:'weight',
|
||
showAmountPerGuest:true,
|
||
showBoxCount:true,
|
||
showPriceBreakdown:true,
|
||
showDiscountBadge:true,
|
||
showGallery:true,
|
||
showControl:true,
|
||
showExtras:true,
|
||
showFinalPhoto:true,
|
||
showClientName:true,
|
||
showDate:true,
|
||
showGuests:true,
|
||
showFooterNote:true,
|
||
texts:{
|
||
proposalLabel:'Индивидуальное предложение',
|
||
menuTitle:'Меню',
|
||
summaryTitle:'Итого',
|
||
totalPriceTitle:'Итоговая стоимость',
|
||
heroNote:'Фирменные боксы с закусками — готово к подаче на стол.',
|
||
salesTitle:'Почему с нами удобно',
|
||
salesNote:'Меню составлено специально под ваше событие. Закуски приезжают в фирменных боксах, их удобно поставить на стол и подать без сложной сервировки. Мы готовим заказ в день доставки, используем свежие ингредиенты и контролируем детали до передачи заказа.',
|
||
finalGalleryTitle:'Как это выглядит на вашем столе',
|
||
weightMetricLabel:'Вес еды на гостя',
|
||
piecesMetricLabel:'Канапе на гостя',
|
||
amountPerGuestLabel:'Сумма на гостя',
|
||
boxCountLabel:'Количество боксов',
|
||
pricingTitle:'Расчёт стоимости',
|
||
priceItemsLabel:'Стоимость позиций',
|
||
discountLabel:'Скидка',
|
||
promoLabel:'Промокод',
|
||
menuAfterDiscountLabel:'Меню после скидок',
|
||
deliveryLabel:'Доставка',
|
||
totalLabel:'Итого',
|
||
controlTitle:'Всё под контролем команды Солнца',
|
||
extrasTitle:'Дополнительно к заказу рекомендуем',
|
||
footerNote:'Доставка считается отдельно от меню и не участвует в скидках.'
|
||
},
|
||
controlLines:[
|
||
'Фирменная упаковка',
|
||
'Уточняем детали доставки',
|
||
'Стоимость заказа не изменится',
|
||
'Готовим закуски в день мероприятия',
|
||
'Контролируем заказ до момента передачи',
|
||
'Привозим заказ вовремя и передаём вам лично',
|
||
'Всё организованно! Вы просто встречаете гостей!'
|
||
],
|
||
extraServices:['Напитки','Десерты','Фрукты','Одноразовая посуда']
|
||
};
|
||
const clone=v=>JSON.parse(JSON.stringify(v));
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const bool=(v,d)=>typeof v==='boolean'?v:d;
|
||
const lines=(value,fallback)=>{
|
||
const arr=Array.isArray(value)?value:String(value??'').split(/\r?\n/);
|
||
const clean=arr.map(x=>String(x||'').trim()).filter(Boolean).slice(0,20);
|
||
return clean.length?clean:[...fallback];
|
||
};
|
||
function normalize(raw={}){
|
||
const d=clone(DEFAULTS),t=raw&&typeof raw==='object'?raw:{};
|
||
d.metricMode=t.metricMode==='pieces'?'pieces':'weight';
|
||
['showAmountPerGuest','showBoxCount','showPriceBreakdown','showDiscountBadge','showGallery','showControl','showExtras','showFinalPhoto','showClientName','showDate','showGuests','showFooterNote'].forEach(k=>d[k]=bool(t[k],d[k]));
|
||
if(t.texts&&typeof t.texts==='object')Object.keys(d.texts).forEach(k=>{if(t.texts[k]!=null)d.texts[k]=String(t.texts[k]).trim()||d.texts[k]});
|
||
// v3 separates the short first-screen note from the longer selling copy.
|
||
// Preserve any custom v2 hero text by moving it into the new selling block.
|
||
if(Number(t.version||0)<3&&t.texts&&typeof t.texts==='object'&&!String(t.texts.salesNote||'').trim()){
|
||
const legacyHero=String(t.texts.heroNote||'').trim();
|
||
if(legacyHero)d.texts.salesNote=legacyHero;
|
||
d.texts.heroNote=DEFAULTS.texts.heroNote;
|
||
}
|
||
d.controlLines=lines(t.controlLines,d.controlLines);
|
||
d.extraServices=lines(t.extraServices,d.extraServices);
|
||
// Since v17.5.17 table photos are automatic whitespace fillers, not an optional section.
|
||
d.showFinalPhoto=true;
|
||
return d;
|
||
}
|
||
function read(){try{return normalize(JSON.parse(localStorage.getItem(KEY)||'{}'))}catch(_){return clone(DEFAULTS)}}
|
||
let state=read();
|
||
function save(next){state=normalize(next);localStorage.setItem(KEY,JSON.stringify(state));window.dispatchEvent(new CustomEvent('sunclientoffersettingschange',{detail:clone(state)}));return clone(state)}
|
||
function reset(){return save(DEFAULTS)}
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-client-offer-settings-style';
|
||
style.textContent=`
|
||
.sun-client-offer-settings-card{grid-column:1/-1!important}
|
||
.sun-client-offer-settings-head{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap}
|
||
.sun-client-offer-settings-head>div:first-child{flex:1;min-width:250px}
|
||
.sun-client-offer-settings-card details{border:1px solid #e4e7ea;border-radius:14px;background:#fff;margin-top:10px;overflow:hidden}
|
||
.sun-client-offer-settings-card summary{cursor:pointer;padding:13px 15px;font-weight:800;color:#2a3138;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:10px}
|
||
.sun-client-offer-settings-card summary::-webkit-details-marker{display:none}
|
||
.sun-client-offer-settings-card summary:after{content:'+';font-size:20px;font-weight:500;color:#7a828a}.sun-client-offer-settings-card details[open] summary:after{content:'−'}
|
||
.sun-client-offer-settings-body{border-top:1px solid #eceff1;padding:14px 15px 16px}
|
||
.sun-client-offer-settings-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:12px}
|
||
.sun-client-offer-settings-grid label,.sun-client-offer-settings-body>label{display:grid;gap:6px;font-size:12px;color:#59636d;font-weight:700}
|
||
.sun-client-offer-settings-grid input,.sun-client-offer-settings-grid select,.sun-client-offer-settings-body textarea{width:100%;border:1px solid #d9dee3;border-radius:10px;background:#fff;color:#20262c;padding:10px 11px;font:inherit}
|
||
.sun-client-offer-settings-body textarea{min-height:135px;resize:vertical;line-height:1.5}
|
||
.sun-client-offer-switches{display:grid;grid-template-columns:repeat(3,minmax(170px,1fr));gap:9px}
|
||
.sun-client-offer-switch{display:flex!important;grid-template-columns:none!important;align-items:center;gap:9px!important;border:1px solid #e2e6e9;border-radius:11px;padding:10px 11px;color:#303941!important;background:#fafbfb}
|
||
.sun-client-offer-switch input{width:17px!important;height:17px;margin:0;accent-color:var(--sun-brand-sun,#FFC107)}
|
||
.sun-client-offer-settings-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;padding-top:14px;border-top:1px solid #eceff1}
|
||
.sun-client-offer-settings-actions .hint{margin:0 0 0 auto}
|
||
@media(max-width:760px){.sun-client-offer-settings-grid,.sun-client-offer-switches{grid-template-columns:1fr}.sun-client-offer-settings-actions .hint{width:100%;margin:0}}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
function html(){
|
||
const s=state,t=s.texts;
|
||
return `<div class="sun-client-offer-settings-head"><div><h2>Настройки предложения клиенту</h2><p class="hint">Тексты и состав блоков применяются ко всем шаблонам и к PDF. Внутренний номер заказа клиенту не показывается.</p></div></div>
|
||
<details open><summary>Показатели в блоке «Итого»</summary><div class="sun-client-offer-settings-body">
|
||
<div class="sun-client-offer-settings-grid"><label>Первый показатель<select data-offer-setting="metricMode"><option value="weight" ${s.metricMode==='weight'?'selected':''}>Вес еды на гостя</option><option value="pieces" ${s.metricMode==='pieces'?'selected':''}>Количество канапе на гостя</option></select></label><label>Подпись «Вес еды»<input data-offer-text="weightMetricLabel" value="${esc(t.weightMetricLabel)}"></label><label>Подпись «Канапе»<input data-offer-text="piecesMetricLabel" value="${esc(t.piecesMetricLabel)}"></label><label>Подпись «Сумма на гостя»<input data-offer-text="amountPerGuestLabel" value="${esc(t.amountPerGuestLabel)}"></label><label>Подпись «Количество боксов»<input data-offer-text="boxCountLabel" value="${esc(t.boxCountLabel)}"></label></div>
|
||
<p class="hint" style="margin-top:10px">Вес и количество боксов считаются только из раздела «Боксы». Напитки, посуда и дополнения в эти показатели не попадают. В составе предложения количество канапе/закусок каждого вида показывается итогом с учётом количества заказанных боксов.</p>
|
||
<div class="sun-client-offer-switches" style="margin-top:12px"><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showAmountPerGuest" ${s.showAmountPerGuest?'checked':''}>Сумма на гостя</label><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showBoxCount" ${s.showBoxCount?'checked':''}>Количество боксов</label><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showPriceBreakdown" ${s.showPriceBreakdown?'checked':''}>Расчёт стоимости</label><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showDiscountBadge" ${s.showDiscountBadge?'checked':''}>Значок скидки</label><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showGallery" ${s.showGallery?'checked':''}>Фотогалерея</label></div>
|
||
</div></details>
|
||
<details><summary>Основные тексты</summary><div class="sun-client-offer-settings-body"><div class="sun-client-offer-settings-grid">
|
||
<label>Метка предложения<input data-offer-text="proposalLabel" value="${esc(t.proposalLabel)}"></label>
|
||
<label>Заголовок меню<input data-offer-text="menuTitle" value="${esc(t.menuTitle)}"></label>
|
||
<label>Заголовок итогов<input data-offer-text="summaryTitle" value="${esc(t.summaryTitle)}"></label>
|
||
<label>Подпись итоговой стоимости<input data-offer-text="totalPriceTitle" value="${esc(t.totalPriceTitle)}"></label>
|
||
</div><label style="margin-top:12px">Короткий текст первого экрана (2–3 строки)<textarea data-offer-text="heroNote">${esc(t.heroNote)}</textarea></label><label style="margin-top:12px">Нижнее примечание<textarea data-offer-text="footerNote">${esc(t.footerNote)}</textarea></label></div></details>
|
||
<details><summary>Продающий блок после первого экрана</summary><div class="sun-client-offer-settings-body"><div class="sun-client-offer-settings-grid"><label>Заголовок блока<input data-offer-text="salesTitle" value="${esc(t.salesTitle)}"></label><label>Заголовок финальной фотогалереи<input data-offer-text="finalGalleryTitle" value="${esc(t.finalGalleryTitle)}"></label></div><label style="margin-top:12px">Продающий текст<textarea data-offer-text="salesNote">${esc(t.salesNote)}</textarea></label><p class="hint" style="margin-top:9px">Текст не обрезается: блок увеличивается и при необходимости переносится на следующую страницу PDF.</p></div></details>
|
||
<details><summary>Тексты расчёта стоимости</summary><div class="sun-client-offer-settings-body"><div class="sun-client-offer-settings-grid">
|
||
<label>Заголовок блока<input data-offer-text="pricingTitle" value="${esc(t.pricingTitle)}"></label>
|
||
<label>Стоимость позиций<input data-offer-text="priceItemsLabel" value="${esc(t.priceItemsLabel)}"></label>
|
||
<label>Скидка<input data-offer-text="discountLabel" value="${esc(t.discountLabel)}"></label>
|
||
<label>Промокод<input data-offer-text="promoLabel" value="${esc(t.promoLabel)}"></label>
|
||
<label>Меню после скидок<input data-offer-text="menuAfterDiscountLabel" value="${esc(t.menuAfterDiscountLabel)}"></label>
|
||
<label>Доставка<input data-offer-text="deliveryLabel" value="${esc(t.deliveryLabel)}"></label>
|
||
<label>Итого<input data-offer-text="totalLabel" value="${esc(t.totalLabel)}"></label>
|
||
</div></div></details>
|
||
<details><summary>Блок «Всё под контролем»</summary><div class="sun-client-offer-settings-body"><div class="sun-client-offer-settings-grid"><label>Заголовок<input data-offer-text="controlTitle" value="${esc(t.controlTitle)}"></label><label class="sun-client-offer-switch" style="align-self:end"><input type="checkbox" data-offer-setting="showControl" ${s.showControl?'checked':''}>Показывать блок</label></div><label style="margin-top:12px">Пункты — каждый с новой строки<textarea data-offer-list="controlLines">${esc(s.controlLines.join('\n'))}</textarea></label></div></details>
|
||
<details><summary>Дополнительно к заказу</summary><div class="sun-client-offer-settings-body"><div class="sun-client-offer-settings-grid"><label>Заголовок<input data-offer-text="extrasTitle" value="${esc(t.extrasTitle)}"></label><label class="sun-client-offer-switch" style="align-self:end"><input type="checkbox" data-offer-setting="showExtras" ${s.showExtras?'checked':''}>Показывать блок</label></div><label style="margin-top:12px">Рекомендации — каждая с новой строки<textarea data-offer-list="extraServices">${esc(s.extraServices.join('\n'))}</textarea></label></div></details>
|
||
<details><summary>Что показывать клиенту</summary><div class="sun-client-offer-settings-body"><div class="sun-client-offer-switches"><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showClientName" ${s.showClientName?'checked':''}>Имя клиента</label><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showDate" ${s.showDate?'checked':''}>Дата мероприятия</label><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showGuests" ${s.showGuests?'checked':''}>Количество гостей</label><span class="sun-client-offer-switch" style="cursor:default">Фото на столе: авто, только в свободное место</span><label class="sun-client-offer-switch"><input type="checkbox" data-offer-setting="showFooterNote" ${s.showFooterNote?'checked':''}>Нижнее примечание</label></div></div></details>
|
||
<div class="sun-client-offer-settings-actions"><button type="button" class="primary" data-offer-settings-save>Сохранить настройки предложения</button><button type="button" class="outline" data-offer-settings-reset>Вернуть стандартные тексты</button><p class="hint">Изменения применятся к следующему открытию предложения; открытое предложение обновится автоматически.</p></div>`;
|
||
}
|
||
function collect(card){
|
||
const next=clone(state);
|
||
card.querySelectorAll('[data-offer-setting]').forEach(el=>{const k=el.dataset.offerSetting;if(el.type==='checkbox')next[k]=el.checked;else next[k]=el.value});
|
||
next.texts={...next.texts};
|
||
card.querySelectorAll('[data-offer-text]').forEach(el=>{next.texts[el.dataset.offerText]=String(el.value||'').trim()});
|
||
card.querySelectorAll('[data-offer-list]').forEach(el=>{next[el.dataset.offerList]=String(el.value||'').split(/\r?\n/).map(x=>x.trim()).filter(Boolean)});
|
||
return normalize(next);
|
||
}
|
||
function bind(card){
|
||
card.querySelector('[data-offer-settings-save]')?.addEventListener('click',()=>{save(collect(card));card.innerHTML=html();bind(card);try{window.SunEnterprise?.toast?.('Настройки предложения сохранены.')}catch(_){}});
|
||
card.querySelector('[data-offer-settings-reset]')?.addEventListener('click',()=>{reset();card.innerHTML=html();bind(card);try{window.SunEnterprise?.toast?.('Стандартные настройки предложения восстановлены.')}catch(_){}});
|
||
}
|
||
function ensureCard(){
|
||
const view=document.getElementById('enterprise-settings');if(!view)return;
|
||
const grid=view.querySelector('.enterprise-grid');if(!grid)return;
|
||
let card=document.getElementById('sunClientOfferSettingsCard');
|
||
if(card&&grid.contains(card))return;
|
||
card=document.createElement('section');card.id='sunClientOfferSettingsCard';card.className='enterprise-card wide sun-client-offer-settings-card';card.innerHTML=html();
|
||
const templates=document.getElementById('sunOfferTemplateSettingsCard');
|
||
if(templates&&templates.parentElement===grid)templates.insertAdjacentElement('afterend',card);else{
|
||
const brand=document.getElementById('sunBrandThemeSettingsCard');
|
||
if(brand&&brand.parentElement===grid)brand.insertAdjacentElement('afterend',card);else grid.appendChild(card);
|
||
}
|
||
bind(card);
|
||
}
|
||
function boot(){
|
||
ensureCard();
|
||
const settings=document.getElementById('enterprise-settings');if(settings)new MutationObserver(()=>queueMicrotask(ensureCard)).observe(settings,{childList:true,subtree:true});
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const label=(b.dataset.navLabel||b.textContent||'').trim();if(label==='Настройки')setTimeout(ensureCard,35)},true);
|
||
window.addEventListener('storage',e=>{if(e.key===KEY){state=read();setTimeout(()=>{const card=document.getElementById('sunClientOfferSettingsCard');if(card){card.innerHTML=html();bind(card)}},20);window.dispatchEvent(new CustomEvent('sunclientoffersettingschange',{detail:clone(state)}));}});
|
||
window.addEventListener('sun:cloud-state-applied',()=>{state=read();setTimeout(()=>{const card=document.getElementById('sunClientOfferSettingsCard');if(card){card.innerHTML=html();bind(card)}},30)});
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
window.SunClientOfferSettings={key:KEY,defaults:()=>clone(DEFAULTS),get:()=>clone(state),set:save,reset};
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: client-offer.js ===== */
|
||
(function(){
|
||
'use strict';
|
||
if(window.__sunClientOfferV2)return; window.__sunClientOfferV2=true;
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const money=v=>`${Math.round(Number(v||0)).toLocaleString('ru-RU')} ₽`;
|
||
const num=v=>Math.max(0,Number(v||0));
|
||
const toast=(m,t='success')=>{try{return window.SunEnterprise?.toast?.(m,t)}catch(_){} if(t==='error'||t==='warn')console.warn(m);};
|
||
const EMBEDDED_LOGO='data:image/png;base64,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';
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-client-offer-style';
|
||
style.textContent=`
|
||
#sunClientOfferModal{z-index:100050}
|
||
#sunClientOfferModal .dialog{width:min(1180px,calc(100vw - 28px));max-height:94vh;padding:0;overflow:hidden;border-radius:16px;background:#fffaf2}
|
||
.sun-offer-modal-head{display:flex;align-items:center;gap:10px;padding:13px 15px;background:#fffdf9;border-bottom:1px solid #e8d9bf;flex-wrap:wrap}
|
||
.sun-offer-modal-head h2{margin:0;color:#28533a;font-size:19px}.sun-offer-modal-head .hint{margin:0;color:#73808a;font-size:12px}
|
||
.sun-offer-modal-head .spacer{flex:1}.sun-offer-modal-actions{display:flex;gap:7px;flex-wrap:wrap}.sun-offer-modal-actions button{min-height:36px}
|
||
.sun-offer-editor{display:none;padding:12px 15px;background:#fff;border-bottom:1px solid #e8d9bf}.sun-offer-editor.on{display:block}.sun-offer-editor-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px}.sun-offer-editor label{display:grid;gap:4px;font-size:10px;font-weight:700;color:#607068}.sun-offer-editor label.wide{grid-column:span 2}.sun-offer-editor input,.sun-offer-editor textarea{width:100%;box-sizing:border-box;border:1px solid #d9d2c7;border-radius:9px;padding:8px 9px;background:#fff;font:12px/1.35 Arial;color:#1d2b24}.sun-offer-editor textarea{min-height:62px;resize:vertical}.sun-offer-editor-note{margin:8px 0 0;color:#778178;font-size:10px}.sun-offer-editor-gallery{display:grid;grid-template-columns:minmax(0,1fr) 190px;gap:12px;align-items:center;border:1px solid #e7dfd2;border-radius:10px;padding:9px;background:#fffdf9}.sun-offer-editor-gallery b{display:block;font-size:11px;color:#425248}.sun-offer-editor-gallery small{display:block;margin-top:4px;font-size:10px;line-height:1.4;color:#7b827d}.sun-offer-editor-gallery-images{display:grid;grid-template-columns:1fr 1fr;gap:5px}.sun-offer-editor-gallery-images img{width:100%;height:70px;object-fit:cover;border-radius:7px;border:1px solid #e5ded3}
|
||
.sun-offer-frame-wrap{height:min(78vh,900px);background:#eee9e0;padding:12px;overflow:auto}
|
||
#sunClientOfferPreview{display:grid;gap:14px;justify-items:center;min-height:100%}.sun-offer-page-canvas{display:block;width:min(100%,794px);height:auto;background:#fff;box-shadow:0 5px 24px #4a38272b}.sun-offer-rendering{align-self:center;justify-self:center;margin:70px 20px;padding:14px 18px;border-radius:12px;background:#fff;color:#526159;font-size:12px;box-shadow:0 4px 18px #00000012}
|
||
.sun-offer-stale{display:none;padding:7px 12px;background:#fff6df;color:#8b6213;border-radius:8px;font-size:12px}.sun-offer-stale.on{display:block}
|
||
#new .actions #sunClientOfferButton{background:#fff8e9;border-color:#c99b47;color:#28533a}
|
||
@media(max-width:900px){.sun-offer-editor-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||
@media(max-width:700px){.sun-offer-editor-gallery{grid-template-columns:1fr}.sun-offer-editor-gallery-images img{height:95px}#sunClientOfferModal{padding:0}#sunClientOfferModal .dialog{width:100vw;height:100vh;max-height:none;border-radius:0}.sun-offer-frame-wrap{height:calc(100vh - 112px);padding:5px}.sun-offer-modal-head{padding:9px}.sun-offer-modal-head h2{font-size:16px}.sun-offer-modal-head .hint{display:none}.sun-offer-modal-actions{width:100%;display:grid;grid-template-columns:1.15fr 1fr 1fr 1fr}.sun-offer-modal-actions button{padding:8px 4px;font-size:10px}.sun-offer-editor{padding:9px}.sun-offer-editor-grid{grid-template-columns:1fr}.sun-offer-editor label.wide{grid-column:auto}}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
const getBoxes=()=>{try{if(typeof boxes!=='undefined'&&Array.isArray(boxes))return boxes}catch(_){}try{const v=JSON.parse(localStorage.getItem('sunBoxes')||'[]');return Array.isArray(v)?v:[]}catch(_){return[]}};
|
||
const getLegacyBoxes=()=>Array.isArray(window.SUN_LEGACY_CATALOG_V175)?window.SUN_LEGACY_CATALOG_V175:[];
|
||
const normalizedCatalogName=value=>String(value||'').toLowerCase().replace(/ё/g,'е').replace(/[^a-zа-я0-9]+/gi,' ').trim().replace(/\s+/g,' ');
|
||
function resolveCatalogLine(line,all=getBoxes()){
|
||
const id=String(line?.id??'');
|
||
let item=(all||[]).find(x=>String(x.id)===id);
|
||
if(item)return item;
|
||
const legacy=getLegacyBoxes();
|
||
item=legacy.find(x=>String(x.id)===id);
|
||
if(item)return item;
|
||
const name=normalizedCatalogName(line?.name||line?.catalogName||'');
|
||
if(name){
|
||
item=(all||[]).find(x=>normalizedCatalogName(x?.name)===name)||legacy.find(x=>normalizedCatalogName(x?.name)===name);
|
||
if(item)return item;
|
||
}
|
||
return null;
|
||
}
|
||
const getOrders=()=>{try{if(typeof orders!=='undefined'&&Array.isArray(orders))return orders}catch(_){}try{const v=JSON.parse(localStorage.getItem('sunOrders')||'[]');return Array.isArray(v)?v:[]}catch(_){return[]}};
|
||
const getDraft=()=>{try{return typeof draft!=='undefined'?draft:null}catch(_){return null}};
|
||
const catName=id=>{try{return window.sunCatalogCategoryName?.(id)||({0:'Боксы',1:'Посуда и упаковка',2:'Дополнительно',3:'Напитки',4:'Доставка',5:'Премиум'}[Number(id)]||'Каталог')}catch(_){return 'Каталог'}};
|
||
|
||
function linePrice(line,item){const p=Number(line?.price);return Number.isFinite(p)&&p>=0?p:num(item?.price)}
|
||
function discountAmount(value,type,base){const n=num(value);return Math.min(base,type==='amount'?n:base*Math.min(100,n)/100)}
|
||
function priceCalc(order){
|
||
const all=getBoxes();
|
||
const base=(order?.lines||[]).reduce((sum,line)=>{const item=resolveCatalogLine(line,all);return sum+linePrice(line,item)*num(line.qty)},0);
|
||
const manual=discountAmount(order?.manualDiscountValue,order?.manualDiscountType,base);
|
||
const afterManual=Math.max(0,base-manual);
|
||
const promo=discountAmount(order?.promoDiscountValue,order?.promoDiscountType,afterManual);
|
||
const itemsTotal=Math.max(0,afterManual-promo);
|
||
const delivery=num(order?.deliveryCost);
|
||
return {base,manual,promo,itemsTotal,delivery,total:itemsTotal+delivery,discount:manual+promo};
|
||
}
|
||
function parseMeasure(text){
|
||
const s=String(text||'').toLowerCase().replace(/,/g,'.');
|
||
const m=s.match(/(\d+(?:\.\d+)?)\s*(кг|kg|мл|ml|л|l|г|g)(?:\s|$|[^a-zа-яё])/iu)||s.match(/(\d+(?:\.\d+)?)\s*(кг|kg|мл|ml|л|l|г|g)$/iu);
|
||
if(!m)return null;const value=Number(m[1]),u=m[2];if(!Number.isFinite(value))return null;
|
||
if(u==='кг'||u==='kg')return {kind:'food',value:value*1000,unit:'г'};
|
||
if(u==='г'||u==='g')return {kind:'food',value,unit:'г'};
|
||
if(u==='л'||u==='l')return {kind:'drink',value:value*1000,unit:'мл'};
|
||
if(u==='мл'||u==='ml')return {kind:'drink',value,unit:'мл'};
|
||
return null;
|
||
}
|
||
function ingredientWeight(row){if(!Array.isArray(row))return 0;const direct=Math.max(0,num(row[3]));if(direct)return direct;const m=String(row[0]||'').match(/[—–-]\s*(\d+(?:[.,]\d+)?)\s*г(?:\s*\/\s*шт\.?)?\s*$/i);return m?Math.max(0,num(String(m[1]).replace(',','.'))):0}
|
||
function cleanIngredientName(name){return String(name||'').replace(/\s*[—–-]\s*\d+(?:[.,]\d+)?\s*г(?:\s*\/\s*шт\.?)?\s*$/i,'').trim()}
|
||
function ingredientLine(row){if(!Array.isArray(row)||!String(row[0]||'').trim())return'';const name=cleanIngredientName(row[0]),qty=Math.max(0,num(row[1])),unit=String(row[2]||'').trim(),w=ingredientWeight(row);if(/^поз\.?$/i.test(unit)&&qty<=1)return `${name}${w?` — ${w.toLocaleString('ru-RU')} г/шт.`:''}`;const shown=Number.isInteger(qty)?String(qty):qty.toLocaleString('ru-RU',{maximumFractionDigits:2});return `${shown} ${unit} ${name}${w?` — ${w.toLocaleString('ru-RU')} г/шт.`:''}`.replace(/\s+/g,' ').trim()}
|
||
function composition(item){
|
||
// Client-facing composition is independent from kitchen TTK ingredients.
|
||
if(Array.isArray(item?.composition)&&item.composition.length)return item.composition.map(x=>String(x||'').trim()).filter(Boolean);
|
||
if(Array.isArray(item?.ingredients)&&item.ingredients.some(x=>Array.isArray(x)&&x[0]))return item.ingredients.map(ingredientLine).filter(Boolean);
|
||
return [];
|
||
}
|
||
function orderSignature(order){
|
||
const clean={id:String(order?.id??''),event:order?.event||'',date:order?.date||'',time:order?.time||'',contact:order?.contact||'',guestsCount:num(order?.guestsCount),deliveryCost:num(order?.deliveryCost),manualDiscountValue:num(order?.manualDiscountValue),manualDiscountType:order?.manualDiscountType||'percent',promoId:order?.promoId||'',promoCode:order?.promoCode||'',promoDiscountValue:num(order?.promoDiscountValue),promoDiscountType:order?.promoDiscountType||'percent',lines:(order?.lines||[]).map(l=>({id:String(l.id),qty:num(l.qty),price:Number.isFinite(Number(l.price))?Number(l.price):null}))};
|
||
return JSON.stringify(clean);
|
||
}
|
||
function currentSavedOrder(id){return getOrders().find(o=>String(o.id)===String(id))||null}
|
||
function currentOfferSourceOrder(id,preferDraft=false){
|
||
const saved=currentSavedOrder(id),d=getDraft();
|
||
if(preferDraft&&d&&String(d.id)===String(id))return d;
|
||
return saved;
|
||
}
|
||
function snapshotIsStale(order,snapshot){
|
||
if(!snapshot||Number(snapshot.version||0)<7||snapshot.sourceSignature!==orderSignature(order))return true;
|
||
const all=getBoxes();
|
||
return (snapshot.items||[]).some(item=>{
|
||
const missingName=!String(item?.name||'').trim()||String(item?.name||'').trim()==='Позиция';
|
||
const missingPhoto=!String(item?.photo||'').trim();
|
||
return (missingName||missingPhoto)&&Boolean(resolveCatalogLine(item,all));
|
||
});
|
||
}
|
||
function sectionTitle(item){const s=String(item?.catalogSection||'').trim();return s||String(item?.categoryName||'Меню')}
|
||
function canapePieces(item){
|
||
// v17.5.1: any food box may contain countable ready-to-serve pieces, not only sections named "canape".
|
||
if(![0,5].includes(Number(item?.category??item?.categoryId??0)))return 0;
|
||
const explicit=Math.max(0,num(item?.pieces));
|
||
if(explicit>0)return explicit;
|
||
let total=0;
|
||
if(Array.isArray(item?.ingredients)){
|
||
for(const row of item.ingredients){
|
||
if(!Array.isArray(row))continue;
|
||
const value=num(row[1]),unit=String(row[2]||'').toLowerCase().replace(/\s+/g,'');
|
||
if(/^(шт\.?|пор\.?|кус\.?|шарик(?:а|ов|и)?|креманк(?:а|и|е)?)$/i.test(unit))total+=value;
|
||
}
|
||
}
|
||
if(total>0)return total;
|
||
for(const text of Array.isArray(item?.composition)?item.composition:[]){
|
||
const re=/(\d+(?:[.,]\d+)?)\s*(?:шт\.?|пор\.?|кус\.?)/ig;let m;while((m=re.exec(String(text||''))))total+=num(String(m[1]).replace(',','.'));
|
||
}
|
||
return total;
|
||
}
|
||
|
||
function multiplyLeadingPieces(text,qty){
|
||
const raw=String(text||'').trim(),q=Math.max(0,num(qty));if(!raw||q<=0)return raw;
|
||
return raw.replace(/^(\d+(?:[.,]\d+)?)\s*(шт\.?|пор\.?|кус\.?)\s+/i,(m,n,u)=>{
|
||
const total=Number(String(n).replace(',','.'))*q;
|
||
const shown=Number.isInteger(total)?String(total):total.toLocaleString('ru-RU',{maximumFractionDigits:2});
|
||
return `${shown} ${u} `;
|
||
});
|
||
}
|
||
function totalComposition(item,qty,categoryId){
|
||
const list=composition(item);return [0,5].includes(Number(categoryId))?list.map(x=>multiplyLeadingPieces(x,qty)):list;
|
||
}
|
||
function offerComposition(item){
|
||
const list=Array.isArray(item?.compositionTotal)&&item.compositionTotal.length?item.compositionTotal:(Array.isArray(item?.composition)?item.composition:[]);
|
||
return list.map(x=>String(x||'').trim()).filter(Boolean);
|
||
}
|
||
function roundUp(value,step=1){const n=Math.max(0,Number(value||0)),s=Math.max(1,Number(step||1));return Math.ceil((n-1e-9)/s)*s}
|
||
function guestRangeLabel(raw){
|
||
const value=String(raw||'').trim().replace(/(\d)\s*[-–—]\s*(\d)/g,'$1–$2');
|
||
if(!value)return'';return /(?:гост|чел)/i.test(value)?value:`${value} гостей`;
|
||
}
|
||
function boxQuantityLabel(qty){const n=Math.max(0,Math.round(num(qty)));return n?`${n} ${plural(n,'бокс','бокса','боксов')}`:''}
|
||
function totalMeasureLabel(item){
|
||
const raw=String(item?.weight||'').trim(),qty=Math.max(0,num(item?.qty));if(!raw||!qty)return'';
|
||
const measure=parseMeasure(raw);if(!measure)return qty===1?`Вес бокса: ${raw}`:`Вес бокса: ${raw} × ${qty}`;
|
||
const total=measure.value*qty,shown=(Number.isInteger(total)?total:Math.round(total*100)/100).toLocaleString('ru-RU',{maximumFractionDigits:2});
|
||
return `Общий ${measure.kind==='drink'?'объём':'вес'}: ${shown} ${measure.unit}`;
|
||
}
|
||
function weightLabel(item){
|
||
const raw=String(item?.weight||'').trim(),isBox=[0,5].includes(Number(item?.categoryId??item?.category??0)),pieces=isBox?Math.max(0,num(item?.pieces)):0,qty=Math.max(0,num(item?.qty));
|
||
if(isBox){
|
||
const parts=[];
|
||
const totalWeight=totalMeasureLabel(item);if(totalWeight)parts.push(totalWeight);else if(raw)parts.push(`Общий вес: ${raw}`);
|
||
if(pieces&&qty)parts.push(`Всего: ${(pieces*qty).toLocaleString('ru-RU',{maximumFractionDigits:2})} шт.`);
|
||
return parts.join(' · ')
|
||
}
|
||
return raw?`Вес / объём: ${raw}`:'';
|
||
}
|
||
|
||
async function createSnapshot(order){
|
||
const all=getBoxes(),p=priceCalc(order),guests=num(order?.guestsCount)||num($('eventGuests')?.value),selected=new Set((order?.lines||[]).map(l=>String(l.id)));
|
||
let foodGrams=0,drinkMl=0,foodPieces=0;
|
||
const items=(order?.lines||[]).map(line=>{
|
||
const item=resolveCatalogLine(line,all)||{};const qty=num(line.qty),price=linePrice(line,item),measure=parseMeasure(item.weight),rawItemCategory=item.category,rawLineCategory=line.category,itemCategory=rawItemCategory!==undefined&&rawItemCategory!==null&&rawItemCategory!==''?Number(rawItemCategory):NaN,lineCategory=rawLineCategory!==undefined&&rawLineCategory!==null&&rawLineCategory!==''?Number(rawLineCategory):NaN,categoryId=Number.isFinite(itemCategory)?itemCategory:(Number.isFinite(lineCategory)?lineCategory:-1),pieces=canapePieces(item),comp=composition(item),compositionTotal=[0,5].includes(Number(categoryId))?comp.map(x=>multiplyLeadingPieces(x,qty)):comp;
|
||
if(measure){if(measure.kind==='drink'||categoryId===3)drinkMl+=measure.value*qty;else if([0,5].includes(categoryId))foodGrams+=measure.value*qty;}
|
||
if([0,5].includes(categoryId)&&pieces>0)foodPieces+=pieces*qty;
|
||
return {id:String(item.id??line.id??''),name:String(item.name||line.name||'Позиция'),categoryId,categoryName:catName(categoryId),catalogSection:String(item.catalogSection||''),qty,unitPrice:price,sum:price*qty,weight:String(item.weight||''),boxGuests:String(item.guests||''),photo:String(item.photo||''),composition:comp,compositionTotal,pieces};
|
||
}).filter(x=>x.qty>0);
|
||
const extras=all.filter(item=>[1,2,3].includes(Number(item.category))&&!selected.has(String(item.id))&&!item.hidden).slice(0,6).map(item=>({name:String(item.name||''),categoryName:catName(item.category)})).filter(x=>x.name);
|
||
return {version:8,createdAt:new Date().toISOString(),orderId:order?.id||'',sourceSignature:orderSignature(order),event:String(order?.event||''),date:String(order?.date||''),time:String(order?.time||''),client:String(order?.contact||''),guests,foodGrams,foodPieces,drinkMl,pricing:p,promoCode:String(order?.promoCode||''),items,extras,offerOverrides:structuredClone(order?.clientOfferSnapshot?.offerOverrides||{})};
|
||
}
|
||
function persistSnapshot(orderId,snapshot){
|
||
let list=getOrders(),i=list.findIndex(o=>String(o.id)===String(orderId));if(i<0)return false;
|
||
list[i].clientOfferSnapshot=snapshot;list[i].clientOfferUpdatedAt=snapshot.createdAt;
|
||
try{if(typeof orders!=='undefined'&&Array.isArray(orders)){const j=orders.findIndex(o=>String(o.id)===String(orderId));if(j>=0){orders[j].clientOfferSnapshot=snapshot;orders[j].clientOfferUpdatedAt=snapshot.createdAt;}}}catch(_){}
|
||
const d=getDraft();if(d&&String(d.id)===String(orderId)){d.clientOfferSnapshot=snapshot;d.clientOfferUpdatedAt=snapshot.createdAt;}
|
||
try{if(typeof persist==='function')persist();else localStorage.setItem('sunOrders',JSON.stringify(list));return true}catch(err){console.error(err);toast('Не удалось сохранить предложение: возможно, переполнено локальное хранилище.','error');return false}
|
||
}
|
||
|
||
function ensureModal(){
|
||
if($('sunClientOfferModal'))return;
|
||
const modal=document.createElement('div');modal.id='sunClientOfferModal';modal.className='modal';modal.innerHTML=`<div class="dialog"><div class="sun-offer-modal-head"><div><h2>Предложение клиенту</h2><p class="hint" id="sunOfferMeta"></p></div><div class="spacer"></div><div id="sunOfferStale" class="sun-offer-stale">Заказ изменён — предложение будет автоматически обновлено из текущих данных.</div><div class="sun-offer-modal-actions"><button class="primary" id="sunOfferPdf" type="button">Скачать PDF</button><button class="outline" id="sunOfferEdit" type="button">Редактировать</button><button class="outline" id="sunOfferRefresh" type="button">Обновить</button><button class="outline" id="sunOfferClose" type="button">Закрыть</button></div></div><section class="sun-offer-editor" id="sunOfferEditor"><div class="sun-offer-editor-grid"><label>Название мероприятия<input data-offer-data="event" type="text"></label><label>Имя клиента<input data-offer-data="client" type="text"></label><label>Дата<input data-offer-data="date" type="date"></label><label>Гостей<input data-offer-data="guests" type="number" min="0" step="1"></label><label>Метка предложения<input data-offer-text="proposalLabel" type="text"></label><label>Заголовок меню<input data-offer-text="menuTitle" type="text"></label><label class="wide">Короткий текст первого блока<textarea data-offer-text="heroNote"></textarea></label><label>Заголовок продающего блока<input data-offer-text="salesTitle" type="text"></label><label class="wide">Продающий текст<textarea data-offer-text="salesNote"></textarea></label><label>Заголовок итогов<input data-offer-text="summaryTitle" type="text"></label><label>Заголовок стоимости<input data-offer-text="totalPriceTitle" type="text"></label><label>Заголовок блока «Всё под контролем»<input data-offer-text="controlTitle" type="text"></label><label>Блок рекомендаций<input data-offer-text="extrasTitle" type="text"></label><label class="wide">Пункты блока «Всё под контролем» — каждый с новой строки<textarea data-offer-list="controlLines" rows="7"></textarea></label><label>Заголовок фото на столе<input data-offer-text="finalGalleryTitle" type="text"></label><label class="wide">Примечание внизу<textarea data-offer-text="footerNote"></textarea></label><div class="sun-offer-editor-gallery wide"><div><b>Фото «Как это выглядит на вашем столе»</b><small>В PDF автоматически попадёт 1 или 2 фото только если на последней странице есть свободное место.</small></div><div class="sun-offer-editor-gallery-images"><img src="offer-gallery/001.jpg" alt=""><img src="offer-gallery/002.jpg" alt=""></div></div></div><p class="sun-offer-editor-note">Изменения относятся только к этому предложению. Просмотр ниже — те же страницы, которые попадут в скачанный PDF.</p></section><div class="sun-offer-frame-wrap"><div id="sunClientOfferPreview" aria-label="Предпросмотр предложения"></div></div></div>`;document.body.appendChild(modal);
|
||
$('sunOfferClose').onclick=()=>window.closeModal?.('sunClientOfferModal');
|
||
$('sunOfferRefresh').onclick=refreshCurrentOffer;$('sunOfferPdf').onclick=downloadCurrentOfferPdf;$('sunOfferEdit').onclick=toggleOfferEditor;bindOfferEditor();
|
||
}
|
||
|
||
function dateText(raw){if(!raw)return'Дата уточняется';const d=new Date(`${raw}T00:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU',{day:'numeric',month:'long'})}
|
||
function perGuest(value,guests){return guests>0?value/guests:0}
|
||
function metricValue(value,unit,guests){if(!guests||!value)return'—';const per=value/guests,rounded=unit==='г'?roundUp(per,5):roundUp(per,1);return `${rounded.toLocaleString('ru-RU')} ${unit}`}
|
||
function itemPerGuest(item,guests){const measure=parseMeasure(item?.weight);if(!guests||!measure)return'—';const per=measure.value*num(item.qty)/guests,rounded=measure.kind==='drink'?roundUp(per,1):roundUp(per,5);return `${rounded.toLocaleString('ru-RU')} ${measure.kind==='drink'?'мл':'г'}`}
|
||
const EXTRA_SERVICES=[
|
||
['Напитки',''],
|
||
['Десерты',''],
|
||
['Фрукты',''],
|
||
['Одноразовая посуда','']
|
||
];
|
||
const DELIVERY_CONTROL=[
|
||
'Фирменная упаковка',
|
||
'Уточняем детали доставки',
|
||
'Стоимость заказа не изменится',
|
||
'Готовим закуски в день мероприятия',
|
||
'Контролируем заказ до момента передачи',
|
||
'Привозим заказ вовремя и передаём вам лично',
|
||
'Всё организованно! Вы просто встречаете гостей!'
|
||
];
|
||
const FINAL_GALLERY_PATHS=[...(Array.isArray(window.SUN_OFFER_GALLERY_DATA)?window.SUN_OFFER_GALLERY_DATA.filter(Boolean).slice(0,2):[])];if(FINAL_GALLERY_PATHS.length<2)FINAL_GALLERY_PATHS.push(...Array.from({length:2-FINAL_GALLERY_PATHS.length},(_,i)=>`offer-gallery/${String(FINAL_GALLERY_PATHS.length+i+1).padStart(3,'0')}.jpg`));
|
||
function plural(n,one,few,many){const x=Math.abs(Number(n||0))%100,y=x%10;return x>10&&x<20?many:y>1&&y<5?few:y===1?one:many}
|
||
|
||
const SAFE_PIXEL='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Zwv8AAAAASUVORK5CYII=';
|
||
const isSafeRasterDataUrl=src=>/^data:image\/(?!svg\+xml)[a-z0-9.+-]+;/i.test(String(src||''));
|
||
async function asDataUrlOne(src){
|
||
if(!src)return'';
|
||
const raw=String(src).trim();
|
||
if(isSafeRasterDataUrl(raw))return raw;
|
||
if(/^data:/i.test(raw))return'';
|
||
try{
|
||
const url=new URL(raw,document.baseURI||location.href);
|
||
if(!['http:','https:','blob:'].includes(url.protocol))return'';
|
||
const sameOrigin=url.protocol==='blob:'||url.origin===location.origin;
|
||
const res=await fetch(url,{mode:'cors',credentials:sameOrigin?'same-origin':'omit',cache:'force-cache'});
|
||
if(!res.ok)throw new Error(`HTTP ${res.status}`);
|
||
const blob=await res.blob();
|
||
if(!/^image\/(?!svg\+xml)[a-z0-9.+-]+$/i.test(String(blob.type||'')))throw new Error('Неподдерживаемый формат изображения');
|
||
const data=await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(String(r.result||''));r.onerror=()=>reject(r.error||new Error('FileReader'));r.readAsDataURL(blob)});
|
||
return isSafeRasterDataUrl(data)?data:'';
|
||
}catch(err){return''}
|
||
}
|
||
async function asDataUrl(src){
|
||
const list=window.SunCatalogPhoto?.candidates?.(src)||[src];
|
||
for(const candidate of list){const data=await asDataUrlOne(candidate);if(data)return data;}
|
||
console.warn('PDF image unavailable after all fallbacks:',src);
|
||
return'';
|
||
}
|
||
async function preparedSnapshot(snapshot){
|
||
const copy=structuredClone(snapshot),liveCatalog=getBoxes(),legacyCatalog=getLegacyBoxes();
|
||
// Preview uses normal image URLs. Expensive Base64 conversion is deferred until PDF download.
|
||
copy.logo=EMBEDDED_LOGO||'sun-logo.png';
|
||
copy.finalPhoto='';
|
||
copy.finalGallery=FINAL_GALLERY_PATHS.slice(0,2);
|
||
const dataOverrides=copy.offerOverrides?.data&&typeof copy.offerOverrides.data==='object'?copy.offerOverrides.data:{};
|
||
if(Object.prototype.hasOwnProperty.call(dataOverrides,'event'))copy.event=String(dataOverrides.event??'');
|
||
if(Object.prototype.hasOwnProperty.call(dataOverrides,'client'))copy.client=String(dataOverrides.client??'');
|
||
if(Object.prototype.hasOwnProperty.call(dataOverrides,'date'))copy.date=String(dataOverrides.date??'');
|
||
if(Object.prototype.hasOwnProperty.call(dataOverrides,'guests'))copy.guests=Math.max(0,num(dataOverrides.guests));
|
||
for(const item of copy.items){
|
||
// Quantity and price come from the order snapshot; catalog presentation data is always current.
|
||
const live=liveCatalog.find(x=>String(x.id)===String(item.id))||legacyCatalog.find(x=>String(x.id)===String(item.id));
|
||
if(live){
|
||
item.name=String(live.name||item.name||'Позиция');
|
||
if(Number.isFinite(Number(live.category)))item.categoryId=Number(live.category);
|
||
item.categoryName=catName(item.categoryId);
|
||
item.photo=String(live.photo||'');item.weight=String(live.weight||'');item.boxGuests=String(live.guests||'');item.catalogSection=String(live.catalogSection||item.catalogSection||'');
|
||
const comp=composition(live);item.composition=comp;item.compositionTotal=[0,5].includes(Number(item.categoryId))?comp.map(x=>multiplyLeadingPieces(x,item.qty)):comp;item.pieces=canapePieces(live);
|
||
}else item.compositionTotal=[0,5].includes(Number(item.categoryId))?(item.composition||[]).map(x=>multiplyLeadingPieces(x,item.qty)):(item.composition||[]);
|
||
item.photoData=item.photo||copy.logo;
|
||
}
|
||
copy.foodGrams=0;copy.drinkMl=0;copy.foodPieces=0;
|
||
for(const item of copy.items){const measure=parseMeasure(item.weight);if(measure){if(measure.kind==='drink'||Number(item.categoryId)===3)copy.drinkMl+=measure.value*num(item.qty);else if([0,5].includes(Number(item.categoryId)))copy.foodGrams+=measure.value*num(item.qty);}if([0,5].includes(Number(item.categoryId))&&num(item.pieces)>0)copy.foodPieces+=num(item.pieces)*num(item.qty)}
|
||
return copy;
|
||
}
|
||
async function preparedPdfSnapshot(prepared){
|
||
const copy=structuredClone(prepared),cache=new Map();
|
||
const resolve=src=>{const key=String(src||'');if(!key)return Promise.resolve('');if(!cache.has(key))cache.set(key,asDataUrl(key));return cache.get(key);};
|
||
copy.logo=(await resolve(copy.logo||'sun-logo.png'))||SAFE_PIXEL;
|
||
copy.finalGallery=(await Promise.all((copy.finalGallery||[]).map(resolve))).filter(Boolean);
|
||
await Promise.all((copy.items||[]).map(async item=>{item.photoData=(await resolve(item.photoData||item.photo||''))||copy.logo;}));
|
||
return copy;
|
||
}
|
||
|
||
const OFFER_TEMPLATE_KEY='sunOfferTemplateV1';
|
||
const OFFER_TEMPLATE_IDS=new Set(['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience','midnight-compact','emerald-gold','neon-emerald']);
|
||
function offerTemplateId(){
|
||
try{
|
||
const api=window.SunOfferTemplate?.get?.();
|
||
if(api?.id&&OFFER_TEMPLATE_IDS.has(api.id))return api.id;
|
||
const raw=JSON.parse(localStorage.getItem(OFFER_TEMPLATE_KEY)||'{}')||{};
|
||
if(raw.id&&OFFER_TEMPLATE_IDS.has(raw.id))return raw.id;
|
||
}catch(_){}
|
||
return 'light';
|
||
}
|
||
|
||
const CLIENT_OFFER_SETTINGS_KEY='sunClientOfferSettingsV1';
|
||
const CLIENT_OFFER_DEFAULTS={metricMode:'weight',showAmountPerGuest:true,showBoxCount:true,showPriceBreakdown:true,showDiscountBadge:true,showGallery:true,showControl:true,showExtras:true,showFinalPhoto:true,showClientName:true,showDate:true,showGuests:true,showFooterNote:true,texts:{proposalLabel:'Индивидуальное предложение',menuTitle:'Меню',summaryTitle:'Итого',totalPriceTitle:'Итоговая стоимость',heroNote:'Фирменные боксы с закусками — готово к подаче на стол.',salesTitle:'Почему с нами удобно',salesNote:'Меню составлено специально под ваше событие. Закуски приезжают в фирменных боксах, их удобно поставить на стол и подать без сложной сервировки. Мы готовим заказ в день доставки, используем свежие ингредиенты и контролируем детали до передачи заказа.',finalGalleryTitle:'Как это выглядит на вашем столе',weightMetricLabel:'Вес еды на гостя',piecesMetricLabel:'Канапе на гостя',amountPerGuestLabel:'Сумма на гостя',boxCountLabel:'Количество боксов',pricingTitle:'Расчёт стоимости',priceItemsLabel:'Стоимость позиций',discountLabel:'Скидка',promoLabel:'Промокод',menuAfterDiscountLabel:'Меню после скидок',deliveryLabel:'Доставка',totalLabel:'Итого',controlTitle:'Всё под контролем команды Солнца',extrasTitle:'Дополнительно к заказу рекомендуем',footerNote:'Доставка считается отдельно от меню и не участвует в скидках.'},controlLines:[...DELIVERY_CONTROL],extraServices:EXTRA_SERVICES.map(x=>x[0])};
|
||
function offerSettings(){
|
||
try{const api=window.SunClientOfferSettings?.get?.();if(api)return api}catch(_){}
|
||
try{
|
||
const raw=JSON.parse(localStorage.getItem(CLIENT_OFFER_SETTINGS_KEY)||'{}')||{},d=structuredClone(CLIENT_OFFER_DEFAULTS);
|
||
Object.keys(d).forEach(k=>{if(k!=='texts'&&k!=='controlLines'&&k!=='extraServices'&&raw[k]!=null)d[k]=raw[k]});
|
||
if(raw.texts&&typeof raw.texts==='object')d.texts={...d.texts,...raw.texts};
|
||
if(Array.isArray(raw.controlLines)&&raw.controlLines.length)d.controlLines=raw.controlLines;
|
||
if(Array.isArray(raw.extraServices)&&raw.extraServices.length)d.extraServices=raw.extraServices;
|
||
return d;
|
||
}catch(_){return structuredClone(CLIENT_OFFER_DEFAULTS)}
|
||
}
|
||
function offerSettingsForSnapshot(s){
|
||
const cfg=offerSettings(),textOverrides=s?.offerOverrides?.texts;
|
||
if(textOverrides&&typeof textOverrides==='object')cfg.texts={...cfg.texts,...textOverrides};
|
||
if(Array.isArray(s?.offerOverrides?.controlLines))cfg.controlLines=s.offerOverrides.controlLines.map(x=>String(x||'').trim()).filter(Boolean).slice(0,20);
|
||
// Table photos are automatic: they are shown only when the final page has free space.
|
||
cfg.showFinalPhoto=true;
|
||
return cfg;
|
||
}
|
||
function offerBoxCount(items){return (items||[]).filter(item=>[0,5].includes(Number(item.categoryId??0))).reduce((n,item)=>n+num(item.qty),0)}
|
||
function offerPrimaryMetric(s,cfg,g){
|
||
if(cfg.metricMode==='pieces')return {value:g&&num(s.foodPieces)?`${roundUp(num(s.foodPieces)/g,1).toLocaleString('ru-RU')} шт.`:'—',label:cfg.texts?.piecesMetricLabel||'Канапе на гостя'};
|
||
return {value:metricValue(s.foodGrams,'г',g),label:cfg.texts?.weightMetricLabel||'Вес еды на гостя'};
|
||
}
|
||
|
||
function offerIconSvg(title=''){
|
||
const t=String(title).toLowerCase();
|
||
if(/рыб|мор|лосос|кревет/.test(t))return '<svg viewBox="0 0 24 24"><path d="M4 12c3.4-4 7.2-5.6 11.5-3.5L20 6v4.3L22 12l-2 1.7V18l-4.5-2.5C11.2 17.6 7.4 16 4 12Z"/><circle cx="10" cy="11" r=".8"/></svg>';
|
||
if(/мяс|ростбиф|бургер|ветчин|салям/.test(t))return '<svg viewBox="0 0 24 24"><path d="M7 7c3-4 9-4 11 1 2 5-2 10-7 10-5 0-8-5-5-9l1-2Z"/><path d="M9 10c2-2 5-2 6 1"/></svg>';
|
||
if(/вег|овощ|салат/.test(t))return '<svg viewBox="0 0 24 24"><path d="M6 18c0-7 5-11 12-12-1 7-5 12-12 12Z"/><path d="M7 17c3-3 5-5 9-8"/></svg>';
|
||
if(/десерт|слад|макарон|торт/.test(t))return '<svg viewBox="0 0 24 24"><path d="M6 9h12l-1 10H7L6 9Z"/><path d="M8 9c0-3 2-5 4-5s4 2 4 5"/><path d="M10 5c1 1 3 1 4 0"/></svg>';
|
||
return '<svg viewBox="0 0 24 24"><path d="M4 8 12 4l8 4-8 4-8-4Z"/><path d="M4 8v8l8 4 8-4V8M12 12v8"/></svg>';
|
||
}
|
||
function offerGroups(items){
|
||
const map=new Map();
|
||
(items||[]).forEach(item=>{const k=sectionTitle(item)||'Меню';if(!map.has(k))map.set(k,[]);map.get(k).push(item)});
|
||
return [...map.entries()].map(([name,list])=>({name,list}));
|
||
}
|
||
function editorialGroups(items){
|
||
return offerGroups(items).map(group=>{
|
||
const weights=group.list.map(item=>parseMeasure(item.weight)).filter(m=>m&&m.kind!=='drink').map(m=>Math.round(m.value)).filter(v=>v>0);
|
||
const minW=weights.length?Math.min(...weights):0,maxW=weights.length?Math.max(...weights):0;
|
||
const qty=group.list.reduce((n,item)=>n+Math.max(0,num(item.qty)),0);
|
||
const weightText=minW?(minW===maxW?`${minW} г`:`${minW}-${maxW} г`):'';
|
||
const positions=`${group.list.length} ${plural(group.list.length,'блюдо','блюда','блюд')}`;
|
||
return {...group,qty,weightText,positions,meta:[weightText,positions].filter(Boolean).join(' · ')};
|
||
});
|
||
}
|
||
function offerHtml(s){
|
||
const templateId=OFFER_TEMPLATE_IDS.has(s.offerTemplateId)?s.offerTemplateId:offerTemplateId();
|
||
const themeClass=`theme-${templateId}`;
|
||
const cfg=offerSettingsForSnapshot(s),p=s.pricing||{},g=num(s.guests),menuPer=perGuest(p.itemsTotal,g);
|
||
const items=s.items||[],boxCount=offerBoxCount(items),positionCount=items.length,groups=offerGroups(items),heroPhoto=items.find(item=>item.photoData)?.photoData||s.finalGallery?.[0]||s.logo||'';
|
||
const primaryMetric=offerPrimaryMetric(s,cfg,g),eventName=String(s.event||'Персональное предложение').trim(),clientName=cfg.showClientName?String(s.client||'').trim():'';
|
||
const visibleDate=cfg.showDate?dateText(s.date):'',visibleGuests=cfg.showGuests&&g?`${g} ${plural(g,'гость','гостя','гостей')}`:'';
|
||
const discountPct=p.base>0?Math.round((p.discount/p.base)*100):0;
|
||
const compositionText=(item,sep=' · ')=>offerComposition(item).join(sep);
|
||
const itemMeta=item=>[weightLabel(item),compositionText(item)].filter(Boolean).join(' · ');
|
||
const photo=(item,i,cls='')=>`<figure class="offer-photo ${cls}"><span>${i+1}</span><img src="${esc(item.photoData||s.logo||'')}" alt="${esc(item.name)}" loading="lazy" decoding="async"><figcaption>${esc(item.name)}</figcaption></figure>`;
|
||
const row=item=>`<div class="offer-row"><span class="row-icon">${offerIconSvg(sectionTitle(item))}</span><div class="row-main"><b>${esc(item.name)}</b>${itemMeta(item)?`<small>${esc(itemMeta(item))}</small>`:''}</div><span class="row-qty">× ${Math.round(num(item.qty))}</span></div>`;
|
||
const groupTable=group=>`<section class="table-group"><div class="table-head"><h3>${esc(group.name)}</h3><span>Итого по позиции</span><span>Количество</span></div>${group.list.map(item=>{const comp=compositionText(item);return `<div class="table-line"><div><b>${esc(item.name)}</b>${comp?`<small>${esc(comp)}</small>`:''}</div><span>${esc(weightLabel(item)||item.weight||'—')}</span><strong>× ${Math.round(num(item.qty))}</strong></div>`}).join('')}</section>`;
|
||
const gallery=items.map((item,i)=>photo(item,i)).join('');
|
||
const controlLines=Array.isArray(cfg.controlLines)&&cfg.controlLines.length?cfg.controlLines:DELIVERY_CONTROL;
|
||
const extrasList=Array.isArray(cfg.extraServices)&&cfg.extraServices.length?cfg.extraServices:EXTRA_SERVICES.map(x=>x[0]);
|
||
const deliveryControl=controlLines.map(item=>`<li><span>✓</span>${esc(item)}</li>`).join('');
|
||
const deliveryControlShort=controlLines.slice(0,5).map(item=>`<li><span>✓</span>${esc(item)}</li>`).join('');
|
||
const extraServices=extrasList.map(name=>`<div class="extra-service"><span>+</span><b>${esc(name)}</b></div>`).join('');
|
||
const manual=p.manual?`<div><span>${esc(cfg.texts.discountLabel||'Скидка')}</span><b>- ${money(p.manual)}</b></div>`:'';
|
||
const promo=p.promo?`<div><span>${esc(cfg.texts.promoLabel||'Промокод')}${s.promoCode?` ${esc(s.promoCode)}`:''}</span><b>- ${money(p.promo)}</b></div>`:'';
|
||
const breakdown=cfg.showPriceBreakdown?`<div class="price-breakdown"><div><span>${esc(cfg.texts.priceItemsLabel||'Стоимость позиций')}</span><b>${money(p.base)}</b></div>${manual}${promo}<div><span>${esc(cfg.texts.menuAfterDiscountLabel||'Меню после скидок')}</span><b>${money(p.itemsTotal)}</b></div><div><span>${esc(cfg.texts.deliveryLabel||'Доставка')}</span><b>${money(p.delivery)}</b></div><div class="grand"><span>${esc(cfg.texts.totalLabel||'Итого')}</span><b>${money(p.total)}</b></div></div>`:'';
|
||
const metricCards=[`<div class="metric"><b>${esc(primaryMetric.value)}</b><small>${esc(primaryMetric.label)}</small></div>`,cfg.showAmountPerGuest?`<div class="metric"><b>${g?money(menuPer):'—'}</b><small>${esc(cfg.texts.amountPerGuestLabel||'Сумма на гостя')}</small></div>`:'',cfg.showBoxCount?`<div class="metric"><b>${Math.round(boxCount)}</b><small>${esc(cfg.texts.boxCountLabel||'Количество боксов')}</small></div>`:''].filter(Boolean);
|
||
const metrics=`<div class="metrics" style="--metric-count:${metricCards.length}">${metricCards.join('')}</div>`;
|
||
const infoParts=[];
|
||
if(cfg.showControl)infoParts.push(`<article class="info-card"><h2>${esc(cfg.texts.controlTitle)}</h2><ul class="control-list">${deliveryControl}</ul></article>`);
|
||
if(cfg.showExtras)infoParts.push(`<article class="info-card"><h2>${esc(cfg.texts.extrasTitle)}</h2><div class="extra-grid">${extraServices}</div></article>`);
|
||
const info=infoParts.length?`<section class="info-grid ${infoParts.length===1?'single':''}">${infoParts.join('')}</section>`:'';
|
||
const actions='';
|
||
const brand=`<header class="brandbar"><img class="brand-logo" src="${esc(s.logo||'')}" alt="Солнце Кейтеринг">${visibleDate?`<div class="brand-meta"><span>${esc(visibleDate)}</span></div>`:''}</header>`;
|
||
const heroStats=[visibleDate?`<span class="hero-stat"><i>◷</i>${esc(visibleDate)}</span>`:'',visibleGuests?`<span class="hero-stat"><i>○</i>${esc(visibleGuests)}</span>`:''].filter(Boolean).join('');
|
||
const standardHero=`<section class="hero"><div class="hero-copy"><div class="kicker">${esc(cfg.texts.proposalLabel)}</div><h1>${esc(eventName)}</h1>${clientName?`<div class="client-line">${esc(clientName)}</div>`:''}${heroStats?`<div class="hero-stats">${heroStats}</div>`:''}<div class="hero-rule"></div><p class="hero-note">${esc(cfg.texts.heroNote)}</p></div><div class="hero-photo"><img src="${esc(heroPhoto)}" alt="${esc(eventName)}"></div></section>`;
|
||
const sellingBlock=(String(cfg.texts.salesTitle||'').trim()||String(cfg.texts.salesNote||'').trim())?`<section class="sales-block"><div class="sales-accent"></div><div>${String(cfg.texts.salesTitle||'').trim()?`<h2>${esc(cfg.texts.salesTitle)}</h2>`:''}${String(cfg.texts.salesNote||'').trim()?`<p>${esc(cfg.texts.salesNote)}</p>`:''}</div></section>`:'';
|
||
const identityLine=[clientName,visibleDate,visibleGuests].filter(Boolean).join(' · ');
|
||
const blackControl=cfg.showControl?`<article class="delivery-key"><h2>${esc(cfg.texts.controlTitle)}</h2><ul class="control-list">${deliveryControlShort}</ul></article>`:'';
|
||
const blackExtras=cfg.showExtras?`<div class="addons-strip"><h2>${esc(cfg.texts.extrasTitle)}</h2><div class="extra-grid">${extraServices}</div></div>`:'';
|
||
const egSideParts=[cfg.showControl?`<div class="eg-delivery"><h2>${esc(cfg.texts.controlTitle)}</h2><ul class="control-list">${deliveryControlShort}</ul></div>`:'',cfg.showExtras?`<div class="eg-addons"><h2>${esc(cfg.texts.extrasTitle)}</h2><div class="extra-grid">${extraServices}</div></div>`:''].filter(Boolean);
|
||
const egSide=egSideParts.length?`<aside class="eg-side">${egSideParts.join('')}</aside>`:'';
|
||
const egFacts=[visibleDate,clientName,visibleGuests].filter(Boolean);
|
||
const footerNote=cfg.showFooterNote?`<div class="offer-note">${esc(cfg.texts.footerNote)}</div>`:'';
|
||
let body='';
|
||
if(templateId==='editorial-grid'){
|
||
const editorial=editorialGroups(items);
|
||
const editorialRows=editorial.map((group,gi)=>`<div class="ed-group-row"><span class="ed-group-icon">${String(gi+1).padStart(2,'0')}</span><div><b>${esc(group.name)}</b><small>${esc(group.meta)}</small></div><span class="ed-group-qty">× ${Math.round(group.qty)}</span></div>`).join('');
|
||
const editorialGallery=items.slice(0,8).map((item,i)=>photo(item,i,'ed-photo')).join('');
|
||
body=`${brand}<section class="ed-hero"><div class="ed-hero-copy"><div class="kicker">${esc(cfg.texts.proposalLabel)}</div><h1>${esc(eventName)}</h1>${clientName?`<div class="client-line">${esc(clientName)}</div>`:''}<div class="ed-hero-facts">${visibleDate?`<span>◷ ${esc(visibleDate)}</span>`:''}${visibleGuests?`<span>○ ${esc(visibleGuests)}</span>`:''}</div><div class="ed-rule"></div><p>${esc(cfg.texts.heroNote)}</p></div><div class="ed-hero-photo"><img src="${esc(heroPhoto)}" alt="${esc(eventName)}"></div></section>${sellingBlock}<div class="ed-section-title"><span>${esc(cfg.texts.menuTitle)}</span></div><section class="ed-group-grid">${editorialRows}</section>${cfg.showGallery?`<section class="ed-photo-grid">${editorialGallery}</section>`:''}<section class="summary ed-summary"><div class="summary-left"><h2 class="summary-title">${esc(cfg.texts.summaryTitle)}</h2>${metrics}</div><div class="summary-price"><small>${esc(cfg.texts.totalPriceTitle)}</small><b>${money(p.total)}</b><em>${g?`на гостя: ${money(menuPer)}`:'Финальная сумма'}</em>${cfg.showDiscountBadge&&discountPct?`<span class="discount-pill">-${discountPct}% учтено</span>`:''}</div></section>${breakdown}${info}${actions}`;
|
||
}else if(templateId==='light'){
|
||
body=`${brand}${standardHero}${sellingBlock}<div class="section-heading"><span>${esc(cfg.texts.menuTitle)}</span></div><section class="menu-grid light-menu">${items.map(row).join('')}</section>${cfg.showGallery?`<section class="gallery light-gallery">${gallery}</section>`:''}<section class="summary"><div class="summary-left"><h2 class="summary-title">${esc(cfg.texts.summaryTitle)}</h2>${metrics}</div><div class="summary-price"><small>${esc(cfg.texts.totalPriceTitle)}</small><b>${money(p.total)}</b><em>${g?`на гостя: ${money(menuPer)}`:'Финальная сумма'}</em>${cfg.showDiscountBadge&&discountPct?`<span class="discount-pill">-${discountPct}% учтено</span>`:''}</div>${breakdown}</section>${info}${actions}`;
|
||
}else if(templateId==='midnight-glass'){
|
||
body=`${brand}<section class="mg-hero"><div><span class="eyebrow">${esc(cfg.texts.proposalLabel)}</span><h1>${esc(cfg.texts.menuTitle)} на ваше событие</h1><p>${esc(eventName)}${identityLine?` · ${esc(identityLine)}`:''}</p><p class="mg-hero-note">${esc(cfg.texts.heroNote)}</p><div class="quality-icons"><span>Разнообразие блюд</span><span>Свежие ингредиенты</span><span>Премиальное качество</span></div></div><img src="${esc(heroPhoto)}" alt=""></section>${sellingBlock}<section class="mg-split"><div class="mg-list"><h2>${esc(cfg.texts.menuTitle)}</h2>${items.map(row).join('')}</div>${cfg.showGallery?`<div class="mg-gallery">${items.slice(0,8).map((item,i)=>photo(item,i)).join('')}</div>`:''}</section><section class="summary mg-summary"><div class="summary-left"><h2 class="summary-title">${esc(cfg.texts.summaryTitle)}</h2>${metrics}</div><div class="summary-price"><small>${esc(cfg.texts.totalPriceTitle)}</small><b>${money(p.total)}</b><em>${g?`на гостя: ${money(menuPer)}`:'Финальная сумма'}</em>${cfg.showDiscountBadge&&discountPct?`<span class="discount-pill">-${discountPct}% учтено</span>`:''}</div>${breakdown}</section>${info}${actions}`;
|
||
}else if(templateId==='midnight-compact'){
|
||
body=`${brand}<section class="mc-intro"><div><span class="eyebrow">${esc(cfg.texts.proposalLabel)}</span><h1>${esc(eventName)}</h1>${identityLine?`<p>${esc(identityLine)}</p>`:''}</div><div class="mc-note">${esc(cfg.texts.heroNote)}</div></section><section class="mc-menu"><div class="mc-tables">${groups.map(groupTable).join('')}</div>${cfg.showGallery?`<aside class="mc-photo-column"><h2>Боксы в заказе</h2>${items.filter(item=>[0,5].includes(Number(item.categoryId))).map((item,i)=>photo(item,i)).join('')}</aside>`:''}</section><section class="mc-total"><h2>${esc(cfg.texts.summaryTitle)}</h2>${metrics}<div class="mc-price"><small>${esc(cfg.texts.totalPriceTitle)}</small><b>${money(p.total)}</b>${cfg.showDiscountBadge&&discountPct?`<span>-${discountPct}%</span>`:''}</div></section>${info}${breakdown}${actions}`;
|
||
}else if(templateId==='black-gold'){
|
||
body=`${brand}<section class="bg-hero"><img src="${esc(heroPhoto)}" alt=""><div class="bg-overlay"><span>${esc(cfg.texts.proposalLabel)}</span><h1>Кейтеринг<br>для вашего события</h1><p>${esc(eventName)}${identityLine?`<br>${esc(identityLine)}`:''}</p></div></section><div class="section-heading editorial"><span>${esc(cfg.texts.menuTitle)} для вашего мероприятия</span></div><section class="product-grid">${items.map((item,i)=>`<article class="product-card"><div class="product-photo"><img src="${esc(item.photoData||s.logo||'')}" alt="${esc(item.name)}"><span>${String(i+1).padStart(2,'0')}</span><b>×${Math.round(num(item.qty))}</b></div><h3>${esc(item.name)}</h3><p>${esc([weightLabel(item)].filter(Boolean).join(' · ')||sectionTitle(item))}</p>${compositionText(item,', ')?`<small>${esc(compositionText(item,', '))}</small>`:''}</article>`).join('')}</section><section class="bg-bottom ${blackControl?'':'single'}"><article><h2>${esc(cfg.texts.summaryTitle)}</h2>${metrics}<div class="bg-price">${money(p.total)}${g?`<small>на гостя: ${money(menuPer)}</small>`:''}</div></article>${blackControl}</section>${blackExtras}${breakdown}${actions}`;
|
||
}else if(templateId==='emerald-gold'){
|
||
body=`${brand}<section class="eg-hero"><div class="eg-copy"><span class="eyebrow">${esc(cfg.texts.proposalLabel)}</span><h1>${esc(eventName)}</h1><p>${esc(cfg.texts.heroNote)}</p>${egFacts.length?`<div class="eg-facts" style="--fact-count:${egFacts.length}">${egFacts.map(x=>`<span>${esc(x)}</span>`).join('')}</div>`:''}<div class="eg-gold-summary">${metrics}<div><small>${esc(cfg.texts.totalPriceTitle)}</small><b>${money(p.total)}</b></div></div></div><div class="eg-photo-stack"><img class="main" src="${esc(heroPhoto)}" alt="">${items[1]?`<img class="secondary" src="${esc(items[1].photoData||s.logo||'')}" alt="">`:''}</div></section>${sellingBlock}<section class="eg-main ${egSide?'':'single'}"><div class="eg-menu"><h2>${esc(cfg.texts.menuTitle)}</h2>${items.map(row).join('')}</div>${egSide}</section>${breakdown}${actions}`;
|
||
}else{
|
||
body=`${brand}<section class="neon-hero"><div>${visibleDate?`<span>${esc(visibleDate)}</span>`:''}<h1>${esc(eventName)}</h1><p>${esc(cfg.texts.heroNote)}</p></div><img src="${esc(heroPhoto)}" alt="">${visibleGuests?`<b>${g}<small>гостей</small></b>`:''}</section><section class="neon-quality"><p>${esc(cfg.texts.proposalLabel)}</p><span>Свежие ингредиенты</span><span>Авторские рецепты</span><span>Контроль качества</span></section><section class="neon-main"><div class="neon-tables">${groups.map(groupTable).join('')}</div><aside class="neon-gallery"><h2>Наши боксы</h2><div>${items.filter(item=>[0,5].includes(Number(item.categoryId))).map((item,i)=>photo(item,i)).join('')}</div></aside></section><section class="neon-total"><div><h2>${esc(cfg.texts.summaryTitle)}</h2>${metrics}</div><div><small>${esc(cfg.texts.totalPriceTitle)}</small><b>${money(p.total)}</b>${cfg.showDiscountBadge&&discountPct?`<span>-${discountPct}%</span>`:''}</div></section>${info}${breakdown}${actions}`;
|
||
}
|
||
const finalGalleryItems=cfg.showFinalPhoto&&Array.isArray(s.finalGallery)?s.finalGallery.filter(Boolean).slice(0,2):[];
|
||
const finalPhoto=finalGalleryItems.length?`<section class="final-gallery-card"><h2>${esc(cfg.texts.finalGalleryTitle||'Как это выглядит на вашем столе')}</h2><div class="final-gallery-grid">${finalGalleryItems.map((src,i)=>`<figure><img src="${esc(src)}" alt="Сервировка Солнце Кейтеринг ${i+1}" loading="lazy" decoding="async"></figure>`).join('')}</div></section>`:'';
|
||
return `<!doctype html><html lang="ru" class="${themeClass}"><head><meta charset="utf-8"><base href="${esc(document.baseURI||location.href)}"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>Предложение клиенту</title><style>
|
||
:root{--cream:#ffffff;--paper:#ffffff;--paper2:#f7f8f6;--ink:#171917;--green:#476f4d;--green-deep:#28533a;--green-soft:#6d8f6a;--gold:#c99b47;--gold2:#e2c28b;--line:#e7d8bd;--muted:#7b756b;--shadow:0 12px 36px rgba(59,65,55,.07)}*{box-sizing:border-box}html{background:var(--cream)}body{margin:0;background:var(--cream);color:var(--ink);font-family:Arial,Helvetica,sans-serif;-webkit-font-smoothing:antialiased}.page{width:min(1100px,100%);margin:0 auto;padding:24px 26px 48px}.brandbar{display:flex;align-items:center;justify-content:space-between;gap:18px;margin:0 0 22px}.brand-logo{width:210px;max-height:76px;object-fit:contain;object-position:left center}.brand-meta{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end;color:var(--muted);font-size:12px}.brand-meta span{border:1px solid var(--line);border-radius:999px;padding:8px 12px;background:color-mix(in srgb,var(--paper) 88%,transparent)}.hero{display:grid;grid-template-columns:44% 56%;border:1px solid var(--line);border-radius:27px;overflow:hidden;background:var(--paper);box-shadow:var(--shadow);min-height:390px}.hero-copy{padding:38px 36px 32px;display:flex;flex-direction:column;justify-content:center}.kicker,.eyebrow{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--gold);font-weight:800}.hero h1{font-family:Georgia,'Times New Roman',serif;font-weight:500;font-size:48px;line-height:1.03;margin:12px 0 0;color:var(--ink)}.client-line{margin:11px 0 0;color:var(--green-deep);font-family:Georgia,'Times New Roman',serif;font-size:22px}.hero-stats{display:flex;gap:20px;flex-wrap:wrap;margin:25px 0 22px}.hero-stat{display:flex;align-items:center;gap:8px;font-size:15px}.hero-stat i{width:25px;height:25px;border:1px solid var(--green);border-radius:8px;display:grid;place-items:center;color:var(--green);font-style:normal}.hero-rule{height:1px;background:linear-gradient(90deg,var(--gold2),transparent);margin-bottom:20px}.hero-note{font-size:16px;line-height:1.55;color:color-mix(in srgb,var(--ink) 82%,var(--muted));margin:0}.sales-block{display:grid;grid-template-columns:6px minmax(0,1fr);gap:24px;margin-top:18px;padding:26px 30px;border:1px solid var(--line);border-radius:21px;background:var(--paper);box-shadow:var(--shadow)}.sales-accent{border-radius:999px;background:linear-gradient(180deg,var(--gold),var(--green))}.sales-block h2{margin:0 0 10px;font-family:Georgia,'Times New Roman',serif;font-size:25px;font-weight:500;color:var(--green-deep)}.sales-block p{margin:0;color:color-mix(in srgb,var(--ink) 84%,var(--muted));font-size:15px;line-height:1.65;white-space:pre-line;overflow-wrap:anywhere}.mg-hero-note{margin:13px 0 0!important;color:var(--muted)!important;font-size:14px!important;line-height:1.5!important}.hero-photo{min-height:390px}.hero-photo img{display:block;width:100%;height:100%;min-height:390px;object-fit:cover}.section-heading{display:flex;align-items:center;gap:14px;margin:31px 0 20px;color:var(--green-deep);font-family:Georgia,'Times New Roman',serif;font-size:28px;text-align:center}.section-heading:before,.section-heading:after{content:'';height:1px;flex:1;background:var(--gold2)}.menu-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.offer-row{display:grid;grid-template-columns:52px minmax(0,1fr) auto;align-items:center;gap:13px;min-height:80px;padding:12px 15px;border:1px solid var(--line);border-radius:17px;background:var(--paper)}.row-icon{width:45px;height:45px;border-radius:50%;display:grid;place-items:center;background:var(--green-deep);color:#fff}.row-icon svg{width:24px;height:24px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.row-main{min-width:0}.row-main b{display:block;font-family:Georgia,'Times New Roman',serif;font-size:18px;font-weight:500;line-height:1.22}.row-main small{display:block;color:var(--muted);font-size:11px;line-height:1.4;margin-top:5px;overflow-wrap:anywhere;white-space:normal}.row-qty{min-width:52px;text-align:center;padding:8px 10px;border:1px solid var(--line);border-radius:999px;font-family:Georgia,serif}.gallery{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;margin:18px 0 22px}.offer-photo{margin:0;position:relative;min-width:0}.offer-photo>span{position:absolute;left:7px;top:7px;z-index:2;min-width:23px;height:23px;padding:0 5px;border-radius:50%;display:grid;place-items:center;background:color-mix(in srgb,var(--green-deep) 90%,transparent);color:#fff;font-size:10px;font-weight:800}.offer-photo img{display:block;width:100%;aspect-ratio:1/1;object-fit:cover;border-radius:13px;border:1px solid var(--line);background:var(--paper2)}.offer-photo figcaption{display:none}.summary{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(250px,.7fr);border:1px solid var(--line);border-radius:22px;background:var(--paper);overflow:hidden;box-shadow:var(--shadow);margin-top:18px}.summary-left{padding:25px 28px}.summary-title{font-family:Georgia,serif;color:var(--green-deep);font-size:26px;text-align:center;margin:0 0 18px}.metrics{display:grid;grid-template-columns:repeat(var(--metric-count,3),1fr);gap:8px}.metric{text-align:center;padding:5px 10px}.metric b{display:block;font-family:Georgia,serif;font-size:25px;font-weight:500}.metric small{display:block;color:var(--muted);font-size:10px;margin-top:6px;line-height:1.3}.summary-price{border-left:1px solid var(--line);padding:25px;display:flex;flex-direction:column;justify-content:center;text-align:center}.summary-price small,.mc-price small,.neon-total small,.eg-gold-summary small{color:var(--gold);text-transform:uppercase;letter-spacing:.06em;font-size:10px}.summary-price b,.mc-price b,.neon-total>div>b,.eg-gold-summary>div>b{font-family:Georgia,serif;color:var(--green-deep);font-size:34px;font-weight:500;margin:7px 0}.summary-price em{font-style:normal;color:var(--muted);font-size:12px}.discount-pill{align-self:center;margin-top:8px;padding:5px 11px;border-radius:8px;background:color-mix(in srgb,var(--green) 12%,var(--paper));color:var(--green);font-size:11px}.price-breakdown{grid-column:1/-1;border-top:1px solid var(--line);padding:12px 24px;background:color-mix(in srgb,var(--paper) 90%,var(--paper2))}.price-breakdown>div{display:flex;justify-content:space-between;gap:20px;padding:5px 0;color:var(--muted);font-size:11px}.price-breakdown .grand{font-weight:800;color:var(--green-deep);font-size:13px;border-top:1px dashed var(--line);padding-top:8px;margin-top:3px}.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px}.info-card{border:1px solid var(--line);border-radius:20px;background:var(--paper);padding:22px 24px}.info-card h2{font-family:Georgia,serif;color:var(--green-deep);font-size:19px;font-weight:500;text-align:center;margin:0 0 13px}.control-list{display:grid;gap:7px;margin:0;padding:0;list-style:none}.control-list li{display:flex;align-items:flex-start;gap:8px;font-size:11px;line-height:1.4}.control-list li span{flex:0 0 16px;width:16px;height:16px;border-radius:50%;display:grid;place-items:center;background:color-mix(in srgb,var(--green) 16%,var(--paper));color:var(--green-deep);font-size:9px}.extra-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.extra-service{display:flex;align-items:center;gap:8px;font-size:11px}.extra-service span{width:17px;height:17px;border-radius:50%;display:grid;place-items:center;background:color-mix(in srgb,var(--gold) 16%,var(--paper));color:var(--gold)}.footer-actions{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}.footer-action{border-radius:15px;min-height:56px;display:flex;align-items:center;justify-content:center;border:1px solid var(--line);font-family:Georgia,serif;font-size:17px;text-decoration:none;cursor:pointer}.footer-action.download{background:transparent;color:var(--green-deep);border-color:var(--gold2)}.footer-action.order{background:var(--green-deep);color:#fff}.final-photo-card{margin-top:22px;border-radius:22px;overflow:hidden;border:1px solid var(--line)}.final-photo-card img{display:block;width:100%;height:auto}.final-gallery-card{margin-top:22px;padding:24px;border:1px solid var(--line);border-radius:22px;background:var(--paper)}.final-gallery-card h2{margin:0 0 18px;text-align:center;font-family:Georgia,'Times New Roman',serif;font-size:27px;font-weight:500;color:var(--green-deep)}.final-gallery-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.final-gallery-grid figure{margin:0;overflow:hidden;border-radius:14px;border:1px solid var(--line);background:var(--paper2);aspect-ratio:4/3}.final-gallery-grid img{display:block;width:100%;height:100%;object-fit:cover}.footer{display:flex;justify-content:space-between;gap:12px;padding:22px 3px 0;color:var(--muted);font-size:10px}
|
||
.table-group{border:1px solid var(--line);border-radius:16px;overflow:hidden;background:var(--paper);margin-bottom:12px}.table-head,.table-line{display:grid;grid-template-columns:minmax(0,1fr) 190px 72px;gap:10px;align-items:center}.table-head{padding:13px 15px;border-bottom:1px solid var(--line);color:var(--muted);font-size:10px}.table-head h3{margin:0;color:var(--green-deep);font-size:16px}.table-line{padding:12px 15px;border-bottom:1px solid color-mix(in srgb,var(--line) 65%,transparent);font-size:12px}.table-line:last-child{border-bottom:0}.table-line b{display:block}.table-line small{display:block;color:var(--muted);font-size:9px;line-height:1.4;margin-top:5px;overflow-wrap:anywhere;white-space:normal}.table-line>span,.table-line>strong{text-align:center}.table-line>span{white-space:normal;line-height:1.35;font-size:10px}.table-line>strong{white-space:nowrap}
|
||
html.theme-editorial-grid,body.theme-editorial-grid{--cream:#fbfaf6;--paper:#fffefb;--paper2:#f7f4ed;--ink:#163127;--green:#245f46;--green-deep:#1f5a42;--green-soft:#e9f1eb;--gold:#b99355;--gold2:#d8c4a2;--line:#e8dfcf;--muted:#776f62;background:#fbfaf6;color:var(--ink)}
|
||
.theme-editorial-grid .page{max-width:980px;padding:20px 22px 42px}.theme-editorial-grid .brandbar{margin-bottom:16px}.theme-editorial-grid .brand-logo{width:205px}.ed-hero{display:grid;grid-template-columns:43% 57%;min-height:360px;border:1px solid var(--line);border-radius:18px;overflow:hidden;background:var(--paper)}.ed-hero-copy{padding:30px 28px;display:flex;flex-direction:column;justify-content:center}.ed-hero h1{font-family:Georgia,'Times New Roman',serif;font-size:43px;line-height:1.04;font-weight:500;margin:10px 0 4px;color:var(--ink)}.ed-hero-facts{display:flex;gap:18px;flex-wrap:wrap;margin:22px 0 17px;font-size:13px;color:var(--ink)}.ed-rule{height:1px;background:linear-gradient(90deg,var(--gold2),transparent);margin:0 0 17px}.ed-hero-copy p{margin:0;color:var(--muted);font-size:13px;line-height:1.55}.ed-hero-photo{min-height:360px}.ed-hero-photo img{width:100%;height:100%;min-height:360px;display:block;object-fit:cover}.ed-section-title{display:flex;align-items:center;gap:18px;margin:22px 0 14px;font-family:Georgia,'Times New Roman',serif;font-size:25px;color:var(--green-deep);text-align:center}.ed-section-title:before,.ed-section-title:after{content:'';height:1px;flex:1;background:var(--gold2)}.ed-group-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.ed-group-row{display:grid;grid-template-columns:44px minmax(0,1fr) auto;align-items:center;gap:10px;min-height:65px;padding:8px 12px;border:1px solid var(--line);border-radius:13px;background:var(--paper)}.ed-group-icon{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;background:var(--green-deep);color:#fff}.ed-group-icon{font-size:10px;font-weight:800;letter-spacing:.02em}.ed-group-icon svg{display:none}.ed-group-row b{display:block;font-family:Georgia,'Times New Roman',serif;font-size:15px;font-weight:500;color:var(--ink);line-height:1.2}.ed-group-row small{display:block;margin-top:4px;color:var(--gold);font-size:10px}.ed-group-qty{min-width:48px;text-align:center;padding:6px 9px;border:1px solid var(--line);border-radius:999px;font-size:11px;color:var(--ink)}.ed-photo-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;margin:11px 0 14px}.ed-photo-grid .offer-photo{overflow:hidden;border-radius:10px}.ed-photo-grid .offer-photo img{aspect-ratio:1.28/1;border-radius:10px}.ed-photo-grid .offer-photo>span{display:none}.ed-summary{margin-top:12px}.theme-editorial-grid .summary{border-radius:16px;box-shadow:none}.theme-editorial-grid .info-card{border-radius:15px}.theme-editorial-grid .sales-block{box-shadow:none;border-radius:15px;padding:20px 24px;margin-top:12px}.theme-editorial-grid .final-gallery-card{border-radius:16px}.theme-editorial-grid>.price-breakdown,.theme-editorial-grid .price-breakdown{background:#fbfaf6;border:1px solid var(--line);border-radius:14px;margin-top:10px}@media(max-width:760px){.ed-hero{grid-template-columns:1fr}.ed-hero-photo{min-height:260px}.ed-hero-photo img{min-height:260px}.ed-group-grid{grid-template-columns:1fr}.ed-photo-grid{grid-template-columns:1fr 1fr}}
|
||
html.theme-midnight-glass,body.theme-midnight-glass{--cream:#071521;--paper:#0b2230;--paper2:#0b1b27;--ink:#f2f5f6;--green:#1bb69b;--green-deep:#e4bd6b;--gold:#d5aa56;--gold2:#5f5131;--line:#294a5b;--muted:#9caeb7;background:radial-gradient(circle at 40% 0,#103b3f 0,transparent 34%),#06131d;color:var(--ink)}.theme-midnight-glass .brand-logo{filter:none}.mg-hero{display:grid;grid-template-columns:55% 45%;border:1px solid var(--line);border-radius:24px;overflow:hidden;background:linear-gradient(145deg,#0b2830,#071620);min-height:370px}.mg-hero>div{padding:48px 44px}.mg-hero h1{font-family:Georgia,serif;font-size:48px;font-weight:500;line-height:1.06;margin:17px 0 13px}.mg-hero p{color:var(--muted);font-size:17px}.mg-hero>img{width:100%;height:100%;object-fit:cover;min-height:370px}.quality-icons{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:32px}.quality-icons span{padding:13px 9px;border:1px solid var(--line);border-radius:13px;text-align:center;font-size:10px}.mg-split{display:grid;grid-template-columns:1.08fr .92fr;gap:15px;margin-top:18px}.mg-list,.mg-gallery{border:1px solid var(--line);border-radius:22px;background:color-mix(in srgb,var(--paper) 92%,transparent);padding:20px}.mg-list h2{font-family:Georgia,serif;margin:0 0 14px;font-weight:500}.mg-list .offer-row{border:0;border-bottom:1px solid var(--line);border-radius:0;background:transparent;min-height:68px}.mg-gallery{display:grid;grid-template-columns:1fr 1fr;gap:9px;align-content:start}.mg-gallery .offer-photo img{aspect-ratio:1.2/1}.theme-midnight-glass .row-main b,.theme-midnight-glass .metric b{color:#fff}.theme-midnight-glass .summary,.theme-midnight-glass .info-card{background:#0a202b}.theme-midnight-glass .footer-action.order{background:linear-gradient(135deg,#11876f,#0d695a)}
|
||
html.theme-midnight-compact,body.theme-midnight-compact{--cream:#03131c;--paper:#0a1b25;--paper2:#071720;--ink:#f4f3ef;--green:#67a76e;--green-deep:#f0cb75;--gold:#dfb957;--gold2:#5c4f31;--line:#344b58;--muted:#9ca9af;background:#03131c;color:var(--ink)}.mc-intro{display:grid;grid-template-columns:1.2fr .8fr;gap:20px;border:1px solid var(--line);border-radius:18px;padding:26px 28px;background:#081c26}.mc-intro h1{font-size:36px;margin:9px 0;font-family:Arial,sans-serif}.mc-intro p,.mc-note{color:var(--muted);line-height:1.5}.mc-note{display:flex;align-items:center;border-left:1px solid var(--line);padding-left:25px}.mc-menu{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(270px,.5fr);gap:14px;margin-top:16px}.mc-photo-column{border:1px solid var(--line);border-radius:18px;background:var(--paper);padding:15px;display:grid;grid-template-columns:1fr 1fr;gap:8px;align-content:start}.mc-photo-column h2{grid-column:1/-1;margin:2px 0 8px;color:var(--green-deep);font-size:17px}.mc-photo-column .offer-photo img{aspect-ratio:1/1}.mc-total{display:grid;grid-template-columns:auto minmax(0,1fr) 230px;gap:18px;align-items:center;border:1px solid var(--line);border-radius:18px;padding:17px 20px;margin-top:14px;background:#071820}.mc-total h2{margin:0;font-family:Georgia,serif;color:var(--green-deep)}.mc-total .metrics{min-width:0}.mc-price{text-align:right}.mc-price span{display:inline-block;margin-left:7px;padding:4px 8px;border-radius:999px;background:#284b32;color:#d9f3dc}.theme-midnight-compact .table-head h3,.theme-midnight-compact .row-main b,.theme-midnight-compact .metric b{color:#fff}.theme-midnight-compact .info-card{background:#081b24}.theme-midnight-compact .footer-action.order{background:#0d6e3e}
|
||
html.theme-black-gold,body.theme-black-gold{--cream:#0d0d0c;--paper:#151513;--paper2:#10100e;--ink:#f2eee6;--green:#c19035;--green-deep:#d5ab5d;--gold:#c99b47;--gold2:#55451f;--line:#3b362c;--muted:#aaa49b;background:#0d0d0c;color:var(--ink)}.bg-hero{height:540px;position:relative;border-radius:24px;overflow:hidden;border:1px solid var(--line)}.bg-hero>img{width:100%;height:100%;object-fit:cover}.bg-hero:after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,rgba(9,9,8,.96) 0%,rgba(10,10,9,.72) 42%,rgba(0,0,0,.08) 75%)}.bg-overlay{position:absolute;z-index:2;left:42px;top:60px;width:47%}.bg-overlay>span{color:var(--gold);text-transform:uppercase;letter-spacing:.12em;font-size:11px}.bg-overlay h1{font-family:Georgia,serif;font-size:58px;font-weight:400;line-height:1.02;margin:18px 0;color:#fff;text-transform:uppercase}.bg-overlay p{color:#c8c3ba;line-height:1.55}.section-heading.editorial{color:#e8dfcf;text-transform:uppercase}.product-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:13px}.product-card{border:1px solid var(--line);border-radius:16px;overflow:hidden;background:#151513;padding-bottom:16px}.product-photo{height:230px;position:relative}.product-photo img{width:100%;height:100%;object-fit:cover}.product-photo span,.product-photo b{position:absolute;top:10px;background:#090908;color:#fff;border-radius:999px;padding:5px 8px;font-size:10px}.product-photo span{left:10px;color:var(--gold)}.product-photo b{right:10px}.product-card h3{font-family:Georgia,serif;font-weight:500;font-size:20px;margin:14px 15px 6px;color:#fff}.product-card p,.product-card small{display:block;margin:0 15px;color:var(--muted);font-size:10px;line-height:1.45;overflow-wrap:anywhere;white-space:normal}.product-card small{margin-top:7px}.bg-bottom{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:15px}.bg-bottom>article,.addons-strip{border:1px solid var(--line);border-radius:18px;padding:20px;background:#151513}.bg-bottom h2,.addons-strip h2{margin:0 0 14px;color:var(--gold);font-family:Georgia,serif;font-weight:500}.bg-price{font-family:Georgia,serif;font-size:34px;color:#fff;margin-top:16px}.bg-price small{display:block;color:var(--gold);font:11px Arial;margin-top:4px}.delivery-key .control-list{grid-template-columns:1fr 1fr}.addons-strip{margin-top:14px}.addons-strip .extra-grid{grid-template-columns:repeat(4,1fr)}.theme-black-gold .metric b,.theme-black-gold .row-main b{color:#fff}.theme-black-gold .footer-action.order{background:#c99b47;color:#181208}
|
||
html.theme-emerald-gold,body.theme-emerald-gold{--cream:#061318;--paper:#0b2023;--paper2:#08171a;--ink:#f2efe7;--green:#1d795f;--green-deep:#dfbc72;--gold:#d7ac55;--gold2:#67572f;--line:#314a4c;--muted:#abb3b0;background:radial-gradient(circle at 100% 0,#0e3934 0,transparent 30%),#061318;color:var(--ink)}.eg-hero{display:grid;grid-template-columns:58% 42%;gap:0;border:1px solid var(--line);border-radius:24px;overflow:hidden;background:#081b1e;min-height:560px}.eg-copy{padding:45px 38px}.eg-copy h1{font-family:Georgia,serif;font-size:55px;font-weight:400;line-height:1.03;margin:18px 0 16px;color:#fff}.eg-copy p{font-size:16px;line-height:1.5;color:var(--muted)}.eg-copy p b{color:var(--gold)}.eg-facts{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:22px}.eg-facts span{padding:11px;border:1px solid var(--line);border-radius:13px;text-align:center;font-size:11px}.eg-gold-summary{margin-top:24px;padding:19px;border-radius:17px;background:linear-gradient(145deg,#b88731,#e6bf67);color:#1c1a13}.eg-gold-summary .metrics{margin-bottom:15px}.eg-gold-summary .metric b,.eg-gold-summary .metric small{color:#1c1a13}.eg-gold-summary>div:last-child{display:flex;justify-content:space-between;align-items:end;border-top:1px solid rgba(0,0,0,.18);padding-top:12px}.eg-gold-summary>div:last-child b{color:#173e32;margin:0}.eg-photo-stack{position:relative;overflow:hidden}.eg-photo-stack .main{width:100%;height:100%;object-fit:cover}.eg-photo-stack .secondary{position:absolute;right:18px;bottom:22px;width:62%;height:38%;object-fit:cover;border:2px solid var(--gold);border-radius:16px;transform:rotate(-5deg);box-shadow:0 16px 30px rgba(0,0,0,.35)}.eg-main{display:grid;grid-template-columns:1.15fr .85fr;gap:14px;margin-top:15px}.eg-menu,.eg-delivery,.eg-addons{border:1px solid var(--line);border-radius:20px;background:#0a1e21;padding:18px}.eg-menu h2,.eg-delivery h2,.eg-addons h2{margin:0 0 13px;color:var(--gold);font-family:Georgia,serif;font-weight:500}.eg-menu .offer-row{background:#071719;margin-bottom:8px}.eg-side{display:grid;gap:14px;align-content:start}.eg-addons .extra-grid{grid-template-columns:1fr}.theme-emerald-gold .row-main b,.theme-emerald-gold .metric b{color:#fff}.theme-emerald-gold .footer-action.order{background:#13734d}
|
||
html.theme-neon-emerald,body.theme-neon-emerald{--cream:#020d11;--paper:#06191d;--paper2:#041417;--ink:#effff5;--green:#45f27d;--green-deep:#69ff94;--gold:#d2aa45;--gold2:#5d4d22;--line:#23643f;--muted:#9bb4a9;background:radial-gradient(circle at 45% 0,#063229 0,transparent 31%),#020d11;color:var(--ink)}.neon-hero{display:grid;grid-template-columns:1fr 44%;min-height:300px;border:1px solid #2f9b59;border-radius:20px;overflow:hidden;position:relative;background:#05191b}.neon-hero>div{padding:36px 30px}.neon-hero>div>span{color:var(--green);font-weight:800;font-size:11px}.neon-hero h1{font-size:38px;line-height:1.1;margin:14px 0 12px}.neon-hero p{color:var(--muted);font-size:15px}.neon-hero>img{width:100%;height:100%;object-fit:cover}.neon-hero>b{position:absolute;right:24px;bottom:22px;width:88px;height:88px;border-radius:50%;display:grid;place-items:center;background:#05221d;border:1px solid var(--green);font-size:27px;box-shadow:0 0 22px rgba(69,242,125,.13)}.neon-hero>b small{display:block;font:9px Arial;text-transform:uppercase;color:var(--green)}.neon-quality{display:grid;grid-template-columns:2fr repeat(3,1fr);gap:10px;align-items:center;border:1px solid var(--line);border-radius:18px;padding:15px 18px;margin-top:14px;background:#05171a}.neon-quality p{font-size:11px;line-height:1.4;color:var(--muted)}.neon-quality span{text-align:center;color:var(--green);font-size:10px}.neon-main{display:grid;grid-template-columns:1.25fr .75fr;gap:14px;margin-top:14px}.neon-gallery{border:1px solid var(--line);border-radius:18px;background:#05171a;padding:13px}.neon-gallery h2{text-transform:uppercase;color:var(--green);text-align:center;font-size:15px}.neon-gallery>div{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.neon-gallery .offer-photo img{border-color:#355d45}.neon-tables .table-group{background:#05171a}.neon-tables .table-head h3{color:var(--green);text-transform:uppercase}.neon-total{display:grid;grid-template-columns:1.35fr .65fr;gap:14px;border:1px solid var(--line);border-radius:18px;padding:18px;margin-top:14px;background:#052019}.neon-total h2{margin:0 0 12px;color:var(--green);font-size:17px}.neon-total>div:last-child{text-align:right}.neon-total>div>b{display:block;color:var(--green);font-size:30px}.neon-total>div>span{display:inline-block;padding:5px 9px;border-radius:999px;background:#0b4a29;color:#8affaa}.theme-neon-emerald .metric b,.theme-neon-emerald .row-main b{color:#fff}.theme-neon-emerald .info-card{background:#05171a}.theme-neon-emerald .footer-action.order{background:linear-gradient(135deg,#14a94e,#087035);box-shadow:0 0 22px rgba(69,242,125,.17)}
|
||
.info-grid.single,.bg-bottom.single,.eg-main.single{grid-template-columns:1fr}.eg-facts{grid-template-columns:repeat(var(--fact-count,3),1fr)}
|
||
.offer-note{font-size:9px;color:var(--muted);margin:12px 0 0;text-align:center;line-height:1.45}@media(max-width:820px){.page{padding:14px 13px 34px}.brand-logo{width:165px}.brand-meta{display:none}.hero,.mg-hero,.eg-hero,.neon-hero{grid-template-columns:1fr}.hero-copy,.mg-hero>div,.eg-copy{padding:28px 24px}.hero h1,.mg-hero h1,.eg-copy h1{font-size:37px}.hero-photo,.hero-photo img,.mg-hero>img{min-height:260px}.menu-grid,.info-grid,.mg-split,.mc-menu,.bg-bottom,.eg-main,.neon-main,.neon-total{grid-template-columns:1fr}.gallery{grid-template-columns:repeat(3,1fr)}.product-grid{grid-template-columns:1fr 1fr}.bg-hero{height:470px}.bg-overlay{width:78%;left:25px;top:38px}.bg-overlay h1{font-size:43px}.mc-intro{grid-template-columns:1fr}.mc-note{border-left:0;border-top:1px solid var(--line);padding:15px 0 0}.mc-total{grid-template-columns:1fr}.mc-price{text-align:left}.eg-photo-stack{min-height:330px}.neon-quality{grid-template-columns:1fr 1fr}.summary{grid-template-columns:1fr}.summary-price{border-left:0;border-top:1px solid var(--line)}}
|
||
@media(max-width:520px){.page{padding:11px 9px 28px}.hero h1,.mg-hero h1,.eg-copy h1{font-size:32px}.quality-icons{grid-template-columns:1fr}.product-grid{grid-template-columns:1fr}.gallery{grid-template-columns:repeat(2,1fr)}.table-head,.table-line{grid-template-columns:minmax(0,1fr) 108px 52px}.table-head{font-size:8px}.mc-photo-column{grid-template-columns:1fr 1fr}.neon-gallery>div{grid-template-columns:1fr 1fr}.metrics{grid-template-columns:1fr}.metric{padding:8px;border-bottom:1px solid var(--line)}.extra-grid,.addons-strip .extra-grid{grid-template-columns:1fr}.footer-actions{grid-template-columns:1fr}.final-gallery-grid{grid-template-columns:1fr}.bg-overlay h1{font-size:34px}}
|
||
@media print{@page{size:A4;margin:8mm}html,body{background:var(--cream)!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}.page{width:100%!important;max-width:none!important;padding:0!important}.brandbar,.hero,.mg-hero,.mc-intro,.bg-hero,.eg-hero,.neon-hero,.offer-row,.table-group,.product-card,.summary,.info-card,.bg-bottom>article,.addons-strip,.eg-menu,.eg-delivery,.eg-addons,.neon-quality,.neon-gallery,.neon-total{break-inside:avoid;page-break-inside:avoid}.footer-actions{display:none!important}.row-main small,.table-line small,.control-list li{overflow-wrap:anywhere}.final-photo-card{break-before:page;page-break-before:always}.final-gallery-card{break-inside:avoid;page-break-inside:avoid}.final-gallery-grid figure{break-inside:avoid;page-break-inside:avoid}}
|
||
</style></head><body><main class="page">${body}${footerNote}${finalPhoto}<footer class="footer"><span>Солнце Кейтеринг · Санкт-Петербург</span><span>+7 (921) 954-04-40 · solnce-keytering.ru</span></footer></main></body></html>`;
|
||
}
|
||
let offerRenderSeq=0;
|
||
let currentOffer={orderId:null,snapshot:null,prepared:null,pdfPrepared:null,pages:null,pdfBlob:null,pdfPromise:null,renderToken:0,preferDraft:false,transient:false,editTimer:null};
|
||
function renderOfferPages(pages){const host=$('sunClientOfferPreview');if(!host)return;host.innerHTML='';for(const page of (pages||[])){page.classList.add('sun-offer-page-canvas');page.setAttribute('aria-label','Страница предложения');host.appendChild(page)}}
|
||
function offerRenderMessage(text='Обновляю предложение…'){const host=$('sunClientOfferPreview');if(host)host.innerHTML=`<div class="sun-offer-rendering">${esc(text)}</div>`}
|
||
async function renderOffer(snapshot){
|
||
const renderToken=++offerRenderSeq;currentOffer.snapshot=snapshot;currentOffer.pdfBlob=null;currentOffer.pdfPromise=null;currentOffer.pages=null;offerRenderMessage();const pdfBtn=$('sunOfferPdf');if(pdfBtn){pdfBtn.disabled=true;pdfBtn.textContent='Готовлю просмотр…'}
|
||
const prepared=await preparedSnapshot(snapshot);const explicitTemplate=String(snapshot?.offerTemplateId||prepared?.offerTemplateId||'').trim();prepared.offerTemplateId=OFFER_TEMPLATE_IDS.has(explicitTemplate)?explicitTemplate:offerTemplateId();
|
||
const pdfPrepared=await preparedPdfSnapshot(prepared),pages=await renderOfferPdfPages(pdfPrepared);
|
||
if(renderToken!==offerRenderSeq)return null;
|
||
currentOffer.snapshot=snapshot;currentOffer.prepared=prepared;currentOffer.pdfPrepared=pdfPrepared;currentOffer.pages=pages;currentOffer.renderToken=renderToken;
|
||
renderOfferPages(pages);syncOfferEditor();
|
||
if(pdfBtn){pdfBtn.disabled=false;pdfBtn.textContent='Скачать PDF'}
|
||
return pages;
|
||
}
|
||
function toggleOfferEditor(){const panel=$('sunOfferEditor'),btn=$('sunOfferEdit');if(!panel)return;const on=!panel.classList.contains('on');panel.classList.toggle('on',on);if(btn)btn.textContent=on?'Скрыть редактор':'Редактировать';if(on)syncOfferEditor()}
|
||
function offerOverrideTextValue(key){const over=currentOffer.snapshot?.offerOverrides?.texts||{},cfg=offerSettings();return Object.prototype.hasOwnProperty.call(over,key)?String(over[key]??''):String(cfg.texts?.[key]??'')}
|
||
function offerOverrideDataValue(key){const over=currentOffer.snapshot?.offerOverrides?.data||{},snap=currentOffer.snapshot||{};if(Object.prototype.hasOwnProperty.call(over,key))return over[key]??'';return key==='guests'?(snap.guests??''):(snap[key]??'')}
|
||
function syncOfferEditor(){const panel=$('sunOfferEditor');if(!panel||!currentOffer.snapshot)return;panel.querySelectorAll('[data-offer-text]').forEach(el=>{if(document.activeElement!==el)el.value=offerOverrideTextValue(el.dataset.offerText)});panel.querySelectorAll('[data-offer-data]').forEach(el=>{if(document.activeElement!==el)el.value=offerOverrideDataValue(el.dataset.offerData)});panel.querySelectorAll('[data-offer-list="controlLines"]').forEach(el=>{if(document.activeElement!==el){const over=currentOffer.snapshot?.offerOverrides?.controlLines;const lines=Array.isArray(over)?over:offerSettings().controlLines;el.value=(lines||[]).join('\n')}})}
|
||
function saveOfferOverrides(){if(!currentOffer.snapshot)return;currentOffer.snapshot.createdAt=new Date().toISOString();if(!currentOffer.transient&¤tOffer.orderId)persistSnapshot(currentOffer.orderId,currentOffer.snapshot);const meta=$('sunOfferMeta');if(meta)meta.textContent=`Изменено ${new Date(currentOffer.snapshot.createdAt).toLocaleString('ru-RU')}`}
|
||
function scheduleOfferEditRender(){clearTimeout(currentOffer.editTimer);currentOffer.editTimer=setTimeout(()=>{if(currentOffer.snapshot)renderOffer(currentOffer.snapshot).catch(err=>{console.error(err);toast('Не удалось обновить предпросмотр.','error')})},500)}
|
||
function bindOfferEditor(){const panel=$('sunOfferEditor');if(!panel||panel.dataset.bound)return;panel.dataset.bound='1';panel.addEventListener('input',e=>{const el=e.target.closest('[data-offer-text],[data-offer-data],[data-offer-list]');if(!el||!currentOffer.snapshot)return;currentOffer.snapshot.offerOverrides=currentOffer.snapshot.offerOverrides||{};if(el.dataset.offerText){currentOffer.snapshot.offerOverrides.texts=currentOffer.snapshot.offerOverrides.texts||{};currentOffer.snapshot.offerOverrides.texts[el.dataset.offerText]=el.value}else if(el.dataset.offerList==='controlLines'){currentOffer.snapshot.offerOverrides.controlLines=String(el.value||'').split(/\r?\n/).map(x=>x.trim()).filter(Boolean).slice(0,20)}else{currentOffer.snapshot.offerOverrides.data=currentOffer.snapshot.offerOverrides.data||{};currentOffer.snapshot.offerOverrides.data[el.dataset.offerData]=el.dataset.offerData==='guests'?Math.max(0,Number(el.value||0)):el.value}currentOffer.pdfBlob=null;currentOffer.pages=null;scheduleOfferEditRender()});panel.addEventListener('change',saveOfferOverrides);panel.addEventListener('focusout',e=>{if(e.target.matches?.('[data-offer-text],[data-offer-data],[data-offer-list]'))saveOfferOverrides()})}
|
||
function setMeta(order,snapshot,{transient=false,autoRefreshed=false}={}){
|
||
const stale=snapshotIsStale(order,snapshot),notice=$('sunOfferStale');
|
||
if(notice){
|
||
notice.classList.toggle('on',Boolean(stale||transient));
|
||
notice.textContent=transient
|
||
?'В предложении учтены текущие изменения заказа. Сохраните заказ, чтобы закрепить их.'
|
||
:(stale?'Заказ изменён. Предложение обновляется по актуальным данным…':'');
|
||
}
|
||
if($('sunOfferMeta'))$('sunOfferMeta').textContent=`Версия от ${new Date(snapshot.createdAt).toLocaleString('ru-RU')}${autoRefreshed?' · обновлено автоматически':''}`;
|
||
}
|
||
|
||
async function openOfferForOrder(orderId,{forceRefresh=false,preferDraft=false}={}){
|
||
ensureModal();
|
||
const saved=currentSavedOrder(orderId);if(!saved)return toast('Заказ не найден.','error');
|
||
const order=currentOfferSourceOrder(orderId,preferDraft)||saved;
|
||
const draftDiff=order!==saved&&orderSignature(order)!==orderSignature(saved);
|
||
let snapshot=saved.clientOfferSnapshot,autoRefreshed=false;
|
||
const stale=snapshotIsStale(order,snapshot);
|
||
if(stale||forceRefresh){
|
||
if(forceRefresh)toast('Обновляю предложение…');
|
||
snapshot=await createSnapshot(order);autoRefreshed=stale&&!forceRefresh;
|
||
// Never freeze an unsaved draft into the saved order. Saved orders, however,
|
||
// always receive the fresh snapshot so every template/PDF opens consistently.
|
||
if(!draftDiff&&!persistSnapshot(saved.id,snapshot))return;
|
||
}
|
||
currentOffer={orderId:saved.id,snapshot,prepared:null,pdfPrepared:null,pages:null,pdfBlob:null,pdfPromise:null,renderToken:0,preferDraft:Boolean(preferDraft),transient:draftDiff,editTimer:null};
|
||
setMeta(order,snapshot,{transient:draftDiff,autoRefreshed});
|
||
window.modal?.('sunClientOfferModal');await renderOffer(snapshot);
|
||
if(autoRefreshed)toast('Предложение автоматически обновлено по текущему заказу.');
|
||
}
|
||
async function openCurrentDraftOffer(){
|
||
const d=getDraft();if(!d?.id)return alert('Сначала сохраните заказ. После сохранения появится предложение клиенту.');
|
||
const saved=currentSavedOrder(d.id);if(!saved)return alert('Сначала сохраните заказ.');
|
||
return openOfferForOrder(saved.id,{preferDraft:true});
|
||
}
|
||
async function refreshCurrentOffer(){
|
||
if(!currentOffer.orderId)return;
|
||
await openOfferForOrder(currentOffer.orderId,{forceRefresh:true,preferDraft:currentOffer.preferDraft});
|
||
toast('Предложение обновлено.');
|
||
}
|
||
async function ensureCurrentOfferFresh(){
|
||
if(!currentOffer.orderId)return false;
|
||
const saved=currentSavedOrder(currentOffer.orderId);if(!saved)return false;
|
||
const order=currentOfferSourceOrder(currentOffer.orderId,currentOffer.preferDraft)||saved;
|
||
if(!snapshotIsStale(order,currentOffer.snapshot))return true;
|
||
await openOfferForOrder(currentOffer.orderId,{forceRefresh:true,preferDraft:currentOffer.preferDraft});
|
||
return true;
|
||
}
|
||
function filename(ext){const d=currentOffer.prepared?.date||currentOffer.snapshot?.date||'';return `predlozhenie-klientu${d?'-'+d:''}.${ext}`}
|
||
|
||
function pdfRoundRect(ctx,x,y,w,h,r,fill,stroke){
|
||
const rr=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+rr,y);ctx.arcTo(x+w,y,x+w,y+h,rr);ctx.arcTo(x+w,y+h,x,y+h,rr);ctx.arcTo(x,y+h,x,y,rr);ctx.arcTo(x,y,x+w,y,rr);ctx.closePath();if(fill){ctx.fillStyle=fill;ctx.fill()}if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=2;ctx.stroke()}
|
||
}
|
||
function pdfWrap(ctx,text,maxWidth){
|
||
const input=String(text||'').replace(/\s+/g,' ').trim();if(!input)return[];
|
||
const words=input.split(' '),lines=[];let line='';
|
||
const pushLong=word=>{let chunk='';for(const ch of word){const next=chunk+ch;if(ctx.measureText(next).width>maxWidth&&chunk){lines.push(chunk);chunk=ch}else chunk=next}return chunk};
|
||
for(const word of words){
|
||
if(ctx.measureText(word).width>maxWidth){if(line){lines.push(line);line=''}line=pushLong(word);continue}
|
||
const test=line?`${line} ${word}`:word;
|
||
if(ctx.measureText(test).width<=maxWidth)line=test;else{if(line)lines.push(line);line=word}
|
||
}
|
||
if(line)lines.push(line);return lines;
|
||
}
|
||
function pdfText(ctx,text,x,y,maxWidth,lineHeight,opts={}){
|
||
ctx.save();ctx.font=opts.font||'28px Arial';ctx.fillStyle=opts.color||'#1a1a1a';ctx.textAlign=opts.align||'left';ctx.textBaseline='top';
|
||
let lines=pdfWrap(ctx,text,maxWidth);const maxLines=Math.max(0,Number(opts.maxLines||0));
|
||
if(maxLines&&lines.length>maxLines){lines=lines.slice(0,maxLines);let last=lines[lines.length-1]||'';while(last&&ctx.measureText(last+'...').width>maxWidth)last=last.slice(0,-1);lines[lines.length-1]=last+'...'}
|
||
const lh=Math.max(lineHeight||0,Math.ceil(parseFloat(String(ctx.font).match(/([0-9.]+)px/)?.[1]||16)*1.18));
|
||
lines.forEach((line,i)=>ctx.fillText(line,x,y+i*lh));ctx.restore();return lines.length*lh;
|
||
}
|
||
function pdfDrawCheck(ctx,x,y,r,fill,stroke){ctx.save();ctx.fillStyle=fill;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();ctx.strokeStyle=stroke;ctx.lineWidth=Math.max(2,r*.22);ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(x-r*.45,y);ctx.lineTo(x-r*.1,y+r*.34);ctx.lineTo(x+r*.52,y-r*.38);ctx.stroke();ctx.restore()}
|
||
function pdfLoadImage(src){return new Promise(resolve=>{const safe=isSafeRasterDataUrl(src)?String(src):'';if(!safe)return resolve(null);const img=new Image();let done=false;const finish=v=>{if(done)return;done=true;clearTimeout(timer);resolve(v)};const timer=setTimeout(()=>finish(null),4500);img.onload=()=>finish(img);img.onerror=()=>finish(null);img.src=safe})}
|
||
function pdfDrawCover(ctx,img,x,y,w,h,r=24){
|
||
if(!img)return;const scale=Math.max(w/img.naturalWidth,h/img.naturalHeight),sw=w/scale,sh=h/scale,sx=(img.naturalWidth-sw)/2,sy=(img.naturalHeight-sh)/2;
|
||
ctx.save();pdfRoundRect(ctx,x,y,w,h,r);ctx.clip();ctx.drawImage(img,sx,sy,sw,sh,x,y,w,h);ctx.restore();
|
||
}
|
||
function pdfDrawContain(ctx,img,x,y,w,h){
|
||
if(!img)return;const scale=Math.min(w/img.naturalWidth,h/img.naturalHeight),dw=img.naturalWidth*scale,dh=img.naturalHeight*scale;ctx.drawImage(img,x+(w-dw)/2,y+(h-dh)/2,dw,dh);
|
||
}
|
||
const pdfFromJpegs=images=>window.SunPdfEngine.fromJpegs(images);
|
||
|
||
function pdfCanvasJpeg(canvas){
|
||
return new Promise((resolve,reject)=>{
|
||
let done=false;const finish=(err,value)=>{if(done)return;done=true;clearTimeout(timer);err?reject(err):resolve(value)};
|
||
const fromDataUrl=()=>{try{const data=canvas.toDataURL('image/jpeg',0.94),base64=String(data).split(',')[1]||'';if(!base64)throw new Error('Пустое изображение страницы');const raw=atob(base64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);finish(null,{width:canvas.width,height:canvas.height,bytes})}catch(err){finish(err)}};
|
||
const timer=setTimeout(fromDataUrl,1800);
|
||
try{
|
||
if(typeof canvas.toBlob!=='function')return fromDataUrl();
|
||
canvas.toBlob(async blob=>{if(done)return;if(!blob)return fromDataUrl();try{finish(null,{width:canvas.width,height:canvas.height,bytes:new Uint8Array(await blob.arrayBuffer())})}catch(err){fromDataUrl()}},'image/jpeg',0.94);
|
||
}catch(_){fromDataUrl()}
|
||
})
|
||
}
|
||
|
||
function pdfThemeFor(id){
|
||
const themes={
|
||
light:{BG:'#ffffff',PANEL:'#ffffff',LINE:'#e7d8bd',TEXT:'#1d211d',MUTED:'#6f746e',GOLD:'#c99b47',GOLD2:'#e2c28b',GREEN:'#476f4d',GREEN_DEEP:'#28533a',GREEN_SOFT:'#f1f5ef',FOOT:'#ffffff'},
|
||
'editorial-grid':{BG:'#fbfaf6',PANEL:'#fffefb',LINE:'#e8dfcf',TEXT:'#163127',MUTED:'#776f62',GOLD:'#b99355',GOLD2:'#d8c4a2',GREEN:'#245f46',GREEN_DEEP:'#1f5a42',GREEN_SOFT:'#e9f1eb',FOOT:'#fbfaf6'},
|
||
'midnight-glass':{BG:'#061521',PANEL:'#0b2230',LINE:'#315263',TEXT:'#f4f1e8',MUTED:'#9aaeb8',GOLD:'#d4a64b',GOLD2:'#765f35',GREEN:'#24a98f',GREEN_DEEP:'#d7ad58',GREEN_SOFT:'#123d38',FOOT:'#04111b'},
|
||
'midnight-compact':{BG:'#03131c',PANEL:'#0a1b25',LINE:'#3a4d57',TEXT:'#f4f3ef',MUTED:'#9daab1',GOLD:'#e0b751',GOLD2:'#5e5231',GREEN:'#62a868',GREEN_DEEP:'#f1c96c',GREEN_SOFT:'#264b37',FOOT:'#06141c'},
|
||
'black-gold':{BG:'#0c0c0b',PANEL:'#151513',LINE:'#453d2e',TEXT:'#f2eee6',MUTED:'#aaa49b',GOLD:'#c99a3c',GOLD2:'#57451f',GREEN:'#b98a31',GREEN_DEEP:'#d2a650',GREEN_SOFT:'#342c1d',FOOT:'#090908'},
|
||
'emerald-gold':{BG:'#061318',PANEL:'#0b2023',LINE:'#3b4f50',TEXT:'#f2efe7',MUTED:'#a7b0ae',GOLD:'#d7ac55',GOLD2:'#6a5930',GREEN:'#1c765d',GREEN_DEEP:'#d9b466',GREEN_SOFT:'#123b35',FOOT:'#07171a'},
|
||
'neon-emerald':{BG:'#020e12',PANEL:'#06191d',LINE:'#277b4d',TEXT:'#effff5',MUTED:'#9cb3aa',GOLD:'#d0a841',GOLD2:'#665322',GREEN:'#39f07c',GREEN_DEEP:'#67ff91',GREEN_SOFT:'#0c3f27',FOOT:'#020b0e'}
|
||
};
|
||
return themes[id]||themes.light;
|
||
}
|
||
async function renderOfferPdfPagesBase(s){
|
||
const id=s.offerTemplateId||offerTemplateId(),W=1000,H=1414,SCALE=1.6,M=52,T=pdfThemeFor(id),{BG,PANEL,LINE,TEXT,MUTED,GOLD,GOLD2,GREEN,GREEN_DEEP,GREEN_SOFT,FOOT}=T;
|
||
const cfg=offerSettingsForSnapshot(s),items=s.items||[],imageMap=new Map();
|
||
for(const item of items)imageMap.set(item.id,await pdfLoadImage(item.photoData||s.logo||''));
|
||
const logo=await pdfLoadImage(s.logo||'');
|
||
const finalGallery=cfg.showFinalPhoto?(await Promise.all((Array.isArray(s.finalGallery)?s.finalGallery:[]).map(pdfLoadImage))).filter(Boolean):[];
|
||
const heroImg=items.map(i=>imageMap.get(i.id)).find(Boolean)||finalGallery[0]||logo,p=s.pricing||{},g=num(s.guests),menuPer=perGuest(p.itemsTotal,g),boxCount=offerBoxCount(items),discountPct=p.base>0?Math.round((p.discount/p.base)*100):0;
|
||
const primaryMetric=offerPrimaryMetric(s,cfg,g),visibleDate=cfg.showDate?dateText(s.date):'',visibleClient=cfg.showClientName?String(s.client||'').trim():'',visibleGuests=cfg.showGuests&&g?`${g} ${plural(g,'гость','гостя','гостей')}`:'';
|
||
const identity=[visibleClient,visibleDate,visibleGuests].filter(Boolean).join(' · '),control=Array.isArray(cfg.controlLines)&&cfg.controlLines.length?cfg.controlLines:DELIVERY_CONTROL,extras=Array.isArray(cfg.extraServices)&&cfg.extraServices.length?cfg.extraServices:EXTRA_SERVICES.map(x=>x[0]);
|
||
const canvases=[];let canvas,ctx,y;const bottom=H-78;
|
||
function newPage(){canvas=document.createElement('canvas');canvas.width=Math.round(W*SCALE);canvas.height=Math.round(H*SCALE);ctx=canvas.getContext('2d',{alpha:false});ctx.setTransform(SCALE,0,0,SCALE,0,0);ctx.fillStyle=BG;ctx.fillRect(0,0,W,H);y=M;canvases.push(canvas);return ctx}
|
||
function space(h){if(y+h>bottom)newPage()}
|
||
function fitBlockHeight(natural,minHeight=natural,gap=0,softRatio=.84){
|
||
const h=Math.max(1,Number(natural||0)),minH=Math.min(h,Math.max(1,Number(minHeight||h))),available=Math.max(0,bottom-y-gap);
|
||
if(h<=available)return h;
|
||
if(available>=minH&&available>=h*softRatio)return available;
|
||
newPage();return h;
|
||
}
|
||
function card(x,yy,w,h,r=20,fill=PANEL,stroke=LINE){pdfRoundRect(ctx,x,yy,w,h,r,fill,stroke)}
|
||
function line(x1,yy1,x2,yy2,color=LINE,width=1){const snap=v=>width===1?Math.round(v)+.5:v;ctx.save();ctx.strokeStyle=color;ctx.lineWidth=width;ctx.beginPath();ctx.moveTo(snap(x1),snap(yy1));ctx.lineTo(snap(x2),snap(yy2));ctx.stroke();ctx.restore()}
|
||
function wrapCount(text,width,font){ctx.save();ctx.font=font;const n=Math.max(1,pdfWrap(ctx,String(text||''),width).length);ctx.restore();return n}
|
||
function wrappedLines(text,width,font){ctx.save();ctx.font=font;const out=pdfWrap(ctx,String(text||''),width);ctx.restore();return out}
|
||
function drawWrappedLines(lines,x,yy,lineHeight,{font='12px Arial',color=TEXT,align='left'}={}){ctx.save();ctx.font=font;ctx.fillStyle=color;ctx.textAlign=align;ctx.textBaseline='top';lines.forEach((line,i)=>ctx.fillText(line,x,yy+i*lineHeight));ctx.restore()}
|
||
function compositionTextPdf(item,sep=' · '){return offerComposition(item).join(sep)}
|
||
function brandHeader(){space(82);if(logo)pdfDrawContain(ctx,logo,M,y,195,62);if(visibleDate)pdfText(ctx,visibleDate,W-M,y+24,300,24,{font:'14px Arial',color:MUTED,align:'right',maxLines:1});y+=78}
|
||
function hero(){
|
||
const event=s.event||'Персональное предложение';
|
||
if(id==='black-gold'){
|
||
space(425);const h=405;card(M,y,W-2*M,h,24,'#0c0c0b',LINE);if(heroImg)pdfDrawCover(ctx,heroImg,M,y,W-2*M,h,24);const grad=ctx.createLinearGradient(M,y,M+650,y);grad.addColorStop(0,'rgba(0,0,0,.94)');grad.addColorStop(.68,'rgba(0,0,0,.48)');grad.addColorStop(1,'rgba(0,0,0,0)');ctx.save();pdfRoundRect(ctx,M,y,W-2*M,h,24);ctx.clip();ctx.fillStyle=grad;ctx.fillRect(M,y,W-2*M,h);ctx.restore();pdfText(ctx,cfg.texts.proposalLabel,M+34,y+36,430,22,{font:'700 12px Arial',color:GOLD,maxLines:1});pdfText(ctx,'Кейтеринг для вашего события',M+34,y+78,500,56,{font:'500 43px Georgia',color:'#ffffff',maxLines:3});pdfText(ctx,[event,identity].filter(Boolean).join(' · '),M+34,y+273,510,28,{font:'16px Arial',color:'#ded8cd',maxLines:3});y+=h+18;return;
|
||
}
|
||
if(id==='editorial-grid'){
|
||
const leftW=386,rightX=M+leftW,rightW=W-M-rightX,leftTextW=leftW-56,eventLines=wrappedLines(event,leftTextW,'500 34px Georgia'),noteLines=wrappedLines(cfg.texts.heroNote,leftTextW,'12px Arial');
|
||
const h=Math.max(350,94+eventLines.length*43+(visibleClient?31:0)+([visibleDate,visibleGuests].filter(Boolean).length?42:0)+Math.max(52,noteLines.length*19+18));
|
||
space(h+18);card(M,y,W-2*M,h,18,PANEL,LINE);if(heroImg)pdfDrawCover(ctx,heroImg,rightX+1,y+1,rightW-1,h-2,17);
|
||
pdfText(ctx,cfg.texts.proposalLabel,M+26,y+26,leftTextW,20,{font:'700 11px Arial',color:GOLD,maxLines:2});
|
||
drawWrappedLines(eventLines,M+26,y+62,43,{font:'500 34px Georgia',color:TEXT});let ty=y+62+eventLines.length*43+12;
|
||
if(visibleClient){pdfText(ctx,visibleClient,M+26,ty,leftTextW,25,{font:'500 17px Georgia',color:GREEN_DEEP,maxLines:1});ty+=31}
|
||
const facts=[visibleDate,visibleGuests].filter(Boolean).join(' · ');if(facts){pdfText(ctx,facts,M+26,ty,leftTextW,22,{font:'12px Arial',color:TEXT,maxLines:2});ty+=38}
|
||
line(M+26,ty,M+26+leftTextW,ty,GOLD2,1);ty+=15;if(noteLines.length)drawWrappedLines(noteLines,M+26,ty,19,{font:'12px Arial',color:MUTED});
|
||
y+=h+18;return;
|
||
}
|
||
if(id==='midnight-compact'){
|
||
const innerX=M+28,innerW=W-2*M-56,leftW=510,gap=28,rightX=innerX+leftW+gap,rightW=innerW-leftW-gap;
|
||
const eventLines=wrapCount(event,leftW,'700 31px Arial'),noteLines=wrapCount(cfg.texts.heroNote,rightW,'15px Arial'),h=Math.max(228,138+Math.max(eventLines*42,noteLines*21));space(h+20);card(M,y,W-2*M,h,20,PANEL,LINE);
|
||
pdfText(ctx,cfg.texts.proposalLabel,innerX,y+24,leftW,22,{font:'700 12px Arial',color:GOLD,maxLines:1});
|
||
pdfText(ctx,event,innerX,y+57,leftW,42,{font:'700 31px Arial',color:TEXT,maxLines:3});
|
||
line(rightX-gap/2,y+28,rightX-gap/2,y+h-28,LINE,1);
|
||
pdfText(ctx,cfg.texts.heroNote,rightX,y+55,rightW,21,{font:'15px Arial',color:GREEN_DEEP,maxLines:7});
|
||
if(identity)pdfText(ctx,identity,innerX,y+h-39,leftW,23,{font:'13px Arial',color:MUTED,maxLines:1});
|
||
y+=h+18;return;
|
||
}
|
||
const left=id==='neon-emerald'?500:425,rightX=M+left,rightW=W-M-rightX,leftTextW=left-56;
|
||
const eventFont=id==='neon-emerald'?'700 34px Arial':'500 38px Georgia',eventLH=48,noteFont='12px Arial',noteLH=20;
|
||
const eventLines=wrappedLines(event,leftTextW,eventFont),noteLines=wrappedLines(cfg.texts.heroNote,leftTextW,noteFont);
|
||
const identityRows=(visibleClient?31:0)+([visibleDate,visibleGuests].filter(Boolean).length?52:0);
|
||
const textNeed=68+Math.max(1,eventLines.length)*eventLH+24+identityRows+(noteLines.length?Math.max(54,noteLines.length*noteLH+14):0)+22;
|
||
const h=Math.max(id==='emerald-gold'?380:335,textNeed);space(h+20);card(M,y,W-2*M,h,24,PANEL,LINE);if(heroImg)pdfDrawCover(ctx,heroImg,rightX+1,y+1,rightW-1,h-2,22);
|
||
pdfText(ctx,cfg.texts.proposalLabel,M+28,y+29,leftTextW,22,{font:'700 12px Arial',color:id==='neon-emerald'?GREEN:GOLD,maxLines:2});
|
||
drawWrappedLines(eventLines,M+28,y+68,eventLH,{font:eventFont,color:TEXT});
|
||
let ty=y+68+Math.max(1,eventLines.length)*eventLH+20;if(visibleClient){pdfText(ctx,visibleClient,M+28,ty,leftTextW,27,{font:'500 18px Georgia',color:GREEN_DEEP,maxLines:1});ty+=31}
|
||
const facts=[visibleDate,visibleGuests].filter(Boolean).join(' · ');if(facts){pdfText(ctx,facts,M+28,ty,leftTextW,26,{font:'14px Arial',color:TEXT,maxLines:2});ty+=48}
|
||
if(noteLines.length){line(M+28,ty-8,M+28+leftTextW,ty-8,GOLD2,1);drawWrappedLines(noteLines,M+28,ty+6,noteLH,{font:noteFont,color:MUTED})}
|
||
y+=h+18;
|
||
}
|
||
function sellingBlock(){
|
||
const title=String(cfg.texts.salesTitle||'').trim(),body=String(cfg.texts.salesNote||'').trim();if(!title&&!body)return;
|
||
const x=M,w=W-2*M,padX=30,titleFont='500 22px Georgia',bodyFont='14px Arial',titleLH=30,bodyLH=23,innerW=w-padX*2-12;
|
||
let bodyLines=wrappedLines(body,innerW,bodyFont),first=true;
|
||
do{
|
||
const titleLines=first&&title?wrappedLines(title,innerW,titleFont):[];
|
||
const headingH=titleLines.length?titleLines.length*titleLH+12:0;
|
||
const minRows=Math.min(3,Math.max(1,bodyLines.length||1));
|
||
let minH=30+headingH+(bodyLines.length?minRows*bodyLH:0)+24;
|
||
if(bottom-y<minH)newPage();
|
||
const available=Math.max(120,bottom-y),maxRows=Math.max(1,Math.floor((available-54-headingH)/bodyLH));
|
||
const chunk=bodyLines.splice(0,maxRows),h=Math.min(available,30+headingH+Math.max(0,chunk.length*bodyLH)+24);
|
||
card(x,y,w,h,18,PANEL,LINE);ctx.save();ctx.fillStyle=GOLD;pdfRoundRect(ctx,x+18,y+22,6,Math.max(34,h-44),3,GOLD);ctx.restore();
|
||
let yy=y+22;if(titleLines.length){drawWrappedLines(titleLines,x+38,yy,titleLH,{font:titleFont,color:GREEN_DEEP});yy+=headingH}
|
||
if(chunk.length)drawWrappedLines(chunk,x+38,yy,bodyLH,{font:bodyFont,color:TEXT});
|
||
y+=h+18;first=false;if(bodyLines.length)newPage();
|
||
}while(bodyLines.length);
|
||
}
|
||
function sectionTitlePdf(text=cfg.texts.menuTitle){const titleFont=id==='black-gold'?'500 27px Georgia':id==='editorial-grid'?'500 24px Georgia':'700 24px Arial',lines=wrapCount(text,W-2*M,titleFont),h=Math.max(56,24+lines*30);space(h);pdfText(ctx,text,M,y+4,W-2*M,30,{font:titleFont,color:GREEN_DEEP,align:id==='black-gold'?'left':'center',maxLines:2});line(M,y+h-10,W-M,y+h-10,GOLD2,1);y+=h}
|
||
function itemCardLight(item,x,yy,w,forcedHeight=0){
|
||
const nameLines=Math.max(1,wrapCount(item.name,w-224,'500 18px Georgia'));
|
||
const meta=[weightLabel(item),compositionTextPdf(item)].filter(Boolean).join(' · ');
|
||
const metaLines=meta?Math.max(1,wrapCount(meta,w-230,'11px Arial')):0;
|
||
const textH=nameLines*27+(metaLines?8+metaLines*20:0),minH=36+textH,naturalH=Math.max(150,minH),h=Math.max(minH,Number(forcedHeight||naturalH));
|
||
card(x,yy,w,h,17,PANEL,LINE);
|
||
const img=imageMap.get(item.id)||logo;if(img)pdfDrawCover(ctx,img,x+10,yy+10,120,h-20,13);
|
||
pdfText(ctx,item.name,x+146,yy+18,w-224,27,{font:'500 18px Georgia',color:TEXT,maxLines:nameLines});
|
||
if(meta)pdfText(ctx,meta,x+146,yy+18+nameLines*27+8,w-230,20,{font:'11px Arial',color:MUTED,maxLines:metaLines});
|
||
card(x+w-70,yy+16,50,30,15,PANEL,LINE);
|
||
pdfText(ctx,`x${Math.round(num(item.qty))}`,x+w-45,yy+22,42,19,{font:'14px Arial',color:TEXT,align:'center',maxLines:1});
|
||
return h
|
||
}
|
||
function drawItems(){
|
||
sectionTitlePdf(id==='black-gold'?`${cfg.texts.menuTitle} для вашего мероприятия`:cfg.texts.menuTitle);
|
||
if(id==='black-gold'){
|
||
const gap=12,cw=(W-2*M-gap*2)/3;
|
||
for(let i=0;i<items.length;i+=3){
|
||
const row=items.slice(i,i+3);
|
||
const measured=row.map(item=>{
|
||
const nameLines=Math.max(1,wrapCount(item.name,cw-32,'500 17px Georgia'));
|
||
const meta=[weightLabel(item),compositionTextPdf(item,', ')].filter(Boolean).join(' · ');
|
||
const metaLines=meta?Math.max(1,wrapCount(meta,cw-32,'10px Arial')):0;
|
||
const minH=252+nameLines*24+(metaLines?8+metaLines*19:0)+18;
|
||
return {item,nameLines,meta,metaLines,minH,h:Math.max(340,minH)};
|
||
});
|
||
const naturalRh=Math.max(...measured.map(v=>v.h),340),minRh=Math.max(...measured.map(v=>v.minH)),rh=fitBlockHeight(naturalRh,minRh,14,.88);
|
||
measured.forEach((v,j)=>{
|
||
const {item,nameLines,meta,metaLines}=v,x=M+j*(cw+gap);
|
||
card(x,y,cw,rh,16,PANEL,LINE);const img=imageMap.get(item.id)||logo;if(img)pdfDrawCover(ctx,img,x+1,y+1,cw-2,205,15);
|
||
pdfText(ctx,String(i+j+1).padStart(2,'0'),x+12,y+12,38,20,{font:'700 11px Arial',color:GOLD,maxLines:1});
|
||
pdfText(ctx,`x${Math.round(num(item.qty))}`,x+cw-13,y+12,40,20,{font:'700 11px Arial',color:'#ffffff',align:'right',maxLines:1});
|
||
pdfText(ctx,item.name,x+16,y+222,cw-32,24,{font:'500 17px Georgia',color:TEXT,maxLines:nameLines});
|
||
if(meta)pdfText(ctx,meta,x+16,y+222+nameLines*24+8,cw-32,19,{font:'10px Arial',color:MUTED,maxLines:metaLines});
|
||
});
|
||
y+=rh+14
|
||
}
|
||
return;
|
||
}
|
||
if(id==='editorial-grid'){
|
||
const groups=editorialGroups(items),gap=10,cw=(W-2*M-gap)/2;
|
||
for(let i=0;i<groups.length;i+=2){
|
||
const pair=groups.slice(i,i+2),rh=78;space(rh+10);
|
||
pair.forEach((group,j)=>{
|
||
const x=M+j*(cw+gap);card(x,y,cw,rh,13,PANEL,LINE);
|
||
ctx.save();ctx.fillStyle=GREEN_DEEP;ctx.beginPath();ctx.arc(x+31,y+39,20,0,Math.PI*2);ctx.fill();ctx.restore();
|
||
pdfText(ctx,String(i+j+1).padStart(2,'0'),x+31,y+30,30,18,{font:'700 10px Arial',color:'#ffffff',align:'center',maxLines:1});
|
||
pdfText(ctx,group.name,x+62,y+15,cw-150,24,{font:'500 16px Georgia',color:TEXT,maxLines:2});
|
||
if(group.meta)pdfText(ctx,group.meta,x+62,y+48,cw-150,18,{font:'10px Arial',color:GOLD,maxLines:1});
|
||
card(x+cw-68,y+24,48,28,14,PANEL,LINE);pdfText(ctx,`x${Math.round(group.qty)}`,x+cw-44,y+29,40,18,{font:'11px Arial',color:TEXT,align:'center',maxLines:1});
|
||
});
|
||
y+=rh+10;
|
||
}
|
||
if(cfg.showGallery)drawGallery(4,126,false);return;
|
||
}
|
||
if(id==='midnight-compact'){
|
||
const groups=offerGroups(items);
|
||
for(const group of groups){
|
||
const titleLines=wrapCount(group.name,600,'700 16px Arial'),headerH=Math.max(45,18+titleLines*24);space(headerH+20);
|
||
card(M,y,W-2*M,headerH,13,PANEL,LINE);pdfText(ctx,group.name,M+18,y+12,650,24,{font:'700 16px Arial',color:GREEN_DEEP,maxLines:titleLines});y+=headerH;
|
||
for(const item of group.list){
|
||
const nameLines=Math.max(1,wrapCount(item.name,535,'14px Arial'));
|
||
const comp=compositionTextPdf(item);
|
||
const compLines=comp?Math.max(1,wrapCount(comp,535,'10px Arial')):0;
|
||
const minH=28+nameLines*21+(compLines?5+compLines*17:0)+10,naturalH=Math.max(64,minH),h=fitBlockHeight(naturalH,minH,0,.88);
|
||
line(M,y,W-M,y,LINE,1);
|
||
pdfText(ctx,item.name,M+18,y+14,535,21,{font:'14px Arial',color:TEXT,maxLines:nameLines});
|
||
if(comp)pdfText(ctx,comp,M+18,y+14+nameLines*21+5,535,17,{font:'10px Arial',color:MUTED,maxLines:compLines});
|
||
pdfText(ctx,weightLabel(item)||item.weight||'—',M+588,y+14,188,36,{font:'11px Arial',color:MUTED,align:'center',maxLines:2});
|
||
pdfText(ctx,`x${Math.round(num(item.qty))}`,W-M-25,y+14,80,21,{font:'700 13px Arial',color:TEXT,align:'right',maxLines:1});
|
||
y+=h
|
||
}
|
||
y+=10
|
||
}
|
||
if(cfg.showGallery)drawGallery(5,118,true);return;
|
||
}
|
||
if(id==='neon-emerald'){
|
||
const gap=12,cw=(W-2*M-gap)/2;
|
||
for(let i=0;i<items.length;i+=2){
|
||
const row=items.slice(i,i+2);
|
||
const measured=row.map(item=>{
|
||
const nameLines=Math.max(1,wrapCount(item.name,cw-202,'700 15px Arial'));
|
||
const meta=[weightLabel(item),compositionTextPdf(item)].filter(Boolean).join(' · ');
|
||
const metaLines=meta?Math.max(1,wrapCount(meta,cw-202,'10px Arial')):0;
|
||
const minH=38+nameLines*22+(metaLines?6+metaLines*18:0)+20;
|
||
return {item,nameLines,meta,metaLines,minH,h:Math.max(124,minH)};
|
||
});
|
||
const naturalRh=Math.max(...measured.map(v=>v.h)),minRh=Math.max(...measured.map(v=>v.minH)),rh=fitBlockHeight(naturalRh,minRh,12,.88);
|
||
measured.forEach((v,j)=>{
|
||
const {item,nameLines,meta,metaLines}=v,x=M+j*(cw+gap);card(x,y,cw,rh,16,PANEL,LINE);
|
||
const img=imageMap.get(item.id)||logo;if(img)pdfDrawCover(ctx,img,x+8,y+8,102,rh-16,12);
|
||
pdfText(ctx,item.name,x+122,y+17,cw-202,22,{font:'700 15px Arial',color:TEXT,maxLines:nameLines});
|
||
if(meta)pdfText(ctx,meta,x+122,y+17+nameLines*22+6,cw-202,18,{font:'10px Arial',color:GREEN,maxLines:metaLines});
|
||
pdfText(ctx,`x${Math.round(num(item.qty))}`,x+cw-18,y+18,55,20,{font:'700 12px Arial',color:GREEN,align:'right',maxLines:1});
|
||
});
|
||
y+=rh+12
|
||
}
|
||
return;
|
||
}
|
||
if(id==='emerald-gold'){
|
||
for(const item of items){
|
||
const nameLines=Math.max(1,wrapCount(item.name,W-2*M-220,'500 18px Georgia'));
|
||
const meta=[weightLabel(item),compositionTextPdf(item)].filter(Boolean).join(' · ');
|
||
const metaLines=meta?Math.max(1,wrapCount(meta,W-2*M-240,'10px Arial')):0;
|
||
const minH=38+nameLines*27+(metaLines?8+metaLines*19:0)+20,naturalH=Math.max(130,minH),h=fitBlockHeight(naturalH,minH,10,.88);
|
||
card(M,y,W-2*M,h,18,PANEL,LINE);const img=imageMap.get(item.id)||logo;if(img)pdfDrawCover(ctx,img,W-M-160,y+8,150,h-16,12);
|
||
pdfText(ctx,item.name,M+22,y+18,W-2*M-220,27,{font:'500 18px Georgia',color:TEXT,maxLines:nameLines});
|
||
if(meta)pdfText(ctx,meta,M+22,y+18+nameLines*27+8,W-2*M-240,19,{font:'10px Arial',color:MUTED,maxLines:metaLines});
|
||
pdfText(ctx,`x${Math.round(num(item.qty))}`,W-M-176,y+18,58,22,{font:'700 12px Arial',color:GOLD,align:'right',maxLines:1});y+=h+10
|
||
}
|
||
return;
|
||
}
|
||
const gap=14,cw=(W-2*M-gap)/2;
|
||
for(let i=0;i<items.length;i+=2){
|
||
const row=items.slice(i,i+2),measured=row.map(item=>{
|
||
const meta=[weightLabel(item),compositionTextPdf(item)].filter(Boolean).join(' · ');
|
||
const nameLines=Math.max(1,wrapCount(item.name,cw-224,'500 18px Georgia'));
|
||
const metaLines=meta?Math.max(1,wrapCount(meta,cw-230,'11px Arial')):0,minH=36+nameLines*27+(metaLines?8+metaLines*20:0);
|
||
return {minH,h:Math.max(150,minH)};
|
||
}),naturalRh=Math.max(...measured.map(v=>v.h)),minRh=Math.max(...measured.map(v=>v.minH)),rh=fitBlockHeight(naturalRh,minRh,14,.88);
|
||
row.forEach((item,j)=>itemCardLight(item,M+j*(cw+gap),y,cw,rh));y+=rh+14
|
||
}
|
||
// PDF cards already contain the position photo; a second gallery duplicated the same images and created extra sparse pages.
|
||
}
|
||
function drawGallery(cols=6,h=120,boxesOnly=false){const galleryItems=boxesOnly?items.filter(i=>[0,5].includes(Number(i.categoryId))):items;if(!galleryItems.length)return;const gap=8,shown=Math.min(galleryItems.length,cols*2),rows=Math.ceil(shown/cols),natural=rows*h+(rows-1)*gap,minCell=Math.min(h,96),minTotal=rows*minCell+(rows-1)*gap,total=fitBlockHeight(natural,minTotal,20,.86),cellH=Math.max(minCell,(total-(rows-1)*gap)/rows),w=(W-2*M-gap*(cols-1))/cols;for(let i=0;i<shown;i++){const img=imageMap.get(galleryItems[i].id)||logo;if(img)pdfDrawCover(ctx,img,M+(i%cols)*(w+gap),y+Math.floor(i/cols)*(cellH+gap),w,cellH,10)}y+=total+18}
|
||
function summary(){
|
||
const metricList=[[primaryMetric.value,primaryMetric.label],...(cfg.showAmountPerGuest?[[g?money(menuPer):'—',cfg.texts.amountPerGuestLabel||'Сумма на гостя']]:[]),...(cfg.showBoxCount?[[String(Math.round(boxCount)),cfg.texts.boxCountLabel||'Количество боксов']]:[])];
|
||
const titleLines=wrapCount(cfg.texts.summaryTitle,470,'500 22px Georgia'),naturalH=Math.max(246,170+titleLines*31),minH=Math.max(218,170+titleLines*31),h=fitBlockHeight(naturalH,minH,18,.9);card(M,y,W-2*M,h,20,PANEL,LINE);const leftW=520,rightX=M+leftW;pdfText(ctx,cfg.texts.summaryTitle,M+24,y+20,leftW-48,31,{font:'500 22px Georgia',color:GREEN_DEEP,maxLines:3});const metricsY=y+30+titleLines*31,mw=(leftW-44)/Math.max(1,metricList.length);metricList.forEach((it,i)=>{const x=M+22+i*mw;pdfText(ctx,it[0],x+mw/2,metricsY,mw-20,34,{font:'500 23px Georgia',color:TEXT,align:'center',maxLines:1});pdfText(ctx,it[1],x+mw/2,metricsY+43,mw-20,38,{font:'10px Arial',color:MUTED,align:'center',maxLines:2});if(i<metricList.length-1)line(x+mw,metricsY-2,x+mw,metricsY+88,LINE,1)});line(rightX,y+24,rightX,y+h-24,LINE,1);pdfText(ctx,cfg.texts.totalPriceTitle.toUpperCase(),rightX+32,y+38,W-M-rightX-54,44,{font:'700 11px Arial',color:GOLD,maxLines:2});pdfText(ctx,money(p.total),rightX+32,y+88,W-M-rightX-54,45,{font:'500 34px Georgia',color:GREEN_DEEP,maxLines:1});if(g&&cfg.showAmountPerGuest)pdfText(ctx,`на гостя: ${money(menuPer)}`,rightX+32,y+140,W-M-rightX-54,23,{font:'13px Arial',color:MUTED,maxLines:1});if(cfg.showDiscountBadge&&discountPct){card(rightX+32,y+h-58,142,34,10,GREEN_SOFT,GREEN_SOFT);pdfText(ctx,`-${discountPct}% учтено`,rightX+103,y+h-50,120,19,{font:'12px Arial',color:GREEN,align:'center',maxLines:1})}y+=h+18;
|
||
}
|
||
function measureListHeight(list,width,font='12px Arial',lineH=20,minRow=31){return list.reduce((sum,t)=>sum+Math.max(minRow,wrapCount(t,width,font)*lineH+9),0)}
|
||
function drawControlCard(x,yy,w,h){card(x,yy,w,h,18,PANEL,LINE);const titleLines=wrapCount(cfg.texts.controlTitle,w-40,'500 18px Georgia');pdfText(ctx,cfg.texts.controlTitle,x+w/2,yy+20,w-40,27,{font:'500 18px Georgia',color:GREEN_DEEP,align:'center',maxLines:3});let cy=yy+34+titleLines*27;for(const t of control){const lines=wrapCount(t,w-78,'12px Arial'),rowH=Math.max(31,lines*20+9);pdfDrawCheck(ctx,x+29,cy+9,8,GREEN_SOFT,GREEN_DEEP);pdfText(ctx,t,x+48,cy,w-70,rowH,{font:'12px Arial',color:TEXT,maxLines:5});cy+=rowH}}
|
||
function drawExtrasCard(x,yy,w,h){card(x,yy,w,h,18,PANEL,LINE);const titleLines=wrapCount(cfg.texts.extrasTitle,w-40,'500 18px Georgia');pdfText(ctx,cfg.texts.extrasTitle,x+w/2,yy+20,w-40,27,{font:'500 18px Georgia',color:GREEN_DEEP,align:'center',maxLines:3});let cy=yy+36+titleLines*27;for(const name of extras){const lines=wrapCount(name,w-70,'12px Arial'),rowH=Math.max(37,lines*20+9);ctx.save();ctx.fillStyle=GREEN_SOFT;ctx.beginPath();ctx.arc(x+31,cy+8,8,0,Math.PI*2);ctx.fill();ctx.restore();pdfText(ctx,'+',x+31,cy+1,16,16,{font:'700 11px Arial',color:GOLD,align:'center',maxLines:1});pdfText(ctx,name,x+50,cy,w-70,rowH,{font:'12px Arial',color:TEXT,maxLines:4});cy+=rowH}}
|
||
function infoCards(){
|
||
if(!cfg.showControl&&!cfg.showExtras)return;const gap=14,count=(cfg.showControl?1:0)+(cfg.showExtras?1:0),cw=count===1?W-2*M:(W-2*M-gap)/2;
|
||
const heights=[];if(cfg.showControl){const title=wrapCount(cfg.texts.controlTitle,cw-40,'500 18px Georgia');heights.push(50+title*27+measureListHeight(control,cw-78))}if(cfg.showExtras){const title=wrapCount(cfg.texts.extrasTitle,cw-40,'500 18px Georgia');heights.push(58+title*27+measureListHeight(extras,cw-70,'12px Arial',20,37))}const minH=Math.max(1,...heights),naturalH=Math.max(230,minH),h=fitBlockHeight(naturalH,minH,18,.9);let x=M;if(cfg.showControl){drawControlCard(x,y,cw,h);x+=cw+gap}if(cfg.showExtras)drawExtrasCard(x,y,cw,h);y+=h+18;
|
||
}
|
||
function footerNoteNaturalHeight(){if(!cfg.showFooterNote||!String(cfg.texts.footerNote||'').trim())return 0;const lines=wrapCount(cfg.texts.footerNote,W-2*M-34,'11px Arial');return Math.max(44,22+lines*18)+12}
|
||
function pricing(){if(!cfg.showPriceBreakdown)return;const rows=[[cfg.texts.priceItemsLabel||'Стоимость позиций',p.base],...(p.manual?[[cfg.texts.discountLabel||'Скидка',-p.manual]]:[]),...(p.promo?[[`${cfg.texts.promoLabel||'Промокод'}${s.promoCode?' '+s.promoCode:''}`,-p.promo]]:[]),[cfg.texts.menuAfterDiscountLabel||'Меню после скидок',p.itemsTotal],[cfg.texts.deliveryLabel||'Доставка',p.delivery],[cfg.texts.totalLabel||'Итого',p.total]],title=cfg.texts.pricingTitle||'Расчёт стоимости',titleLines=wrapCount(title,420,'500 18px Georgia'),naturalH=48+titleLines*27+rows.length*31,minH=44+titleLines*27+rows.length*26,footerReserve=footerNoteNaturalHeight();
|
||
if(y+naturalH+16<=bottom&&footerReserve&&y+naturalH+16+footerReserve>bottom)newPage();
|
||
const h=fitBlockHeight(naturalH,minH,16,.88),rowStep=Math.max(26,Math.min(31,(h-44-titleLines*27)/Math.max(1,rows.length)));card(M,y,W-2*M,h,16,PANEL,LINE);pdfText(ctx,title,M+20,y+16,420,27,{font:'500 18px Georgia',color:GREEN_DEEP,maxLines:2});let yy=y+24+titleLines*27;for(const [label,val] of rows){const grand=label===(cfg.texts.totalLabel||'Итого');pdfText(ctx,label,M+20,yy,490,21,{font:grand?'700 13px Arial':'12px Arial',color:grand?GREEN_DEEP:MUTED,maxLines:1});pdfText(ctx,(val<0?'- ':'')+money(Math.abs(val)),W-M-20,yy,250,21,{font:grand?'700 14px Arial':'12px Arial',color:grand?GREEN_DEEP:TEXT,align:'right',maxLines:1});yy+=rowStep}y+=h+16}
|
||
function footerNoteBlock(){if(!cfg.showFooterNote||!String(cfg.texts.footerNote||'').trim())return;const lines=wrapCount(cfg.texts.footerNote,W-2*M-34,'11px Arial'),naturalH=Math.max(44,22+lines*18),minH=Math.max(36,18+lines*16),h=fitBlockHeight(naturalH,minH,12,.82);card(M,y,W-2*M,h,12,PANEL,LINE);pdfText(ctx,cfg.texts.footerNote,W/2,y+Math.max(8,(h-lines*18)/2),W-2*M-34,18,{font:'11px Arial',color:MUTED,align:'center',maxLines:6});y+=h+12}
|
||
function finalGalleryFill(){
|
||
if(!finalGallery.length)return;
|
||
const title=cfg.texts.finalGalleryTitle||'Как это выглядит на вашем столе',gap=12;
|
||
const titleLines=wrappedLines(title,W-2*M,'500 21px Georgia'),titleH=Math.max(36,titleLines.length*28+8);
|
||
const free=Math.max(0,bottom-y),minPhotoH=92;
|
||
// This block may consume only the whitespace already left on the current last page.
|
||
// It never calls newPage() and never creates an extra page.
|
||
if(free<titleH+minPhotoH)return;
|
||
drawWrappedLines(titleLines,W/2,y,28,{font:'500 21px Georgia',color:GREEN_DEEP,align:'center'});
|
||
y+=titleH;
|
||
const available=Math.max(minPhotoH,bottom-y),count=available>=205&&finalGallery.length>=2?2:1;
|
||
if(count===1){const img=finalGallery[1]||finalGallery[0];if(img)pdfDrawCover(ctx,img,M,y,W-2*M,available,14)}
|
||
else{const imgs=finalGallery.slice(0,2),cellW=(W-2*M-gap)/2;if(imgs[0])pdfDrawCover(ctx,imgs[0],M,y,cellW,available,14);if(imgs[1])pdfDrawCover(ctx,imgs[1],M+cellW+gap,y,cellW,available,14)}
|
||
y=bottom;
|
||
}
|
||
newPage();
|
||
if(id==='editorial-grid'){brandHeader();hero();drawItems();summary();pricing();sellingBlock();infoCards();finalGalleryFill();footerNoteBlock();}
|
||
else if(id==='midnight-glass'){brandHeader();hero();sellingBlock();summary();infoCards();drawItems();pricing();finalGalleryFill();footerNoteBlock();}
|
||
else if(id==='emerald-gold'){brandHeader();hero();summary();sellingBlock();infoCards();pricing();drawItems();footerNoteBlock();finalGalleryFill();}
|
||
else{brandHeader();hero();sellingBlock();drawItems();summary();infoCards();pricing();footerNoteBlock();finalGalleryFill();}
|
||
canvases.forEach((c,i)=>{const x=c.getContext('2d');x.setTransform(SCALE,0,0,SCALE,0,0);x.save();x.fillStyle=FOOT;x.fillRect(0,H-52,W,52);x.strokeStyle=LINE;x.lineWidth=1;x.beginPath();x.moveTo(0,H-52.5);x.lineTo(W,H-52.5);x.stroke();pdfText(x,'Солнце Кейтеринг · Санкт-Петербург',M,H-36,620,22,{font:'12px Arial',color:MUTED,maxLines:1});pdfText(x,`${i+1} / ${canvases.length}`,W-M,H-36,100,22,{font:'12px Arial',color:MUTED,align:'right',maxLines:1});x.restore()});
|
||
return canvases;
|
||
}
|
||
async function renderOfferPdfPages(s){
|
||
const classic=window.SunClassicOfferPDFV1767;
|
||
if(classic?.renderPages)return classic.renderPages(s,renderOfferPdfPagesBase);
|
||
return renderOfferPdfPagesBase(s);
|
||
}
|
||
async function pdfBlobFromPages(canvases){const jpeg=[];for(const c of (canvases||[]))jpeg.push(await pdfCanvasJpeg(c));return pdfFromJpegs(jpeg)}
|
||
async function buildOfferPdfBlob(s){return pdfBlobFromPages(await renderOfferPdfPages(s))}
|
||
async function triggerPdfDownload(blob,name){
|
||
const isAppleTouch=/iP(?:ad|hone|od)/i.test(navigator.userAgent||'')||(navigator.platform==='MacIntel'&&Number(navigator.maxTouchPoints||0)>1);
|
||
if(isAppleTouch&&typeof File==='function'&&navigator.share&&navigator.canShare){
|
||
try{const file=new File([blob],name,{type:'application/pdf'});if(navigator.canShare({files:[file]})){await navigator.share({files:[file],title:'Предложение клиенту'});return 'share'}}catch(err){if(err?.name==='AbortError')return 'cancel';console.warn('PDF share fallback:',err)}
|
||
}
|
||
const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.style.display='none';a.rel='noopener';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),120000);return 'download';
|
||
}
|
||
function withTimeout(promise,ms,label='Операция'){return Promise.race([promise,new Promise((_,reject)=>setTimeout(()=>reject(new Error(label+' превысила время ожидания')),ms))])}
|
||
function prepareCurrentOfferPdf(prepared,renderToken=currentOffer.renderToken){
|
||
const btn=$('sunOfferPdf');if(btn){btn.disabled=true;btn.textContent='Готовлю PDF…'}
|
||
let promise;
|
||
const source=currentOffer.pages?.length?pdfBlobFromPages(currentOffer.pages):preparedPdfSnapshot(prepared).then(buildOfferPdfBlob);
|
||
promise=withTimeout(source,60000,'Подготовка PDF').then(blob=>{if(currentOffer.renderToken===renderToken){currentOffer.pdfBlob=blob;if(btn){btn.disabled=false;btn.textContent='Скачать PDF'}}return blob}).catch(err=>{console.error(err);if(currentOffer.renderToken===renderToken){currentOffer.pdfBlob=null;if(btn){btn.disabled=false;btn.textContent='Скачать PDF'}}toast(`Не удалось подготовить PDF: ${err?.message||'неизвестная ошибка'}. Нажмите «Скачать PDF», чтобы повторить.`,'error');return null}).finally(()=>{if(currentOffer.pdfPromise===promise)currentOffer.pdfPromise=null});currentOffer.pdfPromise=promise;return promise;
|
||
}
|
||
async function downloadCurrentOfferPdf(){
|
||
if(!currentOffer.prepared&&!currentOffer.pdfBlob){toast('Сначала откройте предложение клиенту.','error');return}
|
||
const btn=$('sunOfferPdf');if(btn){btn.disabled=true;btn.textContent='Готовлю PDF…'}
|
||
try{
|
||
if(currentOffer.editTimer){clearTimeout(currentOffer.editTimer);currentOffer.editTimer=null;saveOfferOverrides();await renderOffer(currentOffer.snapshot)}
|
||
else if(!currentOffer.pages?.length&¤tOffer.snapshot)await renderOffer(currentOffer.snapshot);
|
||
await ensureCurrentOfferFresh();
|
||
let blob=currentOffer.pdfBlob;
|
||
if(!blob&¤tOffer.pdfPromise)blob=await currentOffer.pdfPromise;
|
||
if(!blob&¤tOffer.prepared)blob=await prepareCurrentOfferPdf(currentOffer.prepared);
|
||
// One automatic retry after a failed background preparation.
|
||
if(!blob&¤tOffer.prepared)blob=await prepareCurrentOfferPdf(currentOffer.prepared);
|
||
if(!blob)throw new Error('PDF не удалось сформировать');
|
||
currentOffer.pdfBlob=blob;
|
||
const mode=await triggerPdfDownload(blob,filename('pdf'));
|
||
if(mode==='share')toast('PDF готов. Выберите «Сохранить в Файлы» или нужное приложение.');
|
||
else if(mode!=='cancel')toast('PDF скачивается.');
|
||
}catch(err){console.error(err);toast(`Не удалось скачать PDF: ${err?.message||'неизвестная ошибка'}. Попробуйте ещё раз.`,'error')}
|
||
finally{if(btn){btn.disabled=false;btn.textContent='Скачать PDF'}}
|
||
}
|
||
|
||
|
||
function ensureButton(){
|
||
const actions=document.querySelector('#new .actions');if(!actions)return;let b=$('sunClientOfferButton');if(!b){b=document.createElement('button');b.id='sunClientOfferButton';b.type='button';b.className='outline';b.textContent='Предложение клиенту';b.onclick=openCurrentDraftOffer;const del=$('deleteOrderBtn');if(del)window.SunSafe.insertBefore(actions,b,del);else actions.appendChild(b);}const d=getDraft();b.title=d?.id?'Открыть клиентское предложение':'Сначала сохраните заказ';
|
||
}
|
||
function patchOrderViewer(){qa('.sun-order-view-actions').forEach(actions=>{if(actions.querySelector('[data-sun-offer-id]'))return;const edit=actions.querySelector('[data-viewer-action="edit"]');const id=edit?.dataset?.id;if(!id)return;const b=document.createElement('button');b.className='outline';b.type='button';b.textContent='Предложение';b.dataset.sunOfferId=id;b.onclick=()=>openOfferForOrder(id);const spacer=actions.querySelector('.spacer');window.SunSafe.insertBefore(actions,b,spacer||null);});}
|
||
|
||
async function catalogPhotoChanged(itemId){
|
||
if(!currentOffer.snapshot||!currentOffer.orderId)return;
|
||
if(!$('sunClientOfferModal')?.classList.contains('on'))return;
|
||
if(!currentOffer.snapshot.items?.some(item=>String(item.id)===String(itemId)))return;
|
||
await renderOffer(currentOffer.snapshot);
|
||
}
|
||
|
||
const refreshOpenOffer=()=>{if(currentOffer.snapshot&¤tOffer.orderId)renderOffer(currentOffer.snapshot).catch(err=>console.warn('Offer refresh:',err))};
|
||
window.addEventListener('sunoffertemplatechange',refreshOpenOffer);
|
||
window.addEventListener('sunclientoffersettingschange',refreshOpenOffer);
|
||
window.sunOpenClientOffer=openOfferForOrder;window.sunOpenCurrentClientOffer=openCurrentDraftOffer;window.sunGetClientOfferPdfBlob=()=>currentOffer.pdfBlob;window.sunClientOfferDownload=downloadCurrentOfferPdf;window.sunClientOfferCatalogChanged=catalogPhotoChanged;window.sunClientOfferDebugHtml=offerHtml;window.sunClientOfferDebugPdf=async prepared=>buildOfferPdfBlob(await preparedPdfSnapshot(prepared));window.sunClientOfferDebugPdfPages=async prepared=>renderOfferPdfPages(await preparedPdfSnapshot(prepared));window.sunClientOfferDebugCreateSnapshot=createSnapshot;window.sunClientOfferDebugBoxCount=offerBoxCount;
|
||
(()=>{let queued=false;const run=()=>{if(queued)return;queued=true;setTimeout(()=>{queued=false;ensureButton();patchOrderViewer();},50);};[$('new'),$('orders')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
||
document.addEventListener('click',e=>{const b=e.target.closest('[data-sun-offer-id]');if(b){e.preventDefault();e.stopPropagation();openOfferForOrder(b.dataset.sunOfferId)}},true);
|
||
ensureModal();ensureButton();patchOrderViewer();setTimeout(ensureButton,150);setTimeout(patchOrderViewer,300);
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: auto-offer.js ===== */
|
||
(function(){
|
||
'use strict';
|
||
if(window.__sunAutoOfferV1)return;window.__sunAutoOfferV1=true;
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const money=v=>`${Math.round(Number(v||0)).toLocaleString('ru-RU')} \u20bd`;
|
||
const txt={
|
||
gen:'\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0435',
|
||
title:'\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0443',
|
||
hint:'\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0433\u043e\u0441\u0442\u0435\u0439. \u0411\u044e\u0434\u0436\u0435\u0442 \u0438 \u0432\u0435\u0441 \u043c\u043e\u0436\u043d\u043e \u043d\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u044f\u0442\u044c.',
|
||
guests:'\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0433\u043e\u0441\u0442\u0435\u0439',
|
||
budget:'\u0411\u044e\u0434\u0436\u0435\u0442 \u043d\u0430 \u0433\u043e\u0441\u0442\u044f, \u20bd',
|
||
weight:'\u0412\u0435\u0441 \u043d\u0430 \u0433\u043e\u0441\u0442\u044f, \u0433',
|
||
optional:'\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e',
|
||
close:'\u0417\u0430\u043a\u0440\u044b\u0442\u044c',
|
||
regen:'\u041f\u0435\u0440\u0435\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c',
|
||
apply:'\u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u043a \u0437\u0430\u043a\u0430\u0437\u0443',
|
||
total:'\u041c\u0435\u043d\u044e',per:'\u041d\u0430 \u0433\u043e\u0441\u0442\u044f',weightPer:'\u0412\u0435\u0441 / \u0433\u043e\u0441\u0442\u044f',piecesPer:'\u0417\u0430\u043a\u0443\u0441\u043e\u043a / \u0433\u043e\u0441\u0442\u044f',
|
||
empty:'\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0433\u043e\u0441\u0442\u0435\u0439 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u00bb.',
|
||
lowBudget:'\u0417\u0430\u0434\u0430\u043d\u043d\u044b\u0439 \u0431\u044e\u0434\u0436\u0435\u0442 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043d\u0438\u0437\u043a\u0438\u0439 \u0434\u043b\u044f \u043f\u043e\u0434\u0431\u043e\u0440\u0430 \u043c\u0435\u043d\u044e.',
|
||
applied:'\u041f\u043e\u0434\u043e\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u043c\u0435\u043d\u044e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 \u0437\u0430\u043a\u0430\u0437. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c\u00bb.',
|
||
noEvent:'\u2014 \u041d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u2014',custom:'\u0421\u0432\u043e\u0451\u2026',customPh:'\u041d\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0444\u043e\u0440\u043c\u0430\u0442'
|
||
};
|
||
const toast=(m,t='success')=>{try{window.SunEnterprise?.toast?.(m,t)}catch(_){};};
|
||
const getBoxes=()=>{try{if(Array.isArray(boxes))return boxes}catch(_){}try{return JSON.parse(localStorage.getItem('sunBoxes')||'[]')}catch(_){return[]}};
|
||
const getOrders=()=>{try{if(Array.isArray(orders))return orders}catch(_){}try{return JSON.parse(localStorage.getItem('sunOrders')||'[]')}catch(_){return[]}};
|
||
const getDraft=()=>{try{return draft}catch(_){return null}};
|
||
const parseWeight=v=>{const s=String(v||'').toLowerCase().replace(/,/g,'.'),m=s.match(/(\d+(?:\.\d+)?)\s*(\u043a\u0433|kg|\u0433|g)(?:\s|$)/);if(!m)return 0;const n=Number(m[1]);return m[2]==='\u043a\u0433'||m[2]==='kg'?n*1000:n};
|
||
const guestMax=v=>{const a=String(v||'').match(/\d+/g)?.map(Number)||[];return a.length?Math.max(...a):0};
|
||
function portions(item){
|
||
const explicit=Math.max(0,Number(item?.pieces||0));if(explicit>0)return explicit;
|
||
let sum=0;
|
||
for(const x of item?.ingredients||[]){if(!Array.isArray(x))continue;const q=Math.max(0,Number(x[1]||0)),u=String(x[2]||'').toLowerCase();if(/\u0448\u0442|\u043f\u043e\u0440|\u043a\u0443\u0441/.test(u))sum+=q;}
|
||
if(sum>=5)return sum;
|
||
return Math.max(sum,guestMax(item?.guests),10);
|
||
}
|
||
function popularityMap(){
|
||
const stats=new Map();let maxOrders=0,maxQty=0;
|
||
for(const o of getOrders()){
|
||
if(String(o?.status||'').toLowerCase()==='отменён')continue;
|
||
const seen=new Set();
|
||
for(const l of o?.lines||[]){const id=String(l?.id??'');if(!id)continue;let st=stats.get(id);if(!st){st={orders:0,qty:0};stats.set(id,st)}st.qty+=Math.max(0,Number(l?.qty||0));if(!seen.has(id)){st.orders+=1;seen.add(id)}}
|
||
}
|
||
for(const st of stats.values()){maxOrders=Math.max(maxOrders,st.orders);maxQty=Math.max(maxQty,st.qty)}
|
||
const out=new Map();for(const [id,st] of stats)out.set(id,(maxOrders?st.orders/maxOrders:0)*.78+(maxQty?st.qty/maxQty:0)*.22);return out;
|
||
}
|
||
function foodCandidates(){const pop=popularityMap();return getBoxes().filter(x=>Number(x?.category||0)===0&&x?.hidden!==true&&Number(x?.price||0)>0&&parseWeight(x?.weight)>0&&!/\u0433\u043e\u0442\u043e\u0432\u044b\u0435\s+\u043c\u0435\u043d\u044e/i.test(String(x?.catalogSection||''))).map(x=>({raw:x,id:String(x.id),name:String(x.name||''),section:String(x.catalogSection||'\u0411\u043e\u043a\u0441\u044b'),price:Number(x.price||0),grams:parseWeight(x.weight),portions:portions(x),photo:String(x.photo||'sun-logo.png'),popularity:Number(pop.get(String(x.id))||0)}));}
|
||
function hashNoise(text,seed){let h=2166136261^(seed*2654435761);for(const c of String(text)){h^=c.charCodeAt(0);h=Math.imul(h,16777619)}return ((h>>>0)%10000)/10000;}
|
||
function stateKey(s){return [...s.q.entries()].sort((a,b)=>a[0].localeCompare(b[0])).map(([id,q])=>id+':'+q).join('|')}
|
||
function score(s,t,seed){
|
||
const pg=s.portions/t.targetPortions,pp=pg<1?(1-pg)*5:(pg-1)*1.15;
|
||
let wp=0;if(t.targetWeight){const r=s.grams/t.targetWeight;wp=r<1?(1-r)*4:(r-1)*1.1;}
|
||
let bp=0;if(t.budget){if(s.cost>t.budget)return 100+(s.cost-t.budget)/t.budget*20;bp=Math.max(0,(t.budget-s.cost)/t.budget)*.72}else bp=(s.cost/Math.max(1,t.guests*5000))*.04;
|
||
const boxes=[...s.q.values()].reduce((a,b)=>a+b,0),unique=s.q.size,sections=s.sections.size;
|
||
const minPositions=t.guests>=15?4:(t.guests>=8?3:2);const diversity=(boxes-unique)*.08+Math.max(0,Math.min(boxes,4)-sections)*.055-Math.min(sections,4)*.025+Math.max(0,minPositions-unique)*.24;
|
||
const popularityBonus=boxes?Math.min(.22,(Number(s.popularity||0)/boxes)*.18):0;
|
||
return pp+wp+bp+diversity-popularityBonus+hashNoise(stateKey(s),seed)*.012;
|
||
}
|
||
function generateMenu(t,seed){
|
||
const cand=foodCandidates();if(!cand.length)return null;
|
||
const empty={q:new Map(),portions:0,grams:0,cost:0,sections:new Set(),popularity:0};
|
||
let beam=[empty],all=[empty];
|
||
const maxBoxes=Math.min(30,Math.max(4,Math.ceil(t.targetPortions/20)+4,t.targetWeight?Math.ceil(t.targetWeight/700)+4:0));
|
||
for(let step=0;step<maxBoxes;step++){
|
||
const map=new Map();
|
||
for(const st of beam){
|
||
for(const c of cand){
|
||
const old=st.q.get(c.id)||0;if(old>=4)continue;
|
||
const cost=st.cost+c.price;if(t.budget&&cost>t.budget)continue;
|
||
const q=new Map(st.q);q.set(c.id,old+1);const sections=new Set(st.sections);sections.add(c.section);
|
||
const ns={q,portions:st.portions+c.portions,grams:st.grams+c.grams,cost,sections,popularity:Number(st.popularity||0)+Number(c.popularity||0)};const key=stateKey(ns),prev=map.get(key);
|
||
if(!prev||score(ns,t,seed)<score(prev,t,seed))map.set(key,ns);
|
||
}
|
||
}
|
||
if(!map.size)break;
|
||
const next=[...map.values()].sort((a,b)=>score(a,t,seed)-score(b,t,seed)).slice(0,130);beam=next;all.push(...next.slice(0,60));
|
||
const best=next[0];if(best&&best.portions>=t.targetPortions*1.05&&(!t.targetWeight||best.grams>=t.targetWeight*1.03)&&step>=3)break;
|
||
}
|
||
const useful=all.filter(s=>s.q.size>0&&( !t.budget || s.cost<=t.budget));if(!useful.length)return null;
|
||
const best=useful.sort((a,b)=>score(a,t,seed)-score(b,t,seed))[0];
|
||
const items=[...best.q.entries()].map(([id,qty])=>{const c=cand.find(x=>x.id===id);return {...c,qty,sum:c.price*qty,totalGrams:c.grams*qty,totalPortions:c.portions*qty}}).sort((a,b)=>a.section.localeCompare(b.section,'ru')||a.name.localeCompare(b.name,'ru'));
|
||
return {...best,items};
|
||
}
|
||
function ensureStyle(){if($('sun-auto-offer-style'))return;const st=document.createElement('style');st.id='sun-auto-offer-style';st.textContent=`
|
||
#sunAutoOfferModal{z-index:100060}#sunAutoOfferModal .dialog{width:min(980px,calc(100vw - 28px));max-height:94vh;padding:0;overflow:auto;border-radius:18px;background:#f5f3fa}.sun-auto-head{position:sticky;top:0;z-index:3;display:flex;align-items:flex-start;gap:12px;padding:15px 17px;background:#fff;border-bottom:1px solid #e4e0ef}.sun-auto-head h2{margin:0;color:#241d45;font-size:20px}.sun-auto-head p{margin:4px 0 0;color:#7b748d;font-size:12px}.sun-auto-head .spacer{flex:1}.sun-auto-form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:16px 17px 5px}.sun-auto-form label{display:grid;gap:6px;color:#5d5670;font-size:11px;font-weight:800}.sun-auto-form small{font-weight:500;color:#9b94aa}.sun-auto-form input{width:100%;border:1px solid #d9d2e9;border-radius:11px;padding:10px 11px;background:#fff;color:#29243d}.sun-auto-generate{margin:10px 17px 2px}.sun-auto-result{padding:12px 17px 18px}.sun-auto-placeholder{padding:30px;text-align:center;border:1px dashed #cbc2df;border-radius:16px;color:#827a95;background:#fff}.sun-auto-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;margin-bottom:12px}.sun-auto-kpi{background:#241d45;color:#fff;border-radius:14px;padding:13px}.sun-auto-kpi small{display:block;color:#c9c2db;font-size:10px}.sun-auto-kpi b{display:block;margin-top:5px;font-size:18px}.sun-auto-warning{margin:0 0 12px;padding:10px 12px;border-radius:11px;background:#fff4d9;color:#77550c;font-size:12px}.sun-auto-items{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.sun-auto-item{display:grid;grid-template-columns:82px minmax(0,1fr);gap:10px;padding:9px;background:#fff;border:1px solid #e1dceb;border-radius:14px}.sun-auto-item img{width:82px;height:82px;object-fit:cover;border-radius:10px;background:#eeeaf7}.sun-auto-item h3{margin:2px 0 5px;color:#302a45;font-size:13px;line-height:1.25}.sun-auto-item p{margin:3px 0;color:#827b92;font-size:10px}.sun-auto-item b{color:#5b48a6}.sun-auto-foot{display:flex;justify-content:flex-end;gap:8px;padding:13px 17px;background:#fff;border-top:1px solid #e4e0ef;position:sticky;bottom:0}.sun-event-custom-input{display:none!important;margin-top:6px!important}.sun-event-custom-input.on{display:block!important}#sunOrderCards .sun-order-card.sun-has-prepayment .sun-order-card-date{background:linear-gradient(145deg,#238b57,#36ad72)!important;border-color:#238b57!important;box-shadow:0 5px 14px rgba(35,139,87,.24)!important}
|
||
@media(max-width:700px){#sunAutoOfferModal{padding:0}#sunAutoOfferModal .dialog{width:100vw;height:100vh;max-height:none;border-radius:0}.sun-auto-form{grid-template-columns:1fr}.sun-auto-summary{grid-template-columns:1fr 1fr}.sun-auto-items{grid-template-columns:1fr}.sun-auto-head h2{font-size:17px}}
|
||
`;document.head.appendChild(st)}
|
||
let generationSeed=1,currentResult=null,currentTargets=null;
|
||
function ensureModal(){if($('sunAutoOfferModal'))return;const m=document.createElement('div');m.className='modal';m.id='sunAutoOfferModal';m.innerHTML=`<div class="dialog"><div class="sun-auto-head"><div><h2>${txt.title}</h2><p>${txt.hint}</p></div><div class="spacer"></div><button class="outline" id="sunAutoClose" type="button">${txt.close}</button></div><div class="sun-auto-form"><label>${txt.guests}<input id="sunAutoGuests" type="number" min="1" step="1" inputmode="numeric" placeholder="20"></label><label>${txt.budget} <small>${txt.optional}</small><input id="sunAutoBudget" type="number" min="0" step="10" inputmode="decimal" placeholder="1500"></label><label>${txt.weight} <small>${txt.optional}</small><input id="sunAutoWeight" type="number" min="0" step="10" inputmode="decimal" placeholder="300"></label></div><button class="primary sun-auto-generate" id="sunAutoGenerate" type="button">${txt.gen}</button><div class="sun-auto-result" id="sunAutoResult"><div class="sun-auto-placeholder">${txt.empty}</div></div><div class="sun-auto-foot" id="sunAutoFoot" style="display:none"><button class="outline" id="sunAutoRegen" type="button">${txt.regen}</button><button class="primary" id="sunAutoApply" type="button">${txt.apply}</button></div></div>`;document.body.appendChild(m);$('sunAutoClose').onclick=closeGenerator;$('sunAutoGenerate').onclick=()=>runGenerator(false);$('sunAutoRegen').onclick=()=>runGenerator(true);$('sunAutoApply').onclick=applyGenerated;}
|
||
function openGenerator(){ensureStyle();ensureModal();const d=getDraft()||{},g=Number($('eventGuests')?.value||d.guestsCount||0);$('sunAutoGuests').value=g||'';$('sunAutoResult').innerHTML=`<div class="sun-auto-placeholder">${txt.empty}</div>`;$('sunAutoFoot').style.display='none';currentResult=null;$('sunAutoOfferModal').classList.add('on');}
|
||
function closeGenerator(){$('sunAutoOfferModal')?.classList.remove('on')}
|
||
function runGenerator(regen){
|
||
const guests=Math.max(0,Math.round(Number($('sunAutoGuests')?.value||0))),perBudget=Math.max(0,Number($('sunAutoBudget')?.value||0)),perWeight=Math.max(0,Number($('sunAutoWeight')?.value||0));
|
||
if(!guests){toast(txt.guests,'warn');$('sunAutoGuests')?.focus();return}
|
||
if(regen)generationSeed++;const targets={guests,targetPortions:guests*5,targetWeight:perWeight?guests*perWeight:0,budget:perBudget?guests*perBudget:0,perBudget,perWeight};const result=generateMenu(targets,generationSeed);currentTargets=targets;currentResult=result;renderGenerated(result,targets);
|
||
}
|
||
function renderGenerated(r,t){const root=$('sunAutoResult'),foot=$('sunAutoFoot');if(!r){root.innerHTML=`<div class="sun-auto-warning">${txt.lowBudget}</div>`;foot.style.display='none';return}
|
||
const pp=r.cost/t.guests,wp=r.grams/t.guests,pieces=r.portions/t.guests;const warns=[];if(r.portions<t.targetPortions*.92)warns.push(`\u041f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c ${pieces.toFixed(1)} \u0437\u0430\u043a\u0443\u0441\u043e\u043a \u043d\u0430 \u0433\u043e\u0441\u0442\u044f \u0432\u043c\u0435\u0441\u0442\u043e \u0446\u0435\u043b\u0435\u0432\u044b\u0445 5.`);if(t.targetWeight&&r.grams<t.targetWeight*.9)warns.push(`\u0412\u0435\u0441 ${Math.round(wp)} \u0433/\u0433\u043e\u0441\u0442\u044f \u043d\u0438\u0436\u0435 \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0445 ${Math.round(t.perWeight)} \u0433.`);
|
||
root.innerHTML=`<div class="sun-auto-summary"><div class="sun-auto-kpi"><small>${txt.total}</small><b>${money(r.cost)}</b></div><div class="sun-auto-kpi"><small>${txt.per}</small><b>${money(pp)}</b></div><div class="sun-auto-kpi"><small>${txt.weightPer}</small><b>${Math.round(wp).toLocaleString('ru-RU')} \u0433</b></div><div class="sun-auto-kpi"><small>${txt.piecesPer}</small><b>${pieces.toFixed(1)}</b></div></div>${warns.length?`<div class="sun-auto-warning">${warns.join('<br>')}</div>`:''}<div class="sun-auto-items">${r.items.map(i=>`<article class="sun-auto-item"><img src="${esc(i.photo)}" alt=""><div><h3>${esc(i.name)}</h3><p>${esc(i.section)} \u00b7 <b>\u00d7${i.qty}</b></p><p>${money(i.sum)} \u00b7 ${Math.round(i.totalGrams).toLocaleString('ru-RU')} \u0433 \u00b7 ${Math.round(i.totalPortions)} \u043f\u043e\u0440\u0446.</p></div></article>`).join('')}</div>`;foot.style.display='flex';}
|
||
function applyGenerated(){if(!currentResult||!currentTargets)return;const d=getDraft();if(!d)return;d.lines=currentResult.items.map(i=>({id:i.id,qty:i.qty}));d.guestsCount=currentTargets.guests;const g=$('eventGuests');if(g){g.value=currentTargets.guests;g.dispatchEvent(new Event('input',{bubbles:true}))}try{window.render?.();window.updateOrderSummary?.()}catch(_){}closeGenerator();toast(txt.applied,'success');}
|
||
function ensureGeneratorButton(){const actions=document.querySelector('#new .actions');if(!actions)return;let b=$('sunAutoOfferButton');if(!b){b=document.createElement('button');b.id='sunAutoOfferButton';b.type='button';b.className='outline';b.textContent=txt.gen;b.onclick=openGenerator;const offer=$('sunClientOfferButton');if(offer)offer.insertAdjacentElement('afterend',b);else actions.appendChild(b)}}
|
||
function decoratePaidCards(){const map=new Map(getOrders().map(o=>[String(o.id),o]));qa('#sunOrderCards .sun-order-card').forEach(card=>{const o=map.get(String(card.dataset.orderId||''));card.classList.toggle('sun-has-prepayment',Number(o?.prepayment||0)>0)})}
|
||
function ensureEventOptions(){
|
||
const s=$('event');if(!s||s.tagName!=='SELECT')return;const keep=String(s.value||'');
|
||
qa('option',s).filter(o=>(o.textContent||'').trim()==='Новое мероприятие'||o.dataset.sunCustom==='1').forEach(o=>o.remove());
|
||
$('sunEventCustomInput')?.remove();
|
||
let blank=qa('option',s).find(o=>o.dataset.sunBlank==='1'||o.value==='');
|
||
if(!blank){blank=document.createElement('option');blank.value='';blank.textContent=txt.noEvent;blank.dataset.sunBlank='1';s.insertBefore(blank,s.firstChild)}else{blank.value='';if(blank.textContent!==txt.noEvent)blank.textContent=txt.noEvent;blank.dataset.sunBlank='1';if(s.firstElementChild!==blank)s.insertBefore(blank,s.firstChild)}
|
||
const opts=qa('option',s);s.value=opts.some(o=>o.value===keep)?keep:'';
|
||
if(s.dataset.sunPersistentEventBound!=='1'){s.dataset.sunPersistentEventBound='1';s.addEventListener('change',()=>{const d=getDraft();if(d)d.event=s.value||''})}
|
||
}
|
||
function syncEventFromDraft(){ensureEventOptions();const s=$('event'),d=getDraft();if(!s||!d)return;const value=String(d.event||'').trim();if(!value){s.value='';return}let opt=qa('option',s).find(o=>o.value===value);if(!opt){opt=document.createElement('option');opt.value=value;opt.textContent=value;opt.dataset.sunLegacy='1';s.appendChild(opt)}s.value=value;}
|
||
function prepareEventForSave(){ensureEventOptions();const s=$('event'),d=getDraft();if(s&&d)d.event=s.value||'';}
|
||
const oldSave=window.saveOrder;if(typeof oldSave==='function')window.saveOrder=function(...a){prepareEventForSave();const r=oldSave.apply(this,a);setTimeout(()=>{syncEventFromDraft();decoratePaidCards()},0);return r};
|
||
const oldOpen=window.openOrder;if(typeof oldOpen==='function')window.openOrder=function(...a){const r=oldOpen.apply(this,a);setTimeout(syncEventFromDraft,0);setTimeout(syncEventFromDraft,80);return r};
|
||
const oldReset=window.resetDraft;if(typeof oldReset==='function')window.resetDraft=function(...a){const r=oldReset.apply(this,a);setTimeout(syncEventFromDraft,0);return r};
|
||
ensureStyle();ensureModal();ensureGeneratorButton();ensureEventOptions();syncEventFromDraft();decoratePaidCards();
|
||
(()=>{let queued=false;const run=()=>{if(queued)return;queued=true;setTimeout(()=>{queued=false;ensureGeneratorButton();ensureEventOptions();decoratePaidCards();},50);};[$('new'),$('orders')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
||
setTimeout(()=>{ensureGeneratorButton();syncEventFromDraft();decoratePaidCards()},180);setTimeout(()=>{ensureGeneratorButton();syncEventFromDraft();decoratePaidCards()},700);
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: mobile-pwa.js ===== */
|
||
/* Solnce Catering classic PWA/sync layer - keeps the original application UI */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunClassicPwaV1)return;
|
||
window.__sunClassicPwaV1=true;
|
||
|
||
const q=(s,r=document)=>r.querySelector(s);
|
||
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
|
||
/* PWA only. No alternate mobile chrome or mobile home is created here. */
|
||
function initPwa(){
|
||
if('serviceWorker' in navigator && /^https?:$/.test(location.protocol)){
|
||
navigator.serviceWorker.register('./service-worker.js').then(reg=>reg.update()).catch(()=>{});
|
||
}
|
||
}
|
||
|
||
/* Cross-device sync. This is deliberately UI-neutral. */
|
||
const SYNC_META='sunCloudSyncMetaV1';
|
||
const SYNC_TOKEN='sunCloudSyncTokenV1';
|
||
const SYNC_WORKSPACE='sunCloudSyncWorkspaceV1';
|
||
const sync={busy:false,status:'local',timer:null,lastError:''};
|
||
const excludedKey=k=>k===SYNC_META||k===SYNC_TOKEN||k===SYNC_WORKSPACE||k.startsWith('sunCloudSyncTransient');
|
||
|
||
function hash(s){
|
||
s=String(s??'');
|
||
let h=2166136261;
|
||
for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619)}
|
||
return (h>>>0).toString(16);
|
||
}
|
||
function getMeta(){try{return JSON.parse(localStorage.getItem(SYNC_META)||'{}')||{}}catch(_){return{}}}
|
||
function setMeta(m){localStorage.setItem(SYNC_META,JSON.stringify(m))}
|
||
function syncWorkspace(){return localStorage.getItem(SYNC_WORKSPACE)||'main'}
|
||
function syncHeaders(){return {'Content-Type':'application/json','X-Sun-Token':localStorage.getItem(SYNC_TOKEN)||''}}
|
||
|
||
function captureSyncQuery(){
|
||
try{
|
||
const u=new URL(location.href);
|
||
const token=u.searchParams.get('syncToken');
|
||
const workspace=u.searchParams.get('workspace');
|
||
let changed=false;
|
||
if(token){localStorage.setItem(SYNC_TOKEN,token);u.searchParams.delete('syncToken');changed=true}
|
||
if(workspace){localStorage.setItem(SYNC_WORKSPACE,workspace);u.searchParams.delete('workspace');changed=true}
|
||
if(changed)history.replaceState(null,'',u.pathname+u.search+u.hash);
|
||
}catch(_){ }
|
||
}
|
||
|
||
function setSyncStatus(status,msg=''){
|
||
sync.status=status;
|
||
sync.lastError=msg;
|
||
const statusEl=q('#sunSyncSettingsStatus');
|
||
if(statusEl){
|
||
statusEl.textContent=status==='ok'?'Синхронизация активна':status==='error'?`Ошибка: ${msg||'нет связи'}`:'Локальный режим';
|
||
}
|
||
}
|
||
|
||
async function pullRemote(){
|
||
const token=localStorage.getItem(SYNC_TOKEN);
|
||
if(!token||!/^https?:$/.test(location.protocol))return {entries:{}};
|
||
const r=await fetch(`/api/sync?workspace=${encodeURIComponent(syncWorkspace())}`,{headers:syncHeaders(),cache:'no-store'});
|
||
if(!r.ok)throw new Error(`HTTP ${r.status}`);
|
||
return r.json();
|
||
}
|
||
|
||
function scanLocalChanges(meta,force=false){
|
||
const changes={};
|
||
const now=Date.now();
|
||
for(let i=0;i<localStorage.length;i++){
|
||
const key=localStorage.key(i);
|
||
if(!key||excludedKey(key))continue;
|
||
const value=localStorage.getItem(key)??'';
|
||
const h=hash(value);
|
||
const old=meta[key];
|
||
if(force||!old||old.h!==h){
|
||
meta[key]={h,t:now};
|
||
changes[key]={value,updatedAt:now};
|
||
}
|
||
}
|
||
return changes;
|
||
}
|
||
|
||
async function pushChanges(changes){
|
||
if(!Object.keys(changes).length)return;
|
||
const r=await fetch('/api/sync',{method:'POST',headers:syncHeaders(),body:JSON.stringify({workspace:syncWorkspace(),changes})});
|
||
if(!r.ok)throw new Error(`HTTP ${r.status}`);
|
||
return r.json();
|
||
}
|
||
|
||
function activeView(){return qa('.view.on').find(v=>v.id)||null}
|
||
function refreshActiveView(){
|
||
const active=document.activeElement;
|
||
const editing=active&&/^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName);
|
||
if(editing||qa('.modal.on').length)return;
|
||
const view=activeView();
|
||
if(!view)return;
|
||
const map={orders:'Заказы',calendar:'Календарь',shopping:'Закупки',stock:'Склад',clients:'Клиенты',mailings:'Рассылки',money:'Деньги',team:'Команда',suppliers:'Поставщики',stats:'Статистика','sun-print-center':'Печать','sun-routes-view':'Маршруты','enterprise-settings':'Настройки'};
|
||
const label=map[view.id];
|
||
if(!label)return;
|
||
const button=qa('header nav button').find(b=>String(b.textContent||'').trim()===label||String(b.textContent||'').trim()===(label==='Закупки'?'Закупка':label));
|
||
if(button){try{HTMLElement.prototype.click.call(button)}catch(_){}}
|
||
}
|
||
|
||
async function syncNow(){
|
||
if(sync.busy)return;
|
||
const token=localStorage.getItem(SYNC_TOKEN);
|
||
if(!token||!/^https?:$/.test(location.protocol)){setSyncStatus('local');ensureSyncSettings();return}
|
||
sync.busy=true;
|
||
try{
|
||
const meta=getMeta();
|
||
const remote=await pullRemote();
|
||
let remoteChanged=false;
|
||
const entries=remote.entries||{};
|
||
Object.entries(entries).forEach(([key,item])=>{
|
||
if(excludedKey(key)||!item)return;
|
||
const rt=Number(item.updatedAt||0),lt=Number(meta[key]?.t||0);
|
||
if(rt>lt){
|
||
localStorage.setItem(key,String(item.value??''));
|
||
meta[key]={h:hash(item.value??''),t:rt};
|
||
remoteChanged=true;
|
||
}
|
||
});
|
||
const changes=scanLocalChanges(meta,Object.keys(entries).length===0);
|
||
setMeta(meta);
|
||
await pushChanges(changes);
|
||
setSyncStatus('ok');
|
||
ensureSyncSettings();
|
||
if(remoteChanged){
|
||
window.dispatchEvent(new CustomEvent('suncloudsync',{detail:{remoteChanged:true}}));
|
||
refreshActiveView();
|
||
}
|
||
}catch(e){
|
||
setSyncStatus('error',e?.message||'нет связи');
|
||
ensureSyncSettings();
|
||
}finally{sync.busy=false}
|
||
}
|
||
window.sunSyncNow=syncNow;
|
||
|
||
function ensureSyncSettings(){
|
||
const settings=q('#enterprise-settings');
|
||
if(!settings||q('#sunSyncSettingsCard'))return;
|
||
const card=document.createElement('section');
|
||
card.id='sunSyncSettingsCard';
|
||
card.className='settings-card';
|
||
card.style.cssText='margin-top:14px;border:1px solid #e2e4e9;border-radius:12px;background:#fff;padding:14px';
|
||
const token=localStorage.getItem(SYNC_TOKEN)||'';
|
||
card.innerHTML=`<h2 style="margin:0 0 7px;color:#30284f">Синхронизация устройств</h2><p class="hint" style="margin:0 0 10px">Одна база для компьютера и телефона. Интерфейс приложения остаётся одинаковым на всех устройствах.</p><div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"><label>Рабочая база<input id="sunSyncWorkspaceInput" value="${esc(syncWorkspace())}"></label><label>Режим<input value="${token?'Общая база':'Локально'}" readonly></label></div><p style="margin:9px 0"><b id="sunSyncSettingsStatus">${token?'Подключение…':'Локальный режим'}</b></p><div class="actions"><button class="primary" type="button" id="sunSyncNowBtn">Синхронизировать сейчас</button><button class="outline" type="button" id="sunSyncForgetBtn">Отключить общую базу</button></div>${token?'':'<p class="hint">Чтобы включить общую базу, запускайте приложение через server.js и открывайте на телефоне ссылку сервера.</p>'}`;
|
||
const audit=[...settings.children].find(el=>(el.querySelector?.(':scope > h2')?.textContent||'').trim()==='История изменений');
|
||
if(audit)window.SunSafe.insertBefore(settings,card,audit);else settings.appendChild(card);
|
||
q('#sunSyncWorkspaceInput',card)?.addEventListener('change',e=>{localStorage.setItem(SYNC_WORKSPACE,String(e.target.value||'main').trim()||'main');syncNow()});
|
||
const now=q('#sunSyncNowBtn',card);if(now)now.onclick=()=>syncNow();
|
||
const forget=q('#sunSyncForgetBtn',card);if(forget)forget.onclick=()=>{if(confirm('Отключить синхронизацию на этом устройстве? Локальные данные останутся.')){localStorage.removeItem(SYNC_TOKEN);setSyncStatus('local');card.remove();ensureSyncSettings()}};
|
||
setSyncStatus(sync.status,sync.lastError);
|
||
}
|
||
|
||
function initSync(){
|
||
// Legacy same-Wi-Fi server sync is intentionally disabled in Cloud v2 builds.
|
||
// Supabase synchronization is handled by cloud-sync-v2.js.
|
||
setSyncStatus('local');
|
||
}
|
||
|
||
function init(){
|
||
initPwa();
|
||
initSync();
|
||
document.documentElement.classList.add('sun-classic-mobile-ready');
|
||
}
|
||
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: batch-update.js ===== */
|
||
(function(){
|
||
'use strict';
|
||
if(window.__sunBatchUpdate20260828)return;window.__sunBatchUpdate20260828=true;
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const orderList=()=>{try{if(typeof orders!=='undefined'&&Array.isArray(orders))return orders}catch(_){}try{return JSON.parse(localStorage.getItem('sunOrders')||'[]')}catch(_){return[]}};
|
||
const localToday=()=>{const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`};
|
||
const dateKey=o=>/^\d{4}-\d{2}-\d{2}$/.test(String(o?.date||''))?String(o.date):'';
|
||
const timeKey=o=>/^\d{2}:\d{2}$/.test(String(o?.time||''))?String(o.time):'23:59';
|
||
let ordersMode='current';
|
||
function splitOrders(all){
|
||
const today=localToday();
|
||
const current=[],done=[];
|
||
all.forEach(o=>{const d=dateKey(o);(d&&d<today?done:current).push(o)});
|
||
current.sort((a,b)=>{const ad=dateKey(a)||'9999-12-31',bd=dateKey(b)||'9999-12-31';return (ad+timeKey(a)).localeCompare(bd+timeKey(b))||Number(a.id||0)-Number(b.id||0)});
|
||
done.sort((a,b)=>{const ad=dateKey(a)||'0000-00-00',bd=dateKey(b)||'0000-00-00';return (bd+timeKey(b)).localeCompare(ad+timeKey(a))||Number(b.id||0)-Number(a.id||0)});
|
||
return {current,done};
|
||
}
|
||
function ensureOrdersTabs(){
|
||
const view=$('orders');if(!view)return;
|
||
let bar=$('sunOrdersPeriodTabs');
|
||
if(!bar){bar=document.createElement('div');bar.id='sunOrdersPeriodTabs';bar.className='sun-orders-period-tabs';bar.innerHTML='<button type="button" data-orders-period="current">Текущие</button><button type="button" data-orders-period="done">Завершённые</button>';const anchor=view.querySelector('.sun-orders-viewbar')||view.querySelector('.head')||view.firstElementChild?.nextSibling;const h=view.querySelector('h1');if(h)h.insertAdjacentElement('afterend',bar);else view.prepend(bar);bar.addEventListener('click',e=>{const b=e.target.closest('[data-orders-period]');if(!b)return;ordersMode=b.dataset.ordersPeriod==='done'?'done':'current';window.renderOrders?.();});}
|
||
const split=splitOrders(orderList());
|
||
qa('[data-orders-period]',bar).forEach(b=>{const key=b.dataset.ordersPeriod;b.classList.toggle('on',key===ordersMode);const text=key==='current'?`Текущие · ${split.current.length}`:`Завершённые · ${split.done.length}`;if(b.textContent!==text)b.textContent=text});
|
||
}
|
||
const previousRenderOrders=window.renderOrders;
|
||
if(typeof previousRenderOrders==='function')window.renderOrders=function(...args){
|
||
ensureOrdersTabs();
|
||
let ref=null,backup=null;
|
||
try{if(typeof orders!=='undefined'&&Array.isArray(orders)){ref=orders;backup=orders.slice();const split=splitOrders(backup),show=ordersMode==='done'?split.done:split.current;orders.length=0;orders.push(...show)}}catch(_){}
|
||
let result;
|
||
try{result=previousRenderOrders.apply(this,args)}finally{if(ref&&backup){ref.length=0;ref.push(...backup)}}
|
||
setTimeout(()=>{ensureOrdersTabs();compactSettingsLists();setDeliverySteps()},0);
|
||
return result;
|
||
};
|
||
|
||
let backupExpanded=false,auditExpanded=false;
|
||
function compactList(host,rowSelector,kind){
|
||
if(!host)return;const rows=qa(rowSelector,host);const expanded=kind==='backup'?backupExpanded:auditExpanded;
|
||
rows.forEach((r,i)=>{r.style.display=(expanded||i<4)?'':'none'});
|
||
let b=host.querySelector(':scope > .sun-compact-toggle');
|
||
if(rows.length<=4){b?.remove();return}
|
||
if(!b){b=document.createElement('button');b.type='button';b.className='outline sun-compact-toggle';b.onclick=()=>{if(kind==='backup')backupExpanded=!backupExpanded;else auditExpanded=!auditExpanded;compactSettingsLists()};host.appendChild(b)}
|
||
const text=expanded?'Скрыть':`Показать ещё (${rows.length-4})`;if(b.textContent!==text)b.textContent=text;
|
||
}
|
||
function compactSettingsLists(){compactList($('backupList'),'.backup-row','backup');compactList($('auditList'),'.audit-row','audit')}
|
||
|
||
function setDeliverySteps(){
|
||
const delivery=$('deliveryCost');if(delivery)delivery.step='100';
|
||
const transport=$('orderDeliveryCost');if(transport)transport.step='100';
|
||
}
|
||
|
||
function ensureCss(){if($('sun-batch-update-style'))return;const st=document.createElement('style');st.id='sun-batch-update-style';st.textContent=`
|
||
.sun-orders-period-tabs{display:flex;gap:7px;align-items:center;margin:10px 0 13px;padding:4px;width:max-content;max-width:100%;border-radius:12px;background:#f1eff8;border:1px solid #e1dced}.sun-orders-period-tabs button{border:0;background:transparent;color:#716a84;border-radius:9px;padding:7px 12px;font-size:11px;font-weight:800;cursor:pointer}.sun-orders-period-tabs button.on{background:#241d45;color:#fff;box-shadow:0 4px 12px rgba(36,29,69,.18)}.sun-compact-toggle{margin-top:9px!important}.sun-extra-services-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:8px}.sun-extra-service{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid #d9d2e9;border-radius:14px;background:#fff;color:#4e4961}.sun-extra-service:before{content:'✓';display:grid;place-items:center;flex:0 0 24px;height:24px;border-radius:8px;background:#241d45;color:#f2c85f;font-weight:900}.sun-extra-service b{display:block;color:#241d45;font-size:14px}.sun-extra-service small{display:block;color:#817a94;margin-top:2px;line-height:1.3}@media(max-width:620px){.sun-orders-period-tabs{width:100%}.sun-orders-period-tabs button{flex:1}.sun-extra-services-grid{grid-template-columns:1fr}}
|
||
`;document.head.appendChild(st)}
|
||
|
||
function maintain(){ensureCss();ensureOrdersTabs();compactSettingsLists();setDeliverySteps()}
|
||
(()=>{let queued=false;const run=()=>{if(queued)return;queued=true;setTimeout(()=>{queued=false;maintain();},60);};[$('new'),$('orders'),$('enterprise-settings')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
||
document.addEventListener('click',e=>{const nav=e.target.closest('header nav button');if(nav&&String(nav.dataset.navLabel||nav.textContent||'').trim()==='Настройки')setTimeout(compactSettingsLists,80)},true);
|
||
maintain();setTimeout(maintain,200);setTimeout(maintain,900);
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: cloud-sync-v2.js ===== */
|
||
(() => {
|
||
'use strict';
|
||
if (window.SunCloudV2) return;
|
||
|
||
const VERSION = '2026.09.05-cloud-rbac-v7-developer-support';
|
||
const CONFIG_KEY = 'sunCloudV2Config';
|
||
const CLIENT_ID_KEY = 'sunCloudClientIdV2';
|
||
const DB_NAME = 'SunCloudV2';
|
||
const DB_VERSION = 1;
|
||
const BUCKET = 'sun-media';
|
||
const SUPABASE_JS = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.112.4';
|
||
const MEDIA_PREFIX = '__sun_media__:';
|
||
const SYNC_DEBOUNCE = 1800;
|
||
|
||
const $ = id => document.getElementById(id);
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const clone = value => value == null ? value : (typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value)));
|
||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||
const toast = (text, type='info', timeout=4500) => {
|
||
try { if (window.SunEnterprise?.toast) return window.SunEnterprise.toast(text, type, timeout); } catch (_) {}
|
||
console[type === 'error' ? 'error' : 'log']('[SunCloud]', text);
|
||
};
|
||
|
||
const DEFAULT_SUPABASE_URL = 'https://cksuehzcimitsxmeloes.supabase.co';
|
||
const DEFAULT_SUPABASE_KEY = 'sb_publishable_v8Z3hEBnu7zsDwAb5KCWcg_T96IejsM';
|
||
const SUPABASE_API_PROXY = 'https://api.caterium.ru';
|
||
function supabaseProxyFetch(input, init) {
|
||
try {
|
||
const url = typeof input === 'string' ? input : input?.url;
|
||
if (url && url.indexOf(DEFAULT_SUPABASE_URL) === 0) {
|
||
const proxied = SUPABASE_API_PROXY + url.slice(DEFAULT_SUPABASE_URL.length);
|
||
input = typeof input === 'string' ? proxied : new Request(proxied, input);
|
||
}
|
||
} catch (_) {}
|
||
return fetch(input, init);
|
||
}
|
||
|
||
let config = loadConfig();
|
||
let client = null;
|
||
let clientSignature = '';
|
||
let session = null;
|
||
let memberships = [];
|
||
let workspace = null;
|
||
let supportMode = null;
|
||
let membershipsLoading = false;
|
||
let membershipsLoaded = false;
|
||
let realtimeChannel = null;
|
||
let syncTimer = null;
|
||
let isSyncing = false;
|
||
let dirty = false;
|
||
const CLOUD_RPC_TIMEOUT_MS=12000;
|
||
const CLOUD_CONFLICT_MAX_RETRIES=4;
|
||
function errText(e,fallback='Неизвестная ошибка'){
|
||
if(e==null)return fallback;
|
||
if(typeof e==='string')return e||fallback;
|
||
if(e.message)return String(e.message);
|
||
try{const j=JSON.stringify(e);if(j&&j!=='{}'&&j!=='null')return j;}catch(_){}
|
||
return fallback;
|
||
}
|
||
async function sunCloudAwait(promise,label='Облачный запрос',timeout=CLOUD_RPC_TIMEOUT_MS){
|
||
let timer=0;
|
||
try{return await Promise.race([Promise.resolve(promise),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label+': превышено время ожидания ('+Math.round(timeout/1000)+' сек.)')),timeout)})]);}
|
||
finally{if(timer)clearTimeout(timer)}
|
||
}
|
||
let suppressStorageTracking = false;
|
||
let observedSignature = '';
|
||
let lastStatus = 'local';
|
||
let lastError = '';
|
||
|
||
function loadConfig() {
|
||
let value = {};
|
||
try { value = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}') || {}; } catch (_) {}
|
||
if (!value.url || !value.key) {
|
||
try {
|
||
const legacy = JSON.parse(localStorage.getItem('sunEnterpriseSettingsV1') || '{}') || {};
|
||
const cloud = legacy.cloud || {};
|
||
value.url ||= String(cloud.supabaseUrl || '').trim();
|
||
value.key ||= String(cloud.anonKey || '').trim();
|
||
} catch (_) {}
|
||
}
|
||
return {
|
||
url: String(value.url || DEFAULT_SUPABASE_URL || '').trim().replace(/\/$/, ''),
|
||
key: String(value.key || DEFAULT_SUPABASE_KEY || '').trim(),
|
||
autoSync: value.autoSync !== false,
|
||
workspaceId: String(value.workspaceId || ''),
|
||
migrated: value.migrated && typeof value.migrated === 'object' ? value.migrated : {},
|
||
lastSync: String(value.lastSync || ''),
|
||
localWorkspaceId: String(value.localWorkspaceId || ''),
|
||
tenantStorageReady: value.tenantStorageReady === true,
|
||
legacyLocalWorkspaceId: String(value.legacyLocalWorkspaceId || (value.tenantStorageReady === true ? '' : (value.workspaceId || '')))
|
||
};
|
||
}
|
||
|
||
function saveConfig() {
|
||
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
||
}
|
||
|
||
function disableLegacyCloud() {
|
||
try {
|
||
const raw = localStorage.getItem('sunEnterpriseSettingsV1');
|
||
if (!raw) return;
|
||
const data = JSON.parse(raw);
|
||
if (!data || typeof data !== 'object') return;
|
||
data.cloud = Object.assign({}, data.cloud || {}, {enabled:false, autoSync:false, lastSync:data.cloud?.lastSync || ''});
|
||
localStorage.setItem('sunEnterpriseSettingsV1', JSON.stringify(data));
|
||
} catch (_) {}
|
||
}
|
||
|
||
function clientId() {
|
||
let id = localStorage.getItem(CLIENT_ID_KEY);
|
||
if (!id) {
|
||
id = crypto.randomUUID ? crypto.randomUUID() : `client-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||
localStorage.setItem(CLIENT_ID_KEY, id);
|
||
}
|
||
return id;
|
||
}
|
||
|
||
function workspaceMigrated() {
|
||
return Boolean(workspace?.id && config.migrated?.[workspace.id]);
|
||
}
|
||
|
||
const ROLE_LABELS = {admin:'Администратор',manager:'Менеджер',kitchen:'Кухня',courier:'Курьер',viewer:'Только просмотр'};
|
||
function hasPermission(permission) {
|
||
if (!workspace || workspace.is_active === false) return false;
|
||
const value=workspace.permissions?.[permission];
|
||
if (typeof value === 'boolean') return value;
|
||
return workspace.role === 'admin';
|
||
}
|
||
function canWrite() {
|
||
if (supportMode) return false;
|
||
if (!workspace || workspace.is_active === false) return false;
|
||
const permissions=workspace.permissions || {};
|
||
const entries=Object.entries(permissions);
|
||
if (entries.length) return entries.some(([key,value]) => value === true && (key.endsWith('.edit') || key.endsWith('.create') || key.endsWith('.delete') || key === 'users.manage' || key === 'backups.manage'));
|
||
return workspace.role === 'admin';
|
||
}
|
||
|
||
function roleLabel(role) {
|
||
return ROLE_LABELS[role] || role || '—';
|
||
}
|
||
|
||
function shouldSyncKey(key) {
|
||
key = String(key || '');
|
||
if (!key.startsWith('sun')) return false;
|
||
if (key === CONFIG_KEY || key === CLIENT_ID_KEY) return false;
|
||
if (key === 'sunUsersV1' || key === 'sunSessionUserV1') return false;
|
||
if (key === 'sunFallbackBackupsV1') return false;
|
||
if (key === 'sunStaticMapGeocodeCacheV2') return false;
|
||
if (key === 'sunCloudSyncMetaV1' || key === 'sunCloudSyncTokenV1' || key === 'sunCloudSyncWorkspaceV1') return false;
|
||
if (key === 'sunOrdersView' || key === 'sunCalendarMode' || key === 'sunCatalogViewModeV1' || key === 'sunNewOrderSplitRatioV5') return false;
|
||
if (key.startsWith('sunRecoveryCorrupt.')) return false;
|
||
return true;
|
||
}
|
||
|
||
function sanitizeEnterpriseSettings(raw) {
|
||
try {
|
||
const data = JSON.parse(raw);
|
||
if (!data || typeof data !== 'object') return raw;
|
||
data.rolesEnabled = false;
|
||
if (data.cloud) data.cloud = {enabled:false,supabaseUrl:'',anonKey:'',workspaceId:'',autoSync:false,lastSync:''};
|
||
return JSON.stringify(data);
|
||
} catch (_) { return raw; }
|
||
}
|
||
|
||
function openDb() {
|
||
return new Promise((resolve, reject) => {
|
||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||
request.onupgradeneeded = () => {
|
||
const db = request.result;
|
||
if (!db.objectStoreNames.contains('meta')) db.createObjectStore('meta', {keyPath:'key'});
|
||
if (!db.objectStoreNames.contains('media')) db.createObjectStore('media', {keyPath:'path'});
|
||
};
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
}
|
||
|
||
async function idbGet(store, key) {
|
||
try {
|
||
const db = await openDb();
|
||
return await new Promise((resolve, reject) => {
|
||
const request = db.transaction(store, 'readonly').objectStore(store).get(key);
|
||
request.onsuccess = () => resolve(request.result || null);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
} catch (_) { return null; }
|
||
}
|
||
|
||
async function idbPut(store, value) {
|
||
try {
|
||
const db = await openDb();
|
||
await new Promise((resolve, reject) => {
|
||
const tx = db.transaction(store, 'readwrite');
|
||
tx.objectStore(store).put(value);
|
||
tx.oncomplete = resolve;
|
||
tx.onerror = () => reject(tx.error);
|
||
tx.onabort = () => reject(tx.error || new Error('IndexedDB transaction aborted'));
|
||
});
|
||
return true;
|
||
} catch (_) { return false; }
|
||
}
|
||
|
||
// Keep the browser working copy isolated by cloud workspace. The live app still
|
||
// reads the historical unprefixed sun* keys, so before changing workspace we
|
||
// snapshot those keys to IndexedDB, clear them, and restore only the target
|
||
// workspace. This prevents orders/clients/finance/etc. from leaking between
|
||
// accounts while avoiding a second large copy inside localStorage.
|
||
function isTenantLocalKey(key) {
|
||
key=String(key||'');
|
||
if(!key.startsWith('sun'))return false;
|
||
if(key===CONFIG_KEY||key===CLIENT_ID_KEY)return false;
|
||
if(key.startsWith('sunCloud'))return false;
|
||
if(key==='sunStaticMapGeocodeCacheV2')return false;
|
||
return true;
|
||
}
|
||
|
||
function tenantLocalKeys() {
|
||
const out=[];
|
||
for(let i=0;i<localStorage.length;i++){
|
||
const key=localStorage.key(i);
|
||
if(key&&isTenantLocalKey(key))out.push(key);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
async function captureTenantLocal(workspaceId) {
|
||
const id=String(workspaceId||'').trim();
|
||
if(!id)return false;
|
||
const storage={};
|
||
for(const key of tenantLocalKeys())storage[key]=localStorage.getItem(key)??'';
|
||
return await idbPut('meta',{key:`tenant-local:${id}`,value:{workspaceId:id,capturedAt:new Date().toISOString(),storage}});
|
||
}
|
||
|
||
function clearTenantLocal() {
|
||
tenantLocalKeys().forEach(key=>localStorage.removeItem(key));
|
||
}
|
||
|
||
async function restoreTenantLocal(workspaceId) {
|
||
const id=String(workspaceId||'').trim();
|
||
if(!id)return false;
|
||
const row=await idbGet('meta',`tenant-local:${id}`),storage=row?.value?.storage;
|
||
if(!storage||typeof storage!=='object')return false;
|
||
for(const [key,value] of Object.entries(storage)){
|
||
if(isTenantLocalKey(key))localStorage.setItem(key,String(value??''));
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function switchTenantLocal(nextWorkspaceId) {
|
||
const next=String(nextWorkspaceId||'').trim();
|
||
// One-time upgrade adoption. If the old config already named a workspace,
|
||
// current local data belongs to that legacy workspace only. This prevents a
|
||
// signed-out old database from being accidentally attached to a different
|
||
// account on the first login after upgrading.
|
||
if(!config.tenantStorageReady){
|
||
const legacy=String(config.legacyLocalWorkspaceId||'').trim();
|
||
if(legacy&&legacy!==next){
|
||
await captureTenantLocal(legacy);
|
||
clearTenantLocal();
|
||
config.tenantStorageReady=true;
|
||
config.localWorkspaceId=next;
|
||
config.legacyLocalWorkspaceId='';
|
||
let restored=false;
|
||
if(next)restored=await restoreTenantLocal(next);
|
||
if(next&&!restored)config.migrated[next]=false;
|
||
saveConfig();
|
||
return {changed:true,restored,legacyCaptured:true};
|
||
}
|
||
if(next){
|
||
config.tenantStorageReady=true;
|
||
config.localWorkspaceId=next;
|
||
config.legacyLocalWorkspaceId='';
|
||
saveConfig();
|
||
await captureTenantLocal(next);
|
||
return {changed:false,restored:true,adopted:true};
|
||
}
|
||
// A purely local pre-cloud installation has no workspace id yet. Keep it
|
||
// untouched until the first company is selected, so migration stays safe.
|
||
return {changed:false,restored:false};
|
||
}
|
||
const prev=String(config.localWorkspaceId||'').trim();
|
||
if(prev===next)return {changed:false,restored:Boolean(next)};
|
||
if(prev)await captureTenantLocal(prev);
|
||
clearTenantLocal();
|
||
config.localWorkspaceId=next;
|
||
let restored=false;
|
||
if(next)restored=await restoreTenantLocal(next);
|
||
// If this device has no saved working copy for the target workspace, force a
|
||
// clean cloud pull on reload instead of merging an empty browser state.
|
||
if(next&&!restored)config.migrated[next]=false;
|
||
saveConfig();
|
||
return {changed:true,restored};
|
||
}
|
||
|
||
async function adoptCurrentLocalAsWorkspace(workspaceId) {
|
||
const id=String(workspaceId||'').trim();
|
||
if(!id)return false;
|
||
config.tenantStorageReady=true;
|
||
config.localWorkspaceId=id;
|
||
config.legacyLocalWorkspaceId='';
|
||
config.workspaceId=id;
|
||
config.migrated[id]=false;
|
||
saveConfig();
|
||
await captureTenantLocal(id);
|
||
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));
|
||
}
|
||
|
||
let signOutInProgress=false;
|
||
|
||
function clearLocalSupabaseSessionFallback() {
|
||
// Supabase normally clears its own persisted auth token. If the network call
|
||
// is blocked or hangs, remove only this project's auth keys so the user can
|
||
// still leave the account on the current device without touching app data.
|
||
let projectRef='';
|
||
try{projectRef=new URL(config.url).hostname.split('.')[0]||'';}catch(_){}
|
||
const prefixes=projectRef?[`sb-${projectRef}-auth-token`]:[];
|
||
for(const storage of [localStorage,sessionStorage]){
|
||
try{
|
||
const remove=[];
|
||
for(let i=0;i<storage.length;i++){
|
||
const key=storage.key(i)||'';
|
||
if(prefixes.some(prefix=>key===prefix||key.startsWith(prefix+'.')||key.startsWith(prefix+'-')))remove.push(key);
|
||
}
|
||
remove.forEach(key=>storage.removeItem(key));
|
||
}catch(_){}
|
||
}
|
||
}
|
||
|
||
function setSignOutUiBusy(busy) {
|
||
const selectors=['#sunCloudSignOutV2','#sunLogoutBtn','#sunGateSignOutV3','#sunCloudUsePasswordV3','[data-signout]','[data-saas-signout]'];
|
||
document.querySelectorAll(selectors.join(',')).forEach(btn=>{
|
||
if(!(btn instanceof HTMLButtonElement))return;
|
||
if(busy){btn.dataset.sunOldText=btn.textContent||'';btn.disabled=true;btn.textContent='Выходим…';}
|
||
else{btn.disabled=false;if(btn.dataset.sunOldText){btn.textContent=btn.dataset.sunOldText;delete btn.dataset.sunOldText;}}
|
||
});
|
||
}
|
||
|
||
async function signOutAndReload() {
|
||
if(signOutInProgress)return;
|
||
signOutInProgress=true;
|
||
setSignOutUiBusy(true);
|
||
clearTimeout(syncTimer);
|
||
setStatus('syncing','Выходим из аккаунта…');
|
||
try{
|
||
// Preserve the company working copy first. Do not block logout on a cloud
|
||
// sync: the workspace snapshot remains available and will sync after the
|
||
// next login if there were unsent changes.
|
||
const snapshot=Promise.resolve().then(async()=>{if(supportMode){clearTenantLocal();supportMode=null;config.localWorkspaceId='';saveConfig();return {changed:true};}return switchTenantLocal('');});
|
||
await Promise.race([snapshot,sleep(2200).then(()=>{throw new Error('Tenant snapshot timeout');})]);
|
||
}catch(error){
|
||
console.warn('[SunCloud] tenant snapshot before sign-out failed',error);
|
||
try{clearTenantLocal();config.localWorkspaceId='';saveConfig();}catch(_){}
|
||
}
|
||
try{
|
||
if(client?.auth){
|
||
const result=await Promise.race([
|
||
client.auth.signOut({scope:'local'}),
|
||
sleep(2500).then(()=>{throw new Error('Supabase local sign-out timeout');})
|
||
]);
|
||
if(result?.error)throw result.error;
|
||
}else clearLocalSupabaseSessionFallback();
|
||
}catch(error){
|
||
console.warn('[SunCloud] Supabase sign-out fallback',error);
|
||
try{client?.auth.stopAutoRefresh?.();}catch(_){}
|
||
clearLocalSupabaseSessionFallback();
|
||
}
|
||
session=null;workspace=null;memberships=[];membershipsLoading=false;membershipsLoaded=true;unsubscribeRealtime();
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed'));}catch(_){}
|
||
// Reload regardless of network state so a blocked logout endpoint can never
|
||
// leave the user trapped in the previous account screen.
|
||
location.reload();
|
||
}
|
||
|
||
async function getBaseline() {
|
||
if (!workspace?.id) return null;
|
||
const row = await idbGet('meta', `baseline:${workspace.id}`);
|
||
return row?.value || null;
|
||
}
|
||
|
||
async function setBaseline(row) {
|
||
if (!workspace?.id || !row) return;
|
||
await idbPut('meta', {key:`baseline:${workspace.id}`, value:{payload:clone(row.payload),revision:Number(row.revision || 0),updated_at:row.updated_at || '',client_id:row.client_id || ''}});
|
||
}
|
||
|
||
function stable(value) {
|
||
if (value === undefined) return '__undefined__';
|
||
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
||
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
||
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${stable(value[k])}`).join(',')}}`;
|
||
}
|
||
|
||
function equal(a, b) { return stable(a) === stable(b); }
|
||
|
||
function isPlainObject(value) {
|
||
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||
}
|
||
|
||
function idArray(value) {
|
||
return Array.isArray(value) && value.every(item => isPlainObject(item) && item.id != null);
|
||
}
|
||
|
||
function mergeById(base, local, remote, conflicts, path) {
|
||
const bm = new Map((base || []).map(x => [String(x.id), x]));
|
||
const lm = new Map((local || []).map(x => [String(x.id), x]));
|
||
const rm = new Map((remote || []).map(x => [String(x.id), x]));
|
||
const order = [];
|
||
[...(remote || []), ...(local || []), ...(base || [])].forEach(x => { const id=String(x.id); if (!order.includes(id)) order.push(id); });
|
||
const out = [];
|
||
for (const id of order) {
|
||
const b = bm.get(id), l = lm.get(id), r = rm.get(id);
|
||
if (l === undefined && r === undefined) continue;
|
||
if (l === undefined) {
|
||
if (b !== undefined && !equal(r,b)) conflicts.push(`${path}[${id}]: удалено локально, изменено в облаке`);
|
||
if (b === undefined || !equal(r,b)) out.push(clone(r));
|
||
continue;
|
||
}
|
||
if (r === undefined) {
|
||
if (b !== undefined && !equal(l,b)) { conflicts.push(`${path}[${id}]: изменено локально, удалено в облаке`); out.push(clone(l)); }
|
||
else if (b === undefined) out.push(clone(l));
|
||
continue;
|
||
}
|
||
out.push(mergeNode(b, l, r, conflicts, `${path}[${id}]`));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function mergeNode(base, local, remote, conflicts, path='state') {
|
||
if (equal(local, remote)) return clone(local);
|
||
if (equal(local, base)) return clone(remote);
|
||
if (equal(remote, base)) return clone(local);
|
||
|
||
if (idArray(local) && idArray(remote) && (!base || idArray(base))) {
|
||
return mergeById(Array.isArray(base)?base:[], local, remote, conflicts, path);
|
||
}
|
||
|
||
if (isPlainObject(local) && isPlainObject(remote) && (base == null || isPlainObject(base))) {
|
||
const out = {};
|
||
const keys = new Set([...Object.keys(base || {}), ...Object.keys(local), ...Object.keys(remote)]);
|
||
for (const key of keys) {
|
||
const b = base?.[key], l = local[key], r = remote[key];
|
||
if (!(key in local) && !(key in remote)) continue;
|
||
if (!(key in local)) {
|
||
if (b !== undefined && !equal(r,b)) conflicts.push(`${path}.${key}: удалено локально, изменено в облаке`);
|
||
if (b === undefined || !equal(r,b)) out[key] = clone(r);
|
||
continue;
|
||
}
|
||
if (!(key in remote)) {
|
||
if (b !== undefined && !equal(l,b)) { conflicts.push(`${path}.${key}: изменено локально, удалено в облаке`); out[key]=clone(l); }
|
||
else if (b === undefined) out[key]=clone(l);
|
||
continue;
|
||
}
|
||
out[key] = mergeNode(b, l, r, conflicts, `${path}.${key}`);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
conflicts.push(path);
|
||
return clone(local);
|
||
}
|
||
|
||
function mergePayloads(basePayload, localPayload, remotePayload) {
|
||
const conflicts = [];
|
||
const base = basePayload?.storage || {};
|
||
const local = localPayload?.storage || {};
|
||
const remote = remotePayload?.storage || {};
|
||
const merged = {};
|
||
const keys = new Set([...Object.keys(base), ...Object.keys(local), ...Object.keys(remote)]);
|
||
for (const key of keys) {
|
||
const b = base[key], l = local[key], r = remote[key];
|
||
if (l === undefined && r === undefined) continue;
|
||
if (l === undefined) {
|
||
if (b !== undefined && !equal(r,b)) conflicts.push(key);
|
||
if (b === undefined || !equal(r,b)) merged[key] = clone(r);
|
||
continue;
|
||
}
|
||
if (r === undefined) {
|
||
if (b !== undefined && !equal(l,b)) { conflicts.push(key); merged[key] = clone(l); }
|
||
else if (b === undefined) merged[key] = clone(l);
|
||
continue;
|
||
}
|
||
merged[key] = mergeNode(b, l, r, conflicts, key);
|
||
}
|
||
return {payload:{format:'sun-cloud-v2',version:2,storage:merged}, conflicts:[...new Set(conflicts)]};
|
||
}
|
||
|
||
function dataUrlToBlob(dataUrl) {
|
||
const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(dataUrl || '');
|
||
if (!match) throw new Error('Invalid data URL');
|
||
const mime = match[1] || 'application/octet-stream';
|
||
const binary = match[2] ? atob(match[3]) : decodeURIComponent(match[3]);
|
||
const bytes = new Uint8Array(binary.length);
|
||
for (let i=0;i<binary.length;i++) bytes[i] = binary.charCodeAt(i);
|
||
return new Blob([bytes], {type:mime});
|
||
}
|
||
|
||
function blobToDataUrl(blob) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => resolve(String(reader.result || ''));
|
||
reader.onerror = () => reject(reader.error || new Error('FileReader error'));
|
||
reader.readAsDataURL(blob);
|
||
});
|
||
}
|
||
|
||
async function shrinkImageIfNeeded(dataUrl, blob) {
|
||
if (blob.size <= 12 * 1024 * 1024) return blob;
|
||
const image = new Image();
|
||
image.src = dataUrl;
|
||
await new Promise((resolve, reject) => { image.onload=resolve; image.onerror=reject; });
|
||
const max = 2400;
|
||
const scale = Math.min(1, max / Math.max(image.naturalWidth || 1, image.naturalHeight || 1));
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = Math.max(1, Math.round((image.naturalWidth || 1) * scale));
|
||
canvas.height = Math.max(1, Math.round((image.naturalHeight || 1) * scale));
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.fillStyle = '#fff'; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.drawImage(image,0,0,canvas.width,canvas.height);
|
||
return await new Promise(resolve => canvas.toBlob(value => resolve(value || blob), 'image/jpeg', .9));
|
||
}
|
||
|
||
async function sha256(blob) {
|
||
const digest = await crypto.subtle.digest('SHA-256', await blob.arrayBuffer());
|
||
return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2,'0')).join('');
|
||
}
|
||
|
||
function extensionFor(type) {
|
||
return ({'image/jpeg':'jpg','image/png':'png','image/webp':'webp','image/gif':'gif'})[String(type||'').toLowerCase()] || 'bin';
|
||
}
|
||
|
||
async function uploadDataUrl(dataUrl) {
|
||
if (!client || !workspace?.id) return dataUrl;
|
||
let blob = dataUrlToBlob(dataUrl);
|
||
blob = await shrinkImageIfNeeded(dataUrl, blob);
|
||
const hash = await sha256(blob);
|
||
const ext = extensionFor(blob.type);
|
||
const path = `${workspace.id}/media/${hash.slice(0,2)}/${hash}.${ext}`;
|
||
const {error} = await client.storage.from(BUCKET).upload(path, blob, {upsert:false,contentType:blob.type || 'application/octet-stream',cacheControl:'31536000'});
|
||
if (error && !/exist|duplicate|409/i.test(`${error.message || ''} ${error.statusCode || ''}`)) throw error;
|
||
await idbPut('media', {path, blob, cachedAt:new Date().toISOString()});
|
||
return MEDIA_PREFIX + path;
|
||
}
|
||
|
||
async function encodeMedia(value) {
|
||
if (typeof value === 'string') {
|
||
if (/^data:image\//i.test(value)) return await uploadDataUrl(value);
|
||
return value;
|
||
}
|
||
if (Array.isArray(value)) {
|
||
const out = [];
|
||
for (const item of value) out.push(await encodeMedia(item));
|
||
return out;
|
||
}
|
||
if (isPlainObject(value)) {
|
||
const out = {};
|
||
for (const [key,item] of Object.entries(value)) out[key] = await encodeMedia(item);
|
||
return out;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
async function downloadMedia(path) {
|
||
const cached = await idbGet('media', path);
|
||
if (cached?.blob) return await blobToDataUrl(cached.blob);
|
||
const {data, error} = await client.storage.from(BUCKET).download(path);
|
||
if (error) throw error;
|
||
await idbPut('media', {path, blob:data, cachedAt:new Date().toISOString()});
|
||
return await blobToDataUrl(data);
|
||
}
|
||
|
||
async function decodeMedia(value) {
|
||
if (typeof value === 'string' && value.startsWith(MEDIA_PREFIX)) return await downloadMedia(value.slice(MEDIA_PREFIX.length));
|
||
if (Array.isArray(value)) {
|
||
const out=[]; for (const item of value) out.push(await decodeMedia(item)); return out;
|
||
}
|
||
if (isPlainObject(value)) {
|
||
const out={}; for (const [key,item] of Object.entries(value)) out[key]=await decodeMedia(item); return out;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function quotaExceeded(error) {return error?.name==='QuotaExceededError'||/quota|exceeded/i.test(String(error?.message||error||''));}
|
||
|
||
function compactLocalImageData(dataUrl,{maxSide=900,maxChars=130000,quality=.78,force=false}={}) {
|
||
return new Promise(resolve => {
|
||
if (!/^data:image\//i.test(String(dataUrl || ''))) return resolve(dataUrl);
|
||
const image = new Image();
|
||
image.onerror = () => resolve(dataUrl);
|
||
image.onload = () => {
|
||
const iw=Math.max(1,image.naturalWidth||image.width||1), ih=Math.max(1,image.naturalHeight||image.height||1);
|
||
if (!force && String(dataUrl).length<=maxChars && Math.max(iw,ih)<=maxSide) return resolve(dataUrl);
|
||
let scale=Math.min(1,maxSide/Math.max(iw,ih)), q=quality, best=dataUrl;
|
||
for (let attempt=0; attempt<5; attempt++) {
|
||
const canvas=document.createElement('canvas');
|
||
canvas.width=Math.max(1,Math.round(iw*scale)); canvas.height=Math.max(1,Math.round(ih*scale));
|
||
const ctx=canvas.getContext('2d'); if(!ctx) break;
|
||
ctx.fillStyle='#fff'; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.drawImage(image,0,0,canvas.width,canvas.height);
|
||
try { best=canvas.toDataURL('image/jpeg',q)||best; } catch (_) { break; }
|
||
if (best.length<=maxChars) break;
|
||
scale*=.78; q=Math.max(.52,q-.07);
|
||
}
|
||
resolve(best);
|
||
};
|
||
image.src=dataUrl;
|
||
});
|
||
}
|
||
|
||
async function compactSunBoxesForStorage(value, options={}) {
|
||
if (!Array.isArray(value)) return value;
|
||
const out=[];
|
||
for (const item of value) {
|
||
if (!isPlainObject(item)) { out.push(item); continue; }
|
||
const copy={...item};
|
||
if (typeof copy.photo==='string' && /^data:image\//i.test(copy.photo)) copy.photo=await compactLocalImageData(copy.photo,options);
|
||
out.push(copy);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
async function collectLocalPayload() {
|
||
const storage = {};
|
||
for (let i=0;i<localStorage.length;i++) {
|
||
const key = localStorage.key(i);
|
||
if (!key || !shouldSyncKey(key)) continue;
|
||
let raw = localStorage.getItem(key) ?? '';
|
||
if (key === 'sunEnterpriseSettingsV1') raw = sanitizeEnterpriseSettings(raw);
|
||
try {
|
||
const parsed = JSON.parse(raw);
|
||
storage[key] = {t:'j',v:await encodeMedia(parsed)};
|
||
} catch (_) {
|
||
storage[key] = {t:'s',v:await encodeMedia(raw)};
|
||
}
|
||
}
|
||
return {format:'sun-cloud-v2',version:2,storage};
|
||
}
|
||
|
||
function currentSyncableKeys() {
|
||
const out=[];
|
||
for(let i=0;i<localStorage.length;i++){
|
||
const key=localStorage.key(i); if(key&&shouldSyncKey(key)) out.push(key);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
async function applyPayload(payload, replace=false) {
|
||
if (!payload?.storage || typeof payload.storage !== 'object') throw new Error('Некорректный формат облачной базы.');
|
||
suppressStorageTracking = true;
|
||
try {
|
||
if (replace) {
|
||
const remoteKeys = new Set(Object.keys(payload.storage));
|
||
currentSyncableKeys().forEach(key => { if (!remoteKeys.has(key)) localStorage.removeItem(key); });
|
||
}
|
||
for (const [key,entry] of Object.entries(payload.storage)) {
|
||
if (!shouldSyncKey(key) || !entry) continue;
|
||
let decoded = await decodeMedia(entry.v);
|
||
if (key === 'sunBoxes' && entry.t === 'j') decoded = await compactSunBoxesForStorage(decoded,{maxSide:900,maxChars:130000,quality:.78});
|
||
let raw = entry.t === 'j' ? JSON.stringify(decoded) : String(decoded ?? '');
|
||
try {
|
||
localStorage.setItem(key, raw);
|
||
} catch (error) {
|
||
if (key !== 'sunBoxes' || entry.t !== 'j' || !quotaExceeded(error)) throw error;
|
||
decoded = await compactSunBoxesForStorage(decoded,{maxSide:640,maxChars:80000,quality:.66,force:true});
|
||
raw = JSON.stringify(decoded);
|
||
try { localStorage.setItem(key, raw); }
|
||
catch (retryError) {
|
||
if (quotaExceeded(retryError)) throw new Error('Каталог с фотографиями все еще превышает локальный лимит браузера даже после оптимизации фото.');
|
||
throw retryError;
|
||
}
|
||
toast('Фотографии каталога оптимизированы, чтобы не превышать лимит браузера.','info',5200);
|
||
}
|
||
}
|
||
} finally {
|
||
suppressStorageTracking = false;
|
||
}
|
||
refreshRuntime();
|
||
}
|
||
|
||
function refreshRuntime() {
|
||
try { if (typeof boxes !== 'undefined') boxes = JSON.parse(localStorage.getItem('sunBoxes') || '[]'); } catch (_) {}
|
||
try { if (typeof orders !== 'undefined') orders = JSON.parse(localStorage.getItem('sunOrders') || '[]'); } catch (_) {}
|
||
for (const name of ['render','renderOrders','renderStats','renderClients','renderStock','renderManager']) {
|
||
try { if (typeof window[name] === 'function') window[name](); } catch (_) {}
|
||
}
|
||
try { window.dispatchEvent(new CustomEvent('sun:cloud-state-applied')); } catch (_) {}
|
||
}
|
||
|
||
function payloadEmpty(payload) {
|
||
return !payload?.storage || Object.keys(payload.storage).length === 0;
|
||
}
|
||
|
||
async function fetchRemoteRow() {
|
||
if (!client || !workspace?.id) return null;
|
||
const {data,error} = await sunCloudAwait(client.rpc('sun_fetch_app_state',{p_workspace:workspace.id}),'Загрузка облачной базы');
|
||
if (error) throw error;
|
||
return Array.isArray(data) ? (data[0] || null) : (data || null);
|
||
}
|
||
|
||
async function upsertPayload(payload, expectedRevision=null) {
|
||
const args={p_workspace:workspace.id,p_payload:payload,p_client_id:clientId(),p_expected_revision:expectedRevision==null?null:Number(expectedRevision)};
|
||
const {data,error} = await sunCloudAwait(client.rpc('sun_save_app_state_v17',args),'Сохранение облачной базы');
|
||
if (error) throw error;
|
||
return Array.isArray(data) ? data[0] : data;
|
||
}
|
||
|
||
async function backupBeforeCloud(reason) {
|
||
try { await window.SunEnterprise?.createBackup?.(reason); } catch (_) {}
|
||
}
|
||
|
||
async function migrateLocalToCloud() {
|
||
if (!canWrite()) return toast('Для переноса данных нужны права на изменение рабочей базы.','warn');
|
||
if (!navigator.onLine) return toast('Нет подключения к интернету. Локальные данные сохранены и никуда не пропадут.','warn');
|
||
setStatus('syncing','Создаю первую облачную копию…');
|
||
try {
|
||
await backupBeforeCloud('Перед первым переносом в Supabase');
|
||
const remote = await fetchRemoteRow();
|
||
if (remote && !payloadEmpty(remote.payload)) {
|
||
if (!confirm('В этой рабочей базе уже есть облачные данные. Заменить их текущими данными с этого устройства?')) {
|
||
setStatus('ready','Перенос отменён.'); return;
|
||
}
|
||
}
|
||
const localPayload = await collectLocalPayload();
|
||
const row = await upsertPayload(localPayload, remote?.revision ?? null);
|
||
await setBaseline(row);
|
||
config.migrated[workspace.id] = true;
|
||
config.lastSync = row.updated_at || new Date().toISOString();
|
||
config.autoSync = true;
|
||
saveConfig(); dirty=false;
|
||
setStatus('ready','Все текущие данные перенесены в облако.');
|
||
toast('Данные и загруженные фотографии перенесены в Supabase. Автосинхронизация включена.','success',6500);
|
||
subscribeRealtime();
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-sync-complete',{detail:{row:clone(row),payload:clone(localPayload),kind:'migrate'}}));}catch(_){}
|
||
renderCloudUI();
|
||
} catch (error) { handleError(error, 'Не удалось перенести данные в облако.'); }
|
||
}
|
||
|
||
async function pullRemote({replace=false,quiet=false}={}) {
|
||
if (!client || !workspace?.id) return;
|
||
setStatus('syncing','Загружаю облачные данные…');
|
||
try {
|
||
const row = await fetchRemoteRow();
|
||
if (!row || payloadEmpty(row.payload)) {
|
||
setStatus('ready','В облаке пока нет данных.'); return;
|
||
}
|
||
if (!quiet) await backupBeforeCloud('Перед загрузкой данных из Supabase');
|
||
await applyPayload(row.payload, replace);
|
||
await setBaseline(row);
|
||
config.migrated[workspace.id] = true;
|
||
config.lastSync = row.updated_at || new Date().toISOString();
|
||
saveConfig(); dirty=false;
|
||
setStatus('ready','Облачные данные загружены.');
|
||
if (!quiet) toast('Данные загружены из облака.','success');
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-sync-complete',{detail:{row:clone(row),payload:clone(row.payload),kind:'pull'}}));}catch(_){}
|
||
renderCloudUI();
|
||
} catch (error) { handleError(error,'Не удалось загрузить облачные данные.'); }
|
||
}
|
||
|
||
async function syncNow({quiet=false,retryCount=0}={}) {
|
||
if (isSyncing || !client || !session || !workspace?.id || !workspaceMigrated()) return;
|
||
if (!navigator.onLine) { setStatus('offline','Офлайн — изменения ждут подключения.'); return; }
|
||
isSyncing = true;
|
||
setStatus('syncing','Синхронизация…');
|
||
try {
|
||
const [baseline, remote, localPayload] = await Promise.all([getBaseline(), fetchRemoteRow(), collectLocalPayload()]);
|
||
if (!remote || payloadEmpty(remote.payload)) {
|
||
if (!canWrite()) throw new Error('В облаке нет базы, а у пользователя нет прав на её создание.');
|
||
const row = await upsertPayload(localPayload, remote?.revision ?? null);
|
||
await setBaseline(row); dirty=false; config.lastSync=row.updated_at||new Date().toISOString();saveConfig();
|
||
setStatus('ready','Синхронизировано.'); if(!quiet)toast('Синхронизация завершена.','success'); return;
|
||
}
|
||
|
||
if (!baseline) {
|
||
await applyPayload(remote.payload, true);
|
||
await setBaseline(remote); dirty=false; config.lastSync=remote.updated_at||new Date().toISOString();saveConfig();
|
||
setStatus('ready','Подключена существующая облачная база.'); if(!quiet)toast('Загружена актуальная облачная база.','success'); return;
|
||
}
|
||
|
||
const merged = mergePayloads(baseline.payload, localPayload, remote.payload);
|
||
let finalRow = remote;
|
||
const changedVsRemote = !equal(merged.payload, remote.payload);
|
||
const changedVsLocal = !equal(merged.payload, localPayload);
|
||
|
||
if (changedVsRemote) {
|
||
if (!canWrite()) {
|
||
await applyPayload(remote.payload, true);
|
||
finalRow = remote;
|
||
} else {
|
||
finalRow = await upsertPayload(merged.payload, remote.revision);
|
||
if (changedVsLocal) await applyPayload(merged.payload, true);
|
||
}
|
||
} else if (changedVsLocal) {
|
||
await applyPayload(remote.payload, true);
|
||
}
|
||
|
||
await setBaseline(finalRow);
|
||
dirty=false;
|
||
config.lastSync=finalRow.updated_at||new Date().toISOString();saveConfig();
|
||
setStatus('ready', merged.conflicts.length ? `Синхронизировано, объединено конфликтов: ${merged.conflicts.length}.` : 'Синхронизировано.');
|
||
if (merged.conflicts.length) toast(`Изменения с двух устройств объединены. Проверено конфликтов: ${merged.conflicts.length}.`,'warn',6500);
|
||
else if (!quiet) toast('Синхронизация завершена.','success');
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-sync-complete',{detail:{row:clone(finalRow),payload:clone(finalRow.payload),kind:'sync',conflicts:merged.conflicts.length}}));}catch(_){}
|
||
window.SunStabilityV17?.markSynced?.(finalRow?.revision);
|
||
renderCloudUI();
|
||
} catch (error) {
|
||
const msg=String(error?.message||'');
|
||
if(msg.includes('SUN_CONFLICT')){
|
||
const nextRetry=Number(retryCount||0)+1;
|
||
try{window.SunStabilityV17?.recordConflict?.(msg);}catch(_){}
|
||
if(nextRetry>CLOUD_CONFLICT_MAX_RETRIES){handleError(error,'Не удалось синхронизировать после нескольких безопасных повторов.');}
|
||
else{const delay=Math.min(5000,400*Math.pow(2,nextRetry-1));setStatus('pending','Облачная версия изменилась. Повтор '+nextRetry+'/'+CLOUD_CONFLICT_MAX_RETRIES+' через '+(Math.round(delay/100)/10)+' сек.…');setTimeout(()=>syncNow({quiet:true,retryCount:nextRetry}),delay);}
|
||
}else handleError(error,'Ошибка синхронизации.');
|
||
}
|
||
finally { isSyncing=false; }
|
||
}
|
||
|
||
async function handleRealtimeRow(row) {
|
||
if (!row || row.client_id === clientId() || isSyncing || !workspaceMigrated()) return;
|
||
try {
|
||
const baseline = await getBaseline();
|
||
if (!baseline) { await applyPayload(row.payload,true);await setBaseline(row);setStatus('ready','Получены данные с другого устройства.');return; }
|
||
const localPayload = await collectLocalPayload();
|
||
const merged = mergePayloads(baseline.payload, localPayload, row.payload);
|
||
await applyPayload(merged.payload, true);
|
||
if (!equal(merged.payload,row.payload) && canWrite()) {
|
||
const saved=await upsertPayload(merged.payload,row.revision);await setBaseline(saved);
|
||
} else await setBaseline(row);
|
||
dirty=false;config.lastSync=row.updated_at||new Date().toISOString();saveConfig();
|
||
setStatus('ready','Получены изменения с другого устройства.');
|
||
toast(merged.conflicts.length?'Получены облачные изменения; параллельные правки объединены.':'Данные обновлены с другого устройства.','success',3500);
|
||
renderCloudUI();
|
||
} catch (error) { handleError(error,'Не удалось применить изменения с другого устройства.'); }
|
||
}
|
||
|
||
function scheduleSync(reason='change') {
|
||
dirty = true;
|
||
try{window.SunStabilityV17?.markPending?.(reason);}catch(_){}
|
||
if (!config.autoSync || !workspaceMigrated() || !session || !workspace || !canWrite()) {
|
||
updatePill(); return;
|
||
}
|
||
clearTimeout(syncTimer);
|
||
syncTimer = setTimeout(() => syncNow({quiet:true}), SYNC_DEBOUNCE);
|
||
if (!navigator.onLine) setStatus('offline','Офлайн — изменения сохранены локально.');
|
||
else setStatus('pending',`Изменения ожидают синхронизации (${reason}).`);
|
||
}
|
||
|
||
function storageSignature() {
|
||
const parts=[];
|
||
for(let i=0;i<localStorage.length;i++){
|
||
const key=localStorage.key(i); if(!key||!shouldSyncKey(key))continue;
|
||
const value=localStorage.getItem(key)||'';
|
||
let h=2166136261;
|
||
const step=Math.max(1,Math.floor(value.length/64));
|
||
for(let j=0;j<value.length;j+=step){h^=value.charCodeAt(j);h=Math.imul(h,16777619);}
|
||
parts.push(`${key}:${value.length}:${h>>>0}`);
|
||
}
|
||
return parts.sort().join('|');
|
||
}
|
||
|
||
function installStorageTracking() {
|
||
if (Storage.prototype.__sunCloudV2Tracked) return;
|
||
const nativeSet = Storage.prototype.setItem;
|
||
const nativeRemove = Storage.prototype.removeItem;
|
||
Object.defineProperty(Storage.prototype,'__sunCloudV2Tracked',{value:true,configurable:true});
|
||
Storage.prototype.setItem=function(key,value){
|
||
const old = this===localStorage ? this.getItem(key) : null;
|
||
const result = nativeSet.call(this,key,value);
|
||
if(this===localStorage&&!suppressStorageTracking&&shouldSyncKey(key)&&old!==String(value))scheduleSync(key);
|
||
return result;
|
||
};
|
||
Storage.prototype.removeItem=function(key){
|
||
const had=this===localStorage&&this.getItem(key)!==null;
|
||
const result=nativeRemove.call(this,key);
|
||
if(this===localStorage&&!suppressStorageTracking&&had&&shouldSyncKey(key))scheduleSync(key);
|
||
return result;
|
||
};
|
||
observedSignature=storageSignature();
|
||
setInterval(()=>{
|
||
if(document.hidden||suppressStorageTracking)return;
|
||
const next=storageSignature();
|
||
if(next!==observedSignature){observedSignature=next;scheduleSync('данные приложения');}
|
||
},9000);
|
||
['sun:catalog-categories-changed','sun:client-offer-catalog-changed','sun:cloud-dirty'].forEach(name=>window.addEventListener(name,()=>scheduleSync(name)));
|
||
}
|
||
|
||
function setStatus(kind, text='') {
|
||
lastStatus=kind;lastError=kind==='error'?text:'';
|
||
const el=$('sunCloudV2Status');
|
||
if(el){el.textContent=text||statusText();el.dataset.kind=kind;}
|
||
updatePill();
|
||
}
|
||
|
||
function statusText() {
|
||
if(lastStatus==='error')return lastError||'Ошибка облака';
|
||
if(!config.url||!config.key)return 'Облако не подключено';
|
||
if(!navigator.onLine)return 'Офлайн — работаем с локальной копией';
|
||
if(!session)return 'Нужно войти в облачный аккаунт';
|
||
if(supportMode)return `Режим поддержки · ${supportMode.name||'Компания'} · только просмотр`;
|
||
if(!workspace)return 'Нужно выбрать или создать рабочую базу';
|
||
if(!workspaceMigrated())return 'Нужен первый перенос данных в облако';
|
||
if(lastStatus==='syncing')return 'Синхронизация…';
|
||
if(dirty)return 'Есть несинхронизированные изменения';
|
||
return config.lastSync?`Синхронизировано ${new Date(config.lastSync).toLocaleString('ru-RU')}`:'Облако готово';
|
||
}
|
||
|
||
// Облачный статус больше не висит поверх всех разделов.
|
||
// Управление облаком доступно только в «Настройки → Облако Supabase».
|
||
function ensurePill() {
|
||
const pill=$('sunCloudPillV2');
|
||
if(pill) pill.remove();
|
||
return null;
|
||
}
|
||
|
||
function updatePill() {
|
||
ensurePill();
|
||
}
|
||
|
||
function injectStyles() {
|
||
if($('sunCloudV2Styles'))return;
|
||
const style=document.createElement('style');style.id='sunCloudV2Styles';style.textContent=`
|
||
.sun-cloud-v2-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.sun-cloud-v2-grid .wide{grid-column:1/-1}.sun-cloud-v2-box{border:1px solid #e0e4e6;border-radius:9px;padding:12px;background:#fbfcfc}.sun-cloud-v2-box h3{margin:0 0 8px;color:#15364c;font-size:14px}.sun-cloud-v2-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.sun-cloud-v2-status{margin-top:10px;border-radius:8px;padding:9px 10px;background:#edf4f6;color:#365161;font-size:12px}.sun-cloud-v2-status[data-kind="error"]{background:#fff0ee;color:#943d3d}.sun-cloud-v2-status[data-kind="ready"]{background:#eef8f2;color:#347154}.sun-cloud-v2-status[data-kind="offline"]{background:#fff7e6;color:#8b6b25}.sun-cloud-v2-note{font-size:12px;color:#68717a;margin:6px 0}.sun-cloud-v2-user{font-weight:800;color:#15364c}.sun-cloud-v2-code{font:700 13px/1.2 ui-monospace,monospace;background:#f2f4f5;border-radius:7px;padding:8px;word-break:break-all}.sun-cloud-v2-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.sun-cloud-v2-row label{flex:1;min-width:170px}.sun-cloud-v2-small{font-size:11px;color:#7b8388}
|
||
@media(max-width:760px){.sun-cloud-v2-grid{grid-template-columns:1fr}.sun-cloud-v2-grid .wide{grid-column:auto}}
|
||
`;document.head.appendChild(style);
|
||
}
|
||
|
||
function findCloudCard() {
|
||
const cards=[...document.querySelectorAll('#enterprise-settings .enterprise-card')];
|
||
return cards.find(card=>(card.querySelector('h2')?.textContent||'').trim()==='Облачная синхронизация') || $('sunCloudV2Card');
|
||
}
|
||
|
||
function ensureCloudCard() {
|
||
const card=findCloudCard();if(!card)return null;
|
||
if(card.id!=='sunCloudV2Card'){card.id='sunCloudV2Card';card.classList.add('wide');renderCloudUI();}
|
||
return card;
|
||
}
|
||
|
||
function renderCloudUI() {
|
||
const card=findCloudCard();if(!card)return;
|
||
card.id='sunCloudV2Card';card.classList.add('wide');
|
||
const connected=Boolean(config.url&&config.key);
|
||
const signed=Boolean(session?.user);
|
||
const selected=Boolean(workspace);
|
||
card.innerHTML=`<h2>Профиль и аккаунт</h2><p class="hint">Вход, выход, рабочая компания и пользователи компании.</p>
|
||
<div class="sun-cloud-v2-grid">
|
||
${connected&&!signed?`<div class="sun-cloud-v2-box wide"><h3>Войти в профиль</h3><div class="sun-cloud-v2-row"><label>Email<input id="sunCloudEmailV2" type="email" autocomplete="username"></label><label>Пароль<input id="sunCloudPasswordV2" type="password" autocomplete="current-password"></label></div><div class="sun-cloud-v2-actions"><button class="primary" id="sunCloudSignInV2" type="button">Войти</button><button class="outline" id="sunCloudSignUpV2" type="button">Зарегистрироваться</button></div></div>`:''}
|
||
${!connected?`<div class="sun-cloud-v2-box wide"><h3>Подключение недоступно</h3><p class="sun-cloud-v2-note">Техническое подключение настраивается только из кабинета разработчика.</p></div>`:''}
|
||
${signed?`<div class="sun-cloud-v2-box wide"><h3>Профиль</h3><div class="sun-cloud-v2-row"><span class="sun-cloud-v2-user">${esc(session.user.email||session.user.id)}</span><button class="outline" id="sunCloudSignOutV2" type="button">Выйти</button></div></div>`:''}
|
||
${signed?`<div class="sun-cloud-v2-box wide"><h3>Пароль</h3><div class="sun-cloud-v2-row"><label>Новый пароль<input id="sunAccountNewPasswordV1" type="password" autocomplete="new-password" minlength="8"></label><label>Подтвердите пароль<input id="sunAccountNewPassword2V1" type="password" autocomplete="new-password" minlength="8"></label></div><div class="sun-cloud-v2-actions"><button class="outline" id="sunAccountChangePasswordV1" type="button">Изменить пароль</button></div></div>`:''}
|
||
${signed&&!selected?`<div class="sun-cloud-v2-box wide"><h3>Рабочая компания</h3><p class="sun-cloud-v2-note">${membershipsLoading?'Загружаю доступные рабочие базы…':'У этого аккаунта пока нет доступной рабочей базы.'}</p><div class="sun-cloud-v2-actions"><button class="outline" id="sunCloudReloadMembershipsV2" type="button">Проверить ещё раз</button><button class="primary" id="sunCloudCreateWorkspaceV2" type="button">Создать новую компанию</button></div></div>`:''}
|
||
${signed&&selected?`<div class="sun-cloud-v2-box wide"><h3>Рабочая компания</h3><div><b>${esc(workspace.name)}</b>${supportMode?'<span class="sun-cloud-v2-note" style="margin-left:8px">· поддержка, только просмотр</span>':''}</div><div class="sun-cloud-v2-note">Роль: ${supportMode?'Разработчик · поддержка':esc(roleLabel(workspace.role))}${workspace.display_name?` · ${esc(workspace.display_name)}`:''}</div>${!supportMode&&memberships.length>1?`<label>Компания<select id="sunCloudWorkspaceSelectV2">${memberships.map(m=>`<option value="${esc(m.id)}" ${m.id===workspace.id?'selected':''}>${esc(m.name)} · ${esc(roleLabel(m.role))}</option>`).join('')}</select></label>`:''}</div>`:''}
|
||
${signed&&selected&&!supportMode?`<div class="sun-cloud-v2-box wide"><h3>Синхронизация</h3><p class="sun-cloud-v2-note">Принудительно загрузить актуальные заказы, клиентов и остальные данные с сервера, заменив локальную копию на этом устройстве. Перед загрузкой автоматически создаётся резервная копия.</p><div class="sun-cloud-v2-actions"><button class="outline" id="sunCloudPullV2" type="button">Загрузить из облака</button></div></div>`:''}
|
||
${signed&&selected&&!supportMode&&hasPermission('users.manage')?`<div class="sun-cloud-v2-box wide"><h3>Пользователи</h3><p class="sun-cloud-v2-note">Управление сотрудниками и правами внутри этой компании.</p><div class="sun-cloud-v2-actions"><button class="primary" id="sunCloudManageUsersV3" type="button">Пользователи и права</button></div></div>`:''}
|
||
</div><div class="sun-cloud-v2-status" id="sunCloudV2Status" data-kind="${esc(lastStatus)}">${esc(statusText())}</div>`;
|
||
bindCloudUI();
|
||
}
|
||
|
||
function bindCloudUI() {
|
||
$('sunCloudConnectV2')?.addEventListener('click', async()=>{
|
||
config.url=String($('sunCloudUrlV2')?.value||'').trim().replace(/\/$/,'');config.key=String($('sunCloudKeyV2')?.value||'').trim();saveConfig();
|
||
if(!config.url||!config.key)return toast('Введите Project URL и Publishable key.','warn');
|
||
client=null;session=null;workspace=null;memberships=[];await connectFromConfig(false);renderCloudUI();
|
||
});
|
||
$('sunCloudSignInV2')?.addEventListener('click',()=>signIn(false));
|
||
$('sunCloudSignUpV2')?.addEventListener('click',()=>signIn(true));
|
||
$('sunCloudSignOutV2')?.addEventListener('click',signOutAndReload);
|
||
$('sunAccountChangePasswordV1')?.addEventListener('click',async()=>{
|
||
const p1i=$('sunAccountNewPasswordV1'),p2i=$('sunAccountNewPassword2V1');
|
||
const p1=String(p1i?.value||''),p2=String(p2i?.value||'');
|
||
if(p1.length<8)return toast('Новый пароль должен содержать минимум 8 символов.','warn');
|
||
if(p1!==p2)return toast('Пароли не совпадают.','warn');
|
||
if(!client)return toast('Облачный сервис не подключён.','error');
|
||
try{const r=await client.auth.updateUser({password:p1});if(r.error)throw r.error;if(p1i)p1i.value='';if(p2i)p2i.value='';toast('Пароль изменён.','success');}
|
||
catch(e){toast(String(e?.message||e||'Не удалось изменить пароль.'),'error');}
|
||
});
|
||
$('sunCloudCreateWorkspaceV2')?.addEventListener('click',createWorkspace);
|
||
$('sunCloudReloadMembershipsV2')?.addEventListener('click',async()=>{const tenantSwitched=await loadMemberships();if(tenantSwitched){location.reload();return;}renderCloudUI();if(workspace){subscribeRealtime();await pullRemote({replace:true,quiet:true});}});
|
||
$('sunCloudMigrateV2')?.addEventListener('click',migrateLocalToCloud);
|
||
$('sunCloudSyncNowV2')?.addEventListener('click',()=>syncNow({quiet:false}));
|
||
$('sunCloudPullV2')?.addEventListener('click',async()=>{if(confirm('Заменить локальную рабочую копию актуальными данными из облака? Перед загрузкой будет создана резервная копия.'))await pullRemote({replace:true,quiet:false});});
|
||
$('sunCloudAutoV2')?.addEventListener('change',e=>{config.autoSync=e.target.checked;saveConfig();if(config.autoSync)scheduleSync('автосинхронизация включена');renderCloudUI();});
|
||
$('sunCloudWorkspaceSelectV2')?.addEventListener('change',async e=>{const next=String(e.target.value||'');config.workspaceId=next;saveConfig();const switched=await switchTenantLocal(next);if(switched.changed){location.reload();return;}selectWorkspace();subscribeRealtime();renderCloudUI();if(workspaceMigrated())await syncNow({quiet:true});else await pullRemote({replace:true,quiet:true});});
|
||
$('sunCloudManageUsersV3')?.addEventListener('click',()=>window.dispatchEvent(new CustomEvent('sun:open-cloud-users')));
|
||
}
|
||
|
||
async function loadSupabaseLibrary() {
|
||
if(window.supabase?.createClient)return;
|
||
const existing=$('sunSupabaseJsV2');
|
||
if(existing){for(let i=0;i<80&&!window.supabase?.createClient;i++)await sleep(100);if(window.supabase?.createClient)return;}
|
||
await new Promise((resolve,reject)=>{
|
||
const script=document.createElement('script');script.id='sunSupabaseJsV2';script.src=SUPABASE_JS;script.async=true;script.onload=resolve;script.onerror=()=>reject(new Error('Не удалось загрузить библиотеку Supabase. Проверьте интернет.'));document.head.appendChild(script);
|
||
});
|
||
if(!window.supabase?.createClient)throw new Error('Supabase SDK не загрузился.');
|
||
}
|
||
|
||
async function connectFromConfig(silent=true) {
|
||
if(!config.url||!config.key){setStatus('local','Облако не подключено.');return;}
|
||
setStatus('syncing','Подключаю Supabase…');
|
||
try{
|
||
await loadSupabaseLibrary();
|
||
const signature=`${config.url}|${config.key}`;
|
||
if(!client||signature!==clientSignature){
|
||
client=window.supabase.createClient(config.url,config.key,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true},global:{fetch:supabaseProxyFetch}});clientSignature=signature;
|
||
client.auth.onAuthStateChange((_event,next)=>{session=next;membershipsLoading=Boolean(next?.user);membershipsLoaded=!next?.user;setTimeout(async()=>{try{if(!next?.user){const sw=await switchTenantLocal('');if(sw.changed){location.reload();return;}}else{const sw=await loadMemberships();if(sw){location.reload();return;}}}finally{renderCloudUI();updatePill();}},0);});
|
||
}
|
||
const {data,error}=await client.auth.getSession();if(error)throw error;session=data.session||null;
|
||
const tenantSwitched=session?await loadMemberships():(await switchTenantLocal('')).changed;
|
||
if(tenantSwitched){location.reload();return;}
|
||
if(session&&workspace){
|
||
subscribeRealtime();
|
||
if(workspaceMigrated())await syncNow({quiet:true});
|
||
else {
|
||
const remote=await fetchRemoteRow();
|
||
if(remote&&!payloadEmpty(remote.payload)){
|
||
await backupBeforeCloud('Перед автоматическим подключением к существующей облачной базе');
|
||
await applyPayload(remote.payload,true);await setBaseline(remote);config.migrated[workspace.id]=true;config.lastSync=remote.updated_at||new Date().toISOString();saveConfig();dirty=false;setStatus('ready','Подключена существующая облачная база.');
|
||
} else setStatus('setup','Готово к первому переносу данных.');
|
||
}
|
||
} else if(session)setStatus('setup',membershipsLoading?'Загружаю рабочую базу…':'Для этого аккаунта рабочая база не найдена.');
|
||
else setStatus('auth','Подключено. Войдите в аккаунт.');
|
||
if(!silent)toast('Supabase подключён.','success');
|
||
}catch(error){handleError(error,'Не удалось подключиться к Supabase.');}
|
||
finally{renderCloudUI();}
|
||
}
|
||
|
||
async function signIn(signUp) {
|
||
const email=String($('sunCloudEmailV2')?.value||'').trim();const password=String($('sunCloudPasswordV2')?.value||'');
|
||
if(!email||password.length<6)return toast('Введите email и пароль не короче 6 символов.','warn');
|
||
setStatus('syncing',signUp?'Создаю аккаунт…':'Выполняю вход…');
|
||
try{
|
||
const result=signUp?await client.auth.signUp({email,password,options:{data:{registration_source:'caterium_public_signup'}}}):await client.auth.signInWithPassword({email,password});
|
||
if(result.error)throw result.error;session=result.data.session||null;
|
||
if(signUp&&!session){const retry=await client.auth.signInWithPassword({email,password});if(retry.error)throw retry.error;session=retry.data.session||null;}
|
||
{const tenantSwitched=await loadMemberships();if(tenantSwitched){location.reload();return;}setStatus('setup','Вход выполнен.');toast('Вход выполнен.','success');}
|
||
renderCloudUI();
|
||
}catch(error){handleError(error,signUp?'Не удалось создать аккаунт.':'Не удалось войти.');}
|
||
}
|
||
|
||
async function loadMemberships() {
|
||
memberships=[];workspace=null;membershipsLoaded=false;
|
||
if(!client||!session?.user)return false;
|
||
membershipsLoading=true;
|
||
try{
|
||
let rows=[];
|
||
const rpc=await sunCloudAwait(client.rpc('sun_my_workspaces'),'Загрузка рабочей базы');
|
||
if(!rpc.error&&Array.isArray(rpc.data)){
|
||
rows=rpc.data.map(x=>({workspace_id:x.id,name:x.name,role:x.role,display_name:x.display_name,is_active:x.is_active,permissions:x.permissions}));
|
||
}else{
|
||
// Compatibility fallback for an older server schema.
|
||
const direct=await sunCloudAwait(client.from('sun_workspace_members').select('workspace_id,role,display_name,is_active,permissions').eq('user_id',session.user.id),'Загрузка рабочей базы');
|
||
if(direct.error)throw rpc.error||direct.error;
|
||
const active=(direct.data||[]).filter(x=>x.is_active!==false);
|
||
if(active.length){
|
||
const ids=active.map(x=>x.workspace_id), names={};
|
||
const w=await sunCloudAwait(client.from('sun_workspaces').select('id,name').in('id',ids),'Загрузка рабочей базы');if(w.error)throw w.error;(w.data||[]).forEach(x=>names[x.id]=x.name);
|
||
rows=active.map(x=>({...x,name:names[x.workspace_id]||'Рабочая база'}));
|
||
}
|
||
}
|
||
memberships=rows.filter(x=>x.is_active!==false).map(x=>({id:x.workspace_id,name:x.name||'Рабочая база',role:x.role,display_name:x.display_name||'',is_active:x.is_active!==false,permissions:x.permissions||{}}));
|
||
if(memberships.length&&!memberships.some(x=>x.id===config.workspaceId))config.workspaceId=memberships[0].id;
|
||
if(!memberships.length)config.workspaceId='';
|
||
saveConfig();selectWorkspace();
|
||
const switched=await switchTenantLocal(workspace?.id||'');
|
||
membershipsLoaded=true;
|
||
if(switched.changed)return true;
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:clone(workspace),user:clone(session.user)}}));}catch(_){}
|
||
return false;
|
||
}catch(error){
|
||
membershipsLoaded=true;
|
||
setStatus('error',error?.message||'Не удалось загрузить рабочую базу.');
|
||
return false;
|
||
}finally{membershipsLoading=false;}
|
||
}
|
||
|
||
function selectWorkspace(){workspace=memberships.find(x=>x.id===config.workspaceId)||memberships[0]||null;if(workspace&&config.workspaceId!==workspace.id){config.workspaceId=workspace.id;saveConfig();}}
|
||
|
||
async function createTrialWorkspace(companyName='Новая компания'){
|
||
if(!client||!session?.user)throw new Error('Сначала войдите в аккаунт.');
|
||
const name=String(companyName||'').trim()||'Новая компания';
|
||
await loadMemberships();
|
||
if(workspace)return clone(workspace);
|
||
setStatus('syncing','Создаю компанию и включаю 14-дневный Полный тариф…');
|
||
const {data,error}=await client.rpc('sun_create_workspace',{p_name:name});if(error)throw error;
|
||
const newId=String(data||'');if(!newId)throw new Error('Сервер не вернул ID новой компании.');
|
||
await switchTenantLocal(newId);
|
||
seedOfficialCatalogForFreshWorkspace();
|
||
try{localStorage.setItem('sunCompanyProfileV1',JSON.stringify({name,createdAt:new Date().toISOString()}));}catch(_){}
|
||
await adoptCurrentLocalAsWorkspace(newId);
|
||
await loadMemberships();
|
||
if(!workspace||String(workspace.id)!==newId){config.workspaceId=newId;saveConfig();selectWorkspace();}
|
||
if(!workspace)throw new Error('Компания создана, но не появилась в списке доступных компаний.');
|
||
const remote=await fetchRemoteRow();
|
||
const localPayload=await collectLocalPayload();
|
||
const row=await upsertPayload(localPayload,remote?.revision??null);
|
||
await setBaseline(row);
|
||
config.migrated[newId]=true;config.lastSync=row?.updated_at||new Date().toISOString();config.autoSync=true;saveConfig();dirty=false;
|
||
try{await client.rpc('sun_v17_mirror_legacy',{p_workspace:newId,p_payload:localPayload,p_client_id:clientId()});}catch(_){}
|
||
subscribeRealtime();
|
||
setStatus('ready','Компания создана · Полный тариф бесплатно 14 дней.');
|
||
renderCloudUI();
|
||
return clone(workspace);
|
||
}
|
||
|
||
async function createWorkspace(){
|
||
const name=String($('sunCloudWorkspaceNameV2')?.value||'Новая компания').trim()||'Новая компания';
|
||
try{await createTrialWorkspace(name);toast('Компания создана: Полный тариф на 14 дней, каталог готов, заказы и клиенты пустые.','success',7000);}
|
||
catch(error){handleError(error,'Не удалось создать рабочую базу.');}
|
||
}
|
||
|
||
async function acceptInviteToken(rawToken){
|
||
const token=String(rawToken||'').trim();if(!token)throw new Error('Приглашение не найдено.');
|
||
if(!client||!session?.user)throw new Error('Сначала войдите в Caterium.');
|
||
setStatus('syncing','Подключаю компанию по приглашению…');
|
||
const {data,error}=await client.rpc('sun_accept_invite',{p_token:token});if(error)throw error;
|
||
config.workspaceId=String(data||'');saveConfig();
|
||
await loadMemberships();
|
||
subscribeRealtime();
|
||
const remote=await fetchRemoteRow();
|
||
if(remote&&!payloadEmpty(remote.payload)){
|
||
await backupBeforeCloud('Перед подключением к компании по приглашению');
|
||
await applyPayload(remote.payload,true);await setBaseline(remote);
|
||
config.migrated[workspace.id]=true;config.lastSync=remote.updated_at||new Date().toISOString();saveConfig();
|
||
}
|
||
setStatus('ready','Вы присоединились к компании.');renderCloudUI();
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:clone(workspace)}}));}catch(_){}
|
||
return clone(workspace);
|
||
}
|
||
|
||
async function joinWorkspace(){
|
||
const token=String($('sunCloudInviteCodeV2')?.value||'').trim();if(!token)return toast('Введите код приглашения.','warn');
|
||
try{await acceptInviteToken(token);toast('Вы присоединились к компании.','success',6000);}
|
||
catch(error){handleError(error,'Не удалось принять приглашение. Возможно, ссылка истекла или уже использована.');}
|
||
}
|
||
|
||
async function createInvite(){
|
||
const role=String($('sunCloudInviteRoleV2')?.value||'manager');
|
||
try{const {data,error}=await client.rpc('sun_create_invite',{p_workspace:workspace.id,p_role:role});if(error)throw error;const token=String(data||'');const host=$('sunCloudInviteResultV2');if(host)host.innerHTML=`<p class="sun-cloud-v2-note">Передайте сотруднику этот одноразовый код (действует 7 дней):</p><div class="sun-cloud-v2-code">${esc(token)}</div><div class="sun-cloud-v2-actions"><button class="outline" id="sunCloudCopyInviteV2" type="button">Скопировать код</button></div>`;$('sunCloudCopyInviteV2')?.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(token);toast('Код скопирован.','success');}catch(_){toast('Скопируйте код вручную.','warn');}});}
|
||
catch(error){handleError(error,'Не удалось создать приглашение.');}
|
||
}
|
||
|
||
function unsubscribeRealtime(){if(realtimeChannel&&client){try{client.removeChannel(realtimeChannel);}catch(_){}}realtimeChannel=null;}
|
||
function subscribeRealtime(){
|
||
unsubscribeRealtime();if(!client||!session||!workspace?.id)return;
|
||
realtimeChannel=client.channel(`sun-state-${workspace.id}-${clientId()}`).on('postgres_changes',{event:'INSERT',schema:'public',table:'sun_sync_events',filter:`workspace_id=eq.${workspace.id}`},async payload=>{
|
||
if(payload?.new?.client_id===clientId())return;
|
||
try{const row=await fetchRemoteRow();if(row)await handleRealtimeRow(row);}catch(error){handleError(error,'Не удалось получить изменения с другого устройства.');}
|
||
}).subscribe(status=>{if(status==='CHANNEL_ERROR')setStatus('error','Realtime не подключился. Обычная синхронизация продолжит работать.');});
|
||
}
|
||
|
||
function handleError(error, message) {
|
||
console.error('[SunCloudV2]',error);const detail=String(error?.message||error||'Неизвестная ошибка');setStatus('error',`${message} ${detail}`);toast(`${message} ${detail}`,'error',7500);renderCloudUI();
|
||
}
|
||
|
||
function observeSettings() {
|
||
const settingsRoot=$('enterprise-settings');if(settingsRoot){const observer=new MutationObserver(()=>{if(findCloudCard()&&!$('sunCloudV2Card'))ensureCloudCard();});observer.observe(settingsRoot,{childList:true,subtree:true});}
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const name=String(b.dataset.navLabel||b.textContent||'').trim();if(name==='Настройки')setTimeout(()=>{ensureCloudCard();renderCloudUI();},80);},true);
|
||
setTimeout(()=>{ensureCloudCard();renderCloudUI();},250);
|
||
}
|
||
|
||
function hookEnterpriseSchedule() {
|
||
const timer=setInterval(()=>{if(window.SunEnterprise){window.SunEnterprise.scheduleCloudPush=()=>scheduleSync('сохранение приложения');clearInterval(timer);}},250);
|
||
setTimeout(()=>clearInterval(timer),10000);
|
||
}
|
||
|
||
window.addEventListener('online',()=>{setStatus('pending','Интернет восстановлен. Синхронизирую…');if(workspaceMigrated())syncNow({quiet:true});});
|
||
window.addEventListener('offline',()=>setStatus('offline','Офлайн — изменения сохраняются на этом устройстве.'));
|
||
|
||
|
||
async function enterSupportWorkspace(workspaceId,name='Компания') {
|
||
const id=String(workspaceId||'').trim();if(!id)throw new Error('Не выбрана компания.');
|
||
if(!client||!session?.user)throw new Error('Сначала войдите в аккаунт разработчика.');
|
||
const admin=await sunCloudAwait(client.rpc('sun_is_platform_admin'),'Проверка доступа разработчика',8000);if(admin.error)throw admin.error;if(admin.data!==true)throw new Error('Требуется аккаунт разработчика.');
|
||
const previousWorkspaceId=String(config.workspaceId||'');
|
||
const previousLocalWorkspaceId=String(config.localWorkspaceId||workspace?.id||'');
|
||
if(previousLocalWorkspaceId)await captureTenantLocal(previousLocalWorkspaceId);
|
||
clearTenantLocal();config.localWorkspaceId='';saveConfig();unsubscribeRealtime();
|
||
const result=await sunCloudAwait(client.rpc('sun_dev_support_snapshot',{p_workspace:id}),'Загрузка компании в режиме поддержки',12000);if(result.error)throw result.error;
|
||
const row=Array.isArray(result.data)?result.data[0]:result.data;if(!row?.payload)throw new Error('У компании нет облачной рабочей копии.');
|
||
await applyPayload(row.payload,true);
|
||
supportMode={workspaceId:id,name:String(name||'Компания'),previousWorkspaceId,previousLocalWorkspaceId,enteredAt:new Date().toISOString()};
|
||
workspace={id,name:String(name||'Компания'),role:'platform_support',display_name:'Разработчик · поддержка',is_active:true,permissions:{}};
|
||
dirty=false;setStatus('ready',`Режим поддержки · ${name||'Компания'} · только просмотр`);
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:clone(workspace),support:true}}));window.dispatchEvent(new CustomEvent('sun:cloud-state-applied',{detail:{workspaceId:id,support:true}}));}catch(_){}
|
||
renderCloudUI();return clone(supportMode);
|
||
}
|
||
|
||
async function refreshSupportWorkspace(){
|
||
if(!supportMode||!client||!session?.user)return false;
|
||
const id=String(supportMode.workspaceId||workspace?.id||'').trim();if(!id)return false;
|
||
const result=await sunCloudAwait(client.rpc('sun_dev_support_snapshot',{p_workspace:id}),'Обновление данных компании',12000);if(result.error)throw result.error;
|
||
const row=Array.isArray(result.data)?result.data[0]:result.data;if(!row?.payload)throw new Error('У компании нет облачной рабочей копии.');
|
||
await applyPayload(row.payload,true);dirty=false;config.lastSync=new Date().toISOString();saveConfig();
|
||
setStatus('ready','Режим поддержки · '+(supportMode.name||'Компания')+' · только просмотр');
|
||
try{window.dispatchEvent(new CustomEvent('sun:cloud-state-applied',{detail:{workspaceId:id,support:true,refresh:true}}));}catch(_){}
|
||
renderCloudUI();return true;
|
||
}
|
||
|
||
async function exitSupportWorkspace() {
|
||
if(!supportMode)return false;
|
||
clearTenantLocal();supportMode=null;workspace=null;config.localWorkspaceId='';saveConfig();
|
||
const switched=await loadMemberships();renderCloudUI();
|
||
if(switched||workspace){location.reload();return true;}
|
||
return true;
|
||
}
|
||
|
||
async function setTechnicalConnection(url,key) {
|
||
const admin=client&&session?.user?await client.rpc('sun_is_platform_admin'):null;
|
||
if(!admin||admin.error||admin.data!==true)throw new Error('Техническое подключение доступно только разработчику.');
|
||
config.url=String(url||'').trim().replace(/\/$/,'');config.key=String(key||'').trim();saveConfig();
|
||
if(!config.url||!config.key)throw new Error('Введите Project URL и Publishable key.');
|
||
client=null;clientSignature='';session=null;workspace=null;memberships=[];supportMode=null;
|
||
await connectFromConfig(false);return true;
|
||
}
|
||
|
||
function setAutoSyncDeveloper(enabled){config.autoSync=enabled!==false;saveConfig();if(config.autoSync&&workspace&&!supportMode)scheduleSync('автосинхронизация включена');renderCloudUI();return config.autoSync;}
|
||
window.SunCloudV2={
|
||
VERSION,
|
||
signOut:signOutAndReload,
|
||
adoptCurrentLocalAsWorkspace,
|
||
createTrialWorkspace,
|
||
acceptInvite:acceptInviteToken,
|
||
syncNow:()=>syncNow({quiet:false}),
|
||
pull:()=>pullRemote({replace:true,quiet:false}),
|
||
migrate:migrateLocalToCloud,
|
||
hasPermission,
|
||
roleLabel,
|
||
getClient:()=>client,
|
||
getSession:()=>session?clone(session):null,
|
||
getWorkspace:()=>workspace?clone(workspace):null,
|
||
getClientId:()=>clientId(),
|
||
enterSupportWorkspace,
|
||
refreshSupportWorkspace,
|
||
exitSupportWorkspace,
|
||
isSupportMode:()=>Boolean(supportMode),
|
||
getSupportMode:()=>supportMode?clone(supportMode):null,
|
||
getTechnicalConfig:()=>({url:config.url,key:config.key,autoSync:config.autoSync,lastSync:config.lastSync,workspaceId:config.workspaceId,localWorkspaceId:config.localWorkspaceId}),
|
||
setTechnicalConnection,
|
||
setAutoSyncDeveloper,
|
||
snapshotLocal:()=>collectLocalPayload(),
|
||
reloadMemberships:async()=>{await loadMemberships();renderCloudUI();return workspace?clone(workspace):null;},
|
||
status:()=>({connected:Boolean(client),signedIn:Boolean(session),supportMode:supportMode?clone(supportMode):null,workspace:workspace?clone(workspace):null,membershipsLoading,membershipsLoaded,membershipCount:memberships.length,dirty,lastStatus,lastError})
|
||
};
|
||
|
||
async function boot(){
|
||
injectStyles();disableLegacyCloud();installStorageTracking();hookEnterpriseSchedule();observeSettings();updatePill();
|
||
if(config.url&&config.key)await connectFromConfig(true);else setStatus('local','Облако не подключено. Откройте Настройки → Облако Supabase.');
|
||
}
|
||
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: admin-rbac-v3.js ===== */
|
||
(() => {
|
||
'use strict';
|
||
if (window.SunAdminRBACV3) return;
|
||
|
||
const VERSION = '2026.09.06-admin-rbac-v7-granular-admins';
|
||
const $ = id => document.getElementById(id);
|
||
const qa = (s,r=document) => [...r.querySelectorAll(s)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const toast = (text,type='info',timeout=4500) => {
|
||
try { if (window.SunEnterprise?.toast) return window.SunEnterprise.toast(text,type,timeout); } catch (_) {}
|
||
alert(text);
|
||
};
|
||
|
||
const ROLE_LABELS = {admin:'Администратор',manager:'Менеджер',kitchen:'Кухня',courier:'Курьер',viewer:'Только просмотр'};
|
||
const GROUPS = [
|
||
{title:'Заказы', items:[['orders.view','Просмотр заказов'],['orders.create','Создание заказов'],['orders.edit','Изменение заказов'],['orders.delete','Удаление заказов'],['calendar.view','Календарь'],['map.view','Карта']]},
|
||
{title:'Клиенты и каталог', items:[['clients.view','Просмотр клиентов'],['clients.edit','Изменение клиентов'],['catalog.view','Просмотр каталога'],['catalog.edit','Изменение каталога']]},
|
||
{title:'Производство и доставка', items:[['production.view','Просмотр производства'],['production.edit','Изменение производства'],['shopping.view','Просмотр закупок'],['shopping.edit','Изменение закупок'],['stock.view','Просмотр склада'],['stock.edit','Изменение склада'],['routes.view','Просмотр маршрутов'],['routes.edit','Изменение маршрутов']]},
|
||
{title:'Управление', items:[['mailings.view','Просмотр рассылок'],['mailings.edit','Изменение рассылок'],['money.view','Просмотр денег'],['money.edit','Изменение денег'],['stats.view','Статистика'],['team.view','Просмотр команды'],['team.edit','Изменение команды'],['suppliers.view','Просмотр поставщиков'],['suppliers.edit','Изменение поставщиков'],['print.view','Печать']]},
|
||
{title:'Администрирование', items:[['settings.view','Просмотр настроек'],['settings.edit','Изменение настроек'],['users.manage','Пользователи и права'],['audit.view','История изменений'],['backups.manage','Резервные копии']]}
|
||
];
|
||
|
||
const PRESETS = {
|
||
admin: Object.fromEntries(GROUPS.flatMap(g=>g.items).map(([k])=>[k,true])),
|
||
manager: {
|
||
'orders.view':true,'orders.create':true,'orders.edit':true,'orders.delete':false,'calendar.view':true,'map.view':true,
|
||
'clients.view':true,'clients.edit':true,'catalog.view':true,'catalog.edit':false,
|
||
'production.view':false,'production.edit':false,'shopping.view':false,'shopping.edit':false,'stock.view':false,'stock.edit':false,'routes.view':true,'routes.edit':false,
|
||
'mailings.view':true,'mailings.edit':true,'money.view':false,'money.edit':false,'stats.view':true,'team.view':true,'team.edit':false,'suppliers.view':false,'suppliers.edit':false,'print.view':true,
|
||
'settings.view':false,'settings.edit':false,'users.manage':false,'audit.view':false,'backups.manage':false
|
||
},
|
||
kitchen: {
|
||
'orders.view':true,'orders.create':false,'orders.edit':false,'orders.delete':false,'calendar.view':true,'map.view':false,
|
||
'clients.view':false,'clients.edit':false,'catalog.view':true,'catalog.edit':false,
|
||
'production.view':true,'production.edit':true,'shopping.view':true,'shopping.edit':false,'stock.view':true,'stock.edit':true,'routes.view':false,'routes.edit':false,
|
||
'mailings.view':false,'mailings.edit':false,'money.view':false,'money.edit':false,'stats.view':false,'team.view':true,'team.edit':false,'suppliers.view':false,'suppliers.edit':false,'print.view':true,
|
||
'settings.view':false,'settings.edit':false,'users.manage':false,'audit.view':false,'backups.manage':false
|
||
},
|
||
courier: {
|
||
'orders.view':true,'orders.create':false,'orders.edit':false,'orders.delete':false,'calendar.view':true,'map.view':true,
|
||
'clients.view':false,'clients.edit':false,'catalog.view':false,'catalog.edit':false,
|
||
'production.view':false,'production.edit':false,'shopping.view':false,'shopping.edit':false,'stock.view':false,'stock.edit':false,'routes.view':true,'routes.edit':true,
|
||
'mailings.view':false,'mailings.edit':false,'money.view':false,'money.edit':false,'stats.view':false,'team.view':false,'team.edit':false,'suppliers.view':false,'suppliers.edit':false,'print.view':false,
|
||
'settings.view':false,'settings.edit':false,'users.manage':false,'audit.view':false,'backups.manage':false
|
||
},
|
||
viewer: {
|
||
'orders.view':true,'orders.create':false,'orders.edit':false,'orders.delete':false,'calendar.view':true,'map.view':true,
|
||
'clients.view':true,'clients.edit':false,'catalog.view':true,'catalog.edit':false,
|
||
'production.view':false,'production.edit':false,'shopping.view':false,'shopping.edit':false,'stock.view':false,'stock.edit':false,'routes.view':false,'routes.edit':false,
|
||
'mailings.view':false,'mailings.edit':false,'money.view':false,'money.edit':false,'stats.view':true,'team.view':false,'team.edit':false,'suppliers.view':false,'suppliers.edit':false,'print.view':true,
|
||
'settings.view':false,'settings.edit':false,'users.manage':false,'audit.view':false,'backups.manage':false
|
||
}
|
||
};
|
||
|
||
let members = [];
|
||
let editing = null;
|
||
let inviting = false;
|
||
let applying = false;
|
||
let lastWorkspaceId = '';
|
||
|
||
function cloud(){ return window.SunCloudV2 || null; }
|
||
function workspace(){ return cloud()?.getWorkspace?.() || null; }
|
||
function session(){ return cloud()?.getSession?.() || null; }
|
||
function client(){ return cloud()?.getClient?.() || null; }
|
||
function has(p){ const c=cloud(); return c?.hasPermission ? c.hasPermission(p) : false; }
|
||
function currentPermissions(){ return workspace()?.permissions || {}; }
|
||
|
||
function normalizeLegacyLocalAuth(){
|
||
try{
|
||
const raw=localStorage.getItem('sunEnterpriseSettingsV1');
|
||
if(!raw)return;
|
||
const s=JSON.parse(raw)||{};
|
||
if(s.rolesEnabled){s.rolesEnabled=false;localStorage.setItem('sunEnterpriseSettingsV1',JSON.stringify(s));}
|
||
}catch(_){}
|
||
}
|
||
|
||
function installStyle(){
|
||
if($('sun-admin-rbac-style'))return;
|
||
const style=document.createElement('style');style.id='sun-admin-rbac-style';style.textContent=`
|
||
#sun-cloud-users-modal .dialog{max-width:980px;width:min(980px,calc(100vw - 24px));max-height:92vh;overflow:auto}
|
||
.sun-rbac-toolbar{display:flex;gap:8px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:8px 0 12px}
|
||
.sun-rbac-members{display:grid;gap:8px}.sun-rbac-member{display:grid;grid-template-columns:minmax(220px,1.4fr) minmax(150px,.7fr) auto;gap:10px;align-items:center;border:1px solid #dfe5e8;border-radius:10px;padding:10px 12px;background:#fff}
|
||
.sun-rbac-member small{display:block;color:#75808a;margin-top:3px}.sun-rbac-off{opacity:.58}.sun-rbac-perm-groups{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:12px}.sun-rbac-group{border:1px solid #dfe5e8;border-radius:10px;padding:10px;background:#fafcfc}.sun-rbac-group h4{margin:0 0 8px;color:#17384d}.sun-rbac-check{display:flex!important;flex-direction:row!important;align-items:center!important;gap:8px!important;margin:6px 0!important}.sun-rbac-check input{width:17px!important;height:17px!important;flex:0 0 auto}.sun-rbac-role-line{display:grid;grid-template-columns:1.2fr 1fr auto;gap:10px;align-items:end}.sun-rbac-danger{margin-left:auto}.sun-rbac-section-title{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:18px 0 8px}.sun-rbac-section-title h3{margin:0;color:#17384d;font-size:15px}.sun-rbac-invite{display:grid;grid-template-columns:minmax(190px,1.2fr) minmax(130px,.65fr) auto;gap:10px;align-items:center;border:1px dashed #cfd9df;border-radius:10px;padding:10px 12px;background:#fbfcfc}.sun-rbac-invite small{display:block;color:#75808a;margin-top:3px}.sun-rbac-invite-form{border:1px solid #dfe5e8;border-radius:12px;padding:14px;background:#fbfcfc}.sun-rbac-invite-grid{display:grid;grid-template-columns:1.2fr 1.5fr 1fr;gap:10px}.sun-rbac-linkbox{margin-top:12px;padding:10px;border-radius:9px;background:#f1f5f6;word-break:break-all;font:600 12px/1.45 ui-monospace,monospace}.sun-rbac-lock-note{font-size:12px;color:#6e7880;margin-top:6px}.sun-cloud-identity-v3{font-size:12px;color:#adc0cc;padding:3px 7px}.sun-cloud-pin-card{border-top:1px solid #e2e7e9;margin-top:10px;padding-top:10px}
|
||
body.sun-cloud-auth-required .view,body.sun-cloud-auth-required header{filter:blur(2px);pointer-events:none;user-select:none}.sun-cloud-auth-gate{position:fixed;inset:0;z-index:10000;background:#132f44eb;display:grid;place-items:center;padding:20px}.sun-cloud-auth-card{width:min(430px,100%);background:#fff;border-radius:16px;padding:22px;box-shadow:0 24px 80px #0005}.sun-cloud-auth-brand{display:flex;align-items:center;gap:12px;margin-bottom:16px}.sun-cloud-auth-brand img{width:58px;height:58px;object-fit:contain}.sun-cloud-auth-card h2{margin:0;color:#17384d}.sun-cloud-auth-card label{display:block;margin:10px 0}.sun-cloud-auth-card input{width:100%}.sun-auth-password{position:relative}.sun-auth-password input{padding-right:46px}.sun-auth-eye{position:absolute;right:7px;bottom:6px;width:34px;height:34px;border:0;background:transparent;border-radius:8px;cursor:pointer;font-size:17px;color:#536674}.sun-auth-eye:hover{background:#eef2f3}.sun-auth-switch{margin:14px 0 0;text-align:center;font-size:13px}.sun-auth-switch button{border:0;background:transparent;color:#245c7c;font-weight:800;cursor:pointer;text-decoration:underline;text-underline-offset:3px}.sun-cloud-auth-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.sun-cloud-auth-error{color:#9a3838;font-size:12px;margin-top:8px;min-height:18px}.sun-auth-mode-tabs{display:grid;grid-template-columns:1fr 1fr;gap:6px;background:#f2f4f5;border-radius:11px;padding:4px;margin:10px 0 14px}.sun-auth-mode-tabs button{border:0;border-radius:8px;padding:9px 10px;background:transparent;font-weight:800;cursor:pointer}.sun-auth-mode-tabs button.on{background:#fff;box-shadow:0 2px 10px #0001;color:#17384d}.sun-auth-trial{background:#eef8f2;color:#286447;border-radius:10px;padding:10px 11px;font-size:12px;font-weight:700;margin:10px 0}.sun-auth-success{background:#eef8f2;color:#286447;border-radius:10px;padding:10px 11px;font-size:12px;margin-top:10px}
|
||
@media(max-width:760px){.sun-rbac-perm-groups{grid-template-columns:1fr}.sun-rbac-member,.sun-rbac-invite{grid-template-columns:1fr}.sun-rbac-role-line,.sun-rbac-invite-grid{grid-template-columns:1fr}}
|
||
`;document.head.appendChild(style);
|
||
}
|
||
|
||
function navPermission(label){
|
||
const map={
|
||
'Главная':'app.read','Новый заказ':'orders.create','Заказы':'orders.view','Календарь':'calendar.view','Карта':'map.view',
|
||
'Производство':'production.view','Закупки':'shopping.view','Склад':'stock.view','Клиенты':'clients.view','Рассылки':'mailings.view',
|
||
'Деньги':'money.view','Команда':'team.view','Поставщики':'suppliers.view','Статистика':'stats.view','Печать':'print.view','Маршруты':'routes.view','Настройки':'settings.view'
|
||
};
|
||
return map[label] || null;
|
||
}
|
||
|
||
function canSeeSettings(){return has('settings.view')||has('users.manage')||has('backups.manage')||has('audit.view');}
|
||
|
||
function applyNavPermissions(){
|
||
const ws=workspace(), ss=session();
|
||
if(!ws||!ss?.user)return;
|
||
qa('header nav button').forEach(btn=>{
|
||
const label=String(btn.dataset.navLabel||btn.textContent||'').trim();
|
||
let allowed=true;
|
||
if(label==='Настройки')allowed=canSeeSettings();
|
||
else {const perm=navPermission(label);if(perm)allowed=has(perm);}
|
||
btn.hidden=!allowed;btn.style.display=allowed?'':'none';
|
||
});
|
||
const label=$('sunCurrentUserLabel');
|
||
if(label)label.textContent=`${ws.display_name||ss.user.email||'Пользователь'} · ${ROLE_LABELS[ws.role]||ws.role}`;
|
||
const logout=$('sunLogoutBtn');
|
||
if(logout){logout.style.display='flex';logout.onclick=async()=>{if(window.SunCloudV2?.signOut)return window.SunCloudV2.signOut();try{await client()?.auth.signOut();}catch(_){}location.reload();};}
|
||
applyActionPermissions();
|
||
purgeSensitiveLocalData();
|
||
}
|
||
|
||
function applyActionPermissions(){
|
||
const del=$('deleteOrderBtn');if(del)del.style.display=(del.style.display==='none'&&!draftIdSafe())?'none':(has('orders.delete')?'inline-block':'none');
|
||
qa('#orders [data-sun-delete-order],#orders button').forEach(b=>{if(/удал/i.test(b.textContent||''))b.style.display=has('orders.delete')?'':'none';});
|
||
const save=qa('#new button').find(b=>(b.textContent||'').trim()==='Сохранить');if(save){const existing=draftIdSafe();save.disabled=existing?!has('orders.edit'):!has('orders.create');save.title=save.disabled?'Недостаточно прав':'';}
|
||
qa('#new .catalog-head button').forEach(b=>{if((b.textContent||'').trim()==='Настроить')b.style.display=has('catalog.edit')?'':'none';});
|
||
qa('#manager .primary,#editor .primary,#editor .danger').forEach(b=>{b.disabled=!has('catalog.edit');});
|
||
}
|
||
function draftIdSafe(){try{return Boolean(window.draft?.id||draft?.id);}catch(_){return false;}}
|
||
|
||
function purgeSensitiveLocalData(){
|
||
const rules=[
|
||
['sunFinanceRecordsV2',()=>has('money.view')||has('money.edit')],
|
||
['sunAuditLogV1',()=>has('audit.view')],
|
||
['sunSuppliers',()=>has('suppliers.view')||has('suppliers.edit')||has('shopping.view')],
|
||
['sunStockMoves',()=>has('stock.view')||has('stock.edit')||has('shopping.view')||has('production.view')],
|
||
['sunStock',()=>has('stock.view')||has('stock.edit')||has('shopping.view')||has('production.view')],
|
||
['sunEmployees',()=>has('team.view')||has('team.edit')||has('production.view')||has('routes.view')],
|
||
['sunClientCommunicationV1',()=>has('clients.view')||has('clients.edit')||has('orders.create')||has('orders.edit')],
|
||
['sunClientLoyaltyV1',()=>has('clients.view')||has('clients.edit')||has('orders.create')||has('orders.edit')]
|
||
];
|
||
for(const [key,ok] of rules){if(!ok())try{localStorage.removeItem(key);}catch(_){}}
|
||
}
|
||
|
||
function guardGlobal(name, permission, mode){
|
||
const fn=window[name];if(typeof fn!=='function'||fn.__sunRbacGuard)return;
|
||
const wrapped=function(...args){
|
||
let perm=permission;
|
||
if(typeof permission==='function')perm=permission(...args);
|
||
if(perm&&!has(perm)){toast('У вас нет прав для этого действия.','warn');return;}
|
||
return fn.apply(this,args);
|
||
};
|
||
wrapped.__sunRbacGuard=true;wrapped.__sunOriginal=fn;window[name]=wrapped;
|
||
}
|
||
|
||
function installGuards(){
|
||
guardGlobal('saveOrder',()=>draftIdSafe()?'orders.edit':'orders.create');
|
||
guardGlobal('deleteOrder','orders.delete');
|
||
guardGlobal('deleteOrderById','orders.delete');
|
||
guardGlobal('saveBox','catalog.edit');
|
||
guardGlobal('removeBox','catalog.edit');
|
||
guardGlobal('editBox','catalog.edit');
|
||
guardGlobal('writeOffOrderStock','stock.edit');
|
||
guardGlobal('sunEditSupplier','suppliers.edit');
|
||
guardGlobal('editStockProduct','stock.edit');
|
||
}
|
||
|
||
function hideLegacyRoleCard(){
|
||
const view=$('enterprise-settings');if(!view)return;
|
||
qa('section',view).filter(s=>(s.querySelector(':scope > h2')?.textContent||'').trim()==='Пользователи и роли').forEach(s=>s.remove());
|
||
if(!has('backups.manage'))qa('section',view).filter(s=>(s.querySelector(':scope > h2')?.textContent||'').includes('Резервное копирование')).forEach(s=>s.style.display='none');
|
||
if(!has('audit.view'))qa('section',view).filter(s=>(s.querySelector(':scope > h2')?.textContent||'').includes('История изменений')).forEach(s=>s.style.display='none');
|
||
}
|
||
|
||
function ensureModal(){
|
||
if($('sun-cloud-users-modal'))return;
|
||
const modal=document.createElement('div');modal.id='sun-cloud-users-modal';modal.className='modal';modal.innerHTML=`<div class="dialog"><div class="dialog-head"><h2>Пользователи и права</h2><button type="button" data-rbac-close>×</button></div><div id="sunRbacBody"></div></div>`;document.body.appendChild(modal);
|
||
modal.addEventListener('click',e=>{if(e.target===modal||e.target.closest('[data-rbac-close]'))closeModal();});
|
||
}
|
||
function openModal(){ensureModal();$('sun-cloud-users-modal').classList.add('on');loadMembers();}
|
||
function closeModal(){$('sun-cloud-users-modal')?.classList.remove('on');editing=null;inviting=false;}
|
||
|
||
async function loadMembers(){
|
||
const c=client(),ws=workspace(),body=$('sunRbacBody');if(!c||!ws||!body)return;
|
||
body.innerHTML='<p class="hint">Загружаю сотрудников…</p>';
|
||
try{
|
||
const memberRes=await c.rpc('sun_list_workspace_members',{p_workspace:ws.id});
|
||
if(memberRes.error)throw memberRes.error;
|
||
members=memberRes.data||[];renderMembers();
|
||
}catch(e){body.innerHTML=`<p class="error">${esc(e.message||e)}</p>`;}
|
||
}
|
||
|
||
async function copyText(value,success='Данные скопированы.'){
|
||
try{await navigator.clipboard.writeText(String(value||''));toast(success,'success');}
|
||
catch(_){prompt('Скопируйте данные:',String(value||''));}
|
||
}
|
||
|
||
function renderMembers(){
|
||
const body=$('sunRbacBody');if(!body)return;
|
||
if(editing){renderEditor();return;}if(inviting){renderEmployeeForm();return;}
|
||
const active=members.map(m=>`<div class="sun-rbac-member ${m.is_active?'':'sun-rbac-off'}"><div><b>${esc(m.display_name||m.email||'Сотрудник')}</b><small>${esc(m.email||'')} · ${m.is_active?'активен':'доступ отключён'}</small></div><span class="role-chip">${esc(ROLE_LABELS[m.role]||m.role)}</span><button class="outline" type="button" data-rbac-edit="${esc(m.user_id)}">Права</button></div>`).join('')||'<p class="hint">Сотрудников пока нет.</p>';
|
||
body.innerHTML=`<div class="sun-rbac-toolbar"><div><b>${esc(workspace()?.name||'Рабочая база')}</b><div class="hint">Создайте сотрудника здесь. Новый сотрудник получит временный пароль для первого входа.</div></div><div class="actions"><button class="primary" type="button" id="sunRbacAddUser">+ Добавить сотрудника</button><button class="outline" type="button" id="sunRbacReload">Обновить</button></div></div><div class="sun-rbac-section-title"><h3>Сотрудники</h3><span class="hint">${members.filter(m=>m.is_active).length}</span></div><div class="sun-rbac-members">${active}</div>`;
|
||
$('sunRbacAddUser').onclick=()=>{inviting=true;renderEmployeeForm();};$('sunRbacReload').onclick=loadMembers;
|
||
qa('[data-rbac-edit]',body).forEach(b=>b.onclick=()=>{editing=members.find(m=>String(m.user_id)===String(b.dataset.rbacEdit));renderEditor();});
|
||
}
|
||
|
||
function renderEmployeeForm(){
|
||
const body=$('sunRbacBody');if(!body)return;
|
||
body.innerHTML=`<div class="sun-rbac-toolbar"><button class="outline" type="button" id="sunRbacEmployeeBack">← Сотрудники</button><div class="hint">Сотруднику не нужна отдельная ссылка-приглашение.</div></div><div class="sun-rbac-invite-form"><h3 style="margin:0 0 12px;color:#17384d">Добавить сотрудника</h3><div class="sun-rbac-invite-grid"><label>Имя<input id="sunRbacEmployeeName" autocomplete="name" placeholder="Например, Анна"></label><label>Email<input id="sunRbacEmployeeEmail" type="email" autocomplete="email" placeholder="name@example.com"></label><label>Роль<select id="sunRbacEmployeeRole">${Object.entries(ROLE_LABELS).map(([k,v])=>`<option value="${k}" ${k==='manager'?'selected':''}>${esc(v)}</option>`).join('')}</select></label></div><p class="hint">Если аккаунта ещё нет, Caterium создаст его и покажет временный пароль. Если аккаунт уже существует, он просто будет добавлен в эту компанию.</p><div class="actions"><button class="primary" type="button" id="sunRbacCreateEmployee">Создать сотрудника</button><button class="outline" type="button" id="sunRbacEmployeeCancel">Отмена</button></div><div id="sunRbacEmployeeResult"></div></div>`;
|
||
const back=()=>{inviting=false;renderMembers();};$('sunRbacEmployeeBack').onclick=back;$('sunRbacEmployeeCancel').onclick=back;$('sunRbacCreateEmployee').onclick=createEmployeeV28;
|
||
}
|
||
|
||
async function createEmployeeV28(){
|
||
const c=client(),ws=workspace(),btn=$('sunRbacCreateEmployee');if(!c||!ws||!btn)return;
|
||
const name=String($('sunRbacEmployeeName')?.value||'').trim(),email=String($('sunRbacEmployeeEmail')?.value||'').trim(),role=String($('sunRbacEmployeeRole')?.value||'manager');
|
||
if(!name)return toast('Введите имя сотрудника.','warn');
|
||
if(!email)return toast('Введите email сотрудника.','warn');
|
||
btn.disabled=true;btn.textContent='Создаю…';
|
||
try{
|
||
const prep=await c.rpc('sun_employee_prepare_v28',{p_workspace:ws.id,p_email:email,p_display_name:name,p_role:role});if(prep.error)throw prep.error;
|
||
let result=prep.data||{};
|
||
if(result.status==='already_member'){
|
||
toast('Этот пользователь уже добавлен в компанию.','warn');btn.disabled=false;btn.textContent='Создать сотрудника';return;
|
||
}
|
||
if(result.status==='existing'){
|
||
const fin=await c.rpc('sun_employee_finalize_v28',{p_workspace:ws.id,p_user_id:result.user_id,p_display_name:name,p_role:role});if(fin.error)throw fin.error;
|
||
result={...(fin.data||{}),created:false};
|
||
}else{
|
||
const call=await c.functions.invoke('caterium-create-employee',{body:{workspace_id:ws.id,email,display_name:name,role}});if(call.error)throw call.error;
|
||
if(call.data?.error)throw new Error(call.data.error);
|
||
result=call.data||{};
|
||
if(result.status==='existing'&&result.user_id){
|
||
const fin=await c.rpc('sun_employee_finalize_v28',{p_workspace:ws.id,p_user_id:result.user_id,p_display_name:name,p_role:role});if(fin.error)throw fin.error;
|
||
result={...(fin.data||{}),created:false};
|
||
}
|
||
}
|
||
const host=$('sunRbacEmployeeResult');
|
||
if(result.created&&result.temporary_password){
|
||
const credentials=`Caterium\nEmail: ${email}\nВременный пароль: ${result.temporary_password}`;
|
||
host.innerHTML=`<div class="sun-auth-success"><b>Сотрудник создан.</b><br>Передайте ему данные для первого входа:</div><div class="sun-rbac-linkbox"><b>Логин:</b> ${esc(email)}<br><b>Временный пароль:</b> ${esc(result.temporary_password)}</div><p class="hint">При первом входе Caterium попросит сотрудника придумать свой новый пароль.</p><div class="actions"><button class="primary" type="button" id="sunRbacCopyEmployee">Скопировать данные</button><button class="outline" type="button" id="sunRbacEmployeeDone">Готово</button></div>`;
|
||
$('sunRbacCopyEmployee').onclick=()=>copyText(credentials,'Логин и временный пароль скопированы.');
|
||
}else{
|
||
host.innerHTML=`<div class="sun-auth-success"><b>Сотрудник добавлен.</b><br>Аккаунт ${esc(email)} уже существовал в Caterium. Сотрудник входит своим обычным паролем.</div><div class="actions"><button class="primary" type="button" id="sunRbacEmployeeDone">Готово</button></div>`;
|
||
}
|
||
$('sunRbacEmployeeDone').onclick=async()=>{inviting=false;await loadMembers();};
|
||
toast('Сотрудник добавлен.','success');
|
||
}catch(e){toast(e.message||String(e),'error',7000);btn.disabled=false;btn.textContent='Создать сотрудника';}
|
||
}
|
||
|
||
function permissionHtml(perms, role){
|
||
return GROUPS.map(g=>`<div class="sun-rbac-group"><h4>${esc(g.title)}</h4>${g.items.map(([key,label])=>{const checked=typeof perms?.[key]==='boolean'?perms[key]:(role==='admin');return `<label class="sun-rbac-check"><input type="checkbox" data-rbac-perm="${esc(key)}" ${checked?'checked':''}><span>${esc(label)}</span></label>`;}).join('')}</div>`).join('');
|
||
}
|
||
|
||
function renderEditor(){
|
||
const body=$('sunRbacBody'),m=editing;if(!body||!m)return;
|
||
const perms=Object.assign({},PRESETS[m.role]||PRESETS.viewer,m.permissions||{});
|
||
const adminCount=members.filter(x=>x.role==='admin'&&x.is_active).length;
|
||
body.innerHTML=`<div class="sun-rbac-toolbar"><button class="outline" type="button" id="sunRbacBack">← Пользователи</button><div class="hint">${esc(m.email||'')}</div></div><div class="sun-rbac-role-line"><label>Имя<input id="sunRbacName" value="${esc(m.display_name||'')}"></label><label>Роль<select id="sunRbacRole">${Object.entries(ROLE_LABELS).map(([k,v])=>`<option value="${k}" ${m.role===k?'selected':''}>${esc(v)}</option>`).join('')}</select></label><label class="sun-rbac-check"><input id="sunRbacActive" type="checkbox" ${m.is_active?'checked':''}> Активный</label></div><div class="sun-rbac-perm-groups" id="sunRbacPerms">${permissionHtml(perms,m.role)}</div><div class="actions" style="margin-top:14px"><button class="primary" type="button" id="sunRbacSave">Сохранить права</button><button class="danger sun-rbac-danger" type="button" id="sunRbacRemove">Удалить доступ</button></div><p class="sun-rbac-lock-note">Активных администраторов: <b>${adminCount}</b>. Ограничение срабатывает только если после изменения не останется ни одного активного администратора. Хотя бы у одного активного администратора должно оставаться право «Пользователи и права».</p>`;
|
||
$('sunRbacBack').onclick=()=>{editing=null;renderMembers();};
|
||
$('sunRbacRole').onchange=()=>{
|
||
const role=$('sunRbacRole').value;
|
||
$('sunRbacPerms').innerHTML=permissionHtml(PRESETS[role]||PRESETS.viewer,role);
|
||
};
|
||
$('sunRbacSave').onclick=saveMember;
|
||
$('sunRbacRemove').onclick=removeMember;
|
||
}
|
||
|
||
function readEditorPermissions(){
|
||
const out={'app.read':true};
|
||
qa('[data-rbac-perm]',$('sunRbacPerms')).forEach(i=>out[i.dataset.rbacPerm]=i.checked);
|
||
return out;
|
||
}
|
||
|
||
async function saveMember(){
|
||
const c=client(),ws=workspace(),m=editing;if(!c||!ws||!m)return;
|
||
const btn=$('sunRbacSave');btn.disabled=true;
|
||
try{
|
||
const {error}=await c.rpc('sun_admin_update_member',{p_workspace:ws.id,p_user:m.user_id,p_display_name:$('sunRbacName').value.trim(),p_role:$('sunRbacRole').value,p_is_active:$('sunRbacActive').checked,p_permissions:readEditorPermissions()});
|
||
if(error)throw error;
|
||
toast('Права пользователя сохранены.','success');editing=null;
|
||
await cloud()?.reloadMemberships?.();
|
||
applyNavPermissions();
|
||
if(has('users.manage'))await loadMembers();else closeModal();
|
||
}catch(e){toast(e.message||String(e),'error',7000);}finally{if(btn)btn.disabled=false;}
|
||
}
|
||
|
||
async function removeMember(){
|
||
const c=client(),ws=workspace(),m=editing;if(!c||!ws||!m)return;
|
||
if(!confirm(`Удалить доступ пользователю ${m.display_name||m.email||''}?`))return;
|
||
try{const {error}=await c.rpc('sun_admin_remove_member',{p_workspace:ws.id,p_user:m.user_id});if(error)throw error;toast('Доступ удалён.','success');editing=null;await cloud()?.reloadMemberships?.();applyNavPermissions();if(has('users.manage'))await loadMembers();else closeModal();}
|
||
catch(e){toast(e.message||String(e),'error',7000);}
|
||
}
|
||
|
||
const hex=bytes=>[...bytes].map(x=>x.toString(16).padStart(2,'0')).join('');
|
||
const unhex=value=>new Uint8Array(String(value||'').match(/.{1,2}/g)?.map(x=>parseInt(x,16))||[]);
|
||
async function pinDigest(pin,salt){
|
||
const raw=new TextEncoder().encode(String(pin||''));
|
||
if(crypto?.subtle){
|
||
const key=await crypto.subtle.importKey('raw',raw,'PBKDF2',false,['deriveBits']);
|
||
const bits=await crypto.subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt,iterations:120000},key,256);
|
||
return hex(new Uint8Array(bits));
|
||
}
|
||
let v=2166136261;for(let n=0;n<5000;n++)for(const b of [...raw,...salt]){v^=b;v=Math.imul(v,16777619);}return String(v>>>0);
|
||
}
|
||
async function makePinRecord(pin){const salt=crypto.getRandomValues(new Uint8Array(16));return `v1:${hex(salt)}:${await pinDigest(pin,salt)}`;}
|
||
async function verifyPin(pin,record){const parts=String(record||'').split(':');if(parts.length!==3||parts[0]!=='v1')return false;const salt=unhex(parts[1]);return (await pinDigest(pin,salt))===parts[2];}
|
||
|
||
function pinKey(){const id=session()?.user?.id;return id?`sunCloudQuickPinV3:${id}`:'';}
|
||
function pinUnlockKey(){const id=session()?.user?.id;return id?`sunCloudQuickPinUnlockedV3:${id}`:'';}
|
||
async function setQuickPin(){
|
||
const id=session()?.user?.id;if(!id)return;
|
||
const p1=prompt('Введите новый PIN (4–8 цифр):','');if(p1===null)return;if(!/^\d{4,8}$/.test(p1))return toast('PIN должен содержать 4–8 цифр.','warn');
|
||
const p2=prompt('Повторите PIN:','');if(p1!==p2)return toast('PIN-коды не совпадают.','warn');
|
||
localStorage.setItem(pinKey(),await makePinRecord(p1));sessionStorage.setItem(pinUnlockKey(),'1');toast('Быстрый PIN включён на этом устройстве.','success');injectQuickPinCard();
|
||
}
|
||
function removeQuickPin(){const key=pinKey();if(!key)return;localStorage.removeItem(key);sessionStorage.removeItem(pinUnlockKey());toast('Быстрый PIN отключён.','success');injectQuickPinCard();}
|
||
function injectQuickPinCard(){
|
||
const card=$('sunCloudV2Card');if(!card||!session()?.user)return;
|
||
let box=$('sunCloudQuickPinCardV3');if(!box){box=document.createElement('div');box.id='sunCloudQuickPinCardV3';box.className='sun-cloud-v2-box wide sun-cloud-pin-card';card.appendChild(box);}
|
||
const enabled=Boolean(pinKey()&&localStorage.getItem(pinKey()));
|
||
box.innerHTML=`<h3>Быстрый вход на этом устройстве</h3><p class="sun-cloud-v2-note">Основной аккаунт — email и пароль. PIN только быстро разблокирует уже авторизованное устройство и не меняет облачные права.</p><div class="sun-cloud-v2-actions"><button class="outline" type="button" id="sunSetQuickPinV3">${enabled?'Изменить PIN':'Включить PIN'}</button>${enabled?'<button class="outline" type="button" id="sunRemoveQuickPinV3">Отключить PIN</button>':''}</div>`;
|
||
$('sunSetQuickPinV3').onclick=setQuickPin;$('sunRemoveQuickPinV3')?.addEventListener('click',removeQuickPin);
|
||
}
|
||
|
||
function showPinLockIfNeeded(){
|
||
const ss=session(),ws=workspace();if(!ss?.user||!ws)return;
|
||
const key=pinKey(), unlock=pinUnlockKey();if(!key||!localStorage.getItem(key)||sessionStorage.getItem(unlock)==='1'||$('sunCloudPinLockV3'))return;
|
||
const overlay=document.createElement('div');overlay.id='sunCloudPinLockV3';overlay.className='sun-cloud-auth-gate';overlay.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Caterium</h2><div class="hint">${esc(ws.display_name||ss.user.email||'Пользователь')} · ${esc(ROLE_LABELS[ws.role]||ws.role)}</div></div></div><label>PIN<input id="sunCloudPinInputV3" type="password" inputmode="numeric" autocomplete="current-password"></label><div class="sun-cloud-auth-actions"><button class="primary" id="sunCloudPinUnlockV3" type="button">Войти</button><button class="outline" id="sunCloudUsePasswordV3" type="button">Выйти из аккаунта</button></div><div class="sun-cloud-auth-error" id="sunCloudPinErrorV3"></div></div>`;document.body.appendChild(overlay);
|
||
const unlockFn=async()=>{const v=$('sunCloudPinInputV3').value;if(await verifyPin(v,localStorage.getItem(key))){sessionStorage.setItem(unlock,'1');overlay.remove();applyNavPermissions();}else $('sunCloudPinErrorV3').textContent='Неверный PIN.';};
|
||
$('sunCloudPinUnlockV3').onclick=unlockFn;$('sunCloudPinInputV3').addEventListener('keydown',e=>{if(e.key==='Enter')unlockFn();});$('sunCloudUsePasswordV3').onclick=async()=>{if(window.SunCloudV2?.signOut)return window.SunCloudV2.signOut();try{await client()?.auth.signOut();}catch(_){}location.reload();};setTimeout(()=>$('sunCloudPinInputV3')?.focus(),50);
|
||
}
|
||
|
||
const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23';
|
||
const LOCAL_ONLY_KEY='sunLocalOnlyModeV1';
|
||
let registrationProvisioning=false;
|
||
function savePendingRegistration(email,companyName){try{localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({email:String(email||'').trim().toLowerCase(),companyName:String(companyName||'').trim()||'Новая компания',createdAt:new Date().toISOString()}));}catch(_){}}
|
||
function getPendingRegistration(email=''){try{const raw=JSON.parse(localStorage.getItem(PENDING_REGISTRATION_KEY)||'null');if(!raw?.email)return null;if(email&&String(raw.email).toLowerCase()!==String(email).trim().toLowerCase())return null;return raw}catch(_){return null}}
|
||
function clearPendingRegistration(){try{localStorage.removeItem(PENDING_REGISTRATION_KEY)}catch(_){}}
|
||
async function finishPendingRegistration(gate,email){
|
||
const pending=getPendingRegistration(email);if(!pending||registrationProvisioning)return false;
|
||
registrationProvisioning=true;const err=gate?.querySelector('#sunGateErrorV3');if(err)err.textContent='Создаю вашу компанию…';
|
||
try{const ws=await cloud()?.createTrialWorkspace?.(pending.companyName||'Новая компания');if(!ws)throw new Error('Не удалось создать компанию.');clearPendingRegistration();if(err)err.textContent='Готово. Открываю Caterium…';setTimeout(async()=>{gate?.remove();document.body.classList.remove('sun-cloud-auth-required');try{await cloud()?.pull?.()}catch(_){}applyNavPermissions();},350);return true;}
|
||
catch(e){if(err)err.textContent=errText(e,'Не удалось завершить регистрацию.');return false}
|
||
finally{registrationProvisioning=false}
|
||
}
|
||
|
||
const INVITE_QUERY_KEY='invite';
|
||
let invitePreviewCache=null;
|
||
let invitePreviewLoading=false;
|
||
function currentInviteToken(){try{return String(new URL(location.href).searchParams.get(INVITE_QUERY_KEY)||'').trim()}catch(_){return ''}}
|
||
function clearInviteFromUrl(){try{const u=new URL(location.href);u.searchParams.delete(INVITE_QUERY_KEY);history.replaceState(null,'',u.toString())}catch(_){}}
|
||
function passwordField(id,label,autocomplete='current-password'){
|
||
return `<label>${esc(label)}<div class="sun-auth-password"><input id="${id}" type="password" autocomplete="${autocomplete}"><button class="sun-auth-eye" type="button" data-toggle-password="${id}" aria-label="Показать пароль">◉</button></div></label>`;
|
||
}
|
||
function bindPasswordEyes(root){qa('[data-toggle-password]',root).forEach(b=>b.onclick=()=>{const i=root.querySelector(`#${b.dataset.togglePassword}`);if(!i)return;i.type=i.type==='password'?'text':'password';b.textContent=i.type==='password'?'◉':'×';b.setAttribute('aria-label',i.type==='password'?'Показать пароль':'Скрыть пароль');});}
|
||
async function getInvitePreview(token){
|
||
if(!token)return null;if(invitePreviewCache?.token===token)return invitePreviewCache.data;if(invitePreviewLoading)return null;
|
||
invitePreviewLoading=true;try{const c=client();if(!c)return null;const {data,error}=await c.rpc('sun_invite_preview_v27',{p_token:token});if(error)throw error;const row=Array.isArray(data)?data[0]:data;invitePreviewCache={token,data:row||null};return row||null;}finally{invitePreviewLoading=false;}
|
||
}
|
||
async function finishInvite(token,gate){
|
||
const err=gate?.querySelector('#sunGateErrorV3');if(err)err.textContent='Добавляю вас в компанию…';
|
||
try{const ws=await cloud()?.acceptInvite?.(token);if(!ws)throw new Error('Не удалось присоединиться к компании.');clearInviteFromUrl();invitePreviewCache=null;gate?.remove();document.body.classList.remove('sun-cloud-auth-required');try{await cloud()?.pull?.()}catch(_){}applyNavPermissions();toast(`Вы присоединились к компании «${ws.name||'Компания'}».`,'success',6500);return true;}
|
||
catch(e){if(err)err.textContent=String(e?.message||e||'Не удалось принять приглашение.');return false;}
|
||
}
|
||
async function renderInviteGate(gate,token){
|
||
let preview;
|
||
try{preview=await getInvitePreview(token);}catch(e){gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Приглашение недоступно</h2><div class="hint">Caterium</div></div></div><p class="sun-cloud-auth-error">${esc(e?.message||e||'Не удалось проверить приглашение.')}</p><div class="sun-cloud-auth-actions"><button class="outline" id="sunInviteGoHome" type="button">Перейти ко входу</button></div></div>`;gate.querySelector('#sunInviteGoHome').onclick=()=>{clearInviteFromUrl();gate.remove();ensureAuthGate();};return;}
|
||
if(!preview||!preview.is_valid){gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Приглашение недействительно</h2><div class="hint">Ссылка истекла, отменена или уже использована.</div></div></div><div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteGoHome" type="button">Перейти ко входу</button></div></div>`;gate.querySelector('#sunInviteGoHome').onclick=()=>{clearInviteFromUrl();gate.remove();ensureAuthGate();};return;}
|
||
const ss=session(),signedEmail=String(ss?.user?.email||'').toLowerCase(),inviteEmail=String(preview.email||'').toLowerCase();
|
||
if(ss?.user){
|
||
if(signedEmail===inviteEmail){gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Приглашение в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">${esc(preview.display_name||preview.email||'Сотрудник')} · ${esc(ROLE_LABELS[preview.role]||preview.role)}</div></div></div><p class="hint">Вы вошли как <b>${esc(ss.user.email||'')}</b>. Нажмите кнопку, чтобы добавить эту компанию в ваш аккаунт.</p><div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteAcceptSigned" type="button">Присоединиться к компании</button><button class="outline" id="sunInviteSignOut" type="button">Войти другим аккаунтом</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div></div>`;gate.querySelector('#sunInviteAcceptSigned').onclick=()=>finishInvite(token,gate);gate.querySelector('#sunInviteSignOut').onclick=async()=>{try{await window.SunCloudV2?.signOut?.()}catch(_){}};return;}
|
||
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Приглашение в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">Для ${esc(preview.email||'другого email')}</div></div></div><p class="hint">Сейчас вы вошли как <b>${esc(ss.user.email||'')}</b>. Для принятия приглашения нужно войти под адресом <b>${esc(preview.email||'')}</b>.</p><div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteSignOut" type="button">Выйти и продолжить</button></div></div>`;gate.querySelector('#sunInviteSignOut').onclick=async()=>{try{await window.SunCloudV2?.signOut?.()}catch(_){}};return;
|
||
}
|
||
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Вас пригласили в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">Роль: ${esc(ROLE_LABELS[preview.role]||preview.role)}</div></div></div><label>Имя<input id="sunInviteNameV27" autocomplete="name" value="${esc(preview.display_name||'')}"></label><label>Email<input id="sunInviteEmailV27" type="email" value="${esc(preview.email||'')}" readonly></label>${passwordField('sunInvitePasswordV27','Пароль','new-password')}${passwordField('sunInvitePassword2V27','Подтвердите пароль','new-password')}<div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteJoinV27" type="button">Присоединиться</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div><p class="hint">Если аккаунт с этим email уже есть — введите его пароль. Если аккаунта ещё нет — придумайте новый пароль и повторите его.</p></div>`;
|
||
bindPasswordEyes(gate);const btn=gate.querySelector('#sunInviteJoinV27');const run=async()=>{const c=client(),email=String(preview.email||'').trim(),name=String(gate.querySelector('#sunInviteNameV27')?.value||'').trim(),password=String(gate.querySelector('#sunInvitePasswordV27')?.value||''),password2=String(gate.querySelector('#sunInvitePassword2V27')?.value||''),err=gate.querySelector('#sunGateErrorV3');if(password.length<6){err.textContent='Пароль должен содержать минимум 6 символов.';return;}if(password!==password2){err.textContent='Пароли не совпадают.';return;}btn.disabled=true;err.textContent='Проверяю аккаунт…';try{let r=await c.auth.signInWithPassword({email,password});if(r.error){r=await c.auth.signUp({email,password,options:{data:{name,registration_source:'caterium_invite_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error)throw new Error('Этот email уже зарегистрирован. Введите пароль от существующего аккаунта.');r=retry;}}const userId=r.data.user?.id||r.data.session?.user?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');err.textContent='Аккаунт готов. Подключаю компанию…';setTimeout(()=>finishInvite(token,gate),180);}catch(e){err.textContent=String(e?.message||e||'Не удалось войти или создать аккаунт.');btn.disabled=false;}};btn.onclick=run;gate.querySelector('#sunInvitePassword2V27').addEventListener('keydown',e=>{if(e.key==='Enter')run()});
|
||
}
|
||
|
||
// Password changes now happen from account settings (see cloud-sync-v2.js's
|
||
// renderCloudUI "Пароль" box) instead of a forced post-login gate: the old
|
||
// gate raced with membership loading (loadMemberships finishing and
|
||
// ensureAuthGate's periodic poll tearing the gate down as soon as
|
||
// signedIn+workspace was true) and could disappear before the employee
|
||
// ever saw it.
|
||
|
||
function ensureAuthGate(){
|
||
const st=cloud()?.status?.();if(!st?.connected)return;
|
||
const ws=workspace(),ss=session(),inviteToken=currentInviteToken();
|
||
if(inviteToken){
|
||
let gate=$('sunCloudAuthGateV3');if(!gate){document.body.classList.add('sun-cloud-auth-required');gate=document.createElement('div');gate.id='sunCloudAuthGateV3';gate.className='sun-cloud-auth-gate';gate.innerHTML='<div class="sun-cloud-auth-card"><p class="hint">Проверяю приглашение…</p></div>';document.body.appendChild(gate);renderInviteGate(gate,inviteToken);}return;
|
||
}
|
||
if(st.signedIn&&ws){$('sunCloudAuthGateV3')?.remove();document.body.classList.remove('sun-cloud-auth-required');try{localStorage.removeItem(LOCAL_ONLY_KEY)}catch(_){}return;}
|
||
if(!st.signedIn){try{if(localStorage.getItem(LOCAL_ONLY_KEY)==='1'){$('sunCloudAuthGateV3')?.remove();document.body.classList.remove('sun-cloud-auth-required');return;}}catch(_){}}
|
||
if($('sunCloudAuthGateV3')){if(st.signedIn&&st.membershipsLoading){const e=$('sunGateErrorV3');if(e)e.textContent='Загружаю рабочую базу…';}return;}
|
||
document.body.classList.add('sun-cloud-auth-required');
|
||
const gate=document.createElement('div');gate.id='sunCloudAuthGateV3';gate.className='sun-cloud-auth-gate';
|
||
if(st.signedIn&&ss?.user&&!ws){
|
||
const pending=getPendingRegistration(ss.user.email||'');
|
||
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>${pending?'Создаю компанию':st.membershipsLoading?'Загружаю рабочую базу':'Аккаунт готов'}</h2><div class="hint">${esc(ss.user.email||'Аккаунт авторизован')}</div></div></div><p class="hint">${pending?'Регистрация завершена. Сейчас подготовим вашу рабочую компанию.':st.membershipsLoading?'Проверяю доступы этого аккаунта на сервере.':'У этого аккаунта пока нет компании. Укажите название, чтобы создать её.'}</p>${!pending&&!st.membershipsLoading?'<label>Название компании<input id="sunGateRecoveryCompanyV25" autocomplete="organization" value="Моя компания"></label>':''}<div class="sun-cloud-auth-actions"><button class="primary" id="sunGateRetryWorkspaceV3" type="button">${pending?'Продолжить':st.membershipsLoading?'Проверить ещё раз':'Создать компанию'}</button><button class="outline" id="sunGateSignOutV3" type="button">Выйти</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3">${pending?'Подготавливаю компанию…':st.membershipsLoading?'Загружаю рабочую базу…':''}</div></div>`;
|
||
document.body.appendChild(gate);if(pending&&!st.membershipsLoading)setTimeout(()=>finishPendingRegistration(gate,ss.user.email||''),80);
|
||
gate.querySelector('#sunGateRetryWorkspaceV3').onclick=async()=>{const e=gate.querySelector('#sunGateErrorV3');if(getPendingRegistration(ss.user.email||''))return finishPendingRegistration(gate,ss.user.email||'');if(!st.membershipsLoading){const companyName=String(gate.querySelector('#sunGateRecoveryCompanyV25')?.value||'').trim();if(!companyName){e.textContent='Введите название компании.';return;}savePendingRegistration(ss.user.email||'',companyName);return finishPendingRegistration(gate,ss.user.email||'');}e.textContent='Проверяю доступ…';try{const found=await cloud()?.reloadMemberships?.();if(found){gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();setTimeout(applyNavPermissions,100);}else e.textContent='Рабочая база для этого аккаунта не найдена.';}catch(err){e.textContent=errText(err,'Не удалось проверить доступ.');}};
|
||
gate.querySelector('#sunGateSignOutV3').onclick=async()=>{if(window.SunCloudV2?.signOut)return window.SunCloudV2.signOut();try{await client()?.auth.signOut();}catch(_){}location.reload();};return;
|
||
}
|
||
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2 id="sunGateTitleV3">Вход в Caterium</h2><div class="hint" id="sunGateSubtitleV3">Введите данные своего аккаунта</div></div></div><div id="sunGateRegisterFieldsV27" hidden><label>Название компании<input id="sunGateCompanyV3" autocomplete="organization" placeholder="Например, Мой Кейтеринг"></label></div><label>Email<input id="sunGateEmailV3" type="email" autocomplete="username"></label>${passwordField('sunGatePasswordV3','Пароль','current-password')}<div id="sunGateConfirmWrapV27" hidden>${passwordField('sunGatePassword2V27','Подтвердите пароль','new-password')}</div><div class="sun-cloud-auth-actions"><button class="primary" id="sunGateSubmitV3" type="button">Войти</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div><p class="sun-auth-switch"><span id="sunGateSwitchTextV27">Нет аккаунта?</span> <button type="button" id="sunGateSwitchV27">Создать аккаунт</button></p><p class="sun-auth-switch"><button type="button" id="sunGateLocalOnlyV1" style="background:none;border:0;padding:0;color:inherit;opacity:.6;text-decoration:underline;cursor:pointer;font:inherit">Работать локально без интернета</button></p></div>`;document.body.appendChild(gate);bindPasswordEyes(gate);
|
||
gate.querySelector('#sunGateLocalOnlyV1').onclick=()=>{try{localStorage.setItem(LOCAL_ONLY_KEY,'1')}catch(_){}location.reload();};
|
||
const emailInput=gate.querySelector('#sunGateEmailV3'),passwordInput=gate.querySelector('#sunGatePasswordV3'),password2Input=gate.querySelector('#sunGatePassword2V27'),companyInput=gate.querySelector('#sunGateCompanyV3'),submitBtn=gate.querySelector('#sunGateSubmitV3'),gateError=gate.querySelector('#sunGateErrorV3');let registerMode=false;
|
||
const setMode=(registration)=>{registerMode=Boolean(registration);gate.querySelector('#sunGateRegisterFieldsV27').hidden=!registerMode;gate.querySelector('#sunGateConfirmWrapV27').hidden=!registerMode;gate.querySelector('#sunGateTitleV3').textContent=registerMode?'Создать аккаунт Caterium':'Вход в Caterium';gate.querySelector('#sunGateSubtitleV3').textContent=registerMode?'Заполните данные — после регистрации Caterium откроется автоматически':'Введите данные своего аккаунта';submitBtn.textContent=registerMode?'Создать аккаунт':'Войти';gate.querySelector('#sunGateSwitchTextV27').textContent=registerMode?'Уже есть аккаунт?':'Нет аккаунта?';gate.querySelector('#sunGateSwitchV27').textContent=registerMode?'Войти':'Создать аккаунт';passwordInput.autocomplete=registerMode?'new-password':'current-password';gateError.textContent='';if(registerMode)setTimeout(()=>companyInput.focus(),30);else setTimeout(()=>emailInput.focus(),30)};
|
||
async function auth(){const c=client(),email=emailInput.value.trim(),password=passwordInput.value,password2=password2Input.value,companyName=String(companyInput.value||'').trim();if(!c){gateError.textContent='Облачный сервис не подключён.';return;}if(registerMode&&!companyName){gateError.textContent='Введите название компании.';return;}if(!email||password.length<6){gateError.textContent='Введите email и пароль минимум из 6 символов.';return;}if(registerMode&&password!==password2){gateError.textContent='Пароли не совпадают.';return;}submitBtn.disabled=true;gateError.textContent=registerMode?'Создаю аккаунт…':'Выполняю вход…';try{let r;if(registerMode){savePendingRegistration(email,companyName);const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data.session){r=existing;}else{r=await c.auth.signUp({email,password,options:{data:{company_name:companyName,registration_source:'caterium_public_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error){clearPendingRegistration();throw new Error('Этот email уже зарегистрирован. Войдите в аккаунт или восстановите пароль.');}r=retry;}}}else r=await c.auth.signInWithPassword({email,password});if(r.error)throw r.error;const authUser=r.data.user||r.data.session?.user||null;const userId=authUser?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');gateError.textContent=registerMode?'Аккаунт готов. Создаю компанию…':'Вход выполнен. Загружаю рабочую базу…';setTimeout(async()=>{await cloud()?.reloadMemberships?.();if(registerMode&&workspace()){clearPendingRegistration();gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();return;}if(getPendingRegistration(email)&&!workspace()){await finishPendingRegistration(gate,email);return;}if(workspace()){gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();}else{gate.remove();ensureAuthGate();}},180);}catch(err){gateError.textContent=errText(err,'Не удалось выполнить операцию.')}finally{submitBtn.disabled=false}}
|
||
gate.querySelector('#sunGateSwitchV27').onclick=()=>setMode(!registerMode);submitBtn.onclick=auth;passwordInput.addEventListener('keydown',e=>{if(e.key==='Enter'&&!registerMode)auth()});password2Input.addEventListener('keydown',e=>{if(e.key==='Enter'&®isterMode)auth()});setMode(false);
|
||
}
|
||
|
||
function onCloudChanged(){
|
||
if(applying)return;applying=true;
|
||
try{
|
||
const ws=workspace();if(ws?.id!==lastWorkspaceId){lastWorkspaceId=ws?.id||'';}
|
||
ensureAuthGate();normalizeLegacyLocalAuth();installGuards();applyNavPermissions();hideLegacyRoleCard();injectQuickPinCard();showPinLockIfNeeded();
|
||
}finally{applying=false;}
|
||
}
|
||
|
||
window.addEventListener('sun:open-cloud-users',openModal);
|
||
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(onCloudChanged,0));
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(()=>{hideLegacyRoleCard();injectQuickPinCard();},120);},true);
|
||
(()=>{let queued=false;const run=()=>{if(queued||applying)return;queued=true;setTimeout(()=>{queued=false;if(!applying){applyNavPermissions();hideLegacyRoleCard();}},60);};[document.querySelector('header'),document.getElementById('enterprise-settings')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
||
|
||
installStyle();normalizeLegacyLocalAuth();ensureModal();
|
||
let tries=0;const boot=setInterval(()=>{tries++;if(window.SunCloudV2){clearInterval(boot);setTimeout(onCloudChanged,80);}else if(tries>80)clearInterval(boot);},100);
|
||
|
||
setInterval(()=>{if(document.hidden)return;try{ensureAuthGate();if(workspace()&&session()?.user){applyNavPermissions();injectQuickPinCard();showPinLockIfNeeded();}}catch(_){}},5000);
|
||
|
||
window.SunAdminRBACV3={VERSION,openUsers:openModal,apply:applyNavPermissions,hasPermission:has};
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: order-payment-ui-v1.js ===== */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunOrderPaymentUIV1)return;
|
||
window.__sunOrderPaymentUIV1=true;
|
||
|
||
const KEY='sunOrderPaymentColorsV1';
|
||
const DEFAULTS={
|
||
unpaid:'#d84a4a',
|
||
prepaid:'#35a46b',
|
||
paid:'#3b82f6',
|
||
cancelled:'#9aa0aa'
|
||
};
|
||
const LABELS={
|
||
unpaid:'Без предоплаты',
|
||
prepaid:'Предоплата внесена',
|
||
paid:'Оплачен полностью',
|
||
cancelled:'Отменён'
|
||
};
|
||
const $=id=>document.getElementById(id);
|
||
const hex=value=>/^#[0-9a-f]{6}$/i.test(String(value||''))?String(value).toLowerCase():null;
|
||
const readColors=()=>{
|
||
let saved={};
|
||
try{saved=JSON.parse(localStorage.getItem(KEY)||'{}')||{}}catch(_){saved={}}
|
||
const next={};
|
||
Object.keys(DEFAULTS).forEach(key=>next[key]=hex(saved[key])||DEFAULTS[key]);
|
||
return next;
|
||
};
|
||
const saveColors=colors=>{
|
||
const safe={};Object.keys(DEFAULTS).forEach(key=>safe[key]=hex(colors[key])||DEFAULTS[key]);
|
||
localStorage.setItem(KEY,JSON.stringify(safe));
|
||
decorateOrders();
|
||
return safe;
|
||
};
|
||
const liveOrders=()=>{try{return Array.isArray(orders)?orders:JSON.parse(localStorage.getItem('sunOrders')||'[]')}catch(_){return[]}};
|
||
const byId=id=>liveOrders().find(order=>String(order?.id)===String(id));
|
||
const orderTotal=order=>{
|
||
try{if(typeof window.orderTotalValue==='function'){const value=Number(window.orderTotalValue(order));if(Number.isFinite(value))return Math.max(0,value)}}catch(_){}
|
||
const direct=Number(order?.total);return Number.isFinite(direct)?Math.max(0,direct):0;
|
||
};
|
||
function paymentState(order){
|
||
if(String(order?.status||'').trim()==='Отменён')return'cancelled';
|
||
const paid=Math.max(0,Number(order?.prepayment||0));
|
||
const total=orderTotal(order);
|
||
if(total>0&&paid>=total-0.5)return'paid';
|
||
if(paid>0)return'prepaid';
|
||
return'unpaid';
|
||
}
|
||
window.sunOrderPaymentState=paymentState;
|
||
window.sunOrderPaymentColors=()=>({...readColors()});
|
||
|
||
function readableText(color){
|
||
const value=hex(color)||'#000000';
|
||
const r=parseInt(value.slice(1,3),16),g=parseInt(value.slice(3,5),16),b=parseInt(value.slice(5,7),16);
|
||
return ((r*299+g*587+b*114)/1000)>168?'#252a32':'#ffffff';
|
||
}
|
||
function shadowColor(color,alpha=.24){
|
||
const value=hex(color)||'#000000';
|
||
const r=parseInt(value.slice(1,3),16),g=parseInt(value.slice(3,5),16),b=parseInt(value.slice(5,7),16);
|
||
return `rgba(${r},${g},${b},${alpha})`;
|
||
}
|
||
function paintDateTile(tile,order){
|
||
if(!tile||!order)return;
|
||
const state=paymentState(order),colors=readColors(),color=colors[state],text=readableText(color);
|
||
tile.dataset.paymentState=state;
|
||
tile.title=LABELS[state];
|
||
tile.setAttribute('aria-label',`${tile.textContent.trim()}: ${LABELS[state]}`);
|
||
tile.style.setProperty('background',color,'important');
|
||
tile.style.setProperty('border-color',color,'important');
|
||
tile.style.setProperty('box-shadow',`0 5px 14px ${shadowColor(color)}`,'important');
|
||
tile.style.setProperty('color',text,'important');
|
||
tile.querySelectorAll('strong,span,small').forEach(node=>node.style.setProperty('color',text,'important'));
|
||
}
|
||
function decorateOrders(){
|
||
document.querySelectorAll('#ordersList .sun-order-payment-marker,#sunOrderCards .sun-order-payment-marker').forEach(marker=>marker.remove());
|
||
document.querySelectorAll('#ordersList .sun-order-number-cell').forEach(cell=>cell.classList.remove('sun-order-number-cell'));
|
||
document.querySelectorAll('#sunOrderCards .sun-order-number-with-payment').forEach(host=>host.classList.remove('sun-order-number-with-payment'));
|
||
document.querySelectorAll('#sunOrderCards .sun-order-card').forEach(card=>{
|
||
const order=byId(card.dataset.orderId),tile=card.querySelector('.sun-order-card-date');
|
||
if(order&&tile)paintDateTile(tile,order);
|
||
});
|
||
}
|
||
let decorateQueued=false;
|
||
function queueDecorate(){if(decorateQueued)return;decorateQueued=true;requestAnimationFrame(()=>{decorateQueued=false;decorateOrders()})}
|
||
|
||
const priorRenderOrders=window.renderOrders;
|
||
if(typeof priorRenderOrders==='function')window.renderOrders=function(...args){const result=priorRenderOrders.apply(this,args);queueDecorate();return result};
|
||
|
||
// One authoritative deletion path for list, cards and the current order form.
|
||
window.deleteOrderById=function(id){
|
||
const list=liveOrders();
|
||
const order=list.find(item=>String(item?.id)===String(id));
|
||
if(!order)return false;
|
||
if(!confirm(`Удалить заказ №${order.id}${order.event?` «${order.event}»`:''}?`))return false;
|
||
try{
|
||
if(Array.isArray(orders)){
|
||
const index=orders.findIndex(item=>String(item?.id)===String(id));
|
||
if(index>=0)orders.splice(index,1);
|
||
}else{
|
||
localStorage.setItem('sunOrders',JSON.stringify(list.filter(item=>String(item?.id)!==String(id))));
|
||
}
|
||
if(typeof persist==='function')persist();
|
||
else localStorage.setItem('sunOrders',JSON.stringify(Array.isArray(orders)?orders:list.filter(item=>String(item?.id)!==String(id))));
|
||
}catch(error){
|
||
console.error('Order deletion failed',error);
|
||
window.SunEnterprise?.toast?.('Не удалось удалить заказ.','error',5000);
|
||
return false;
|
||
}
|
||
try{window.SunEnterprise?.recordAudit?.('Удалён заказ',`№${order.id} · ${order.event||'Заказ'}`)}catch(_){}
|
||
const currentId=(()=>{try{return draft?.id}catch(_){return null}})();
|
||
if(String(currentId)===String(id)&&typeof window.resetDraft==='function')window.resetDraft();
|
||
try{window.renderOrders?.()}catch(_){}
|
||
try{window.renderStats?.()}catch(_){}
|
||
try{window.sunRenderOrderCards?.()}catch(_){}
|
||
window.dispatchEvent(new CustomEvent('sun:order-deleted',{detail:{id:order.id}}));
|
||
window.SunEnterprise?.toast?.(`Заказ №${order.id} удалён.`,'success');
|
||
return true;
|
||
};
|
||
window.deleteOrder=function(){
|
||
const id=(()=>{try{return draft?.id}catch(_){return null}})();
|
||
if(!id)return false;
|
||
const deleted=window.deleteOrderById(id);
|
||
if(!deleted)return false;
|
||
const button=[...document.querySelectorAll('header nav button')].find(btn=>String(btn.dataset.navLabel||btn.textContent||'').trim()==='Заказы');
|
||
if(button)button.click();
|
||
else if(typeof window.show==='function')try{window.show('orders',document.querySelectorAll('header nav button')[1])}catch(_){}
|
||
return true;
|
||
};
|
||
|
||
function settingsCard(){
|
||
const view=$('enterprise-settings'),grid=view?.querySelector('.enterprise-grid');
|
||
if(!grid)return null;
|
||
let card=$('sunOrderPaymentColorsCard');
|
||
if(card)return card;
|
||
card=document.createElement('section');
|
||
card.className='enterprise-card';card.id='sunOrderPaymentColorsCard';
|
||
grid.appendChild(card);
|
||
renderSettingsCard(card);
|
||
return card;
|
||
}
|
||
function renderSettingsCard(card=settingsCard()){
|
||
if(!card)return;
|
||
const colors=readColors();
|
||
card.innerHTML=`<h2>Цвета оплаты заказов</h2><p class="hint">Цвет плашки с датой в разделе «Заказы» показывает состояние оплаты. Цвет каждого состояния можно выбрать самостоятельно.</p><div class="sun-payment-color-settings">${Object.keys(DEFAULTS).map(key=>`<label><span><i class="sun-payment-settings-dot" style="background:${colors[key]}"></i>${LABELS[key]}</span><input type="color" data-payment-color="${key}" value="${colors[key]}" aria-label="Цвет: ${LABELS[key]}"></label>`).join('')}</div><div class="actions"><button class="outline" id="sunResetPaymentColors" type="button">Вернуть стандартные цвета</button></div><p class="hint">Правила: 0 ₽ предоплаты — «Без предоплаты»; предоплата меньше суммы — «Предоплата внесена»; оплачена вся сумма — «Оплачен полностью»; статус «Отменён» — отдельный цвет.</p>`;
|
||
card.querySelectorAll('[data-payment-color]').forEach(input=>input.addEventListener('input',()=>{
|
||
const next=readColors();next[input.dataset.paymentColor]=input.value;saveColors(next);
|
||
const dot=input.closest('label')?.querySelector('.sun-payment-settings-dot');if(dot)dot.style.background=input.value;
|
||
}));
|
||
$('sunResetPaymentColors')?.addEventListener('click',()=>{localStorage.setItem(KEY,JSON.stringify(DEFAULTS));renderSettingsCard(card);decorateOrders();window.SunEnterprise?.toast?.('Стандартные цвета восстановлены.','success')});
|
||
}
|
||
function ensureSettingsCard(){const card=settingsCard();if(card)renderSettingsCard(card)}
|
||
|
||
const style=document.createElement('style');style.id='sun-order-payment-ui-v1-style';style.textContent=`
|
||
#orderForm .sun-order-line-price input{width:100%!important;max-width:104px!important;min-width:78px!important;padding:2px 7px!important;font-variant-numeric:tabular-nums!important;letter-spacing:.01em!important}
|
||
.sun-payment-color-settings{display:grid;gap:8px;margin-top:10px}
|
||
.sun-payment-color-settings label{display:grid;grid-template-columns:minmax(0,1fr) 52px;gap:10px;align-items:center;padding:8px 9px;border:1px solid #e3e6e9;border-radius:9px;background:#fafbfc}
|
||
.sun-payment-color-settings label>span{display:flex;align-items:center;gap:8px;color:#3d4652;font-weight:700;font-size:12.5px}
|
||
.sun-payment-color-settings input[type="color"]{width:48px;height:32px;padding:2px;border:1px solid #cfd5da;border-radius:7px;background:#fff;cursor:pointer}
|
||
.sun-payment-settings-dot{display:inline-block;width:13px;height:13px;border-radius:3px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.12)}
|
||
`;document.head.appendChild(style);
|
||
|
||
document.addEventListener('click',event=>{
|
||
const nav=event.target.closest('header nav button');
|
||
if(nav&&String(nav.dataset.navLabel||nav.textContent||'').trim()==='Настройки')setTimeout(ensureSettingsCard,60);
|
||
},true);
|
||
const ordersHost=$('orders');if(ordersHost)new MutationObserver(queueDecorate).observe(ordersHost,{childList:true,subtree:true});
|
||
const settingsHost=$('enterprise-settings');if(settingsHost)new MutationObserver(()=>{if(settingsHost.classList.contains('on'))setTimeout(()=>{if(!$('sunOrderPaymentColorsCard'))ensureSettingsCard()},0)}).observe(settingsHost,{childList:true,subtree:true});
|
||
window.addEventListener('sun:cloud-state-applied',()=>{queueDecorate();if($('enterprise-settings')?.classList.contains('on'))setTimeout(ensureSettingsCard,0)});
|
||
window.addEventListener('storage',event=>{if(event.key===KEY)queueDecorate()});
|
||
queueDecorate();
|
||
setTimeout(()=>{queueDecorate();if($('enterprise-settings')?.classList.contains('on'))ensureSettingsCard()},140);
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: order-crm-v1.js ===== */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunOrderCrmV1)return;window.__sunOrderCrmV1=true;
|
||
const $=id=>document.getElementById(id),qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const SOURCE_KEY='sunLeadSourcesV1';
|
||
const DEFAULT_SOURCES=['Заявка с сайта','CaterMe','Avito','Рекомендация','Повторный клиент','Соцсети','Другое'];
|
||
const getDraft=()=>{try{return draft}catch(_){return null}};
|
||
const getOrders=()=>{try{return Array.isArray(orders)?orders:[]}catch(_){try{const x=JSON.parse(localStorage.getItem('sunOrders')||'[]');return Array.isArray(x)?x:[]}catch(__){return[]}}};
|
||
const money=v=>typeof window.money==='function'?window.money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')} ₽`;
|
||
const toast=(m,t='success')=>{try{if(window.SunEnterprise?.toast)return window.SunEnterprise.toast(m,t)}catch(_){} if(t==='warn'||t==='error')console.warn(m)};
|
||
const style=document.createElement('style');style.id='sun-order-crm-v1-style';style.textContent=`
|
||
/* The precise-address fields existed in an older module but were hidden by a later polish layer. */
|
||
#details .sun-address-precision-title{display:block!important;grid-column:1/-1;margin:10px 0 0;font-size:13px;font-weight:850;color:#241d45}
|
||
#details .sun-delivery-region,#details .sun-delivery-locality,#details .sun-delivery-district{display:flex!important}
|
||
#details .sun-delivery-district{grid-column:1/-1!important}
|
||
#details .sun-lead-source-field,#details .sun-known-addresses-field{grid-column:1/-1}
|
||
#details .sun-client-match{grid-column:1/-1;display:none;padding:9px 11px;border:1px solid #d7d0e7;border-radius:11px;background:#f7f5fb;color:#514b68;font-size:12px;line-height:1.35}
|
||
#details .sun-client-match.on{display:block}
|
||
#details .sun-geo-confirm{display:none;margin-top:10px;padding:11px;border:1px solid #dfd9eb;border-radius:12px;background:#faf9fd}
|
||
#details .sun-geo-confirm.on{display:block}
|
||
#details .sun-geo-confirm strong{display:block;color:#241d45;font-size:12px;margin-bottom:5px}
|
||
#details .sun-geo-confirm p{margin:0 0 9px;color:#666078;font-size:11px;line-height:1.4}
|
||
#details .sun-geo-confirm .actions{display:flex;gap:7px;flex-wrap:wrap;margin:0}
|
||
#details .sun-geo-confirm.ok{border-color:#b8dcc4;background:#f3fbf5}
|
||
#details .sun-geo-confirm.ok strong{color:#28683d}
|
||
.sun-crm-stats-table{width:100%;border-collapse:collapse;font-size:12px}
|
||
.sun-crm-stats-table th,.sun-crm-stats-table td{padding:9px 8px;border-bottom:1px solid #ece9f2;text-align:right;white-space:nowrap}
|
||
.sun-crm-stats-table th:first-child,.sun-crm-stats-table td:first-child{text-align:left;white-space:normal}
|
||
.sun-crm-stats-table th{color:#817a94;font-size:10px;text-transform:uppercase;letter-spacing:.04em}
|
||
.sun-crm-stats-table td b{color:#241d45}
|
||
.sun-lead-settings-list{display:grid;gap:7px;margin:10px 0}
|
||
.sun-lead-settings-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px}
|
||
@media(max-width:700px){.sun-crm-stats-table{font-size:10px}.sun-crm-stats-table th,.sun-crm-stats-table td{padding:7px 4px}.sun-crm-hide-mobile{display:none}}
|
||
`;document.head.appendChild(style);
|
||
|
||
function readSources(){
|
||
try{const a=JSON.parse(localStorage.getItem(SOURCE_KEY)||'null');if(Array.isArray(a)){const clean=a.map(x=>String(x||'').trim()).filter(Boolean);if(clean.length)return [...new Set(clean)]}}catch(_){}
|
||
return DEFAULT_SOURCES.slice();
|
||
}
|
||
function saveSources(list){
|
||
const clean=[...new Set((list||[]).map(x=>String(x||'').trim()).filter(Boolean))];
|
||
localStorage.setItem(SOURCE_KEY,JSON.stringify(clean.length?clean:DEFAULT_SOURCES));
|
||
refreshSourceSelect();renderCrmStats();
|
||
}
|
||
function setSelectOptions(select,values,current=''){
|
||
if(!select)return;const cur=String(current||select.value||'');
|
||
const all=values.slice();if(cur&&!all.includes(cur))all.push(cur);
|
||
select.innerHTML=`<option value="">Не указано</option>`+all.map(v=>`<option value="${esc(v)}">${esc(v)}</option>`).join('');select.value=cur;
|
||
}
|
||
function ensureSourceField(){
|
||
const grid=document.querySelector('#details .sun-delivery-details');if(!grid)return null;
|
||
let select=$('orderLeadSource');if(!select){
|
||
const label=document.createElement('label');label.className='sun-lead-source-field';label.innerHTML='<span>Узнали из</span><select id="orderLeadSource"></select>';
|
||
const loyalty=$('orderLoyaltyCard')?.closest('label'),phone=$('phone')?.closest('label');
|
||
(loyalty||phone)?.insertAdjacentElement('afterend',label);if(!label.isConnected)window.SunSafe.insertBefore(grid,label,grid.firstChild);
|
||
select=label.querySelector('select');select.addEventListener('change',()=>{const d=getDraft();if(d)d.leadSource=select.value});
|
||
}
|
||
setSelectOptions(select,readSources(),getDraft()?.leadSource||select.value);return select;
|
||
}
|
||
function refreshSourceSelect(){const s=ensureSourceField();if(s)setSelectOptions(s,readSources(),getDraft()?.leadSource||s.value)}
|
||
|
||
function ensureSettingsCard(){
|
||
const grid=document.querySelector('#enterprise-settings .enterprise-grid');if(!grid)return;
|
||
let card=$('sunLeadSourcesSettingsCard');
|
||
if(!card){card=document.createElement('section');card.id='sunLeadSourcesSettingsCard';card.className='enterprise-card';card.innerHTML=`<h2>Источники клиентов</h2><p class="hint">Список для поля «Узнали из» в деталях заказа. Эти данные используются в статистике.</p><div class="sun-lead-settings-list" id="sunLeadSourcesRows"></div><div class="actions"><button class="outline" id="sunLeadSourceAdd" type="button">Добавить источник</button><button class="primary" id="sunLeadSourcesSave" type="button">Сохранить список</button></div>`;
|
||
const eventCard=$('sunEventTypesSettingsCard');if(eventCard)eventCard.insertAdjacentElement('afterend',card);else grid.appendChild(card);
|
||
$('sunLeadSourceAdd').onclick=()=>addSourceSettingsRow('');$('sunLeadSourcesSave').onclick=()=>saveSources(qa('#sunLeadSourcesRows input').map(x=>x.value));
|
||
}
|
||
const rows=$('sunLeadSourcesRows');if(rows&&!rows.dataset.ready){rows.dataset.ready='1';renderSourceSettingsRows()}
|
||
}
|
||
function addSourceSettingsRow(value=''){
|
||
const rows=$('sunLeadSourcesRows');if(!rows)return;const row=document.createElement('div');row.className='sun-lead-settings-row';row.innerHTML=`<input value="${esc(value)}" placeholder="Название источника"><button class="outline" type="button">Убрать</button>`;row.querySelector('button').onclick=()=>row.remove();rows.appendChild(row);row.querySelector('input').focus();
|
||
}
|
||
function renderSourceSettingsRows(){const rows=$('sunLeadSourcesRows');if(!rows)return;rows.innerHTML='';readSources().forEach(addSourceSettingsRow)}
|
||
|
||
function normalizePhone(raw){
|
||
let d=String(raw||'').replace(/\D/g,'');if(d.length===11&&d[0]==='8')d='7'+d.slice(1);else if(d.length===10)d='7'+d;return d;
|
||
}
|
||
function orderStamp(o){return `${o?.date||''}T${o?.time||''}|${String(o?.id||'').padStart(12,'0')}`}
|
||
function structuredAddress(o){
|
||
const keys=['deliveryRegion','deliveryLocality','deliveryDistrict','deliveryStreet','deliveryHouse','deliveryCorpus','deliveryEntrance','deliveryFloor','deliveryApartment','deliveryOffice'];
|
||
const data={};keys.forEach(k=>data[k]=String(o?.[k]||'').trim());data.address=String(o?.address||'').trim();
|
||
data.structured=keys.some(k=>data[k]);return data;
|
||
}
|
||
function profileFromOrders(list){
|
||
const sorted=list.slice().sort((a,b)=>orderStamp(b).localeCompare(orderStamp(a)));if(!sorted.length)return null;
|
||
const latest=sorted[0],seen=new Set(),addresses=[];
|
||
sorted.forEach(o=>{const a=structuredAddress(o),key=a.structured?JSON.stringify(a):a.address;if(key&&!seen.has(key)){seen.add(key);addresses.push(a)}});
|
||
return {name:String(latest.contact||''),phone:String(latest.phone||''),ordersCount:sorted.length,latest,addresses};
|
||
}
|
||
function profileByPhone(raw){const n=normalizePhone(raw);if(n.length<10)return null;return profileFromOrders(getOrders().filter(o=>normalizePhone(o.phone)===n))}
|
||
function readLoyaltyStore(){try{const v=JSON.parse(localStorage.getItem('sunClientLoyaltyV1')||'{}');return v&&typeof v==='object'?v:{}}catch(_){return{}}}
|
||
function profileByCard(raw){
|
||
const card=String(raw||'').trim().toLowerCase();if(!card)return null;
|
||
let direct=getOrders().filter(o=>String(o.loyaltyCardNumber||o.loyaltyCard||'').trim().toLowerCase()===card);if(direct.length)return profileFromOrders(direct);
|
||
const store=readLoyaltyStore(),entry=Object.entries(store).find(([,v])=>String(v?.cardNumber||'').trim().toLowerCase()===card);if(!entry)return null;
|
||
const key=entry[0];if(key.startsWith('p:'))return profileByPhone(key.slice(2));
|
||
if(key.startsWith('n:')){const name=key.slice(2).trim().toLowerCase();return profileFromOrders(getOrders().filter(o=>String(o.contact||'').trim().toLowerCase()===name))}
|
||
return null;
|
||
}
|
||
const addrIds=['deliveryRegion','deliveryLocality','deliveryDistrict','deliveryStreet','deliveryHouse','deliveryCorpus','deliveryEntrance','deliveryFloor','deliveryApartment','deliveryOffice'];
|
||
function setSelectValue(el,value){if(!el)return;const v=String(value||'');if(v&&el.tagName==='SELECT'&&![...el.options].some(o=>o.value===v))el.add(new Option(v,v));el.value=v}
|
||
function composeFullAddress(){
|
||
const val=id=>String($(id)?.value||'').trim();const address=$('address');if(!address)return'';
|
||
const parts=[val('deliveryRegion'),val('deliveryLocality'),val('deliveryDistrict'),val('deliveryStreet'),val('deliveryHouse')?`д. ${val('deliveryHouse')}`:'',val('deliveryCorpus')?`корп. ${val('deliveryCorpus')}`:'',val('deliveryEntrance')?`подъезд ${val('deliveryEntrance')}`:'',val('deliveryFloor')?`этаж ${val('deliveryFloor')}`:'',val('deliveryApartment')?`кв. ${val('deliveryApartment')}`:'',val('deliveryOffice')?`офис ${val('deliveryOffice')}`:''].filter(Boolean);
|
||
const full=parts.join(', ');address.value=full;const d=getDraft();if(d){addrIds.forEach(id=>d[id]=val(id));d.address=full}return full;
|
||
}
|
||
window.sunComposePreciseDeliveryAddress=composeFullAddress;
|
||
function fillAddress(a){
|
||
if(!a)return;const has=Boolean(a.structured);
|
||
addrIds.forEach(id=>{const el=$(id);if(!el)return;const v=has?String(a[id]||''):(id==='deliveryStreet'?String(a.address||''):'');if(id==='deliveryRegion')setSelectValue(el,v);else el.value=v});
|
||
composeFullAddress();resetGeoConfirmation(true);
|
||
}
|
||
function ensureClientUi(){
|
||
const grid=document.querySelector('#details .sun-delivery-details');if(!grid)return;
|
||
if(!$('sunClientMatch')){const box=document.createElement('div');box.id='sunClientMatch';box.className='sun-client-match';const lead=$('orderLeadSource')?.closest('label');(lead||grid.firstElementChild)?.insertAdjacentElement('afterend',box);if(!box.isConnected)window.SunSafe.insertBefore(grid,box,grid.firstChild)}
|
||
if(!$('sunKnownAddresses')){const label=document.createElement('label');label.className='sun-known-addresses-field';label.style.display='none';label.innerHTML='<span>Предыдущие адреса клиента</span><select id="sunKnownAddresses"></select>';const office=$('deliveryOffice')?.closest('label');office?.insertAdjacentElement('afterend',label);if(!label.isConnected)grid.appendChild(label);label.querySelector('select').onchange=e=>{const a=e.target._addresses?.[Number(e.target.value)];if(a)fillAddress(a)}}
|
||
}
|
||
function showProfile(profile,reason){
|
||
ensureClientUi();const box=$('sunClientMatch'),known=$('sunKnownAddresses'),knownLabel=known?.closest('label');if(!profile){box?.classList.remove('on');if(knownLabel)knownLabel.style.display='none';return false}
|
||
const contact=$('contact'),phone=$('phone');if(contact&&profile.name)contact.value=profile.name;if(phone&&profile.phone)phone.value=profile.phone;
|
||
if(profile.addresses[0])fillAddress(profile.addresses[0]);
|
||
if(known&&knownLabel){known._addresses=profile.addresses;known.innerHTML=profile.addresses.map((a,i)=>`<option value="${i}">${esc(a.address||[a.deliveryLocality,a.deliveryStreet,a.deliveryHouse].filter(Boolean).join(', ')||`Адрес ${i+1}`)}</option>`).join('');knownLabel.style.display=profile.addresses.length>1?'flex':'none'}
|
||
if(box){box.innerHTML=`Клиент найден по ${reason}: <b>${esc(profile.name||profile.phone||'клиент')}</b> · заказов: ${profile.ordersCount}${profile.addresses.length>1?` · сохранено адресов: ${profile.addresses.length}`:''}. Данные заполнены автоматически, адрес можно изменить.`;box.classList.add('on')}
|
||
const d=getDraft();if(d){d.contact=contact?.value||'';d.phone=phone?.value||''}return true;
|
||
}
|
||
let lookupTimer=0;
|
||
function schedulePhoneLookup(){clearTimeout(lookupTimer);lookupTimer=setTimeout(()=>{const d=getDraft();if(d?.id)return;const p=profileByPhone($('phone')?.value);if(p)showProfile(p,'телефону')},260)}
|
||
function lookupCard(){const d=getDraft();if(d?.id)return;const field=$('orderLoyaltyCard'),p=profileByCard(field?.value);if(p)showProfile(p,'карте лояльности')}
|
||
|
||
function totalFor(o){if(Number.isFinite(Number(o?.total))&&Number(o.total)>0)return Number(o.total);try{return typeof window.orderTotalValue==='function'?Number(window.orderTotalValue(o)||0):0}catch(_){return 0}}
|
||
function statsPeriodOrders(){
|
||
const period=$('statsPeriod')?.value||'year',now=new Date();now.setHours(12,0,0,0);let from=null,to=null;
|
||
if(period==='month'){from=new Date(now.getFullYear(),now.getMonth(),1,12);to=new Date(now.getFullYear(),now.getMonth()+1,1,12)}
|
||
else if(period==='90'){from=new Date(now);from.setDate(from.getDate()-89);to=new Date(now);to.setDate(to.getDate()+1)}
|
||
else if(period==='year'){from=new Date(now.getFullYear(),now.getMonth()-11,1,12);to=new Date(now.getFullYear(),now.getMonth()+1,1,12)}
|
||
return getOrders().filter(o=>{if(String(o.status||'Новый')==='Отменён')return false;if(!from)return true;const d=o.date?new Date(`${o.date}T12:00:00`):null;return d&&!Number.isNaN(d.getTime())&&d>=from&&d<to});
|
||
}
|
||
function groupedStats(list,keyFn){
|
||
const m=new Map();list.forEach(o=>{const name=String(keyFn(o)||'').trim()||'Не указано',r=m.get(name)||{name,count:0,sum:0};r.count++;r.sum+=totalFor(o);m.set(name,r)});return [...m.values()].map(r=>({...r,avg:r.count?r.sum/r.count:0,share:list.length?r.count/list.length*100:0})).sort((a,b)=>b.count-a.count||b.sum-a.sum||a.name.localeCompare(b.name,'ru'));
|
||
}
|
||
function statTable(rows){return rows.length?`<div style="overflow:auto"><table class="sun-crm-stats-table"><thead><tr><th>Категория</th><th>Заказов</th><th>Доля</th><th>Выручка</th><th class="sun-crm-hide-mobile">Средний чек</th></tr></thead><tbody>${rows.map(r=>`<tr><td><b>${esc(r.name)}</b></td><td>${r.count}</td><td>${r.share.toLocaleString('ru-RU',{maximumFractionDigits:1})}%</td><td>${esc(money(r.sum))}</td><td class="sun-crm-hide-mobile">${esc(money(r.avg))}</td></tr>`).join('')}</tbody></table></div>`:'<div class="stats-empty">Данных пока нет.</div>'}
|
||
function renderCrmStats(){
|
||
const grid=$('statsList')?.querySelector('.stats-grid');if(!grid)return;grid.querySelectorAll('.sun-crm-stats-panel').forEach(x=>x.remove());const list=statsPeriodOrders();
|
||
const eventPanel=document.createElement('section');eventPanel.className='stats-panel wide sun-crm-stats-panel';eventPanel.innerHTML=`<div class="stats-panel-head"><h2>Типы мероприятий</h2><small>количество · доля · выручка · средний чек</small></div>${statTable(groupedStats(list,o=>o.event))}`;
|
||
const sourcePanel=document.createElement('section');sourcePanel.className='stats-panel wide sun-crm-stats-panel';sourcePanel.innerHTML=`<div class="stats-panel-head"><h2>Откуда приходят заказы</h2><small>по полю «Узнали из»</small></div>${statTable(groupedStats(list,o=>o.leadSource))}`;
|
||
grid.append(eventPanel,sourcePanel);
|
||
}
|
||
|
||
function resetGeoConfirmation(clear=false){const d=getDraft();if(d?.geo){if(clear)d.geo=null;else{delete d.geo.confirmed;delete d.geo.confirmedAt}}syncGeoConfirmUi()}
|
||
function ensureGeoConfirmUi(){
|
||
const box=$('orderGeoBox');if(!box)return null;let ui=$('sunGeoConfirm');if(!ui){ui=document.createElement('div');ui.id='sunGeoConfirm';ui.className='sun-geo-confirm';ui.innerHTML='<strong id="sunGeoFoundTitle">Найденный адрес</strong><p id="sunGeoFoundText"></p><div class="actions"><button type="button" class="primary" id="sunGeoConfirmOk">Адрес верный</button><button type="button" class="outline" id="sunGeoConfirmEdit">Уточнить адрес</button></div>';box.appendChild(ui);$('sunGeoConfirmOk').onclick=()=>{const d=getDraft();if(!d?.geo)return;d.geo.confirmed=true;d.geo.confirmedAt=new Date().toISOString();if(d.id){const saved=getOrders().find(o=>String(o.id)===String(d.id));if(saved)saved.geo=structuredClone(d.geo);try{persist()}catch(_){}}syncGeoConfirmUi();toast('Адрес подтверждён.','success')};$('sunGeoConfirmEdit').onclick=()=>{const d=getDraft();if(d)d.geo=null;const locality=$('deliveryLocality')||$('deliveryStreet');locality?.focus();syncGeoConfirmUi()}}
|
||
return ui;
|
||
}
|
||
function syncGeoConfirmUi(){
|
||
const ui=ensureGeoConfirmUi(),d=getDraft();if(!ui)return;const geo=d?.geo;if(!geo||!Number.isFinite(Number(geo.lat))||!Number.isFinite(Number(geo.lon))){ui.classList.remove('on','ok');return}ui.classList.add('on');ui.classList.toggle('ok',geo.confirmed===true);$('sunGeoFoundTitle').textContent=geo.confirmed?'Адрес подтверждён':'Проверьте найденный адрес';$('sunGeoFoundText').textContent=String(geo.formatted||d.address||'');$('sunGeoConfirmOk').style.display=geo.confirmed?'none':'';
|
||
}
|
||
|
||
function persistPreciseSaved(){
|
||
const d=getDraft();if(!d)return;const full=composeFullAddress();if(!d.id)return;const saved=getOrders().find(o=>String(o.id)===String(d.id));if(!saved)return;addrIds.forEach(id=>saved[id]=String($(id)?.value||'').trim());saved.address=full;saved.leadSource=$('orderLeadSource')?.value||'';if(d.geo)saved.geo=structuredClone(d.geo);try{persist()}catch(_){try{localStorage.setItem('sunOrders',JSON.stringify(getOrders()))}catch(__){}}
|
||
}
|
||
|
||
function init(){
|
||
ensureSourceField();ensureClientUi();ensureGeoConfirmUi();
|
||
const mapSensitive=new Set(['deliveryRegion','deliveryLocality','deliveryDistrict','deliveryStreet','deliveryHouse','deliveryCorpus']);
|
||
addrIds.forEach(id=>$(id)?.addEventListener('input',()=>setTimeout(()=>{composeFullAddress();if(mapSensitive.has(id))resetGeoConfirmation(true)},0)));
|
||
$('deliveryRegion')?.addEventListener('change',()=>setTimeout(()=>{composeFullAddress();resetGeoConfirmation(true)},0));
|
||
$('phone')?.addEventListener('input',schedulePhoneLookup);$('phone')?.addEventListener('blur',schedulePhoneLookup);
|
||
$('orderLoyaltyCard')?.addEventListener('input',()=>{clearTimeout(lookupTimer);lookupTimer=setTimeout(lookupCard,260)});$('orderLoyaltyCard')?.addEventListener('blur',lookupCard);
|
||
$('orderGeoCreate')?.addEventListener('click',()=>setTimeout(syncGeoConfirmUi,200));
|
||
const geoBox=$('orderGeoBox');if(geoBox)new MutationObserver(()=>setTimeout(syncGeoConfirmUi,0)).observe(geoBox,{childList:true,subtree:true,characterData:true});
|
||
const stats=$('statsList');if(stats)new MutationObserver(()=>setTimeout(renderCrmStats,0)).observe(stats,{childList:true,subtree:false});
|
||
$('statsPeriod')?.addEventListener('change',()=>setTimeout(renderCrmStats,0));renderCrmStats();
|
||
}
|
||
|
||
const priorSave=window.saveOrder;
|
||
if(typeof priorSave==='function')window.saveOrder=function(...args){
|
||
const d=getDraft(),source=ensureSourceField();if(d&&source)d.leadSource=source.value;composeFullAddress();
|
||
const street=String($('deliveryStreet')?.value||'').trim(),house=String($('deliveryHouse')?.value||'').trim(),locality=String($('deliveryLocality')?.value||'').trim();
|
||
if(street&&house&&!locality&&!confirm('Не указан город / населённый пункт. Похожие адреса могут существовать в разных местах. Сохранить заказ без уточнения?'))return false;
|
||
if(street&&house&&d?.geo&&d.geo.confirmed!==true&&!confirm('Точка уже найдена на карте, но адрес не подтверждён. Сохранить без подтверждения?'))return false;
|
||
const r=priorSave.apply(this,args),finish=()=>{persistPreciseSaved();renderCrmStats();syncGeoConfirmUi();return undefined};if(r&&typeof r.then==='function')return r.then(v=>{finish();return v});finish();return r;
|
||
};
|
||
const priorOpen=window.openOrder;
|
||
if(typeof priorOpen==='function')window.openOrder=function(...args){const r=priorOpen.apply(this,args);setTimeout(()=>{const s=ensureSourceField();if(s)s.value=String(getDraft()?.leadSource||'');composeFullAddress();ensureClientUi();syncGeoConfirmUi();$('sunClientMatch')?.classList.remove('on');const l=$('sunKnownAddresses')?.closest('label');if(l)l.style.display='none'},0);return r};
|
||
const priorReset=window.resetDraft;
|
||
if(typeof priorReset==='function')window.resetDraft=function(...args){const r=priorReset.apply(this,args);setTimeout(()=>{const s=ensureSourceField();if(s)s.value='';$('sunClientMatch')?.classList.remove('on');const l=$('sunKnownAddresses')?.closest('label');if(l)l.style.display='none';syncGeoConfirmUi()},0);return r};
|
||
const priorStats=window.renderStats;
|
||
if(typeof priorStats==='function')window.renderStats=function(...args){const r=priorStats.apply(this,args);setTimeout(renderCrmStats,0);return r};
|
||
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const label=String(b.dataset.navLabel||b.textContent||'').trim();if(label==='Настройки')setTimeout(()=>{ensureSettingsCard();renderSourceSettingsRows()},90);if(label==='Статистика')setTimeout(renderCrmStats,80)},true);
|
||
const settings=$('enterprise-settings');if(settings)new MutationObserver(()=>{if(settings.classList.contains('on'))setTimeout(ensureSettingsCard,0)}).observe(settings,{childList:true,subtree:true});
|
||
window.addEventListener('sun:cloud-state-applied',()=>setTimeout(()=>{refreshSourceSelect();renderCrmStats()},50));
|
||
init();setTimeout(()=>{if($('enterprise-settings')?.classList.contains('on'))ensureSettingsCard();syncGeoConfirmUi()},200);
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: brand-theme-v1.js ===== */
|
||
/* Sun Catering theme editor v4: simple color presets, staged preview, reliable sidebar apply */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunBrandThemeV4)return;window.__sunBrandThemeV4=true;
|
||
const KEY='sunBrandThemeV1';
|
||
const BRAND={sun:'#FFC107',accent:'#FF8A00',text:'#1A1A1A',background:'#FFF7E6',surface:'#F3E8D3',fresh:'#A8C783',success:'#6D8F6A',trust:'#6C7A89',warm:'#C7744D',border:'#E7E7E7'};
|
||
const UI_FIRM={background:'#FFF7E6',card:'#FFFFFF',primary:'#1A1A1A',accent:'#FFC107',text:'#1A1A1A',muted:'#6C7A89',border:'#E7E7E7',input:'#FFFFFF',buttonText:'#FFFFFF'};
|
||
const UI_STANDARD={background:'#F5F6FA',card:'#FFFFFF',primary:'#15364C',accent:'#C99A32',text:'#203040',muted:'#68717A',border:'#D9DDE0',input:'#FFFFFF',buttonText:'#FFFFFF'};
|
||
const SIDEBAR_STANDARD={background:'#32255D',background2:'#171D38',glow:'#9574FF',border:'#4B3C77',icons:'#E9E8F6',iconBackground:'#3A2F68',text:'#E9E8F6',muted:'#A9A8C6',activeBackground:'#463A74',activeText:'#FFFFFF',activeIcon:'#FFE48D',indicator:'#E4B844',toolsBackground:'#3A315F',toolsBorder:'#544A7B',collapseBackground:'#FFFFFF',collapseText:'#433D68',badgeBackground:'#FF6C7C',badgeText:'#FFFFFF'};
|
||
const SIDEBAR_FIRM={background:'#1A1A1A',background2:'#2A2A2A',glow:'#FFC107',border:'#3A372F',icons:'#FFC107',iconBackground:'#2A281D',text:'#FFF7E6',muted:'#C9B98E',activeBackground:'#34301F',activeText:'#FFC107',activeIcon:'#FFC107',indicator:'#FF8A00',toolsBackground:'#242424',toolsBorder:'#414141',collapseBackground:'#FFFFFF',collapseText:'#1A1A1A',badgeBackground:'#C7744D',badgeText:'#FFFFFF'};
|
||
const SIDEBAR_LIGHT={background:'#FFF7E6',background2:'#F3E8D3',glow:'#FFC107',border:'#E2D5BC',icons:'#C7744D',iconBackground:'#FFFDF9',text:'#1A1A1A',muted:'#6C7A89',activeBackground:'#FFFFFF',activeText:'#C7744D',activeIcon:'#C7744D',indicator:'#FF8A00',toolsBackground:'#FFFFFF',toolsBorder:'#E2D5BC',collapseBackground:'#1A1A1A',collapseText:'#FFFFFF',badgeBackground:'#C7744D',badgeText:'#FFFFFF'};
|
||
const SIDEBAR_EMERALD={background:'#123C30',background2:'#071F19',glow:'#FFC107',border:'#285849',icons:'#FFC107',iconBackground:'#173E33',text:'#FFF7E6',muted:'#A8C783',activeBackground:'#1C4C3D',activeText:'#FFF7E6',activeIcon:'#FFC107',indicator:'#FF8A00',toolsBackground:'#16382F',toolsBorder:'#2B5A4B',collapseBackground:'#FFF7E6',collapseText:'#123C30',badgeBackground:'#C7744D',badgeText:'#FFFFFF'};
|
||
const SIDEBAR_SUNSET={background:'#49301B',background2:'#1A1A1A',glow:'#FF8A00',border:'#69462A',icons:'#FFC107',iconBackground:'#3A2C1E',text:'#FFF7E6',muted:'#D6BD91',activeBackground:'#62401D',activeText:'#FFF7E6',activeIcon:'#FFC107',indicator:'#FF8A00',toolsBackground:'#34281F',toolsBorder:'#5A4632',collapseBackground:'#FFF7E6',collapseText:'#49301B',badgeBackground:'#C7744D',badgeText:'#FFFFFF'};
|
||
const UI_LABELS={background:'Фон приложения',card:'Карточки и окна',primary:'Основные кнопки',accent:'Акцент / обводка',text:'Основной текст',muted:'Вторичный текст',border:'Границы',input:'Поля ввода',buttonText:'Текст основных кнопок'};
|
||
const SIDE_LABELS={background:'Основной цвет панели',background2:'Второй цвет / низ панели',glow:'Свечение панели',border:'Граница панели',icons:'Цвет значков',iconBackground:'Фон значков',text:'Основной текст панели',muted:'Подписи групп',activeBackground:'Фон активного пункта',activeText:'Текст активного пункта',activeIcon:'Активный значок',indicator:'Полоса активного пункта',toolsBackground:'Поиск / уведомления',toolsBorder:'Граница служебных кнопок',collapseBackground:'Кнопка свернуть',collapseText:'Стрелка сворачивания',badgeBackground:'Уведомление',badgeText:'Текст уведомления'};
|
||
const BRAND_LABELS={sun:'Солнечный',accent:'Оранжевый',text:'Графит',background:'Кремовый фон',surface:'Бежевый',fresh:'Свежий зелёный',success:'Фирменный зелёный',trust:'Серо-синий',warm:'Терракота',border:'Светлая граница'};
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const clone=o=>JSON.parse(JSON.stringify(o));
|
||
const isHex=v=>/^#[0-9a-f]{6}$/i.test(String(v||'').trim());
|
||
const norm=(v,f)=>isHex(v)?String(v).toUpperCase():f;
|
||
const hexRgba=(hex,a=.25)=>{const h=norm(hex,'#000000').slice(1);return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`};
|
||
const defaults=()=>({version:4,palette:{...BRAND},ui:{...UI_FIRM},sidebar:{...SIDEBAR_FIRM}});
|
||
const standardState=()=>({version:4,palette:{...BRAND},ui:{...UI_STANDARD},sidebar:{...SIDEBAR_STANDARD}});
|
||
const presetState=name=>{
|
||
if(name==='standard')return standardState();
|
||
const sidebar=name==='light'?SIDEBAR_LIGHT:name==='emerald'?SIDEBAR_EMERALD:name==='sunset'?SIDEBAR_SUNSET:SIDEBAR_FIRM;
|
||
return{version:4,palette:{...BRAND},ui:{...UI_FIRM},sidebar:{...sidebar}};
|
||
};
|
||
function read(){
|
||
let raw={};try{raw=JSON.parse(localStorage.getItem(KEY)||'{}')||{}}catch(_){raw={}}
|
||
const base=defaults(),palette={...base.palette,...(raw.palette||{})},ui={...base.ui,...(raw.ui||{})},sidebar={...base.sidebar,...(raw.sidebar||{})};
|
||
Object.keys(BRAND).forEach(k=>palette[k]=norm(palette[k],BRAND[k]));
|
||
Object.keys(UI_FIRM).forEach(k=>ui[k]=norm(ui[k],UI_FIRM[k]));
|
||
Object.keys(SIDEBAR_FIRM).forEach(k=>sidebar[k]=norm(sidebar[k],SIDEBAR_FIRM[k]));
|
||
if(String(ui.buttonText).toUpperCase()===String(ui.primary).toUpperCase())ui.buttonText='#FFFFFF';
|
||
return{version:4,palette,ui,sidebar};
|
||
}
|
||
let savedState=read(),previewState=clone(savedState),dirty=false;
|
||
const persist=()=>localStorage.setItem(KEY,JSON.stringify(savedState));
|
||
|
||
const style=document.createElement('style');style.id='sun-brand-theme-style-v4';style.textContent=`
|
||
:root{--sun-ui-bg:#FFF7E6;--sun-ui-card:#fff;--sun-ui-primary:#1A1A1A;--sun-ui-accent:#FFC107;--sun-ui-text:#1A1A1A;--sun-ui-muted:#6C7A89;--sun-ui-border:#E7E7E7;--sun-ui-input:#fff;--sun-ui-button-text:#fff;--sun-sidebar-bg:#32255D;--sun-sidebar-bg2:#171D38;--sun-sidebar-glow:#9574FF;--sun-sidebar-border:#4B3C77;--sun-sidebar-icons:#E9E8F6;--sun-sidebar-icon-bg:#3A2F68;--sun-sidebar-text:#E9E8F6;--sun-sidebar-muted:#A9A8C6;--sun-sidebar-active-bg:#463A74;--sun-sidebar-active-text:#fff;--sun-sidebar-active-icon:#FFE48D;--sun-sidebar-indicator:#E4B844;--sun-sidebar-tools-bg:#3A315F;--sun-sidebar-tools-border:#544A7B;--sun-sidebar-collapse-bg:#fff;--sun-sidebar-collapse-text:#433D68;--sun-sidebar-badge-bg:#FF6C7C;--sun-sidebar-badge-text:#fff}
|
||
body{background:var(--sun-ui-bg)!important;color:var(--sun-ui-text)!important}:root{--n:var(--sun-ui-primary)!important;--g:var(--sun-ui-accent)!important;--p:var(--sun-ui-bg)!important;--l:var(--sun-ui-border)!important}
|
||
.card,.page,.enterprise-card,.dialog,.sun-order-lines-table,.dash3-panel{background:var(--sun-ui-card)!important;color:var(--sun-ui-text)!important;border-color:var(--sun-ui-border)!important}.page h1,.catalog h1,.enterprise-card h2,.enterprise-card h3{color:var(--sun-ui-primary)!important}.hint,label,.enterprise-card .hint{color:var(--sun-ui-muted)!important}input,select,textarea{background:var(--sun-ui-input)!important;color:var(--sun-ui-text)!important;border-color:var(--sun-ui-border)!important}
|
||
button.primary,.primary{background:var(--sun-ui-primary)!important;border-color:var(--sun-ui-primary)!important;color:var(--sun-ui-button-text)!important}.primary svg,.primary *{color:inherit!important}.outline{border-color:var(--sun-ui-accent)!important;color:var(--sun-ui-primary)!important;background:var(--sun-ui-card)!important}
|
||
@media(min-width:901px){.sun-enterprise-sidebar header .brand{color:var(--sun-sidebar-text)!important;border-bottom-color:color-mix(in srgb,var(--sun-sidebar-text) 12%,transparent)!important}.sun-enterprise-sidebar .sun-nav-group{border-bottom-color:color-mix(in srgb,var(--sun-sidebar-text) 10%,transparent)!important}.sun-enterprise-sidebar .sun-nav-group::before{color:var(--sun-sidebar-muted)!important}.sun-enterprise-sidebar header nav .sun-nav-button{color:var(--sun-sidebar-text)!important}.sun-enterprise-sidebar header nav .sun-nav-button::before{background:var(--sun-sidebar-icons)!important;color:var(--sun-sidebar-icons)!important}.sun-enterprise-sidebar header nav .sun-nav-button:hover{background:color-mix(in srgb,var(--sun-sidebar-active-bg) 72%,transparent)!important;color:var(--sun-sidebar-text)!important}.sun-enterprise-sidebar header nav .sun-nav-button.on{background:var(--sun-sidebar-active-bg)!important;color:var(--sun-sidebar-active-text)!important}.sun-enterprise-sidebar header nav .sun-nav-button.on::before{background:var(--sun-sidebar-active-icon)!important;color:var(--sun-sidebar-active-icon)!important}.sun-enterprise-sidebar header nav .sun-nav-button.on::after{background:var(--sun-sidebar-indicator)!important;box-shadow:0 0 10px color-mix(in srgb,var(--sun-sidebar-indicator) 42%,transparent)!important}.sun-side-tools .sun-side-action{color:var(--sun-sidebar-text)!important;border-color:var(--sun-sidebar-tools-border)!important;background:var(--sun-sidebar-tools-bg)!important}.sun-current-user,#sunLogoutBtn{color:var(--sun-sidebar-muted)!important}.sun-sidebar-collapse{background:var(--sun-sidebar-collapse-bg)!important;color:var(--sun-sidebar-collapse-text)!important;border-color:var(--sun-sidebar-border)!important}#sunNotificationsBtn .badge{background:var(--sun-sidebar-badge-bg)!important;color:var(--sun-sidebar-badge-text)!important;border-color:var(--sun-sidebar-bg)!important}}
|
||
@media(max-width:900px){.sun-enterprise-sidebar header,.sun-enterprise-sidebar header nav{background:linear-gradient(165deg,var(--sun-sidebar-bg),var(--sun-sidebar-bg2))!important;color:var(--sun-sidebar-text)!important}.sun-enterprise-sidebar header nav .sun-nav-button{color:var(--sun-sidebar-text)!important}.sun-enterprise-sidebar header nav .sun-nav-button::before{background:var(--sun-sidebar-icons)!important}.sun-enterprise-sidebar header nav .sun-nav-button.on{background:var(--sun-sidebar-active-bg)!important;color:var(--sun-sidebar-active-text)!important}.sun-enterprise-sidebar header nav .sun-nav-button.on::before{background:var(--sun-sidebar-active-icon)!important}}
|
||
.sun-brand-settings-card{overflow:hidden}.sun-theme-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:11px 0 14px}.sun-theme-sections{display:grid;gap:13px}.sun-theme-box{border:1px solid var(--sun-ui-border);border-radius:12px;padding:13px;background:color-mix(in srgb,var(--sun-ui-card) 96%,var(--sun-ui-bg))}.sun-theme-box h3{margin:0 0 9px}.sun-theme-panel-main{display:grid;grid-template-columns:minmax(250px,.8fr) minmax(340px,1.2fr);gap:14px;align-items:start}.sun-theme-preview{min-height:190px;border-radius:16px;background:radial-gradient(circle at 10% 0,color-mix(in srgb,var(--sun-sidebar-glow) 35%,transparent),transparent 35%),linear-gradient(150deg,var(--sun-sidebar-bg),var(--sun-sidebar-bg2));padding:15px;color:var(--sun-sidebar-text);border:1px solid var(--sun-sidebar-border)}.sun-theme-preview-head{display:flex;align-items:center;gap:10px;padding-bottom:12px;border-bottom:1px solid color-mix(in srgb,var(--sun-sidebar-text) 13%,transparent)}.sun-theme-preview-logo{width:35px;height:35px;border-radius:50%;background:var(--sun-sidebar-icons);box-shadow:0 0 18px color-mix(in srgb,var(--sun-sidebar-glow) 45%,transparent)}.sun-theme-preview-menu{display:grid;gap:7px;margin-top:13px}.sun-theme-preview-row{height:38px;border-radius:10px;display:flex;align-items:center;gap:10px;padding:0 10px;color:var(--sun-sidebar-text)}.sun-theme-preview-row.on{background:var(--sun-sidebar-active-bg);color:var(--sun-sidebar-active-text);box-shadow:inset 3px 0 0 var(--sun-sidebar-indicator)}.sun-theme-preview-row i{width:25px;height:25px;display:grid;place-items:center;color:var(--sun-sidebar-icons);font-style:normal}.sun-theme-preview-row.on i{color:var(--sun-sidebar-active-icon)}.sun-theme-preview small{color:var(--sun-sidebar-muted)}
|
||
.sun-sidebar-core-colors{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-bottom:12px}.sun-brand-setting-grid{display:grid;grid-template-columns:repeat(3,minmax(185px,1fr));gap:8px}.sun-brand-color-field{display:grid;grid-template-columns:34px minmax(0,1fr) 82px;gap:7px;align-items:center;padding:7px 8px;border:1px solid var(--sun-ui-border);border-radius:9px;background:var(--sun-ui-card)}.sun-brand-color-field label{font-size:10.5px;line-height:1.2}.sun-brand-color-field input[type=color]{width:32px!important;height:30px!important;padding:2px!important;border:1px solid var(--sun-ui-border)!important;border-radius:7px!important}.sun-brand-color-field input[type=text]{width:82px!important;height:30px!important;padding:4px 6px!important;border-radius:7px!important;font:700 10px ui-monospace,monospace!important;text-transform:uppercase}
|
||
.sun-brand-palette-toggle{margin-top:10px}.sun-brand-palette-toggle summary{cursor:pointer;font-weight:750;color:var(--sun-ui-primary)}.sun-brand-palette{display:grid;grid-template-columns:repeat(5,minmax(90px,1fr));gap:8px;margin-top:10px}.sun-brand-swatch{border:1px solid var(--sun-ui-border);border-radius:10px;background:var(--sun-ui-card);overflow:hidden}.sun-brand-swatch-top{height:38px;background:var(--swatch)}.sun-brand-swatch-body{padding:6px 7px}.sun-brand-swatch-body b{display:block;font-size:10px}.sun-brand-swatch-body small{font:700 9px ui-monospace,monospace;color:var(--sun-ui-muted)}.sun-theme-savebar{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:15px -13px -13px;padding:12px 13px;background:color-mix(in srgb,var(--sun-ui-card) 94%,transparent);border-top:1px solid var(--sun-ui-border);backdrop-filter:blur(10px)}.sun-theme-savebar .actions{display:flex;gap:8px;flex-wrap:wrap}.sun-theme-dirty{font-size:11px;color:var(--sun-ui-muted)}.sun-theme-dirty strong{color:var(--sun-ui-accent)}
|
||
header .brand.sun-brand-home-link{cursor:pointer!important;border-radius:10px;transition:background .15s ease}.sun-enterprise-sidebar header .brand.sun-brand-home-link:hover{background:rgba(255,255,255,.06)!important}.sun-brand-home-link:focus-visible{outline:2px solid var(--sun-sidebar-indicator);outline-offset:2px}
|
||
@media(max-width:1100px){.sun-brand-setting-grid{grid-template-columns:repeat(2,minmax(180px,1fr))}}@media(max-width:760px){.sun-theme-panel-main,.sun-sidebar-core-colors,.sun-brand-setting-grid{grid-template-columns:1fr}.sun-brand-palette{grid-template-columns:repeat(2,1fr)}.sun-theme-savebar{align-items:flex-start;flex-direction:column}}
|
||
`;document.head.appendChild(style);
|
||
|
||
function applyActual(s=savedState){
|
||
document.body.classList.remove('sun-custom-sidebar-icons');
|
||
const root=document.documentElement.style;
|
||
Object.entries(s.palette).forEach(([k,v])=>root.setProperty(`--sun-brand-${k}`,v));
|
||
Object.entries(s.ui).forEach(([k,v])=>root.setProperty(`--sun-ui-${k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase())}`,v));
|
||
const map={background:'--sun-sidebar-bg',background2:'--sun-sidebar-bg2',glow:'--sun-sidebar-glow',border:'--sun-sidebar-border',icons:'--sun-sidebar-icons',iconBackground:'--sun-sidebar-icon-bg',text:'--sun-sidebar-text',muted:'--sun-sidebar-muted',activeBackground:'--sun-sidebar-active-bg',activeText:'--sun-sidebar-active-text',activeIcon:'--sun-sidebar-active-icon',indicator:'--sun-sidebar-indicator',toolsBackground:'--sun-sidebar-tools-bg',toolsBorder:'--sun-sidebar-tools-border',collapseBackground:'--sun-sidebar-collapse-bg',collapseText:'--sun-sidebar-collapse-text',badgeBackground:'--sun-sidebar-badge-bg',badgeText:'--sun-sidebar-badge-text'};
|
||
Object.entries(map).forEach(([k,v])=>root.setProperty(v,s.sidebar[k]));
|
||
const header=document.querySelector('header');
|
||
if(header){
|
||
header.style.setProperty('background',`radial-gradient(circle at 12% 0%,${hexRgba(s.sidebar.glow,.30)},transparent 31%),linear-gradient(165deg,${s.sidebar.background} 0%,${s.sidebar.background2} 100%)`,'important');
|
||
header.style.setProperty('border-right',`1px solid ${s.sidebar.border}`,'important');
|
||
header.style.setProperty('box-shadow',`9px 0 34px ${hexRgba(s.sidebar.background,.22)}`,'important');
|
||
}
|
||
const nav=document.querySelector('header nav');
|
||
if(nav&&matchMedia('(max-width:900px)').matches)nav.style.setProperty('background',`linear-gradient(165deg,${s.sidebar.background},${s.sidebar.background2})`,'important');
|
||
else nav?.style.removeProperty('background');
|
||
window.dispatchEvent(new CustomEvent('sunbrandthemechange',{detail:clone(s)}));
|
||
}
|
||
function markDirty(){dirty=JSON.stringify(previewState)!==JSON.stringify(savedState);const el=document.querySelector('[data-theme-dirty]');if(el)el.innerHTML=dirty?'<strong>Есть несохранённые изменения</strong> · пока меняется только предпросмотр':'Все изменения сохранены'}
|
||
function field(scope,key,label,value){return`<div class="sun-brand-color-field" data-color-scope="${scope}" data-color-key="${key}"><input type="color" value="${esc(value)}" aria-label="${esc(label)}"><label>${esc(label)}</label><input type="text" value="${esc(value)}" maxlength="7" spellcheck="false" aria-label="HEX ${esc(label)}"></div>`}
|
||
function paletteHtml(){return Object.keys(BRAND).map(k=>`<div class="sun-brand-swatch" style="--swatch:${esc(BRAND[k])}"><div class="sun-brand-swatch-top"></div><div class="sun-brand-swatch-body"><b>${esc(BRAND_LABELS[k])}</b><small>${esc(BRAND[k])}</small></div></div>`).join('')}
|
||
function updatePreviewNode(card){
|
||
const p=card.querySelector('.sun-theme-preview');if(!p)return;
|
||
const s=previewState.sidebar;
|
||
const vars={background:'--sun-sidebar-bg',background2:'--sun-sidebar-bg2',glow:'--sun-sidebar-glow',border:'--sun-sidebar-border',icons:'--sun-sidebar-icons',iconBackground:'--sun-sidebar-icon-bg',text:'--sun-sidebar-text',muted:'--sun-sidebar-muted',activeBackground:'--sun-sidebar-active-bg',activeText:'--sun-sidebar-active-text',activeIcon:'--sun-sidebar-active-icon',indicator:'--sun-sidebar-indicator'};
|
||
Object.entries(vars).forEach(([k,v])=>p.style.setProperty(v,s[k]));
|
||
}
|
||
function cardHtml(){
|
||
return`<h2>Цвета интерфейса</h2><p class="hint">Выберите готовый вариант или настройте цвета вручную. Изменения сначала видны в примере. Реальная левая панель меняется только после «Сохранить настройки».</p>
|
||
<div class="sun-theme-toolbar"><button class="outline" type="button" data-theme-preset="standard">Стандартная фиолетовая</button><button class="outline" type="button" data-theme-preset="firm">Фирменная тёмная</button><button class="outline" type="button" data-theme-preset="light">Фирменная светлая</button><button class="outline" type="button" data-theme-preset="emerald">Фирменная зелёная</button><button class="outline" type="button" data-theme-preset="sunset">Солнечная графитовая</button></div>
|
||
<div class="sun-theme-sections">
|
||
<section class="sun-theme-box"><h3>Левая панель</h3><div class="sun-theme-panel-main"><div class="sun-theme-preview"><div class="sun-theme-preview-head"><div class="sun-theme-preview-logo"></div><div><b>Солнце</b><small>Предпросмотр боковой панели</small></div></div><div class="sun-theme-preview-menu"><div class="sun-theme-preview-row"><i>⌂</i>Главная</div><div class="sun-theme-preview-row on"><i>▤</i>Заказы</div><div class="sun-theme-preview-row"><i>⚙</i>Настройки</div></div></div><div><h3 style="margin-top:0">Главные цвета панели</h3><div class="sun-sidebar-core-colors">${field('sidebar','background','Основной цвет панели',previewState.sidebar.background)}${field('sidebar','background2','Низ / второй цвет',previewState.sidebar.background2)}${field('sidebar','icons','Цвет значков',previewState.sidebar.icons)}</div><p class="hint">Основной цвет — это фон всей большой левой панели. Для однотонной панели поставьте одинаковый цвет в первых двух полях.</p></div></div><details style="margin-top:12px"><summary><b>Дополнительные цвета панели</b></summary><div class="sun-brand-setting-grid" style="margin-top:10px">${Object.keys(SIDEBAR_FIRM).filter(k=>!['background','background2','icons'].includes(k)).map(k=>field('sidebar',k,SIDE_LABELS[k],previewState.sidebar[k])).join('')}</div></details></section>
|
||
<section class="sun-theme-box"><h3>Основной интерфейс</h3><div class="sun-brand-setting-grid">${Object.keys(UI_FIRM).map(k=>field('ui',k,UI_LABELS[k],previewState.ui[k])).join('')}</div><details class="sun-brand-palette-toggle"><summary>Показать фирменные цвета «Солнца»</summary><div class="sun-brand-palette">${paletteHtml()}</div></details></section>
|
||
</div><div class="sun-theme-savebar"><div class="sun-theme-dirty" data-theme-dirty>${dirty?'<strong>Есть несохранённые изменения</strong> · пока меняется только предпросмотр':'Все изменения сохранены'}</div><div class="actions"><button class="outline" type="button" data-theme-cancel>Отменить изменения</button><button class="danger" type="button" data-reset-standard>Вернуть стандартную версию</button><button class="primary" type="button" data-theme-save>Сохранить настройки</button></div></div>`;
|
||
}
|
||
function rerender(card){card.innerHTML=cardHtml();bind(card);updatePreviewNode(card)}
|
||
function bind(card){
|
||
card.querySelectorAll('[data-color-scope]').forEach(row=>{const scope=row.dataset.colorScope,key=row.dataset.colorKey,c=row.querySelector('input[type=color]'),t=row.querySelector('input[type=text]');const applyValue=v=>{const fallback=scope==='ui'?UI_FIRM[key]:SIDEBAR_FIRM[key];const val=norm(v,fallback);previewState[scope][key]=val;if(c)c.value=val;if(t)t.value=val;updatePreviewNode(card);markDirty()};c?.addEventListener('input',()=>applyValue(c.value));t?.addEventListener('input',()=>{if(isHex(t.value))applyValue(t.value)});t?.addEventListener('blur',()=>{if(!isHex(t.value))t.value=previewState[scope][key]})});
|
||
card.querySelectorAll('[data-theme-preset]').forEach(b=>b.addEventListener('click',()=>{previewState=presetState(b.dataset.themePreset);markDirty();rerender(card)}));
|
||
card.querySelector('[data-theme-save]')?.addEventListener('click',()=>{savedState=clone(previewState);savedState.version=4;persist();dirty=false;applyActual(savedState);rerender(card);window.SunEnterprise?.toast?.('Настройки цветов сохранены.','success')});
|
||
card.querySelector('[data-theme-cancel]')?.addEventListener('click',()=>{previewState=clone(savedState);dirty=false;rerender(card)});
|
||
card.querySelector('[data-reset-standard]')?.addEventListener('click',()=>{if(!confirm('Вернуть стандартную фиолетовую панель и стандартные цвета интерфейса?'))return;savedState=standardState();previewState=clone(savedState);dirty=false;persist();applyActual(savedState);rerender(card);window.SunEnterprise?.toast?.('Стандартная версия восстановлена.','success')});
|
||
}
|
||
function ensureCard(){const view=document.getElementById('enterprise-settings'),grid=view?.querySelector('.enterprise-grid');if(!grid)return;let card=document.getElementById('sunBrandThemeSettingsCard');if(card&&grid.contains(card)){if(!card.dataset.bound){rerender(card);card.dataset.bound='1'}return}card=document.createElement('section');card.id='sunBrandThemeSettingsCard';card.className='enterprise-card wide sun-brand-settings-card';card.innerHTML=cardHtml();const history=[...grid.children].find(el=>(el.querySelector(':scope > h2')?.textContent||'').trim()==='История изменений');if(history)window.SunSafe.insertBefore(grid,card,history);else grid.appendChild(card);bind(card);updatePreviewNode(card);card.dataset.bound='1'}
|
||
function bindBrandHome(){const brand=document.querySelector('header .brand');if(!brand||brand.dataset.sunHomeBound)return;brand.dataset.sunHomeBound='1';brand.classList.add('sun-brand-home-link');brand.setAttribute('role','link');brand.setAttribute('tabindex','0');brand.setAttribute('title','Новый заказ');const go=()=>{const btn=[...document.querySelectorAll('header nav button')].find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Новый заказ')||document.querySelector('header nav .nav-new');btn?.click()};brand.addEventListener('click',go);brand.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();go()}})}
|
||
function syncFromStorage(){savedState=read();if(!dirty)previewState=clone(savedState);applyActual(savedState);const card=document.getElementById('sunBrandThemeSettingsCard');if(card&&!dirty)rerender(card)}
|
||
applyActual(savedState);previewState=clone(savedState);
|
||
const boot=()=>{applyActual(savedState);ensureCard();bindBrandHome();const settings=document.getElementById('enterprise-settings');if(settings)new MutationObserver(()=>queueMicrotask(()=>{if(settings.classList.contains('on')&&!document.getElementById('sunBrandThemeSettingsCard'))ensureCard();bindBrandHome()})).observe(settings,{childList:true,subtree:true});document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const name=String(b.dataset.navLabel||b.textContent||'').trim();if(name!=='Настройки'&&dirty){previewState=clone(savedState);dirty=false;const card=document.getElementById('sunBrandThemeSettingsCard');if(card)rerender(card)}},true);window.addEventListener('resize',()=>applyActual(savedState));window.addEventListener('storage',e=>{if(e.key===KEY)syncFromStorage()});window.addEventListener('suncloudsync',syncFromStorage);window.addEventListener('sun:cloud-state-applied',syncFromStorage)};
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
window.SunBrandTheme={get:()=>clone(savedState),preview:()=>clone(previewState),save:()=>{savedState=clone(previewState);persist();dirty=false;applyActual(savedState)},resetStandard:()=>{savedState=standardState();previewState=clone(savedState);persist();dirty=false;applyActual(savedState);ensureCard()},applyPreset:name=>{previewState=presetState(name);dirty=true;ensureCard()}};
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: offer-template-v1.js ===== */
|
||
/* Sun Catering: selectable client-offer templates */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunOfferTemplateV1)return;
|
||
window.__sunOfferTemplateV1=true;
|
||
|
||
const KEY='sunOfferTemplateV1';
|
||
const DEFAULT_ID='light';
|
||
const TEMPLATES=[
|
||
{id:'light',name:'Минимализм',desc:'Светлая премиальная классика: крупная обложка, фотография, факты события и чистая сетка.',thumb:'offer-templates/thumb-light.jpg'},
|
||
{id:'midnight-glass',name:'Luxury Dark',desc:'Тёмная ресторанная презентация с золотыми акцентами и крупной food-фотографией.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||
{id:'editorial-grid',name:'Editorial Magazine',desc:'Редакционный журнальный макет с нумерацией разделов, крупной типографикой и фотополосой.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||
{id:'warm-sun',name:'Warm Sun',desc:'Тёплый фирменный дизайн в солнечной палитре с мягкими формами и сценарием подготовки.',thumb:'offer-templates/thumb-light.jpg'},
|
||
{id:'bento-cards',name:'Bento Cards',desc:'Современная карточная композиция: метрики, фото, преимущества и итог собраны в bento-сетку.',thumb:'offer-templates/thumb-light.jpg'},
|
||
{id:'event-story',name:'Event Story',desc:'Предложение как история события: таймлайн от заявки до готового стола и большая фотография.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||
{id:'black-gold',name:'Food First',desc:'Максимальный акцент на еде: полноэкранное фото, чёрный фон и золотая ресторанная подача.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||
{id:'personal-letter',name:'Personal Letter',desc:'Персональное предложение в форме красивого письма клиенту с мягкой типографикой.',thumb:'offer-templates/thumb-light.jpg'},
|
||
{id:'event-ticket',name:'Event Ticket',desc:'Дизайн приглашения/билета: данные события, меню, итог и декоративный ticket-блок.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||
{id:'solar-experience',name:'Solar Experience',desc:'Яркий фирменный солнечный макет с орбитальной композицией и сильным первым экраном.',thumb:'offer-templates/thumb-light.jpg'},
|
||
{id:'midnight-compact',name:'Midnight Compact · архив',desc:'Возвращённый скрытый компактный тёмный шаблон.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||
{id:'emerald-gold',name:'Emerald Gold · архив',desc:'Возвращённый премиальный изумрудно-золотой шаблон.',thumb:'offer-templates/thumb-emerald-gold.jpg'},
|
||
{id:'neon-emerald',name:'Neon Emerald · архив',desc:'Возвращённый скрытый неоново-изумрудный шаблон.',thumb:'offer-templates/thumb-midnight-glass.jpg'}
|
||
];
|
||
|
||
const esc=window.SunSafe.escapeHTML;
|
||
function parse(){
|
||
try{
|
||
const raw=JSON.parse(localStorage.getItem(KEY)||'{}')||{};
|
||
const id=TEMPLATES.some(x=>x.id===raw.id)?raw.id:DEFAULT_ID;
|
||
return {version:1,id};
|
||
}catch(_){return {version:1,id:DEFAULT_ID}}
|
||
}
|
||
let state=parse();
|
||
function selected(){return TEMPLATES.find(x=>x.id===state.id)||TEMPLATES[0]}
|
||
function save(id){
|
||
if(!TEMPLATES.some(x=>x.id===id))id=DEFAULT_ID;
|
||
state={version:1,id};
|
||
localStorage.setItem(KEY,JSON.stringify(state));
|
||
window.dispatchEvent(new CustomEvent('sunoffertemplatechange',{detail:{...state,template:selected()}}));
|
||
}
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-offer-template-settings-style';
|
||
style.textContent=`
|
||
.sun-offer-template-card{grid-column:1/-1!important}
|
||
.sun-offer-template-card .hint{max-width:980px}
|
||
.sun-offer-template-grid{display:grid;grid-template-columns:repeat(4,minmax(200px,1fr));gap:14px;margin-top:14px}
|
||
.sun-offer-template-option{position:relative;border:1px solid #e3e6e9;border-radius:16px;background:#fff;padding:10px;cursor:pointer;transition:.16s ease;overflow:hidden;box-shadow:0 4px 16px rgba(30,35,40,.035)}
|
||
.sun-offer-template-option:hover{transform:translateY(-1px);box-shadow:0 8px 22px rgba(30,35,40,.08)}
|
||
.sun-offer-template-option.on{border-color:var(--sun-brand-sun,#FFC107);box-shadow:0 0 0 2px color-mix(in srgb,var(--sun-brand-sun,#FFC107) 30%,transparent),0 9px 24px rgba(30,35,40,.09)}
|
||
.sun-offer-template-thumb{display:block;width:100%;height:245px;object-fit:cover;object-position:top center;border-radius:11px;background:#eef0f2;border:1px solid #eceef0}
|
||
.sun-offer-template-info{padding:10px 3px 2px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px 10px;align-items:start}
|
||
.sun-offer-template-info strong{font-size:14px;color:#24282d}.sun-offer-template-info p{grid-column:1/-1;margin:0;color:#6c737c;font-size:11px;line-height:1.42}
|
||
.sun-offer-template-check{width:25px;height:25px;border-radius:50%;border:1px solid #d8dde2;display:grid;place-items:center;color:transparent;font-weight:900;background:#fff}
|
||
.sun-offer-template-option.on .sun-offer-template-check{background:var(--sun-brand-sun,#FFC107);border-color:var(--sun-brand-sun,#FFC107);color:#1a1a1a}
|
||
.sun-offer-template-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px;padding-top:12px;border-top:1px solid #eceef1}
|
||
.sun-offer-template-toolbar .current{font-size:12px;color:#66707a}.sun-offer-template-toolbar .current b{color:#263238}
|
||
@media(max-width:1100px){.sun-offer-template-grid{grid-template-columns:repeat(2,minmax(210px,1fr))}}
|
||
@media(max-width:680px){.sun-offer-template-grid{grid-template-columns:1fr}.sun-offer-template-thumb{height:290px}}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
function offerTemplateMini(id){
|
||
if(id==='editorial-grid')return '<i class="mini-head"></i><i class="mini-editorial-hero"></i><i class="mini-editorial-stack a"></i><i class="mini-editorial-stack b"></i><i class="mini-editorial-grid"></i>';
|
||
if(id==='midnight-glass')return '<i class="mini-head"></i><i class="mini-midnight-hero"></i><i class="mini-midnight-total"></i><i class="mini-midnight-left"></i><i class="mini-midnight-right"></i>';
|
||
if(id==='emerald-gold')return '<i class="mini-head"></i><i class="mini-emerald-hero"></i><i class="mini-emerald-gold"></i><i class="mini-emerald-info"></i><i class="mini-emerald-menu"></i>';
|
||
return '<i class="mini-head"></i><i class="mini-light-hero"></i><i class="mini-light-row a"></i><i class="mini-light-row b"></i><i class="mini-light-row c"></i><i class="mini-light-total"></i>';
|
||
}
|
||
function cardHtml(){
|
||
const cur=selected();
|
||
return `<h2>Шаблон предложения клиенту</h2>
|
||
<p class="hint">Выберите внешний вид, который будет использоваться в «Предложении клиенту» и в PDF. Реальные фотографии боксов и данные заказа остаются одинаковыми — меняется только дизайн.</p>
|
||
<div class="sun-offer-template-grid">${TEMPLATES.map(t=>`<article class="sun-offer-template-option ${t.id===state.id?'on':''}" data-offer-template="${esc(t.id)}" tabindex="0" role="radio" aria-checked="${t.id===state.id?'true':'false'}"><div class="sun-offer-template-thumb sun-offer-template-mini sun-offer-template-mini-${esc(t.id)}" data-template-mini="${esc(t.id)}" aria-hidden="true">${offerTemplateMini(t.id)}</div><div class="sun-offer-template-info"><strong>${esc(t.name)}</strong><span class="sun-offer-template-check">✓</span><p>${esc(t.desc)}</p></div></article>`).join('')}</div>
|
||
<div class="sun-offer-template-toolbar"><span class="current">Сейчас выбран: <b>${esc(cur.name)}</b></span><button type="button" class="outline" data-offer-template-reset>Вернуть светлый фирменный</button></div>`;
|
||
}
|
||
function bind(card){
|
||
const choose=(el)=>{
|
||
const id=el?.dataset?.offerTemplate;if(!id)return;
|
||
save(id);card.innerHTML=cardHtml();bind(card);
|
||
try{window.SunEnterprise?.toast?.(`Шаблон «${selected().name}» выбран.`)}catch(_){ }
|
||
};
|
||
card.querySelectorAll('[data-offer-template]').forEach(el=>{
|
||
el.addEventListener('click',()=>choose(el));
|
||
el.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();choose(el)}});
|
||
});
|
||
card.querySelector('[data-offer-template-reset]')?.addEventListener('click',()=>{save(DEFAULT_ID);card.innerHTML=cardHtml();bind(card)});
|
||
}
|
||
function ensureCard(){
|
||
const view=document.getElementById('enterprise-settings');if(!view)return;
|
||
const grid=view.querySelector('.enterprise-grid');if(!grid)return;
|
||
let card=document.getElementById('sunOfferTemplateSettingsCard');
|
||
if(card&&grid.contains(card))return;
|
||
card=document.createElement('section');card.id='sunOfferTemplateSettingsCard';card.className='enterprise-card wide sun-offer-template-card';card.innerHTML=cardHtml();
|
||
const brand=document.getElementById('sunBrandThemeSettingsCard');
|
||
if(brand&&brand.parentElement===grid)brand.insertAdjacentElement('afterend',card);else{
|
||
const history=[...grid.children].find(el=>(el.querySelector(':scope > h2')?.textContent||'').trim()==='История изменений');
|
||
if(history)window.SunSafe.insertBefore(grid,card,history);else grid.appendChild(card);
|
||
}
|
||
bind(card);
|
||
}
|
||
function boot(){
|
||
ensureCard();
|
||
const settings=document.getElementById('enterprise-settings');
|
||
if(settings)new MutationObserver(()=>queueMicrotask(ensureCard)).observe(settings,{childList:true,subtree:true});
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const label=(b.dataset.navLabel||b.textContent||'').trim();if(label==='Настройки')setTimeout(ensureCard,30)},true);
|
||
window.addEventListener('storage',e=>{if(e.key===KEY){state=parse();ensureCard();window.dispatchEvent(new CustomEvent('sunoffertemplatechange',{detail:{...state,template:selected()}}));}});
|
||
window.addEventListener('suncloudsync',()=>{state=parse();setTimeout(ensureCard,25);window.dispatchEvent(new CustomEvent('sunoffertemplatechange',{detail:{...state,template:selected()}}));});
|
||
window.addEventListener('sun:cloud-state-applied',()=>{state=parse();setTimeout(ensureCard,25);window.dispatchEvent(new CustomEvent('sunoffertemplatechange',{detail:{...state,template:selected()}}));});
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
|
||
window.SunOfferTemplate={
|
||
key:KEY,
|
||
list:()=>TEMPLATES.map(x=>({...x})),
|
||
get:()=>({...state,template:{...selected()}}),
|
||
set:id=>save(id)
|
||
};
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: stability-v17.js ===== */
|
||
(() => {
|
||
'use strict';
|
||
if (window.SunStabilityV17) return;
|
||
|
||
const VERSION = '17.7.3';
|
||
const RELEASE = 'v17.7.3 Clients Server Read';
|
||
const DB_NAME = 'SunStabilityV17';
|
||
const DB_VERSION = 1;
|
||
const DAILY_KEY = 'sunV17DailyBackup';
|
||
const ERROR_DEDUPE_MS=5*60*1000;
|
||
const STABILITY_RPC_TIMEOUT_MS=10000;
|
||
const NETWORK_BACKOFF_MAX_MS=5*60*1000;
|
||
const errorSeen=new Map();
|
||
let flushBusy=false,mirrorBusy=false,backupBusy=false,initialBusy=false,networkFailures=0,nextNetworkAttemptAt=0;
|
||
const $ = id => document.getElementById(id);
|
||
const networkish=e=>/failed to fetch|networkerror|load failed|network|timeout|время ожидания/i.test(String(e?.message||e||''));
|
||
function noteNetworkFailure(){networkFailures=Math.min(networkFailures+1,8);nextNetworkAttemptAt=Date.now()+Math.min(NETWORK_BACKOFF_MAX_MS,5000*Math.pow(2,networkFailures-1));}
|
||
function noteNetworkSuccess(){networkFailures=0;nextNetworkAttemptAt=0;}
|
||
function networkReady(){return navigator.onLine&&Date.now()>=nextNetworkAttemptAt;}
|
||
async function stabilityRpc(promise,label){let timer=0;try{return await Promise.race([Promise.resolve(promise),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label+': timeout')),STABILITY_RPC_TIMEOUT_MS)})]);}finally{if(timer)clearTimeout(timer)}}
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const toast=(t,k='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(t,k,ms)}catch(_){} console.log('[V17]',t)};
|
||
const cloud=()=>window.SunCloudV2;
|
||
const client=()=>cloud()?.getClient?.()||null;
|
||
const workspace=()=>cloud()?.getWorkspace?.()||null;
|
||
const clientId=()=>cloud()?.getClientId?.()||'local';
|
||
let lastMirrorAt='';
|
||
let conflictCount=0;
|
||
let booted=false;
|
||
|
||
function openDb(){
|
||
return new Promise((resolve,reject)=>{
|
||
const r=indexedDB.open(DB_NAME,DB_VERSION);
|
||
r.onupgradeneeded=()=>{
|
||
const db=r.result;
|
||
if(!db.objectStoreNames.contains('outbox'))db.createObjectStore('outbox',{keyPath:'key'});
|
||
if(!db.objectStoreNames.contains('errors'))db.createObjectStore('errors',{keyPath:'id'});
|
||
if(!db.objectStoreNames.contains('meta'))db.createObjectStore('meta',{keyPath:'key'});
|
||
};
|
||
r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);
|
||
});
|
||
}
|
||
async function idbPut(store,value){try{const db=await openDb();await new Promise((res,rej)=>{const tx=db.transaction(store,'readwrite');tx.objectStore(store).put(value);tx.oncomplete=res;tx.onerror=()=>rej(tx.error)});return true}catch(_){return false}}
|
||
async function idbGet(store,key){try{const db=await openDb();return await new Promise((res,rej)=>{const r=db.transaction(store,'readonly').objectStore(store).get(key);r.onsuccess=()=>res(r.result||null);r.onerror=()=>rej(r.error)})}catch(_){return null}}
|
||
async function idbDelete(store,key){try{const db=await openDb();await new Promise((res,rej)=>{const tx=db.transaction(store,'readwrite');tx.objectStore(store).delete(key);tx.oncomplete=res;tx.onerror=()=>rej(tx.error)});return true}catch(_){return false}}
|
||
async function idbAll(store){try{const db=await openDb();return await new Promise((res,rej)=>{const r=db.transaction(store,'readonly').objectStore(store).getAll();r.onsuccess=()=>res(r.result||[]);r.onerror=()=>rej(r.error)})}catch(_){return[]}}
|
||
|
||
async function markPending(reason='change'){
|
||
const ws=workspace()?.id||'local';
|
||
const old=await idbGet('outbox',`sync:${ws}`);
|
||
await idbPut('outbox',{key:`sync:${ws}`,workspaceId:ws,reason:String(reason),createdAt:old?.createdAt||Date.now(),updatedAt:Date.now(),attempts:Number(old?.attempts||0)+1});
|
||
refreshCardSoon();
|
||
}
|
||
async function markSynced(revision){
|
||
const ws=workspace()?.id||'local';
|
||
await idbDelete('outbox',`sync:${ws}`);
|
||
await idbPut('meta',{key:`lastSync:${ws}`,at:Date.now(),revision:Number(revision||0)});
|
||
refreshCardSoon();
|
||
}
|
||
async function recordConflict(message){
|
||
conflictCount++;
|
||
await idbPut('meta',{key:'lastConflict',at:Date.now(),message:String(message||''),count:conflictCount});
|
||
refreshCardSoon();
|
||
}
|
||
|
||
function context(){
|
||
return {url:location.href,online:navigator.onLine,userAgent:navigator.userAgent,release:RELEASE,cloudStatus:cloud()?.status?.()||null};
|
||
}
|
||
async function queueError(message,stack='',level='error',extra={}){
|
||
const text=String(message||'Unknown error').slice(0,4000),wid=workspace()?.id||null,key=level+'|'+(wid||'')+'|'+text,now=Date.now();
|
||
const seen=errorSeen.get(key)||0;if(now-seen<ERROR_DEDUPE_MS)return false;errorSeen.set(key,now);
|
||
const item={id:crypto.randomUUID?crypto.randomUUID():Date.now()+'-'+Math.random(),workspaceId:wid,clientId:clientId(),version:VERSION,level,message:text,stack:String(stack||'').slice(0,12000),context:{...context(),...extra},createdAt:now};
|
||
await idbPut('errors',item);if(networkReady())flushErrors();refreshCardSoon();return true;
|
||
}
|
||
async function flushErrors(){
|
||
const c=client(),ws=workspace();if(flushBusy||!c||!networkReady())return;flushBusy=true;
|
||
try{const items=await idbAll('errors');for(const item of items.slice(0,20)){try{const {error}=await stabilityRpc(c.rpc('sun_v17_log_error',{p_workspace:item.workspaceId||ws?.id||null,p_client_id:item.clientId,p_app_version:item.version,p_level:item.level,p_message:item.message,p_stack:item.stack,p_context:item.context}),'Журнал ошибок');if(error)throw error;await idbDelete('errors',item.id);noteNetworkSuccess();}catch(e){if(networkish(e))noteNetworkFailure();break;}}}
|
||
finally{flushBusy=false}
|
||
}
|
||
|
||
async function mirror(payload){
|
||
const c=client(),ws=workspace();if(mirrorBusy||!c||!ws?.id||!payload||!networkReady())return null;mirrorBusy=true;
|
||
try{const {data,error}=await stabilityRpc(c.rpc('sun_v17_mirror_legacy',{p_workspace:ws.id,p_payload:payload,p_client_id:clientId()}),'Зеркалирование v17');if(error)throw error;noteNetworkSuccess();lastMirrorAt=new Date().toISOString();await idbPut('meta',{key:'mirror:'+ws.id,at:Date.now(),result:data||{}});return data;}
|
||
catch(e){if(networkish(e))noteNetworkFailure();await queueError('Ошибка зеркалирования v17: '+(e.message||e),e.stack||'','warn');return null}
|
||
finally{mirrorBusy=false}
|
||
}
|
||
|
||
async function ensureDailyBackup(){
|
||
const c=client(),ws=workspace();if(backupBusy||!c||!ws?.id||!networkReady())return;backupBusy=true;
|
||
try{const today=new Date().toISOString().slice(0,10);let state={};try{state=JSON.parse(localStorage.getItem(DAILY_KEY)||'{}')||{}}catch(_){}if(state[ws.id]===today)return;const {error}=await stabilityRpc(c.rpc('sun_v17_create_backup',{p_workspace:ws.id,p_kind:'daily',p_label:'Автоматическая копия '+today}),'Ежедневная резервная копия');if(error)throw error;await stabilityRpc(c.rpc('sun_v17_prune_backups',{p_workspace:ws.id}),'Очистка старых резервных копий');noteNetworkSuccess();state[ws.id]=today;localStorage.setItem(DAILY_KEY,JSON.stringify(state));}
|
||
catch(e){if(networkish(e))noteNetworkFailure();await queueError('Не удалось создать ежедневную резервную копию: '+(e.message||e),e.stack||'','warn')}
|
||
finally{backupBusy=false}
|
||
}
|
||
|
||
async function onCloudSync(ev){
|
||
noteNetworkSuccess();
|
||
const payload=ev?.detail?.payload;
|
||
if(payload)await mirror(payload);
|
||
await ensureDailyBackup();await flushErrors();refreshCardSoon();
|
||
}
|
||
|
||
function installErrorHooks(){
|
||
window.addEventListener('error',e=>{if(e?.error?.__sunV17Reported)return;queueError(e?.message||e?.error?.message||'Window error',e?.error?.stack||'', 'error',{source:e?.filename||'',line:e?.lineno||0,column:e?.colno||0});});
|
||
window.addEventListener('unhandledrejection',e=>{const r=e.reason;queueError(`Unhandled promise: ${r?.message||r||'unknown'}`,r?.stack||'','error');});
|
||
}
|
||
|
||
async function diagnostics(){
|
||
const ws=workspace(),c=client();const outbox=await idbGet('outbox',`sync:${ws?.id||'local'}`);const errors=(await idbAll('errors')).length;
|
||
let normalized=null,backups=[];
|
||
if(c&&ws?.id&&navigator.onLine){
|
||
try{const r=await c.rpc('sun_v17_entity_snapshot',{p_workspace:ws.id});if(!r.error)normalized=r.data}catch(_){}
|
||
try{const r=await c.rpc('sun_v17_list_backups',{p_workspace:ws.id,p_limit:30});if(!r.error)backups=r.data||[]}catch(_){}
|
||
}
|
||
let localOrders=0,localCatalog=0;try{localOrders=JSON.parse(localStorage.getItem('sunOrders')||'[]').length}catch(_){}try{localCatalog=JSON.parse(localStorage.getItem('sunBoxes')||'[]').length}catch(_){}
|
||
return {ws,c,outbox,errors,normalized,backups,localOrders,localCatalog,cloudStatus:cloud()?.status?.()||{}};
|
||
}
|
||
|
||
async function createManualBackup(){
|
||
const c=client(),ws=workspace();if(!c||!ws?.id)return toast('Сначала войдите в облачную рабочую базу.','warn');
|
||
try{const {error}=await c.rpc('sun_v17_create_backup',{p_workspace:ws.id,p_kind:'manual',p_label:`Ручная копия · ${new Date().toLocaleString('ru-RU')}`});if(error)throw error;toast('Резервная копия создана.','success');await renderCard()}catch(e){toast(e.message||String(e),'error')}
|
||
}
|
||
async function restoreBackup(id){
|
||
const c=client(),ws=workspace();if(!c||!ws?.id||!id)return;
|
||
if(!confirm('Восстановить выбранную резервную копию? Перед восстановлением будет автоматически создана ещё одна копия текущего состояния.'))return;
|
||
try{const {error}=await c.rpc('sun_v17_restore_backup',{p_workspace:ws.id,p_backup:id});if(error)throw error;toast('Копия восстановлена. Загружаю данные…','success');await cloud()?.pull?.();setTimeout(()=>location.reload(),500)}catch(e){toast(e.message||String(e),'error')}
|
||
}
|
||
function emergencyExport(){
|
||
const data={format:'sun-v17-emergency-local',version:17,createdAt:new Date().toISOString(),storage:{}};
|
||
for(let i=0;i<localStorage.length;i++){const k=localStorage.key(i);if(k?.startsWith('sun'))data.storage[k]=localStorage.getItem(k)}
|
||
const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`sun-emergency-${new Date().toISOString().replace(/[:.]/g,'-')}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);
|
||
}
|
||
|
||
function findSettingsGrid(){return document.querySelector('#enterprise-settings .enterprise-grid')}
|
||
function installStyle(){if($('sunV17Style'))return;const s=document.createElement('style');s.id='sunV17Style';s.textContent=`
|
||
.sun-v17-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px}.sun-v17-stat{border:1px solid #e1e6e8;border-radius:10px;padding:10px;background:#fbfcfc}.sun-v17-stat small{display:block;color:#77838b;margin-bottom:4px}.sun-v17-stat b{font-size:15px;color:#15364c}.sun-v17-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.sun-v17-backups{margin-top:12px;border-top:1px solid #e4e8ea;padding-top:10px}.sun-v17-backup{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;padding:7px 0;border-bottom:1px dashed #e5e8ea}.sun-v17-backup small{display:block;color:#7a858c}.sun-v17-health{font-weight:800}.sun-v17-health.good{color:#347154}.sun-v17-health.warn{color:#9a7016}.sun-v17-health.bad{color:#a64040}
|
||
@media(max-width:900px){.sun-v17-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:600px){.sun-v17-grid{grid-template-columns:1fr}}
|
||
`;document.head.appendChild(s)}
|
||
|
||
async function renderCard(){
|
||
// Backup/stability controls are intentionally hidden from ordinary Settings.
|
||
$('sunV17StabilityCard')?.remove();
|
||
}
|
||
let refreshTimer=null;function refreshCardSoon(){clearTimeout(refreshTimer);refreshTimer=setTimeout(()=>{if($('enterprise-settings')?.classList.contains('on'))renderCard()},250)}
|
||
|
||
function observeSettings(){
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;if(String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(renderCard,120)},true);
|
||
const root=$('enterprise-settings');if(root)new MutationObserver(()=>{if(root.classList.contains('on')&&!$('sunV17StabilityCard'))setTimeout(renderCard,80)}).observe(root,{childList:true,subtree:true,attributes:true,attributeFilter:['class']});
|
||
}
|
||
|
||
async function initialMirrorIfPossible(){
|
||
const c=client(),ws=workspace();if(initialBusy||!c||!ws?.id||!networkReady())return;initialBusy=true;
|
||
try{const payload=await cloud()?.snapshotLocal?.();if(payload)await mirror(payload);await ensureDailyBackup();await flushErrors()}catch(e){await queueError(e.message||String(e),e.stack||'','warn')}
|
||
finally{initialBusy=false}
|
||
}
|
||
|
||
function boot(){if(booted)return;booted=true;installStyle();installErrorHooks();observeSettings();window.addEventListener('sun:cloud-sync-complete',onCloudSync);window.addEventListener('online',()=>{noteNetworkSuccess();flushErrors();setTimeout(initialMirrorIfPossible,900)});window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(initialMirrorIfPossible,900));setTimeout(initialMirrorIfPossible,2600);setInterval(()=>{if(networkReady())flushErrors()},60000)}
|
||
|
||
window.SunStabilityV17={VERSION,RELEASE,markPending,markSynced,recordConflict,queueError,renderCard,diagnostics,createBackup:createManualBackup,emergencyExport};
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: saas-v16.js ===== */
|
||
/* Sun Catering SaaS subscriptions, plan entitlements and platform admin v16 */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.SunSaaSV16)return;
|
||
|
||
const VERSION='2026.08.30-saas-v16';
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const cloud=()=>window.SunCloudV2||null;
|
||
const client=()=>cloud()?.getClient?.()||null;
|
||
const workspace=()=>cloud()?.getWorkspace?.()||null;
|
||
const session=()=>cloud()?.getSession?.()||null;
|
||
const toast=(text,type='info',timeout=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,timeout)}catch(_){console.log('[SaaS]',text)}};
|
||
|
||
const PLAN_LABELS={basic:'Базовый',professional:'Профессиональный',full:'Полный'};
|
||
const FEATURE_LABELS={
|
||
orders:'Заказы',calendar:'Календарь',map:'Карта',clients:'Клиенты',catalog_view:'Каталог',catalog_edit:'Редактирование боксов',
|
||
production:'Производство',shopping:'Закупки',stock:'Склад',routes:'Маршруты',mailings:'Рассылки',money:'Деньги',
|
||
stats_basic:'Статистика',stats_advanced:'Расширенная статистика',team:'Команда',suppliers:'Поставщики',print:'Печать',settings:'Настройки',
|
||
branding:'Фирменное оформление',client_offers:'Предложения клиенту',offer_templates:'Шаблоны предложений',backups:'Резервные копии',audit:'История изменений',users_manage:'Сотрудники и права'
|
||
};
|
||
const NAV_FEATURE={
|
||
'Главная':'orders','Новый заказ':'orders','Заказы':'orders','Календарь':'calendar','Карта':'map','Производство':'production','Закупки':'shopping','Закупка':'shopping',
|
||
'Склад':'stock','Клиенты':'clients','Рассылки':'mailings','Деньги':'money','Команда':'team','Поставщики':'suppliers','Статистика':'stats_basic','Печать':'print','Маршруты':'routes','Настройки':'settings'
|
||
};
|
||
const WRITE_FN_FEATURE={
|
||
saveBox:'catalog_edit',removeBox:'catalog_edit',editBox:'catalog_edit',openManager:'catalog_edit',
|
||
sunOpenClientOffer:'client_offers',sunOpenCurrentClientOffer:'client_offers'
|
||
};
|
||
const PLATFORM_FEATURES=['client_offers','offer_templates','catalog_edit','branding','users_manage','money','stock','production','routes','mailings','suppliers','audit','backups'];
|
||
|
||
let snapshot=null;
|
||
let lastWorkspaceId='';
|
||
let loading=false;
|
||
let guardedFns=new Map();
|
||
let adminRows=[];
|
||
|
||
function hasFeature(key){
|
||
if(!key)return true;
|
||
if(!snapshot)return true; // do not flash-lock before the server snapshot arrives
|
||
return snapshot.features?.[key]===true;
|
||
}
|
||
function isWritable(){if(cloud()?.isSupportMode?.())return false;return !snapshot||snapshot.access_mode==='full';}
|
||
function accessLabel(mode){return mode==='full'?'Активен':mode==='read_only'?'Только просмотр':'Заблокирован';}
|
||
function statusLabel(status){return ({trialing:'Пробный период',active:'Активна',past_due:'Оплата просрочена',canceled:'Отменена',expired:'Истекла'})[status]||status||'—';}
|
||
function formatDate(v){if(!v)return 'без срока';const d=new Date(v);if(Number.isNaN(d.getTime()))return '—';return d.toLocaleDateString('ru-RU');}
|
||
function endDate(){return snapshot?.status==='trialing'?snapshot?.trial_ends_at:snapshot?.current_period_end;}
|
||
function daysLeft(){const end=endDate();if(!end)return null;return Math.ceil((new Date(end).getTime()-Date.now())/86400000);}
|
||
|
||
async function refreshSnapshot(force=false){
|
||
const c=client(),ws=workspace();
|
||
if(!c||!ws?.id){snapshot=null;lastWorkspaceId='';removeSubscriptionUi();return null;}
|
||
if(loading)return snapshot;
|
||
if(!force&&lastWorkspaceId===ws.id&&snapshot)return snapshot;
|
||
loading=true;
|
||
try{
|
||
const {data,error}=await c.rpc('sun_subscription_snapshot',{p_workspace:ws.id});
|
||
if(error)throw error;
|
||
snapshot=Array.isArray(data)?(data[0]||null):data;
|
||
lastWorkspaceId=ws.id;
|
||
applyAll();
|
||
window.dispatchEvent(new CustomEvent('sun:subscription-changed',{detail:snapshot}));
|
||
return snapshot;
|
||
}catch(err){console.error('[SaaS] subscription snapshot',err);snapshot=null;}
|
||
finally{loading=false;}
|
||
return snapshot;
|
||
}
|
||
|
||
function installStyle(){
|
||
if($('sunSaaSV16Style'))return;
|
||
const s=document.createElement('style');s.id='sunSaaSV16Style';s.textContent=`
|
||
.sun-plan-locked{position:relative!important;opacity:.62!important;filter:saturate(.65)}
|
||
header nav button.sun-plan-locked::after{content:'🔒';font-size:10px;margin-left:auto;padding-left:6px}
|
||
.sun-side-collapsed #sunSaaSBadgeV16{display:none!important}
|
||
.sun-saas-plan-card-locked{position:relative;opacity:.62}
|
||
.sun-saas-plan-card-locked::after{content:'Доступно на другом тарифе';position:absolute;inset:auto 12px 12px auto;background:#241f3b;color:#fff;border-radius:999px;padding:5px 9px;font-size:11px;font-weight:800;z-index:4}
|
||
.sun-saas-badge{margin:8px 10px 0;border:1px solid #ffffff24;border-radius:11px;padding:8px 9px;color:#fff;background:#ffffff10;font-size:11px;line-height:1.35}
|
||
.sun-saas-badge b{display:block;font-size:12px}.sun-saas-badge .warn{color:#ffd36b}
|
||
.sun-saas-readonly-banner{position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:9800;background:#fff6df;border:1px solid #e2bd55;color:#6a5216;border-radius:12px;padding:9px 14px;box-shadow:0 8px 30px #0002;font-weight:800;max-width:min(680px,calc(100vw - 24px));text-align:center}
|
||
.sun-saas-blocked{position:fixed;inset:0;z-index:12000;background:#122b3cf2;display:grid;place-items:center;padding:20px}.sun-saas-blocked-card{width:min(520px,100%);background:#fff;border-radius:18px;padding:24px;box-shadow:0 30px 90px #0007;text-align:center}.sun-saas-blocked-card img{width:72px;height:72px;object-fit:contain}.sun-saas-blocked-card h2{margin:8px 0;color:#17384d}.sun-saas-blocked-card p{color:#596773;line-height:1.5}
|
||
.sun-saas-modal{position:fixed;inset:0;z-index:11500;background:#0d2130c9;display:grid;place-items:center;padding:18px}.sun-saas-modal-card{width:min(900px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:18px;padding:20px;box-shadow:0 30px 90px #0007}.sun-saas-modal-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.sun-saas-modal-head h2{margin:0;color:#17384d}.sun-saas-modal-head button{border:0;background:#eef1f2;border-radius:50%;width:34px;height:34px;font-size:20px}.sun-saas-plans{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:16px}.sun-saas-plan{border:1px solid #dfe5e8;border-radius:14px;padding:15px;background:#fafcfc}.sun-saas-plan.current{border:2px solid #ffb400;background:#fffaf0}.sun-saas-plan h3{margin:0 0 5px;color:#18384b}.sun-saas-plan ul{padding-left:18px;color:#4f5b64;font-size:12px;line-height:1.55}.sun-saas-plan .muted{color:#89939a}.sun-saas-upgrade{margin-top:12px;background:#eef4f6;border-radius:12px;padding:12px;color:#52606a}
|
||
.sun-saas-settings-card .sun-saas-status-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;margin-top:12px}.sun-saas-kpi{border:1px solid #e2e7ea;border-radius:11px;padding:10px;background:#fafcfc}.sun-saas-kpi small{display:block;color:#7a858d;margin-bottom:4px}.sun-saas-kpi b{color:#203846}
|
||
#sun-saas-admin{padding:20px}.sun-saas-admin-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}.sun-saas-admin-table{display:grid;gap:10px;margin-top:16px}.sun-saas-company{border:1px solid #dfe5e8;border-radius:14px;padding:14px;background:#fff}.sun-saas-company-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;flex-wrap:wrap}.sun-saas-company h3{margin:0;color:#18384b}.sun-saas-company small{color:#78828a}.sun-saas-company-actions{display:grid;grid-template-columns:1fr 110px auto auto;gap:8px;margin-top:11px;align-items:end}.sun-saas-company-actions label{font-size:11px;color:#64717a}.sun-saas-company-actions select{width:100%}.sun-saas-company-meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}.sun-saas-pill{border-radius:999px;padding:5px 8px;background:#eef3f5;font-size:11px}.sun-saas-pill.good{background:#eaf7ef;color:#347153}.sun-saas-pill.warn{background:#fff5dc;color:#8a681b}.sun-saas-pill.bad{background:#ffebe9;color:#993f3d}
|
||
.sun-saas-onboarding-tabs{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.sun-saas-onboarding-choice{border:1px solid #dfe5e8;background:#fafcfc;border-radius:12px;padding:12px;text-align:left}.sun-saas-onboarding-choice b{display:block;color:#17384d;margin-bottom:3px}.sun-saas-onboarding-choice small{color:#6f7980;line-height:1.35}
|
||
@media(max-width:760px){.sun-saas-plans{grid-template-columns:1fr}.sun-saas-settings-card .sun-saas-status-grid{grid-template-columns:1fr 1fr}.sun-saas-company-actions{grid-template-columns:1fr 1fr}.sun-saas-company-actions button{width:100%}}
|
||
`;document.head.appendChild(s);
|
||
}
|
||
|
||
function removeSubscriptionUi(){
|
||
$('sunSaaSBadgeV16')?.remove();$('sunSaaSReadonlyV16')?.remove();$('sunSaaSBlockedV16')?.remove();$('sunSaaSSettingsCardV16')?.remove();
|
||
qa('[data-sun-plan-locked]').forEach(el=>{el.removeAttribute('data-sun-plan-locked');el.classList.remove('sun-plan-locked')});
|
||
}
|
||
|
||
function featureForNav(btn){return NAV_FEATURE[String(btn.dataset.navLabel||btn.textContent||'').trim()]||null;}
|
||
function markElement(el,feature){
|
||
if(!el)return;
|
||
if(feature&&!hasFeature(feature)){el.dataset.sunPlanLocked=feature;el.classList.add('sun-plan-locked');el.title=`${FEATURE_LABELS[feature]||feature} недоступно на тарифе «${snapshot?.plan_name||''}»`;}
|
||
else{el.removeAttribute('data-sun-plan-locked');el.classList.remove('sun-plan-locked');}
|
||
}
|
||
|
||
function applyFeatureGates(){
|
||
if(!snapshot)return;
|
||
qa('header nav button').forEach(btn=>{if(btn.id==='sunSaaSAdminNavV16')return;markElement(btn,featureForNav(btn));});
|
||
markElement($('sunClientOfferButton'),'client_offers');markElement($('sunAutoOfferButton'),'client_offers');markElement($('sunCloudManageUsersV3'),'users_manage');markElement($('sunCloudCreateInviteV2'),'users_manage');
|
||
qa('#new .catalog-head button,#new .tile.add').forEach(el=>markElement(el,'catalog_edit'));
|
||
const cards=[
|
||
['sunClientOfferSettingsCard','client_offers'],['sunOfferTemplateSettingsCard','offer_templates'],['sunBrandThemeSettingsCard','branding']
|
||
];
|
||
cards.forEach(([id,feature])=>{const el=$(id);if(!el)return;el.dataset.sunFeature=feature;el.classList.toggle('sun-saas-plan-card-locked',!hasFeature(feature));});
|
||
qa('#enterprise-settings section').forEach(card=>{
|
||
const h=(card.querySelector(':scope > h2')?.textContent||'').trim();let f=null;
|
||
if(/Резервн/i.test(h))f='backups';else if(/История изменений/i.test(h))f='audit';
|
||
if(f){card.dataset.sunFeature=f;card.classList.toggle('sun-saas-plan-card-locked',!hasFeature(f));}
|
||
});
|
||
}
|
||
|
||
function guardFunctions(){
|
||
Object.entries(WRITE_FN_FEATURE).forEach(([name,feature])=>{
|
||
const fn=window[name];if(typeof fn!=='function')return;
|
||
if(fn.__sunSaasGuard)return;
|
||
const wrapped=function(...args){
|
||
if(!isWritable())return showUpgrade(feature,'Подписка сейчас работает в режиме просмотра.');
|
||
if(!hasFeature(feature))return showUpgrade(feature);
|
||
return fn.apply(this,args);
|
||
};
|
||
wrapped.__sunSaasGuard=true;wrapped.__sunSaasOriginal=fn;window[name]=wrapped;guardedFns.set(name,wrapped);
|
||
});
|
||
}
|
||
|
||
function injectBadge(){
|
||
if(!snapshot)return;
|
||
let badge=$('sunSaaSBadgeV16');
|
||
const brand=document.querySelector('header .brand');
|
||
if(!brand)return;
|
||
if(!badge){badge=document.createElement('div');badge.id='sunSaaSBadgeV16';badge.className='sun-saas-badge';brand.insertAdjacentElement('afterend',badge);}
|
||
const d=daysLeft(), trial=snapshot.status==='trialing';
|
||
badge.innerHTML=`<b>${esc(snapshot.plan_name||PLAN_LABELS[snapshot.plan_id]||'Тариф')}</b><span class="${snapshot.access_mode==='full'?'':'warn'}">${trial?`Пробный период${d!=null?` · ${Math.max(0,d)} дн.`:''}`:accessLabel(snapshot.access_mode)}</span>`;
|
||
badge.onclick=()=>showPlans();badge.title='Тариф и подписка';badge.style.cursor='pointer';
|
||
}
|
||
|
||
function applyAccessMode(){
|
||
$('sunSaaSReadonlyV16')?.remove();$('sunSaaSBlockedV16')?.remove();
|
||
if(!snapshot)return;
|
||
applyReadOnlyControls();
|
||
if(snapshot.access_mode==='read_only'){
|
||
const b=document.createElement('div');b.id='sunSaaSReadonlyV16';b.className='sun-saas-readonly-banner';b.textContent=`Подписка закончилась. До ${formatDate(snapshot.grace_until)} база доступна только для просмотра.`;document.body.appendChild(b);
|
||
}else if(snapshot.access_mode==='blocked'){
|
||
const x=document.createElement('div');x.id='sunSaaSBlockedV16';x.className='sun-saas-blocked';x.innerHTML=`<div class="sun-saas-blocked-card"><img src="sun-logo.png" alt=""><h2>Доступ к компании приостановлен</h2><p>Подписка закончилась. Все данные сохранены и будут доступны сразу после продления.</p><div class="actions" style="justify-content:center"><button class="primary" type="button" data-saas-show-plans>Посмотреть тарифы</button><button class="outline" type="button" data-saas-signout>Выйти</button></div></div>`;document.body.appendChild(x);
|
||
x.querySelector('[data-saas-show-plans]').onclick=()=>showPlans();x.querySelector('[data-saas-signout]').onclick=async()=>{if(window.SunCloudV2?.signOut)return window.SunCloudV2.signOut();try{await client()?.auth.signOut()}catch(_){}location.reload();};
|
||
}
|
||
}
|
||
|
||
|
||
function applyReadOnlyControls(){
|
||
qa('[data-saas-readonly-disabled]').forEach(el=>{el.disabled=false;el.removeAttribute('data-saas-readonly-disabled')});
|
||
if(snapshot?.access_mode!=='read_only'&&!cloud()?.isSupportMode?.())return;
|
||
qa('#new input,#new textarea,#new select,#manager input,#manager textarea,#manager select,#editor input,#editor textarea,#editor select,#orders select,#enterprise-settings input,#enterprise-settings textarea,#enterprise-settings select').forEach(el=>{if(!el.disabled){el.disabled=true;el.dataset.saasReadonlyDisabled='1';}});
|
||
}
|
||
|
||
function showPlans(feature=null,note=''){
|
||
$('sunSaaSPlansModalV16')?.remove();
|
||
const m=document.createElement('div');m.id='sunSaaSPlansModalV16';m.className='sun-saas-modal';
|
||
const cards=[
|
||
{id:'basic',name:'Базовый',members:'1 пользователь',inc:['Заказы, календарь и клиенты','Каталог для работы с заказом','Базовая статистика и печать'],out:['Редактирование боксов','Предложения клиенту','Финансы и склад']},
|
||
{id:'professional',name:'Профессиональный',members:'до 3 пользователей',inc:['Всё из Базового','Производство, склад и закупки','Финансы, поставщики и маршруты','Фирменное оформление'],out:['Редактирование боксов','Предложения клиенту']},
|
||
{id:'full',name:'Полный',members:'без лимита',inc:['Все функции приложения','Редактирование каталога и боксов','Предложения клиенту и шаблоны','Команда, права и расширенные настройки'],out:[]}
|
||
];
|
||
m.innerHTML=`<div class="sun-saas-modal-card"><div class="sun-saas-modal-head"><div><h2>Тарифы</h2><div class="hint">${feature?`Для функции «${esc(FEATURE_LABELS[feature]||feature)}» нужен другой тариф.`:'Выберите набор функций для компании.'}</div></div><button type="button" data-close>×</button></div>${note?`<div class="sun-saas-upgrade">${esc(note)}</div>`:''}<div class="sun-saas-plans">${cards.map(p=>`<div class="sun-saas-plan ${snapshot?.plan_id===p.id?'current':''}"><h3>${p.name}</h3><div class="hint">${p.members}</div><ul>${p.inc.map(x=>`<li>${esc(x)}</li>`).join('')}${p.out.map(x=>`<li class="muted">— ${esc(x)}</li>`).join('')}</ul>${snapshot?.plan_id===p.id?'<b>Текущий тариф</b>':'<span class="hint">Подключение оплаты добавим на следующем этапе.</span>'}</div>`).join('')}</div><div class="sun-saas-upgrade">Сейчас тариф можно изменить из вашего SaaS-кабинета. Автоматическую оплату подключим отдельным этапом.</div></div>`;
|
||
document.body.appendChild(m);m.onclick=e=>{if(e.target===m||e.target.closest('[data-close]'))m.remove();};
|
||
}
|
||
|
||
function injectSettingsCard(){
|
||
if(!snapshot)return;
|
||
const settings=$('enterprise-settings');if(!settings)return;
|
||
let card=$('sunSaaSSettingsCardV16');if(!card){card=document.createElement('section');card.id='sunSaaSSettingsCardV16';card.className='enterprise-card wide sun-saas-settings-card';const first=settings.querySelector('.enterprise-grid')||settings;first.prepend(card);}
|
||
const end=endDate(),d=daysLeft();
|
||
card.innerHTML=`<h2>Тариф и подписка</h2><p class="hint">Подписка относится ко всей компании. Данные этой рабочей базы изолированы от других компаний.</p><div class="sun-saas-status-grid"><div class="sun-saas-kpi"><small>Тариф</small><b>${esc(snapshot.plan_name||PLAN_LABELS[snapshot.plan_id])}</b></div><div class="sun-saas-kpi"><small>Статус</small><b>${esc(statusLabel(snapshot.status))}</b></div><div class="sun-saas-kpi"><small>${snapshot.status==='trialing'?'Пробный период до':'Оплачено до'}</small><b>${formatDate(end)}${d!=null&&d>=0?` · ${d} дн.`:''}</b></div><div class="sun-saas-kpi"><small>Сотрудники</small><b>${Number(snapshot.member_count||0)} / ${snapshot.max_members==null?'∞':snapshot.max_members}</b></div></div><div class="actions" style="margin-top:12px"><button class="outline" type="button" data-saas-plans>Сравнить тарифы</button>${snapshot.platform_admin?'<button class="primary" type="button" data-saas-admin>Управление SaaS</button>':''}</div>`;
|
||
card.querySelector('[data-saas-plans]').onclick=()=>showPlans();card.querySelector('[data-saas-admin]')?.addEventListener('click',openPlatformAdmin);
|
||
}
|
||
|
||
function ensureAdminView(){
|
||
if($('sun-saas-admin'))return $('sun-saas-admin');
|
||
const v=document.createElement('section');v.id='sun-saas-admin';v.className='view page';document.body.appendChild(v);return v;
|
||
}
|
||
function ensureAdminNav(){
|
||
const nav=document.querySelector('header nav');if(!nav||!snapshot?.platform_admin)return;
|
||
if(window.SunDeveloperV22){$('sunSaaSAdminNavV16')?.remove();window.SunDeveloperV22.ensureNav?.();return;}
|
||
let b=$('sunSaaSAdminNavV16');if(!b){b=document.createElement('button');b.id='sunSaaSAdminNavV16';b.type='button';b.dataset.navLabel='SaaS';b.textContent='SaaS';nav.appendChild(b);b.onclick=()=>openPlatformAdmin(b);}
|
||
b.hidden=false;b.style.display='';
|
||
}
|
||
async function openPlatformAdmin(btn=null){
|
||
if(window.SunDeveloperV22?.open)return window.SunDeveloperV22.open(btn);
|
||
if(!snapshot?.platform_admin)return;
|
||
const v=ensureAdminView();qa('.view').forEach(x=>x.classList.remove('on'));v.classList.add('on');qa('header nav button').forEach(x=>x.classList.remove('on'));(btn||$('sunSaaSAdminNavV16'))?.classList.add('on');
|
||
v.innerHTML='<div class="sun-saas-admin-head"><div><h1>SaaS · компании</h1><p class="hint">Тарифы, сроки и доступы всех компаний.</p></div><button class="outline" type="button" data-reload>Обновить</button></div><p class="hint">Загружаю компании…</p>';
|
||
v.querySelector('[data-reload]').onclick=()=>loadAdminRows();await loadAdminRows();
|
||
}
|
||
async function loadAdminRows(){
|
||
const v=ensureAdminView(),c=client();if(!c)return;
|
||
const keepHead=v.querySelector('.sun-saas-admin-head')?.outerHTML||'';v.innerHTML=keepHead+'<p class="hint">Загружаю компании…</p>';v.querySelector('[data-reload]')?.addEventListener('click',loadAdminRows);
|
||
const {data,error}=await c.rpc('sun_dev_list_companies');if(error){v.insertAdjacentHTML('beforeend',`<p class="hint">${esc(error.message)}</p>`);return;}
|
||
adminRows=data||[];
|
||
const list=document.createElement('div');list.className='sun-saas-admin-table';list.innerHTML=adminRows.map(r=>adminRowHtml(r)).join('')||'<p class="hint">Компаний пока нет.</p>';v.appendChild(list);bindAdminRows(list);
|
||
}
|
||
function adminRowHtml(r){
|
||
const access=r.access_mode,cls=access==='full'?'good':access==='read_only'?'warn':'bad';
|
||
return `<section class="sun-saas-company" data-ws="${esc(r.workspace_id)}"><div class="sun-saas-company-top"><div><h3>${esc(r.workspace_name)}</h3><small>${esc(r.workspace_id)}</small></div><div class="sun-saas-company-meta"><span class="sun-saas-pill ${cls}">${esc(accessLabel(access))}</span><span class="sun-saas-pill">${esc(PLAN_LABELS[r.plan_id]||r.plan_name||'—')}</span><span class="sun-saas-pill">${Number(r.member_count||0)} / ${r.max_members==null?'∞':r.max_members} сотрудников</span><span class="sun-saas-pill">до ${formatDate(r.trial_ends_at||r.current_period_end)}</span></div></div><div class="sun-saas-company-actions"><label>Тариф<select data-plan><option value="basic" ${r.plan_id==='basic'?'selected':''}>Базовый</option><option value="professional" ${r.plan_id==='professional'?'selected':''}>Профессиональный</option><option value="full" ${r.plan_id==='full'?'selected':''}>Полный</option></select></label><label>Срок<select data-days><option value="30">30 дней</option><option value="90">90 дней</option><option value="365">1 год</option></select></label><button class="primary" type="button" data-activate>Активировать</button><button class="outline" type="button" data-expire>Заблокировать</button></div><details style="margin-top:10px"><summary>Индивидуальная функция</summary><div class="sun-saas-company-actions" style="grid-template-columns:1fr 120px auto"><label>Функция<select data-feature>${PLATFORM_FEATURES.map(f=>`<option value="${f}">${esc(FEATURE_LABELS[f]||f)}</option>`).join('')}</select></label><label>Срок<select data-feature-days><option value="30">30 дней</option><option value="90">90 дней</option><option value="365">1 год</option></select></label><button class="outline" type="button" data-feature-enable>Выдать доступ</button></div></details></section>`;
|
||
}
|
||
function bindAdminRows(root){
|
||
qa('.sun-saas-company',root).forEach(row=>{
|
||
const ws=row.dataset.ws;
|
||
row.querySelector('[data-activate]').onclick=async()=>{const plan=row.querySelector('[data-plan]').value,days=Number(row.querySelector('[data-days]').value);await setSubscription(ws,plan,days,'active');};
|
||
row.querySelector('[data-expire]').onclick=async()=>{if(!confirm('Заблокировать доступ этой компании? Данные не удалятся.'))return;await setSubscription(ws,row.querySelector('[data-plan]').value,0,'expired');};
|
||
row.querySelector('[data-feature-enable]').onclick=async()=>{const feature=row.querySelector('[data-feature]').value,days=Number(row.querySelector('[data-feature-days]').value);const r=await client().rpc('sun_dev_set_feature_override',{p_workspace:ws,p_feature:feature,p_enabled:true,p_days:days,p_note:'Выдано из SaaS-кабинета'});if(r.error)return toast(r.error.message,'error');toast('Индивидуальный доступ выдан.','success');};
|
||
});
|
||
}
|
||
async function setSubscription(ws,plan,days,status){
|
||
const {error}=await client().rpc('sun_dev_set_subscription',{p_workspace:ws,p_plan:plan,p_days:days,p_status:status});if(error)return toast(error.message,'error');toast('Подписка обновлена.','success');await loadAdminRows();if(workspace()?.id===ws)await refreshSnapshot(true);
|
||
}
|
||
|
||
function clearWorkspaceLocalData(){
|
||
const preserve=new Set(['sunCloudV2Config','sunCloudClientIdV2','sunStaticMapGeocodeCacheV2']);
|
||
const keys=[];for(let i=0;i<localStorage.length;i++){const k=localStorage.key(i);if(k?.startsWith('sun')&&!preserve.has(k))keys.push(k);}keys.forEach(k=>localStorage.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 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=`<div class="sun-cloud-auth-brand"><img src="sun-logo.png" alt=""><div><h2>Создайте компанию</h2><div class="hint">${esc(ss.user.email||'Аккаунт авторизован')} · 14 дней Полного тарифа бесплатно</div></div></div><p class="hint">У этого аккаунта пока нет рабочей базы. Приглашения временно отключены на период локального тестирования.</p><label>Название компании<input id="sunSaaSCompanyNameV16" value="Моя кейтеринговая компания" autocomplete="organization"></label><div class="sun-saas-onboarding-tabs"><button class="sun-saas-onboarding-choice" type="button" data-create="empty"><b>Новая компания</b><small>Готовый каталог боксов, но без заказов, клиентов и рабочих данных.</small></button><button class="sun-saas-onboarding-choice" type="button" data-create="demo"><b>Загрузить демо</b><small>Примеры боксов и один тестовый заказ.</small></button></div><div class="sun-cloud-auth-actions" style="margin-top:14px"><button class="outline" type="button" data-signout>Выйти</button></div><div class="sun-cloud-auth-error" id="sunSaaSGateErrorV16"></div>`;
|
||
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='Создаю рабочую базу…';
|
||
try{
|
||
const r=await client().rpc('sun_create_workspace',{p_name:name});if(r.error)throw r.error;
|
||
const workspaceId=String(r.data||'');
|
||
seedWorkspace(b.dataset.create);await cloud().adoptCurrentLocalAsWorkspace?.(workspaceId);await cloud().reloadMemberships();err.textContent='Загружаю стартовую базу…';await cloud().migrate();location.reload();
|
||
}catch(e){err.textContent=e?.message||String(e);b.disabled=false;}
|
||
});
|
||
card.querySelector('[data-signout]').onclick=async()=>{if(window.SunCloudV2?.signOut)return window.SunCloudV2.signOut();try{await client()?.auth.signOut()}catch(_){}location.reload();};
|
||
}
|
||
|
||
function applyAll(){installStyle();injectBadge();applyAccessMode();applyFeatureGates();guardFunctions();injectSettingsCard();ensureAdminNav();}
|
||
|
||
document.addEventListener('click',e=>{
|
||
const locked=e.target.closest('[data-sun-plan-locked]');if(locked){e.preventDefault();e.stopImmediatePropagation();return showUpgrade(locked.dataset.sunPlanLocked);}
|
||
const card=e.target.closest('.sun-saas-plan-card-locked');if(card){e.preventDefault();e.stopImmediatePropagation();return showUpgrade(card.dataset.sunFeature);}
|
||
if(snapshot?.access_mode==='read_only'||cloud()?.isSupportMode?.()){
|
||
const t=e.target.closest('button');if(!t)return;
|
||
const text=(t.textContent||'').trim();const safe=/^(Закрыть|Обновить|Предварительный просмотр|Печать|Скачать|Сравнить тарифы|Выйти|Назад|Фильтр|Поиск)/i.test(text);
|
||
const mutating=/Сохран|Удал|Добав|Создать|Измен|Настро|Списать|Отправ|Активир|Заблокир|Выдать|Примен/i.test(text);
|
||
if(mutating&&!safe){e.preventDefault();e.stopImmediatePropagation();showUpgrade(null,cloud()?.isSupportMode?.()?'Режим поддержки: данные компании доступны только для просмотра.':'Льготный период: база доступна только для просмотра.');}
|
||
}
|
||
},true);
|
||
|
||
function showUpgrade(feature,note=''){showPlans(feature,note);}
|
||
|
||
function boot(){
|
||
installStyle();
|
||
const timer=setInterval(async()=>{
|
||
if(document.hidden)return;
|
||
// v17.5.27: onboarding is handled by the unified Caterium auth gate.
|
||
const ws=workspace();
|
||
if(ws?.id&&(ws.id!==lastWorkspaceId||!snapshot))await refreshSnapshot(true);
|
||
else if(ws?.id&&snapshot){applyFeatureGates();guardFunctions();injectSettingsCard();ensureAdminNav();}
|
||
else if(!ws){snapshot=null;lastWorkspaceId='';}
|
||
},5000);
|
||
window.addEventListener('sun:cloud-state-applied',()=>refreshSnapshot(true));
|
||
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>{applyFeatureGates();guardFunctions();},50));
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(()=>{injectSettingsCard();applyFeatureGates();},100);},true);
|
||
setTimeout(()=>refreshSnapshot(true),1200);
|
||
}
|
||
|
||
window.SunSaaSV16={VERSION,getSnapshot:()=>snapshot?structuredClone(snapshot):null,refresh:()=>refreshSnapshot(true),hasFeature,isWritable,showPlans,openAdmin:openPlatformAdmin};
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: settings-polish-v1.js ===== */
|
||
/* Sun Catering v17.5.4: settings layout polish */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunSettingsPolishV1)return;
|
||
window.__sunSettingsPolishV1=true;
|
||
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-settings-polish-v1-style';
|
||
style.textContent=`
|
||
#enterprise-settings>.catalog-head{
|
||
position:sticky;top:0;z-index:45;
|
||
margin:0 -4px 14px;padding:11px 4px 12px;
|
||
background:linear-gradient(180deg,rgba(255,255,255,.99) 0%,rgba(255,255,255,.96) 80%,rgba(255,255,255,0) 100%);
|
||
backdrop-filter:blur(9px);
|
||
}
|
||
#enterprise-settings .sun-settings-cloud-tail{grid-column:1/-1!important}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
function settingsNavButton(){
|
||
return qa('header nav button').find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')||null;
|
||
}
|
||
function scrollSettingsTop(){
|
||
const view=$('enterprise-settings');
|
||
try{window.scrollTo({top:0,left:0,behavior:'auto'})}catch(_){window.scrollTo(0,0)}
|
||
try{document.scrollingElement&&(document.scrollingElement.scrollTop=0)}catch(_){}
|
||
try{view&&(view.scrollTop=0)}catch(_){}
|
||
view?.querySelector('.catalog-head')?.scrollIntoView?.({block:'start',inline:'nearest'});
|
||
}
|
||
|
||
function auditCard(grid){
|
||
return qa(':scope > .enterprise-card',grid).find(card=>(card.querySelector(':scope > h2')?.textContent||'').trim()==='История изменений')||null;
|
||
}
|
||
function cloudCard(grid){
|
||
return $('sunCloudV2Card')||qa(':scope > .enterprise-card',grid).find(card=>/^(Облачная синхронизация|Облако Supabase)$/.test((card.querySelector(':scope > h2')?.textContent||'').trim()))||null;
|
||
}
|
||
function moveBefore(node,anchor){
|
||
if(!node||!anchor||node===anchor||node.parentElement!==anchor.parentElement)return false;
|
||
if(node.nextElementSibling===anchor)return false;
|
||
window.SunSafe.insertBefore(anchor.parentElement,node,anchor);return true;
|
||
}
|
||
|
||
let ordering=false;
|
||
function orderSettingsCards(){
|
||
if(ordering)return;
|
||
const grid=document.querySelector('#enterprise-settings .enterprise-grid');if(!grid)return;
|
||
ordering=true;
|
||
try{
|
||
const audit=auditCard(grid);
|
||
if(!audit)return;
|
||
const requisites=$('sunReceiptSettingsCard');
|
||
const qr=$('sunDefaultQrCard');
|
||
const blank=$('sunBlankSettingsCardV3');
|
||
const cloud=cloudCard(grid);
|
||
if(cloud)cloud.classList.add('sun-settings-cloud-tail','wide');
|
||
|
||
// Bottom operational block: company requisites -> QR -> order blank -> cloud -> history.
|
||
// Reinsert in the desired order so dynamically injected cards settle into one stable tail block.
|
||
[requisites,qr,blank,cloud].forEach(card=>{if(card&&grid.contains(card))moveBefore(card,audit)});
|
||
}finally{ordering=false;}
|
||
}
|
||
|
||
let timer=0;
|
||
function scheduleOrder(delay=40){clearTimeout(timer);timer=setTimeout(orderSettingsCards,delay)}
|
||
|
||
function boot(){
|
||
const btn=settingsNavButton();
|
||
btn?.addEventListener('click',()=>{
|
||
scrollSettingsTop();
|
||
setTimeout(scrollSettingsTop,0);
|
||
setTimeout(scrollSettingsTop,100);
|
||
scheduleOrder(160);
|
||
scheduleOrder(420);
|
||
},true);
|
||
const view=$('enterprise-settings');
|
||
if(view){
|
||
new MutationObserver(()=>scheduleOrder(30)).observe(view,{childList:true,subtree:true});
|
||
if(view.classList.contains('on')){scrollSettingsTop();scheduleOrder(120)}
|
||
}
|
||
window.addEventListener('sun:cloud-state-applied',()=>scheduleOrder(80));
|
||
window.addEventListener('suncloudsync',()=>scheduleOrder(80));
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
;
|
||
|
||
/* ===== MODULE: address-verify-v1.js ===== */
|
||
/* Sun Catering v17.5.4: explicit precise-address verification */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunAddressVerifyV1)return;
|
||
window.__sunAddressVerifyV1=true;
|
||
|
||
const $=id=>document.getElementById(id);
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const val=id=>String($(id)?.value||'').trim();
|
||
const CACHE_KEY='sunPreciseAddressSearchCacheV1';
|
||
let lastRequest=0,selected=null,searchSeq=0,applying=false;
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-address-verify-v1-style';
|
||
style.textContent=`
|
||
.sun-address-verify{margin-top:10px;border-top:1px solid #e5e8eb;padding-top:10px}
|
||
.sun-address-verify-note{font-size:11px;line-height:1.45;color:#6d7782;margin:0 0 9px}
|
||
.sun-address-candidates{display:grid;gap:7px}
|
||
.sun-address-candidate{width:100%;text-align:left;border:1px solid #dfe4e8;border-radius:10px;background:#fff;padding:9px 10px;cursor:pointer;color:#2a3541}
|
||
.sun-address-candidate:hover{border-color:#b9c7d0;background:#fbfcfd}
|
||
.sun-address-candidate.on{border-color:#d2a438;box-shadow:0 0 0 2px rgba(210,164,56,.15);background:#fffdf7}
|
||
.sun-address-candidate b{display:block;font-size:12px;line-height:1.35}.sun-address-candidate small{display:block;margin-top:3px;color:#75808a;line-height:1.35}
|
||
.sun-address-candidate .sun-address-chips{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px}.sun-address-candidate .sun-address-chips span{font-size:9px;font-weight:800;padding:3px 6px;border-radius:999px;background:#eef2f4;color:#53616c}
|
||
.sun-address-selected{margin-top:9px;padding:9px 10px;border:1px solid #e4e7e9;border-radius:10px;background:#fafbfc;font-size:11px;color:#59656f;line-height:1.45}
|
||
#orderGeoConfirm{display:none;margin-top:9px;width:100%;justify-content:center}.sun-address-verify.busy{opacity:.7;pointer-events:none}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
function readCache(){try{return JSON.parse(localStorage.getItem(CACHE_KEY)||'{}')||{}}catch(_){return{}}}
|
||
function writeCache(cache){try{localStorage.setItem(CACHE_KEY,JSON.stringify(cache))}catch(_){} }
|
||
function cleanAddressParts(a={}){
|
||
return {
|
||
region:String(a.state||a.region||'').trim(),
|
||
locality:String(a.city||a.town||a.village||a.municipality||a.hamlet||a.isolated_dwelling||'').trim(),
|
||
district:String(a.city_district||a.state_district||a.county||a.borough||a.suburb||'').trim(),
|
||
street:String(a.road||a.pedestrian||a.residential||a.footway||a.path||'').trim(),
|
||
house:String(a.house_number||'').trim(),
|
||
postcode:String(a.postcode||'').trim()
|
||
};
|
||
}
|
||
function queryParts(){
|
||
const region=val('deliveryRegion')==='Другой регион'?'':val('deliveryRegion');
|
||
return [region,val('deliveryLocality'),val('deliveryDistrict'),val('deliveryStreet'),val('deliveryHouse')?`д. ${val('deliveryHouse')}`:'',val('deliveryCorpus')?`корп. ${val('deliveryCorpus')}`:''].filter(Boolean);
|
||
}
|
||
function queryText(){return queryParts().join(', ')}
|
||
function mapAddress(){return queryText()}
|
||
|
||
async function fetchJson(url,timeout=6500){
|
||
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),timeout);
|
||
try{const r=await fetch(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default',signal:controller.signal});if(!r.ok)throw new Error(`HTTP ${r.status}`);return await r.json();}
|
||
finally{clearTimeout(timer)}
|
||
}
|
||
async function searchNominatim(query){
|
||
const wait=Math.max(0,1100-(Date.now()-lastRequest));if(wait)await new Promise(r=>setTimeout(r,wait));lastRequest=Date.now();
|
||
const region=val('deliveryRegion');
|
||
const params=new URLSearchParams({format:'jsonv2',addressdetails:'1',limit:'5',countrycodes:'ru','accept-language':'ru',q:query});
|
||
if(region==='Санкт-Петербург'||region==='Ленинградская область'){
|
||
params.set('viewbox','27.3,61.4,34.5,58.0');params.set('bounded','1');
|
||
}else if(!region){params.set('viewbox','27.3,61.4,34.5,58.0');}
|
||
const data=await fetchJson(`https://nominatim.openstreetmap.org/search?${params.toString()}`);
|
||
return (Array.isArray(data)?data:[]).filter(r=>Number.isFinite(Number(r.lat))&&Number.isFinite(Number(r.lon))).map(r=>({
|
||
lat:Number(r.lat),lon:Number(r.lon),formatted:String(r.display_name||query),provider:'nominatim',address:r.address||{},parts:cleanAddressParts(r.address||{}),importance:Number(r.importance||0)
|
||
}));
|
||
}
|
||
async function searchPhoton(query){
|
||
const data=await fetchJson(`https://photon.komoot.io/api/?limit=5&lang=ru&q=${encodeURIComponent(query)}`);
|
||
return (Array.isArray(data?.features)?data.features:[]).map(f=>{
|
||
const c=f?.geometry?.coordinates,p=f?.properties||{};if(!Array.isArray(c)||!Number.isFinite(Number(c[0]))||!Number.isFinite(Number(c[1])))return null;
|
||
const address={state:p.state||'',city:p.city||'',town:p.city||'',county:p.county||'',road:p.street||p.name||'',house_number:p.housenumber||'',postcode:p.postcode||''};
|
||
return {lat:Number(c[1]),lon:Number(c[0]),formatted:[p.name,p.housenumber,p.street,p.district,p.city,p.county,p.state,p.postcode].filter(Boolean).join(', ')||query,provider:'photon',address,parts:cleanAddressParts(address),importance:0};
|
||
}).filter(Boolean);
|
||
}
|
||
async function searchCandidates(query){
|
||
const key=query.toLowerCase(),cache=readCache();
|
||
if(Array.isArray(cache[key])&&cache[key].length)return cache[key];
|
||
let rows=[];try{rows=await searchNominatim(query)}catch(_){rows=[]}
|
||
if(!rows.length){try{rows=await searchPhoton(query)}catch(_){rows=[]}}
|
||
if(rows.length){cache[key]=rows.slice(0,5);const keys=Object.keys(cache);if(keys.length>80)keys.slice(0,keys.length-80).forEach(k=>delete cache[k]);writeCache(cache)}
|
||
return rows.slice(0,5);
|
||
}
|
||
|
||
function candidateTitle(c){
|
||
const p=c.parts||{};
|
||
return [p.street,p.house?`д. ${p.house}`:'',p.locality].filter(Boolean).join(', ')||c.formatted;
|
||
}
|
||
function candidateMeta(c){
|
||
const p=c.parts||{};
|
||
return [p.district,p.region,p.postcode].filter(Boolean).join(' · ')||c.formatted;
|
||
}
|
||
function renderCandidates(rows){
|
||
const host=$('sunAddressCandidates');if(!host)return;
|
||
if(!rows.length){host.innerHTML='<div class="sun-address-selected">Точный вариант не найден. Добавьте населённый пункт или район и попробуйте ещё раз.</div>';return}
|
||
host.innerHTML=rows.map((c,i)=>`<button type="button" class="sun-address-candidate" data-address-candidate="${i}"><b>${esc(candidateTitle(c))}</b><small>${esc(candidateMeta(c))}</small><span class="sun-address-chips">${c.parts?.locality?`<span>${esc(c.parts.locality)}</span>`:''}${c.parts?.district?`<span>${esc(c.parts.district)}</span>`:''}${c.parts?.region?`<span>${esc(c.parts.region)}</span>`:''}</span></button>`).join('');
|
||
host.querySelectorAll('[data-address-candidate]').forEach(btn=>btn.addEventListener('click',()=>selectCandidate(rows[Number(btn.dataset.addressCandidate)],btn)));
|
||
}
|
||
function setExistingStatus(text,kind='warn'){
|
||
const s=$('orderGeoStatus');if(s){s.className=`geo-order-status ${kind}`;const span=s.querySelector('span');if(span)span.textContent=text}
|
||
}
|
||
function selectCandidate(candidate,button){
|
||
selected=candidate;
|
||
document.querySelectorAll('.sun-address-candidate').forEach(x=>x.classList.toggle('on',x===button));
|
||
const confirm=$('orderGeoConfirm');if(confirm){confirm.style.display='inline-flex';confirm.disabled=false}
|
||
const selectedBox=$('sunAddressSelected');if(selectedBox)selectedBox.innerHTML=`<b>Выбран вариант:</b><br>${esc(candidate.formatted)}<br><span>Проверьте точку ниже и нажмите «Адрес верный — использовать».</span>`;
|
||
const mini=$('orderGeoMini');if(mini&&window.SunStaticMap?.mediaHtml)mini.innerHTML=window.SunStaticMap.mediaHtml(candidate,'Проверка точного адреса')+'<div class="static-map-credit">© OpenStreetMap contributors</div>';
|
||
const precision=$('orderGeoPrecision');if(precision)precision.textContent=`${candidate.lat.toFixed(5)}, ${candidate.lon.toFixed(5)}`;
|
||
setExistingStatus('Проверьте выбранную точку','warn');
|
||
}
|
||
function setField(id,value,{onlyIf=false}={}){const el=$(id);if(!el||!value)return;if(onlyIf&&String(el.value||'').trim())return;el.value=value;el.dispatchEvent(new Event(el.tagName==='SELECT'?'change':'input',{bubbles:true}))}
|
||
function normalizeRegion(region){
|
||
if(/санкт[- ]?петербург/i.test(region))return 'Санкт-Петербург';
|
||
if(/ленинград/i.test(region))return 'Ленинградская область';
|
||
return region||'';
|
||
}
|
||
function composeFullAddress(){
|
||
if(typeof window.sunComposeDeliveryAddress==='function')return window.sunComposeDeliveryAddress();
|
||
const parts=[val('deliveryRegion'),val('deliveryLocality'),val('deliveryDistrict'),val('deliveryStreet'),val('deliveryHouse')?`д. ${val('deliveryHouse')}`:'',val('deliveryCorpus')?`корп. ${val('deliveryCorpus')}`:'',val('deliveryEntrance')?`подъезд ${val('deliveryEntrance')}`:'',val('deliveryFloor')?`этаж ${val('deliveryFloor')}`:'',val('deliveryApartment')?`кв. ${val('deliveryApartment')}`:'',val('deliveryOffice')?`офис ${val('deliveryOffice')}`:''].filter(Boolean);
|
||
const a=parts.join(', ');if($('address'))$('address').value=a;return a;
|
||
}
|
||
function getDraft(){try{return draft}catch(_){return null}}
|
||
function getOrders(){try{return orders}catch(_){return[]}}
|
||
function confirmCandidate(){
|
||
if(!selected)return;
|
||
const candidate=selected,p=candidate.parts||{};
|
||
applying=true;
|
||
const region=normalizeRegion(p.region);
|
||
if(region){
|
||
const regionSelect=$('deliveryRegion');
|
||
if(regionSelect&&[...regionSelect.options].some(o=>o.value===region||o.text===region)){regionSelect.value=region;regionSelect.dispatchEvent(new Event('change',{bubbles:true}))}
|
||
else if(regionSelect){const option=new Option(region,region);regionSelect.add(option);regionSelect.value=region;regionSelect.dispatchEvent(new Event('change',{bubbles:true}))}
|
||
}
|
||
const localityValue=(region==='Санкт-Петербург'&&/санкт[- ]?петербург/i.test(p.locality||''))?'':(p.locality||'');
|
||
const localityEl=$('deliveryLocality');if(localityEl){localityEl.value=localityValue;localityEl.dispatchEvent(new Event('input',{bubbles:true}))}
|
||
setField('deliveryDistrict',p.district||'',{onlyIf:false});
|
||
setField('deliveryStreet',p.street||val('deliveryStreet'),{onlyIf:false});
|
||
setField('deliveryHouse',p.house||val('deliveryHouse'),{onlyIf:false});
|
||
const full=composeFullAddress();
|
||
const d=getDraft();
|
||
if(d){
|
||
d.deliveryRegion=val('deliveryRegion');d.deliveryLocality=val('deliveryLocality');d.deliveryDistrict=val('deliveryDistrict');d.deliveryStreet=val('deliveryStreet');d.deliveryHouse=val('deliveryHouse');d.deliveryCorpus=val('deliveryCorpus');d.address=full;
|
||
d.geo={lat:candidate.lat,lon:candidate.lon,formatted:candidate.formatted,sourceMapAddress:mapAddress(),sourceAddress:full,verifiedAt:new Date().toISOString(),confirmedAt:new Date().toISOString(),confirmed:true,provider:candidate.provider||'nominatim',geoVersion:3};
|
||
if(d.id){const saved=getOrders().find(o=>String(o.id)===String(d.id));if(saved){Object.assign(saved,{deliveryRegion:d.deliveryRegion,deliveryLocality:d.deliveryLocality,deliveryDistrict:d.deliveryDistrict,deliveryStreet:d.deliveryStreet,deliveryHouse:d.deliveryHouse,deliveryCorpus:d.deliveryCorpus,address:full,geo:structuredClone(d.geo)});try{persist()}catch(_){}}}
|
||
}
|
||
applying=false;
|
||
if($('address'))$('address').dispatchEvent(new Event('input',{bubbles:true}));
|
||
setTimeout(()=>{
|
||
setExistingStatus('Адрес подтверждён','ok');
|
||
const precision=$('orderGeoPrecision');if(precision)precision.textContent=candidate.formatted;
|
||
const confirm=$('orderGeoConfirm');if(confirm){confirm.textContent='Адрес подтверждён ✓';confirm.disabled=true}
|
||
const create=$('orderGeoCreate');if(create)create.textContent='Проверить другой адрес';
|
||
try{window.SunEnterprise?.toast?.('Точка доставки подтверждена.','success')}catch(_){}
|
||
},20);
|
||
}
|
||
|
||
async function runSearch(){
|
||
const query=queryText();
|
||
if(!val('deliveryStreet')||!val('deliveryHouse')){try{window.SunEnterprise?.toast?.('Для точной проверки укажите улицу и дом.','warn')}catch(_){alert('Для точной проверки укажите улицу и дом.')}return}
|
||
const root=$('sunAddressVerify');if(root)root.classList.add('busy');
|
||
const seq=++searchSeq;selected=null;
|
||
const confirm=$('orderGeoConfirm');if(confirm){confirm.style.display='none';confirm.disabled=false;confirm.textContent='Адрес верный — использовать'}
|
||
const host=$('sunAddressCandidates');if(host)host.innerHTML='<div class="sun-address-selected">Ищу подходящие адреса…</div>';
|
||
setExistingStatus('Идёт поиск точного адреса','warn');
|
||
try{
|
||
const rows=await searchCandidates(query);if(seq!==searchSeq)return;renderCandidates(rows);setExistingStatus(rows.length?'Выберите точный вариант':'Адрес не найден',rows.length?'warn':'warn');
|
||
}finally{root?.classList.remove('busy')}
|
||
}
|
||
|
||
function resetVerification(){
|
||
if(applying)return;
|
||
selected=null;searchSeq++;
|
||
const host=$('sunAddressCandidates');if(host)host.innerHTML='';
|
||
const box=$('sunAddressSelected');if(box)box.innerHTML='';
|
||
const confirm=$('orderGeoConfirm');if(confirm){confirm.style.display='none';confirm.disabled=false;confirm.textContent='Адрес верный — использовать'}
|
||
const d=getDraft();if(d?.geo?.confirmed){d.geo=null}
|
||
}
|
||
|
||
function boot(){
|
||
const geo=$('orderGeoBox'),create=$('orderGeoCreate');if(!geo||!create)return;
|
||
let root=$('sunAddressVerify');
|
||
if(!root){
|
||
root=document.createElement('div');root.id='sunAddressVerify';root.className='sun-address-verify';
|
||
root.innerHTML='<p class="sun-address-verify-note">Чтобы не перепутать одинаковые улицы в разных населённых пунктах, нажмите «Проверить адрес», выберите полный вариант и подтвердите точку на карте.</p><div id="sunAddressCandidates" class="sun-address-candidates"></div><div id="sunAddressSelected" class="sun-address-selected" style="display:none"></div><div style="margin-top:8px;font-size:10px;color:#7a838a">Поиск адреса: OpenStreetMap/Nominatim. © OpenStreetMap contributors.</div>';
|
||
const mini=$('orderGeoMini');if(mini)window.SunSafe.insertBefore(geo,root,mini);else geo.appendChild(root);
|
||
const selectedBox=$('sunAddressSelected');new MutationObserver(()=>{selectedBox.style.display=selectedBox.textContent.trim()?'block':'none'}).observe(selectedBox,{childList:true,subtree:true,characterData:true});
|
||
}
|
||
let confirm=$('orderGeoConfirm');
|
||
if(!confirm){confirm=document.createElement('button');confirm.id='orderGeoConfirm';confirm.type='button';confirm.className='primary';confirm.textContent='Адрес верный — использовать';$('orderGeoMini')?.insertAdjacentElement('afterend',confirm);confirm.addEventListener('click',confirmCandidate)}
|
||
create.textContent='Проверить адрес';
|
||
// Capture before the legacy handler so the app no longer silently accepts the first geocoder result.
|
||
create.addEventListener('click',e=>{e.preventDefault();e.stopImmediatePropagation();runSearch()},true);
|
||
['deliveryRegion','deliveryLocality','deliveryDistrict','deliveryStreet','deliveryHouse','deliveryCorpus'].forEach(id=>$(id)?.addEventListener(id==='deliveryRegion'?'change':'input',resetVerification));
|
||
$('address')?.addEventListener('input',()=>setTimeout(()=>{const d=getDraft();if(create&&!d?.geo?.confirmed)create.textContent='Проверить адрес'},0));
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>setTimeout(boot,50),{once:true});else setTimeout(boot,50);
|
||
})();
|
||
;
|
||
|
||
|
||
/* ===== MODULE: catalog-photo-resolver-v17.5.14.js ===== */
|
||
(function(){
|
||
'use strict';
|
||
if(window.__sunCatalogPhotoResolver17514)return;window.__sunCatalogPhotoResolver17514=true;
|
||
const SOURCE_URLS=["https://static.tildacdn.com/stor3131-6532-4762-a534-353638663730/11f4ce48f250257ec824a12e1c541150.jpg","https://static.tildacdn.com/stor6634-3466-4635-a164-386637346334/e94521aba11d1824218728cee4312c03.jpg","https://static.tildacdn.com/stor3034-3036-4334-b233-303363326236/d72f45ce6d6a044d8d892a0718f29d89.jpg","https://static.tildacdn.com/stor3662-3435-4561-b031-663437646233/efd5e71d2a8654b0afa26a6dd7a0595a.jpg","https://static.tildacdn.com/stor6133-6537-4533-a166-666430373166/014bff38be088e524c0a4fd2ee1daab3.jpg","https://static.tildacdn.com/stor6161-3163-4032-b338-363935363839/7b9abb2bc27c854236e234383f044c20.jpg","https://static.tildacdn.com/stor3065-6462-4666-b134-376564613831/527188ad5747060eaa88cdd6b033e1b7.jpg","https://static.tildacdn.com/stor6238-3635-4936-a361-343062343130/6c2841340110a32dee16481cf5776858.jpg","https://static.tildacdn.com/stor6231-3837-4666-b564-656661306534/a9b0efe3696fa0cd6530e3100cce2447.jpg","https://static.tildacdn.com/stor3162-6131-4337-b731-376266363535/6998754c49e80fca1df239d01e91b239.jpg","https://static.tildacdn.com/stor6264-3139-4939-b331-623963306665/c62d84a2c9de7fd115f9878617709a99.jpg","https://static.tildacdn.com/stor3134-6638-4635-b032-623233383830/413674a63540f26bbcded51a2f37c3a7.jpg","https://static.tildacdn.com/stor6438-6236-4831-b038-346437643037/b7305f8e759e161407a9996e0218cf83.jpg","https://static.tildacdn.com/stor3134-6638-4635-b032-623233383830/413674a63540f26bbcded51a2f37c3a7.jpg","https://static.tildacdn.com/stor3536-6463-4535-a135-653763366634/3b6eec9a6310eea531c8cb70b00955a9.jpg","https://static.tildacdn.com/stor3339-6634-4634-b537-363366353636/7ef8a4de7cfc055f633ab29ad7e5fca4.jpg","https://static.tildacdn.com/stor3030-3766-4661-b364-313536363731/f7a5bb4329e864b12eb5aed656c13c2b.jpg","https://static.tildacdn.com/stor3865-3463-4832-a531-326663623661/0ab2f6a1959adeb68f6c64e21fc52e6c.jpg","https://static.tildacdn.com/stor3834-6339-4234-a638-656236303531/d70c6ff9606b8f29f4d0a334643c6fa5.jpg","https://static.tildacdn.com/stor3038-3736-4564-a635-333961643763/ae631a3930ee087c993f0eb5ae477388.jpg","https://static.tildacdn.com/stor3638-3139-4562-b430-356363633431/a0eae79a99b4f1ca59be8a445d975f94.jpg","https://static.tildacdn.com/stor3536-3766-4033-b235-643062373262/1e521931b938e17b0ebe397a447067bb.jpg","https://static.tildacdn.com/stor3966-6635-4139-b965-666363393466/df421e8c29457bb6a660810c14d4dbca.jpg","https://static.tildacdn.com/stor3133-6661-4839-b565-376433313839/471052a775dfa8fae813f1b043171e21.jpg","https://static.tildacdn.com/stor3539-3831-4031-b532-323963396137/235a566973dc035faa18ef9a57b4bef6.jpg","https://static.tildacdn.com/stor6362-6234-4063-b636-373636373465/6c868c53823c2c2799bacbef8b7ec5dc.jpg","https://static.tildacdn.com/stor3439-6237-4364-a566-666531346532/a368a54d5dd3ef381bdb587f81dd3953.jpg","https://static.tildacdn.com/stor6664-6363-4635-b632-383362383935/f5779eef4eaff67dec0cb8a9868fe912.jpg","https://static.tildacdn.com/stor6166-3066-4535-b333-356263356363/25951fa13b57555854635d8fe1a1ecc9.jpg","https://static.tildacdn.com/stor3131-3637-4837-b039-386331346435/c4fd5306cc629e81fe974d0e1238eb06.jpg","https://static.tildacdn.com/stor3235-3535-4639-b164-663664323936/c3dc9dc2931b0f950034495dd8cc6b46.jpg","https://static.tildacdn.com/stor3235-3661-4038-b035-613032303361/6f2c3ac522ad9d7e73d14f3fc4c47630.jpg","https://static.tildacdn.com/stor6166-3066-4535-b333-356263356363/25951fa13b57555854635d8fe1a1ecc9.jpg","https://static.tildacdn.com/stor3836-3438-4835-a132-663962396637/fac8bc386bd98959d75c6b71cd83471b.jpg","https://static.tildacdn.com/stor3334-6361-4633-a266-643132323266/74b63ae226e1ebfbd116a822be4f642e.jpg","https://static.tildacdn.com/stor3136-3834-4562-b037-396264363263/e8c1bbdff0574f52a2184a33d6f2115c.jpg","https://static.tildacdn.com/stor6334-3534-4266-a664-356162323565/c13d0896543e32d12ccad0c0a108dac5.jpg","https://static.tildacdn.com/stor3364-6237-4561-b432-326230363433/3233d197bebae6174651a7732ec8ca3e.jpg","https://static.tildacdn.com/stor3239-6231-4130-a363-643732356464/1a72939cda10b9fc447157193a2c69cf.jpg","https://static.tildacdn.com/stor3832-3732-4635-b732-636135393265/3e414b433c0aab18c0a1131d256c92e5.jpg","https://static.tildacdn.com/stor6431-3634-4935-a638-323064343638/8d7c3b547b6e5abd09fe498c2e0b3853.jpg","https://static.tildacdn.com/stor3362-3331-4631-b738-646138333763/b3932c942568e83ab0babc98b77ee5c3.jpg","https://static.tildacdn.com/stor3565-6565-4739-b638-343639336130/ce325e928513a5c7ad2a5c9328927bd0.jpg","https://static.tildacdn.com/stor3131-3335-4135-b962-356338643761/dabf8e59f8b405c2998cc725627a832a.jpg","https://static.tildacdn.com/stor6365-6464-4263-b066-653333643438/eba899d7b96c0e9f077467bb4076cf56.jpg","https://static.tildacdn.com/stor3064-6631-4061-b231-393166393931/2e792a2bd15012a2c134a17213d64706.jpg","https://static.tildacdn.com/stor3936-6435-4438-a564-643362376266/c0d42f2444a396f1b836f4f5e2d2a2b8.jpg","https://static.tildacdn.com/stor6635-6661-4361-b031-303465633437/9c824de9c9ad08c435f64d62876ca8f2.jpg","https://static.tildacdn.com/stor6530-6533-4235-b631-613831393533/2651153cdc6a396acfe9c7c1f1ffa6e3.jpg","https://static.tildacdn.com/stor3566-6437-4361-b137-343366323736/8f1f58eb858f669ca4acea85ad8bbbce.jpg","https://static.tildacdn.com/stor3363-3539-4532-b731-343434323163/3d114d5bd96795183d342a37ef5bacb7.jpg","https://static.tildacdn.com/stor3234-6630-4664-b230-616165303866/a350de68077dc437a4a7c0d1f07db97b.jpg","https://static.tildacdn.com/stor6566-6236-4038-b038-326331333761/acfb6d855c780c92c4cd6e667229a9d7.jpg","https://static.tildacdn.com/stor3662-3963-4031-b065-646235373965/f8f3bd5b10fe9142e453766305684126.jpg","https://static.tildacdn.com/stor3461-3363-4063-a530-336136633338/951f5c4bcaa35b878fd8f0d4cdf5812a.jpg","https://static.tildacdn.com/stor6335-3638-4432-b230-393163396334/d08deb92d2102c1e81002f0ff5b1cd18.jpg","https://static.tildacdn.com/stor3163-3035-4466-b435-303832663162/2a85461ba4c7132af333c481f6974334.jpg","https://static.tildacdn.com/stor3132-6233-4466-b164-376530633465/a5159b1de5f6bd3d2dde7609c1853f82.jpg","https://static.tildacdn.com/stor3165-6138-4663-b433-393039653034/4ad56410ecf6a163824061d08cb297da.jpg","https://static.tildacdn.com/stor6437-6361-4833-a537-623832376136/29ff2e5084a5f816954ea5621004b0c0.jpg","https://static.tildacdn.com/stor3765-3765-4663-a333-623738326235/02eaafb2bae5cef3edaa761aa7cea142.jpg","https://static.tildacdn.com/stor6365-3039-4931-a333-623061333865/ceb789e801d358ff78f2a4800f79f78c.jpg","https://static.tildacdn.com/stor6262-6636-4563-b332-383166313034/73a699e67040500f90edd197152cb0b9.jpg","https://static.tildacdn.com/stor3366-3663-4664-a136-633432326530/8a056937be29f8f531767a565fc2fb45.jpg","https://static.tildacdn.com/stor3566-3862-4334-b465-376164633737/7ad059a9bf198827dfd142008f81bb28.jpg","https://static.tildacdn.com/stor3532-6333-4432-b037-373031393364/3f233fc73a9932953876810c2530779b.jpg","https://static.tildacdn.com/stor6461-6235-4134-a630-393261313034/f00b5038885f425bf4463b0f070416b2.jpg","https://static.tildacdn.com/stor3963-3634-4262-b363-393536623037/7f3160b562bdb81243fcca8b07ea73ca.jpg","https://static.tildacdn.com/stor6134-6534-4032-b863-326334303439/b6299036af0105e24b66b92fc149fceb.jpg","https://static.tildacdn.com/stor6330-3331-4534-a431-616637323265/cc72f08366e2b3bb3b2cec181fcfb6b2.jpg","https://static.tildacdn.com/stor3864-6138-4939-a263-323633343831/e41074621630fb64c6551fde54a0491a.jpg","https://static.tildacdn.com/stor3530-3337-4465-a331-393235636139/6bed2df48d65e7dfdf673cc6ab1d66f0.jpg","https://static.tildacdn.com/stor3132-6432-4637-b930-353763333565/0e6665f2fddcdc6a2538ff0e4d6714e3.jpg","https://static.tildacdn.com/stor3063-6361-4562-b864-386439643866/5a0a9e8680146e4af16b0b3a71e5e344.jpg","https://static.tildacdn.com/stor3864-3766-4132-a362-336565363634/331d7a3cb423efad3851318ebc243503.jpg","https://static.tildacdn.com/stor3764-3435-4336-a638-386530643039/b66c7fb8fba795f74358798f008ff284.jpg","https://static.tildacdn.com/stor3764-3435-4336-a638-386530643039/b66c7fb8fba795f74358798f008ff284.jpg","https://static.tildacdn.com/stor3764-3435-4336-a638-386530643039/b66c7fb8fba795f74358798f008ff284.jpg","https://static.tildacdn.com/stor3764-3435-4336-a638-386530643039/b66c7fb8fba795f74358798f008ff284.jpg","https://static.tildacdn.com/stor3661-6362-4365-a166-396432366336/b3461ed10ad1d21ba6f327e14f87561a.jpg","https://static.tildacdn.com/stor6136-3763-4363-b632-613035666139/b89d5f88da96f52526ccba83cfb1e631.jpg","https://static.tildacdn.com/stor3764-3435-4336-a638-386530643039/b66c7fb8fba795f74358798f008ff284.jpg","https://static.tildacdn.com/stor3764-3435-4336-a638-386530643039/b66c7fb8fba795f74358798f008ff284.jpg","https://static.tildacdn.com/stor3530-3337-4465-a331-393235636139/6bed2df48d65e7dfdf673cc6ab1d66f0.jpg","https://static.tildacdn.com/stor3535-3432-4638-b763-636162616233/f392e1f738f4ec51be28ac85ff7f34d1.jpg","https://static.tildacdn.com/stor6263-6138-4433-a639-386465356138/915334ed7ec0a973fc528d5b1bcddeb9.jpg","https://static.tildacdn.com/stor6161-3666-4430-b437-303230613863/b6c27133c86b74e669c4d8d14cddaa98.jpg","https://static.tildacdn.com/stor6433-6530-4938-a264-366133396635/26c04c584ac3ce62893f86e3f21ef6a4.jpg","https://static.tildacdn.com/stor3438-3336-4038-b563-636464663765/26624b9790a69468292e752308984af1.jpg","https://static.tildacdn.com/stor3430-3734-4935-a666-616161393566/92e1260daf92944a35c6ce5a64ca38f3.jpg","https://static.tildacdn.com/stor3937-3731-4331-a333-333266353435/018e8f56c6b0053168f0d0cf73872966.jpg","https://static.tildacdn.com/stor3939-3434-4061-a231-636661616561/607dc1f31bc1ca66fb1a611d49407aa5.jpg","https://static.tildacdn.com/stor3836-3436-4239-a536-353332396264/232fcd6e93bf0abc918ef73529cded4b.jpg","https://static.tildacdn.com/stor3065-3632-4533-b333-636566613663/7979f348481e979905836febdf7933a6.jpg","https://static.tildacdn.com/stor3761-3162-4461-b966-323139613763/08d95a9c71b402415f9479b3f9520892.jpg","https://static.tildacdn.com/stor3033-3137-4930-b264-666532373134/69925db7e39c25877ac58869c15037a6.jpg","https://static.tildacdn.com/stor3333-3839-4537-a236-623030313665/4130c8f6bd2b58b41c4edd83eee922a9.jpg","https://static.tildacdn.com/stor6230-3133-4564-b237-646535613637/a2a99a4eeb22cb33b5b5cea2a79a17cb.jpg","https://static.tildacdn.com/stor3137-6538-4132-b739-653932333233/15a1c8db984c5e68f357b2ee12a18c2a.jpg","https://static.tildacdn.com/stor3031-6132-4334-b330-633933383437/7e711a925bef78f54c6417cfed424f7b.jpg","https://static.tildacdn.com/stor6565-3862-4536-a531-396336343530/94e933aa7cb2ca8cc2f9a6975508c69b.jpg","https://static.tildacdn.com/stor3130-6665-4166-a562-346464663365/1ace83e57a93e4bd15c4117e6ebd10f4.jpg","https://static.tildacdn.com/stor6666-6336-4235-b437-663731323432/f733be8a64d1186ac1153c3eba271d59.jpg","https://static.tildacdn.com/stor6561-6664-4163-a232-323236383637/1b4aa616a89592851de475a7633530b2.jpg","https://static.tildacdn.com/stor3364-3366-4961-a230-393136633735/bd16ccb4240c13b0408fcd3cdb263c7f.jpg","https://static.tildacdn.com/stor3133-3966-4466-a633-353062663934/19cfdd0803e1252edb0a229fe8cc10e1.jpg","https://static.tildacdn.com/stor3862-3737-4631-a637-656265333935/be0ca89c31666c8d357814c69f0c1527.jpg","https://static.tildacdn.com/stor3730-3136-4137-b861-393762656436/9b8faf6a48cff9bc8b16cef36396d144.jpg","https://static.tildacdn.com/stor6137-3965-4334-a432-316165313635/7e2aebaffa009dfc1b7a580ceac8a640.jpg","https://static.tildacdn.com/stor3937-3432-4537-a233-646339376631/d0ca8846eba6810423c5421e7060c567.jpg","https://static.tildacdn.com/stor3437-6365-4139-b134-326232643133/8d0a33ea4f3ace73aa73679aa8ac3b4d.jpg","https://static.tildacdn.com/stor6334-3463-4166-b330-643438393235/49f88576b7fb7573735b20dc4161de96.jpg","https://static.tildacdn.com/stor3139-3866-4633-a234-663139643733/5c7060058669060670ed95fb719078c3.jpg"];
|
||
const pad=n=>String(Math.max(1,Math.min(113,Number(n)||0))).padStart(3,'0');
|
||
const indexFrom=value=>{
|
||
const s=String(value||'');
|
||
let m=s.match(/current-(\d{3})\.jpg/i);if(m)return Number(m[1]);
|
||
m=s.match(/catalog-20260902-(\d{3})/i);if(m)return Number(m[1]);
|
||
const clean=s.split(/[?#]/)[0],remoteIndex=SOURCE_URLS.findIndex(url=>url===clean);
|
||
return remoteIndex>=0?remoteIndex+1:0;
|
||
};
|
||
function indexFor(itemOrPath){
|
||
if(itemOrPath&&typeof itemOrPath==='object'){
|
||
const byPhoto=indexFrom(itemOrPath.photo);if(byPhoto)return byPhoto;
|
||
const byId=indexFrom(itemOrPath.id);if(byId)return byId;
|
||
const byOrder=Number(itemOrPath.catalogOrder||0);if(byOrder>=1&&byOrder<=113)return byOrder;
|
||
}
|
||
return indexFrom(itemOrPath);
|
||
}
|
||
function candidates(itemOrPath){
|
||
const idx=indexFor(itemOrPath);
|
||
if(!idx)return [String(itemOrPath&&typeof itemOrPath==='object'?itemOrPath.photo||'':itemOrPath||'')].filter(Boolean);
|
||
const local=`catalog/current-${pad(idx)}.jpg`;
|
||
const api=(location.protocol==='http:'||location.protocol==='https:')?`/api/catalog-photo?i=${idx}`:'';
|
||
const remote=SOURCE_URLS[idx-1]||'';
|
||
const requested=String(itemOrPath&&typeof itemOrPath==='object'?itemOrPath.photo||'':itemOrPath||'');
|
||
return [...new Set([requested,local,api,remote,'sun-logo.png'].filter(Boolean))];
|
||
}
|
||
function fallbackImage(img){
|
||
if(!(img instanceof HTMLImageElement))return;
|
||
let idx=Number(img.dataset.sunCatalogPhotoIndex||0)||indexFor(img.getAttribute('src')||img.src);
|
||
if(!idx)return;
|
||
img.dataset.sunCatalogPhotoIndex=String(idx);
|
||
const list=candidates(`catalog/current-${pad(idx)}.jpg`);
|
||
let step=Number(img.dataset.sunCatalogPhotoStep||0);
|
||
const currentAbs=(()=>{try{return new URL(img.getAttribute('src')||img.src,document.baseURI).href}catch(_){return String(img.src||'')}})();
|
||
while(step<list.length){
|
||
const next=list[step++];
|
||
let nextAbs=next;try{nextAbs=new URL(next,document.baseURI).href}catch(_){}
|
||
if(nextAbs===currentAbs)continue;
|
||
img.dataset.sunCatalogPhotoStep=String(step);
|
||
img.src=next;
|
||
return;
|
||
}
|
||
img.removeAttribute('data-sun-catalog-photo-index');
|
||
img.removeAttribute('data-sun-catalog-photo-step');
|
||
if(!/sun-logo\.png(?:$|[?#])/i.test(String(img.getAttribute('src')||'')))img.src='sun-logo.png';
|
||
}
|
||
document.addEventListener('error',event=>{const img=event.target;if(img instanceof HTMLImageElement)fallbackImage(img)},true);
|
||
function repairExisting(){document.querySelectorAll('img').forEach(img=>{if(indexFor(img.getAttribute('src')||'')&&img.complete&&img.naturalWidth===0)fallbackImage(img)})}
|
||
setTimeout(repairExisting,60);setTimeout(repairExisting,650);
|
||
window.SunCatalogPhoto={sourceUrls:SOURCE_URLS,indexFor,candidates,fallbackImage,repairExisting};
|
||
})();
|
||
|
||
/* ===== MODULE: settings-tabs-v21.js ===== */
|
||
/* Sun Catering v17.5.21: grouped Settings tabs without rewriting existing settings cards. */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.__sunSettingsTabsV21)return;
|
||
window.__sunSettingsTabsV21=true;
|
||
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const TAB_KEY='sunSettingsActiveTabV1';
|
||
const TABS=[
|
||
{id:'account',label:'Аккаунт',hint:'Профиль, компания, тариф, пользователи и история изменений.'},
|
||
{id:'appearance',label:'Оформление',hint:'Цвета интерфейса, боковой панели и готовые цветовые варианты.'},
|
||
{id:'offer',label:'Предложение',hint:'Шаблон и содержимое предложения клиенту.'},
|
||
{id:'orders',label:'Заказы',hint:'Статусы заказов и цвета состояния оплаты.'},
|
||
{id:'references',label:'Справочники',hint:'Типы мероприятий, источники клиентов и вкладки каталога.'},
|
||
{id:'documents',label:'Документы',hint:'Реквизиты чека, QR-код и бланк заказа.'}
|
||
];
|
||
const VALID=new Set(TABS.map(x=>x.id));
|
||
let active=(()=>{try{const v=localStorage.getItem(TAB_KEY);return VALID.has(v)?v:'account'}catch(_){return'account'}})();
|
||
let scheduled=0;
|
||
|
||
const style=document.createElement('style');
|
||
style.id='sun-settings-tabs-v21-style';
|
||
style.textContent=`
|
||
#enterprise-settings .sun-settings-tabs-wrap{margin:0 0 14px;min-width:0}
|
||
#enterprise-settings .sun-settings-tabs{display:flex;gap:6px;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin;padding:3px 2px 7px;white-space:nowrap}
|
||
#enterprise-settings .sun-settings-tab{flex:1 1 0;min-width:118px;border:1px solid #dce2e6;border-radius:10px;background:#fff;color:#53616c;padding:10px 12px;font-size:13px;font-weight:800;cursor:pointer;transition:background .15s ease,border-color .15s ease,color .15s ease,box-shadow .15s ease}
|
||
#enterprise-settings .sun-settings-tab:hover{background:#f7f9fa;border-color:#c8d1d7}
|
||
#enterprise-settings .sun-settings-tab.on{background:#15364c;color:#fff;border-color:#15364c;box-shadow:0 5px 14px rgba(21,54,76,.13)}
|
||
#enterprise-settings .sun-settings-tab:focus-visible{outline:3px solid rgba(212,154,42,.28);outline-offset:2px}
|
||
#enterprise-settings .sun-settings-tab-context{display:flex;gap:7px;align-items:baseline;flex-wrap:wrap;padding:2px 4px 0;color:#6a7680;font-size:12px;line-height:1.4}
|
||
#enterprise-settings .sun-settings-tab-context b{color:#15364c;font-size:13px}
|
||
#enterprise-settings .enterprise-card{order:var(--sun-settings-order,50)}
|
||
#enterprise-settings .enterprise-card[hidden]{display:none!important}
|
||
#enterprise-settings.on #sunBlankSettingsCardV3[hidden]{display:none!important}
|
||
#enterprise-settings .enterprise-grid{align-items:start}
|
||
#enterprise-settings #sunCatalogTabsSettingsCardV21 .sun-settings-catalog-summary{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0 2px}
|
||
#enterprise-settings #sunCatalogTabsSettingsCardV21 .sun-settings-catalog-chip{display:inline-flex;align-items:center;border-radius:999px;background:#f2f5f6;color:#52616c;padding:5px 8px;font-size:11px;font-weight:800}
|
||
@media(max-width:900px){
|
||
#enterprise-settings .sun-settings-tabs{margin-right:-4px;padding-right:16px}
|
||
#enterprise-settings .sun-settings-tab{flex:0 0 auto;min-width:126px}
|
||
#enterprise-settings .sun-settings-tab-context{padding-right:8px}
|
||
}
|
||
@media(max-width:520px){
|
||
#enterprise-settings .sun-settings-tabs-wrap{margin-bottom:11px}
|
||
#enterprise-settings .sun-settings-tab{min-width:116px;padding:9px 11px;font-size:12px}
|
||
#enterprise-settings .sun-settings-tab-context span{display:block;width:100%}
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
function heading(card){return String(card?.querySelector(':scope > h2')?.textContent||card?.querySelector('h2')?.textContent||'').trim()}
|
||
function classify(card){
|
||
const id=String(card?.id||''),h=heading(card);
|
||
if(id==='sunCloudV2Card'||id==='sunSaaSSettingsCardV16'||/^(Профиль и аккаунт|Облачная синхронизация|Тариф и подписка|Пользователи и роли|История изменений)$/.test(h)){
|
||
const order=id==='sunCloudV2Card'||/^(Профиль и аккаунт|Облачная синхронизация)$/.test(h)?10:id==='sunSaaSSettingsCardV16'||h==='Тариф и подписка'?20:h==='Пользователи и роли'?30:90;
|
||
return['account',order];
|
||
}
|
||
if(id==='sunBrandThemeSettingsCard'||/^(Цвета интерфейса|Настройки интерфейса|Оформление интерфейса)$/.test(h))return['appearance',10];
|
||
if(id==='sunOfferTemplateSettingsCard'||h==='Шаблон предложения клиенту')return['offer',10];
|
||
if(id==='sunClientOfferSettingsCard'||h==='Настройки предложения клиенту')return['offer',20];
|
||
if(id==='sunOrderPaymentColorsCard'||h==='Цвета оплаты заказов')return['orders',10];
|
||
if(id==='sunOrderStatusFeatureCard'||h==='Статусы заказов')return['orders',20];
|
||
if(id==='sunEventTypesSettingsCard'||h==='Типы мероприятий')return['references',10];
|
||
if(id==='sunLeadSourcesSettingsCard'||h==='Источники клиентов')return['references',20];
|
||
if(id==='sunCatalogTabsSettingsCardV21'||h==='Вкладки каталога')return['references',30];
|
||
if(id==='sunReceiptSettingsCard'||h==='Настройки реквизитов товарного чека')return['documents',10];
|
||
if(id==='sunDefaultQrCard'||h==='QR-код для бланка заказа')return['documents',20];
|
||
if(id==='sunBlankSettingsCardV3'||h==='Настройки бланка')return['documents',30];
|
||
// Any future/unrecognised settings remain reachable instead of disappearing.
|
||
return['account',60];
|
||
}
|
||
|
||
function ensureCatalogTabsCard(){
|
||
const view=$('enterprise-settings'),grid=view?.querySelector('.enterprise-grid');
|
||
if(!grid||typeof window.sunOpenCategoryManager!=='function')return;
|
||
let card=$('sunCatalogTabsSettingsCardV21');
|
||
if(!card){
|
||
card=document.createElement('section');
|
||
card.id='sunCatalogTabsSettingsCardV21';
|
||
card.className='enterprise-card wide';
|
||
grid.appendChild(card);
|
||
}
|
||
const cats=typeof window.sunCatalogCategories==='function'?window.sunCatalogCategories():[];
|
||
const total=Array.isArray(cats)?cats.length:0,visible=Array.isArray(cats)?cats.filter(x=>!x.hidden).length:0;
|
||
card.innerHTML=`<h2>Вкладки каталога</h2><p class="hint">Название, порядок, цвет и видимость разделов каталога. Скрытые разделы и их товары не удаляются.</p><div class="sun-settings-catalog-summary"><span class="sun-settings-catalog-chip">Всего: ${total}</span><span class="sun-settings-catalog-chip">Видимых: ${visible}</span></div><div class="actions"><button class="primary" id="sunSettingsCatalogTabsBtnV21" type="button">Настроить вкладки каталога</button></div>`;
|
||
$('sunSettingsCatalogTabsBtnV21')?.addEventListener('click',()=>window.sunOpenCategoryManager?.());
|
||
}
|
||
|
||
function ensureBar(){
|
||
const view=$('enterprise-settings');if(!view)return null;
|
||
let wrap=$('sunSettingsTabsWrapV21');
|
||
if(!wrap){
|
||
wrap=document.createElement('div');wrap.id='sunSettingsTabsWrapV21';wrap.className='sun-settings-tabs-wrap';
|
||
wrap.innerHTML=`<div class="sun-settings-tabs" role="tablist" aria-label="Разделы настроек">${TABS.map(t=>`<button class="sun-settings-tab" type="button" role="tab" data-settings-tab-target="${t.id}" aria-selected="false">${t.label}</button>`).join('')}</div><div class="sun-settings-tab-context" id="sunSettingsTabContextV21"></div>`;
|
||
const head=view.querySelector(':scope > .catalog-head');
|
||
if(head)head.insertAdjacentElement('afterend',wrap);else view.prepend(wrap);
|
||
qa('[data-settings-tab-target]',wrap).forEach(btn=>btn.addEventListener('click',()=>activate(btn.dataset.settingsTabTarget,true)));
|
||
}
|
||
return wrap;
|
||
}
|
||
|
||
function applyCards(){
|
||
const view=$('enterprise-settings'),grid=view?.querySelector('.enterprise-grid');if(!grid)return;
|
||
qa(':scope > .enterprise-card',grid).forEach(card=>{
|
||
const [tab,order]=classify(card);
|
||
card.dataset.settingsTab=tab;
|
||
card.style.setProperty('--sun-settings-order',String(order));
|
||
card.hidden=tab!==active;
|
||
card.setAttribute('aria-hidden',tab===active?'false':'true');
|
||
});
|
||
}
|
||
|
||
function renderBar(){
|
||
const wrap=ensureBar();if(!wrap)return;
|
||
const meta=TABS.find(x=>x.id===active)||TABS[0];
|
||
qa('[data-settings-tab-target]',wrap).forEach(btn=>{
|
||
const on=btn.dataset.settingsTabTarget===active;
|
||
btn.classList.toggle('on',on);btn.setAttribute('aria-selected',on?'true':'false');btn.tabIndex=on?0:-1;
|
||
if(on&&wrap.dataset.lastActive!==active)setTimeout(()=>btn.scrollIntoView?.({block:'nearest',inline:'nearest',behavior:'smooth'}),0);
|
||
});
|
||
wrap.dataset.lastActive=active;
|
||
const context=$('sunSettingsTabContextV21');if(context)context.innerHTML=`<b>${meta.label}</b><span>${meta.hint}</span>`;
|
||
}
|
||
|
||
function activate(tab,persist=false){
|
||
if(!VALID.has(tab))tab='account';active=tab;
|
||
if(persist){try{localStorage.setItem(TAB_KEY,active)}catch(_){}}
|
||
renderBar();applyCards();
|
||
const view=$('enterprise-settings');if(view?.classList.contains('on')){
|
||
try{view.querySelector('.sun-settings-tabs-wrap')?.scrollIntoView({block:'nearest',inline:'nearest'})}catch(_){}
|
||
}
|
||
window.dispatchEvent(new CustomEvent('sun:settings-tab-changed',{detail:{tab:active}}));
|
||
}
|
||
|
||
function refresh(){
|
||
const view=$('enterprise-settings');if(!view)return;
|
||
ensureCatalogTabsCard();ensureBar();applyCards();renderBar();
|
||
}
|
||
function schedule(delay=30){clearTimeout(scheduled);scheduled=setTimeout(refresh,delay)}
|
||
|
||
function boot(){
|
||
const view=$('enterprise-settings');if(!view)return;
|
||
const observer=new MutationObserver(()=>schedule(20));observer.observe(view,{childList:true,subtree:true});
|
||
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки'){schedule(0);schedule(90);schedule(240)}},true);
|
||
window.addEventListener('sun:catalog-categories-changed',()=>schedule(20));
|
||
window.addEventListener('sun:cloud-state-applied',()=>schedule(40));
|
||
window.addEventListener('suncloudsync',()=>schedule(40));
|
||
refresh();
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
|
||
/* ===== MODULE: developer-console-v22.js ===== */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.SunDeveloperV22)return;
|
||
const VERSION='2026.09.06-developer-console-v25-auth-mfa-logo';
|
||
const $=id=>String(id||'').startsWith('#')?document.querySelector(id):document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const clone=v=>v==null?v:(typeof structuredClone==='function'?structuredClone(v):JSON.parse(JSON.stringify(v)));
|
||
const cloud=()=>window.SunCloudV2||null;
|
||
const client=()=>cloud()?.getClient?.()||null;
|
||
const session=()=>cloud()?.getSession?.()||null;
|
||
const toast=(t,k='info',ms=4600)=>{try{return window.SunEnterprise?.toast?.(t,k,ms)}catch(_){};alert(t)};
|
||
const fmtDate=v=>{if(!v)return '—';try{return new Date(v).toLocaleString('ru-RU',{dateStyle:'short',timeStyle:'short'})}catch(_){return String(v)}};
|
||
const fmtDay=v=>{if(!v)return '—';try{return new Date(v).toLocaleDateString('ru-RU')}catch(_){return String(v)}};
|
||
const TAB_KEY='sunDeveloperActiveTabV22';
|
||
const TABS=[['overview','Обзор'],['companies','Компании'],['accounts','Аккаунты'],['plans','Тарифы'],['features','Функции'],['system','Система'],['backups','Резервные копии'],['journal','Журнал']];
|
||
const FEATURES={orders:'Заказы',calendar:'Календарь',map:'Карта',clients:'Клиенты',catalog_view:'Каталог · просмотр',catalog_edit:'Каталог · редактирование',production:'Производство',shopping:'Закупки',stock:'Склад',routes:'Маршруты',mailings:'Рассылки',money:'Финансы',stats_basic:'Статистика',stats_advanced:'Расширенная статистика',team:'Команда',suppliers:'Поставщики',print:'Печать',settings:'Настройки',branding:'Фирменное оформление',client_offers:'Предложение клиенту',offer_templates:'Шаблоны предложения',backups:'Резервные копии',audit:'Журнал',users_manage:'Пользователи и права'};
|
||
const PLANS={basic:'Базовый',professional:'Профессиональный',full:'Полный'};
|
||
let platformAdmin=false,checking=false,activeTab=localStorage.getItem(TAB_KEY)||'overview',lastCompanies=[],lastAdminCheckAt=0;
|
||
const DEV_ADMIN_TTL_MS=30000,DEV_ADMIN_TIMEOUT_MS=8000;
|
||
|
||
function installStyle(){if($('sunDeveloperV22Style'))return;const st=document.createElement('style');st.id='sunDeveloperV22Style';st.textContent=`
|
||
#sunDeveloperNavV22{font-weight:800}.sun-dev-view{max-width:1460px;margin:0 auto;padding:24px}.sun-dev-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:14px}.sun-dev-head h1{margin:0}.sun-dev-head-actions{display:flex;gap:8px;flex-wrap:wrap}.sun-dev-tabs{display:flex;gap:7px;overflow-x:auto;padding:4px 2px 12px;scrollbar-width:thin;border-bottom:1px solid #e7e8ec;margin-bottom:18px}.sun-dev-tabs button{white-space:nowrap;border:1px solid #d8dbe2;background:#fff;border-radius:999px;padding:9px 14px;font-weight:750;cursor:pointer}.sun-dev-tabs button.on{background:#171815;color:#fff;border-color:#171815}.sun-dev-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.sun-dev-kpi,.sun-dev-card{background:#fff;border:1px solid #e2e4e9;border-radius:16px;padding:16px;box-shadow:0 8px 28px rgba(30,32,36,.04)}.sun-dev-kpi small{color:#6f7480;display:block;margin-bottom:7px}.sun-dev-kpi b{font-size:24px}.sun-dev-card h2,.sun-dev-card h3{margin:0 0 10px}.sun-dev-toolbar{display:flex;gap:8px;align-items:end;flex-wrap:wrap;margin-bottom:12px}.sun-dev-toolbar label{min-width:180px}.sun-dev-toolbar input,.sun-dev-toolbar select,.sun-dev-card input,.sun-dev-card select,.sun-dev-card textarea{width:100%;box-sizing:border-box;padding:9px 10px;border:1px solid #d6d9df;border-radius:9px;background:#fff}.sun-dev-table-wrap{overflow:auto;border:1px solid #e2e4e9;border-radius:14px;background:#fff}.sun-dev-table{width:100%;border-collapse:collapse;min-width:900px}.sun-dev-table th,.sun-dev-table td{text-align:left;padding:11px 12px;border-bottom:1px solid #eceef2;vertical-align:top;font-size:13px}.sun-dev-table th{background:#f7f8fa;position:sticky;top:0;z-index:1}.sun-dev-actions{display:flex;gap:6px;flex-wrap:wrap}.sun-dev-pill{display:inline-flex;padding:4px 8px;border-radius:999px;background:#f0f2f5;font-size:11px;font-weight:750;margin:2px}.sun-dev-pill.ok{background:#e8f7ef;color:#14653b}.sun-dev-pill.warn{background:#fff3d9;color:#7d5300}.sun-dev-pill.bad{background:#fde8e8;color:#9d2424}.sun-dev-muted{color:#777d88;font-size:12px}.sun-dev-section-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.sun-dev-modal{position:fixed;inset:0;background:rgba(20,22,26,.52);z-index:100000;display:flex;align-items:center;justify-content:center;padding:18px}.sun-dev-modal-card{background:#fff;border-radius:18px;max-width:620px;width:min(620px,100%);max-height:90vh;overflow:auto;padding:20px;box-shadow:0 24px 80px rgba(0,0,0,.28)}.sun-dev-modal-head{display:flex;justify-content:space-between;gap:12px;align-items:center}.sun-dev-modal-head h2{margin:0}.sun-dev-modal-close{border:0;background:#f1f2f4;border-radius:10px;width:36px;height:36px;font-size:22px;cursor:pointer}.sun-dev-mfa-qr{max-width:220px;width:100%;display:block;margin:12px auto;border:10px solid #fff}.sun-dev-support-banner{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:99995;background:#171815;color:#fff;border-radius:14px;padding:10px 12px;box-shadow:0 12px 36px rgba(0,0,0,.28);display:flex;align-items:center;gap:10px;max-width:calc(100vw - 30px)}.sun-dev-support-banner b{white-space:nowrap}.sun-dev-support-banner button{background:#fff;color:#171815;border:0;border-radius:9px;padding:7px 10px;font-weight:800;cursor:pointer}.sun-dev-danger{border:1px solid #efc8c8!important;color:#9f2424!important;background:#fff!important}.sun-dev-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;background:#f5f6f8;border-radius:8px;padding:7px;overflow-wrap:anywhere}.sun-dev-empty{padding:26px;text-align:center;color:#747a84}.sun-dev-check{display:flex;align-items:center;gap:8px}.sun-dev-check input{width:18px!important;height:18px}.sun-dev-pre{white-space:pre-wrap;word-break:break-word;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;background:#f7f8fa;padding:12px;border-radius:10px;max-height:340px;overflow:auto}
|
||
@media(max-width:950px){.sun-dev-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.sun-dev-section-grid{grid-template-columns:1fr}.sun-dev-view{padding:15px}.sun-dev-head{flex-direction:column}.sun-dev-head-actions{width:100%}}
|
||
@media(max-width:540px){.sun-dev-grid{grid-template-columns:1fr}.sun-dev-support-banner{left:15px;right:15px;transform:none;flex-wrap:wrap}.sun-dev-support-banner b{white-space:normal}}
|
||
`;document.head.appendChild(st)}
|
||
|
||
async function rpc(name,args={}){const c=client();if(!c)throw new Error('Supabase не подключён.');const r=await c.rpc(name,args);if(r.error)throw r.error;return r.data}
|
||
async function log(action,workspace=null,user=null,details={}){try{await rpc('sun_dev_log_event',{p_action:action,p_workspace:workspace,p_user:user,p_details:details})}catch(_){}}
|
||
|
||
async function checkPlatformAdmin(force=false){
|
||
if(checking)return platformAdmin;const now=Date.now();if(!force&&lastAdminCheckAt&&now-lastAdminCheckAt<DEV_ADMIN_TTL_MS)return platformAdmin;
|
||
const c=client(),ss=session();if(!c||!ss?.user){platformAdmin=false;lastAdminCheckAt=now;syncChrome();return false}checking=true;let timer=0;
|
||
try{const r=await Promise.race([Promise.resolve(c.rpc('sun_is_platform_admin')),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('Developer access timeout')),DEV_ADMIN_TIMEOUT_MS)})]);platformAdmin=!r.error&&r.data===true;}
|
||
catch(_){platformAdmin=false}finally{clearTimeout(timer);lastAdminCheckAt=Date.now();checking=false;syncChrome()}return platformAdmin
|
||
}
|
||
function isPlatformAdmin(){return platformAdmin===true}
|
||
|
||
function ensureNav(){installStyle();const nav=document.querySelector('header nav');if(!nav||!platformAdmin)return null;$('sunSaaSAdminNavV16')?.remove();let b=$('sunDeveloperNavV22');if(!b){b=document.createElement('button');b.id='sunDeveloperNavV22';b.type='button';b.dataset.navLabel='Разработчик';b.textContent='Разработчик';nav.appendChild(b);b.addEventListener('click',()=>open(b))}b.hidden=false;b.style.display='';return b}
|
||
function ensureView(){let v=$('sun-developer-console-v22');if(!v){v=document.createElement('section');v.id='sun-developer-console-v22';v.className='view page sun-dev-view';document.body.appendChild(v)}return v}
|
||
function showDevView(btn=null){const v=ensureView();qa('.view').forEach(x=>x.classList.remove('on'));v.classList.add('on');qa('header nav button').forEach(x=>x.classList.remove('on'));(btn||$('sunDeveloperNavV22'))?.classList.add('on');window.scrollTo({top:0,behavior:'instant'});return v}
|
||
function shell(){const v=ensureView();v.innerHTML=`<div class="sun-dev-head"><div><h1>Кабинет разработчика</h1><p class="hint" style="margin:6px 0 0">Управление платформой, компаниями и техническим состоянием приложения.</p></div><div class="sun-dev-head-actions"><span class="sun-dev-pill ok">Developer</span><span class="sun-dev-pill">${esc(session()?.user?.email||'')}</span><button class="outline" id="sunDevRefresh" type="button">Обновить</button></div></div><div class="sun-dev-tabs">${TABS.map(([id,label])=>`<button type="button" data-dev-tab="${id}" class="${activeTab===id?'on':''}">${label}</button>`).join('')}</div><div id="sunDevBody"><div class="sun-dev-empty">Загрузка…</div></div>`;qa('[data-dev-tab]',v).forEach(b=>b.onclick=()=>activateTab(b.dataset.devTab));$('sunDevRefresh').onclick=()=>activateTab(activeTab,true)}
|
||
|
||
async function open(btn=null){if(!await checkPlatformAdmin(true))return toast('Этот аккаунт не имеет прав разработчика.','error');if(!await ensureDeveloperMfa())return;ensureNav();showDevView(btn);shell();await activateTab(activeTab,true)}
|
||
async function activateTab(tab,force=false){if(!TABS.some(x=>x[0]===tab))tab='overview';activeTab=tab;localStorage.setItem(TAB_KEY,tab);qa('[data-dev-tab]').forEach(b=>b.classList.toggle('on',b.dataset.devTab===tab));const body=$('sunDevBody');if(!body)return;body.innerHTML='<div class="sun-dev-empty">Загрузка…</div>';try{if(tab==='overview')await renderOverview(body);else if(tab==='companies')await renderCompanies(body);else if(tab==='accounts')await renderAccounts(body);else if(tab==='plans')await renderPlans(body);else if(tab==='features')await renderFeatures(body);else if(tab==='system')await renderSystem(body);else if(tab==='backups')await renderBackups(body);else if(tab==='journal')await renderJournal(body)}catch(e){body.innerHTML=`<div class="sun-dev-card"><h3>Не удалось загрузить раздел</h3><p>${esc(e?.message||e)}</p></div>`}}
|
||
|
||
function modal(title,html){$('sunDevModalV22')?.remove();const m=document.createElement('div');m.className='sun-dev-modal';m.id='sunDevModalV22';m.innerHTML=`<div class="sun-dev-modal-card"><div class="sun-dev-modal-head"><h2>${esc(title)}</h2><button class="sun-dev-modal-close" type="button" data-close>×</button></div><div id="sunDevModalBody" style="margin-top:14px">${html}</div></div>`;document.body.appendChild(m);const close=()=>m.remove();m.addEventListener('click',e=>{if(e.target===m||e.target.closest('[data-close]'))close()});return {m,body:$('#sunDevModalBody'),close}}
|
||
function confirmDanger(text){return confirm(text)}
|
||
|
||
async function ensureDeveloperMfa(){
|
||
const c=client();if(!c?.auth?.mfa)throw new Error('Supabase MFA недоступна.');
|
||
const readAal=async()=>{const a=await c.auth.mfa.getAuthenticatorAssuranceLevel();if(a.error)throw a.error;return a.data||{}};
|
||
const waitAal2=async()=>{for(let i=0;i<12;i++){const a=await readAal();if(a.currentLevel==='aal2')return true;await new Promise(r=>setTimeout(r,180))}return false};
|
||
const initial=await readAal();if(initial.currentLevel==='aal2')return true;
|
||
const listed=await c.auth.mfa.listFactors();if(listed.error)throw listed.error;
|
||
let factor=(listed.data?.totp||[]).find(f=>f.status==='verified');let enrollment=null;
|
||
if(!factor){
|
||
for(const f of (listed.data?.totp||[]).filter(x=>x.status!=='verified')){
|
||
try{const u=await c.auth.mfa.unenroll({factorId:f.id});if(u.error&&!/not found/i.test(String(u.error.message||'')))throw u.error}catch(e){console.warn('[Developer MFA] reset factor',e)}
|
||
}
|
||
const enr=await c.auth.mfa.enroll({factorType:'totp',friendlyName:'Caterium Developer'});if(enr.error)throw enr.error;factor=enr.data;enrollment=enr.data;
|
||
}
|
||
const firstSetup=Boolean(enrollment),qr=enrollment?.totp?.qr_code||'',secret=enrollment?.totp?.secret||'';
|
||
const ui=modal(firstSetup?'Подключение 2FA разработчика':'Подтверждение разработчика',firstSetup?`<p>Отсканируйте этот QR-код приложением-аутентификатором. Затем введите текущие 6 цифр и нажмите <b>Подтвердить и открыть</b>.</p>${qr?`<img class="sun-dev-mfa-qr" src="${esc(qr)}" alt="QR для 2FA">`:''}${secret?`<p class="sun-dev-muted">Если QR не сканируется, добавьте ключ вручную:</p><div class="sun-dev-code">${esc(secret)}</div>`:''}<label style="display:block;margin-top:14px">6-значный код<input id="sunDevMfaCode" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="000000"></label><div class="actions"><button class="primary" id="sunDevMfaVerify" type="button">Подтвердить и открыть</button></div><div id="sunDevMfaError" class="sun-dev-muted" style="margin-top:8px"></div>`:`<p>Введите текущий 6-значный код из приложения-аутентификатора.</p><label>Код<input id="sunDevMfaCode" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="000000"></label><div class="actions"><button class="primary" id="sunDevMfaVerify" type="button">Подтвердить и открыть</button></div><div id="sunDevMfaError" class="sun-dev-muted" style="margin-top:8px"></div>`);
|
||
return await new Promise(resolve=>{
|
||
let settled=false,busy=false;
|
||
const finish=v=>{if(settled)return;settled=true;try{ui.close()}catch(_){}resolve(v)};
|
||
const showError=(e,notify=true)=>{const msg=e?.message||String(e||'Не удалось подтвердить код.');const box=$('sunDevMfaError');if(box)box.textContent=msg;if(notify)toast(msg,'error',6500)};
|
||
const verify=async()=>{
|
||
if(busy)return;
|
||
const code=String(document.getElementById('sunDevMfaCode')?.value||'').replace(/\D/g,'');
|
||
if(code.length!==6){showError(new Error('Введите ровно 6 цифр из приложения-аутентификатора.'),false);return}
|
||
busy=true;const btn=$('sunDevMfaVerify');if(btn){btn.disabled=true;btn.textContent='Проверяю…'};const box=$('sunDevMfaError');if(box)box.textContent='Проверяю код…';
|
||
try{
|
||
let vr;
|
||
if(typeof c.auth.mfa.challengeAndVerify==='function') vr=await c.auth.mfa.challengeAndVerify({factorId:factor.id,code});
|
||
else {const ch=await c.auth.mfa.challenge({factorId:factor.id});if(ch.error)throw ch.error;vr=await c.auth.mfa.verify({factorId:factor.id,challengeId:ch.data.id,code})}
|
||
if(vr?.error)throw vr.error;
|
||
if(!await waitAal2()){
|
||
try{await c.auth.refreshSession()}catch(_){}
|
||
if(!await waitAal2())throw new Error('Код принят, но сессия разработчика не получила AAL2. Выйдите из аккаунта и войдите снова, затем повторите код.');
|
||
}
|
||
toast('2FA подтверждена. Открываю кабинет разработчика…','success',2800);
|
||
finish(true);
|
||
}catch(e){busy=false;if(btn){btn.disabled=false;btn.textContent='Подтвердить и открыть'};showError(e)}
|
||
};
|
||
const b=document.getElementById('sunDevMfaVerify'),inp=document.getElementById('sunDevMfaCode');if(b)b.addEventListener('click',verify);if(inp)inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();verify()}});
|
||
ui.m.addEventListener('click',e=>{if((e.target===ui.m||e.target.closest('[data-close]'))&&!busy)finish(false)});
|
||
setTimeout(()=>inp?.focus(),60);
|
||
})
|
||
}
|
||
|
||
async function renderOverview(body){const d=await rpc('sun_dev_dashboard');body.innerHTML=`<div class="sun-dev-grid"><div class="sun-dev-kpi"><small>Компании</small><b>${Number(d.companies||0)}</b></div><div class="sun-dev-kpi"><small>Аккаунты</small><b>${Number(d.accounts||0)}</b></div><div class="sun-dev-kpi"><small>Активные подписки</small><b>${Number(d.active_subscriptions||0)}</b></div><div class="sun-dev-kpi"><small>Ограниченные компании</small><b>${Number(d.restricted_companies||0)}</b></div><div class="sun-dev-kpi"><small>Ошибки за 24 ч.</small><b>${Number(d.errors_24h||0)}</b></div><div class="sun-dev-kpi"><small>Копии за 24 ч.</small><b>${Number(d.backups_24h||0)}</b></div><div class="sun-dev-kpi"><small>Участники компаний</small><b>${Number(d.memberships||0)}</b></div><div class="sun-dev-kpi"><small>Размер БД</small><b style="font-size:19px">${esc(d.database_size||'—')}</b></div></div><div class="sun-dev-section-grid" style="margin-top:12px"><div class="sun-dev-card"><h3>Платформа</h3><p><b>Версия приложения:</b> <span id="sunDevReleaseVersion">17.6.0</span></p><p><b>PostgreSQL:</b> ${esc(d.postgres_version||'—')}</p><p><b>Последнее изменение рабочей базы:</b> ${fmtDate(d.last_state_at)}</p></div><div class="sun-dev-card"><h3>Безопасность</h3><p><span class="sun-dev-pill ok">MFA AAL2</span> Кабинет разработчика защищён двухфакторной авторизацией.</p><p class="sun-dev-muted">Права разработчика проверяются сервером через sun_platform_admins.</p></div></div>`;try{const r=await fetch('release-manifest.json',{cache:'no-store'});if(r.ok){const m=await r.json();$('#sunDevReleaseVersion').textContent=m.version||'17.6.0'}}catch(_){} }
|
||
|
||
async function getCompanies(){let rows=[];try{rows=await rpc('sun_dev_list_companies')}catch(_){rows=await rpc('sun_dev_list_companies')}lastCompanies=Array.isArray(rows)?rows:[];return lastCompanies}
|
||
function companyOptions(rows,selected=''){return `<option value="">Выберите компанию</option>${rows.map(r=>`<option value="${esc(r.workspace_id)}" ${r.workspace_id===selected?'selected':''}>${esc(r.workspace_name||r.name||r.workspace_id)}</option>`).join('')}`}
|
||
async function renderCompanies(body){const rows=await getCompanies();body.innerHTML=`<div class="sun-dev-toolbar"><label style="flex:1">Поиск<input id="sunDevCompanySearch" placeholder="Название или email владельца"></label><button class="primary" id="sunDevCreateCompany" type="button">Создать компанию</button></div><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Компания</th><th>Владелец</th><th>Тариф</th><th>Доступ</th><th>Сотрудники</th><th>Создана</th><th>Действия</th></tr></thead><tbody id="sunDevCompaniesTbody"></tbody></table></div>`;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=>`<tr data-ws="${esc(r.workspace_id)}"><td><b>${esc(r.workspace_name)}</b><div class="sun-dev-muted">${esc(r.workspace_id)}</div></td><td>${esc(r.owner_email||r.pending_owner_email||'—')}${r.pending_owner_email?'<div class="sun-dev-pill warn">ожидает регистрации</div>':''}</td><td>${esc(PLANS[r.plan_id]||r.plan_name||r.plan_id||'—')}<div class="sun-dev-muted">до ${fmtDay(r.trial_ends_at||r.current_period_end)}</div></td><td><span class="sun-dev-pill ${r.access_mode==='full'?'ok':r.access_mode==='blocked'?'bad':'warn'}">${esc(r.access_mode||r.status||'—')}</span></td><td>${Number(r.member_count||0)} / ${r.max_members==null?'∞':r.max_members}</td><td>${fmtDay(r.created_at)}</td><td><div class="sun-dev-actions"><button class="outline" data-support>Открыть как поддержка</button><button class="outline" data-diag>Диагностика</button><button class="outline" data-plan>Тариф</button><button class="outline sun-dev-danger" data-block>${r.access_mode==='blocked'?'Разблокировать':'Заблокировать'}</button></div></td></tr>`).join('')||'<tr><td colspan="7" class="sun-dev-empty">Компаний не найдено.</td></tr>';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('Создать компанию',`<label>Название<input id="sunDevNewCompanyName" value="Новая компания"></label><label style="display:block;margin-top:10px">Email владельца<input id="sunDevNewOwnerEmail" type="email" placeholder="owner@example.com"></label><div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px"><label>Тариф<select id="sunDevNewPlan"><option value="basic">Базовый</option><option value="professional">Профессиональный</option><option value="full" selected>Полный</option></select></label><label>Срок, дней<input id="sunDevNewDays" type="number" min="1" max="3650" value="30"></label></div><p class="hint">Компания получит текущий стартовый каталог (${Number(window.SUN_OFFICIAL_CATALOG?.length||0)} позиций), а заказы и клиенты будут пустыми.</p><div class="actions"><button class="primary" id="sunDevNewCreate" type="button">Создать</button></div><div id="sunDevNewError" class="sun-dev-muted"></div>`);$('#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=`<p><b>Компания создана:</b> ${esc(data.workspace_name||name)}</p><p>Каталог: ${Number(data.catalog_count||0)} позиций.</p>${data.owner_status==='invite'?`<p>Владелец ещё не зарегистрирован. Код приглашения сохранён на сервере.</p>`:''}<div class="actions"><button class="primary" type="button" data-close>Готово</button></div>`;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}`,`<label>Тариф<select id="sunDevPlanSelect"><option value="basic" ${row.plan_id==='basic'?'selected':''}>Базовый</option><option value="professional" ${row.plan_id==='professional'?'selected':''}>Профессиональный</option><option value="full" ${row.plan_id==='full'?'selected':''}>Полный</option></select></label><label style="display:block;margin-top:10px">Продлить на, дней<select id="sunDevPlanDays"><option value="30">30</option><option value="90">90</option><option value="365">365</option></select></label><div class="actions"><button class="primary" id="sunDevPlanSave">Применить</button></div>`);$('#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}`,`<div class="sun-dev-grid" style="grid-template-columns:repeat(2,minmax(0,1fr))"><div class="sun-dev-kpi"><small>Заказы</small><b>${Number(d.orders||0)}</b></div><div class="sun-dev-kpi"><small>Клиенты</small><b>${Number(d.clients||0)}</b></div><div class="sun-dev-kpi"><small>Каталог</small><b>${Number(d.catalog_items||0)}</b></div><div class="sun-dev-kpi"><small>Ошибки 24 ч.</small><b>${Number(d.errors_24h||0)}</b></div></div><p><b>Revision:</b> ${Number(d.revision||0)}</p><p><b>Обновлено:</b> ${fmtDate(d.state_updated_at)}</p><p><b>Последняя копия:</b> ${fmtDate(d.latest_backup)}</p><p><b>Режим доступа:</b> ${esc(d.access_mode||'—')}</p>`)}
|
||
|
||
async function enterSupport(row){if(!row)return;await cloud().enterSupportWorkspace(row.workspace_id,row.workspace_name);await log('support.ui_enter',row.workspace_id,null,{company:row.workspace_name});ensureSupportBanner();try{window.render?.();window.renderOrders?.();window.renderStats?.()}catch(_){}const orders=qa('header nav button').find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Заказы');if(orders)orders.click();else{qa('.view').forEach(v=>v.classList.remove('on'));$('orders')?.classList.add('on')}toast('Режим поддержки открыт. Изменения не будут сохранены.','info',6500)}
|
||
function ensureSupportBanner(){let b=$('sunDevSupportBannerV22');const sm=cloud()?.getSupportMode?.();if(!sm){b?.remove();return}if(!b){b=document.createElement('div');b.id='sunDevSupportBannerV22';b.className='sun-dev-support-banner';document.body.appendChild(b)}b.innerHTML=`<span>Поддержка:</span><b>${esc(sm.name||'Компания')}</b><span class="sun-dev-pill warn">только просмотр</span><button type="button">Выйти из компании</button>`;b.querySelector('button').onclick=async()=>{try{await log('support.exit',sm.workspaceId,null,{company:sm.name});await cloud().exitSupportWorkspace()}catch(e){toast(e?.message||String(e),'error')}}}
|
||
|
||
async function fetchAccounts(){const rows=await rpc('sun_dev_list_users');const map=new Map();for(const r of rows||[]){const id=String(r.user_id);if(!map.has(id))map.set(id,{id,email:r.email,created_at:r.created_at,last_sign_in_at:r.last_sign_in_at,email_confirmed_at:'',is_platform_admin:r.is_platform_admin===true,is_banned:false,authAdminActions:false,workspaces:[]});if(r.workspace_id)map.get(id).workspaces.push({id:r.workspace_id,name:r.workspace_name,role:r.role,is_active:r.is_active!==false})}return [...map.values()]}
|
||
async function renderAccounts(body){const users=await fetchAccounts();body.innerHTML=`<div class="sun-dev-toolbar"><label style="flex:1">Поиск<input id="sunDevUserSearch" placeholder="Email или компания"></label><span class="sun-dev-pill">${users.length} аккаунтов</span></div><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Аккаунт</th><th>Компании</th><th>Регистрация</th><th>Последний вход</th><th>Статус</th><th>Действия</th></tr></thead><tbody id="sunDevUsersTbody"></tbody></table></div>`;const draw=()=>{const q=String($('#sunDevUserSearch').value||'').trim().toLowerCase();const arr=users.filter(u=>`${u.email||''} ${(u.workspaces||[]).map(w=>w.name).join(' ')}`.toLowerCase().includes(q));$('#sunDevUsersTbody').innerHTML=arr.map(u=>`<tr data-user="${esc(u.id)}"><td><b>${esc(u.email||'—')}</b>${u.is_platform_admin?'<div><span class="sun-dev-pill ok">Разработчик</span></div>':''}<div class="sun-dev-muted">${esc(u.id)}</div></td><td>${(u.workspaces||[]).length?(u.workspaces||[]).map(w=>`<span class="sun-dev-pill">${esc(w.name)} · ${esc(w.role||'')}</span>`).join(''):'—'}</td><td>${fmtDate(u.created_at)}</td><td>${fmtDate(u.last_sign_in_at)}</td><td>${u.is_banned?'<span class="sun-dev-pill bad">заблокирован</span>':'<span class="sun-dev-pill ok">активен</span>'}${u.email_confirmed_at?'<div class="sun-dev-muted">email подтверждён</div>':''}</td><td>${u.authAdminActions?`<div class="sun-dev-actions"><button class="outline" data-recovery>Восстановление</button>${u.is_platform_admin?'':`<button class="outline ${u.is_banned?'':'sun-dev-danger'}" data-ban>${u.is_banned?'Разблокировать':'Заблокировать'}</button>`}</div>`:'<span class="sun-dev-muted">Просмотр аккаунта доступен. Глобальная блокировка Auth и письмо восстановления требуют защищённой backend-функции.</span>'}</td></tr>`).join('')||'<tr><td colspan="6" class="sun-dev-empty">Ничего не найдено.</td></tr>';qa('#sunDevUsersTbody tr[data-user]').forEach(tr=>{const u=users.find(x=>String(x.id)===tr.dataset.user);tr.querySelector('[data-recovery]')?.addEventListener('click',()=>authAdminAction('send_recovery',u));tr.querySelector('[data-ban]')?.addEventListener('click',()=>authAdminAction('set_ban',u,{banned:!u.is_banned}))})};draw();$('#sunDevUserSearch').oninput=draw}
|
||
async function authAdminAction(action,u,extra={}){if(action==='set_ban'&&extra.banned&&!confirmDanger(`Заблокировать вход для ${u.email}?`))return;const r=await client().functions.invoke('caterium-platform-auth-admin',{body:{action,user_id:u.id,...extra}});if(r.error)throw r.error;if(r.data?.error)throw new Error(r.data.error);toast(action==='send_recovery'?'Письмо восстановления отправлено.':extra.banned?'Аккаунт заблокирован.':'Аккаунт разблокирован.','success');activateTab('accounts',true)}
|
||
|
||
async function renderPlans(body){const c=client();const [plansR,featuresR]=await Promise.all([c.from('sun_plans').select('id,name,description,max_members,is_active,sort_order').order('sort_order'),c.from('sun_plan_features').select('plan_id,feature_key,enabled')]);if(plansR.error)throw plansR.error;if(featuresR.error)throw featuresR.error;const plans=plansR.data||[],features=featuresR.data||[];body.innerHTML=`<div class="sun-dev-section-grid">${plans.map(p=>`<div class="sun-dev-card" data-plan-card="${esc(p.id)}"><h2>${esc(p.name)}</h2><p class="sun-dev-muted">${esc(p.description||'')}</p><label>Лимит сотрудников<input type="number" data-max min="1" placeholder="без лимита" value="${p.max_members??''}"></label><div class="actions"><button class="outline" data-save-max>Сохранить лимит</button></div><h3 style="margin-top:16px">Функции</h3><div>${Object.keys(FEATURES).map(f=>{const row=features.find(x=>x.plan_id===p.id&&x.feature_key===f);return `<label class="sun-dev-check"><input type="checkbox" data-feature="${f}" ${row?.enabled?'checked':''}><span>${esc(FEATURES[f])}</span></label>`}).join('')}</div></div>`).join('')}</div>`;qa('[data-plan-card]').forEach(card=>{const plan=card.dataset.planCard;card.querySelector('[data-save-max]').onclick=async()=>{const raw=card.querySelector('[data-max]').value;await rpc('sun_dev_set_plan_max_members',{p_plan:plan,p_max_members:raw===''?null:Number(raw)});toast('Лимит сохранён.','success')};qa('[data-feature]',card).forEach(ch=>ch.onchange=async()=>{ch.disabled=true;try{await rpc('sun_dev_set_plan_feature',{p_plan:plan,p_feature:ch.dataset.feature,p_enabled:ch.checked});toast('Матрица тарифа обновлена.','success')}catch(e){ch.checked=!ch.checked;toast(e?.message||String(e),'error')}finally{ch.disabled=false}})})}
|
||
|
||
async function renderFeatures(body){const rows=lastCompanies.length?lastCompanies:await getCompanies();body.innerHTML=`<div class="sun-dev-toolbar"><label>Компания<select id="sunDevFeatureCompany">${companyOptions(rows)}</select></label><button class="outline" id="sunDevFeatureLoad" type="button">Загрузить</button></div><div id="sunDevFeatureBody" class="sun-dev-empty">Выберите компанию.</div>`;const load=async()=>{const ws=$('#sunDevFeatureCompany').value;if(!ws)return;const data=await rpc('sun_dev_list_workspace_features',{p_workspace:ws});const root=$('sunDevFeatureBody');root.className='sun-dev-table-wrap';root.innerHTML=`<table class="sun-dev-table"><thead><tr><th>Функция</th><th>По тарифу</th><th>Индивидуально</th><th>Итог</th><th>Действия</th></tr></thead><tbody>${(data||[]).map(f=>`<tr data-feature="${esc(f.feature_key)}"><td><b>${esc(FEATURES[f.feature_key]||f.feature_key)}</b></td><td>${f.plan_enabled?'✓':'—'}</td><td>${f.override_enabled==null?'нет':f.override_enabled?'включено':'выключено'}<div class="sun-dev-muted">${f.override_expires_at?`до ${fmtDay(f.override_expires_at)}`:''}</div></td><td><span class="sun-dev-pill ${f.effective_enabled?'ok':'warn'}">${f.effective_enabled?'доступна':'закрыта'}</span></td><td><div class="sun-dev-actions"><button class="outline" data-enable>Включить 30 дн.</button><button class="outline" data-disable>Выключить 30 дн.</button><button class="outline" data-reset>По тарифу</button></div></td></tr>`).join('')}</tbody></table>`;qa('tr[data-feature]',root).forEach(tr=>{const f=tr.dataset.feature;tr.querySelector('[data-enable]').onclick=()=>setOverride(ws,f,true,30,load);tr.querySelector('[data-disable]').onclick=()=>setOverride(ws,f,false,30,load);tr.querySelector('[data-reset]').onclick=async()=>{await rpc('sun_dev_reset_feature_override',{p_workspace:ws,p_feature:f});toast('Индивидуальное правило сброшено.','success');load()}})};$('#sunDevFeatureLoad').onclick=load;$('#sunDevFeatureCompany').onchange=load}
|
||
async function setOverride(ws,f,en,days,refresh){await rpc('sun_dev_set_feature_override',{p_workspace:ws,p_feature:f,p_enabled:en,p_days:days,p_note:'Developer console v17.6.0'});await log('feature.override.set',ws,null,{feature:f,enabled:en,days});toast('Индивидуальная функция обновлена.','success');await refresh()}
|
||
|
||
async function renderSystem(body){const cfg=cloud()?.getTechnicalConfig?.()||{},d=await rpc('sun_dev_dashboard');let stability=null;try{stability=await window.SunStabilityV17?.diagnostics?.()}catch(_){}const rows=lastCompanies.length?lastCompanies:await getCompanies();body.innerHTML=`<div class="sun-dev-section-grid"><div class="sun-dev-card"><h2>Подключение Supabase</h2><label>Project URL<input id="sunDevSupabaseUrl" value="${esc(cfg.url||'')}"></label><label style="display:block;margin-top:10px">Publishable key<input id="sunDevSupabaseKey" type="password" value="${esc(cfg.key||'')}"></label><label class="sun-dev-check" style="margin-top:12px"><input id="sunDevAutoSync" type="checkbox" ${cfg.autoSync?'checked':''}> Автосинхронизация</label><div class="actions"><button class="outline" id="sunDevReconnect">Переподключить</button></div><p class="sun-dev-muted">Эти параметры скрыты от обычных пользователей.</p></div><div class="sun-dev-card"><h2>Состояние</h2><p><b>База:</b> ${esc(d.database_size||'—')}</p><p><b>PostgreSQL:</b> ${esc(d.postgres_version||'—')}</p><p><b>Последняя синхронизация:</b> ${fmtDate(cfg.lastSync)}</p><p><b>Ошибки 24 ч.:</b> ${Number(d.errors_24h||0)}</p><p><b>Client ID:</b> <span class="sun-dev-code">${esc(cloud()?.getClientId?.()||'')}</span></p></div><div class="sun-dev-card"><h2>Стартовый каталог</h2><p><b>${Number(window.SUN_OFFICIAL_CATALOG?.length||0)}</b> позиций · версия ${esc(window.SUN_OFFICIAL_CATALOG_VERSION||'—')}</p><p class="hint">Новые компании получают этот каталог автоматически. Существующие компании не меняются без явной команды.</p><label>Применить к компании<select id="sunDevCatalogCompany">${companyOptions(rows)}</select></label><div class="actions"><button class="outline sun-dev-danger" id="sunDevForceCatalog">Принудительно заменить каталог</button></div></div><div class="sun-dev-card"><h2>Диагностика клиента</h2><div class="sun-dev-pre">${esc(stability?JSON.stringify(stability,null,2):'Диагностика stability-модуля недоступна.')}</div></div></div>`;$('#sunDevAutoSync').onchange=e=>cloud().setAutoSyncDeveloper?.(e.target.checked);$('#sunDevReconnect').onclick=async()=>{if(!confirmDanger('Переподключить приложение к указанному Supabase? Неверные данные могут временно лишить приложение облака.'))return;await cloud().setTechnicalConnection($('#sunDevSupabaseUrl').value,$('#sunDevSupabaseKey').value);toast('Подключение обновлено.','success')};$('#sunDevForceCatalog').onclick=async()=>{const ws=$('#sunDevCatalogCompany').value;if(!ws)return toast('Выберите компанию.','warn');const row=rows.find(r=>r.workspace_id===ws);if(!confirmDanger(`ЗАМЕНИТЬ каталог компании «${row?.workspace_name||ws}» текущим стартовым каталогом? Собственный каталог компании будет перезаписан.`))return;const n=await rpc('sun_dev_seed_workspace_catalog',{p_workspace:ws,p_boxes:window.SUN_OFFICIAL_CATALOG||[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||''),p_replace:true});toast(`Каталог заменён: ${n} позиций.`,'success',6500)}}
|
||
|
||
async function renderBackups(body){const rows=lastCompanies.length?lastCompanies:await getCompanies();body.innerHTML=`<div class="sun-dev-toolbar"><label>Компания<select id="sunDevBackupCompany">${companyOptions(rows)}</select></label><button class="outline" id="sunDevBackupLoad">Загрузить</button><button class="primary" id="sunDevBackupCreate">Создать копию</button></div><div id="sunDevBackupBody" class="sun-dev-empty">Выберите компанию.</div>`;const load=async()=>{const ws=$('#sunDevBackupCompany').value;if(!ws)return;const data=await rpc('sun_v17_list_backups',{p_workspace:ws,p_limit:60});const root=$('sunDevBackupBody');root.className='sun-dev-table-wrap';root.innerHTML=`<table class="sun-dev-table"><thead><tr><th>Дата</th><th>Тип</th><th>Метка</th><th>ID</th><th>Действия</th></tr></thead><tbody>${(data||[]).map(b=>`<tr data-backup="${esc(b.id)}"><td>${fmtDate(b.created_at)}</td><td>${esc(b.kind)}</td><td>${esc(b.label||'—')}</td><td class="sun-dev-muted">${esc(b.id)}</td><td><button class="outline sun-dev-danger" data-restore>Восстановить</button></td></tr>`).join('')||'<tr><td colspan="5" class="sun-dev-empty">Копий нет.</td></tr>'}</tbody></table>`;qa('tr[data-backup]',root).forEach(tr=>tr.querySelector('[data-restore]').onclick=async()=>{if(!confirmDanger('Восстановить эту резервную копию? Перед восстановлением автоматически создастся новая копия текущего состояния.'))return;await rpc('sun_v17_restore_backup',{p_workspace:ws,p_backup:tr.dataset.backup});await log('backup.restore',ws,null,{backup_id:tr.dataset.backup});toast('Резервная копия восстановлена.','success',6500);load()})};$('#sunDevBackupLoad').onclick=load;$('#sunDevBackupCompany').onchange=load;$('#sunDevBackupCreate').onclick=async()=>{const ws=$('#sunDevBackupCompany').value;if(!ws)return toast('Выберите компанию.','warn');const id=await rpc('sun_v17_create_backup',{p_workspace:ws,p_kind:'manual',p_label:'Создано из кабинета разработчика'});await log('backup.create',ws,null,{backup_id:id});toast('Резервная копия создана.','success');load()}}
|
||
|
||
async function renderJournal(body){const [activity,errors]=await Promise.all([rpc('sun_dev_list_activity',{p_limit:150}),rpc('sun_dev_list_errors',{p_workspace:null,p_limit:100})]);body.innerHTML=`<div class="sun-dev-section-grid"><div class="sun-dev-card" style="overflow:auto"><h2>Действия разработчика</h2><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Действие</th><th>Компания / аккаунт</th><th>Детали</th></tr></thead><tbody>${(activity||[]).map(a=>`<tr><td>${fmtDate(a.created_at)}</td><td><b>${esc(a.action)}</b><div class="sun-dev-muted">${esc(a.actor_email||'')}</div></td><td>${esc(a.workspace_name||a.target_email||'—')}</td><td class="sun-dev-muted">${esc(JSON.stringify(a.details||{}))}</td></tr>`).join('')||'<tr><td colspan="4">Записей пока нет.</td></tr>'}</tbody></table></div></div><div class="sun-dev-card" style="overflow:auto"><h2>Ошибки приложения</h2><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Компания</th><th>Версия</th><th>Ошибка</th></tr></thead><tbody>${(errors||[]).map(e=>`<tr><td>${fmtDate(e.created_at)}</td><td>${esc(e.workspace_name||'—')}</td><td>${esc(e.app_version||'—')}</td><td><b>${esc(e.level||'error')}</b><div>${esc(e.message||'')}</div></td></tr>`).join('')||'<tr><td colspan="4">Ошибок нет.</td></tr>'}</tbody></table></div></div></div>`}
|
||
|
||
function enhanceDeveloperGate(){const gate=$('sunCloudAuthGateV3'),ss=session(),ws=cloud()?.getWorkspace?.(),st=cloud()?.status?.();if(!platformAdmin||!gate||!ss?.user||ws||st?.membershipsLoading||st?.membershipsLoaded===false)return;const card=gate.querySelector('.sun-cloud-auth-card');if(!card||card.dataset.devEnhanced==='1')return;card.dataset.devEnhanced='1';gate.dataset.saasEnhanced='1';card.innerHTML=`<div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Аккаунт разработчика</h2><div class="hint">${esc(ss.user.email||'')}</div></div></div><p class="hint">Этот аккаунт управляет платформой и не обязан иметь собственную рабочую компанию.</p><div class="sun-cloud-auth-actions"><button class="primary" type="button" data-open-dev>Открыть кабинет разработчика</button><button class="outline" type="button" data-signout>Выйти</button></div>`;card.querySelector('[data-open-dev]').onclick=()=>open();card.querySelector('[data-signout]').onclick=()=>cloud()?.signOut?.()}
|
||
function syncChrome(){if(platformAdmin){ensureNav();enhanceDeveloperGate()}else{$('sunDeveloperNavV22')?.remove()}ensureSupportBanner()}
|
||
function boot(){installStyle();let tries=0;const timer=setInterval(async()=>{tries++;if(document.hidden)return;if(client()&&session()?.user){await checkPlatformAdmin(false);enhanceDeveloperGate();ensureSupportBanner()}else if(!session()?.user){platformAdmin=false;syncChrome()}if(tries>40)clearInterval(timer)},3000);window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>checkPlatformAdmin(true),120));window.addEventListener('sun:cloud-state-applied',()=>ensureSupportBanner());document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;if(String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки'&&platformAdmin)setTimeout(()=>{$('sunSaaSSettingsCardV16')?.querySelector('[data-saas-admin]')?.remove()},120)},true);setTimeout(()=>checkPlatformAdmin(true),900)}
|
||
|
||
window.SunDeveloperV22={VERSION,open,isPlatformAdmin,checkPlatformAdmin,ensureNav,activateTab,ensureSupportBanner};
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
|
||
/* ===== MODULE: chat-v31.js ===== */
|
||
(() => {
|
||
'use strict';
|
||
if (window.SunChatV31) return;
|
||
|
||
const VERSION = '2026.09.06-chat-v31';
|
||
const $ = id => document.getElementById(id);
|
||
const qa = (s,r=document) => [...r.querySelectorAll(s)];
|
||
const esc=window.SunSafe.escapeHTML;
|
||
const toast = (text,type='info',timeout=4500) => { try { if(window.SunEnterprise?.toast) return window.SunEnterprise.toast(text,type,timeout); } catch (_) {} alert(text); };
|
||
const ROLE_LABELS = {admin:'Администратор',manager:'Менеджер',kitchen:'Кухня',courier:'Курьер',viewer:'Просмотр'};
|
||
|
||
let threads = [];
|
||
let members = [];
|
||
let messages = [];
|
||
let activeThread = null;
|
||
let workspaceChannel = null;
|
||
let threadChannel = null;
|
||
let workspaceChannelId = '';
|
||
let threadChannelId = '';
|
||
let online = new Set();
|
||
let pendingFiles = [];
|
||
let loadingThreads = false;
|
||
let loadingMessages = false;
|
||
let typingStopTimer = null;
|
||
let lastTypingSent = 0;
|
||
let typingUsers = new Map();
|
||
let sidebarMode = 'general';
|
||
let chatDock = 'global';
|
||
let bootRetryTimer = null;
|
||
|
||
function cloud(){ return window.SunCloudV2 || null; }
|
||
function client(){ return cloud()?.getClient?.() || null; }
|
||
function session(){ return cloud()?.getSession?.() || null; }
|
||
function workspace(){ return cloud()?.getWorkspace?.() || null; }
|
||
function supportMode(){ return Boolean(cloud()?.isSupportMode?.()); }
|
||
function available(){ return Boolean(client() && session()?.user && workspace()?.id && !supportMode()); }
|
||
function me(){ return session()?.user?.id || ''; }
|
||
function meName(){ const ws=workspace();return ws?.display_name || session()?.user?.email?.split('@')[0] || 'Сотрудник'; }
|
||
async function rpc(name,args={}){ const c=client();if(!c)throw new Error('Supabase не подключён.');const r=await c.rpc(name,args);if(r.error)throw r.error;return r.data; }
|
||
function fmtTime(v){ if(!v)return '';try{return new Intl.DateTimeFormat('ru-RU',{hour:'2-digit',minute:'2-digit'}).format(new Date(v))}catch(_){return ''} }
|
||
function fmtDay(v){ if(!v)return '';try{return new Intl.DateTimeFormat('ru-RU',{day:'numeric',month:'long'}).format(new Date(v))}catch(_){return ''} }
|
||
function sameDay(a,b){ try{const x=new Date(a),y=new Date(b);return x.getFullYear()===y.getFullYear()&&x.getMonth()===y.getMonth()&&x.getDate()===y.getDate()}catch(_){return false} }
|
||
function bytes(n){ n=Number(n||0);if(n<1024)return `${n} Б`;if(n<1024*1024)return `${Math.round(n/1024)} КБ`;return `${(n/1024/1024).toFixed(1)} МБ`; }
|
||
function safeName(name){ return String(name||'file').replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/\s+/g,' ').trim().slice(0,120)||'file'; }
|
||
function uid(){ try{return window.sunUUID?.() || crypto.randomUUID()}catch(_){return `${Date.now()}-${Math.random().toString(36).slice(2)}`} }
|
||
function currentOrderId(){ try{return typeof draft!=='undefined'&&draft?.id?String(draft.id):''}catch(_){return ''} }
|
||
function savedOrders(){ try{return Array.isArray(orders)?orders.slice():[]}catch(_){return []} }
|
||
|
||
function installStyle(){
|
||
if($('sun-chat-style-v31'))return;
|
||
$('sun-chat-style-v29')?.remove();
|
||
const s=document.createElement('style');s.id='sun-chat-style-v31';s.textContent=`
|
||
#sunChatNavV29{position:relative}.sun-chat-nav-badge{display:none;position:absolute;right:5px;top:5px;min-width:18px;height:18px;border-radius:999px;padding:0 5px;background:#d83b35;color:#fff;font:800 10px/18px Arial;text-align:center;box-shadow:0 0 0 2px var(--sun-sidebar-bg,#32255d)}.sun-chat-nav-badge.on{display:block}
|
||
#sun-chat-view{padding:16px;min-height:calc(100vh - 70px);background:#f3f4f7}.sun-chat-shell{max-width:1420px;margin:0 auto;height:calc(100vh - 102px);min-height:620px;background:#fff;border:1px solid #e0e4e9;border-radius:18px;overflow:hidden;display:grid;grid-template-columns:330px minmax(0,1fr);box-shadow:0 12px 40px rgba(30,40,60,.07)}
|
||
.sun-chat-side{border-right:1px solid #e6e9ed;background:#fafbfc;display:flex;flex-direction:column;min-width:0}.sun-chat-side-head{padding:17px 17px 12px;border-bottom:1px solid #e8ebef}.sun-chat-side-head h1{font-size:22px;margin:0;color:#17384d}.sun-chat-side-head p{margin:5px 0 11px;color:#7a838d;font-size:12px}.sun-chat-section-tabs{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}.sun-chat-section-tab{position:relative;border:1px solid #dfe4e8;background:#fff;color:#53616d;border-radius:10px;min-height:34px;padding:6px 5px;font-size:10.5px;font-weight:850;cursor:pointer}.sun-chat-section-tab.on{background:#17384d;color:#fff;border-color:#17384d}.sun-chat-section-badge{display:none;position:absolute;right:3px;top:2px;min-width:15px;height:15px;padding:0 4px;border-radius:999px;background:#d83b35;color:#fff;font:800 8px/15px Arial}.sun-chat-section-badge.on{display:block}.sun-chat-side-scroll{overflow:auto;padding:10px;flex:1}.sun-chat-group-title{display:flex;align-items:center;justify-content:space-between;padding:10px 8px 6px;color:#8a919a;font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:900}.sun-chat-list{display:grid;gap:3px}.sun-chat-thread{width:100%;border:0;background:transparent;border-radius:11px;padding:9px 10px;text-align:left;display:grid;grid-template-columns:36px minmax(0,1fr) auto;gap:9px;align-items:center;cursor:pointer;color:#223342}.sun-chat-thread:hover{background:#f0f3f5}.sun-chat-thread.on{background:#eaf1f4}.sun-chat-avatar{width:36px;height:36px;border-radius:12px;background:#e3eaee;display:grid;place-items:center;font-weight:900;color:#36566b;position:relative;flex:none}.sun-chat-avatar.company{background:#1e4057;color:#fff}.sun-chat-avatar.order{background:#f3ead3;color:#7b5d1f}.sun-chat-presence-dot{position:absolute;width:10px;height:10px;border-radius:50%;right:-2px;bottom:-2px;background:#aeb5bb;border:2px solid #fff}.sun-chat-presence-dot.on{background:#28a568}.sun-chat-thread-main{min-width:0}.sun-chat-thread-title{font-size:13px;font-weight:850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sun-chat-thread-preview{font-size:10.5px;color:#838b94;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sun-chat-unread{min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:#17384d;color:#fff;font:800 10px/20px Arial;text-align:center}.sun-chat-role{font-size:9px;color:#9299a1;margin-top:2px}
|
||
.sun-chat-main{display:flex;flex-direction:column;min-width:0;min-height:0;background:#fff}.sun-chat-head{min-height:68px;border-bottom:1px solid #e7eaee;padding:10px 16px;display:flex;align-items:center;gap:12px}.sun-chat-head-back{border:0;background:#f0f2f4;border-radius:10px;padding:8px 10px;cursor:pointer;font-weight:800}.sun-chat-head-text{min-width:0;flex:1}.sun-chat-head-title{font-size:16px;font-weight:900;color:#213649;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sun-chat-head-sub{font-size:11px;color:#808891;margin-top:3px}.sun-chat-online-text{color:#22845a}.sun-chat-body{flex:1;min-height:0;overflow:auto;background:linear-gradient(#f8f9fb,#f5f7f9);padding:18px}.sun-chat-empty{height:100%;display:grid;place-items:center;text-align:center;color:#858d96;padding:30px}.sun-chat-empty b{display:block;color:#52616d;font-size:18px;margin-bottom:5px}.sun-chat-day{text-align:center;margin:10px 0 14px}.sun-chat-day span{display:inline-block;background:#e7ebee;color:#707a83;border-radius:999px;padding:4px 9px;font-size:10px}.sun-chat-msg{max-width:min(680px,82%);margin:6px 0;display:flex;gap:8px;align-items:flex-end}.sun-chat-msg.own{margin-left:auto;flex-direction:row-reverse}.sun-chat-bubble{border:1px solid #e1e5e9;background:#fff;border-radius:15px 15px 15px 4px;padding:9px 11px;box-shadow:0 3px 12px rgba(20,30,40,.035);min-width:100px}.sun-chat-msg.own .sun-chat-bubble{background:#e8f1f4;border-color:#d8e6eb;border-radius:15px 15px 4px 15px}.sun-chat-sender{font-size:10px;font-weight:850;color:#47667a;margin-bottom:4px}.sun-chat-text{font-size:13px;line-height:1.42;color:#253642;overflow-wrap:anywhere;white-space:pre-wrap}.sun-chat-meta{display:flex;gap:7px;justify-content:flex-end;align-items:center;margin-top:5px;font-size:9px;color:#8c949c}.sun-chat-read{font-weight:750;color:#718693}.sun-chat-files{display:grid;gap:6px;margin-top:7px}.sun-chat-file{border:1px solid #dfe4e8;border-radius:10px;background:rgba(255,255,255,.7);overflow:hidden;display:block;color:#2d536a;text-decoration:none;max-width:360px}.sun-chat-file-img{width:100%;max-height:260px;display:block;object-fit:cover;background:#eef1f3}.sun-chat-file-line{display:flex;align-items:center;gap:8px;padding:8px 9px}.sun-chat-file-icon{font-size:20px}.sun-chat-file-name{font-size:11px;font-weight:750;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sun-chat-file-size{font-size:9px;color:#8a929a;margin-top:2px}
|
||
.sun-chat-typing{height:22px;padding:3px 16px;color:#71808a;font-size:10px;background:#f8f9fb}.sun-chat-composer{border-top:1px solid #e4e8eb;padding:10px 12px;background:#fff}.sun-chat-pending{display:flex;gap:6px;flex-wrap:wrap;margin:0 0 8px}.sun-chat-pending:empty{display:none}.sun-chat-pending-chip{display:flex;align-items:center;gap:6px;background:#eef2f4;border-radius:9px;padding:5px 7px;max-width:260px}.sun-chat-pending-chip span{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sun-chat-pending-chip button{border:0;background:transparent;cursor:pointer;font-size:15px}.sun-chat-compose-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:end}.sun-chat-attach,.sun-chat-send{border:0;border-radius:11px;height:40px;cursor:pointer;font-weight:850}.sun-chat-attach{width:42px;background:#eef2f4;color:#476170;font-size:20px}.sun-chat-send{padding:0 17px;background:#17384d;color:#fff}.sun-chat-send:disabled{opacity:.55;cursor:default}.sun-chat-input{width:100%;min-height:40px;max-height:130px;resize:none;border:1px solid #d7dde1;border-radius:12px;padding:10px 12px;line-height:1.35;outline:none}.sun-chat-input:focus{border-color:#7896a9;box-shadow:0 0 0 3px rgba(70,110,135,.08)}
|
||
.sun-chat-order-tab{position:relative}.sun-chat-order-tab .sun-chat-mini-badge{position:absolute;top:-2px;right:-3px;background:#d83b35;color:#fff;border-radius:999px;min-width:16px;height:16px;padding:0 4px;font:800 9px/16px Arial}.sun-order-chat-panel{display:none;min-height:570px;height:min(68vh,760px);border:1px solid #e1e5e9;border-radius:14px;overflow:hidden;margin-top:10px;background:#fff}.sun-order-chat-panel.on{display:block}.sun-order-chat-panel .sun-chat-main{height:100%;min-height:0}.sun-chat-offline-note{padding:14px;border-radius:12px;background:#fff6e2;color:#85631c;margin:18px}
|
||
@media(max-width:850px){#sun-chat-view{padding:0}.sun-chat-shell{height:calc(100vh - 94px);min-height:0;border-radius:0;border-left:0;border-right:0;grid-template-columns:1fr}.sun-chat-side{display:flex}.sun-chat-main{display:none}.sun-chat-shell.thread-open .sun-chat-side{display:none}.sun-chat-shell.thread-open .sun-chat-main{display:flex}.sun-chat-msg{max-width:92%}.sun-chat-body{padding:12px}.sun-chat-section-tab{font-size:10px}.sun-order-chat-panel{height:62vh;min-height:460px}.sun-order-chat-panel .sun-chat-main{display:flex!important}.sun-order-chat-panel .sun-chat-head{min-height:58px}}
|
||
`;document.head.appendChild(s);
|
||
}
|
||
|
||
function ensureView(){
|
||
installStyle();
|
||
let v=$('sun-chat-view');
|
||
if(!v){
|
||
v=document.createElement('section');v.id='sun-chat-view';v.className='view';v.innerHTML=`<div class="sun-chat-shell" id="sunChatShellV29"><aside class="sun-chat-side"><div class="sun-chat-side-head"><h1>Чат</h1><p id="sunChatCompanyNameV29">Команда Caterium</p><div class="sun-chat-section-tabs"><button class="sun-chat-section-tab on" type="button" data-chat-section="general">Общий<span class="sun-chat-section-badge" data-chat-section-badge="general"></span></button><button class="sun-chat-section-tab" type="button" data-chat-section="direct">Личные<span class="sun-chat-section-badge" data-chat-section-badge="direct"></span></button><button class="sun-chat-section-tab" type="button" data-chat-section="orders">Заказы<span class="sun-chat-section-badge" data-chat-section-badge="orders"></span></button></div></div><div class="sun-chat-side-scroll" id="sunChatSidebarV29"></div></aside><main class="sun-chat-main" id="sunChatMainV29"><div class="sun-chat-head" id="sunChatHeadV29"></div><div class="sun-chat-body" id="sunChatBodyV29"><div class="sun-chat-empty"><div><b>Выберите чат</b><span>Общий чат компании, сотрудника или обсуждение заказа.</span></div></div></div><div class="sun-chat-typing" id="sunChatTypingV29"></div><div class="sun-chat-composer" id="sunChatComposerV29" hidden><div class="sun-chat-pending" id="sunChatPendingV29"></div><div class="sun-chat-compose-row"><button class="sun-chat-attach" type="button" id="sunChatAttachV29" title="Добавить фото или файл">+</button><textarea class="sun-chat-input" id="sunChatInputV29" rows="1" maxlength="4000" placeholder="Сообщение…"></textarea><button class="sun-chat-send" type="button" id="sunChatSendV29">Отправить</button><input type="file" id="sunChatFilesV29" hidden multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv,.zip"></div></div></main></div>`;
|
||
const firstModal=document.querySelector('body > .modal');if(firstModal)firstModal.before(v);else document.body.appendChild(v);
|
||
$('sunChatAttachV29').onclick=()=>$('sunChatFilesV29').click();
|
||
$('sunChatFilesV29').onchange=e=>addPendingFiles(e.target.files);
|
||
$('sunChatSendV29').onclick=sendMessage;
|
||
$('sunChatInputV29').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}});
|
||
$('sunChatInputV29').addEventListener('input',()=>{autoGrowInput();sendTyping(true);});
|
||
qa('[data-chat-section]',v).forEach(b=>b.addEventListener('click',()=>switchSidebarMode(b.dataset.chatSection)));
|
||
}
|
||
return v;
|
||
}
|
||
|
||
function ensureNav(){
|
||
const nav=document.querySelector('header nav');if(!nav)return null;
|
||
let b=$('sunChatNavV29');
|
||
if(!available()){b?.remove();return null;}
|
||
if(!b){
|
||
b=document.createElement('button');b.id='sunChatNavV29';b.type='button';b.dataset.navLabel='Чат';b.className='nav-chat';b.innerHTML=`Чат <span class="sun-chat-nav-badge" id="sunChatNavBadgeV29"></span>`;
|
||
b.addEventListener('click',()=>openMain({mode:'general'}));
|
||
}
|
||
if(typeof window.sunRegisterNavButton==='function'){
|
||
window.sunRegisterNavButton({button:b,group:'management',beforeLabel:'Настройки',navLabel:'Чат',className:'nav-chat'});
|
||
}else{
|
||
const group=nav.querySelector('.sun-nav-management')||nav;
|
||
if(b.parentElement!==group)group.appendChild(b);
|
||
b.classList.add('sun-nav-button','nav-chat');
|
||
}
|
||
return b;
|
||
}
|
||
|
||
function setNavBadge(n){ const b=$('sunChatNavBadgeV29');if(!b)return;n=Number(n||0);b.textContent=n>99?'99+':String(n);b.classList.toggle('on',n>0); }
|
||
function setSectionBadge(mode,n){const b=document.querySelector(`[data-chat-section-badge="${mode}"]`);if(!b)return;n=Number(n||0);b.textContent=n>99?'99+':String(n);b.classList.toggle('on',n>0);}
|
||
function setActiveNav(){qa('header nav button').forEach(x=>x.classList.remove('on'));$('sunChatNavV29')?.classList.add('on');qa('.view').forEach(x=>x.classList.remove('on'));ensureView().classList.add('on');}
|
||
function threadIcon(t){if(t.kind==='company')return ['КО','company'];if(t.kind==='order')return ['№','order'];return [String(t.title||'?').trim().slice(0,1).toUpperCase(),''];}
|
||
function threadById(id){return threads.find(t=>String(t.thread_id)===String(id))||null;}
|
||
function directByUser(id){return threads.find(t=>t.kind==='direct'&&String(t.other_user_id)===String(id))||null;}
|
||
function orderThreadByOrder(id){return threads.find(t=>t.kind==='order'&&String(t.order_id)===String(id))||null;}
|
||
function memberById(id){return members.find(m=>String(m.user_id)===String(id))||null;}
|
||
|
||
function switchSidebarMode(mode){
|
||
sidebarMode=['general','direct','orders'].includes(mode)?mode:'general';
|
||
qa('[data-chat-section]').forEach(b=>b.classList.toggle('on',b.dataset.chatSection===sidebarMode));
|
||
renderSidebar();
|
||
if(sidebarMode==='general'){
|
||
const t=threads.find(x=>x.kind==='company');
|
||
if(t&&(!activeThread||activeThread.kind!=='company'))openThread(t);
|
||
}else if(activeThread&&((sidebarMode==='direct'&&activeThread.kind!=='direct')||(sidebarMode==='orders'&&activeThread.kind!=='order'))){
|
||
activeThread=null;messages=[];unsubscribeThread();$('sunChatShellV29')?.classList.remove('thread-open');$('sunChatComposerV29').hidden=true;renderHead();renderMessages();
|
||
}
|
||
}
|
||
|
||
function renderSidebar(){
|
||
const root=$('sunChatSidebarV29');if(!root)return;
|
||
$('sunChatCompanyNameV29').textContent=workspace()?.name||'Компания';
|
||
qa('[data-chat-section]').forEach(b=>b.classList.toggle('on',b.dataset.chatSection===sidebarMode));
|
||
const general=threads.find(t=>t.kind==='company');
|
||
const others=members.filter(m=>String(m.user_id)!==String(me()));
|
||
const localOrders=savedOrders().sort((a,b)=>Number(b?.id||0)-Number(a?.id||0));
|
||
const row=t=>{const [letter,cls]=threadIcon(t);const isOn=activeThread&&String(activeThread.thread_id)===String(t.thread_id);return `<button class="sun-chat-thread ${isOn?'on':''}" type="button" data-chat-thread="${esc(t.thread_id)}"><span class="sun-chat-avatar ${cls}">${esc(letter)}${t.kind==='direct'?`<i class="sun-chat-presence-dot ${online.has(String(t.other_user_id))?'on':''}"></i>`:''}</span><span class="sun-chat-thread-main"><span class="sun-chat-thread-title">${esc(t.title||'Чат')}</span><span class="sun-chat-thread-preview">${esc(t.last_message|| (t.kind==='company'?'Сообщения всей команды':t.kind==='order'?'Обсуждение заказа':'Личная переписка'))}</span></span>${Number(t.unread_count||0)>0?`<span class="sun-chat-unread">${Number(t.unread_count)>99?'99+':Number(t.unread_count)}</span>`:'<span></span>'}</button>`};
|
||
const memberRow=m=>{const t=directByUser(m.user_id);if(t)return row(t);const isOnline=online.has(String(m.user_id));return `<button class="sun-chat-thread" type="button" data-chat-user="${esc(m.user_id)}"><span class="sun-chat-avatar">${esc(String(m.display_name||m.email||'?').trim().slice(0,1).toUpperCase())}<i class="sun-chat-presence-dot ${isOnline?'on':''}"></i></span><span class="sun-chat-thread-main"><span class="sun-chat-thread-title">${esc(m.display_name||m.email||'Сотрудник')}</span><span class="sun-chat-role">${esc(ROLE_LABELS[m.role]||m.role||'Сотрудник')} · ${isOnline?'онлайн':'не в сети'}</span></span><span></span></button>`};
|
||
const orderRow=o=>{const t=orderThreadByOrder(o.id);if(t)return row({...t,title:`№${o.id}${o.event?` · ${o.event}`:''}`});return `<button class="sun-chat-thread" type="button" data-chat-order="${esc(o.id)}"><span class="sun-chat-avatar order">№</span><span class="sun-chat-thread-main"><span class="sun-chat-thread-title">№${esc(o.id)}${o.event?` · ${esc(o.event)}`:''}</span><span class="sun-chat-thread-preview">${esc([o.date,o.time].filter(Boolean).join(' · ')||'Открыть обсуждение заказа')}</span></span><span></span></button>`};
|
||
if(sidebarMode==='general')root.innerHTML=`<div class="sun-chat-group-title"><span>Общий чат компании</span></div><div class="sun-chat-list">${general?row(general):'<div class="hint" style="padding:8px">Загружаю общий чат…</div>'}</div>`;
|
||
if(sidebarMode==='direct')root.innerHTML=`<div class="sun-chat-group-title"><span>Сотрудники</span><span>${others.length}</span></div><div class="sun-chat-list">${others.length?others.map(memberRow).join(''):'<div class="hint" style="padding:8px">Добавьте сотрудников — здесь появятся личные чаты.</div>'}</div>`;
|
||
if(sidebarMode==='orders')root.innerHTML=`<div class="sun-chat-group-title"><span>Чаты заказов</span><span>${localOrders.length}</span></div><div class="sun-chat-list">${localOrders.length?localOrders.map(orderRow).join(''):'<div class="hint" style="padding:8px">Сохранённых заказов пока нет.</div>'}</div>`;
|
||
qa('[data-chat-thread]',root).forEach(b=>b.onclick=()=>openThread(threadById(b.dataset.chatThread)));
|
||
qa('[data-chat-user]',root).forEach(b=>b.onclick=()=>openDirect(b.dataset.chatUser));
|
||
qa('[data-chat-order]',root).forEach(b=>b.onclick=()=>openOrderGlobal(b.dataset.chatOrder));
|
||
updateUnreadBadges();
|
||
}
|
||
|
||
function updateUnreadBadges(){
|
||
const general=threads.filter(t=>t.kind==='company').reduce((n,t)=>n+Number(t.unread_count||0),0);
|
||
const direct=threads.filter(t=>t.kind==='direct').reduce((n,t)=>n+Number(t.unread_count||0),0);
|
||
const order=threads.filter(t=>t.kind==='order').reduce((n,t)=>n+Number(t.unread_count||0),0);
|
||
setSectionBadge('general',general);setSectionBadge('direct',direct);setSectionBadge('orders',order);setNavBadge(general+direct+order);updateOrderChatBadge();
|
||
}
|
||
|
||
function activeSubtitle(t){
|
||
if(!t)return '';
|
||
if(t.kind==='direct'){const m=memberById(t.other_user_id);return online.has(String(t.other_user_id))?'<span class="sun-chat-online-text">онлайн</span>':esc(ROLE_LABELS[m?.role]||'сотрудник');}
|
||
if(t.kind==='company'){const count=members.length,on=members.filter(m=>online.has(String(m.user_id))).length;return `${count} сотрудников · ${on} онлайн`;}
|
||
return 'Обсуждение заказа';
|
||
}
|
||
|
||
function renderHead(){
|
||
const h=$('sunChatHeadV29');if(!h)return;
|
||
if(!activeThread){h.innerHTML='';return;}
|
||
const mobileBack=chatDock==='global'?`<button class="sun-chat-head-back" type="button" data-chat-mobile-back>←</button>`:'';
|
||
const orderBack=chatDock==='order'?`<button class="sun-chat-head-back" type="button" data-chat-order-back>← Заказ</button>`:'';
|
||
h.innerHTML=`${orderBack||mobileBack}<div class="sun-chat-head-text"><div class="sun-chat-head-title">${esc(activeThread.title||'Чат')}</div><div class="sun-chat-head-sub">${activeSubtitle(activeThread)}</div></div>`;
|
||
h.querySelector('[data-chat-mobile-back]')?.addEventListener('click',()=>{activeThread=null;messages=[];unsubscribeThread();$('sunChatShellV29')?.classList.remove('thread-open');renderSidebar();renderHead();renderMessages();$('sunChatComposerV29').hidden=true;});
|
||
h.querySelector('[data-chat-order-back]')?.addEventListener('click',()=>$('orderTab')?.click());
|
||
}
|
||
|
||
function renderMessages(){
|
||
const root=$('sunChatBodyV29');if(!root)return;
|
||
if(!activeThread){root.innerHTML='<div class="sun-chat-empty"><div><b>Выберите чат</b><span>Общий чат компании, сотрудника или обсуждение заказа.</span></div></div>';return;}
|
||
if(loadingMessages){root.innerHTML='<div class="sun-chat-empty"><div><b>Загружаю сообщения…</b></div></div>';return;}
|
||
if(!messages.length){root.innerHTML='<div class="sun-chat-empty"><div><b>Пока нет сообщений</b><span>Напишите первое сообщение.</span></div></div>';return;}
|
||
let prev=null,html='';
|
||
for(const m of messages){
|
||
if(!prev||!sameDay(prev,m.created_at))html+=`<div class="sun-chat-day"><span>${esc(fmtDay(m.created_at))}</span></div>`;
|
||
prev=m.created_at;const own=String(m.sender_user_id)===String(me());const atts=Array.isArray(m.attachments)?m.attachments:[];
|
||
const files=atts.length?`<div class="sun-chat-files">${atts.map((a,i)=>`<a class="sun-chat-file" href="#" data-chat-attachment="${esc(a.path||'')}" data-chat-type="${esc(a.type||'')}" data-chat-message="${esc(m.message_id)}" data-chat-index="${i}" target="_blank" rel="noopener"><div class="sun-chat-file-preview"></div><div class="sun-chat-file-line"><span class="sun-chat-file-icon">${String(a.type||'').startsWith('image/')?'▣':'▤'}</span><span style="min-width:0"><div class="sun-chat-file-name">${esc(a.name||'Файл')}</div><div class="sun-chat-file-size">${esc(bytes(a.size))}</div></span></div></a>`).join('')}</div>`:'';
|
||
const read=own?(Number(m.read_by_count||0)>0?(activeThread.kind==='direct'?'Прочитано':`Прочитали: ${Number(m.read_by_count)}`):'Доставлено'):'';
|
||
html+=`<div class="sun-chat-msg ${own?'own':''}"><div class="sun-chat-bubble">${!own&&activeThread.kind!=='direct'?`<div class="sun-chat-sender">${esc(m.sender_name||m.sender_email||'Сотрудник')}</div>`:''}${m.body?`<div class="sun-chat-text">${esc(m.body)}</div>`:''}${files}<div class="sun-chat-meta"><span>${esc(fmtTime(m.created_at))}</span>${read?`<span class="sun-chat-read">${esc(read)}</span>`:''}</div></div></div>`;
|
||
}
|
||
root.innerHTML=html;hydrateAttachments();requestAnimationFrame(()=>{root.scrollTop=root.scrollHeight});
|
||
}
|
||
|
||
async function hydrateAttachments(){
|
||
const c=client();if(!c)return;
|
||
const links=qa('[data-chat-attachment]',$('sunChatBodyV29'));
|
||
for(const a of links){const path=a.dataset.chatAttachment;if(!path)continue;try{const r=await c.storage.from('sun-chat').createSignedUrl(path,3600);if(r.error)throw r.error;const url=r.data?.signedUrl;if(!url)continue;a.href=url;const type=a.dataset.chatType||'';if(type.startsWith('image/')){const p=a.querySelector('.sun-chat-file-preview');if(p)p.innerHTML=`<img class="sun-chat-file-img" alt="Вложение" src="${esc(url)}">`;}}catch(e){a.onclick=ev=>{ev.preventDefault();toast('Не удалось открыть вложение.','error')}}}
|
||
}
|
||
|
||
function renderTyping(){const root=$('sunChatTypingV29');if(!root)return;const names=[...typingUsers.values()].filter(x=>x&&x.user_id!==me()&&Date.now()-x.at<3500).map(x=>x.name||'Сотрудник');root.textContent=names.length?`${names.slice(0,2).join(', ')} ${names.length>1?'печатают':'печатает'}…`:'';}
|
||
function renderPending(){const root=$('sunChatPendingV29');if(!root)return;root.innerHTML=pendingFiles.map((f,i)=>`<div class="sun-chat-pending-chip"><span>${esc(f.name)} · ${esc(bytes(f.size))}</span><button type="button" data-remove-file="${i}">×</button></div>`).join('');qa('[data-remove-file]',root).forEach(b=>b.onclick=()=>{pendingFiles.splice(Number(b.dataset.removeFile),1);renderPending()});}
|
||
function autoGrowInput(){const x=$('sunChatInputV29');if(!x)return;x.style.height='auto';x.style.height=Math.min(130,x.scrollHeight)+'px';}
|
||
|
||
async function loadThreads({quiet=false}={}){
|
||
if(!available()||loadingThreads)return;
|
||
loadingThreads=true;
|
||
try{
|
||
const ws=workspace();await rpc('sun_chat_get_company_thread_v29',{p_workspace:ws.id});
|
||
const [t,m]=await Promise.all([rpc('sun_chat_list_threads_v29',{p_workspace:ws.id}),rpc('sun_chat_list_members_v29',{p_workspace:ws.id})]);
|
||
threads=Array.isArray(t)?t:[];members=Array.isArray(m)?m:[];
|
||
if(activeThread){const fresh=threadById(activeThread.thread_id);if(fresh)activeThread={...activeThread,...fresh};}
|
||
renderSidebar();renderHead();updateUnreadBadges();
|
||
}catch(e){if(!quiet)toast(e.message||String(e),'error',6500)}finally{loadingThreads=false;}
|
||
}
|
||
|
||
async function loadMessages({mark=true,quiet=false}={}){
|
||
if(!activeThread||!available()||loadingMessages)return;
|
||
loadingMessages=true;renderMessages();
|
||
try{
|
||
const data=await rpc('sun_chat_list_messages_v29',{p_thread:activeThread.thread_id,p_limit:120,p_before:null});messages=Array.isArray(data)?data:[];
|
||
if(mark){const t=threadById(activeThread.thread_id);if(Number(t?.unread_count||0)>0){await rpc('sun_chat_mark_read_v29',{p_thread:activeThread.thread_id});await loadThreads({quiet:true});}}
|
||
}catch(e){if(!quiet)toast(e.message||String(e),'error',6500)}finally{loadingMessages=false;renderMessages();}
|
||
}
|
||
|
||
async function openThread(t){
|
||
if(!t)return;activeThread=t;messages=[];pendingFiles=[];$('sunChatShellV29')?.classList.add('thread-open');$('sunChatComposerV29').hidden=false;$('sunChatInputV29').value='';renderPending();renderSidebar();renderHead();renderMessages();await subscribeThread(t.thread_id);await loadMessages({mark:true});setTimeout(()=>$('sunChatInputV29')?.focus(),80);
|
||
}
|
||
|
||
async function openDirect(userId){
|
||
if(!available())return;try{sidebarMode='direct';const id=await rpc('sun_chat_get_direct_thread_v29',{p_workspace:workspace().id,p_other_user:userId});await loadThreads({quiet:true});const t=threadById(id)||{thread_id:id,kind:'direct',other_user_id:userId,title:memberById(userId)?.display_name||'Сотрудник',unread_count:0};await openThread(t);}catch(e){toast(e.message||String(e),'error',6500)}
|
||
}
|
||
|
||
async function getOrderThread(orderId){
|
||
const id=await rpc('sun_chat_get_order_thread_v29',{p_workspace:workspace().id,p_order_id:String(orderId)});await loadThreads({quiet:true});return threadById(id)||{thread_id:id,kind:'order',order_id:String(orderId),title:`Заказ №${orderId}`,unread_count:0};
|
||
}
|
||
|
||
async function openOrderGlobal(orderId){
|
||
if(!available())return toast('Войдите в Caterium, чтобы открыть обсуждение заказа.','warn');if(!orderId)return toast('Сначала сохраните заказ.','warn');
|
||
try{dockMainGlobal();setActiveNav();sidebarMode='orders';await ensureWorkspaceChannel();const t=await getOrderThread(orderId);renderSidebar();await openThread(t);}catch(e){toast(e.message||String(e),'error',6500)}
|
||
}
|
||
|
||
function ensureOrderChatPanel(){
|
||
const sub=document.querySelector('#new .subnav');if(!sub)return null;
|
||
let p=$('sunOrderChatPanelV31');
|
||
if(!p){p=document.createElement('div');p.id='sunOrderChatPanelV31';p.className='sun-order-chat-panel';sub.after(p);}
|
||
return p;
|
||
}
|
||
|
||
function dockMainGlobal(){
|
||
const main=$('sunChatMainV29'),shell=$('sunChatShellV29');if(main&&shell&&main.parentElement!==shell)shell.appendChild(main);chatDock='global';
|
||
}
|
||
function dockMainOrder(){
|
||
const main=$('sunChatMainV29'),panel=ensureOrderChatPanel();if(main&&panel&&main.parentElement!==panel)panel.appendChild(main);chatDock='order';
|
||
}
|
||
|
||
function setOrderChatMode(on){
|
||
const sub=document.querySelector('#new .subnav'),card=sub?.closest('.card'),panel=ensureOrderChatPanel(),chatTab=$('sunOrderChatTabV31');if(!card||!panel)return;
|
||
const money=card.querySelector(':scope > .money-form'),summary=card.querySelector(':scope > .order-summary'),actions=card.querySelector(':scope > .actions');
|
||
if(on){
|
||
$('orderForm')?.style.setProperty('display','none');$('details')?.style.setProperty('display','none');
|
||
if(money)money.style.display='none';if(summary)summary.style.display='none';if(actions)actions.style.display='none';
|
||
panel.classList.add('on');panel.hidden=false;$('orderTab')?.classList.remove('on');$('detailTab')?.classList.remove('on');chatTab?.classList.add('on');dockMainOrder();
|
||
}else{
|
||
panel.classList.remove('on');panel.hidden=true;if(money)money.style.display='';if(summary)summary.style.display='';if(actions)actions.style.display='';chatTab?.classList.remove('on');dockMainGlobal();
|
||
}
|
||
}
|
||
|
||
async function openOrderInline(orderId){
|
||
if(!available())return toast('Войдите в Caterium, чтобы открыть обсуждение заказа.','warn');if(!orderId)return toast('Сначала сохраните заказ — после этого появится чат.','warn');
|
||
try{ensureView();ensureOrderDiscussionTab();setOrderChatMode(true);await ensureWorkspaceChannel();sidebarMode='orders';const t=await getOrderThread(orderId);await openThread(t);}catch(e){setOrderChatMode(false);toast(e.message||String(e),'error',6500)}
|
||
}
|
||
|
||
async function openMain({threadId=null,mode='general'}={}){
|
||
if(!available())return toast('Чат доступен после входа в рабочую компанию.','warn');ensureView();dockMainGlobal();ensureNav();setActiveNav();sidebarMode=['general','direct','orders'].includes(mode)?mode:'general';await ensureWorkspaceChannel();await loadThreads();let t=threadId?threadById(threadId):null;if(!t&&sidebarMode==='general')t=threads.find(x=>x.kind==='company')||null;if(t)await openThread(t);else{activeThread=null;messages=[];unsubscribeThread();$('sunChatShellV29')?.classList.remove('thread-open');$('sunChatComposerV29').hidden=true;renderSidebar();renderHead();renderMessages();}
|
||
}
|
||
|
||
async function addPendingFiles(fileList){
|
||
const files=[...(fileList||[])];$('sunChatFilesV29').value='';
|
||
for(const f of files){if(pendingFiles.length>=5){toast('Можно отправить не более 5 файлов за раз.','warn');break}if(f.size>15*1024*1024){toast(`Файл «${f.name}» больше 15 МБ.`,'warn');continue}pendingFiles.push(f)}renderPending();
|
||
}
|
||
|
||
async function uploadPending(){
|
||
const c=client(),ws=workspace();if(!c||!ws||!activeThread)return [];
|
||
const result=[];
|
||
for(const f of pendingFiles){const path=`${ws.id}/${activeThread.thread_id}/${uid()}-${safeName(f.name)}`;const up=await c.storage.from('sun-chat').upload(path,f,{contentType:f.type||'application/octet-stream',upsert:false});if(up.error)throw up.error;result.push({path,name:f.name,type:f.type||'application/octet-stream',size:f.size});}
|
||
return result;
|
||
}
|
||
|
||
async function sendMessage(){
|
||
if(!activeThread||!available())return;const input=$('sunChatInputV29'),btn=$('sunChatSendV29');const text=String(input?.value||'').trim();if(!text&&!pendingFiles.length)return;
|
||
btn.disabled=true;btn.textContent='…';
|
||
try{const attachments=await uploadPending();await rpc('sun_chat_send_message_v29',{p_thread:activeThread.thread_id,p_body:text,p_attachments:attachments});input.value='';input.style.height='auto';pendingFiles=[];renderPending();sendTyping(false);await Promise.all([loadThreads({quiet:true}),loadMessages({mark:false,quiet:true})]);}
|
||
catch(e){toast(e.message||String(e),'error',7000)}finally{btn.disabled=false;btn.textContent='Отправить';input?.focus();}
|
||
}
|
||
|
||
async function ensureWorkspaceChannel(){
|
||
if(!available())return unsubscribeWorkspace();const c=client(),ws=workspace(),ss=session();if(!c||!ws||!ss)return;
|
||
if(workspaceChannel&&workspaceChannelId===ws.id)return;
|
||
unsubscribeWorkspace();workspaceChannelId=ws.id;
|
||
try{if(c.realtime?.setAuth)await c.realtime.setAuth(ss.access_token);}catch(_){}
|
||
const ch=c.channel(`sun-chat-workspace:${ws.id}`,{config:{private:true,presence:{key:me()},broadcast:{ack:true}}});workspaceChannel=ch;
|
||
ch.on('broadcast',{event:'chat_changed'},async()=>{await loadThreads({quiet:true});if(activeThread){const fresh=threadById(activeThread.thread_id);if(fresh&&Number(fresh.unread_count||0)>0)await loadMessages({mark:true,quiet:true});else renderMessages();}})
|
||
.on('presence',{event:'sync'},()=>{online=new Set(Object.keys(ch.presenceState()||{}));renderSidebar();renderHead();})
|
||
.on('presence',{event:'join'},()=>{online=new Set(Object.keys(ch.presenceState()||{}));renderSidebar();renderHead();})
|
||
.on('presence',{event:'leave'},()=>{online=new Set(Object.keys(ch.presenceState()||{}));renderSidebar();renderHead();})
|
||
.subscribe(async(status)=>{if(status==='SUBSCRIBED'){try{await ch.track({user_id:me(),name:meName(),at:new Date().toISOString()})}catch(_){}await loadThreads({quiet:true});}});
|
||
}
|
||
|
||
function unsubscribeWorkspace(){if(workspaceChannel){try{client()?.removeChannel?.(workspaceChannel)}catch(_){}workspaceChannel=null;}workspaceChannelId='';online=new Set();unsubscribeThread();}
|
||
async function subscribeThread(id){
|
||
if(!available()||!id)return;const c=client(),ss=session();if(threadChannel&&threadChannelId===id)return;unsubscribeThread();threadChannelId=id;
|
||
try{if(c.realtime?.setAuth)await c.realtime.setAuth(ss.access_token);}catch(_){}
|
||
const ch=c.channel(`sun-chat-thread:${id}`,{config:{private:true,broadcast:{ack:false,self:false}}});threadChannel=ch;
|
||
ch.on('broadcast',{event:'message'},()=>loadMessages({mark:true,quiet:true}))
|
||
.on('broadcast',{event:'read'},()=>loadMessages({mark:false,quiet:true}))
|
||
.on('broadcast',{event:'typing'},payload=>{const p=payload?.payload||{};if(!p.user_id||String(p.user_id)===String(me()))return;if(p.typing)typingUsers.set(String(p.user_id),{user_id:p.user_id,name:p.name||'Сотрудник',at:Date.now()});else typingUsers.delete(String(p.user_id));renderTyping();setTimeout(()=>renderTyping(),3600)})
|
||
.subscribe();
|
||
}
|
||
function unsubscribeThread(){if(threadChannel){try{client()?.removeChannel?.(threadChannel)}catch(_){}threadChannel=null;}threadChannelId='';typingUsers.clear();renderTyping();}
|
||
async function sendTyping(flag){
|
||
if(!threadChannel||!activeThread)return;const now=Date.now();if(flag&&now-lastTypingSent<700){clearTimeout(typingStopTimer);typingStopTimer=setTimeout(()=>sendTyping(false),1800);return}lastTypingSent=now;
|
||
try{await threadChannel.send({type:'broadcast',event:'typing',payload:{user_id:me(),name:meName(),typing:Boolean(flag)}})}catch(_){}
|
||
clearTimeout(typingStopTimer);if(flag)typingStopTimer=setTimeout(()=>sendTyping(false),1800);
|
||
}
|
||
|
||
function updateOrderChatBadge(){
|
||
const b=$('sunOrderChatTabV31');if(!b)return;const id=currentOrderId(),t=id?orderThreadByOrder(id):null,n=Number(t?.unread_count||0);let badge=b.querySelector('.sun-chat-mini-badge');if(n>0){if(!badge){badge=document.createElement('span');badge.className='sun-chat-mini-badge';b.appendChild(badge);}badge.textContent=n>99?'99+':String(n);}else badge?.remove();
|
||
}
|
||
|
||
function ensureOrderDiscussionTab(){
|
||
const sub=document.querySelector('#new .subnav');if(!sub)return;
|
||
ensureOrderChatPanel();
|
||
let b=$('sunOrderChatTabV31');
|
||
$('sunOrderChatTabV29')?.remove();
|
||
if(!b){
|
||
b=document.createElement('button');b.id='sunOrderChatTabV31';b.type='button';b.className='sun-chat-order-tab';b.textContent='Чат';sub.appendChild(b);
|
||
b.addEventListener('click',e=>{e.preventDefault();const id=currentOrderId();if(!id)return toast('Сначала сохраните заказ — после этого появится чат.','warn');openOrderInline(id);});
|
||
}
|
||
b.title=currentOrderId()?'Чат этого заказа':'Сначала сохраните заказ';b.style.opacity=currentOrderId()?'1':'.55';
|
||
['orderTab','detailTab'].forEach(id=>{const x=$(id);if(x&&!x.dataset.sunChatCloseBound){x.dataset.sunChatCloseBound='1';x.addEventListener('click',()=>setOrderChatMode(false));}});
|
||
updateOrderChatBadge();
|
||
}
|
||
|
||
function openOrderFromCard(id){
|
||
if(!id)return;
|
||
try{if(typeof window.openOrder==='function')window.openOrder(Number(id));}catch(e){return toast(e.message||String(e),'error')}
|
||
setTimeout(()=>openOrderInline(String(id)),60);
|
||
}
|
||
|
||
function decorateOrderCards(){
|
||
qa('#sunOrderCards .sun-order-card').forEach(card=>{const actions=card.querySelector('.sun-order-card-actions');if(!actions||actions.querySelector('[data-card-chat]'))return;const b=document.createElement('button');b.className='mini';b.type='button';b.dataset.cardChat=card.dataset.orderId;b.textContent='Чат';b.onclick=e=>{e.stopPropagation();openOrderFromCard(b.dataset.cardChat)};actions.prepend(b);});
|
||
}
|
||
|
||
function safeStateChanged(){
|
||
try{
|
||
installStyle();ensureView();ensureOrderDiscussionTab();decorateOrderCards();
|
||
if(available()){ensureNav();ensureWorkspaceChannel();loadThreads({quiet:true});}
|
||
else{setNavBadge(0);$('sunChatNavV29')?.remove();unsubscribeWorkspace();activeThread=null;threads=[];members=[];messages=[];setOrderChatMode(false);}
|
||
}catch(e){console.error('[Caterium Chat] state refresh failed',e);}
|
||
}
|
||
|
||
function boot(){
|
||
installStyle();ensureView();ensureOrderDiscussionTab();
|
||
const priorOpen=window.openOrder;
|
||
if(typeof priorOpen==='function'&&!priorOpen.__sunChatV31){const wrapped=function(...args){setOrderChatMode(false);const r=priorOpen.apply(this,args);setTimeout(()=>{ensureOrderDiscussionTab();decorateOrderCards();},0);return r};wrapped.__sunChatV31=true;window.openOrder=wrapped;}
|
||
const priorRenderOrders=window.renderOrders;
|
||
if(typeof priorRenderOrders==='function'&&!priorRenderOrders.__sunChatV31){const wrapped=function(...args){const r=priorRenderOrders.apply(this,args);setTimeout(decorateOrderCards,0);return r};wrapped.__sunChatV31=true;window.renderOrders=wrapped;}
|
||
document.addEventListener('click',e=>{const nav=e.target.closest('header nav button');if(!nav)return;if(nav.id!=='sunChatNavV29')setOrderChatMode(false);setTimeout(()=>{ensureOrderDiscussionTab();decorateOrderCards();},20);},true);
|
||
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(safeStateChanged,60));
|
||
window.addEventListener('sun:cloud-state-applied',()=>setTimeout(safeStateChanged,60));
|
||
window.addEventListener('online',()=>{if(available())ensureWorkspaceChannel()});
|
||
window.addEventListener('beforeunload',()=>{try{workspaceChannel?.untrack?.()}catch(_){}});
|
||
let tries=0;bootRetryTimer=setInterval(()=>{tries++;safeStateChanged();if(available()||tries>=24){clearInterval(bootRetryTimer);bootRetryTimer=null;}},750);setTimeout(safeStateChanged,250);
|
||
}
|
||
|
||
window.SunChatV29={VERSION,open:openMain,openOrder:openOrderInline,openDirect,refresh:()=>loadThreads(),getThreads:()=>threads.slice(),getMembers:()=>members.slice()};
|
||
window.SunChatV31=window.SunChatV29;
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|
||
|