caterium-app/supabase/migrations/20260918060000_trial_banquet.sql
pavlov346346-source 40b5c146c2 feat: add photos for the 34 training-catalog items that had none
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>
2026-09-22 17:00:45 +03:00

101 lines
42 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Generated by ops/demo/build-trial-banquet.mjs. Additive demo upgrade only.
begin;
create or replace function public.caterium_trial_banquet_seed_v1()
returns jsonb language sql immutable security definer set search_path=public,pg_temp
as $fn$ select $demo${"version":1,"banquet":[{"id":"demo-banquet-v1-caprese","name":"Капрезе с моцареллой и базиликом","category":6,"catalogSection":"Холодные закуски","price":290,"weight":"120 г","pieces":1,"photo":"demo/images/banquet-caprese.webp","demo":true,"composition":["Моцарелла мини, Томаты, Базилик, Масло оливковое"],"ingredients":[["Моцарелла мини",0.05,"кг"],["Томаты",0.07,"кг"],["Базилик",0.006,"кг"],["Масло оливковое",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б01","basis":"На 1 порцию / 1 гостя","outputGrams":120,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-mozzarella","name":"Моцарелла мини","unit":"кг","gross":0.05,"net":0.05,"unitCost":950},{"productId":"demo-v1-stock-tomato","name":"Томаты","unit":"кг","gross":0.07,"net":0.06,"unitCost":260},{"productId":"demo-v1-stock-basil","name":"Базилик","unit":"кг","gross":0.006,"net":0.005,"unitCost":1500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.005,"net":0.005,"unitCost":1100}],"steps":["Нарезать томаты и моцареллу.","Выложить слоями, добавить базилик и масло."],"allergens":["Молоко"],"ingredientCost":80.2,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-roastbeef","name":"Ростбиф с корнишонами и горчичным соусом","category":6,"catalogSection":"Холодные закуски","price":390,"weight":"100 г","pieces":1,"photo":"demo/images/banquet-roastbeef.webp","demo":true,"composition":["Ростбиф готовый, Корнишоны, Горчица, Сливки"],"ingredients":[["Ростбиф готовый",0.065,"кг"],["Корнишоны",0.022,"кг"],["Горчица",0.005,"кг"],["Сливки",0.01,"кг"]],"ttk":{"number":"ДЕМО-Б02","basis":"На 1 порцию / 1 гостя","outputGrams":100,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-beef","name":"Ростбиф готовый","unit":"кг","gross":0.065,"net":0.065,"unitCost":2000},{"productId":"demo-v1-stock-pickle","name":"Корнишоны","unit":"кг","gross":0.022,"net":0.02,"unitCost":400},{"productId":"demo-v1-stock-mustard","name":"Горчица","unit":"кг","gross":0.005,"net":0.005,"unitCost":350},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.01,"net":0.01,"unitCost":500}],"steps":["Нарезать готовый ростбиф тонкими ломтиками.","Смешать сливки с горчицей, подать с корнишонами."],"allergens":["Молоко","Горчица"],"ingredientCost":145.55,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-salmon-roll","name":"Рулетики из лосося со сливочным сыром","category":6,"catalogSection":"Холодные закуски","price":350,"weight":"90 г","pieces":1,"photo":"demo/images/banquet-salmon-roll.webp","demo":true,"composition":["Лосось слабосолёный, Сыр сливочный, Огурцы, Укроп"],"ingredients":[["Лосось слабосолёный",0.045,"кг"],["Сыр сливочный",0.025,"кг"],["Огурцы",0.022,"кг"],["Укроп",0.003,"кг"]],"ttk":{"number":"ДЕМО-Б03","basis":"На 1 порцию / 1 гостя","outputGrams":90,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-salmon","name":"Лосось слабосолёный","unit":"кг","gross":0.045,"net":0.045,"unitCost":2400},{"productId":"demo-v1-stock-cream","name":"Сыр сливочный","unit":"кг","gross":0.025,"net":0.025,"unitCost":850},{"productId":"demo-v1-stock-cucumber","name":"Огурцы","unit":"кг","gross":0.022,"net":0.018,"unitCost":220},{"productId":"demo-v1-stock-dill","name":"Укроп","unit":"кг","gross":0.003,"net":0.002,"unitCost":700}],"steps":["Нарезать лосось и огурец тонкими ломтиками.","Распределить сыр, свернуть рулетики, украсить укропом."],"allergens":["Рыба","Молоко"],"ingredientCost":136.19,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-hummus","name":"Овощные палочки с хумусом","category":6,"catalogSection":"Холодные закуски","price":230,"weight":"160 г","pieces":1,"photo":"demo/images/banquet-hummus.webp","demo":true,"composition":["Морковь, Огурцы, Перец сладкий, Хумус готовый"],"ingredients":[["Морковь",0.048,"кг"],["Огурцы",0.044,"кг"],["Перец сладкий",0.04,"кг"],["Хумус готовый",0.05,"кг"]],"ttk":{"number":"ДЕМО-Б04","basis":"На 1 порцию / 1 гостя","outputGrams":160,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-carrot","name":"Морковь","unit":"кг","gross":0.048,"net":0.04,"unitCost":90},{"productId":"demo-v1-stock-cucumber","name":"Огурцы","unit":"кг","gross":0.044,"net":0.04,"unitCost":220},{"productId":"demo-v1-stock-pepper","name":"Перец сладкий","unit":"кг","gross":0.04,"net":0.03,"unitCost":380},{"productId":"demo-v1-stock-hummus","name":"Хумус готовый","unit":"кг","gross":0.05,"net":0.05,"unitCost":600}],"steps":["Очистить овощи и нарезать палочками.","Подать с порцией готового хумуса."],"allergens":["Кунжут в составе хумуса"],"ingredientCost":59.2,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-caesar","name":"Салат с курицей, сухариками и сыром","category":6,"catalogSection":"Салаты","price":340,"weight":"180 г","pieces":1,"photo":"demo/images/banquet-caesar.webp","demo":true,"composition":["Филе курицы готовое, Салат листовой, Томаты черри, Хлеб тостовый, Сыр гауда, Сливки, Горчица"],"ingredients":[["Филе курицы готовое",0.06,"кг"],["Салат листовой",0.042,"кг"],["Томаты черри",0.035,"кг"],["Хлеб тостовый",0.023,"кг"],["Сыр гауда",0.015,"кг"],["Сливки",0.015,"кг"],["Горчица",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б05","basis":"На 1 порцию / 1 гостя","outputGrams":180,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-chicken","name":"Филе курицы готовое","unit":"кг","gross":0.06,"net":0.06,"unitCost":850},{"productId":"demo-v1-stock-lettuce","name":"Салат листовой","unit":"кг","gross":0.042,"net":0.035,"unitCost":700},{"productId":"demo-v1-stock-cherry","name":"Томаты черри","unit":"кг","gross":0.035,"net":0.03,"unitCost":450},{"productId":"demo-v1-stock-toast","name":"Хлеб тостовый","unit":"кг","gross":0.023,"net":0.02,"unitCost":240},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.015,"net":0.015,"unitCost":900},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.015,"net":0.015,"unitCost":500},{"productId":"demo-v1-stock-mustard","name":"Горчица","unit":"кг","gross":0.005,"net":0.005,"unitCost":350}],"steps":["Подсушить кубики хлеба, подготовить салат и томаты.","Нарезать готовую курицу, соединить с овощами и сыром.","Заправить сливками с горчицей, сверху добавить сухарики."],"allergens":["Глютен","Молоко","Горчица"],"ingredientCost":124.42,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-olivier","name":"Оливье с индейкой","category":6,"catalogSection":"Салаты","price":280,"weight":"180 г","pieces":1,"photo":"demo/images/banquet-olivier.webp","demo":true,"composition":["Картофель, Морковь, Ветчина из индейки, Яйцо куриное без скорлупы, Горошек зелёный консервированный, Корнишоны, Сливки, Горчица"],"ingredients":[["Картофель",0.065,"кг"],["Морковь",0.026,"кг"],["Ветчина из индейки",0.035,"кг"],["Яйцо куриное без скорлупы",0.022,"кг"],["Горошек зелёный консервированный",0.02,"кг"],["Корнишоны",0.022,"кг"],["Сливки",0.012,"кг"],["Горчица",0.003,"кг"]],"ttk":{"number":"ДЕМО-Б06","basis":"На 1 порцию / 1 гостя","outputGrams":180,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-banquet-v1-stock-potato","name":"Картофель","unit":"кг","gross":0.065,"net":0.05,"unitCost":80},{"productId":"demo-v1-stock-carrot","name":"Морковь","unit":"кг","gross":0.026,"net":0.02,"unitCost":90},{"productId":"demo-v1-stock-turkey","name":"Ветчина из индейки","unit":"кг","gross":0.035,"net":0.035,"unitCost":1000},{"productId":"demo-banquet-v1-stock-egg","name":"Яйцо куриное без скорлупы","unit":"кг","gross":0.022,"net":0.02,"unitCost":260},{"productId":"demo-banquet-v1-stock-peas","name":"Горошек зелёный консервированный","unit":"кг","gross":0.02,"net":0.02,"unitCost":300},{"productId":"demo-v1-stock-pickle","name":"Корнишоны","unit":"кг","gross":0.022,"net":0.02,"unitCost":400},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.012,"net":0.012,"unitCost":500},{"productId":"demo-v1-stock-mustard","name":"Горчица","unit":"кг","gross":0.003,"net":0.003,"unitCost":350}],"steps":["Отварить картофель, морковь и яйцо; остудить.","Нарезать овощи, яйцо, ветчину и корнишоны кубиком.","Добавить горошек и заправить сливками с горчицей."],"allergens":["Яйцо","Молоко","Горчица"],"ingredientCost":70.11,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-greek","name":"Овощной салат с моцареллой и маслинами","category":6,"catalogSection":"Салаты","price":290,"weight":"180 г","pieces":1,"photo":"demo/images/banquet-greek.webp","demo":true,"composition":["Томаты, Огурцы, Перец сладкий, Моцарелла мини, Маслины без косточек, Масло оливковое"],"ingredients":[["Томаты",0.055,"кг"],["Огурцы",0.044,"кг"],["Перец сладкий",0.032,"кг"],["Моцарелла мини",0.04,"кг"],["Маслины без косточек",0.017,"кг"],["Масло оливковое",0.01,"кг"]],"ttk":{"number":"ДЕМО-Б07","basis":"На 1 порцию / 1 гостя","outputGrams":180,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-tomato","name":"Томаты","unit":"кг","gross":0.055,"net":0.05,"unitCost":260},{"productId":"demo-v1-stock-cucumber","name":"Огурцы","unit":"кг","gross":0.044,"net":0.04,"unitCost":220},{"productId":"demo-v1-stock-pepper","name":"Перец сладкий","unit":"кг","gross":0.032,"net":0.025,"unitCost":380},{"productId":"demo-v1-stock-mozzarella","name":"Моцарелла мини","unit":"кг","gross":0.04,"net":0.04,"unitCost":950},{"productId":"demo-v1-stock-olive","name":"Маслины без косточек","unit":"кг","gross":0.017,"net":0.015,"unitCost":650},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.01,"net":0.01,"unitCost":1100}],"steps":["Подготовить и крупно нарезать овощи.","Добавить моцареллу и маслины, заправить маслом."],"allergens":["Молоко"],"ingredientCost":96.19,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-julienne","name":"Жюльен с курицей и грибами","category":6,"catalogSection":"Горячие закуски","price":310,"weight":"130 г","pieces":1,"photo":"demo/images/banquet-julienne.webp","demo":true,"composition":["Филе курицы готовое, Шампиньоны, Лук репчатый, Сливки, Сыр гауда, Масло оливковое"],"ingredients":[["Филе курицы готовое",0.04,"кг"],["Шампиньоны",0.06,"кг"],["Лук репчатый",0.015,"кг"],["Сливки",0.025,"кг"],["Сыр гауда",0.015,"кг"],["Масло оливковое",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б08","basis":"На 1 порцию / 1 гостя","outputGrams":130,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-chicken","name":"Филе курицы готовое","unit":"кг","gross":0.04,"net":0.04,"unitCost":850},{"productId":"demo-v1-stock-mushroom","name":"Шампиньоны","unit":"кг","gross":0.06,"net":0.04,"unitCost":340},{"productId":"demo-v1-stock-onion","name":"Лук репчатый","unit":"кг","gross":0.015,"net":0.01,"unitCost":80},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.025,"net":0.02,"unitCost":500},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.015,"net":0.015,"unitCost":900},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.005,"net":0.005,"unitCost":1100}],"steps":["Обжарить нарезанные грибы и лук на масле.","Добавить готовую курицу и сливки, распределить по формам.","Посыпать сыром и запечь до расплавления."],"allergens":["Молоко"],"ingredientCost":87.1,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-stuffed-mushrooms","name":"Шампиньоны, запечённые с сыром","category":6,"catalogSection":"Горячие закуски","price":270,"weight":"120 г","pieces":1,"photo":"demo/images/banquet-stuffed-mushrooms.webp","demo":true,"composition":["Шампиньоны, Сыр сливочный, Сыр гауда, Укроп, Масло оливковое"],"ingredients":[["Шампиньоны",0.125,"кг"],["Сыр сливочный",0.02,"кг"],["Сыр гауда",0.01,"кг"],["Укроп",0.003,"кг"],["Масло оливковое",0.003,"кг"]],"ttk":{"number":"ДЕМО-Б09","basis":"На 1 порцию / 1 гостя","outputGrams":120,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-mushroom","name":"Шампиньоны","unit":"кг","gross":0.125,"net":0.085,"unitCost":340},{"productId":"demo-v1-stock-cream","name":"Сыр сливочный","unit":"кг","gross":0.02,"net":0.02,"unitCost":850},{"productId":"demo-v1-stock-gouda","name":"Сыр гауда","unit":"кг","gross":0.01,"net":0.01,"unitCost":900},{"productId":"demo-v1-stock-dill","name":"Укроп","unit":"кг","gross":0.003,"net":0.002,"unitCost":700},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.003,"net":0.003,"unitCost":1100}],"steps":["Подготовить шляпки грибов, ножки мелко нарезать.","Смешать ножки со сливочным сыром и укропом, наполнить шляпки.","Добавить гауду и запечь."],"allergens":["Молоко"],"ingredientCost":73.9,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-chicken","name":"Куриное филе с овощами и сливочным соусом","category":6,"catalogSection":"Горячие блюда","price":490,"weight":"220 г","pieces":1,"photo":"demo/images/banquet-chicken.webp","demo":true,"composition":["Филе куриное сырое, Перец сладкий, Морковь, Сливки, Масло оливковое"],"ingredients":[["Филе куриное сырое",0.18,"кг"],["Перец сладкий",0.045,"кг"],["Морковь",0.032,"кг"],["Сливки",0.03,"кг"],["Масло оливковое",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б10","basis":"На 1 порцию / 1 гостя","outputGrams":220,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-banquet-v1-stock-chicken-raw","name":"Филе куриное сырое","unit":"кг","gross":0.18,"net":0.135,"unitCost":480},{"productId":"demo-v1-stock-pepper","name":"Перец сладкий","unit":"кг","gross":0.045,"net":0.03,"unitCost":380},{"productId":"demo-v1-stock-carrot","name":"Морковь","unit":"кг","gross":0.032,"net":0.025,"unitCost":90},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.03,"net":0.025,"unitCost":500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.005,"net":0.005,"unitCost":1100}],"steps":["Обжарить куриное филе и довести до готовности.","Отдельно приготовить перец и морковь.","Добавить сливочный соус, подать с овощами; учесть потери при приготовлении."],"allergens":["Молоко"],"ingredientCost":126.88,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-cod","name":"Треска с лимоном и овощами","category":6,"catalogSection":"Горячие блюда","price":590,"weight":"200 г","pieces":1,"photo":"demo/images/banquet-cod.webp","demo":true,"composition":["Филе трески, Томаты, Перец сладкий, Лимон, Масло оливковое"],"ingredients":[["Филе трески",0.17,"кг"],["Томаты",0.045,"кг"],["Перец сладкий",0.03,"кг"],["Лимон",0.015,"кг"],["Масло оливковое",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б11","basis":"На 1 порцию / 1 гостя","outputGrams":200,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-banquet-v1-stock-whitefish","name":"Филе трески","unit":"кг","gross":0.17,"net":0.13,"unitCost":950},{"productId":"demo-v1-stock-tomato","name":"Томаты","unit":"кг","gross":0.045,"net":0.035,"unitCost":260},{"productId":"demo-v1-stock-pepper","name":"Перец сладкий","unit":"кг","gross":0.03,"net":0.02,"unitCost":380},{"productId":"demo-banquet-v1-stock-lemon","name":"Лимон","unit":"кг","gross":0.015,"net":0.01,"unitCost":300},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.005,"net":0.005,"unitCost":1100}],"steps":["Подготовить филе и нарезать овощи.","Запечь рыбу с овощами, лимоном и маслом до готовности."],"allergens":["Рыба"],"ingredientCost":194.6,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-beef-hot","name":"Ростбиф с грибами в сливочном соусе","category":6,"catalogSection":"Горячие блюда","price":690,"weight":"220 г","pieces":1,"photo":"demo/images/banquet-beef-hot.webp","demo":true,"composition":["Ростбиф готовый, Шампиньоны, Лук репчатый, Сливки, Масло оливковое"],"ingredients":[["Ростбиф готовый",0.125,"кг"],["Шампиньоны",0.08,"кг"],["Лук репчатый",0.024,"кг"],["Сливки",0.03,"кг"],["Масло оливковое",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б12","basis":"На 1 порцию / 1 гостя","outputGrams":220,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-beef","name":"Ростбиф готовый","unit":"кг","gross":0.125,"net":0.12,"unitCost":2000},{"productId":"demo-v1-stock-mushroom","name":"Шампиньоны","unit":"кг","gross":0.08,"net":0.055,"unitCost":340},{"productId":"demo-v1-stock-onion","name":"Лук репчатый","unit":"кг","gross":0.024,"net":0.015,"unitCost":80},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.03,"net":0.025,"unitCost":500},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.005,"net":0.005,"unitCost":1100}],"steps":["Обжарить грибы и лук, добавить сливки.","Прогреть готовый ростбиф и подать с грибным соусом."],"allergens":["Молоко"],"ingredientCost":299.62,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-mash","name":"Картофельное пюре","category":6,"catalogSection":"Гарниры","price":160,"weight":"150 г","pieces":1,"photo":"demo/images/banquet-mash.webp","demo":true,"composition":["Картофель, Молоко, Масло сливочное"],"ingredients":[["Картофель",0.17,"кг"],["Молоко",0.025,"кг"],["Масло сливочное",0.01,"кг"]],"ttk":{"number":"ДЕМО-Б13","basis":"На 1 порцию / 1 гостя","outputGrams":150,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-banquet-v1-stock-potato","name":"Картофель","unit":"кг","gross":0.17,"net":0.12,"unitCost":80},{"productId":"demo-banquet-v1-stock-milk","name":"Молоко","unit":"кг","gross":0.025,"net":0.02,"unitCost":110},{"productId":"demo-banquet-v1-stock-butter","name":"Масло сливочное","unit":"кг","gross":0.01,"net":0.01,"unitCost":1100}],"steps":["Очистить и отварить картофель, слить воду.","Размять с тёплым молоком и сливочным маслом."],"allergens":["Молоко"],"ingredientCost":27.35,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-rice","name":"Рис с овощами","category":6,"catalogSection":"Гарниры","price":170,"weight":"150 г","pieces":1,"photo":"demo/images/banquet-rice.webp","demo":true,"composition":["Рис сухой, Морковь, Перец сладкий, Горошек зелёный консервированный, Масло оливковое"],"ingredients":[["Рис сухой",0.04,"кг"],["Морковь",0.025,"кг"],["Перец сладкий",0.025,"кг"],["Горошек зелёный консервированный",0.015,"кг"],["Масло оливковое",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б14","basis":"На 1 порцию / 1 гостя","outputGrams":150,"waterGrams":50,"pieces":1,"rows":[{"productId":"demo-banquet-v1-stock-rice","name":"Рис сухой","unit":"кг","gross":0.04,"net":0.04,"unitCost":180},{"productId":"demo-v1-stock-carrot","name":"Морковь","unit":"кг","gross":0.025,"net":0.02,"unitCost":90},{"productId":"demo-v1-stock-pepper","name":"Перец сладкий","unit":"кг","gross":0.025,"net":0.02,"unitCost":380},{"productId":"demo-banquet-v1-stock-peas","name":"Горошек зелёный консервированный","unit":"кг","gross":0.015,"net":0.015,"unitCost":300},{"productId":"demo-v1-stock-oil","name":"Масло оливковое","unit":"кг","gross":0.005,"net":0.005,"unitCost":1100}],"steps":["Отварить рис; учебный расчёт учитывает 50 г поглощённой воды на порцию.","Приготовить овощи, добавить горошек и смешать с рисом."],"allergens":[],"ingredientCost":28.95,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-berry-cream","name":"Ягодный десерт со сливочным кремом","category":6,"catalogSection":"Десерты","price":240,"weight":"110 г","pieces":1,"photo":"demo/images/banquet-berry-cream.webp","demo":true,"composition":["Сыр сливочный, Сливки, Крошка бисквитная, Клубника, Голубика, Пудра сахарная"],"ingredients":[["Сыр сливочный",0.035,"кг"],["Сливки",0.02,"кг"],["Крошка бисквитная",0.015,"кг"],["Клубника",0.028,"кг"],["Голубика",0.011,"кг"],["Пудра сахарная",0.005,"кг"]],"ttk":{"number":"ДЕМО-Б15","basis":"На 1 порцию / 1 гостя","outputGrams":110,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-cream","name":"Сыр сливочный","unit":"кг","gross":0.035,"net":0.035,"unitCost":850},{"productId":"demo-v1-stock-cooking-cream","name":"Сливки","unit":"кг","gross":0.02,"net":0.02,"unitCost":500},{"productId":"demo-v1-stock-biscuit","name":"Крошка бисквитная","unit":"кг","gross":0.015,"net":0.015,"unitCost":500},{"productId":"demo-v1-stock-strawberry","name":"Клубника","unit":"кг","gross":0.028,"net":0.025,"unitCost":750},{"productId":"demo-v1-stock-blueberry","name":"Голубика","unit":"кг","gross":0.011,"net":0.01,"unitCost":1300},{"productId":"demo-v1-stock-sugar","name":"Пудра сахарная","unit":"кг","gross":0.005,"net":0.005,"unitCost":160}],"steps":["Смешать сыр, сливки и сахарную пудру.","Выложить в креманку бисквит, крем и подготовленные ягоды."],"allergens":["Молоко","Глютен","Яйцо в составе бисквита"],"ingredientCost":83.35,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-cheese-honey","name":"Бри с виноградом, мёдом и орехами","category":6,"catalogSection":"Десерты","price":310,"weight":"100 г","pieces":1,"photo":"demo/images/banquet-cheese-honey.webp","demo":true,"composition":["Сыр бри, Виноград, Орех грецкий очищенный, Мёд"],"ingredients":[["Сыр бри",0.045,"кг"],["Виноград",0.039,"кг"],["Орех грецкий очищенный",0.01,"кг"],["Мёд",0.01,"кг"]],"ttk":{"number":"ДЕМО-Б16","basis":"На 1 порцию / 1 гостя","outputGrams":100,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-brie","name":"Сыр бри","unit":"кг","gross":0.045,"net":0.045,"unitCost":1600},{"productId":"demo-v1-stock-grape","name":"Виноград","unit":"кг","gross":0.039,"net":0.035,"unitCost":420},{"productId":"demo-v1-stock-walnut","name":"Орех грецкий очищенный","unit":"кг","gross":0.01,"net":0.01,"unitCost":1000},{"productId":"demo-v1-stock-honey","name":"Мёд","unit":"кг","gross":0.01,"net":0.01,"unitCost":800}],"steps":["Нарезать бри и подготовить виноград.","Подать с грецким орехом и мёдом."],"allergens":["Молоко","Орехи"],"ingredientCost":106.38,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-fruit","name":"Фруктово-ягодное ассорти","category":6,"catalogSection":"Фрукты и ягоды","price":290,"weight":"180 г","pieces":1,"photo":"demo/images/banquet-fruit.webp","demo":true,"composition":["Виноград, Клубника, Голубика"],"ingredients":[["Виноград",0.088,"кг"],["Клубника",0.077,"кг"],["Голубика",0.032,"кг"]],"ttk":{"number":"ДЕМО-Б17","basis":"На 1 порцию / 1 гостя","outputGrams":180,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-grape","name":"Виноград","unit":"кг","gross":0.088,"net":0.08,"unitCost":420},{"productId":"demo-v1-stock-strawberry","name":"Клубника","unit":"кг","gross":0.077,"net":0.07,"unitCost":750},{"productId":"demo-v1-stock-blueberry","name":"Голубика","unit":"кг","gross":0.032,"net":0.03,"unitCost":1300}],"steps":["Подготовить ягоды и виноград, удалить плодоножки.","Выложить порционное ассорти."],"allergens":[],"ingredientCost":136.31,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}},{"id":"demo-banquet-v1-bread","name":"Хлебная корзинка с зелёным маслом","category":6,"catalogSection":"Хлеб и масло","price":120,"weight":"60 г","pieces":1,"photo":"demo/images/banquet-bread.webp","demo":true,"composition":["Багет, Хлеб ржаной, Масло сливочное, Укроп"],"ingredients":[["Багет",0.03,"кг"],["Хлеб ржаной",0.02,"кг"],["Масло сливочное",0.008,"кг"],["Укроп",0.003,"кг"]],"ttk":{"number":"ДЕМО-Б18","basis":"На 1 порцию / 1 гостя","outputGrams":60,"waterGrams":0,"pieces":1,"rows":[{"productId":"demo-v1-stock-baguette","name":"Багет","unit":"кг","gross":0.03,"net":0.03,"unitCost":240},{"productId":"demo-v1-stock-rye","name":"Хлеб ржаной","unit":"кг","gross":0.02,"net":0.02,"unitCost":220},{"productId":"demo-banquet-v1-stock-butter","name":"Масло сливочное","unit":"кг","gross":0.008,"net":0.008,"unitCost":1100},{"productId":"demo-v1-stock-dill","name":"Укроп","unit":"кг","gross":0.003,"net":0.002,"unitCost":700}],"steps":["Нарезать хлеб, при желании подсушить.","Смешать масло с укропом, подать отдельно."],"allergens":["Глютен","Молоко"],"ingredientCost":22.5,"note":"Учебная рецептура и примерные цены. Выход указан после приготовления. Перед использованием в работе уточните нормы, стоимость и аллергены."}}],"stock":[{"id":"demo-banquet-v1-stock-potato","name":"Картофель","cost":80,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-fresh","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-butter","name":"Масло сливочное","cost":1100,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-fresh","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-milk","name":"Молоко","cost":110,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-fresh","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-egg","name":"Яйцо куриное без скорлупы","cost":260,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-fresh","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-peas","name":"Горошек зелёный консервированный","cost":300,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-chicken-raw","name":"Филе куриное сырое","cost":480,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-protein","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-whitefish","name":"Филе трески","cost":950,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-protein","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-lemon","name":"Лимон","cost":300,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-fresh","lastPurchaseDate":"","lastPurchasePrice":0},{"id":"demo-banquet-v1-stock-rice","name":"Рис сухой","cost":180,"unit":"кг","qty":0,"min":0.1,"supplierId":"demo-v1-supplier-grocery","lastPurchaseDate":"","lastPurchasePrice":0}]}$demo$::jsonb $fn$;
revoke all on function public.caterium_trial_banquet_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');
end $fn$;
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 not (v_status->>'canUpgrade')::boolean 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$;
revoke all on function public.caterium_install_trial_banquet(uuid) from public,anon;
grant execute on function public.caterium_install_trial_banquet(uuid) to authenticated;
-- Both onboarding's existing trigger and the install button use this entry point.
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 return public.caterium_install_trial_banquet(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));
return public.caterium_install_trial_banquet(p_workspace);
end $fn$;
notify pgrst,'reload schema';
commit;