Files
neon-bar86.ru/site/scripts/export-crm-seed.ts
T
2026-10-11 22:24:54 +05:00

145 lines
5.3 KiB
TypeScript
Raw 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.
/**
* Экспорт меню и табаков сайта в 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}`,
);