From 3fe4ef1c751bc280a74ee00aeac40a1866675c4e Mon Sep 17 00:00:00 2001 From: Louis-Andre Date: Sat, 21 Mar 2026 09:31:50 +0000 Subject: [PATCH] feat: category-tag configurator + tag rename cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 8 +- frontend/src/App.tsx | 2 + frontend/src/api/categoryTagConfig.ts | 30 ++++ frontend/src/components/Layout/AppMenu.tsx | 6 + .../settings/CategoryTagConfigurator.tsx | 142 ++++++++++++++++++ frontend/src/pages/settings/SettingsPage.tsx | 13 ++ prisma/schema.prisma | 10 ++ .../admin/admin.category-tag.routes.ts | 38 +++++ .../admin/admin.category-tag.service.ts | 86 +++++++++++ .../consultation/consultation.service.ts | 25 ++- src/seed.ts | 34 ++++- src/server.ts | 2 + 12 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 frontend/src/api/categoryTagConfig.ts create mode 100644 frontend/src/pages/settings/CategoryTagConfigurator.tsx create mode 100644 frontend/src/pages/settings/SettingsPage.tsx create mode 100644 src/modules/admin/admin.category-tag.routes.ts create mode 100644 src/modules/admin/admin.category-tag.service.ts diff --git a/CLAUDE.md b/CLAUDE.md index 29df8f9..b6bd01d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 83ffbd2..bed7b88 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/api/categoryTagConfig.ts b/frontend/src/api/categoryTagConfig.ts new file mode 100644 index 0000000..9b36d23 --- /dev/null +++ b/frontend/src/api/categoryTagConfig.ts @@ -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; +} + +export async function getCategoryTagConfig() { + const { data } = await client.get('/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; +} diff --git a/frontend/src/components/Layout/AppMenu.tsx b/frontend/src/components/Layout/AppMenu.tsx index 0117044..a543d5f 100644 --- a/frontend/src/components/Layout/AppMenu.tsx +++ b/frontend/src/components/Layout/AppMenu.tsx @@ -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: , label: 'Администрирование', }, + { + key: '/settings', + icon: , + label: 'Настройки', + }, ]; export function AppMenu() { diff --git a/frontend/src/pages/settings/CategoryTagConfigurator.tsx b/frontend/src/pages/settings/CategoryTagConfigurator.tsx new file mode 100644 index 0000000..5a42ddd --- /dev/null +++ b/frontend/src/pages/settings/CategoryTagConfigurator.tsx @@ -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(null); + const [config, setConfig] = useState(null); + const [rows, setRows] = useState([]); + + const buildRows = useCallback((cfg: CategoryTagConfigResponse): RowData[] => { + return cfg.categories.map((cat) => { + // Collect suppliers for all mapped tags + const supplierNames = new Set(); + 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 = [ + { + title: 'Категория материала', + dataIndex: 'label', + key: 'label', + width: 220, + render: (text: string, record: RowData) => ( + + {text} +
+ {record.code} +
+ ), + }, + { + title: 'Теги поставщиков (привязка)', + dataIndex: 'mappedTags', + key: 'mappedTags', + width: 350, + render: (_: string[], record: RowData) => ( +