Add admin console planning surface

This commit is contained in:
2026-05-24 23:53:59 +00:00
parent 42c4482341
commit ef5e36b63d
2 changed files with 297 additions and 2 deletions

199
src/app/admin/page.tsx Normal file
View File

@@ -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 (
<>
<section className="page-hero admin-hero">
<p className="brand-kicker">Admin console</p>
<h2>Operations control room for Holiday Property Booking</h2>
<p>
This console gives the day-to-day shape for managing properties, pricing, bookings, content, and settings
without weakening the payment truth model.
</p>
<div className="admin-hero-grid">
<article className="admin-metric-card">
<p className="footer-label">Published properties</p>
<strong>{featuredProperties.length}</strong>
<span>Catalog entries available to guests</span>
</article>
<article className="admin-metric-card">
<p className="footer-label">Booking rules</p>
<strong>{bookingCatalog.reduce((sum, property) => sum + property.seasonalRates.length + property.availabilityBlocks.length, 0)}</strong>
<span>Seasonal and availability overrides captured</span>
</article>
<article className="admin-metric-card">
<p className="footer-label">Editable pages</p>
<strong>{contentPages.length}</strong>
<span>Public content routes ready for editing</span>
</article>
</div>
</section>
<div className="page-layout admin-layout">
<Section
eyebrow="Primary areas"
title="Back-office screens follow the way the business is actually run"
description="The layout is grouped around the daily operating tasks rather than a generic CMS tree."
>
<div className="admin-area-grid">
{adminAreas.map((area) => (
<article key={area.id} className="admin-card">
<p className="footer-label">{area.id}</p>
<h3>{area.title}</h3>
<p>{area.description}</p>
<ul className="admin-action-list">
{area.actions.map((action) => (
<li key={action}>{action}</li>
))}
</ul>
</article>
))}
</div>
</Section>
<aside className="content-sidebar admin-sidebar">
<article className="content-card">
<p className="footer-label">Guardrails</p>
<ul className="admin-notes">
{managementNotes.map((note) => (
<li key={note}>{note}</li>
))}
</ul>
</article>
<article className="content-card">
<h3>Quick links</h3>
<ul className="link-list">
<li>
<Link href="/">Public site</Link>
</li>
<li>
<Link href="/contact">Contact page</Link>
</li>
<li>
<Link href="/faqs">FAQs</Link>
</li>
</ul>
</article>
</aside>
</div>
<Section
eyebrow="Property management"
title="Each property keeps its own pricing, availability, and publish state"
description="The inventory is shaped to support multiple properties without special-casing a single listing."
>
<div className="admin-table">
<div className="admin-table-row admin-table-head">
<span>Property</span>
<span>Status</span>
<span>Rate from</span>
<span>Rules</span>
</div>
{bookingCatalog.map((property) => (
<div key={property.slug} className="admin-table-row">
<span>
<strong>{property.name}</strong>
<small>{property.area}</small>
</span>
<span>{property.published ? 'Published' : 'Draft'}</span>
<span>{formatPoundsFromCents(property.baseNightlyCents)}</span>
<span>
{property.seasonalRates.length} seasonal / {property.availabilityBlocks.length} availability block(s)
</span>
</div>
))}
</div>
</Section>
<Section
eyebrow="Payments and bookings"
title="Booking records stay separate from payment truth"
description="The admin surface makes it obvious where bookings are pending, paid, confirmed, or blocked."
>
<div className="admin-summary-grid">
<article className="admin-card">
<h3>Booking states</h3>
<ul className="admin-bullet-list">
<li>Pending payment before Stripe verification</li>
<li>Payment received once the webhook confirms the event</li>
<li>Confirmed after the booking is safe to display as truth</li>
<li>Cancelled or failed when payment or availability breaks</li>
</ul>
</article>
<article className="admin-card">
<h3>Webhook review</h3>
<p>
The webhook log view will be the audit point for checkout sessions, payment intents, and notification
triggers.
</p>
</article>
</div>
</Section>
<Section
eyebrow="Content"
title="Editorial pages are managed separately from booking data"
description="Content, SEO, and testimonials stay editable without coupling them to booking rules."
>
<div className="admin-content-grid">
{contentPages.map((page) => (
<article key={page.slug} className="admin-card">
<p className="footer-label">{page.slug}</p>
<h3>{page.title}</h3>
<p>{page.seoDescription}</p>
</article>
))}
</div>
</Section>
</>
);
}

View File

@@ -291,7 +291,8 @@ main {
.search-field input, .search-field input,
.contact-form input, .contact-form input,
.contact-form textarea { .contact-form textarea,
.contact-form select {
width: 100%; width: 100%;
border: 1px solid rgba(26, 23, 20, 0.14); border: 1px solid rgba(26, 23, 20, 0.14);
border-radius: 0.9rem; border-radius: 0.9rem;
@@ -302,7 +303,8 @@ main {
.search-field input:focus, .search-field input:focus,
.contact-form 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: 2px solid rgba(122, 84, 61, 0.28);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -571,6 +573,100 @@ main {
letter-spacing: -0.04em; 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 { .page-layout {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.75fr); grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.75fr);