diff --git a/hifi-src/src/i18n/de.js b/hifi-src/src/i18n/de.js
index bb8600f..a8a3ef1 100644
--- a/hifi-src/src/i18n/de.js
+++ b/hifi-src/src/i18n/de.js
@@ -154,6 +154,7 @@ export default {
totalPrice: 'Gesamtpreis (ca.)',
productLoading: 'Produkt wird geladen…',
requestContact: 'Kontakt anfragen',
+ featuredBadge: 'Empfohlen',
},
leistungen: {
metaTitle: 'Leistungen',
diff --git a/hifi-src/src/i18n/en.js b/hifi-src/src/i18n/en.js
index e84a8c7..965dc18 100644
--- a/hifi-src/src/i18n/en.js
+++ b/hifi-src/src/i18n/en.js
@@ -154,6 +154,7 @@ export default {
totalPrice: 'Total price (approx.)',
productLoading: 'Loading product…',
requestContact: 'Request contact',
+ featuredBadge: 'Recommended',
},
leistungen: {
metaTitle: 'Services',
diff --git a/hifi-src/src/pages/admin/Packages.jsx b/hifi-src/src/pages/admin/Packages.jsx
index 79cac6d..5b684bf 100644
--- a/hifi-src/src/pages/admin/Packages.jsx
+++ b/hifi-src/src/pages/admin/Packages.jsx
@@ -1,8 +1,20 @@
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client.js';
+import IconPicker from '../../components/IconPicker.jsx';
+import DynamicIcon from '../../components/DynamicIcon.jsx';
-const emptyForm = { name: '', car_model_id: '', description: '', markup_type: 'none', markup_value: 0, sort_order: 0 };
+const emptyForm = {
+ name: '',
+ car_model_id: '',
+ description: '',
+ markup_type: 'none',
+ markup_value: 0,
+ icon_name: '',
+ tagline: '',
+ is_featured: false,
+ sort_order: 0,
+};
export default function Packages() {
const [packages, setPackages] = useState([]);
@@ -62,6 +74,9 @@ export default function Packages() {
description: pkg.description || '',
markup_type: pkg.markup_type || 'none',
markup_value: pkg.markup_value || 0,
+ icon_name: pkg.icon_name || '',
+ tagline: pkg.tagline || '',
+ is_featured: !!pkg.is_featured,
sort_order: pkg.sort_order,
});
};
@@ -139,7 +154,17 @@ export default function Packages() {
{filteredPackages.map((pkg) => (
| {pkg.brand_name} {pkg.model_name} |
- {pkg.name} |
+
+
+ {pkg.icon_name && }
+ {pkg.name}
+ {pkg.is_featured && (
+
+ Empfohlen
+
+ )}
+
+ |
{pkg.markup_type === 'fixed' && `+${pkg.markup_value} €`}
{pkg.markup_type === 'percent' && `+${pkg.markup_value} %`}
@@ -196,6 +221,28 @@ export default function Packages() {
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
/>
+
+
+ setForm({ ...form, icon_name: name })} />
+
+
+
+ setForm({ ...form, tagline: e.target.value })}
+ placeholder="z. B. Perfekt für den Einstieg"
+ className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
+ />
+
+
diff --git a/hifi-src/src/pages/public/ModelPage.jsx b/hifi-src/src/pages/public/ModelPage.jsx
index 63eab28..eb8a72e 100644
--- a/hifi-src/src/pages/public/ModelPage.jsx
+++ b/hifi-src/src/pages/public/ModelPage.jsx
@@ -4,6 +4,7 @@ import { api } from '../../api/client.js';
import usePageMeta from '../../hooks/usePageMeta.js';
import MaintenanceNotice from '../../components/MaintenanceNotice.jsx';
import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.jsx';
+import DynamicIcon from '../../components/DynamicIcon.jsx';
import { useMaintenance } from '../../context/MaintenanceContext.jsx';
import { useLanguage } from '../../context/LanguageContext.jsx';
@@ -70,18 +71,37 @@ export default function ModelPage() {
{t('modelPage.empty')}
)}
-
+
{packages.map((pkg) => (
-
-
- {pkg.name}
-
- {t('modelPage.totalPrice')}
- {formatPrice(pkg.total_price)}
+
+ {pkg.is_featured && (
+
+ {t('modelPage.featuredBadge')}
+
+ )}
+
+ {pkg.icon_name && (
+
+
+ )}
+
+ {pkg.name}
+ {pkg.tagline && {pkg.tagline} }
+
+
+ {t('modelPage.totalPrice')}
+ {formatPrice(pkg.total_price)}
-
+
{pkg.products.map((product) => (
-
{product.name_override || product.scraped_name || t('modelPage.productLoading')}
@@ -96,7 +116,9 @@ export default function ModelPage() {
{t('modelPage.requestContact')}
diff --git a/hifi/api/database/migration_package_design.sql b/hifi/api/database/migration_package_design.sql
new file mode 100644
index 0000000..54b2e40
--- /dev/null
+++ b/hifi/api/database/migration_package_design.sql
@@ -0,0 +1,9 @@
+-- Design-Felder pro Paket (Icon, Slogan, "Empfohlen"-Hervorhebung) fuer die
+-- Pakete-Karten auf der Modell-Seite. Additiv, nichts wird geloescht.
+-- Hinweis: Auf der Live-Seite reicht stattdessen ein Klick auf "Datenbankstruktur aktualisieren"
+-- unter Admin-Panel -> Einstellungen -> Datenbank.
+
+ALTER TABLE packages
+ ADD COLUMN IF NOT EXISTS icon_name VARCHAR(100) NULL,
+ ADD COLUMN IF NOT EXISTS tagline VARCHAR(150) NULL,
+ ADD COLUMN IF NOT EXISTS is_featured TINYINT(1) NOT NULL DEFAULT 0;
diff --git a/hifi/api/database/schema.sql b/hifi/api/database/schema.sql
index 2586a17..e3ae055 100644
--- a/hifi/api/database/schema.sql
+++ b/hifi/api/database/schema.sql
@@ -147,6 +147,9 @@ CREATE TABLE packages (
description TEXT NULL,
markup_type ENUM('none','fixed','percent') NOT NULL DEFAULT 'none',
markup_value DECIMAL(10,2) NOT NULL DEFAULT 0,
+ icon_name VARCHAR(100) NULL,
+ tagline VARCHAR(150) NULL,
+ is_featured TINYINT(1) NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
diff --git a/hifi/api/src/Controllers/ModelController.php b/hifi/api/src/Controllers/ModelController.php
index 40af475..26569ab 100644
--- a/hifi/api/src/Controllers/ModelController.php
+++ b/hifi/api/src/Controllers/ModelController.php
@@ -90,7 +90,7 @@ class ModelController
}
$pkgStmt = $db->prepare(
- 'SELECT id, name, slug, description, markup_type, markup_value, sort_order
+ 'SELECT id, name, slug, description, markup_type, markup_value, icon_name, tagline, is_featured, sort_order
FROM packages WHERE car_model_id = ? ORDER BY sort_order, name'
);
$pkgStmt->execute([$model['id']]);
@@ -125,6 +125,7 @@ class ModelController
unset($package['markup_type'], $package['markup_value']);
$package['products'] = $products;
$package['total_price'] = round($total, 2);
+ $package['is_featured'] = (bool) $package['is_featured'];
}
unset($package);
}
diff --git a/hifi/api/src/Controllers/PackageController.php b/hifi/api/src/Controllers/PackageController.php
index 9742f06..19d08b6 100644
--- a/hifi/api/src/Controllers/PackageController.php
+++ b/hifi/api/src/Controllers/PackageController.php
@@ -22,14 +22,19 @@ class PackageController
public static function index(): void
{
$stmt = Database::connection()->query(
- 'SELECT p.id, p.name, p.slug, p.description, p.markup_type, p.markup_value, p.sort_order, p.car_model_id,
- m.name AS model_name, b.name AS brand_name
+ 'SELECT p.id, p.name, p.slug, p.description, p.markup_type, p.markup_value, p.icon_name, p.tagline,
+ p.is_featured, p.sort_order, p.car_model_id, m.name AS model_name, b.name AS brand_name
FROM packages p
JOIN car_models m ON m.id = p.car_model_id
JOIN brands b ON b.id = m.brand_id
ORDER BY b.name, m.name, p.sort_order, p.name'
);
- Http::send($stmt->fetchAll());
+ $packages = $stmt->fetchAll();
+ foreach ($packages as &$package) {
+ $package['is_featured'] = (bool) $package['is_featured'];
+ }
+ unset($package);
+ Http::send($packages);
}
public static function store(): void
@@ -46,8 +51,8 @@ class PackageController
$db = Database::connection();
$stmt = $db->prepare(
- 'INSERT INTO packages (car_model_id, name, slug, description, markup_type, markup_value, sort_order)
- VALUES (?, ?, ?, ?, ?, ?, ?)'
+ 'INSERT INTO packages (car_model_id, name, slug, description, markup_type, markup_value, icon_name, tagline, is_featured, sort_order)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$modelId,
@@ -56,6 +61,9 @@ class PackageController
$body['description'] ?? null,
$markupType,
$markupValue,
+ trim($body['icon_name'] ?? '') ?: null,
+ trim($body['tagline'] ?? '') ?: null,
+ !empty($body['is_featured']) ? 1 : 0,
(int) ($body['sort_order'] ?? 0),
]);
@@ -76,7 +84,7 @@ class PackageController
[$markupType, $markupValue] = self::normalizeMarkup($body);
$stmt = Database::connection()->prepare(
- 'UPDATE packages SET car_model_id = ?, name = ?, slug = ?, description = ?, markup_type = ?, markup_value = ?, sort_order = ? WHERE id = ?'
+ 'UPDATE packages SET car_model_id = ?, name = ?, slug = ?, description = ?, markup_type = ?, markup_value = ?, icon_name = ?, tagline = ?, is_featured = ?, sort_order = ? WHERE id = ?'
);
$stmt->execute([
$modelId,
@@ -85,6 +93,9 @@ class PackageController
$body['description'] ?? null,
$markupType,
$markupValue,
+ trim($body['icon_name'] ?? '') ?: null,
+ trim($body['tagline'] ?? '') ?: null,
+ !empty($body['is_featured']) ? 1 : 0,
(int) ($body['sort_order'] ?? 0),
$id,
]);
diff --git a/hifi/api/src/Controllers/SettingsController.php b/hifi/api/src/Controllers/SettingsController.php
index 66c5fa6..f86bbf7 100644
--- a/hifi/api/src/Controllers/SettingsController.php
+++ b/hifi/api/src/Controllers/SettingsController.php
@@ -16,7 +16,7 @@ class SettingsController
private const TABLE_COLUMNS = [
'brands' => ['id', 'name', 'slug', 'sort_order', 'created_at', 'updated_at'],
'car_models' => ['id', 'brand_id', 'name', 'slug', 'sort_order', 'created_at', 'updated_at'],
- 'packages' => ['id', 'car_model_id', 'name', 'slug', 'description', 'markup_type', 'markup_value', 'sort_order', 'created_at', 'updated_at'],
+ 'packages' => ['id', 'car_model_id', 'name', 'slug', 'description', 'markup_type', 'markup_value', 'icon_name', 'tagline', 'is_featured', 'sort_order', 'created_at', 'updated_at'],
'package_products' => ['id', 'package_id', 'source_url', 'name_override', 'scraped_name', 'scraped_price', 'scraped_currency', 'scraped_image_url', 'price_updated_at', 'scrape_status', 'scrape_error', 'sort_order', 'created_at', 'updated_at'],
'package_upgrades' => ['id', 'package_id', 'name', 'description', 'price', 'sort_order', 'created_at', 'updated_at'],
'services' => ['id', 'icon_name', 'title', 'description', 'image_path', 'cta_label', 'cta_url', 'sort_order', 'created_at', 'updated_at'],
diff --git a/hifi/api/src/Support/Schema.php b/hifi/api/src/Support/Schema.php
index 0dc3d84..ef8afed 100644
--- a/hifi/api/src/Support/Schema.php
+++ b/hifi/api/src/Support/Schema.php
@@ -158,6 +158,9 @@ class Schema
description TEXT NULL,
markup_type ENUM('none','fixed','percent') NOT NULL DEFAULT 'none',
markup_value DECIMAL(10,2) NOT NULL DEFAULT 0,
+ icon_name VARCHAR(100) NULL,
+ tagline VARCHAR(150) NULL,
+ is_featured TINYINT(1) NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -355,6 +358,9 @@ class Schema
'description' => 'TEXT NULL',
'markup_type' => "ENUM('none','fixed','percent') NOT NULL DEFAULT 'none'",
'markup_value' => 'DECIMAL(10,2) NOT NULL DEFAULT 0',
+ 'icon_name' => 'VARCHAR(100) NULL',
+ 'tagline' => 'VARCHAR(150) NULL',
+ 'is_featured' => 'TINYINT(1) NOT NULL DEFAULT 0',
'sort_order' => 'INT NOT NULL DEFAULT 0',
'created_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP',
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|