Merge branch 'feature/vik-115-admin-console' into develop
This commit is contained in:
199
src/app/admin/page.tsx
Normal file
199
src/app/admin/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user