u
This commit is contained in:
69
TODO.md
69
TODO.md
@@ -1,69 +0,0 @@
|
|||||||
# TODO — nächster Schritt (Brands / Artikelfelder)
|
|
||||||
|
|
||||||
**dashback** hat die HTTP-Routen geliefert (siehe `../dashback/TODO.md`). Alle Endpoints stehen. `companyId` kommt **immer aus der Auth-Session** (nicht aus `?companyId=`), gesteuert über `google_auth.setSelectedCompany`. Hier die UI bauen.
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
| UI-Bedarf | HTTP | Request | Response |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| Brand-Liste | `GET /api/brand` | – | `{ brands: Brand[] }` |
|
|
||||||
| Brand anlegen / updaten | `PUT /api/brand` | `{ name, gsprpinfo? }` | `{ brand: Brand }` |
|
|
||||||
| Brand löschen | `DELETE /api/brand/:id` | – | `204` |
|
|
||||||
| EANs setzen (replace-all) | `PUT /api/article/:sku/eans` | `{ eans: string[] }` | `{ article }` (Detail) |
|
|
||||||
| Brand setzen / entfernen | `PUT /api/article/:sku/brand` | `{ brandId: number \| null }` | `{ article }` (Detail) |
|
|
||||||
| Gewicht setzen | `PUT /api/article/:sku/weight` | `{ weight: number }` | `{ article }` (Detail) |
|
|
||||||
| Kategorien setzen (replace-all) | `PUT /api/article/:sku/categories` | `{ categoryIds: number[] }` | `{ article }` (Detail) |
|
|
||||||
| Kategorie umbenennen | `PATCH /api/category/:id` | `{ name }` | `{ category: { id, name, slug } }` |
|
|
||||||
| Kategorie-Reihenfolge | `PUT /api/category/order` | `{ parentId, categoryIds: number[] }` | `{ categoryIds }` |
|
|
||||||
| **Kategorie verschieben** (Frontend fertig, Backend-Vorschlag offen) | `PATCH /api/category/:id` | `{ parentId: number }` | `{ category: { id, parentId, name, slug } }` |
|
|
||||||
| Detail liefert neue Felder | `GET /api/article/:sku` | – | `{ article }` mit `eans`, `categoryIds`, `weight`, `brand` |
|
|
||||||
|
|
||||||
`Brand` = `{ id, name, gsprpinfo \| null }`.
|
|
||||||
|
|
||||||
Alle PUT/PATCH-Endpoints der Artikel liefern dasselbe Shape wie `GET /api/article/:sku`.
|
|
||||||
|
|
||||||
## Umsetzung
|
|
||||||
|
|
||||||
### API-Client (`src/api/index.js`)
|
|
||||||
- Alle Calls über den bestehenden Client (`credentials: 'include'`) — Frontend spricht nur HTTP, nie direkt den Store.
|
|
||||||
- Neuen Methoden für: `brand.list` / `brand.upsert` / `brand.delete` und `article.setEansBySku` / `article.setBrandIdBySku` / `article.setWeightBySku` / `article.setCategoryIdsBySku` sowie `category.rename`.
|
|
||||||
- **Kein `companyId` in Request/Pfad/Query senden** — die Company kommt aus der Session.
|
|
||||||
|
|
||||||
### Brand-UI
|
|
||||||
- **Liste**: lädt `GET /api/brand` und zeigt `name` (+ optional `gsprpinfo`).
|
|
||||||
- **Anlegen/Updaten**: ein Formular (Name + optional GSPRP-Info). Upsert matcht per `name` — gleicher Name ⇒ Update, sonst Create. Leeres/fehlendes `name` ⇒ API `400 Invalid brand`.
|
|
||||||
- **Löschen**: Bestätigung; `409 Brand in use` als Fehler anzeigen (Brand ist noch an Artikeln).
|
|
||||||
- Hängt an `selectedCompanyId`: bei Company-Wechsel Liste neu laden / leeren.
|
|
||||||
|
|
||||||
### Artikel-Detail-Felder
|
|
||||||
- **EANs**: Mehrfach-Eingabe (Liste). Replace-all: leere Liste löscht alle. `400 Invalid eans` bei leeren/duplizierten Einträgen.
|
|
||||||
- **Brand**: Dropdown aus `GET /api/brand`; „Keine Brand“ ⇒ `brandId: null`. Deal nur mit Brands derselben Company (sonst `404 Brand not found`).
|
|
||||||
- **Gewicht**: Zahlenfeld, `>= 0`. `400 Invalid weight` sonst.
|
|
||||||
- **Kategorien**: Mehrfachauswahl aus der Category-Tree; nur Kategorien derselben Company (sonst `400 Invalid categoryIds`). Leere Liste entfernt alle.
|
|
||||||
- **Kategorie umbenennen**: Inline/Formular im Category-Tree → `PATCH /api/category/:id` mit `{ name }`. Leeres `name` ⇒ `400 Name required`. Nach Save das zurückgegebene `{ id, name, slug }` im Baum aktualisieren.
|
|
||||||
- **Kategorie-Reihenfolge (Drag & Drop)**: nach Sortierung → `PUT /api/category/order` mit `{ parentId, categoryIds }` (die neuen Kind-IDs in gewünschter Reihenfolge). `parentId` ist der aktuelle Elternknoten. `400 Invalid category order` bei falscher Menge/Duplikaten; nur Children desselben Elternteils angeben.
|
|
||||||
- Nach jedem Save das zurückgegebene Detail-Objekt in den Store übernehmen.
|
|
||||||
|
|
||||||
### Kategorie in Unterkategorie ablegen (Tree-Drop) — [VORSCHLAG an dashback, offen]
|
|
||||||
- **UI fertig**: oberes Drittel der Edit-Kachel zeigt links Move-Icon (`DragHandle`), rechts Tree-Icon (`AccountTree`). Beide sind Drag-Handles (`handle: .sortable-handle, .sortable-tree`). Ziehen am **Tree**-Icon sortiert die Geschwister **nicht** neu um (`handleSort` ignoriert Tree-Draggs).
|
|
||||||
- **Backend-Vorschlag** (fehlt noch in `dashback/docs/api/routes/`):
|
|
||||||
- `PATCH /api/category/:id` erweitern: optional `parentId` im Body, zusätzlich zu `name`.
|
|
||||||
- Request: `{ parentId: number }` → verschiebt die Kategorie unter den neuen Eltern. `{ name }` → nur Umbenennen (bestehend).
|
|
||||||
- Response: `{ category: { id, parentId, name, slug } }`.
|
|
||||||
- Erforderliche Validierung: Ziel-Eltern existiert (404), kein Zyklus/kein Verschieben in sich selbst oder eigene Nachkommen (400), Company-Scope (403).
|
|
||||||
- **Bis der Vertrag feststeht**: keine Frontend-Implementierung des Drops (Regel `no-assumed-api`).
|
|
||||||
|
|
||||||
### Fehlerbehandlung
|
|
||||||
- Fehler als Toast/Formular-Fehler abbilden:
|
|
||||||
- `Brand in use` (409), `Invalid eans` / `Invalid weight` / `Invalid categoryIds` / `Invalid brand` (400)
|
|
||||||
- `Brand not found` / `Article not found` / `Category not found` (404)
|
|
||||||
- `Name required` (400) bei leerem Kategorienamen
|
|
||||||
- `No company selected` (400) → Company-Wechsel-Flow
|
|
||||||
- `Not authorized` (403)
|
|
||||||
|
|
||||||
## Referenz
|
|
||||||
- API-Doku: `../dashback/docs/api/README.md` und `../dashback/docs/api/routes/`
|
|
||||||
- `brand/list.md`, `brand/upsert.md`, `brand/delete.md`
|
|
||||||
- `article/set-eans.md`, `article/set-brand.md`, `article/set-weight.md`, `article/set-categories.md`
|
|
||||||
- `category/rename.md`
|
|
||||||
- Company-Scope: `google_auth.setSelectedCompany` (Store) — siehe `../dashdbschema/README.md`.
|
|
||||||
@@ -21,6 +21,8 @@ body {
|
|||||||
.sortable-drag {
|
.sortable-drag {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
||||||
|
cursor: grabbing !important;
|
||||||
|
user-select: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shopify Droppable: aktuell anvisierte Ziel-Kachel hervorheben */
|
/* Shopify Droppable: aktuell anvisierte Ziel-Kachel hervorheben */
|
||||||
|
|||||||
@@ -236,6 +236,10 @@ class ArtikelKategorienPage extends Component {
|
|||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
filter: '.not-sortable, .sortable-handle-btn',
|
filter: '.not-sortable, .sortable-handle-btn',
|
||||||
|
// Eigener Fallback-Drag statt nativer HTML5-DnD: sonst zeigt der Browser
|
||||||
|
// über Button/Input den Text-I-Cursor bzw. native Hover-Stile.
|
||||||
|
forceFallback: true,
|
||||||
|
fallbackOnBody: true,
|
||||||
onStart: this.handleSortStart,
|
onStart: this.handleSortStart,
|
||||||
onEnd: this.handleSort,
|
onEnd: this.handleSort,
|
||||||
});
|
});
|
||||||
@@ -247,6 +251,7 @@ class ArtikelKategorienPage extends Component {
|
|||||||
this.sortable = null;
|
this.sortable = null;
|
||||||
}
|
}
|
||||||
this.checkpointOrder = null;
|
this.checkpointOrder = null;
|
||||||
|
document.body.classList.remove('tree-dragging');
|
||||||
};
|
};
|
||||||
|
|
||||||
sortableIds = () => {
|
sortableIds = () => {
|
||||||
@@ -261,9 +266,11 @@ class ArtikelKategorienPage extends Component {
|
|||||||
handleSortStart = () => {
|
handleSortStart = () => {
|
||||||
// Snapshot der Reihenfolge vor dem ersten Drag.
|
// Snapshot der Reihenfolge vor dem ersten Drag.
|
||||||
this.checkpointOrder = this.sortableIds();
|
this.checkpointOrder = this.sortableIds();
|
||||||
|
document.body.classList.add('tree-dragging');
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSort = () => {
|
handleSort = () => {
|
||||||
|
document.body.classList.remove('tree-dragging');
|
||||||
const ids = this.sortableIds();
|
const ids = this.sortableIds();
|
||||||
const before = this.checkpointOrder ?? [];
|
const before = this.checkpointOrder ?? [];
|
||||||
this.checkpointOrder = null;
|
this.checkpointOrder = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user