Make package cards configurable and give them a proper tiered design
Packages now support an optional icon, a short tagline, and an "empfohlen" flag, all editable in Admin > Pakete. The model page switches from a plain stacked list to a responsive grid where the featured package gets a highlighted border, badge, and bolder CTA — fixes the tiers looking inconsistent when only some packages had bullet content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6a127b70ce
commit
9e417d0f04
10 changed files with 120 additions and 19 deletions
|
|
@ -154,6 +154,7 @@ export default {
|
||||||
totalPrice: 'Gesamtpreis (ca.)',
|
totalPrice: 'Gesamtpreis (ca.)',
|
||||||
productLoading: 'Produkt wird geladen…',
|
productLoading: 'Produkt wird geladen…',
|
||||||
requestContact: 'Kontakt anfragen',
|
requestContact: 'Kontakt anfragen',
|
||||||
|
featuredBadge: 'Empfohlen',
|
||||||
},
|
},
|
||||||
leistungen: {
|
leistungen: {
|
||||||
metaTitle: 'Leistungen',
|
metaTitle: 'Leistungen',
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ export default {
|
||||||
totalPrice: 'Total price (approx.)',
|
totalPrice: 'Total price (approx.)',
|
||||||
productLoading: 'Loading product…',
|
productLoading: 'Loading product…',
|
||||||
requestContact: 'Request contact',
|
requestContact: 'Request contact',
|
||||||
|
featuredBadge: 'Recommended',
|
||||||
},
|
},
|
||||||
leistungen: {
|
leistungen: {
|
||||||
metaTitle: 'Services',
|
metaTitle: 'Services',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,20 @@
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { api } from '../../api/client.js';
|
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() {
|
export default function Packages() {
|
||||||
const [packages, setPackages] = useState([]);
|
const [packages, setPackages] = useState([]);
|
||||||
|
|
@ -62,6 +74,9 @@ export default function Packages() {
|
||||||
description: pkg.description || '',
|
description: pkg.description || '',
|
||||||
markup_type: pkg.markup_type || 'none',
|
markup_type: pkg.markup_type || 'none',
|
||||||
markup_value: pkg.markup_value || 0,
|
markup_value: pkg.markup_value || 0,
|
||||||
|
icon_name: pkg.icon_name || '',
|
||||||
|
tagline: pkg.tagline || '',
|
||||||
|
is_featured: !!pkg.is_featured,
|
||||||
sort_order: pkg.sort_order,
|
sort_order: pkg.sort_order,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -139,7 +154,17 @@ export default function Packages() {
|
||||||
{filteredPackages.map((pkg) => (
|
{filteredPackages.map((pkg) => (
|
||||||
<tr key={pkg.id} className="bg-white dark:bg-neutral-950">
|
<tr key={pkg.id} className="bg-white dark:bg-neutral-950">
|
||||||
<td className="px-4 py-2">{pkg.brand_name} {pkg.model_name}</td>
|
<td className="px-4 py-2">{pkg.brand_name} {pkg.model_name}</td>
|
||||||
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">{pkg.name}</td>
|
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
{pkg.icon_name && <DynamicIcon name={pkg.icon_name} className="h-4 w-4 text-brand-600 dark:text-brand-400" />}
|
||||||
|
{pkg.name}
|
||||||
|
{pkg.is_featured && (
|
||||||
|
<span className="rounded-full bg-brand-100 px-2 py-0.5 text-xs font-semibold text-brand-700 dark:bg-brand-900/40 dark:text-brand-400">
|
||||||
|
Empfohlen
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-neutral-500 dark:text-neutral-400">
|
<td className="px-4 py-2 text-neutral-500 dark:text-neutral-400">
|
||||||
{pkg.markup_type === 'fixed' && `+${pkg.markup_value} €`}
|
{pkg.markup_type === 'fixed' && `+${pkg.markup_value} €`}
|
||||||
{pkg.markup_type === 'percent' && `+${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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Icon (optional)</label>
|
||||||
|
<IconPicker value={form.icon_name} onChange={(name) => setForm({ ...form, icon_name: name })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Kurzer Slogan (optional)</label>
|
||||||
|
<input
|
||||||
|
value={form.tagline}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.is_featured}
|
||||||
|
onChange={(e) => setForm({ ...form, is_featured: e.target.checked })}
|
||||||
|
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
Als empfohlen hervorheben
|
||||||
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Aufschlag</label>
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Aufschlag</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { api } from '../../api/client.js';
|
||||||
import usePageMeta from '../../hooks/usePageMeta.js';
|
import usePageMeta from '../../hooks/usePageMeta.js';
|
||||||
import MaintenanceNotice from '../../components/MaintenanceNotice.jsx';
|
import MaintenanceNotice from '../../components/MaintenanceNotice.jsx';
|
||||||
import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.jsx';
|
import MaintenanceBypassBanner from '../../components/MaintenanceBypassBanner.jsx';
|
||||||
|
import DynamicIcon from '../../components/DynamicIcon.jsx';
|
||||||
import { useMaintenance } from '../../context/MaintenanceContext.jsx';
|
import { useMaintenance } from '../../context/MaintenanceContext.jsx';
|
||||||
import { useLanguage } from '../../context/LanguageContext.jsx';
|
import { useLanguage } from '../../context/LanguageContext.jsx';
|
||||||
|
|
||||||
|
|
@ -70,18 +71,37 @@ export default function ModelPage() {
|
||||||
<p className="text-neutral-500 dark:text-neutral-400">{t('modelPage.empty')}</p>
|
<p className="text-neutral-500 dark:text-neutral-400">{t('modelPage.empty')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{packages.map((pkg) => (
|
{packages.map((pkg) => (
|
||||||
<div key={pkg.id} className="rounded-xl border border-neutral-200 bg-white p-6 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
|
<div
|
||||||
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
|
key={pkg.id}
|
||||||
<h2 className="text-lg font-bold text-neutral-900 dark:text-white">{pkg.name}</h2>
|
className={`relative flex flex-col rounded-xl border bg-white p-6 shadow-sm dark:bg-neutral-900 ${
|
||||||
<div className="text-right">
|
pkg.is_featured
|
||||||
<p className="text-xs uppercase tracking-wide text-neutral-400">{t('modelPage.totalPrice')}</p>
|
? 'border-brand-500 ring-2 ring-brand-500/50 dark:border-brand-400 dark:ring-brand-400/40'
|
||||||
<p className="text-xl font-extrabold text-brand-600 dark:text-brand-400">{formatPrice(pkg.total_price)}</p>
|
: 'border-neutral-200 dark:border-neutral-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pkg.is_featured && (
|
||||||
|
<span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-brand-500 px-3 py-1 text-xs font-bold text-white shadow">
|
||||||
|
{t('modelPage.featuredBadge')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pkg.icon_name && (
|
||||||
|
<div className="mb-3 flex h-11 w-11 items-center justify-center rounded-full bg-brand-100 text-brand-600 dark:bg-brand-900/40 dark:text-brand-400">
|
||||||
|
<DynamicIcon name={pkg.icon_name} className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 className="text-lg font-bold text-neutral-900 dark:text-white">{pkg.name}</h2>
|
||||||
|
{pkg.tagline && <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">{pkg.tagline}</p>}
|
||||||
|
|
||||||
|
<div className="my-4">
|
||||||
|
<p className="text-xs uppercase tracking-wide text-neutral-400">{t('modelPage.totalPrice')}</p>
|
||||||
|
<p className="text-2xl font-extrabold text-brand-600 dark:text-brand-400">{formatPrice(pkg.total_price)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="mb-5 list-disc space-y-1 pl-5 text-sm text-neutral-700 dark:text-neutral-300">
|
<ul className="mb-5 flex-1 list-disc space-y-1 pl-5 text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
{pkg.products.map((product) => (
|
{pkg.products.map((product) => (
|
||||||
<li key={product.id}>
|
<li key={product.id}>
|
||||||
{product.name_override || product.scraped_name || t('modelPage.productLoading')}
|
{product.name_override || product.scraped_name || t('modelPage.productLoading')}
|
||||||
|
|
@ -96,7 +116,9 @@ export default function ModelPage() {
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={contactUrl(pkg)}
|
to={contactUrl(pkg)}
|
||||||
className="inline-block rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600"
|
className={`inline-block rounded-md px-4 py-2 text-center text-sm font-semibold text-white ${
|
||||||
|
pkg.is_featured ? 'bg-brand-600 hover:bg-brand-700' : 'bg-brand-500 hover:bg-brand-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{t('modelPage.requestContact')}
|
{t('modelPage.requestContact')}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
9
hifi/api/database/migration_package_design.sql
Normal file
9
hifi/api/database/migration_package_design.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -147,6 +147,9 @@ CREATE TABLE packages (
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
markup_type ENUM('none','fixed','percent') NOT NULL DEFAULT 'none',
|
markup_type ENUM('none','fixed','percent') NOT NULL DEFAULT 'none',
|
||||||
markup_value DECIMAL(10,2) NOT NULL DEFAULT 0,
|
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,
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class ModelController
|
||||||
}
|
}
|
||||||
|
|
||||||
$pkgStmt = $db->prepare(
|
$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'
|
FROM packages WHERE car_model_id = ? ORDER BY sort_order, name'
|
||||||
);
|
);
|
||||||
$pkgStmt->execute([$model['id']]);
|
$pkgStmt->execute([$model['id']]);
|
||||||
|
|
@ -125,6 +125,7 @@ class ModelController
|
||||||
unset($package['markup_type'], $package['markup_value']);
|
unset($package['markup_type'], $package['markup_value']);
|
||||||
$package['products'] = $products;
|
$package['products'] = $products;
|
||||||
$package['total_price'] = round($total, 2);
|
$package['total_price'] = round($total, 2);
|
||||||
|
$package['is_featured'] = (bool) $package['is_featured'];
|
||||||
}
|
}
|
||||||
unset($package);
|
unset($package);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,19 @@ class PackageController
|
||||||
public static function index(): void
|
public static function index(): void
|
||||||
{
|
{
|
||||||
$stmt = Database::connection()->query(
|
$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,
|
'SELECT p.id, p.name, p.slug, p.description, p.markup_type, p.markup_value, p.icon_name, p.tagline,
|
||||||
m.name AS model_name, b.name AS brand_name
|
p.is_featured, p.sort_order, p.car_model_id, m.name AS model_name, b.name AS brand_name
|
||||||
FROM packages p
|
FROM packages p
|
||||||
JOIN car_models m ON m.id = p.car_model_id
|
JOIN car_models m ON m.id = p.car_model_id
|
||||||
JOIN brands b ON b.id = m.brand_id
|
JOIN brands b ON b.id = m.brand_id
|
||||||
ORDER BY b.name, m.name, p.sort_order, p.name'
|
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
|
public static function store(): void
|
||||||
|
|
@ -46,8 +51,8 @@ class PackageController
|
||||||
|
|
||||||
$db = Database::connection();
|
$db = Database::connection();
|
||||||
$stmt = $db->prepare(
|
$stmt = $db->prepare(
|
||||||
'INSERT INTO packages (car_model_id, name, slug, description, markup_type, markup_value, sort_order)
|
'INSERT INTO packages (car_model_id, name, slug, description, markup_type, markup_value, icon_name, tagline, is_featured, sort_order)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$modelId,
|
$modelId,
|
||||||
|
|
@ -56,6 +61,9 @@ class PackageController
|
||||||
$body['description'] ?? null,
|
$body['description'] ?? null,
|
||||||
$markupType,
|
$markupType,
|
||||||
$markupValue,
|
$markupValue,
|
||||||
|
trim($body['icon_name'] ?? '') ?: null,
|
||||||
|
trim($body['tagline'] ?? '') ?: null,
|
||||||
|
!empty($body['is_featured']) ? 1 : 0,
|
||||||
(int) ($body['sort_order'] ?? 0),
|
(int) ($body['sort_order'] ?? 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -76,7 +84,7 @@ class PackageController
|
||||||
[$markupType, $markupValue] = self::normalizeMarkup($body);
|
[$markupType, $markupValue] = self::normalizeMarkup($body);
|
||||||
|
|
||||||
$stmt = Database::connection()->prepare(
|
$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([
|
$stmt->execute([
|
||||||
$modelId,
|
$modelId,
|
||||||
|
|
@ -85,6 +93,9 @@ class PackageController
|
||||||
$body['description'] ?? null,
|
$body['description'] ?? null,
|
||||||
$markupType,
|
$markupType,
|
||||||
$markupValue,
|
$markupValue,
|
||||||
|
trim($body['icon_name'] ?? '') ?: null,
|
||||||
|
trim($body['tagline'] ?? '') ?: null,
|
||||||
|
!empty($body['is_featured']) ? 1 : 0,
|
||||||
(int) ($body['sort_order'] ?? 0),
|
(int) ($body['sort_order'] ?? 0),
|
||||||
$id,
|
$id,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class SettingsController
|
||||||
private const TABLE_COLUMNS = [
|
private const TABLE_COLUMNS = [
|
||||||
'brands' => ['id', 'name', 'slug', 'sort_order', 'created_at', 'updated_at'],
|
'brands' => ['id', 'name', 'slug', 'sort_order', 'created_at', 'updated_at'],
|
||||||
'car_models' => ['id', 'brand_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_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'],
|
'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'],
|
'services' => ['id', 'icon_name', 'title', 'description', 'image_path', 'cta_label', 'cta_url', 'sort_order', 'created_at', 'updated_at'],
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,9 @@ class Schema
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
markup_type ENUM('none','fixed','percent') NOT NULL DEFAULT 'none',
|
markup_type ENUM('none','fixed','percent') NOT NULL DEFAULT 'none',
|
||||||
markup_value DECIMAL(10,2) NOT NULL DEFAULT 0,
|
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,
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
@ -355,6 +358,9 @@ class Schema
|
||||||
'description' => 'TEXT NULL',
|
'description' => 'TEXT NULL',
|
||||||
'markup_type' => "ENUM('none','fixed','percent') NOT NULL DEFAULT 'none'",
|
'markup_type' => "ENUM('none','fixed','percent') NOT NULL DEFAULT 'none'",
|
||||||
'markup_value' => 'DECIMAL(10,2) NOT NULL DEFAULT 0',
|
'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',
|
'sort_order' => 'INT NOT NULL DEFAULT 0',
|
||||||
'created_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
'created_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
||||||
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue