feat: category-tag configurator + tag rename cleanup
- Rename supplier tags MATERIAUX→METAL_PROKAT, METAL→METIZ_KREPEZH - Add CategoryTagMapping Prisma model for DB-stored mappings - consultation.service.ts loads mapping from DB (fallback to defaults) - Backend GET/PUT /admin/category-tag-config endpoints - Frontend /settings page with Table: categories × tags × suppliers - Sidebar menu entry Настройки - Seed 13 category-tag default mappings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9333c2db28
commit
3fe4ef1c75
@ -198,6 +198,7 @@ prisma/
|
||||
- [x] bugfixes-22a (SPEC 22A: 4 bugs achats — Bug1 consultation CONFIRMED only (exclut DRAFT), Bug2 supplier pre-select verifie OK via buildMatrix, Bug3+4 suppression colonne Приоритет, ajustement largeurs colonnes Описание 220/Категория 140/Заказ 100, scroll 1500) - implemente 21/03/2026
|
||||
- [x] fix-supplier-preselect (SPEC 22C: fix pre-selection fournisseurs dans drawer consultation — backend contactEmail empty string→null, frontend buildMatrix suppression filtre supplierEmail pour pre-remplissage matrix) - implemente 21/03/2026
|
||||
- [x] frontend-visual-validation (SPEC 22B: outil validation visuelle autonome — validate-frontend.js Puppeteer+Claude Vision, skill SKILL.md, section CLAUDE.md, criteres fichier texte, JSON output pass/issues/suggestions, max 3 iterations) - implemente 21/03/2026
|
||||
- [x] category-tag-configurator (SPEC 22E: nettoyage tags MATERIAUX→METAL_PROKAT+METAL→METIZ_KREPEZH, CategoryTagMapping model DB, CATEGORY_TO_TAG dynamique depuis DB, GET/PUT /admin/category-tag-config, frontend /settings page Table Select multi-tags+fournisseurs auto, seed mappings 13 categories, RBAC ADMIN write) - implemente 21/03/2026
|
||||
- [ ] invoices (facturation)
|
||||
- [ ] reports (rapports financiers)
|
||||
|
||||
@ -373,6 +374,7 @@ Skill complete: /root/.claude/skills/frontend-visual-validation/SKILL.md
|
||||
- purchaseRoutes GET / 404: la route globale GET /api/v1/purchase-requirements manquait (seuls /summary, /consolidated, /:id existaient). Ajout listAll method+route. listByOrder reste sous /orders/:orderId/purchase-requirements.
|
||||
- pdf-parse v2 API: PDFParse est une classe (pas une fonction). Utiliser `new PDFParse({ data: buffer })` puis `parser.getText()` puis `parser.destroy()`. Import: `import { PDFParse } from 'pdf-parse'` (named export, pas default).
|
||||
- Anthropic SDK import ESM: `import Anthropic from '@anthropic-ai/sdk'` (default import fonctionne). Types: `Anthropic.ContentBlockParam`, `Anthropic.TextBlock`. Env var ANTHROPIC_API_KEY.
|
||||
- CategoryTagMapping in DB: le mapping MaterialCategory→SupplierTag est stocke dans la table category_tag_mappings (pas en dur dans le code). consultation.service.ts charge le mapping depuis la DB a chaque appel. Seed defaults dans seed.ts. API admin GET/PUT /admin/category-tag-config.
|
||||
|
||||
## Decisions architecturales
|
||||
- 09/03/2026: Architecture v2 adoptee. n8n = orchestrateur leger.
|
||||
@ -489,6 +491,8 @@ Skill complete: /root/.claude/skills/frontend-visual-validation/SKILL.md
|
||||
|
||||
- 21/03/2026: Frontend Visual Validation (SPEC 22B) — Outil autonome /opt/erp/validate-frontend.js: Puppeteer screenshot 1280x900 + Claude Sonnet vision API via https.request (pas SDK). Auth JWT via API login + localStorage injection (meme pattern que screenshot-api.js). Criteres dans fichier texte externe. Output JSON {pass, issues, suggestions, screenshot}. Exit code 0=pass, 1=fail, 2=error. Pas de dotenv dependency (lecture .env manuelle). Skill /root/.claude/skills/frontend-visual-validation/SKILL.md. Section CLAUDE.md obligatoire post-modification frontend.
|
||||
|
||||
## Derniere session (21/03/2026, fix-supplier-preselect)
|
||||
- SPEC 22C: fix supplier pre-selection in consultation drawer — empty string email caused buildMatrix to skip all suppliers
|
||||
- 21/03/2026: Category-Tag Configurator (SPEC 22E) — CategoryTagMapping model en DB remplace le mapping CATEGORY_TO_TAG hardcode dans consultation.service.ts. Tags renommes MATERIAUX→METAL_PROKAT, METAL→METIZ_KREPEZH. Backend: CategoryTagConfigService GET/PUT, routes /admin/category-tag-config, consultation charge mapping depuis DB avec fallback defaults. Frontend: /settings page avec Table 3 colonnes (categorie/tags Select multiple/fournisseurs auto-calcules), menu sidebar Настройки (ToolOutlined). Seed 13 mappings defaults.
|
||||
|
||||
## Derniere session (21/03/2026, category-tag-configurator)
|
||||
- SPEC 22E: nettoyage tags + configurateur categories-tags frontend
|
||||
- Prochaine etape: invoices, reports
|
||||
|
||||
@ -11,6 +11,7 @@ import { PurchasesPage } from '@/pages/purchases/PurchasesPage';
|
||||
import { FinancePage } from '@/pages/finance/FinancePage';
|
||||
import { NotificationsPage } from '@/pages/notifications/NotificationsPage';
|
||||
import { AdminPage } from '@/pages/admin/AdminPage';
|
||||
import { SettingsPage } from '@/pages/settings/SettingsPage';
|
||||
import { NotFound } from '@/pages/NotFound';
|
||||
|
||||
function App() {
|
||||
@ -50,6 +51,7 @@ function App() {
|
||||
<Route path="admin/users" element={<AdminPage />} />
|
||||
<Route path="admin/audit" element={<AdminPage />} />
|
||||
<Route path="admin/config" element={<AdminPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
30
frontend/src/api/categoryTagConfig.ts
Normal file
30
frontend/src/api/categoryTagConfig.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import client from './client';
|
||||
|
||||
export interface CategoryConfig {
|
||||
code: string;
|
||||
label: string;
|
||||
mappedTags: string[];
|
||||
}
|
||||
|
||||
export interface TagInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
label: string;
|
||||
supplierCount: number;
|
||||
}
|
||||
|
||||
export interface CategoryTagConfigResponse {
|
||||
categories: CategoryConfig[];
|
||||
tags: TagInfo[];
|
||||
suppliersByTag: Record<string, { id: number; name: string }[]>;
|
||||
}
|
||||
|
||||
export async function getCategoryTagConfig() {
|
||||
const { data } = await client.get<CategoryTagConfigResponse>('/admin/category-tag-config');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateCategoryTagMapping(category: string, tagNames: string[]) {
|
||||
const { data } = await client.put('/admin/category-tag-config', { category, tagNames });
|
||||
return data;
|
||||
}
|
||||
@ -7,6 +7,7 @@ import {
|
||||
DollarOutlined,
|
||||
BellOutlined,
|
||||
SettingOutlined,
|
||||
ToolOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import type { MenuProps } from 'antd';
|
||||
@ -47,6 +48,11 @@ const menuItems: MenuProps['items'] = [
|
||||
icon: <SettingOutlined />,
|
||||
label: 'Администрирование',
|
||||
},
|
||||
{
|
||||
key: '/settings',
|
||||
icon: <ToolOutlined />,
|
||||
label: 'Настройки',
|
||||
},
|
||||
];
|
||||
|
||||
export function AppMenu() {
|
||||
|
||||
142
frontend/src/pages/settings/CategoryTagConfigurator.tsx
Normal file
142
frontend/src/pages/settings/CategoryTagConfigurator.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Table, Select, Tag, message, Typography, Spin } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
getCategoryTagConfig,
|
||||
updateCategoryTagMapping,
|
||||
type CategoryConfig,
|
||||
type TagInfo,
|
||||
type CategoryTagConfigResponse,
|
||||
} from '@/api/categoryTagConfig';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
interface RowData {
|
||||
key: string;
|
||||
code: string;
|
||||
label: string;
|
||||
mappedTags: string[];
|
||||
suppliers: string[];
|
||||
}
|
||||
|
||||
export function CategoryTagConfigurator() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
const [config, setConfig] = useState<CategoryTagConfigResponse | null>(null);
|
||||
const [rows, setRows] = useState<RowData[]>([]);
|
||||
|
||||
const buildRows = useCallback((cfg: CategoryTagConfigResponse): RowData[] => {
|
||||
return cfg.categories.map((cat) => {
|
||||
// Collect suppliers for all mapped tags
|
||||
const supplierNames = new Set<string>();
|
||||
for (const tagName of cat.mappedTags) {
|
||||
const suppliers = cfg.suppliersByTag[tagName] ?? [];
|
||||
for (const s of suppliers) supplierNames.add(s.name);
|
||||
}
|
||||
return {
|
||||
key: cat.code,
|
||||
code: cat.code,
|
||||
label: cat.label,
|
||||
mappedTags: cat.mappedTags,
|
||||
suppliers: Array.from(supplierNames),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getCategoryTagConfig();
|
||||
setConfig(data);
|
||||
setRows(buildRows(data));
|
||||
} catch {
|
||||
message.error('Ошибка загрузки конфигурации');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [buildRows]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, [fetchConfig]);
|
||||
|
||||
const handleTagChange = async (categoryCode: string, tagNames: string[]) => {
|
||||
setSaving(categoryCode);
|
||||
try {
|
||||
await updateCategoryTagMapping(categoryCode, tagNames);
|
||||
message.success('Сохранено');
|
||||
await fetchConfig();
|
||||
} catch {
|
||||
message.error('Ошибка сохранения');
|
||||
} finally {
|
||||
setSaving(null);
|
||||
}
|
||||
};
|
||||
|
||||
const tagOptions = (config?.tags ?? []).map((t: TagInfo) => ({
|
||||
label: t.label,
|
||||
value: t.name,
|
||||
}));
|
||||
|
||||
const columns: ColumnsType<RowData> = [
|
||||
{
|
||||
title: 'Категория материала',
|
||||
dataIndex: 'label',
|
||||
key: 'label',
|
||||
width: 220,
|
||||
render: (text: string, record: RowData) => (
|
||||
<span>
|
||||
<strong>{text}</strong>
|
||||
<br />
|
||||
<span style={{ color: '#999', fontSize: 11 }}>{record.code}</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Теги поставщиков (привязка)',
|
||||
dataIndex: 'mappedTags',
|
||||
key: 'mappedTags',
|
||||
width: 350,
|
||||
render: (_: string[], record: RowData) => (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
value={record.mappedTags}
|
||||
options={tagOptions}
|
||||
onChange={(vals) => handleTagChange(record.code, vals)}
|
||||
loading={saving === record.code}
|
||||
placeholder="Выберите теги..."
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Поставщики',
|
||||
dataIndex: 'suppliers',
|
||||
key: 'suppliers',
|
||||
render: (suppliers: string[]) =>
|
||||
suppliers.length > 0
|
||||
? suppliers.map((s) => (
|
||||
<Tag key={s} color="blue" style={{ marginBottom: 2 }}>
|
||||
{s}
|
||||
</Tag>
|
||||
))
|
||||
: <span style={{ color: '#999' }}>—</span>,
|
||||
},
|
||||
];
|
||||
|
||||
if (loading) return <Spin size="large" style={{ display: 'block', margin: '40px auto' }} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 16 }}>Привязка категорий к тегам поставщиков</Title>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
frontend/src/pages/settings/SettingsPage.tsx
Normal file
13
frontend/src/pages/settings/SettingsPage.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Typography } from 'antd';
|
||||
import { CategoryTagConfigurator } from './CategoryTagConfigurator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<div>
|
||||
<Title level={3}>Настройки</Title>
|
||||
<CategoryTagConfigurator />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1296,6 +1296,16 @@ model OnecBankAccount {
|
||||
@@map("onec_bank_accounts")
|
||||
}
|
||||
|
||||
// Mapping MaterialCategory → SupplierTag names (configurable)
|
||||
model CategoryTagMapping {
|
||||
id Int @id @default(autoincrement())
|
||||
category String @unique
|
||||
tagNames String[] @map("tag_names")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("category_tag_mappings")
|
||||
}
|
||||
|
||||
// 1C OData — cashflow categories (статьи ДДС)
|
||||
model OnecCashflowCategory {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
38
src/modules/admin/admin.category-tag.routes.ts
Normal file
38
src/modules/admin/admin.category-tag.routes.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CategoryTagConfigService } from './admin.category-tag.service.js';
|
||||
import { authorize } from '../auth/auth.middleware.js';
|
||||
|
||||
export async function categoryTagConfigRoutes(server: FastifyInstance) {
|
||||
server.addHook('preHandler', server.authenticate);
|
||||
const svc = new CategoryTagConfigService(server.prisma);
|
||||
|
||||
// GET / — Get full category-tag config
|
||||
server.get('/', {
|
||||
preHandler: [authorize(['ADMIN', 'DIRECTION_FIN', 'DIRECTION_OPS', 'ACHETEUR'] as Role[])],
|
||||
}, async (_req, rep) => {
|
||||
try {
|
||||
return rep.send(await svc.getConfig());
|
||||
} catch (e) {
|
||||
const err = e as { statusCode?: number; message?: string };
|
||||
return rep.status(err.statusCode ?? 500).send({ error: err.message ?? 'Internal error' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT / — Update a single category mapping
|
||||
server.put('/', {
|
||||
preHandler: [authorize(['ADMIN'] as Role[])],
|
||||
}, async (req, rep) => {
|
||||
const body = req.body as { category?: string; tagNames?: string[] } | undefined;
|
||||
if (!body?.category || !Array.isArray(body?.tagNames)) {
|
||||
return rep.status(400).send({ error: 'category (string) and tagNames (string[]) required' });
|
||||
}
|
||||
try {
|
||||
const result = await svc.updateMapping(body.category, body.tagNames);
|
||||
return rep.send(result);
|
||||
} catch (e) {
|
||||
const err = e as { statusCode?: number; message?: string };
|
||||
return rep.status(err.statusCode ?? 500).send({ error: err.message ?? 'Internal error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
86
src/modules/admin/admin.category-tag.service.ts
Normal file
86
src/modules/admin/admin.category-tag.service.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
|
||||
export class CategoryTagConfigService {
|
||||
private prisma: PrismaClient;
|
||||
|
||||
constructor(prisma: PrismaClient) {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
// Load mappings from DB
|
||||
const mappings = await this.prisma.categoryTagMapping.findMany({
|
||||
orderBy: { category: 'asc' },
|
||||
});
|
||||
|
||||
// Load all tags with supplier counts
|
||||
const tags = await this.prisma.supplierTag.findMany({
|
||||
include: {
|
||||
supplierLinks: {
|
||||
include: {
|
||||
supplier: { select: { id: true, companyName: true, status: true, deletedAt: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
METAL: 'Металл/Металлопрокат',
|
||||
FASTENERS: 'Крепёж/Метизы',
|
||||
COMPONENTS: 'Комплектующие',
|
||||
PAINT: 'Покраска',
|
||||
LASER_FLAT: 'Лазерная резка (лист)',
|
||||
LASER_TUBE: 'Лазерная резка (труба)',
|
||||
TUBE_BENDING: 'Гибка труб',
|
||||
ROD_BENDING: 'Гибка прутка',
|
||||
CNC_MILLING: 'Фрезеровка ЧПУ',
|
||||
GALVANIC: 'Гальваника',
|
||||
MACHINING_EXT: 'Мехобработка (внеш.)',
|
||||
MESH_WELDING: 'Сварка сеток',
|
||||
SPRING_COILING: 'Навивка пружин',
|
||||
};
|
||||
|
||||
// Build categories array
|
||||
const categories = mappings.map((m) => ({
|
||||
code: m.category,
|
||||
label: categoryLabels[m.category] ?? m.category,
|
||||
mappedTags: m.tagNames,
|
||||
}));
|
||||
|
||||
// Build tags array
|
||||
const tagsResult = tags.map((t) => {
|
||||
const activeSuppliers = t.supplierLinks.filter(
|
||||
(l) => l.supplier.status === 'ACTIVE' && !l.supplier.deletedAt,
|
||||
);
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
label: t.label ?? t.name,
|
||||
supplierCount: activeSuppliers.length,
|
||||
};
|
||||
});
|
||||
|
||||
// Build suppliersByTag
|
||||
const suppliersByTag: Record<string, { id: number; name: string }[]> = {};
|
||||
for (const t of tags) {
|
||||
const activeSuppliers = t.supplierLinks
|
||||
.filter((l) => l.supplier.status === 'ACTIVE' && !l.supplier.deletedAt)
|
||||
.map((l) => ({ id: l.supplier.id, name: l.supplier.companyName }));
|
||||
if (activeSuppliers.length > 0) {
|
||||
suppliersByTag[t.name] = activeSuppliers;
|
||||
}
|
||||
}
|
||||
|
||||
return { categories, tags: tagsResult, suppliersByTag };
|
||||
}
|
||||
|
||||
async updateMapping(category: string, tagNames: string[]) {
|
||||
const result = await this.prisma.categoryTagMapping.upsert({
|
||||
where: { category },
|
||||
update: { tagNames },
|
||||
create: { category, tagNames },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -20,11 +20,11 @@ export interface ConsultationDraft {
|
||||
htmlBody: string;
|
||||
}
|
||||
|
||||
// Mapping MaterialCategory → SupplierTag name(s)
|
||||
const CATEGORY_TO_TAG: Record<string, string[]> = {
|
||||
METAL: ['MATERIAUX', 'METAL'],
|
||||
FASTENERS: ['METAL', 'PIECES_DETACHEES'],
|
||||
COMPONENTS: ['COMPOSANTS', 'PIECES_DETACHEES'],
|
||||
// Default mapping (fallback if DB empty)
|
||||
const DEFAULT_CATEGORY_TO_TAG: Record<string, string[]> = {
|
||||
METAL: ['METAL_PROKAT'],
|
||||
FASTENERS: ['METIZ_KREPEZH'],
|
||||
COMPONENTS: ['COMPOSANTS'],
|
||||
PAINT: ['PEINTURE'],
|
||||
LASER_FLAT: ['SOUS_TRAITANCE_LASER'],
|
||||
LASER_TUBE: ['SOUS_TRAITANCE_LASER'],
|
||||
@ -76,6 +76,16 @@ export class ConsultationService {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
|
||||
private async loadCategoryToTag(): Promise<Record<string, string[]>> {
|
||||
const mappings = await this.prisma.categoryTagMapping.findMany();
|
||||
if (mappings.length === 0) return DEFAULT_CATEGORY_TO_TAG;
|
||||
const result: Record<string, string[]> = {};
|
||||
for (const m of mappings) {
|
||||
result[m.category] = m.tagNames;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async generateConsultationDrafts(orderId: number): Promise<ConsultationDraft[]> {
|
||||
// Load order for orderCode
|
||||
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
|
||||
@ -121,11 +131,14 @@ export class ConsultationService {
|
||||
byCategory.get(cat)!.push(item);
|
||||
}
|
||||
|
||||
// Load configurable mapping from DB
|
||||
const categoryToTag = await this.loadCategoryToTag();
|
||||
|
||||
// For each category, find tagged suppliers
|
||||
const supplierItemsMap = new Map<number, { supplier: any; items: ConsultationItem[] }>();
|
||||
|
||||
for (const [category, catItems] of byCategory) {
|
||||
const tagNames = CATEGORY_TO_TAG[category] ?? [];
|
||||
const tagNames = categoryToTag[category] ?? [];
|
||||
let suppliers: any[];
|
||||
|
||||
if (tagNames.length > 0) {
|
||||
|
||||
34
src/seed.ts
34
src/seed.ts
@ -50,7 +50,7 @@ const SUPPLIERS = [
|
||||
accountantName: 'Зайцева Ольга Николаевна', accountantPhone: '+7-8202-55-33-12', accountantEmail: 'buh@severstal-metiz.ru',
|
||||
paymentDelayDays: 14, deliveryDelayDays: 5, discountPercent: 3, status: 'ACTIVE' as const,
|
||||
notes: 'Основной поставщик метизов и крепежа. Скидка 3% при объёме от 200 000 руб. Доставка ТК до Твери.',
|
||||
tags: ['METAL'],
|
||||
tags: ['METIZ_KREPEZH'],
|
||||
},
|
||||
{
|
||||
companyName: 'ООО "ТМК-Маркет"', inn: '7700000102', kpp: '770001001', ogrn: '1027700000102',
|
||||
@ -59,7 +59,7 @@ const SUPPLIERS = [
|
||||
accountantName: 'Попова Марина Сергеевна', accountantPhone: '+7-495-777-11-23', accountantEmail: 'buh@tmk-market.ru',
|
||||
paymentDelayDays: 7, deliveryDelayDays: 3, discountPercent: 5, status: 'ACTIVE' as const,
|
||||
notes: 'Трубы, профиль, листовой прокат. Склад в Москве, доставка 2-3 дня. Минимальный заказ 100 000 руб.',
|
||||
tags: ['MATERIAUX'],
|
||||
tags: ['METAL_PROKAT'],
|
||||
},
|
||||
{
|
||||
companyName: 'ООО "КраскаПром"', inn: '6900000201', kpp: '690001001', ogrn: '1066900000201',
|
||||
@ -103,7 +103,7 @@ const SUPPLIERS = [
|
||||
// TAGS fournisseurs
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
const SUPPLIER_TAGS = [
|
||||
{ name: 'MATERIAUX', label: 'Материалы' },
|
||||
{ name: 'METAL_PROKAT', label: 'Металл/Металлопрокат' },
|
||||
{ name: 'PIECES_DETACHEES', label: 'Запчасти' },
|
||||
{ name: 'SOUS_TRAITANCE_LASER', label: 'Субподряд сварка/лазер' },
|
||||
{ name: 'PEINTURE', label: 'Покраска' },
|
||||
@ -111,7 +111,7 @@ const SUPPLIER_TAGS = [
|
||||
{ name: 'CONSOMMABLES', label: 'Расходные материалы' },
|
||||
{ name: 'TRANSPORT', label: 'Транспорт' },
|
||||
{ name: 'OUTILLAGE', label: 'Инструмент/оснастка' },
|
||||
{ name: 'METAL', label: 'Метизы/крепёж' },
|
||||
{ name: 'METIZ_KREPEZH', label: 'Метизы/крепёж' },
|
||||
{ name: 'PACKAGING', label: 'Упаковка' },
|
||||
{ name: 'ELECTRICAL', label: 'Электрика' },
|
||||
{ name: 'WELDING', label: 'Сварка' },
|
||||
@ -436,6 +436,32 @@ async function main() {
|
||||
}
|
||||
console.log(` ✓ ${SUPPLIER_TAGS.length} supplier tags`);
|
||||
|
||||
// ── CATEGORY-TAG MAPPINGS ──────────────────────────────────
|
||||
console.log('\nSeeding category-tag mappings...');
|
||||
const CATEGORY_TAG_DEFAULTS: { category: string; tagNames: string[] }[] = [
|
||||
{ category: 'METAL', tagNames: ['METAL_PROKAT'] },
|
||||
{ category: 'FASTENERS', tagNames: ['METIZ_KREPEZH'] },
|
||||
{ category: 'COMPONENTS', tagNames: ['COMPOSANTS'] },
|
||||
{ category: 'PAINT', tagNames: ['PEINTURE'] },
|
||||
{ category: 'LASER_FLAT', tagNames: ['SOUS_TRAITANCE_LASER'] },
|
||||
{ category: 'LASER_TUBE', tagNames: ['SOUS_TRAITANCE_LASER'] },
|
||||
{ category: 'TUBE_BENDING', tagNames: ['SOUS_TRAITANCE_LASER'] },
|
||||
{ category: 'ROD_BENDING', tagNames: ['SOUS_TRAITANCE_LASER'] },
|
||||
{ category: 'CNC_MILLING', tagNames: ['SOUS_TRAITANCE_LASER'] },
|
||||
{ category: 'GALVANIC', tagNames: ['COMPOSANTS'] },
|
||||
{ category: 'MACHINING_EXT', tagNames: ['SOUS_TRAITANCE_LASER'] },
|
||||
{ category: 'MESH_WELDING', tagNames: ['WELDING'] },
|
||||
{ category: 'SPRING_COILING', tagNames: ['COMPOSANTS'] },
|
||||
];
|
||||
for (const m of CATEGORY_TAG_DEFAULTS) {
|
||||
await prisma.categoryTagMapping.upsert({
|
||||
where: { category: m.category },
|
||||
update: { tagNames: m.tagNames },
|
||||
create: m,
|
||||
});
|
||||
}
|
||||
console.log(` ✓ ${CATEGORY_TAG_DEFAULTS.length} category-tag mappings`);
|
||||
|
||||
// ── SUPPLIERS ──────────────────────────────────────────────
|
||||
console.log('\nSeeding suppliers...');
|
||||
const supplierMap: Record<string, number> = {};
|
||||
|
||||
@ -23,6 +23,7 @@ import { notificationRoutes } from './modules/notifications/notifications.routes
|
||||
import { taskRoutes, dashboardMeRoute } from './modules/tasks/tasks.routes.js';
|
||||
import { auditRoutes } from './modules/audit/audit.routes.js';
|
||||
import { adminUserRoutes, adminConfigRoutes } from './modules/admin/admin.routes.js';
|
||||
import { categoryTagConfigRoutes } from './modules/admin/admin.category-tag.routes.js';
|
||||
import { healthRoute, adminSystemRoute } from './modules/admin/admin.system.js';
|
||||
import { clientRoutes } from './modules/clients/clients.routes.js';
|
||||
import { documentFileRoutes } from './modules/document-files/document-files.routes.js';
|
||||
@ -67,6 +68,7 @@ async function start() {
|
||||
await server.register(auditRoutes, { prefix: '/api/v1/audit' });
|
||||
await server.register(adminUserRoutes, { prefix: '/api/v1/admin/users' });
|
||||
await server.register(adminConfigRoutes, { prefix: '/api/v1/admin/config' });
|
||||
await server.register(categoryTagConfigRoutes, { prefix: '/api/v1/admin/category-tag-config' });
|
||||
await server.register(adminSystemRoute, { prefix: '/api/v1/admin/system' });
|
||||
await server.register(clientRoutes, { prefix: '/api/v1/clients' });
|
||||
await server.register(documentFileRoutes, { prefix: '/api/v1/documents/:documentId/files' });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user