The banquet menu (18 dishes) and extras/supplies/delivery items (16) in the training catalog were seeded with photo:'', which the catalog tiles render as a plain red placeholder square. Sourced a real photo for each item (mostly Pexels, free license, picked and visually reviewed one by one to avoid mismatches -- several first search hits were wrong: a live chicken for "chicken caesar salad", a person in frame, branded bottles/cups) and two AI-generated (banquet-caprese, banquet-roastbeef). Converted to square 1024x1024 WebP with sharp. Wires the paths through the actual source of truth: ops/demo/trial- banquet-data.mjs and trial-extras-data.mjs now set photo to the real path, then ops/demo/build-trial-*.mjs regenerated public/demo/*.json and the matching Supabase seed-function migrations, so the client's static JSON fetch and the server-side company-provisioning function stay in sync. Bumped the demo-catalog fetch's own cache-busting query string in training-catalog.js (and its script-tag/precache version), since it's fetched by the client with a separate version from the outer script tags -- the same stale-cache class of bug fixed earlier this session. Verified: all 34 referenced files resolve (200) and decode as real 1024x1024 images; confirmed via the actual rendered catalog tiles (#tiles img[src]) that the extras category -- where the user's red-square screenshot was taken -- now serves the real photo paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
30 KiB
PL/PgSQL
132 lines
30 KiB
PL/PgSQL
-- Generated by ops/demo/build-trial-extras.mjs. Fill empty trial categories only.
|
||
begin;
|
||
create or replace function public.caterium_trial_extras_seed_v1()
|
||
returns jsonb language sql immutable security definer set search_path=public,pg_temp
|
||
as $fn$ select $demo${"version":1,"items":[{"id":"demo-extras-v1-salmon-caprese","name":"Премиум-сет «Лосось и капрезе»","category":5,"catalogSection":"Демонстрационные премиум-сеты","price":3500,"pieces":18,"weight":"710 г","photo":"demo/images/extras-salmon-caprese.webp","demo":true,"composition":["Половина стандартного бокса: Канапе с лососем и сливочным сыром","Половина стандартного бокса: Канапе «Капрезе»"],"ingredients":[["Хлеб ржаной",0.12,"кг"],["Лосось слабосолёный",0.12,"кг"],["Сыр сливочный",0.08,"кг"],["Огурцы",0.042,"кг"],["Укроп",0.005,"кг"],["Коробка для кейтеринга",1,"шт."],["Моцарелла мини",0.18,"кг"],["Томаты черри",0.154,"кг"],["Базилик",0.011,"кг"],["Масло оливковое",0.02,"кг"],["Шпажка бамбуковая",10,"шт."]],"ttk":{"number":"ДЕМО-П1","basis":"На 1 готовый премиум-сет","outputGrams":710,"pieces":18,"rows":[{"productId":"demo-v1-stock-rye","name":"Хлеб ржаной","unit":"кг","gross":0.12,"net":0.12,"unitCost":220},{"productId":"demo-v1-stock-salmon","name":"Лосось слабосолёный","unit":"кг","gross":0.12,"net":0.12,"unitCost":2400},{"productId":"demo-v1-stock-cream","name":"Сыр сливочный","unit":"кг","gross":0.08,"net":0.08,"unitCost":850},{"productId":"demo-v1-stock-cucumber","name":"Огурцы","unit":"кг","gross":0.042,"net":0.036,"unitCost":220},{"productId":"demo-v1-stock-dill","name":"Укроп","unit":"кг","gross":0.005,"net":0.004,"unitCost":700},{"productId":"demo-v1-stock-box","name":"Коробка для кейтеринга","unit":"шт.","gross":1,"net":1,"unitCost":65},{"productId":"demo-v1-stock-mozzarella","name":"Моцарелла мини","unit":"кг","gross":0.18,"net":0.18,"unitCost":950},{"productId":"demo-v1-stock-cherry","name":"Томаты черри","unit":"кг","gross":0.154,"net":0.14,"unitCost":450},{"productId":"demo-v1-stock-basil","name":"Базилик","unit":"кг","gross":0.011,"net":0.01,"unitCost":1500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.02,"net":0.02,"unitCost":1100},{"productId":"demo-v1-stock-skewer","name":"Шпажка бамбуковая","unit":"шт.","gross":10,"net":10,"unitCost":2}],"steps":["Приготовить две мини-подборки в половинном объёме по указанным нормам.","Канапе с лососем и сливочным сыром: Вырезать из ржаного хлеба 16 основ.","Канапе с лососем и сливочным сыром: Нанести сливочный сыр, добавить ломтики огурца и лосося.","Канапе с лососем и сливочным сыром: Украсить укропом и уложить в коробку.","Канапе «Капрезе»: Подготовить черри и базилик, обсушить моцареллу.","Канапе «Капрезе»: Собрать 20 шпажек, распределив ингредиенты поровну.","Канапе «Капрезе»: Добавить оливковое масло, уложить в коробку.","Уложить обе подборки в одну общую коробку."],"allergens":["Рыба","Молоко","Глютен"],"ingredientCost":758.94,"note":"Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет."}},{"id":"demo-extras-v1-meat-cheese","name":"Премиум-сет «Мясо и сыры»","category":5,"catalogSection":"Демонстрационные премиум-сеты","price":3900,"pieces":0,"weight":"750 г","photo":"demo/images/extras-meat-cheese.webp","demo":true,"composition":["Половина стандартного бокса: Мясное ассорти с корнишонами","Половина стандартного бокса: Сырный бокс с виноградом и орехами"],"ingredients":[["Салями",0.09,"кг"],["Ростбиф готовый",0.11,"кг"],["Ветчина из индейки",0.075,"кг"],["Корнишоны",0.045,"кг"],["Маслины без косточек",0.023,"кг"],["Горчица",0.015,"кг"],["Соусник",1,"шт."],["Коробка для кейтеринга",1,"шт."],["Сыр бри",0.1,"кг"],["Сыр гауда",0.09,"кг"],["Сыр с голубой плесенью",0.06,"кг"],["Виноград",0.088,"кг"],["Орех грецкий очищенный",0.025,"кг"],["Мёд",0.02,"кг"],["Крекер",0.025,"кг"]],"ttk":{"number":"ДЕМО-П2","basis":"На 1 готовый премиум-сет","outputGrams":750,"pieces":0,"rows":[{"productId":"demo-v1-stock-salami","name":"Салями","unit":"кг","gross":0.09,"net":0.09,"unitCost":1400},{"productId":"demo-v1-stock-beef","name":"Ростбиф готовый","unit":"кг","gross":0.11,"net":0.11,"unitCost":2000},{"productId":"demo-v1-stock-turkey","name":"Ветчина из индейки","unit":"кг","gross":0.075,"net":0.075,"unitCost":1000},{"productId":"demo-v1-stock-pickle","name":"Корнишоны","unit":"кг","gross":0.045,"net":0.04,"unitCost":400},{"productId":"demo-v1-stock-olive","name":"Маслины без косточек","unit":"кг","gross":0.023,"net":0.02,"unitCost":650},{"productId":"demo-v1-stock-mustard","name":"Горчица","unit":"кг","gross":0.015,"net":0.015,"unitCost":350},{"productId":"demo-v1-stock-sauce-cup","name":"Соусник","unit":"шт.","gross":1,"net":1,"unitCost":7},{"productId":"demo-v1-stock-box","name":"Коробка для кейтеринга","unit":"шт.","gross":1,"net":1,"unitCost":65},{"productId":"demo-v1-stock-brie","name":"Сыр бри","unit":"кг","gross":0.1,"net":0.1,"unitCost":1600},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.09,"net":0.09,"unitCost":900},{"productId":"demo-v1-stock-blue","name":"Сыр с голубой плесенью","unit":"кг","gross":0.06,"net":0.06,"unitCost":1700},{"productId":"demo-v1-stock-grape","name":"Виноград","unit":"кг","gross":0.088,"net":0.08,"unitCost":420},{"productId":"demo-v1-stock-walnut","name":"Орех грецкий очищенный","unit":"кг","gross":0.025,"net":0.025,"unitCost":1000},{"productId":"demo-v1-stock-honey","name":"Мёд","unit":"кг","gross":0.02,"net":0.02,"unitCost":800},{"productId":"demo-v1-stock-cracker","name":"Крекер","unit":"кг","gross":0.025,"net":0.025,"unitCost":500}],"steps":["Приготовить две мини-подборки в половинном объёме по указанным нормам.","Мясное ассорти с корнишонами: Нарезать готовые мясные продукты тонкими ломтиками.","Мясное ассорти с корнишонами: Обсушить корнишоны и маслины, переложить горчицу в соусник.","Мясное ассорти с корнишонами: Разложить ассорти секциями, добавить гарниры.","Сырный бокс с виноградом и орехами: Нарезать сыры порционными кусочками.","Сырный бокс с виноградом и орехами: Подготовить виноград, переложить мёд в соусник.","Сырный бокс с виноградом и орехами: Разложить сыры, виноград, орехи и крекер отдельными секциями.","Уложить обе подборки в одну общую коробку."],"allergens":["Горчица","Молоко","Орехи","Глютен"],"ingredientCost":964.66,"note":"Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет."}},{"id":"demo-extras-v1-mini-buffet","name":"Премиум-сет «Мини-фуршет»","category":5,"catalogSection":"Демонстрационные премиум-сеты","price":2900,"pieces":22,"weight":"820 г","photo":"demo/images/extras-mini-buffet.webp","demo":true,"composition":["Половина стандартного бокса: Брускетты с томатами и базиликом","Половина стандартного бокса: Тарталетки с грибами и сыром"],"ingredients":[["Багет",0.24,"кг"],["Томаты",0.165,"кг"],["Базилик",0.013,"кг"],["Масло оливковое",0.028,"кг"],["Коробка для кейтеринга",1,"шт."],["Тарталетки готовые",0.1,"кг"],["Шампиньоны",0.22,"кг"],["Сливки",0.05,"кг"],["Сыр гауда",0.06,"кг"],["Лук репчатый",0.044,"кг"]],"ttk":{"number":"ДЕМО-П3","basis":"На 1 готовый премиум-сет","outputGrams":820,"pieces":22,"rows":[{"productId":"demo-v1-stock-baguette","name":"Багет","unit":"кг","gross":0.24,"net":0.24,"unitCost":240},{"productId":"demo-v1-stock-tomato","name":"Томаты","unit":"кг","gross":0.165,"net":0.15,"unitCost":260},{"productId":"demo-v1-stock-basil","name":"Базилик","unit":"кг","gross":0.013,"net":0.012,"unitCost":1500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.028,"net":0.028,"unitCost":1100},{"productId":"demo-v1-stock-box","name":"Коробка для кейтеринга","unit":"шт.","gross":1,"net":1,"unitCost":65},{"productId":"demo-v1-stock-tartlet","name":"Тарталетки готовые","unit":"кг","gross":0.1,"net":0.1,"unitCost":750},{"productId":"demo-v1-stock-mushroom","name":"Шампиньоны","unit":"кг","gross":0.22,"net":0.15,"unitCost":340},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.05,"net":0.05,"unitCost":500},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.06,"net":0.06,"unitCost":900},{"productId":"demo-v1-stock-onion","name":"Лук репчатый","unit":"кг","gross":0.044,"net":0.03,"unitCost":80}],"steps":["Приготовить две мини-подборки в половинном объёме по указанным нормам.","Брускетты с томатами и базиликом: Нарезать багет на 24 одинаковых ломтика и подсушить.","Брускетты с томатами и базиликом: Подготовить томаты и базилик, смешать с маслом.","Брускетты с томатами и базиликом: Распределить начинку по ломтикам, уложить в коробку.","Тарталетки с грибами и сыром: Подготовить и обжарить лук с грибами на масле; в расчёте учтены потери массы.","Тарталетки с грибами и сыром: Добавить сливки и распределить начинку по 20 тарталеткам.","Тарталетки с грибами и сыром: Добавить сыр, запечь до расплавления и упаковать после охлаждения.","Уложить обе подборки в одну общую коробку."],"allergens":["Глютен","Молоко"],"ingredientCost":448.12,"note":"Учебный сет и примерные цены. Все количества в таблице рассчитаны на один сет."}},{"id":"demo-extras-v1-water","name":"Вода негазированная, 0,5 л","category":3,"catalogSection":"Безалкогольные напитки","price":90,"pieces":1,"weight":"","photo":"demo/images/extras-water.webp","composition":["Одна бутылка 0,5 л"],"ingredients":[["Демо: вода негазированная 0,5 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-sparkling","name":"Вода газированная, 0,5 л","category":3,"catalogSection":"Безалкогольные напитки","price":100,"pieces":1,"weight":"","photo":"demo/images/extras-sparkling.webp","composition":["Одна бутылка 0,5 л"],"ingredients":[["Демо: вода газированная 0,5 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-juice","name":"Яблочный сок, 1 л","category":3,"catalogSection":"Безалкогольные напитки","price":220,"pieces":1,"weight":"","photo":"demo/images/extras-juice.webp","composition":["Одна упаковка 1 л"],"ingredients":[["Демо: сок яблочный 1 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-mors","name":"Клюквенный морс, 1 л","category":3,"catalogSection":"Безалкогольные напитки","price":280,"pieces":1,"weight":"","photo":"demo/images/extras-mors.webp","composition":["Одна бутылка готового морса 1 л"],"ingredients":[["Демо: морс клюквенный 1 л",1,"шт."]],"demo":true},{"id":"demo-extras-v1-plate","name":"Тарелка одноразовая, 1 шт.","category":1,"catalogSection":"Одноразовая посуда","price":25,"pieces":1,"weight":"","photo":"demo/images/extras-plate.webp","composition":["Одна сервировочная тарелка"],"ingredients":[["Демо: тарелка одноразовая",1,"шт."]],"demo":true},{"id":"demo-extras-v1-fork","name":"Вилка одноразовая, 1 шт.","category":1,"catalogSection":"Одноразовая посуда","price":10,"pieces":1,"weight":"","photo":"demo/images/extras-fork.webp","composition":["Одна вилка"],"ingredients":[["Демо: вилка одноразовая",1,"шт."]],"demo":true},{"id":"demo-extras-v1-glass","name":"Стакан одноразовый, 250 мл","category":1,"catalogSection":"Одноразовая посуда","price":15,"pieces":1,"weight":"","photo":"demo/images/extras-glass.webp","composition":["Один стакан"],"ingredients":[["Демо: стакан одноразовый 250 мл",1,"шт."]],"demo":true},{"id":"demo-extras-v1-napkin","name":"Салфетка сервировочная, 1 шт.","category":1,"catalogSection":"Одноразовая посуда","price":10,"pieces":1,"weight":"","photo":"demo/images/extras-napkin.webp","composition":["Одна бумажная салфетка"],"ingredients":[["Демо: салфетка сервировочная",1,"шт."]],"demo":true},{"id":"demo-extras-v1-ice","name":"Лёд пищевой, пакет 1 кг","category":2,"catalogSection":"Для сервировки","price":180,"pieces":1,"weight":"1 кг","photo":"demo/images/extras-ice.webp","composition":["Один пакет пищевого льда, 1 кг"],"ingredients":[["Демо: лёд пищевой",1,"кг"]],"demo":true},{"id":"demo-extras-v1-tablecloth","name":"Скатерть одноразовая, 1 шт.","category":2,"catalogSection":"Для сервировки","price":250,"pieces":1,"weight":"","photo":"demo/images/extras-tablecloth.webp","composition":["Одна одноразовая скатерть 120 × 180 см"],"ingredients":[["Демо: скатерть одноразовая",1,"шт."]],"demo":true},{"id":"demo-extras-v1-serving-kit","name":"Набор для подачи: щипцы и лопатка","category":2,"catalogSection":"Для сервировки","price":190,"pieces":1,"weight":"","photo":"demo/images/extras-serving-kit.webp","composition":["Один комплект одноразовых приборов для подачи"],"ingredients":[["Демо: набор щипцы и лопатка",1,"шт."]],"demo":true},{"id":"demo-extras-v1-delivery-city","name":"Доставка по городу","category":4,"catalogSection":"Учебные тарифы доставки","price":600,"pieces":0,"weight":"","photo":"demo/images/extras-delivery-city.webp","composition":["Учебный тариф за один адрес в пределах города."],"ingredients":[],"demo":true},{"id":"demo-extras-v1-delivery-outer","name":"Доставка за город","category":4,"catalogSection":"Учебные тарифы доставки","price":1200,"pieces":0,"weight":"","photo":"demo/images/extras-delivery-outer.webp","composition":["Учебный тариф за один адрес в ближайшем пригороде."],"ingredients":[],"demo":true}],"stock":[{"id":"demo-extras-v1-stock-water","name":"Демо: вода негазированная 0,5 л","cost":35,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-sparkling","name":"Демо: вода газированная 0,5 л","cost":40,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-juice","name":"Демо: сок яблочный 1 л","cost":110,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-mors","name":"Демо: морс клюквенный 1 л","cost":140,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-plate","name":"Демо: тарелка одноразовая","cost":12,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-fork","name":"Демо: вилка одноразовая","cost":4,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-glass","name":"Демо: стакан одноразовый 250 мл","cost":6,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-napkin","name":"Демо: салфетка сервировочная","cost":3,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-ice","name":"Демо: лёд пищевой","cost":70,"unit":"кг","qty":0,"min":1,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-tablecloth","name":"Демо: скатерть одноразовая","cost":110,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-extras-v1-stock-serving-kit","name":"Демо: набор щипцы и лопатка","cost":85,"unit":"шт.","qty":0,"min":10,"supplierId":"demo-v1-supplier-pack","lastPurchaseDate":"","lastPurchasePrice":0}]}$demo$::jsonb $fn$;
|
||
revoke all on function public.caterium_trial_extras_seed_v1() from public,anon,authenticated;
|
||
|
||
create or replace function public.caterium_trial_demo_status(p_workspace uuid)
|
||
returns jsonb language plpgsql stable security definer set search_path=public,pg_temp
|
||
as $fn$
|
||
declare
|
||
v_storage jsonb; v_empty boolean:=true; v_key text; v_value jsonb;
|
||
v_allowed boolean; v_installed boolean;
|
||
begin
|
||
if auth.uid() is null or public.sun_member_role(p_workspace) is null then raise exception 'Access denied'; end if;
|
||
select coalesce(payload->'storage','{}'::jsonb) into v_storage from public.sun_app_state where workspace_id=p_workspace;
|
||
v_installed:=coalesce(v_storage#>>'{sunTrialDemoV1,v,version}','')='1';
|
||
foreach v_key in array array['sunBoxes','sunStock','sunSuppliers','sunStockMoves'] loop
|
||
v_value:=v_storage#>array[v_key,'v'];
|
||
if v_value is not null and v_value<>'[]'::jsonb then v_empty:=false; end if;
|
||
if v_storage ? v_key and (jsonb_typeof(v_storage->v_key)<>'object' or v_value is null) then v_empty:=false; end if;
|
||
end loop;
|
||
v_allowed:=public.sun_member_role(p_workspace)='admin'
|
||
and public.sun_subscription_access_mode(p_workspace)='full'
|
||
and (exists(select 1 from public.sun_workspace_subscriptions where workspace_id=p_workspace and status='trialing') or public.sun_is_platform_admin())
|
||
and public.sun_workspace_has_feature(p_workspace,'catalog_edit')
|
||
and public.sun_workspace_has_feature(p_workspace,'stock')
|
||
and public.sun_workspace_has_feature(p_workspace,'suppliers')
|
||
and not exists(select 1 from public.sun_workspace_members m join public.caterium_sidebar_brand_assignment a on a.user_id=m.user_id where m.workspace_id=p_workspace);
|
||
return jsonb_build_object('eligible',coalesce(v_allowed,false),'installed',v_installed,'empty',v_empty,'canInstall',coalesce(v_allowed,false) and v_empty and not v_installed,'canUpgrade',coalesce(v_allowed,false) and v_installed and (coalesce(v_storage#>>'{sunTrialDemoV1,v,banquetVersion}','')<>'1' or coalesce(v_storage#>>'{sunTrialDemoV1,v,extrasVersion}','')<>'1'));
|
||
end $fn$;
|
||
|
||
|
||
create or replace function public.caterium_install_trial_extras(p_workspace uuid)
|
||
returns jsonb language plpgsql security definer set search_path=public,pg_temp
|
||
as $fn$
|
||
declare
|
||
v_state public.sun_app_state%rowtype; v_status jsonb; v_storage jsonb; v_seed jsonb;
|
||
v_catalog jsonb; v_stock jsonb; v_added jsonb; v_products jsonb; v_marker jsonb;
|
||
begin
|
||
if auth.uid() is null or public.sun_member_role(p_workspace) is distinct from 'admin' then raise exception 'Administrator membership required'; end if;
|
||
perform 1 from public.sun_workspaces where id=p_workspace for update;
|
||
v_status:=public.caterium_trial_demo_status(p_workspace);
|
||
if not (v_status->>'eligible')::boolean then raise exception 'Демонстрационная база доступна в пробной или собственной тестовой компании разработчика'; end if;
|
||
if not (v_status->>'installed')::boolean then raise exception 'Сначала установите демонстрационную базу'; end if;
|
||
select * into v_state from public.sun_app_state where workspace_id=p_workspace for update;
|
||
v_storage:=v_state.payload->'storage';
|
||
if coalesce(v_storage#>>'{sunTrialDemoV1,v,extrasVersion}','')='1' then return v_status; end if;
|
||
v_catalog:=coalesce(v_storage#>'{sunBoxes,v}','[]'::jsonb);v_stock:=coalesce(v_storage#>'{sunStock,v}','[]'::jsonb);
|
||
if jsonb_typeof(v_catalog)<>'array' or jsonb_typeof(v_stock)<>'array' then raise exception 'Неверный формат каталога или склада. Данные сохранены без изменений'; end if;
|
||
v_seed:=public.caterium_trial_extras_seed_v1();
|
||
select coalesce(jsonb_agg(n order by ord),'[]'::jsonb) into v_added
|
||
from jsonb_array_elements(v_seed->'items') with ordinality as x(n,ord)
|
||
where not exists(select 1 from jsonb_array_elements(v_catalog) old where coalesce(old->>'category','0')=n->>'category' or old->>'id'=n->>'id');
|
||
select coalesce(jsonb_agg(n order by ord),'[]'::jsonb) into v_products
|
||
from jsonb_array_elements(v_seed->'stock') with ordinality as x(n,ord)
|
||
where not exists(select 1 from jsonb_array_elements(v_stock) old where old->>'id'=n->>'id' or (old->>'name'=n->>'name' and old->>'unit'=n->>'unit'))
|
||
and exists(select 1 from jsonb_array_elements(v_added) i cross join lateral jsonb_array_elements(i->'ingredients') r where r->>0=n->>'name' and r->>2=n->>'unit');
|
||
v_marker:=(v_storage#>'{sunTrialDemoV1,v}')||jsonb_build_object('extrasVersion',1,'extrasInstalledAt',now());
|
||
v_storage:=v_storage||jsonb_build_object('sunBoxes',jsonb_build_object('t','j','v',v_catalog||v_added),'sunStock',jsonb_build_object('t','j','v',v_stock||v_products),'sunTrialDemoV1',jsonb_build_object('t','j','v',v_marker));
|
||
perform public.sun_save_app_state_v17(p_workspace,jsonb_set(v_state.payload,'{storage}',v_storage),'trial-extras-v1',v_state.revision);
|
||
insert into public.sun_platform_audit_events(actor_user_id,action,target_workspace_id,details)
|
||
values(auth.uid(),'trial_demo.extras_install',p_workspace,jsonb_build_object('version',1,'items',jsonb_array_length(v_added)));
|
||
return public.caterium_trial_demo_status(p_workspace);
|
||
end $fn$;
|
||
revoke all on function public.caterium_install_trial_extras(uuid) from public,anon;
|
||
grant execute on function public.caterium_install_trial_extras(uuid) to authenticated;
|
||
|
||
create or replace function public.caterium_install_trial_banquet(p_workspace uuid)
|
||
returns jsonb language plpgsql security definer set search_path=public,pg_temp
|
||
as $fn$
|
||
declare
|
||
v_state public.sun_app_state%rowtype; v_status jsonb; v_storage jsonb;
|
||
v_seed jsonb; v_key text; v_items jsonb; v_added jsonb; v_marker jsonb;
|
||
begin
|
||
if auth.uid() is null or public.sun_member_role(p_workspace) is distinct from 'admin' then raise exception 'Administrator membership required'; end if;
|
||
perform 1 from public.sun_workspaces where id=p_workspace for update;
|
||
v_status:=public.caterium_trial_demo_status(p_workspace);
|
||
if not (v_status->>'eligible')::boolean then raise exception 'Демонстрационная база доступна в пробной или собственной тестовой компании разработчика'; end if;
|
||
if not (v_status->>'installed')::boolean then raise exception 'Сначала установите демонстрационную базу'; end if;
|
||
if coalesce((select payload#>>'{storage,sunTrialDemoV1,v,banquetVersion}' from public.sun_app_state where workspace_id=p_workspace),'')='1' then return v_status; end if;
|
||
select * into v_state from public.sun_app_state where workspace_id=p_workspace for update;
|
||
v_storage:=v_state.payload->'storage'; v_seed:=public.caterium_trial_banquet_seed_v1();
|
||
foreach v_key in array array['sunBoxes','sunStock'] loop
|
||
v_items:=coalesce(v_storage#>array[v_key,'v'],'[]'::jsonb);
|
||
if jsonb_typeof(v_items)<>'array' then raise exception 'Неверный формат каталога или склада. Данные сохранены без изменений'; end if;
|
||
-- Keep every existing row byte-for-byte, including edited demo prices and balances.
|
||
select coalesce(jsonb_agg(n),'[]'::jsonb) into v_added
|
||
from jsonb_array_elements(v_seed->case when v_key='sunBoxes' then 'banquet' else 'stock' end) n
|
||
where not exists(select 1 from jsonb_array_elements(v_items) old where old->>'id'=n->>'id');
|
||
v_storage:=v_storage||jsonb_build_object(v_key,jsonb_build_object('t','j','v',v_items||v_added));
|
||
end loop;
|
||
v_marker:=(v_storage#>'{sunTrialDemoV1,v}')||jsonb_build_object('banquetVersion',1,'banquetInstalledAt',now());
|
||
v_storage:=jsonb_set(v_storage,'{sunTrialDemoV1,v}',v_marker);
|
||
perform public.sun_save_app_state_v17(p_workspace,jsonb_set(v_state.payload,'{storage}',v_storage),'trial-banquet-v1',v_state.revision);
|
||
insert into public.sun_platform_audit_events(actor_user_id,action,target_workspace_id,details)
|
||
values(auth.uid(),'trial_demo.banquet_install',p_workspace,jsonb_build_object('version',1,'dishes',18));
|
||
return public.caterium_trial_demo_status(p_workspace);
|
||
end $fn$;
|
||
|
||
create or replace function public.caterium_install_trial_demo(p_workspace uuid)
|
||
returns jsonb language plpgsql security definer set search_path=public,pg_temp
|
||
as $fn$
|
||
declare
|
||
v_state public.sun_app_state%rowtype; v_status jsonb; v_seed jsonb; v_storage jsonb; v_moves jsonb; v_marker jsonb;
|
||
begin
|
||
if auth.uid() is null or public.sun_member_role(p_workspace) is distinct from 'admin' then raise exception 'Administrator membership required'; end if;
|
||
perform 1 from public.sun_workspaces where id=p_workspace for update;
|
||
v_status:=public.caterium_trial_demo_status(p_workspace);
|
||
if not (v_status->>'eligible')::boolean then raise exception 'Демонстрационная база доступна в пробной или собственной тестовой компании разработчика'; end if;
|
||
if (v_status->>'installed')::boolean then perform public.caterium_install_trial_banquet(p_workspace); return public.caterium_install_trial_extras(p_workspace); end if;
|
||
if not (v_status->>'empty')::boolean then raise exception 'Каталог, склад или поставщики уже заполнены. Данные сохранены без изменений'; end if;
|
||
select * into v_state from public.sun_app_state where workspace_id=p_workspace for update;
|
||
if not found then raise exception 'Рабочая база не найдена'; end if;
|
||
v_seed:=public.caterium_trial_demo_seed_v1();
|
||
select coalesce(jsonb_agg(jsonb_build_object('id','demo-v1-opening-'||(p->>'id'),'productId',p->>'id','productName',p->>'name','unit',p->>'unit',
|
||
'type','set','qty',p->'qty','setTo',p->'qty','cost',p->'cost','date',current_date::text,'createdAt',now(),
|
||
'supplierId',p->>'supplierId','note','Учебный начальный остаток')),'[]'::jsonb) into v_moves
|
||
from jsonb_array_elements(v_seed->'stock') p where (p->>'qty')::numeric>0;
|
||
v_marker:=jsonb_build_object('version',1,'batch','trial-demo-v1','installedAt',now(),'scenario',v_seed->'scenario');
|
||
v_storage:=coalesce(v_state.payload->'storage','{}'::jsonb)||jsonb_build_object(
|
||
'sunBoxes',jsonb_build_object('t','j','v',v_seed->'boxes'),
|
||
'sunStock',jsonb_build_object('t','j','v',v_seed->'stock'),
|
||
'sunSuppliers',jsonb_build_object('t','j','v',v_seed->'suppliers'),
|
||
'sunStockMoves',jsonb_build_object('t','j','v',v_moves),
|
||
'sunTrialDemoV1',jsonb_build_object('t','j','v',v_marker));
|
||
perform public.sun_save_app_state_v17(p_workspace,jsonb_set(v_state.payload,'{storage}',v_storage),'trial-demo-v1',v_state.revision);
|
||
insert into public.sun_platform_audit_events(actor_user_id,action,target_workspace_id,details)
|
||
values(auth.uid(),'trial_demo.install',p_workspace,jsonb_build_object('version',1,'boxes',10));
|
||
perform public.caterium_install_trial_banquet(p_workspace); return public.caterium_install_trial_extras(p_workspace);
|
||
end $fn$;
|
||
|
||
notify pgrst,'reload schema';
|
||
commit;
|