diff --git a/README.md b/README.md index 1ea5b96..7342c82 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Die mitgelieferte Demo-Instanz heißt **„Brittas Nähkiste"** (Kurzwaren/Nähb - **Admin** (Premium, „Warmth & Approachability"): Session-Login statt Browser-Basic-Auth, Rollen (Owner/Redaktion/Versand), Command-Palette (⌘K), Toasts, aufgewertetes Dashboard mit KPI-Trends, Sparkline, Aktivitäts-Feed und Schnellaktionen. - **Visual-Block-Builder**: Vollbild-Editor mit Block-Liste (Drag/▲▼/duplizieren/löschen), Live-Vorschau (Desktop/Mobil) und Block-Einstellungen. Block-Typen: Hero, Rich-Text, Bild, Galerie, Slider, Feature-Grid, Produkt-Grid, CTA-Banner, Abstand, Roh-HTML. - **KI-Editierbarkeit**: token-gesicherte Admin-JSON-API (`/api/admin/*`) plus maschinenlesbares Manifest (`/api/admin`, `/ai-admin.txt`) und ein **MCP-Server** (`mcp/`). +- **Gutschein-/Rabatt-Engine** (v2.1): Codes vom Typ `percent` / `fixed` / `freeshipping` mit Zeitplan, Mindestbestellwert, Gesamt- und Pro-Kunde-Limit, „geheim" (nicht öffentlich listbar) und „automatisch" (greift ohne Code, wenn Bedingungen erfüllt). Admin-Bereich **Rabatte** (Owner/Redaktion) mit Status-Badges (Aktiv/Geplant/Abgelaufen/Aufgebraucht/Inaktiv); Storefront-Einlösung im Checkout über `/api/discount`; serverseitige Re-Validierung in `/api/checkout`; Stripe-Coupon-Anbindung. Popups können einen Code anzeigen (+ Kopieren-Button) — auch für gezielt verteilte geheime Codes; Popup-Stile `modal` / `slidein` / `bar`. +- **Editierbare, gebrandete 404** (v2.1): `src/pages/404.astro` rendert die System-Seite mit Slug `404` über den Block-Builder. Wird per `ensureSystemPages()` bei jedem Boot idempotent angelegt und ist im Admin unter **Inhalte** editierbar. - **Engine**: synchron via `better-sqlite3` (WAL), automatisches Seeding beim ersten Start. - **First-Party-Analytics**: eigene `events`-Tabelle, kein externer Dienst (Session = täglich rollender Hash). - **Branding konfigurierbar**: Shop-Name, Akzentfarbe, Währung u. a. in einer `settings`-Tabelle. @@ -90,7 +92,7 @@ Das `Dockerfile` (node:22-slim) baut `better-sqlite3` nativ, legt `/data` an und ## Datenmodell -`settings`, `products`, `orders`, `customers`, `slides`, `pages` (inkl. `blocks`), `popups`, `subscribers`, `events`, `media`, `users`, `audit` — alles seed-bar und im Admin pflegbar. +`settings`, `products`, `orders` (inkl. `discount_code` / `discount_cents`), `customers`, `slides`, `pages` (inkl. `blocks`; System-Seite `404`), `popups` (inkl. `style` / `discount_id`), `discounts`, `discount_redemptions`, `subscribers`, `events`, `media`, `users`, `audit` — alles seed-bar und im Admin pflegbar. --- diff --git a/mcp/package-lock.json b/mcp/package-lock.json index e321f46..ad21512 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "hd-commerce-mcp", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hd-commerce-mcp", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }, diff --git a/mcp/package.json b/mcp/package.json index f64be14..7c9d16c 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,6 +1,6 @@ { "name": "hd-commerce-mcp", - "version": "2.0.0", + "version": "2.1.0", "private": true, "type": "module", "description": "MCP-Server für hd-commerce — bearbeitet Produkte, Seiten, Slides, Popups & Einstellungen über die Admin-API.", diff --git a/mcp/server.js b/mcp/server.js index d3dac53..b9db2cc 100644 --- a/mcp/server.js +++ b/mcp/server.js @@ -33,12 +33,15 @@ const TOOLS = [ { name: 'list_orders', description: 'Bestellungen auflisten (nur lesen).', inputSchema: { type: 'object', properties: {} } }, { name: 'list_slides', description: 'Slider-Slides auflisten.', inputSchema: { type: 'object', properties: {} } }, { name: 'upsert_slide', description: 'Slide anlegen/aktualisieren.', inputSchema: { type: 'object', properties: { slide: { type: 'object' } }, required: ['slide'] } }, + { name: 'list_discounts', description: 'Alle Rabatte/Gutscheine auflisten.', inputSchema: { type: 'object', properties: {} } }, + { name: 'upsert_discount', description: 'Rabatt anlegen/aktualisieren. Mit id oder code => Update, sonst Create. type: percent|fixed|freeshipping; value bei percent 1-100, bei fixed in Cent.', inputSchema: { type: 'object', properties: { discount: { type: 'object', description: 'Felder: code, title, type, value, min_order_cents, starts_at, expires_at, max_uses, max_per_customer, active, secret, auto' } }, required: ['discount'] } }, + { name: 'delete_discount', description: 'Rabatt löschen (per ID).', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } }, { name: 'get_settings', description: 'Shop-Einstellungen (Key/Value) holen.', inputSchema: { type: 'object', properties: {} } }, { name: 'update_settings', description: 'Shop-Einstellungen aktualisieren (Key/Value-Map, z.B. shop_name, brand_accent).', inputSchema: { type: 'object', properties: { settings: { type: 'object' } }, required: ['settings'] } }, { name: 'get_manifest', description: 'API-Manifest (alle Ressourcen, Felder, Block-Typen).', inputSchema: { type: 'object', properties: {} } }, ]; -const server = new Server({ name: 'hd-commerce', version: '2.0.0' }, { capabilities: { tools: {} } }); +const server = new Server({ name: 'hd-commerce', version: '2.1.0' }, { capabilities: { tools: {} } }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); server.setRequestHandler(CallToolRequestSchema, async (req) => { @@ -57,6 +60,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => { case 'list_orders': out = await api('GET', '/api/admin/orders'); break; case 'list_slides': out = await api('GET', '/api/admin/slides'); break; case 'upsert_slide': out = await api('POST', '/api/admin/slides', a.slide); break; + case 'list_discounts': out = await api('GET', '/api/admin/discounts'); break; + case 'upsert_discount': out = await api('POST', '/api/admin/discounts', a.discount); break; + case 'delete_discount': out = await api('DELETE', '/api/admin/discounts/' + encodeURIComponent(a.id)); break; case 'get_settings': out = await api('GET', '/api/admin/settings'); break; case 'update_settings': out = await api('POST', '/api/admin/settings', a.settings); break; case 'get_manifest': out = await api('GET', '/api/admin'); break; diff --git a/package-lock.json b/package-lock.json index 80e0fa4..5528b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hd-commerce", - "version": "1.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hd-commerce", - "version": "1.0.0", + "version": "2.1.0", "dependencies": { "@astrojs/node": "^9.1.3", "@fontsource-variable/fraunces": "^5.1.0", diff --git a/package.json b/package.json index a6de80c..ac17de9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hd-commerce", "type": "module", - "version": "1.0.0", + "version": "2.1.0", "private": true, "description": "hd-commerce — neutrales SQLite-Commerce-Backend (Admin + API + Demo-Storefront)", "scripts": { diff --git a/public/popups.js b/public/popups.js index 0cc4fd8..075e0a2 100644 --- a/public/popups.js +++ b/public/popups.js @@ -1,4 +1,5 @@ -/* hd-commerce — Popup-Engine (vanilla). Frequenz-Cap via localStorage. */ +/* hd-commerce — Popup-Engine (vanilla). Frequenz-Cap via localStorage. + Stile: modal (zentral), slidein (unten rechts), bar (oben). Typ discount: Code + Kopieren. */ (function () { var root = document.getElementById('popupRoot'); if (!root) return; @@ -19,25 +20,56 @@ localStorage.setItem('hdc_popup_' + p.id, String(Date.now())); if (p.freq === 'session') sessionStorage.setItem('hdc_ps_' + p.id, '1'); } - function build(p) { - var ov = document.createElement('div'); - ov.className = 'hdc-popup-overlay'; + function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); } + + function innerHtml(p) { var isNl = p.type === 'newsletter'; - ov.innerHTML = - '
' + (p.body || '') + '
' + - (isNl - ? '' - : (p.cta_url ? '' + (p.cta_text || 'Mehr') + '' : '')) + - '' + esc(p.body) + '
'; + if (isDisc) { + html += '' + esc(p.discount_code) + '' +
+ '| Code | Typ | Wert | Verbrauch | Status | |
|---|---|---|---|---|---|
| Keine Rabatte | |||||
| {d.code}{d.secret && geheim}{d.auto && auto} {d.title} |
+ {typeLabel[d.type] || d.type} | +{valueLabel(d)} | +{d.used_count}{d.max_uses != null ? ' / ' + d.max_uses : ''} | +{st[1]} | ++ Bearbeiten + + | +