/** * Экспорт меню и табаков сайта в seed-JSON для CRM (crm/seed/*.json). * * Запуск из папки site: npm run export:crm * CRM при старте сеет этими данными таблицы menu_items / tobacco_flavors, * библиотеку игр (games.json, обложки -> seed/covers/) и отдаёт * авторские миксы сайта во вкладку «Миксы» (mixes.json). * После изменения меню на сайте — перегенерируй файлы и пересобери CRM. */ import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { barMenuItems } from "../src/data/barMenu"; import { beerList } from "../src/data/beerMenu"; import { authorTeas } from "../src/data/teaMenu"; import { boardGameDifficultyLabels, boardGames, hookahMixes, ps5Games, tobaccoFlavors, } from "../src/data/catalog"; // Атомарная запись (F-6): обрыв экспорта не оставит битый seed — рестарт CRM // с повреждённым файлом роняется fail-closed'ом и не вычистит каталог. const outDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../crm/seed"); const writeSeedJson = (name: string, data: unknown) => { const target = resolve(outDir, name); const tmp = target + ".tmp"; writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8"); renameSync(tmp, target); }; const siteRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const gamesImagesDir = resolve(siteRoot, "public/images/games"); const menu = [ ...authorTeas.map((tea) => ({ id: tea.id, name: tea.name, category: tea.category, price: tea.price, portion: tea.volume, })), ...beerList.map((beer) => ({ id: beer.id, name: beer.name, category: beer.category, price: beer.price, portion: beer.volume, })), ...barMenuItems.map((item) => ({ id: item.id, name: item.name, category: item.category, price: item.price, portion: item.portion ?? null, })), ]; // Стоп-лист в CRM живёт на уровне отдельных вкусов, а не брендов: // экспортируем все вкусы каталога (включая «Добавку» — её вкусы тоже // участвуют в конструкторе миксов). const tobacco = tobaccoFlavors .map((f) => ({ id: f.id, brand: f.brand, flavor: f.name, note: f.note ?? null, strength: f.strength, cooling: f.cooling, })) .sort( (a, b) => a.brand.localeCompare(b.brand, "ru") || a.flavor.localeCompare(b.flavor, "ru"), ); mkdirSync(outDir, { recursive: true }); writeSeedJson("menu.json", menu); writeSeedJson("tobacco.json", tobacco); // ------------------------------------------------------------------ игры // PS5: note = жанр и игроки; обложка копируется в crm/seed/covers/ // (.jfif переименовывается в .jpg — у CRM mimetype для .jfif не настроен). // Настолки: note = категория, игроки, время, сложность. const coversDir = resolve(outDir, "covers"); mkdirSync(coversDir, { recursive: true }); const coverFile = (image: string): string | null => { if (!image) return null; const srcName = image.split("/").pop() ?? ""; const src = resolve(gamesImagesDir, srcName); if (!existsSync(src)) return null; const dstName = srcName.toLowerCase().endsWith(".jfif") ? srcName.slice(0, -5) + ".jpg" : srcName; cpSync(src, resolve(coversDir, dstName)); return dstName; }; const games = [ ...ps5Games.map((g) => ({ seed_id: g.id, kind: "ps5", title: g.name, note: `${g.genre} · до ${g.maxPlayers} игрок${g.maxPlayers === 1 ? "а" : "ов"}`, cover: coverFile(g.image), })), ...boardGames.map((g) => ({ seed_id: `board-${g.id}`, kind: "board", title: g.name, note: `${g.category} · ${g.minPlayers}–${g.maxPlayers} игроков · ${g.duration} · ` + `${boardGameDifficultyLabels[g.difficulty].toLowerCase()}${g.hit ? " · хит" : ""}`, cover: null, difficulty: g.difficulty, icon: g.icon, accent: g.accent, })), ]; // ------------------------------------------------------------------ миксы сайта // Авторские миксы из каталога сайта: CRM показывает их во вкладке «Миксы» // только на чтение — на сайте они рендерятся сами, дубли в mix_recipes не нужны. const siteMixes = hookahMixes.map((m) => ({ id: m.id, name: m.name, note: m.desc, strength: m.strength, cooling: m.cooling, flavor_tags: m.flavorTags, price: m.price, featured: m.featured ?? false, })); writeSeedJson("games.json", games); writeSeedJson("mixes.json", siteMixes); const coversCopied = readdirSync(coversDir).length; console.log( `menu.json: ${menu.length} позиций, tobacco.json: ${tobacco.length} вкусов,` + ` games.json: ${games.length} игр (обложек скопировано: ${coversCopied}),` + ` mixes.json: ${siteMixes.length} миксов -> ${outDir}`, );