diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 094da1f..a88d479 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,20 +1,20 @@ -import { useState } from 'react'; -import { OverviewDemo } from './sections/OverviewDemo'; -import { PerUnitDemo } from './sections/PerUnitDemo'; -import { TieredVolumeDemo } from './sections/TieredVolumeDemo'; -import { TieredGraduatedDemo } from './sections/TieredGraduatedDemo'; -import { TieredFlatFeeDemo } from './sections/TieredFlatFeeDemo'; -import { TaxDemo } from './sections/TaxDemo'; -import { DiscountDemo } from './sections/DiscountDemo'; +import { useState, useEffect, useCallback } from 'react'; import { CompositePriceDemo } from './sections/CompositePriceDemo'; -import { RecurringBillingDemo } from './sections/RecurringBillingDemo'; import { CurrencyDemo } from './sections/CurrencyDemo'; +import { DiscountDemo } from './sections/DiscountDemo'; import { DynamicTariffDemo } from './sections/DynamicTariffDemo'; -import { GetAGDemo } from './sections/GetAGDemo'; import { ElectricityDemo } from './sections/ElectricityDemo'; import { GasDemo } from './sections/GasDemo'; +import { GetAGDemo } from './sections/GetAGDemo'; import { HouseConnectionDemo } from './sections/HouseConnectionDemo'; import { NonCommodityDemo } from './sections/NonCommodityDemo'; +import { OverviewDemo } from './sections/OverviewDemo'; +import { PerUnitDemo } from './sections/PerUnitDemo'; +import { RecurringBillingDemo } from './sections/RecurringBillingDemo'; +import { TaxDemo } from './sections/TaxDemo'; +import { TieredFlatFeeDemo } from './sections/TieredFlatFeeDemo'; +import { TieredGraduatedDemo } from './sections/TieredGraduatedDemo'; +import { TieredVolumeDemo } from './sections/TieredVolumeDemo'; type SectionItem = { id: string; @@ -35,30 +35,30 @@ function isGroup(s: Section): s is SectionGroup { } const sections: Section[] = [ - { id: 'overview', label: 'Overview', icon: '\uD83C\uDFE0', component: OverviewDemo }, + { id: 'overview', label: 'Overview', icon: '๐Ÿ ', component: OverviewDemo }, { - group: 'Energy & Utility Use Cases', + group: 'Energy Products', items: [ - { id: 'electricity', label: 'Electricity', icon: '\u26A1', component: ElectricityDemo }, - { id: 'gas', label: 'Gas', icon: '\uD83D\uDD25', component: GasDemo }, - { id: 'house-connection', label: 'House Connection', icon: '\uD83C\uDFE1', component: HouseConnectionDemo }, - { id: 'non-commodity', label: 'Non-Commodity', icon: '\uD83D\uDCCB', component: NonCommodityDemo }, + { id: 'electricity', label: 'Electricity', icon: 'โšก', component: ElectricityDemo }, + { id: 'gas', label: 'Gas', icon: '๐Ÿ”ฅ', component: GasDemo }, + { id: 'house-connection', label: 'House Connection', icon: '๐Ÿก', component: HouseConnectionDemo }, + { id: 'non-commodity', label: 'Products & Add-ons', icon: 'โ˜€๏ธ', component: NonCommodityDemo }, ], }, { - group: 'Capabilities', + group: 'Pricing Models', items: [ - { id: 'per-unit', label: 'Per Unit', icon: '\uD83D\uDCE6', component: PerUnitDemo }, - { id: 'tiered-volume', label: 'Tiered Volume', icon: '\uD83D\uDCCA', component: TieredVolumeDemo }, - { id: 'tiered-graduated', label: 'Tiered Graduated', icon: '\uD83D\uDCC8', component: TieredGraduatedDemo }, - { id: 'tiered-flatfee', label: 'Tiered Flat Fee', icon: '\uD83C\uDFF7\uFE0F', component: TieredFlatFeeDemo }, - { id: 'tax', label: 'Tax Handling', icon: '\uD83E\uDDFE', component: TaxDemo }, - { id: 'discounts', label: 'Discounts & Coupons', icon: '\uD83C\uDF9F\uFE0F', component: DiscountDemo }, - { id: 'composite', label: 'Composite Pricing', icon: '\uD83E\uDDE9', component: CompositePriceDemo }, - { id: 'recurring', label: 'Recurring Billing', icon: '\uD83D\uDD04', component: RecurringBillingDemo }, - { id: 'currency', label: 'Currency & Formatting', icon: '\uD83D\uDCB1', component: CurrencyDemo }, - { id: 'dynamic-tariff', label: 'Dynamic Tariff', icon: '\u26A1', component: DynamicTariffDemo }, - { id: 'getag', label: 'GetAG Energy', icon: '\uD83D\uDD0C', component: GetAGDemo }, + { id: 'per-unit', label: 'Per Unit', icon: '๐Ÿ“ฆ', component: PerUnitDemo }, + { id: 'tiered-volume', label: 'Tiered Volume', icon: '๐Ÿ“Š', component: TieredVolumeDemo }, + { id: 'tiered-graduated', label: 'Tiered Graduated', icon: '๐Ÿ“ˆ', component: TieredGraduatedDemo }, + { id: 'tiered-flatfee', label: 'Tiered Flat Fee', icon: '๐Ÿท๏ธ', component: TieredFlatFeeDemo }, + { id: 'tax', label: 'Tax Handling', icon: '๐Ÿงพ', component: TaxDemo }, + { id: 'discounts', label: 'Discounts & Coupons', icon: '๐ŸŽŸ๏ธ', component: DiscountDemo }, + { id: 'composite', label: 'Composite Pricing', icon: '๐Ÿงฉ', component: CompositePriceDemo }, + { id: 'recurring', label: 'Recurring Billing', icon: '๐Ÿ”„', component: RecurringBillingDemo }, + { id: 'currency', label: 'Currency & Formatting', icon: '๐Ÿ’ฑ', component: CurrencyDemo }, + { id: 'dynamic-tariff', label: 'Dynamic Tariff', icon: 'โšก', component: DynamicTariffDemo }, + { id: 'getag', label: 'GetAG Energy', icon: '๐Ÿ”Œ', component: GetAGDemo }, ], }, ]; @@ -77,47 +77,75 @@ function getAllSections(): SectionItem[] { const allSections = getAllSections(); +const isMobile = () => window.matchMedia('(max-width: 767px)').matches; + export default function App() { const [activeSection, setActiveSection] = useState('overview'); - const [sidebarOpen, setSidebarOpen] = useState(true); + const [sidebarOpen, setSidebarOpen] = useState(() => !isMobile()); + + useEffect(() => { + const mq = window.matchMedia('(max-width: 767px)'); + const handler = (e: MediaQueryListEvent) => { + if (e.matches) setSidebarOpen(false); + }; + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, []); + + const navigate = useCallback((id: string) => { + setActiveSection(id); + if (isMobile()) setSidebarOpen(false); + }, []); const ActiveComponent = allSections.find((s) => s.id === activeSection)?.component ?? OverviewDemo; const activeItem = allSections.find((s) => s.id === activeSection); return ( -
+
{/* Sidebar */} {/* Main content */}
-
+
-

- {activeItem?.icon} {activeItem?.label} -

+
+ {activeItem?.icon} +

{activeItem?.label}

+
-
- +
+
diff --git a/demo/src/components/CodeBlock.tsx b/demo/src/components/CodeBlock.tsx index d50dc38..6b8bf20 100644 --- a/demo/src/components/CodeBlock.tsx +++ b/demo/src/components/CodeBlock.tsx @@ -12,16 +12,63 @@ interface Token { } const JS_KEYWORDS = new Set([ - 'import', 'from', 'export', 'default', 'const', 'let', 'var', 'function', - 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', - 'continue', 'new', 'this', 'class', 'extends', 'async', 'await', 'try', - 'catch', 'throw', 'typeof', 'instanceof', 'in', 'of', 'true', 'false', - 'null', 'undefined', 'void', 'type', 'interface', 'enum', 'as', + 'import', + 'from', + 'export', + 'default', + 'const', + 'let', + 'var', + 'function', + 'return', + 'if', + 'else', + 'for', + 'while', + 'do', + 'switch', + 'case', + 'break', + 'continue', + 'new', + 'this', + 'class', + 'extends', + 'async', + 'await', + 'try', + 'catch', + 'throw', + 'typeof', + 'instanceof', + 'in', + 'of', + 'true', + 'false', + 'null', + 'undefined', + 'void', + 'type', + 'interface', + 'enum', + 'as', ]); const BUILTIN = new Set([ - 'console', 'Math', 'JSON', 'Array', 'Object', 'String', 'Number', - 'Boolean', 'Promise', 'Map', 'Set', 'Date', 'Error', 'RegExp', + 'console', + 'Math', + 'JSON', + 'Array', + 'Object', + 'String', + 'Number', + 'Boolean', + 'Promise', + 'Map', + 'Set', + 'Date', + 'Error', + 'RegExp', ]); function tokenize(code: string): Token[] { @@ -137,7 +184,11 @@ export function CodeBlock({ code, title, language = 'typescript' }: CodeBlockPro // Simple bash highlighting: just color comments and strings return code.split('\n').map((line, i) => { if (line.trimStart().startsWith('#')) { - return
{line}
; + return ( +
+ {line} +
+ ); } return
{line}
; }); @@ -145,7 +196,9 @@ export function CodeBlock({ code, title, language = 'typescript' }: CodeBlockPro const tokens = tokenize(code); return tokens.map((token, i) => ( - {token.value} + + {token.value} + )); }, [code, language]); diff --git a/demo/src/components/ProductShowcase.tsx b/demo/src/components/ProductShowcase.tsx new file mode 100644 index 0000000..232fac5 --- /dev/null +++ b/demo/src/components/ProductShowcase.tsx @@ -0,0 +1,274 @@ +interface ProductShowcaseProps { + icon: React.ReactNode; + gradient: string; + title: string; + description: string; + price: string; + priceLabel: string; + features: string[]; + tag?: string; + tagColor?: string; + selected?: boolean; + onToggle?: () => void; +} + +// SVG illustrations for product categories +export function SolarIllustration() { + return ( + + {/* Sky */} + + {/* Sun */} + + + {/* Sun rays */} + {[0, 45, 90, 135, 180, 225, 270, 315].map((angle) => ( + + ))} + {/* House */} + + + {/* Solar panels on roof */} + + + + + + + {/* Battery */} + + + + + {/* Lightning bolt */} + + {/* Ground */} + + + + + + + + + ); +} + +export function WallboxIllustration() { + return ( + + + {/* Garage wall */} + + + {/* Wallbox unit */} + + + + + {/* Charging cable */} + + {/* Car */} + + + {/* Windows */} + + + {/* Wheels */} + + + + + {/* Charging plug connector */} + + {/* LED indicator */} + + + + + + + + + + ); +} + +export function HeatPumpIllustration() { + return ( + + + {/* House silhouette */} + + + {/* Window */} + + + + {/* Door */} + + {/* Heat pump outdoor unit */} + + + {/* Fan grille */} + + + + + {/* Connection pipes */} + + + {/* Heat waves */} + + + + {/* Temperature indicator */} + + + {/* Ground */} + + + + + + + + + ); +} + +export function SmartHomeIllustration() { + return ( + + + {/* House */} + + + {/* Smart windows with glow */} + + + {/* Door */} + + + {/* WiFi symbol above house */} + + + + {/* Thermostat icon */} + + + + 22ยฐ + + {/* Energy meter */} + + + + 3.2kW + + {/* Connection lines */} + + + {/* Ground */} + + + + + + + + + ); +} + +const illustrations: Record = { + solar: SolarIllustration, + emobility: WallboxIllustration, + heating: HeatPumpIllustration, + smarthome: SmartHomeIllustration, +}; + +export function ProductShowcase({ + icon, + gradient, + title, + description, + price, + priceLabel, + features, + tag, + tagColor = 'bg-primary-100 text-primary-700', + selected, + onToggle, +}: ProductShowcaseProps) { + const Illustration = illustrations[gradient] || SolarIllustration; + + return ( +
+
+ + {tag && ( + + {tag} + + )} + {selected !== undefined && ( +
+ {selected ? 'โœ“' : ''} +
+ )} +
+
+
+ {icon} +

{title}

+
+

{description}

+
+ {price} + {priceLabel} +
+
+ {features.map((f, i) => ( +
+ + + + {f} +
+ ))} +
+
+
+ ); +} diff --git a/demo/src/components/ResultCard.tsx b/demo/src/components/ResultCard.tsx index bd4781a..0c54f20 100644 --- a/demo/src/components/ResultCard.tsx +++ b/demo/src/components/ResultCard.tsx @@ -4,24 +4,36 @@ interface ResultCardProps { sublabel?: string; highlight?: boolean; color?: 'default' | 'green' | 'red' | 'blue' | 'amber'; + icon?: React.ReactNode; + large?: boolean; } const colorMap = { - default: 'text-gray-900', - green: 'text-green-600', - red: 'text-red-600', - blue: 'text-primary-600', - amber: 'text-amber-600', + default: { text: 'text-gray-900', bg: 'bg-gray-50', border: 'border-gray-100' }, + green: { text: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-100' }, + red: { text: 'text-red-600', bg: 'bg-red-50', border: 'border-red-100' }, + blue: { text: 'text-primary-600', bg: 'bg-blue-50', border: 'border-blue-100' }, + amber: { text: 'text-amber-600', bg: 'bg-amber-50', border: 'border-amber-100' }, }; -export function ResultCard({ label, value, sublabel, highlight, color = 'default' }: ResultCardProps) { +export function ResultCard({ label, value, sublabel, highlight, color = 'default', icon, large }: ResultCardProps) { + const colors = colorMap[color]; return (
-

{label}

-

{value}

- {sublabel &&

{sublabel}

} +
+ {icon && {icon}} +

{label}

+
+

+ {value} +

+ {sublabel &&

{sublabel}

}
); } diff --git a/demo/src/components/TariffCard.tsx b/demo/src/components/TariffCard.tsx new file mode 100644 index 0000000..27c1619 --- /dev/null +++ b/demo/src/components/TariffCard.tsx @@ -0,0 +1,57 @@ +interface TariffCardProps { + gradient: string; + icon: React.ReactNode; + title: string; + subtitle?: string; + badge?: string; + price: string; + priceUnit: string; + priceLabel?: string; + children?: React.ReactNode; + footer?: React.ReactNode; +} + +export function TariffCard({ + gradient, + icon, + title, + subtitle, + badge, + price, + priceUnit, + priceLabel, + children, + footer, +}: TariffCardProps) { + return ( +
+
+
+
+
+ {icon} +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {badge && ( + + {badge} + + )} +
+
+ {priceLabel &&

{priceLabel}

} +
+ {price} + {priceUnit} +
+
+
+ {children &&
{children}
} + {footer &&
{footer}
} +
+ ); +} diff --git a/demo/src/components/TierChart.tsx b/demo/src/components/TierChart.tsx index f8aa109..25f52a0 100644 --- a/demo/src/components/TierChart.tsx +++ b/demo/src/components/TierChart.tsx @@ -25,16 +25,12 @@ export function TierChart({ bars, title, valueFormatter = (v) => v.toFixed(2) }: {valueFormatter(bar.value)}
{bar.label} - {bar.sublabel && ( - {bar.sublabel} - )} + {bar.sublabel && {bar.sublabel}}
); })} diff --git a/demo/src/index.css b/demo/src/index.css index 86b5ad9..20e379f 100644 --- a/demo/src/index.css +++ b/demo/src/index.css @@ -5,50 +5,162 @@ @layer base { html { scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + body { + @apply bg-gray-50 text-gray-900; } } @layer components { + /* โ”€โ”€ Cards โ”€โ”€ */ .card { - @apply bg-white rounded-xl shadow-sm border border-gray-200 p-6; + @apply bg-white rounded-2xl shadow-sm border border-gray-100 p-6; + } + + /* โ”€โ”€ Tariff Card (utility bill style) โ”€โ”€ */ + .tariff-card { + @apply bg-white rounded-2xl shadow-lg overflow-hidden border border-gray-100; + } + .tariff-card-header { + @apply px-6 py-5 text-white; + } + .tariff-card-body { + @apply px-6 py-5; + } + .tariff-card-footer { + @apply px-6 py-4 bg-gray-50 border-t border-gray-100; + } + .tariff-card-price { + @apply text-4xl font-extrabold tracking-tight; + } + .tariff-card-price-unit { + @apply text-lg font-normal opacity-80 ml-1; + } + .tariff-card-label { + @apply text-xs font-semibold uppercase tracking-widest opacity-70; } + + /* โ”€โ”€ Product Showcase Card โ”€โ”€ */ + .showcase-card { + @apply bg-white rounded-2xl shadow-md border border-gray-100 overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1; + } + .showcase-card-image { + @apply w-full h-48 object-cover flex items-center justify-center; + } + .showcase-card-body { + @apply p-5; + } + .showcase-card-price { + @apply text-2xl font-extrabold text-gray-900; + } + + /* โ”€โ”€ Forms โ”€โ”€ */ .input-field { - @apply w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors; + @apply w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm bg-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-all; } .select-field { - @apply w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors; + @apply w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm bg-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-all; } + + /* โ”€โ”€ Buttons โ”€โ”€ */ .btn-primary { - @apply px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 transition-colors; + @apply px-5 py-2.5 bg-primary-600 text-white rounded-xl text-sm font-semibold hover:bg-primary-700 active:bg-primary-800 transition-all shadow-sm hover:shadow-md; } .btn-secondary { - @apply px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors; + @apply px-5 py-2.5 bg-white text-gray-700 rounded-xl text-sm font-semibold hover:bg-gray-50 transition-all border border-gray-200 shadow-sm; } + + /* โ”€โ”€ Result Values โ”€โ”€ */ .result-value { - @apply text-2xl font-bold text-gray-900; + @apply text-2xl font-extrabold text-gray-900; } .result-label { - @apply text-xs font-medium text-gray-500 uppercase tracking-wider; + @apply text-[11px] font-bold text-gray-400 uppercase tracking-widest; } + + /* โ”€โ”€ Badges โ”€โ”€ */ .badge { - @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; + @apply inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold; } .badge-blue { - @apply badge bg-blue-100 text-blue-800; + @apply badge bg-blue-50 text-blue-700; } .badge-green { - @apply badge bg-green-100 text-green-800; + @apply badge bg-emerald-50 text-emerald-700; } .badge-amber { - @apply badge bg-amber-100 text-amber-800; + @apply badge bg-amber-50 text-amber-700; } .badge-red { - @apply badge bg-red-100 text-red-800; + @apply badge bg-red-50 text-red-700; } + + /* โ”€โ”€ Section Typography โ”€โ”€ */ .section-title { - @apply text-2xl font-bold text-gray-900 mb-2; + @apply text-3xl font-extrabold text-gray-900 mb-2 tracking-tight; } .section-desc { - @apply text-gray-500 mb-6; + @apply text-gray-500 mb-8 text-base leading-relaxed max-w-2xl; + } + + /* โ”€โ”€ Cost Line Item โ”€โ”€ */ + .cost-line { + @apply flex items-center justify-between py-3 border-b border-gray-100 last:border-0; + } + .cost-line-label { + @apply text-sm text-gray-600; + } + .cost-line-value { + @apply text-sm font-bold text-gray-900 tabular-nums; + } + + /* โ”€โ”€ Gradient backgrounds for tariff headers โ”€โ”€ */ + .gradient-electricity { + background: linear-gradient(135deg, #f59e0b 0%, #eab308 50%, #d97706 100%); + } + .gradient-gas { + background: linear-gradient(135deg, #f97316 0%, #ea580c 50%, #dc2626 100%); + } + .gradient-solar { + background: linear-gradient(135deg, #eab308 0%, #f59e0b 50%, #f97316 100%); + } + .gradient-emobility { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 50%, #1d4ed8 100%); + } + .gradient-heating { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 50%, #b91c1c 100%); + } + .gradient-smarthome { + background: linear-gradient(135deg, #10b981 0%, #059669 50%, #047857 100%); + } + .gradient-primary { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 50%, #1d4ed8 100%); + } + .gradient-house { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%); + } + + /* โ”€โ”€ Animated shimmer for loading states โ”€โ”€ */ + @keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } + } + .shimmer { + background: linear-gradient(90deg, transparent 25%, rgba(255,255,255,0.15) 50%, transparent 75%); + background-size: 200% 100%; + animation: shimmer 2s infinite; + } + + /* โ”€โ”€ Slider styling โ”€โ”€ */ + input[type="range"] { + @apply h-2 cursor-pointer; + } + + /* โ”€โ”€ Stat pill โ”€โ”€ */ + .stat-pill { + @apply inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold; } } diff --git a/demo/src/sections/CompositePriceDemo.tsx b/demo/src/sections/CompositePriceDemo.tsx index d23b925..bad660d 100644 --- a/demo/src/sections/CompositePriceDemo.tsx +++ b/demo/src/sections/CompositePriceDemo.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { ResultCard } from '../components/ResultCard'; import { fmtCents, makeTax } from '../helpers'; interface Component { @@ -106,7 +106,8 @@ export function CompositePriceDemo() { if (components.length > 1) setComponents((prev) => prev.filter((_, i) => i !== idx)); }; - const oneTimeItems = result.items?.filter((item: any) => item._price?.type === 'one_time' || !item._price?.billing_period) ?? []; + const oneTimeItems = + result.items?.filter((item: any) => item._price?.type === 'one_time' || !item._price?.billing_period) ?? []; const recurringItems = result.items?.filter((item: any) => item._price?.type === 'recurring') ?? []; return ( @@ -122,7 +123,9 @@ export function CompositePriceDemo() {

Price Components

- +
{components.map((comp, idx) => ( @@ -231,9 +234,7 @@ export function CompositePriceDemo() { return (

{comp.name}

@@ -302,13 +303,17 @@ const compositeItem = { unit_amount_currency: 'EUR', tax: [{ rate: ${taxRate}, type: 'VAT' }], price_components: [ -${components.map((c) => ` { +${components + .map( + (c) => ` { unit_amount_decimal: '${c.unitAmountDecimal}', pricing_model: 'per_unit', is_tax_inclusive: true, type: '${c.type}',${c.billingPeriod ? `\n billing_period: '${c.billingPeriod}',` : ''} description: '${c.name}', - },`).join('\n')} + },`, + ) + .join('\n')} ], }, price_components: [ diff --git a/demo/src/sections/CurrencyDemo.tsx b/demo/src/sections/CurrencyDemo.tsx index 25f664f..420a961 100644 --- a/demo/src/sections/CurrencyDemo.tsx +++ b/demo/src/sections/CurrencyDemo.tsx @@ -1,6 +1,5 @@ -import { useState, useMemo } from 'react'; import { formatAmount, formatAmountFromString, getCurrencySymbol, toIntegerAmount } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; const currencies = [ @@ -78,8 +77,8 @@ export function CurrencyDemo() {

Currency & Formatting

- Multi-currency support with locale-aware formatting. Uses Dinero.js for precise decimal arithmetic - and provides utilities for converting between string/integer representations. + Multi-currency support with locale-aware formatting. Uses Dinero.js for precise decimal arithmetic and provides + utilities for converting between string/integer representations.

@@ -109,7 +108,9 @@ export function CurrencyDemo() { className="select-field mt-1" > {currencies.map((c) => ( - + ))}
@@ -130,20 +131,29 @@ export function CurrencyDemo() {

Function Results

-

formatAmount({'{'} amount: {intAmount}, currency: '{selectedCurrency}', locale: '{locale}' {'}'})

+

+ formatAmount({'{'} amount: {intAmount}, currency: '{selectedCurrency}', locale: '{locale}' {'}'}) +

{formattedAmount}

-

formatAmountFromString({'{'} decimalAmount: '{amount}', currency: '{selectedCurrency}', locale: '{locale}' {'}'})

+

+ formatAmountFromString({'{'} decimalAmount: '{amount}', currency: '{selectedCurrency}', locale: ' + {locale}' {'}'}) +

{formattedFromString}

-

getCurrencySymbol('{selectedCurrency}', '{locale}')

+

+ getCurrencySymbol('{selectedCurrency}', '{locale}') +

{symbol}

toIntegerAmount('{amount}')

-

{integerAmount} (cents)

+

+ {integerAmount} (cents) +

@@ -159,9 +169,7 @@ export function CurrencyDemo() {
@@ -170,7 +178,9 @@ export function CurrencyDemo() {

{c.name}

-

{c.code} ({c.locale})

+

+ {c.code} ({c.locale}) +

{c.formatted} diff --git a/demo/src/sections/DiscountDemo.tsx b/demo/src/sections/DiscountDemo.tsx index 39711d7..e0ea6ce 100644 --- a/demo/src/sections/DiscountDemo.tsx +++ b/demo/src/sections/DiscountDemo.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { ResultCard } from '../components/ResultCard'; import { buildPriceItemDto, fmtCents, makeCoupon } from '../helpers'; type CouponConfig = { @@ -13,7 +13,7 @@ type CouponConfig = { export function DiscountDemo() { const [unitPrice, setUnitPrice] = useState('100.00'); const [quantity, setQuantity] = useState(5); - const [taxRate, setTaxRate] = useState(19); + const [taxRate] = useState(19); const [isTaxInclusive, setIsTaxInclusive] = useState(true); const [couponConfig, setCouponConfig] = useState({ type: 'percentage', @@ -32,22 +32,23 @@ export function DiscountDemo() { }, [unitPrice, quantity, taxRate, isTaxInclusive]); const discountResult = useMemo(() => { - const coupon = couponConfig.type === 'percentage' - ? makeCoupon({ - type: 'percentage', - category: couponConfig.category, - percentageValue: couponConfig.value, - name: `${couponConfig.value}% ${couponConfig.category}`, - ...(couponConfig.category === 'cashback' && { cashbackPeriod: 12 }), - }) - : makeCoupon({ - type: 'fixed', - category: couponConfig.category, - fixedValueDecimal: couponConfig.value, - fixedValue: Math.round(parseFloat(couponConfig.value) * 100), - name: `โ‚ฌ${couponConfig.value} ${couponConfig.category}`, - ...(couponConfig.category === 'cashback' && { cashbackPeriod: 12 }), - }); + const coupon = + couponConfig.type === 'percentage' + ? makeCoupon({ + type: 'percentage', + category: couponConfig.category, + percentageValue: couponConfig.value, + name: `${couponConfig.value}% ${couponConfig.category}`, + ...(couponConfig.category === 'cashback' && { cashbackPeriod: 12 }), + }) + : makeCoupon({ + type: 'fixed', + category: couponConfig.category, + fixedValueDecimal: couponConfig.value, + fixedValue: Math.round(parseFloat(couponConfig.value) * 100), + name: `โ‚ฌ${couponConfig.value} ${couponConfig.category}`, + ...(couponConfig.category === 'cashback' && { cashbackPeriod: 12 }), + }); const item = buildPriceItemDto({ unitAmountDecimal: unitPrice, @@ -74,8 +75,8 @@ export function DiscountDemo() {

Discounts & Coupons

- Apply fixed-value, percentage discounts, and cashback coupons. Coupons are prioritized: - cashback > discounts, percentage > fixed, highest value first. + Apply fixed-value, percentage discounts, and cashback coupons. Coupons are prioritized: cashback > discounts, + percentage > fixed, highest value first.

@@ -142,9 +143,7 @@ export function DiscountDemo() { {/* Work prices */} -
-
-

- {tariffType === 'dual' ? 'Arbeitspreis HT (Peak)' : 'Arbeitspreis (Work Price)'} -

+
+
+

+ {tariffType === 'dual' ? 'Arbeitspreis HT' : 'Arbeitspreis'} +

- +
- + {tariffType === 'dual' && ( -
-

Arbeitspreis NT (Off-Peak)

+
+

Arbeitspreis NT

- +
- + {tariffType === 'dual' ? ( -
+
- +
+ + {consumptionHT.toLocaleString()} kWh +
setConsumptionHT(Number(e.target.value))} - className="w-full mt-1 accent-yellow-500" + className="w-full accent-amber-500" />
- +
+ + {consumptionNT.toLocaleString()} kWh +
setConsumptionNT(Number(e.target.value))} - className="w-full mt-1 accent-indigo-500" + className="w-full accent-indigo-500" />
-

- Total: {totalConsumption.toLocaleString()} kWh/year -

+
+
+ Total consumption + + {totalConsumption.toLocaleString()} kWh/year + +
+
) : (
- +
+ + + {totalConsumption.toLocaleString()} kWh + +
-
+
1,000 kWh 10,000 kWh
@@ -241,110 +254,137 @@ export function ElectricityDemo() {
-
- {/* Cost breakdown */} -
-

Annual Cost Breakdown

-
- {[ - { value: baseCost, color: 'bg-blue-400', label: 'Base' }, - { value: htCost, color: 'bg-yellow-400', label: tariffType === 'dual' ? 'HT' : 'Work' }, - ...(tariffType === 'dual' ? [{ value: ntCost, color: 'bg-indigo-400', label: 'NT' }] : []), - ].map((seg) => ( + {/* Right column โ€” Tariff card + results */} +
+ {/* Main tariff card */} + โšก} + title={tariffType === 'dual' ? 'Doppeltarif (HT/NT)' : 'Einfachtarif (ET)'} + subtitle={`${totalConsumption.toLocaleString()} kWh/year`} + badge={tariffType === 'dual' ? 'HT/NT' : 'SINGLE'} + price={`EUR ${monthlyGross.toFixed(2)}`} + priceUnit="/month" + priceLabel="Estimated monthly cost (gross)" + footer={ +
+ Annual total (gross) + EUR {totalGross.toFixed(2)} +
+ } + > + {/* Cost breakdown inside card */} +
+ {/* Stacked bar */} +
- {(seg.value / totalNet) * 100 > 10 ? seg.label : ''} -
- ))} -
+ className="bg-blue-400 transition-all duration-300" + style={{ width: `${(baseCost / totalNet) * 100}%` }} + title="Grundpreis" + /> +
+ {tariffType === 'dual' && ( +
+ )} +
-
-
-
-
-

Grundpreis

-

EUR {parseFloat(basePrice).toFixed(2)}/year

+
+
+
+
+ Grundpreis +

EUR {parseFloat(basePrice).toFixed(2)}/year

+
- EUR {baseCost.toFixed(2)} + EUR {baseCost.toFixed(2)}
-
-
-
-

- {tariffType === 'dual' ? 'Arbeitspreis HT' : 'Arbeitspreis'} -

-

- {htRate.toFixed(2)} ct/kWh x {(tariffType === 'dual' ? consumptionHT : totalConsumption).toLocaleString()} kWh -

+ +
+
+
+
+ + {tariffType === 'dual' ? 'Arbeitspreis HT' : 'Arbeitspreis'} + +

+ {htRate.toFixed(2)} ct/kWh x{' '} + {(tariffType === 'dual' ? consumptionHT : totalConsumption).toLocaleString()} kWh +

+
- EUR {htCost.toFixed(2)} + EUR {htCost.toFixed(2)}
+ {tariffType === 'dual' && ( -
-
-
-

Arbeitspreis NT

-

- {ntRate.toFixed(2)} ct/kWh x {consumptionNT.toLocaleString()} kWh -

+
+
+
+
+ Arbeitspreis NT +

+ {ntRate.toFixed(2)} ct/kWh x {consumptionNT.toLocaleString()} kWh +

+
- EUR {ntCost.toFixed(2)} + EUR {ntCost.toFixed(2)}
)} -
-
- Net Total (annual) - EUR {totalNet.toFixed(2)} -
+ +
+ Net Total (annual) + EUR {totalNet.toFixed(2)}
-
+ + + {/* Rate comparison for dual tariff */} + {tariffType === 'dual' && ( +
+
+

HT (Peak)

+

{htRate.toFixed(2)}

+

ct/kWh

+

{consumptionHT.toLocaleString()} kWh

+
+
+

NT (Off-Peak)

+

{ntRate.toFixed(2)}

+

ct/kWh

+

{consumptionNT.toLocaleString()} kWh

+
+
+ )} {/* Computed results */}
-

Computed via Library

-
+

+ Computed via @epilot/pricing +

+
- +
- - {/* Rate comparison for dual tariff */} - {tariffType === 'dual' && ( -
-

HT vs NT Rate Comparison

-
-
-

HT (Peak)

-

{htRate.toFixed(2)} ct

-

{consumptionHT.toLocaleString()} kWh

-
-
-

NT (Off-Peak)

-

{ntRate.toFixed(2)} ct

-

{consumptionNT.toLocaleString()} kWh

-
-
-

- Savings vs. all-HT: EUR {((htRate - ntRate) * consumptionNT).toFixed(2)}/year -

-
- )}
- {/* Usage */} -
+ {/* Code block */} +
{ const items: any[] = []; - // Base price items.push( buildPriceItemDto({ unitAmountDecimal: basePrice, @@ -29,9 +29,7 @@ export function GasDemo() { }), ); - // Work price + markup + levies per kWh - const totalPerKwh = - parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy); + const totalPerKwh = parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy); items.push( buildPriceItemDto({ unitAmountDecimal: totalPerKwh.toFixed(4), @@ -53,41 +51,43 @@ export function GasDemo() { const workCost = workRate * consumption; const levyCost = levyRate * consumption; const totalNet = baseCost + workCost + levyCost; + const totalGross = totalNet * (1 + taxRate / 100); + const monthlyGross = totalGross / 12; + const totalKwhRate = workRate + levyRate; return (

Gas Tariff

- German gas supply pricing with Grundpreis, Arbeitspreis, and gas-specific levies - including CO2 tax and gas storage levy. Typical household consumption: 10,000 - 25,000 kWh/year. + Configure a German gas supply tariff with Grundpreis, Arbeitspreis, and gas-specific levies including CO2 tax + and gas storage levy.

-
-
+
+ {/* Left column โ€” Controls */} +
{/* Base price */}
-
-

Grundpreis (Base Price)

-
- - setBasePrice(e.target.value)} - className="input-field mt-1" - /> -
+
+

Grundpreis

+ + setBasePrice(e.target.value)} + className="input-field mt-1" + />
{/* Work price */} -
-
-

Arbeitspreis (Work Price)

+
+
+

Arbeitspreis

- +
- +
- {/* Gas-specific levies */} -
-

Gas Levies

+
+

Gas Levies

- +
- + - +
+ + {consumption.toLocaleString()} kWh +
setConsumption(Number(e.target.value))} - className="w-full mt-2 accent-primary-600" + className="w-full accent-primary-600" /> -
+
5,000 kWh 50,000 kWh
-
- {/* Cost breakdown */} -
-

Annual Cost Breakdown

-
- {[ - { value: baseCost, color: 'bg-blue-400', label: 'Base' }, - { value: workCost, color: 'bg-orange-400', label: 'Work' }, - { value: levyCost, color: 'bg-red-400', label: 'Levies' }, - ].map((seg) => ( -
- {(seg.value / totalNet) * 100 > 10 ? seg.label : ''} -
- ))} + {/* Right column โ€” Tariff card + results */} +
+ {/* Main tariff card */} + ๐Ÿ”ฅ} + title="Erdgas Tarif" + subtitle={`${consumption.toLocaleString()} kWh/year`} + badge="GAS" + price={`EUR ${monthlyGross.toFixed(2)}`} + priceUnit="/month" + priceLabel="Estimated monthly cost (gross)" + footer={ +
+ Annual total (gross) + EUR {totalGross.toFixed(2)} +
+ } + > + {/* Stacked bar */} +
+
+
+
-
-
-
-
-

Grundpreis

-

EUR {parseFloat(basePrice).toFixed(2)}/year

+
+
+
+
+ Grundpreis +

EUR {parseFloat(basePrice).toFixed(2)}/year

- EUR {baseCost.toFixed(2)}
-
-
-
-

Arbeitspreis + Markup

-

+ EUR {baseCost.toFixed(2)} +

+ +
+
+
+
+ Arbeitspreis + Markup +

{workRate.toFixed(2)} ct/kWh x {consumption.toLocaleString()} kWh

- EUR {workCost.toFixed(2)}
-
-
-
-

Gas Levies

-

- {levyRate.toFixed(3)} ct/kWh x {consumption.toLocaleString()} kWh (CO2 + storage) -

+ EUR {workCost.toFixed(2)} +
+ +
+
+
+
+ Gas Levies +

{levyRate.toFixed(3)} ct/kWh (CO2 + storage)

- EUR {levyCost.toFixed(2)}
-
-
- Net Total (annual) - EUR {totalNet.toFixed(2)} + EUR {levyCost.toFixed(2)} +
+ +
+ Net Total (annual) + EUR {totalNet.toFixed(2)} +
+ + + {/* Per-kWh composition */} +
+

Price per kWh Composition

+
+ {[ + { label: 'Work Price', value: parseFloat(workPrice), color: 'bg-orange-200', bar: 'bg-orange-400' }, + { label: 'Markup', value: parseFloat(markup), color: 'bg-orange-100', bar: 'bg-orange-300' }, + { label: 'CO2 Levy', value: parseFloat(co2Levy), color: 'bg-red-100', bar: 'bg-red-400' }, + { label: 'Gas Storage', value: parseFloat(gasStorageLevy), color: 'bg-red-50', bar: 'bg-red-300' }, + ].map((item) => ( +
+ {item.label} +
+
+
+ {item.value.toFixed(3)} ct
+ ))} +
+ Total: {totalKwhRate.toFixed(3)} ct/kWh
{/* Computed results */}
-

Computed via Library

-
+

+ Computed via @epilot/pricing +

+
- +
- - {/* Per-kWh breakdown */} -
-

Per-kWh Price Composition

-
- {[ - { label: 'Work Price', value: parseFloat(workPrice), color: 'bg-orange-200' }, - { label: 'Markup', value: parseFloat(markup), color: 'bg-orange-400' }, - { label: 'CO2 Levy', value: parseFloat(co2Levy), color: 'bg-red-300' }, - { label: 'Gas Storage', value: parseFloat(gasStorageLevy), color: 'bg-red-200' }, - ].map((item) => { - const totalKwh = parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy); - return ( -
- {item.label} -
-
-
- {item.value.toFixed(3)} ct -
- ); - })} -
- - Total: {(parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy)).toFixed(3)} ct/kWh - -
-
-
- {/* Usage */} -
+ {/* Code block */} +
{ @@ -61,8 +61,8 @@ export function GetAGDemo() {

GetAG Energy Pricing

- German energy operator (GetAG) integration supporting base price + work price models - with tiered markups and additional fees. Used for electricity and gas tariffs. + German energy operator (GetAG) integration supporting base price + work price models with tiered markups and + additional fees. Used for electricity and gas tariffs.

@@ -173,14 +173,24 @@ export function GetAGDemo() {
{[ - { label: 'Grundpreis (Base)', value: baseCost, color: 'bg-blue-400', detail: `โ‚ฌ${parseFloat(basePrice).toFixed(2)}/year` }, + { + label: 'Grundpreis (Base)', + value: baseCost, + color: 'bg-blue-400', + detail: `โ‚ฌ${parseFloat(basePrice).toFixed(2)}/year`, + }, { label: 'Arbeitspreis (Work)', value: workCost, color: 'bg-green-400', detail: `${(parseFloat(workPrice) + parseFloat(markupPerUnit)).toFixed(2)} ct/kWh x ${consumption.toLocaleString()} kWh`, }, - { label: 'Additional Markup', value: addCost, color: 'bg-amber-400', detail: `โ‚ฌ${parseFloat(additionalMarkup).toFixed(2)}/year` }, + { + label: 'Additional Markup', + value: addCost, + color: 'bg-amber-400', + detail: `โ‚ฌ${parseFloat(additionalMarkup).toFixed(2)}/year`, + }, ].map((item) => (
@@ -205,7 +215,7 @@ export function GetAGDemo() {

Computed via Library

- + (defaultItems); - const [taxRate, setTaxRate] = useState(19); + const [taxRate] = useState(19); const [distance, setDistance] = useState(15); const [perMeterRate, setPerMeterRate] = useState('85.00'); - // Trench cost based on distance const trenchCost = distance * parseFloat(perMeterRate); const result = useMemo(() => { @@ -42,7 +50,6 @@ export function HouseConnectionDemo() { }), ); - // Add trench/distance-based cost priceItems.push( buildPriceItemDto({ unitAmountDecimal: trenchCost.toFixed(2), @@ -62,9 +69,7 @@ export function HouseConnectionDemo() { }; const toggleItem = (idx: number) => { - setItems((prev) => - prev.map((item, i) => (i === idx ? { ...item, quantity: item.quantity > 0 ? 0 : 1 } : item)), - ); + setItems((prev) => prev.map((item, i) => (i === idx ? { ...item, quantity: item.quantity > 0 ? 0 : 1 } : item))); }; const oneTimeCosts = items @@ -79,58 +84,54 @@ export function HouseConnectionDemo() {

House Connection

- Hausanschluss (house connection) pricing for new builds and renovations. - Combines one-time connection fees, distance-based trench work, and recurring meter costs. + Configure Hausanschluss (house connection) pricing for new builds. Combines connection fees, distance-based + trench work, and recurring meter costs.

-
-
+
+ {/* Left column โ€” Controls */} +
{/* Connection items */}
-

Connection Services

-
+

Connection Services

+
{items.map((item, idx) => (
0 - ? 'bg-white border-primary-200' - : 'bg-gray-50 border-gray-100 opacity-60' + ? 'bg-white border border-primary-100 shadow-sm' + : 'bg-gray-50 border border-transparent opacity-50' }`} >
-
-

{item.name}

+ {item.icon} +
+

{item.name}

{item.type === 'one_time' ? 'One-time' : `${item.billingPeriod}`}
-
- updateItem(idx, 'unitAmountDecimal', e.target.value)} - className="input-field text-xs text-right" - disabled={item.quantity === 0} - /> -
+ updateItem(idx, 'unitAmountDecimal', e.target.value)} + className="input-field w-24 text-xs text-right" + disabled={item.quantity === 0} + />
))} @@ -139,12 +140,13 @@ export function HouseConnectionDemo() { {/* Distance-based pricing */}
-
-

Trench Work (Tiefbau)

-
- +
+

Trench Work (Tiefbau)

+
+
+ + {distance} m +
setDistance(Number(e.target.value))} - className="w-full mt-1 accent-amber-500" + className="w-full accent-amber-500" /> -
+
5 m 100 m
-
+
- +
-
-

Total Trench Cost

-

EUR {trenchCost.toFixed(2)}

+
+

Total

+

EUR {trenchCost.toFixed(2)}

@@ -181,91 +183,116 @@ export function HouseConnectionDemo() {
-
- {/* Visual overview */} -
-

Cost Overview

- - {/* Connection type breakdown */} -
- {items - .filter((i) => i.quantity > 0 && i.type === 'one_time') - .map((item, idx) => { - const cost = parseFloat(item.unitAmountDecimal) * item.quantity; - return ( -
- {item.name} - EUR {cost.toFixed(2)} + {/* Right column โ€” Tariff card + results */} +
+ ๐Ÿก} + title="Hausanschluss" + subtitle="New build connection package" + badge="CONNECTION" + price={`EUR ${totalOneTime.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`} + priceUnit="" + priceLabel="Total one-time costs (net)" + footer={ + recurringCosts > 0 ? ( +
+ Recurring costs + EUR {recurringCosts.toFixed(2)}/month +
+ ) : undefined + } + > + {/* Line items */} + {items + .filter((i) => i.quantity > 0 && i.type === 'one_time') + .map((item, idx) => { + const cost = parseFloat(item.unitAmountDecimal) * item.quantity; + return ( +
+
+ {item.icon} + {item.name}
- ); - })} -
- Trench Work ({distance}m x EUR {parseFloat(perMeterRate).toFixed(2)}) - EUR {trenchCost.toFixed(2)} + EUR {cost.toFixed(2)} +
+ ); + })} + +
+
+ ๐Ÿ› ๏ธ +
+ Trench Work +

+ {distance}m x EUR {parseFloat(perMeterRate).toFixed(2)}/m +

+
- {items - .filter((i) => i.quantity > 0 && i.type === 'recurring') - .map((item, idx) => ( -
+ EUR {trenchCost.toFixed(2)} +
+ + {items + .filter((i) => i.quantity > 0 && i.type === 'recurring') + .map((item, idx) => ( +
+
+ {item.icon}
- {item.name} - /{item.billingPeriod} + {item.name} +

/{item.billingPeriod}

- EUR {parseFloat(item.unitAmountDecimal).toFixed(2)}
- ))} -
- -
-
- One-time costs (net) - EUR {totalOneTime.toFixed(2)} -
- {recurringCosts > 0 && ( -
- Recurring costs (net) - EUR {recurringCosts.toFixed(2)}/month + + EUR {parseFloat(item.unitAmountDecimal).toFixed(2)} +
- )} -
-
+ ))} + {/* Computed results */}
-

Computed via Library

-
+

+ Computed via @epilot/pricing +

+
- + - +
{(result.total_details?.breakdown?.recurrences?.length ?? 0) > 0 && ( -
-

By Recurrence:

- {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( -
- - {r.type === 'one_time' ? 'One-time' : r.billing_period} - - {fmtCents(r.amount_total)} -
- ))} +
+

By Recurrence

+
+ {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( +
+ + {r.type === 'one_time' ? 'One-time' : r.billing_period} + + {fmtCents(r.amount_total)} +
+ ))} +
)}
- {/* Usage */} -
+ {/* Code block */} +
i.quantity > 0).map((i) => ` { +${items + .filter((i) => i.quantity > 0) + .map( + (i) => ` { quantity: ${i.quantity}, _price: { unit_amount_decimal: '${i.unitAmountDecimal}', @@ -277,7 +304,9 @@ ${items.filter((i) => i.quantity > 0).map((i) => ` { description: '${i.name}', }, taxes: [{ tax: { rate: ${taxRate} } }], - },`).join('\n')} + },`, + ) + .join('\n')} { quantity: 1, _price: { @@ -287,7 +316,7 @@ ${items.filter((i) => i.quantity > 0).map((i) => ` { is_tax_inclusive: false, type: 'one_time', tax: [{ rate: ${taxRate}, type: 'VAT' }], - description: 'Trench Work (${distance}m x EUR ${parseFloat(perMeterRate).toFixed(2)}/m)', + description: 'Trench Work (${distance}m)', }, taxes: [{ tax: { rate: ${taxRate} } }], }, diff --git a/demo/src/sections/NonCommodityDemo.tsx b/demo/src/sections/NonCommodityDemo.tsx index 7d4567f..0b39413 100644 --- a/demo/src/sections/NonCommodityDemo.tsx +++ b/demo/src/sections/NonCommodityDemo.tsx @@ -1,7 +1,14 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { + ProductShowcase, + SolarIllustration, + WallboxIllustration, + HeatPumpIllustration, + SmartHomeIllustration, +} from '../components/ProductShowcase'; +import { ResultCard } from '../components/ResultCard'; import { buildPriceItemDto, fmtCents } from '../helpers'; interface Product { @@ -15,43 +22,128 @@ interface Product { } const defaultProducts: Product[] = [ - // Solar - { name: 'Solar Panel System (10 kWp)', category: 'solar', price: '12500.00', quantity: 1, type: 'one_time', enabled: true }, - { name: 'Battery Storage (10 kWh)', category: 'solar', price: '6800.00', quantity: 1, type: 'one_time', enabled: true }, + { + name: 'Solar Panel System (10 kWp)', + category: 'solar', + price: '12500.00', + quantity: 1, + type: 'one_time', + enabled: true, + }, + { + name: 'Battery Storage (10 kWh)', + category: 'solar', + price: '6800.00', + quantity: 1, + type: 'one_time', + enabled: true, + }, { name: 'Solar Installation', category: 'solar', price: '3200.00', quantity: 1, type: 'one_time', enabled: true }, - { name: 'Solar Maintenance Contract', category: 'solar', price: '29.90', quantity: 1, type: 'recurring', billingPeriod: 'monthly', enabled: true }, - // E-Mobility + { + name: 'Solar Maintenance Contract', + category: 'solar', + price: '29.90', + quantity: 1, + type: 'recurring', + billingPeriod: 'monthly', + enabled: true, + }, { name: 'Wallbox (11 kW)', category: 'emobility', price: '899.00', quantity: 1, type: 'one_time', enabled: false }, - { name: 'Wallbox Installation', category: 'emobility', price: '450.00', quantity: 1, type: 'one_time', enabled: false }, - { name: 'Charging Flat Rate', category: 'emobility', price: '59.00', quantity: 1, type: 'recurring', billingPeriod: 'monthly', enabled: false }, - // Heating + { + name: 'Wallbox Installation', + category: 'emobility', + price: '450.00', + quantity: 1, + type: 'one_time', + enabled: false, + }, + { + name: 'Charging Flat Rate', + category: 'emobility', + price: '59.00', + quantity: 1, + type: 'recurring', + billingPeriod: 'monthly', + enabled: false, + }, { name: 'Heat Pump System', category: 'heating', price: '15800.00', quantity: 1, type: 'one_time', enabled: false }, - { name: 'Heat Pump Installation', category: 'heating', price: '4500.00', quantity: 1, type: 'one_time', enabled: false }, - { name: 'Heating Maintenance', category: 'heating', price: '39.90', quantity: 1, type: 'recurring', billingPeriod: 'monthly', enabled: false }, - // Smart Home + { + name: 'Heat Pump Installation', + category: 'heating', + price: '4500.00', + quantity: 1, + type: 'one_time', + enabled: false, + }, + { + name: 'Heating Maintenance', + category: 'heating', + price: '39.90', + quantity: 1, + type: 'recurring', + billingPeriod: 'monthly', + enabled: false, + }, { name: 'Smart Thermostat', category: 'smarthome', price: '249.00', quantity: 1, type: 'one_time', enabled: false }, { name: 'Energy Manager', category: 'smarthome', price: '499.00', quantity: 1, type: 'one_time', enabled: false }, ]; -const categoryMeta: Record = { - solar: { label: 'Solar & Storage', color: 'bg-yellow-400', bg: 'bg-yellow-50', text: 'text-yellow-700', icon: '\u2600\uFE0F' }, - emobility: { label: 'E-Mobility', color: 'bg-blue-400', bg: 'bg-blue-50', text: 'text-blue-700', icon: '\uD83D\uDE97' }, - heating: { label: 'Heating', color: 'bg-red-400', bg: 'bg-red-50', text: 'text-red-700', icon: '\uD83C\uDF21\uFE0F' }, - smarthome: { label: 'Smart Home', color: 'bg-green-400', bg: 'bg-green-50', text: 'text-green-700', icon: '\uD83C\uDFE0' }, +const categoryMeta: Record< + string, + { + label: string; + gradient: string; + bg: string; + text: string; + icon: string; + illustration: React.ComponentType; + features: string[]; + } +> = { + solar: { + label: 'Solar & Storage', + gradient: 'gradient-solar', + bg: 'bg-amber-50', + text: 'text-amber-700', + icon: 'โ˜€๏ธ', + illustration: SolarIllustration, + features: ['10 kWp PV system', 'Battery storage', 'Professional installation', 'Maintenance included'], + }, + emobility: { + label: 'E-Mobility', + gradient: 'gradient-emobility', + bg: 'bg-blue-50', + text: 'text-blue-700', + icon: '๐Ÿš—', + illustration: WallboxIllustration, + features: ['11 kW wallbox', 'Installation included', 'Smart charging', 'Monthly flat rate'], + }, + heating: { + label: 'Heating', + gradient: 'gradient-heating', + bg: 'bg-red-50', + text: 'text-red-700', + icon: '๐ŸŒก๏ธ', + illustration: HeatPumpIllustration, + features: ['Air-to-water system', 'Full installation', 'Smart controls', 'Service contract'], + }, + smarthome: { + label: 'Smart Home', + gradient: 'gradient-smarthome', + bg: 'bg-emerald-50', + text: 'text-emerald-700', + icon: '๐Ÿ ', + illustration: SmartHomeIllustration, + features: ['Smart thermostat', 'Energy monitoring', 'App control', 'Consumption insights'], + }, }; +const categories = ['solar', 'emobility', 'heating', 'smarthome'] as const; + export function NonCommodityDemo() { const [products, setProducts] = useState(defaultProducts); const [taxRate, setTaxRate] = useState(19); - const toggleProduct = (idx: number) => { - setProducts((prev) => prev.map((p, i) => (i === idx ? { ...p, enabled: !p.enabled } : p))); - }; - - const updateProduct = (idx: number, field: keyof Product, value: any) => { - setProducts((prev) => prev.map((p, i) => (i === idx ? { ...p, [field]: value } : p))); - }; - const toggleCategory = (category: string) => { setProducts((prev) => { const catProducts = prev.filter((p) => p.category === category); @@ -60,6 +152,10 @@ export function NonCommodityDemo() { }); }; + const updateProduct = (idx: number, field: keyof Product, value: any) => { + setProducts((prev) => prev.map((p, i) => (i === idx ? { ...p, [field]: value } : p))); + }; + const result = useMemo(() => { const items = products .filter((p) => p.enabled) @@ -76,7 +172,13 @@ export function NonCommodityDemo() { ); if (items.length === 0) { - return { amount_subtotal: 0, amount_tax: 0, amount_total: 0, items: [], total_details: { breakdown: { recurrences: [] } } }; + return { + amount_subtotal: 0, + amount_tax: 0, + amount_total: 0, + items: [], + total_details: { breakdown: { recurrences: [] } }, + }; } return computeAggregatedAndPriceTotals(items); @@ -90,114 +192,111 @@ export function NonCommodityDemo() { .filter((p) => p.type === 'recurring') .reduce((sum, p) => sum + parseFloat(p.price) * p.quantity, 0); - const categories = ['solar', 'emobility', 'heating', 'smarthome']; const categoryTotals = categories.map((cat) => { const catProducts = enabledProducts.filter((p) => p.category === cat); + const meta = categoryMeta[cat]; return { category: cat, - ...categoryMeta[cat], - oneTime: catProducts.filter((p) => p.type === 'one_time').reduce((s, p) => s + parseFloat(p.price) * p.quantity, 0), - recurring: catProducts.filter((p) => p.type === 'recurring').reduce((s, p) => s + parseFloat(p.price) * p.quantity, 0), + ...meta, + oneTime: catProducts + .filter((p) => p.type === 'one_time') + .reduce((s, p) => s + parseFloat(p.price) * p.quantity, 0), + recurring: catProducts + .filter((p) => p.type === 'recurring') + .reduce((s, p) => s + parseFloat(p.price) * p.quantity, 0), count: catProducts.length, }; - }).filter((c) => c.count > 0); + }); + + const isCategoryEnabled = (cat: string) => products.filter((p) => p.category === cat).some((p) => p.enabled); return (
-

Non-Commodity Products

+

Products & Add-ons

- Products and services beyond the energy supply itself โ€” solar panels, battery storage, - wallboxes, heat pumps, and smart home devices. Combines one-time hardware and installation - costs with recurring service and maintenance contracts. + Build product bundles for your customers โ€” solar panels, wallboxes, heat pumps, and smart home devices. Select + categories to configure complete packages with one-time and recurring pricing.

-
-
- {/* Category toggles */} -
-

Product Categories

-
- {categories.map((cat) => { - const meta = categoryMeta[cat]; - const catProducts = products.filter((p) => p.category === cat); - const enabledCount = catProducts.filter((p) => p.enabled).length; - return ( - - ); - })} -
-
+ {/* Product showcase grid โ€” the "journey" visual */} +

Select Product Categories

+
+ {categories.map((cat) => { + const meta = categoryMeta[cat]; + const catProducts = products.filter((p) => p.category === cat); + const catOneTime = catProducts + .filter((p) => p.type === 'one_time') + .reduce((s, p) => s + parseFloat(p.price), 0); + const catRecurring = catProducts + .filter((p) => p.type === 'recurring') + .reduce((s, p) => s + parseFloat(p.price), 0); + const enabled = isCategoryEnabled(cat); - {/* Product list */} + return ( + 0 ? `+ EUR ${catRecurring.toFixed(2)}/mo` : 'one-time'} + features={meta.features} + tag={enabled ? 'Selected' : undefined} + tagColor={enabled ? 'bg-emerald-100 text-emerald-700' : undefined} + selected={enabled} + onToggle={() => toggleCategory(cat)} + /> + ); + })} +
+ +
+ {/* Product details */} +
-

Products & Services

-
+

Products in Bundle

+
{products.map((product, idx) => { const meta = categoryMeta[product.category]; return (
-

{product.name}

-
- {meta.label} +

{product.name}

+
+ {meta.icon} {product.type === 'one_time' ? 'One-time' : product.billingPeriod}
-
- updateProduct(idx, 'quantity', Math.max(1, Number(e.target.value)))} - className="input-field w-12 text-xs text-center" - disabled={!product.enabled} - /> - updateProduct(idx, 'price', e.target.value)} - className="input-field w-24 text-xs text-right" - disabled={!product.enabled} - /> -
+ updateProduct(idx, 'price', e.target.value)} + className="input-field w-24 text-xs text-right" + disabled={!product.enabled} + />
); })} @@ -205,12 +304,8 @@ export function NonCommodityDemo() {
- - setTaxRate(Number(e.target.value))} className="select-field mt-2"> @@ -218,130 +313,152 @@ export function NonCommodityDemo() {
-
- {/* Category cost breakdown */} -
-

Cost by Category

- - {categoryTotals.length === 0 ? ( -

No products selected

- ) : ( - <> - {/* Stacked bar for one-time costs */} - {oneTimeCosts > 0 && ( -
-

One-time costs

-
- {categoryTotals - .filter((c) => c.oneTime > 0) - .map((cat) => ( + {/* Right column โ€” Bundle summary + results */} +
+ {/* Bundle order summary โ€” tariff card style */} +
+
+
+
+

Your Bundle

+
+ + EUR {oneTimeCosts.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+ {monthlyCosts > 0 && ( +

+ EUR {monthlyCosts.toFixed(2)}/month service

+ )} +
+
+

{enabledProducts.length}

+

items selected

+
+
+
+
+ {/* Category cost stacked bar */} + {oneTimeCosts > 0 && ( +
+

+ Cost Distribution +

+
+ {categoryTotals + .filter((c) => c.oneTime > 0) + .map((cat) => { + const colors: Record = { + solar: 'bg-amber-400', + emobility: 'bg-blue-400', + heating: 'bg-red-400', + smarthome: 'bg-emerald-400', + }; + return (
- {(cat.oneTime / oneTimeCosts) * 100 > 15 ? cat.icon : ''} -
- ))} -
-
- )} - -
- {categoryTotals.map((cat) => ( -
-
-
-

{cat.icon} {cat.label}

-

{cat.count} item(s)

-
-
- {cat.oneTime > 0 && ( -

EUR {cat.oneTime.toFixed(2)}

- )} - {cat.recurring > 0 && ( -

+ EUR {cat.recurring.toFixed(2)}/mo

- )} -
-
- ))} - -
-
- One-time total (net) - EUR {oneTimeCosts.toFixed(2)} -
- {monthlyCosts > 0 && ( -
- Monthly total (net) - EUR {monthlyCosts.toFixed(2)}/mo -
- )} + /> + ); + })}
- - )} -
+ )} - {/* Bundle summary */} -
-

Selected Bundle

-
- {enabledProducts.map((p, idx) => ( -
- - {categoryMeta[p.category].icon} {p.name} - {p.quantity > 1 && x{p.quantity}} - - - EUR {(parseFloat(p.price) * p.quantity).toFixed(2)} - {p.type === 'recurring' && /mo} - + {/* Line items */} + {enabledProducts.length === 0 ? ( +

Select products above to build your bundle

+ ) : ( +
+ {categoryTotals + .filter((c) => c.count > 0) + .map((cat) => ( +
+
+ {cat.icon} + {cat.label} +
+ {enabledProducts + .filter((p) => p.category === cat.category) + .map((p, idx) => ( +
+ + {p.name} + {p.quantity > 1 && x{p.quantity}} + + + EUR {(parseFloat(p.price) * p.quantity).toFixed(2)} + {p.type === 'recurring' && ( + /mo + )} + +
+ ))} +
+ ))}
- ))} - {enabledProducts.length === 0 && ( -

No products selected

)}
+ {enabledProducts.length > 0 && ( +
+
+ One-time total (net) + EUR {oneTimeCosts.toFixed(2)} +
+ {monthlyCosts > 0 && ( +
+ Monthly total (net) + EUR {monthlyCosts.toFixed(2)}/mo +
+ )} +
+ )}
{/* Computed results */}
-

Computed via Library

-
+

+ Computed via @epilot/pricing +

+
- + - +
{(result.total_details?.breakdown?.recurrences?.length ?? 0) > 0 && ( -
-

By Recurrence:

- {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( -
- - {r.type === 'one_time' ? 'One-time' : r.billing_period} - - {fmtCents(r.amount_total)} -
- ))} +
+

By Recurrence

+
+ {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( +
+ + {r.type === 'one_time' ? 'One-time' : r.billing_period} + + {fmtCents(r.amount_total)} +
+ ))} +
)}
- {/* Usage */} -
+ {/* Code block */} +
` { +${enabledProducts + .slice(0, 4) + .map( + (p) => ` { quantity: ${p.quantity}, _price: { unit_amount_decimal: '${p.price}', @@ -353,12 +470,13 @@ ${enabledProducts.slice(0, 4).map((p) => ` { description: '${p.name}', }, taxes: [{ tax: { rate: ${taxRate} } }], - },`).join('\n')}${enabledProducts.length > 4 ? `\n // ... ${enabledProducts.length - 4} more items` : ''} + },`, + ) + .join('\n')}${enabledProducts.length > 4 ? `\n // ... ${enabledProducts.length - 4} more items` : ''} ]; const result = computeAggregatedAndPriceTotals(items); -// result.amount_total = ${fmtCents(result.amount_total)} -// Recurrence breakdown available via result.total_details.breakdown.recurrences`} +// result.amount_total = ${fmtCents(result.amount_total)}`} />
diff --git a/demo/src/sections/OverviewDemo.tsx b/demo/src/sections/OverviewDemo.tsx index 7846cfd..2e79e2d 100644 --- a/demo/src/sections/OverviewDemo.tsx +++ b/demo/src/sections/OverviewDemo.tsx @@ -1,194 +1,276 @@ import { CodeBlock } from '../components/CodeBlock'; +import { + SolarIllustration, + WallboxIllustration, + HeatPumpIllustration, + SmartHomeIllustration, +} from '../components/ProductShowcase'; interface OverviewDemoProps { onNavigate: (id: string) => void; } -const features = [ +const energyProducts = [ { - id: 'per-unit', - title: 'Per Unit Pricing', - desc: 'Simple price x quantity calculations with tax support', - icon: '๐Ÿ“ฆ', - color: 'bg-blue-50 border-blue-200', - }, - { - id: 'tiered-volume', - title: 'Tiered Volume', - desc: 'Single tier selected based on total quantity', - icon: '๐Ÿ“Š', - color: 'bg-indigo-50 border-indigo-200', - }, - { - id: 'tiered-graduated', - title: 'Tiered Graduated', - desc: 'Different rates apply to different quantity ranges', - icon: '๐Ÿ“ˆ', - color: 'bg-purple-50 border-purple-200', - }, - { - id: 'tiered-flatfee', - title: 'Tiered Flat Fee', - desc: 'Fixed fee based on quantity range', - icon: '๐Ÿท๏ธ', - color: 'bg-pink-50 border-pink-200', - }, - { - id: 'tax', - title: 'Tax Handling', - desc: 'Inclusive/exclusive tax with multi-rate breakdown', - icon: '๐Ÿงพ', - color: 'bg-green-50 border-green-200', - }, - { - id: 'discounts', - title: 'Discounts & Coupons', - desc: 'Fixed, percentage, and cashback coupon types', - icon: '๐ŸŽŸ๏ธ', - color: 'bg-amber-50 border-amber-200', - }, - { - id: 'composite', - title: 'Composite Pricing', - desc: 'Multi-component bundled price items', - icon: '๐Ÿงฉ', - color: 'bg-cyan-50 border-cyan-200', - }, - { - id: 'recurring', - title: 'Recurring Billing', - desc: 'Billing periods and frequency normalization', - icon: '๐Ÿ”„', - color: 'bg-teal-50 border-teal-200', + id: 'electricity', + title: 'Electricity Tariffs', + desc: 'Single & dual-tariff pricing with Grundpreis, Arbeitspreis, and smart meter support.', + icon: 'โšก', + gradient: 'gradient-electricity', + price: 'from 28.5 ct/kWh', }, { - id: 'currency', - title: 'Currency & Formatting', - desc: 'Multi-currency support with locale-aware formatting', - icon: '๐Ÿ’ฑ', - color: 'bg-emerald-50 border-emerald-200', + id: 'gas', + title: 'Gas Supply', + desc: 'Gas tariffs with CO2 levy, storage levy, and per-kWh work price breakdowns', + icon: '๐Ÿ”ฅ', + gradient: 'gradient-gas', + price: 'from 8.9 ct/kWh', }, { - id: 'dynamic-tariff', - title: 'Dynamic Tariff', - desc: 'Market-based pricing with configurable markup', - icon: 'โšก', - color: 'bg-yellow-50 border-yellow-200', + id: 'house-connection', + title: 'House Connection', + desc: 'Hausanschluss fees with distance-based trench work and multi-utility connections', + icon: '๐Ÿก', + gradient: 'gradient-house', + price: 'from EUR 1,850', }, { - id: 'getag', - title: 'GetAG Energy Pricing', - desc: 'German energy operator integration with tiered markups', - icon: '๐Ÿ”Œ', - color: 'bg-orange-50 border-orange-200', + id: 'non-commodity', + title: 'Products & Add-ons', + desc: 'Solar, wallboxes, heat pumps, and smart home bundles with service contracts', + icon: 'โ˜€๏ธ', + gradient: 'gradient-solar', + price: 'Bundles from EUR 899', }, ]; -const useCases = [ +const addOnShowcase = [ { - id: 'electricity', - title: 'Electricity', - desc: 'Single & dual-tariff (HT/NT) electricity pricing with Grundpreis and Arbeitspreis', - icon: 'โšก', - color: 'bg-yellow-50 border-yellow-200', + title: 'Solar & Battery', + desc: 'Complete PV systems with battery storage and installation', + illustration: SolarIllustration, + price: 'EUR 12,500', + sub: '+ EUR 29.90/mo maintenance', + features: ['10 kWp solar system', '10 kWh battery storage', 'Professional installation', 'Maintenance contract'], }, { - id: 'gas', - title: 'Gas', - desc: 'Gas supply tariffs with CO2 levy, gas storage levy, and per-kWh work price', - icon: '๐Ÿ”ฅ', - color: 'bg-orange-50 border-orange-200', + title: 'E-Mobility', + desc: 'Wallbox charging solutions for home and fleet', + illustration: WallboxIllustration, + price: 'EUR 899', + sub: '+ EUR 59/mo charging flat rate', + features: ['11 kW wallbox', 'Professional installation', 'Smart charging', 'Monthly flat rate'], }, { - id: 'house-connection', - title: 'House Connection', - desc: 'Hausanschluss fees with distance-based trench work and connection services', - icon: '๐Ÿก', - color: 'bg-emerald-50 border-emerald-200', + title: 'Heat Pumps', + desc: 'Modern heating solutions with smart controls', + illustration: HeatPumpIllustration, + price: 'EUR 15,800', + sub: '+ EUR 39.90/mo service', + features: ['Air-to-water system', 'Installation included', 'Smart thermostat', 'Service contract'], }, { - id: 'non-commodity', - title: 'Non-Commodity', - desc: 'Solar panels, wallboxes, heat pumps, and smart home products with service contracts', - icon: '๐Ÿ“‹', - color: 'bg-purple-50 border-purple-200', + title: 'Smart Home', + desc: 'Intelligent energy management and automation', + illustration: SmartHomeIllustration, + price: 'EUR 249', + sub: 'Energy manager from EUR 499', + features: ['Smart thermostat', 'Energy monitoring', 'App control', 'Consumption insights'], }, ]; +const capabilities = [ + { id: 'per-unit', title: 'Per Unit', icon: '๐Ÿ“ฆ' }, + { id: 'tiered-volume', title: 'Tiered Volume', icon: '๐Ÿ“Š' }, + { id: 'tiered-graduated', title: 'Graduated', icon: '๐Ÿ“ˆ' }, + { id: 'tiered-flatfee', title: 'Flat Fee', icon: '๐Ÿท๏ธ' }, + { id: 'tax', title: 'Tax', icon: '๐Ÿงพ' }, + { id: 'discounts', title: 'Discounts', icon: '๐ŸŽŸ๏ธ' }, + { id: 'composite', title: 'Composite', icon: '๐Ÿงฉ' }, + { id: 'recurring', title: 'Recurring', icon: '๐Ÿ”„' }, + { id: 'currency', title: 'Currency', icon: '๐Ÿ’ฑ' }, + { id: 'dynamic-tariff', title: 'Dynamic', icon: 'โšก' }, + { id: 'getag', title: 'GetAG', icon: '๐Ÿ”Œ' }, +]; + export function OverviewDemo({ onNavigate }: OverviewDemoProps) { return (
{/* Hero */} -
-

- epilot Pricing Playground +
+
+
+ Interactive Playground +
+

+ Energy Pricing, +
+ + Made Simple. +

-

- Interactive playground for @epilot/pricing โ€” a comprehensive - pricing calculation engine supporting 6 pricing models, tax handling, - discounts, composite pricing, recurring billing, multi-currency formatting, and - energy-market integrations. Explore each capability below. +

+ Configure tariffs, bundle products, and compute prices in real-time. From electricity and gas to solar panels + and wallboxes โ€” everything your sales team needs.

- {/* Stats */} -
+ {/* Stats row */} +
{[ - { label: 'Pricing Models', value: '6' }, - { label: 'Exported Functions', value: '40+' }, - { label: 'Billing Periods', value: '6' }, - { label: 'Decimal Precision', value: '12 digits' }, + { label: 'Pricing Models', value: '6', color: 'bg-blue-50 text-blue-700' }, + { label: 'Billing Periods', value: '6', color: 'bg-emerald-50 text-emerald-700' }, + { label: 'Functions', value: '40+', color: 'bg-amber-50 text-amber-700' }, + { label: 'Decimal Precision', value: '12 digits', color: 'bg-purple-50 text-purple-700' }, ].map((stat) => ( -
-

{stat.value}

-

{stat.label}

+
+ {stat.value} + {stat.label}
))}
- {/* Use Cases */} -

Energy & Utility Use Cases

-
- {useCases.map((f) => ( - ))}
- {/* Capabilities */} -

Capabilities

-
- {features.map((f) => ( + {/* Add-on Showcase with illustrations */} +

Products & Add-ons

+

Complete product bundles your customers can visualize and configure

+
+ {addOnShowcase.map((product) => { + const Illustration = product.illustration; + return ( + + ); + })} +
+ + {/* Capabilities strip */} +

Pricing Capabilities

+

Explore individual pricing models and features

+
+ {capabilities.map((c) => ( ))}
+ {/* Customer journey visualization */} +
+

Customer Journey

+
+ {[ + { + step: '1', + label: 'Browse Products', + desc: 'Customer selects tariff or product bundle', + icon: '๐Ÿ›’', + color: 'bg-blue-50 text-blue-700', + }, + { + step: '2', + label: 'Configure', + desc: 'Adjust consumption, select add-ons, set preferences', + icon: 'โš™๏ธ', + color: 'bg-amber-50 text-amber-700', + }, + { + step: '3', + label: 'Price Calculation', + desc: 'Real-time pricing with tax, discounts, recurrences', + icon: '๐Ÿ’ฐ', + color: 'bg-emerald-50 text-emerald-700', + }, + { + step: '4', + label: 'Order Summary', + desc: 'Clear breakdown for customer and sales team', + icon: 'โœ…', + color: 'bg-purple-50 text-purple-700', + }, + ].map((s, i) => ( +
+ {i > 0 && ( +
+ + + +
+ )} +
+
+ {s.icon} + Step {s.step} +
+

{s.label}

+

{s.desc}

+
+
+ ))} +
+
+ {/* Quick Start */} -
- +
+
- - {/* Architecture */} -
-

How It Works

-
- {[ - { step: '1', label: 'Price Items', desc: 'Define price, quantity, tax, coupons' }, - { step: '2', label: 'Compute', desc: 'computeAggregatedAndPriceTotals()' }, - { step: '3', label: 'Results', desc: 'Subtotals, tax, discounts, recurrences' }, - ].map((s, i) => ( -
- {i > 0 && ( - - - - )} -
-
- {s.step} -
-

{s.label}

-

{s.desc}

-
-
- ))} -
-
); } diff --git a/demo/src/sections/PerUnitDemo.tsx b/demo/src/sections/PerUnitDemo.tsx index d4ee7a0..77c31bb 100644 --- a/demo/src/sections/PerUnitDemo.tsx +++ b/demo/src/sections/PerUnitDemo.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { ResultCard } from '../components/ResultCard'; import { buildPriceItemDto, fmtCents } from '../helpers'; export function PerUnitDemo() { @@ -84,11 +84,7 @@ export function PerUnitDemo() {
- setCurrency(e.target.value)} className="select-field mt-1"> @@ -123,38 +119,18 @@ export function PerUnitDemo() {

Computed Result

- - - - + + + +

Aggregated Totals

- - + + > = { export function RecurringBillingDemo() { const [unitPrice, setUnitPrice] = useState('29.99'); const [basePeriod, setBasePeriod] = useState('monthly'); - const [quantity, setQuantity] = useState(1); - const [taxRate, setTaxRate] = useState(19); + const [quantity] = useState(1); + const [taxRate] = useState(19); const [isTaxInclusive, setIsTaxInclusive] = useState(true); const result = useMemo(() => { @@ -48,11 +48,7 @@ export function RecurringBillingDemo() { return periods.map((p) => { try { - const normalized = normalizeValueToFrequencyUnit( - unitPrice, - basePeriod as any, - p.value as any, - ); + const normalized = normalizeValueToFrequencyUnit(unitPrice, basePeriod as any, p.value as any); return { period: p.label, periodValue: p.value, @@ -109,8 +105,8 @@ export function RecurringBillingDemo() {

Recurring Billing

- Support for one-time and recurring prices with frequency normalization. - Convert prices between billing periods automatically. + Support for one-time and recurring prices with frequency normalization. Convert prices between billing periods + automatically.

@@ -136,7 +132,9 @@ export function RecurringBillingDemo() { className="select-field mt-1" > {periods.map((p) => ( - + ))}
@@ -180,9 +178,7 @@ export function RecurringBillingDemo() {
@@ -210,8 +206,8 @@ export function RecurringBillingDemo() { {showMixed && (

- Three items: a one-time setup fee, a monthly subscription, and an annual license. - The library groups totals by recurrence type. + Three items: a one-time setup fee, a monthly subscription, and an annual license. The library groups + totals by recurrence type.

@@ -219,9 +215,7 @@ export function RecurringBillingDemo() { (label, i) => (

{label}

-

- {fmtCents(mixedResult.items?.[i]?.amount_total)} -

+

{fmtCents(mixedResult.items?.[i]?.amount_total)}

), )} diff --git a/demo/src/sections/TaxDemo.tsx b/demo/src/sections/TaxDemo.tsx index afc65cc..3fb6c9d 100644 --- a/demo/src/sections/TaxDemo.tsx +++ b/demo/src/sections/TaxDemo.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { ResultCard } from '../components/ResultCard'; import { buildPriceItemDto, fmtCents } from '../helpers'; export function TaxDemo() { @@ -57,8 +57,8 @@ export function TaxDemo() {

Tax Handling

- Compare tax-inclusive vs tax-exclusive pricing side by side. The library handles tax calculations - precisely using Dinero.js for decimal arithmetic. + Compare tax-inclusive vs tax-exclusive pricing side by side. The library handles tax calculations precisely + using Dinero.js for decimal arithmetic.

{/* Controls */} @@ -89,11 +89,7 @@ export function TaxDemo() {
- setTaxRate(Number(e.target.value))} className="select-field mt-1"> @@ -118,11 +114,16 @@ export function TaxDemo() { - +
- The โ‚ฌ{unitPrice} price already contains {taxRate}% tax. - Net = โ‚ฌ{unitPrice} / 1.{String(taxRate).padStart(2, '0')} per unit. + The โ‚ฌ{unitPrice} price already contains {taxRate}% tax. Net = โ‚ฌ{unitPrice} / 1. + {String(taxRate).padStart(2, '0')} per unit.
@@ -137,11 +138,16 @@ export function TaxDemo() { - +
- โ‚ฌ{unitPrice} is the net price. {taxRate}% tax is added on top. - Gross = โ‚ฌ{unitPrice} * 1.{String(taxRate).padStart(2, '0')} per unit. + โ‚ฌ{unitPrice} is the net price. {taxRate}% tax is added on top. Gross = โ‚ฌ{unitPrice} * 1. + {String(taxRate).padStart(2, '0')} per unit.
@@ -157,8 +163,7 @@ export function TaxDemo() { {showMultiTax && (

- Two items with different tax rates (19% standard, 7% reduced). The library tracks - tax breakdown by rate. + Two items with different tax rates (19% standard, 7% reduced). The library tracks tax breakdown by rate.

@@ -174,7 +179,9 @@ export function TaxDemo() { return (
{rate}% - {type} {rate}% + + {type} {rate}% + {fmtCents(t.amount)}
); diff --git a/demo/src/sections/TieredFlatFeeDemo.tsx b/demo/src/sections/TieredFlatFeeDemo.tsx index 685b499..43c6c38 100644 --- a/demo/src/sections/TieredFlatFeeDemo.tsx +++ b/demo/src/sections/TieredFlatFeeDemo.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { ResultCard } from '../components/ResultCard'; import { buildPriceItemDto, fmtCents } from '../helpers'; const defaultTiers = [ @@ -14,7 +14,7 @@ const defaultTiers = [ export function TieredFlatFeeDemo() { const [quantity, setQuantity] = useState(30); const [tiers, setTiers] = useState(defaultTiers); - const [taxRate, setTaxRate] = useState(19); + const [taxRate] = useState(19); const [isTaxInclusive, setIsTaxInclusive] = useState(true); const result = useMemo(() => { @@ -46,8 +46,8 @@ export function TieredFlatFeeDemo() {

Tiered Flat Fee

- A single fixed fee is charged based on the quantity range. Unlike volume pricing, - the fee does not multiply by quantity. + A single fixed fee is charged based on the quantity range. Unlike volume pricing, the fee does{' '} + not multiply by quantity.

@@ -85,9 +85,7 @@ export function TieredFlatFeeDemo() { className="input-field w-28" /> - - {idx === activeTierIdx && Selected} - + {idx === activeTierIdx && Selected} ))} @@ -140,9 +138,7 @@ export function TieredFlatFeeDemo() { โ‚ฌ{tier.flat_fee_amount_decimal} - {isActive && ( - โ† Your plan - )} + {isActive && โ† Your plan}
); })} diff --git a/demo/src/sections/TieredGraduatedDemo.tsx b/demo/src/sections/TieredGraduatedDemo.tsx index c6c26b9..ad4fb66 100644 --- a/demo/src/sections/TieredGraduatedDemo.tsx +++ b/demo/src/sections/TieredGraduatedDemo.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; -import { ResultCard } from '../components/ResultCard'; +import { useState, useMemo } from 'react'; import { CodeBlock } from '../components/CodeBlock'; +import { ResultCard } from '../components/ResultCard'; import { buildPriceItemDto, fmtCents } from '../helpers'; const defaultTiers = [ @@ -76,8 +76,8 @@ export function TieredGraduatedDemo() {

Tiered Graduated Pricing

- Units are spread across tiers. Each tier charges its own rate for the units within its range. - This is the "graduated" model used by many SaaS platforms. + Units are spread across tiers. Each tier charges its own rate for the units within its range. This is the + "graduated" model used by many SaaS platforms.

@@ -184,19 +184,10 @@ export function TieredGraduatedDemo() {
- + 0 - ? fmtCents(Math.round((result.amount_total ?? 0) / quantity)) - : '-' - } + value={quantity > 0 ? fmtCents(Math.round((result.amount_total ?? 0) / quantity)) : '-'} color="blue" />
diff --git a/demo/src/sections/TieredVolumeDemo.tsx b/demo/src/sections/TieredVolumeDemo.tsx index 7605d64..0660a6e 100644 --- a/demo/src/sections/TieredVolumeDemo.tsx +++ b/demo/src/sections/TieredVolumeDemo.tsx @@ -1,8 +1,8 @@ -import { useState, useMemo } from 'react'; import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { useState, useMemo } from 'react'; +import { CodeBlock } from '../components/CodeBlock'; import { ResultCard } from '../components/ResultCard'; import { TierChart } from '../components/TierChart'; -import { CodeBlock } from '../components/CodeBlock'; import { buildPriceItemDto, fmtCents } from '../helpers'; const defaultTiers = [ @@ -15,7 +15,7 @@ const defaultTiers = [ export function TieredVolumeDemo() { const [quantity, setQuantity] = useState(25); const [tiers, setTiers] = useState(defaultTiers); - const [taxRate, setTaxRate] = useState(19); + const [taxRate] = useState(19); const [isTaxInclusive, setIsTaxInclusive] = useState(true); const result = useMemo(() => { @@ -65,7 +65,8 @@ export function TieredVolumeDemo() {

Tiered Volume Pricing

- A single tier is selected based on total quantity. The selected tier's unit price applies to all units. + A single tier is selected based on total quantity. The selected tier's unit price applies to{' '} + all units.

@@ -104,9 +105,7 @@ export function TieredVolumeDemo() { className="input-field w-24" /> - - {idx === activeTierIdx && Active} - + {idx === activeTierIdx && Active} ))} @@ -158,17 +157,14 @@ export function TieredVolumeDemo() {

Computed Result

- +
- How it works: With quantity {quantity}, tier {activeTierIdx + 1} is selected - ({tiers[activeTierIdx]?.unit_amount_decimal}/unit). All {quantity} units use this price. + How it works: With quantity {quantity}, tier {activeTierIdx + 1} is selected ( + {tiers[activeTierIdx]?.unit_amount_decimal}/unit). All {quantity} units use this price.
diff --git a/demo/tsconfig.tsbuildinfo b/demo/tsconfig.tsbuildinfo new file mode 100644 index 0000000..328de2a --- /dev/null +++ b/demo/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/helpers.ts","./src/main.tsx","./src/components/codeblock.tsx","./src/components/resultcard.tsx","./src/components/tierchart.tsx","./src/sections/compositepricedemo.tsx","./src/sections/currencydemo.tsx","./src/sections/discountdemo.tsx","./src/sections/dynamictariffdemo.tsx","./src/sections/electricitydemo.tsx","./src/sections/gasdemo.tsx","./src/sections/getagdemo.tsx","./src/sections/houseconnectiondemo.tsx","./src/sections/noncommoditydemo.tsx","./src/sections/overviewdemo.tsx","./src/sections/perunitdemo.tsx","./src/sections/recurringbillingdemo.tsx","./src/sections/taxdemo.tsx","./src/sections/tieredflatfeedemo.tsx","./src/sections/tieredgraduateddemo.tsx","./src/sections/tieredvolumedemo.tsx"],"version":"5.9.3"} \ No newline at end of file