diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..27f6118 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,199 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { Section } from '@/components/Section'; +import { bookingCatalog, formatPoundsFromCents } from '@/lib/booking'; +import { contentPages, site, featuredProperties } from '@/lib/site'; + +const adminAreas = [ + { + id: 'properties', + title: 'Properties', + description: 'Create, publish, and maintain the public inventory.', + actions: ['Create property', 'Edit details', 'Publish / unpublish', 'Archive property'], + }, + { + id: 'pricing', + title: 'Availability and pricing', + description: 'Set rules that control quoting, seasonal pricing, and holds.', + actions: ['Add pricing rule', 'Add availability block', 'Override season rate', 'Review hold expiry'], + }, + { + id: 'bookings', + title: 'Bookings and payments', + description: 'Track payment truth, booking states, and recovery steps.', + actions: ['Review booking state', 'Inspect payment record', 'Reconcile webhook event', 'Confirm notification'], + }, + { + id: 'content', + title: 'Content and testimonials', + description: 'Edit the public copy without altering the booking model.', + actions: ['Edit page copy', 'Publish FAQ update', 'Manage testimonials', 'Adjust SEO metadata'], + }, + { + id: 'settings', + title: 'Site settings', + description: 'Keep the business name, contact details, and booking rules aligned.', + actions: ['Update contact details', 'Adjust booking hold', 'Update defaults', 'Review guardrails'], + }, +]; + +const managementNotes = [ + 'Admin access is scoped to the back office surface and is expected to require login before mutation actions are enabled.', + 'Booking and payment records remain read-only truth sources until the webhook flow validates them.', + 'Availability and pricing overrides should always be visible in the admin UI before they affect the public quote path.', +]; + +export const metadata: Metadata = { + title: `Admin console | ${site.name}`, + description: 'Admin console planning surface for properties, bookings, pricing, content, and settings.', +}; + +export default function AdminPage() { + return ( + <> +
+

Admin console

+

Operations control room for Holiday Property Booking

+

+ This console gives the day-to-day shape for managing properties, pricing, bookings, content, and settings + without weakening the payment truth model. +

+ +
+
+

Published properties

+ {featuredProperties.length} + Catalog entries available to guests +
+
+

Booking rules

+ {bookingCatalog.reduce((sum, property) => sum + property.seasonalRates.length + property.availabilityBlocks.length, 0)} + Seasonal and availability overrides captured +
+
+

Editable pages

+ {contentPages.length} + Public content routes ready for editing +
+
+
+ +
+
+
+ {adminAreas.map((area) => ( +
+

{area.id}

+

{area.title}

+

{area.description}

+
    + {area.actions.map((action) => ( +
  • {action}
  • + ))} +
+
+ ))} +
+
+ + +
+ +
+
+
+ Property + Status + Rate from + Rules +
+ {bookingCatalog.map((property) => ( +
+ + {property.name} + {property.area} + + {property.published ? 'Published' : 'Draft'} + {formatPoundsFromCents(property.baseNightlyCents)} + + {property.seasonalRates.length} seasonal / {property.availabilityBlocks.length} availability block(s) + +
+ ))} +
+
+ +
+
+
+

Booking states

+
    +
  • Pending payment before Stripe verification
  • +
  • Payment received once the webhook confirms the event
  • +
  • Confirmed after the booking is safe to display as truth
  • +
  • Cancelled or failed when payment or availability breaks
  • +
+
+
+

Webhook review

+

+ The webhook log view will be the audit point for checkout sessions, payment intents, and notification + triggers. +

+
+
+
+ +
+
+ {contentPages.map((page) => ( +
+

{page.slug}

+

{page.title}

+

{page.seoDescription}

+
+ ))} +
+
+ + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b980b88..5ddd2c7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -291,7 +291,8 @@ main { .search-field input, .contact-form input, -.contact-form textarea { +.contact-form textarea, +.contact-form select { width: 100%; border: 1px solid rgba(26, 23, 20, 0.14); border-radius: 0.9rem; @@ -302,7 +303,8 @@ main { .search-field input:focus, .contact-form input:focus, -.contact-form textarea:focus { +.contact-form textarea:focus, +.contact-form select:focus { outline: 2px solid rgba(122, 84, 61, 0.28); outline-offset: 2px; } @@ -571,6 +573,100 @@ main { letter-spacing: -0.04em; } +.admin-hero { + display: grid; + gap: 1rem; +} + +.admin-hero-grid, +.admin-area-grid, +.admin-summary-grid, +.admin-content-grid { + display: grid; + gap: 1rem; +} + +.admin-hero-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.admin-layout { + align-items: start; +} + +.admin-sidebar { + align-self: start; +} + +.admin-metric-card, +.admin-card { + border: 1px solid var(--panel-border); + border-radius: 1.35rem; + background: rgba(255, 255, 255, 0.82); + padding: 1rem; +} + +.admin-metric-card strong { + display: block; + margin-bottom: 0.3rem; + font-size: 2rem; +} + +.admin-metric-card span { + color: var(--text-muted); +} + +.admin-area-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.admin-summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.admin-content-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.admin-action-list, +.admin-bullet-list, +.admin-notes { + margin: 0.75rem 0 0; + padding-left: 1.1rem; + color: var(--text-muted); +} + +.admin-table { + display: grid; + gap: 0.6rem; +} + +.admin-table-row { + display: grid; + grid-template-columns: 1.8fr 0.8fr 0.7fr 1.2fr; + gap: 0.8rem; + align-items: start; + padding: 0.85rem 1rem; + border-radius: 1rem; + background: rgba(255, 255, 255, 0.82); + border: 1px solid var(--panel-border); +} + +.admin-table-head { + background: rgba(46, 102, 97, 0.08); + font-weight: 700; +} + +.admin-table-row strong { + display: block; +} + +.admin-table-row small { + display: block; + color: var(--text-muted); + margin-top: 0.15rem; +} + .page-layout { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.75fr);