Updated the header of these pages to match the rest of the pages.
This commit is contained in:
4 files changed
+216
-209
No files matched your search
@@ -3,6 +3,7 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { TopNav } from "@/components/navigation/top-nav";
|
||||||
import { prisma } from "@/lib/db/prisma";
|
import { prisma } from "@/lib/db/prisma";
|
||||||
import { hasPermission } from "@/lib/auth/permissions";
|
import { hasPermission } from "@/lib/auth/permissions";
|
||||||
import { Permission } from "@/generated/prisma/enums";
|
import { Permission } from "@/generated/prisma/enums";
|
||||||
@@ -70,132 +71,132 @@ export default async function OwnerReportsPage({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-6 pt-12 pb-12">
|
<div className="pb-12">
|
||||||
<div>
|
<TopNav title="Reports" onBack sticky className="bg-stone" />
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Reports</h1>
|
<main className="space-y-6 px-6 py-6">
|
||||||
<p className="text-muted text-sm">
|
<p className="text-muted text-sm">
|
||||||
{fromDate} to {toDate}
|
{fromDate} to {toDate}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{PRESETS.map((preset) => (
|
{PRESETS.map((preset) => (
|
||||||
<Link
|
<Link
|
||||||
key={preset.days}
|
key={preset.days}
|
||||||
href={`/owner/reports?from=${addDays(today, -(preset.days - 1))}&to=${today}`}
|
href={`/owner/reports?from=${addDays(today, -(preset.days - 1))}&to=${today}`}
|
||||||
className="border-line rounded-full border bg-white px-4 py-2 text-xs font-bold"
|
className="border-line rounded-full border bg-white px-4 py-2 text-xs font-bold"
|
||||||
>
|
>
|
||||||
Last {preset.label}
|
Last {preset.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Appointments</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
||||||
<StatTile label="Total" value={appointmentReport.totalAppointments} />
|
|
||||||
<StatTile label="Completed" value={appointmentReport.completed} hint={`${appointmentReport.completionRate}% of total`} />
|
|
||||||
<StatTile label="Cancelled" value={appointmentReport.cancelled} hint={`${appointmentReport.cancellationRate}% of total`} />
|
|
||||||
<StatTile label="No-shows" value={appointmentReport.noShow} hint={`${appointmentReport.noShowRate}% of total`} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
<StatTile label="Gross revenue" value={`£${appointmentReport.grossRevenue}`} />
|
|
||||||
<StatTile label="Discounts given" value={`£${appointmentReport.discountsGiven}`} />
|
|
||||||
<StatTile label="Net revenue" value={`£${appointmentReport.netRevenue}`} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Services</h2>
|
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Appointments</h2>
|
||||||
<Card className="divide-line divide-y px-5">
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
{serviceReport.length === 0 ? (
|
<StatTile label="Total" value={appointmentReport.totalAppointments} />
|
||||||
<p className="text-muted py-4 text-sm">No completed appointments in this range.</p>
|
<StatTile label="Completed" value={appointmentReport.completed} hint={`${appointmentReport.completionRate}% of total`} />
|
||||||
) : (
|
<StatTile label="Cancelled" value={appointmentReport.cancelled} hint={`${appointmentReport.cancellationRate}% of total`} />
|
||||||
serviceReport.map((s) => (
|
<StatTile label="No-shows" value={appointmentReport.noShow} hint={`${appointmentReport.noShowRate}% of total`} />
|
||||||
<div key={s.serviceId} className="flex items-center justify-between py-3 text-sm">
|
</div>
|
||||||
<div>
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<p className="font-bold">{s.name}</p>
|
<StatTile label="Gross revenue" value={`£${appointmentReport.grossRevenue}`} />
|
||||||
<p className="text-muted text-xs">{s.timesCompleted} completed</p>
|
<StatTile label="Discounts given" value={`£${appointmentReport.discountsGiven}`} />
|
||||||
</div>
|
<StatTile label="Net revenue" value={`£${appointmentReport.netRevenue}`} />
|
||||||
<span className="font-bold">£{s.revenue}</span>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Staff</h2>
|
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Services</h2>
|
||||||
<Card className="divide-line divide-y px-5">
|
|
||||||
{staffReport.length === 0 ? (
|
|
||||||
<p className="text-muted py-4 text-sm">No appointments in this range.</p>
|
|
||||||
) : (
|
|
||||||
staffReport.map((s) => (
|
|
||||||
<div key={s.staffId} className="flex items-center justify-between py-3 text-sm">
|
|
||||||
<div>
|
|
||||||
<p className="font-bold">{s.name}</p>
|
|
||||||
<p className="text-muted text-xs">
|
|
||||||
{s.completed} completed · {s.cancelled} cancelled · {s.noShow} no-show
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="font-bold">£{s.revenue}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Customers</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<StatTile label="New customers" value={customerReport.newCustomers} />
|
|
||||||
<StatTile label="Returning customers" value={customerReport.returningCustomers} />
|
|
||||||
</div>
|
|
||||||
{customerReport.topCustomers.length > 0 && (
|
|
||||||
<Card className="divide-line divide-y px-5">
|
<Card className="divide-line divide-y px-5">
|
||||||
{customerReport.topCustomers.map((c) => (
|
{serviceReport.length === 0 ? (
|
||||||
<div key={c.customerId} className="flex items-center justify-between py-3 text-sm">
|
<p className="text-muted py-4 text-sm">No completed appointments in this range.</p>
|
||||||
<div>
|
) : (
|
||||||
<p className="font-bold">{c.name}</p>
|
serviceReport.map((s) => (
|
||||||
<p className="text-muted text-xs">{c.visits} visit{c.visits === 1 ? "" : "s"}</p>
|
<div key={s.serviceId} className="flex items-center justify-between py-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold">{s.name}</p>
|
||||||
|
<p className="text-muted text-xs">{s.timesCompleted} completed</p>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold">£{s.revenue}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold">£{c.spend}</span>
|
))
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Loyalty</h2>
|
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Staff</h2>
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
||||||
<StatTile label="Stamps issued" value={loyaltyReport.stampsIssued} />
|
|
||||||
<StatTile label="Stamps removed" value={loyaltyReport.stampsRemoved} />
|
|
||||||
<StatTile label="Rewards earned" value={loyaltyReport.rewardsEarned} />
|
|
||||||
<StatTile label="Rewards redeemed" value={loyaltyReport.rewardsRedeemed} />
|
|
||||||
</div>
|
|
||||||
{loyaltyReport.rewardsExpired > 0 && <StatTile label="Rewards expired" value={loyaltyReport.rewardsExpired} />}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Notifications</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<StatTile label="Sent" value={notificationReport.totalSent} />
|
|
||||||
<StatTile label="Failed" value={notificationReport.totalFailed} />
|
|
||||||
</div>
|
|
||||||
{notificationReport.byChannel.length > 0 && (
|
|
||||||
<Card className="divide-line divide-y px-5">
|
<Card className="divide-line divide-y px-5">
|
||||||
{notificationReport.byChannel.map((c) => (
|
{staffReport.length === 0 ? (
|
||||||
<div key={c.channel} className="flex items-center justify-between py-3 text-sm">
|
<p className="text-muted py-4 text-sm">No appointments in this range.</p>
|
||||||
<span className="font-bold">{c.channel}</span>
|
) : (
|
||||||
<span className="text-muted">
|
staffReport.map((s) => (
|
||||||
{c.sent} sent{c.failed > 0 ? ` · ${c.failed} failed` : ""}
|
<div key={s.staffId} className="flex items-center justify-between py-3 text-sm">
|
||||||
</span>
|
<div>
|
||||||
</div>
|
<p className="font-bold">{s.name}</p>
|
||||||
))}
|
<p className="text-muted text-xs">
|
||||||
|
{s.completed} completed · {s.cancelled} cancelled · {s.noShow} no-show
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold">£{s.revenue}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
</section>
|
||||||
</section>
|
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Customers</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<StatTile label="New customers" value={customerReport.newCustomers} />
|
||||||
|
<StatTile label="Returning customers" value={customerReport.returningCustomers} />
|
||||||
|
</div>
|
||||||
|
{customerReport.topCustomers.length > 0 && (
|
||||||
|
<Card className="divide-line divide-y px-5">
|
||||||
|
{customerReport.topCustomers.map((c) => (
|
||||||
|
<div key={c.customerId} className="flex items-center justify-between py-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold">{c.name}</p>
|
||||||
|
<p className="text-muted text-xs">{c.visits} visit{c.visits === 1 ? "" : "s"}</p>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold">£{c.spend}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Loyalty</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
|
<StatTile label="Stamps issued" value={loyaltyReport.stampsIssued} />
|
||||||
|
<StatTile label="Stamps removed" value={loyaltyReport.stampsRemoved} />
|
||||||
|
<StatTile label="Rewards earned" value={loyaltyReport.rewardsEarned} />
|
||||||
|
<StatTile label="Rewards redeemed" value={loyaltyReport.rewardsRedeemed} />
|
||||||
|
</div>
|
||||||
|
{loyaltyReport.rewardsExpired > 0 && <StatTile label="Rewards expired" value={loyaltyReport.rewardsExpired} />}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-muted text-xs font-bold tracking-widest uppercase">Notifications</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<StatTile label="Sent" value={notificationReport.totalSent} />
|
||||||
|
<StatTile label="Failed" value={notificationReport.totalFailed} />
|
||||||
|
</div>
|
||||||
|
{notificationReport.byChannel.length > 0 && (
|
||||||
|
<Card className="divide-line divide-y px-5">
|
||||||
|
{notificationReport.byChannel.map((c) => (
|
||||||
|
<div key={c.channel} className="flex items-center justify-between py-3 text-sm">
|
||||||
|
<span className="font-bold">{c.channel}</span>
|
||||||
|
<span className="text-muted">
|
||||||
|
{c.sent} sent{c.failed > 0 ? ` · ${c.failed} failed` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
|
import { TopNav } from "@/components/navigation/top-nav";
|
||||||
import { prisma } from "@/lib/db/prisma";
|
import { prisma } from "@/lib/db/prisma";
|
||||||
import { hasPermission } from "@/lib/auth/permissions";
|
import { hasPermission } from "@/lib/auth/permissions";
|
||||||
import { Permission } from "@/generated/prisma/enums";
|
import { Permission } from "@/generated/prisma/enums";
|
||||||
@@ -24,28 +25,30 @@ export default async function OwnerServicesPage() {
|
|||||||
const categories = await listCategories(prisma, context.businessId);
|
const categories = await listCategories(prisma, context.businessId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 pt-12 pb-12 lg:max-w-3xl">
|
<div className="pb-12">
|
||||||
<h1 className="mb-6 text-2xl font-bold tracking-tight">Services</h1>
|
<TopNav title="Services" onBack sticky className="bg-stone" />
|
||||||
<CatalogueManager
|
<main className="px-6 py-6 lg:max-w-3xl">
|
||||||
categories={categories.map((c) => ({
|
<CatalogueManager
|
||||||
id: c.id,
|
categories={categories.map((c) => ({
|
||||||
name: c.name,
|
id: c.id,
|
||||||
isActive: c.isActive,
|
name: c.name,
|
||||||
services: c.services.map((s) => ({
|
isActive: c.isActive,
|
||||||
id: s.id,
|
services: c.services.map((s) => ({
|
||||||
categoryId: s.categoryId,
|
id: s.id,
|
||||||
name: s.name,
|
categoryId: s.categoryId,
|
||||||
description: s.description ?? "",
|
name: s.name,
|
||||||
imageUrl: s.imageUrl,
|
description: s.description ?? "",
|
||||||
durationMinutes: s.durationMinutes,
|
imageUrl: s.imageUrl,
|
||||||
price: s.price.toString(),
|
durationMinutes: s.durationMinutes,
|
||||||
bufferBeforeMinutes: s.bufferBeforeMinutes,
|
price: s.price.toString(),
|
||||||
bufferAfterMinutes: s.bufferAfterMinutes,
|
bufferBeforeMinutes: s.bufferBeforeMinutes,
|
||||||
isActive: s.isActive,
|
bufferAfterMinutes: s.bufferAfterMinutes,
|
||||||
allowOnlineBooking: s.allowOnlineBooking,
|
isActive: s.isActive,
|
||||||
})),
|
allowOnlineBooking: s.allowOnlineBooking,
|
||||||
}))}
|
})),
|
||||||
/>
|
}))}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
|||||||
import { Plus, Search, ShieldCheck, ShieldPlus } from "lucide-react";
|
import { Plus, Search, ShieldCheck, ShieldPlus } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
|
import { TopNav } from "@/components/navigation/top-nav";
|
||||||
import { prisma } from "@/lib/db/prisma";
|
import { prisma } from "@/lib/db/prisma";
|
||||||
import { hasPermission } from "@/lib/auth/permissions";
|
import { hasPermission } from "@/lib/auth/permissions";
|
||||||
import { Permission } from "@/generated/prisma/enums";
|
import { Permission } from "@/generated/prisma/enums";
|
||||||
@@ -30,80 +31,82 @@ export default async function OwnerTeamPage({
|
|||||||
const { q = "" } = await searchParams;
|
const { q = "" } = await searchParams;
|
||||||
const staff = await listStaffForBusiness(prisma, { businessId: context.businessId, q, includeInactive: true });
|
const staff = await listStaffForBusiness(prisma, { businessId: context.businessId, q, includeInactive: true });
|
||||||
|
|
||||||
|
const headerAction = (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link
|
||||||
|
href="/owner/team/add-administrator"
|
||||||
|
className="border-line text-ink flex h-10 w-10 items-center justify-center rounded-full border"
|
||||||
|
aria-label="Invite administrator"
|
||||||
|
title="Invite Administrator"
|
||||||
|
>
|
||||||
|
<ShieldPlus className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/owner/team/add"
|
||||||
|
className="bg-ink flex h-10 w-10 items-center justify-center rounded-full text-white"
|
||||||
|
aria-label="Add staff member"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 pt-12 pb-12 lg:max-w-4xl">
|
<div className="pb-12">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<TopNav title="Team" onBack sticky className="bg-stone" action={headerAction} />
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Team</h1>
|
<main className="px-6 py-6 lg:max-w-4xl">
|
||||||
<div className="flex items-center gap-2">
|
<form action="/owner/team" method="GET" className="relative mb-4">
|
||||||
<Link
|
<Search className="text-muted absolute top-1/2 left-4 h-4 w-4 -translate-y-1/2" aria-hidden="true" />
|
||||||
href="/owner/team/add-administrator"
|
<label htmlFor="team-search" className="sr-only">
|
||||||
className="border-line text-ink flex h-10 w-10 items-center justify-center rounded-full border"
|
Search staff by name or email
|
||||||
aria-label="Invite administrator"
|
</label>
|
||||||
title="Invite Administrator"
|
<input
|
||||||
>
|
id="team-search"
|
||||||
<ShieldPlus className="h-4 w-4" aria-hidden="true" />
|
type="search"
|
||||||
</Link>
|
name="q"
|
||||||
<Link
|
defaultValue={q}
|
||||||
href="/owner/team/add"
|
placeholder="Search by name or email"
|
||||||
className="bg-ink flex h-10 w-10 items-center justify-center rounded-full text-white"
|
className="border-line focus:ring-ink w-full rounded-xl border bg-white py-3 pr-4 pl-11 text-sm outline-none focus:ring-2"
|
||||||
aria-label="Add staff member"
|
/>
|
||||||
>
|
</form>
|
||||||
<Plus className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</Link>
|
<div className="grid gap-3 lg:grid-cols-2">
|
||||||
|
{staff.length === 0 ? (
|
||||||
|
<p className="text-muted col-span-full rounded-md border border-line bg-white p-5 text-center text-sm">
|
||||||
|
No staff match “{q}”.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
staff.map((s) => (
|
||||||
|
<Link
|
||||||
|
key={s.id}
|
||||||
|
href={`/owner/team/${s.id}`}
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border-line flex items-center gap-4 border bg-white p-5",
|
||||||
|
s.status === "INACTIVE" && "opacity-60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="bg-stone text-muted flex h-12 w-12 shrink-0 items-center justify-center rounded-full text-sm font-bold">
|
||||||
|
{(s.user.name ?? "?").charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-bold">{s.user.name}</p>
|
||||||
|
<p className="text-muted truncate text-xs">{s.user.email}</p>
|
||||||
|
<p className="text-accent mt-0.5 flex items-center gap-1 text-[10px] font-bold tracking-widest uppercase">
|
||||||
|
{s.role.name === "Administrator" && (
|
||||||
|
<ShieldCheck className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
{s.role.name}
|
||||||
|
{s.status === "INACTIVE" && <span className="text-muted"> · Inactive</span>}
|
||||||
|
{s.status === "ACTIVE" && s.invitation && !s.invitation.acceptedAt && (
|
||||||
|
<span className="text-warning-foreground"> · Invite Pending</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<form action="/owner/team" method="GET" className="relative mb-4">
|
|
||||||
<Search className="text-muted absolute top-1/2 left-4 h-4 w-4 -translate-y-1/2" aria-hidden="true" />
|
|
||||||
<label htmlFor="team-search" className="sr-only">
|
|
||||||
Search staff by name or email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="team-search"
|
|
||||||
type="search"
|
|
||||||
name="q"
|
|
||||||
defaultValue={q}
|
|
||||||
placeholder="Search by name or email"
|
|
||||||
className="border-line focus:ring-ink w-full rounded-xl border bg-white py-3 pr-4 pl-11 text-sm outline-none focus:ring-2"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="grid gap-3 lg:grid-cols-2">
|
|
||||||
{staff.length === 0 ? (
|
|
||||||
<p className="text-muted col-span-full rounded-md border border-line bg-white p-5 text-center text-sm">
|
|
||||||
No staff match “{q}”.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
staff.map((s) => (
|
|
||||||
<Link
|
|
||||||
key={s.id}
|
|
||||||
href={`/owner/team/${s.id}`}
|
|
||||||
className={cn(
|
|
||||||
"rounded-md border-line flex items-center gap-4 border bg-white p-5",
|
|
||||||
s.status === "INACTIVE" && "opacity-60",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-stone text-muted flex h-12 w-12 shrink-0 items-center justify-center rounded-full text-sm font-bold">
|
|
||||||
{(s.user.name ?? "?").charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="truncate text-sm font-bold">{s.user.name}</p>
|
|
||||||
<p className="text-muted truncate text-xs">{s.user.email}</p>
|
|
||||||
<p className="text-accent mt-0.5 flex items-center gap-1 text-[10px] font-bold tracking-widest uppercase">
|
|
||||||
{s.role.name === "Administrator" && (
|
|
||||||
<ShieldCheck className="h-3 w-3 shrink-0" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
{s.role.name}
|
|
||||||
{s.status === "INACTIVE" && <span className="text-muted"> · Inactive</span>}
|
|
||||||
{s.status === "ACTIVE" && s.invitation && !s.invitation.acceptedAt && (
|
|
||||||
<span className="text-warning-foreground"> · Invite Pending</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -28,8 +28,8 @@ export default async function StaffCalendarSettingsPage() {
|
|||||||
<TopNav title="Google Calendar" onBack sticky className="bg-stone" />
|
<TopNav title="Google Calendar" onBack sticky className="bg-stone" />
|
||||||
<main className="mx-auto max-w-2xl space-y-6 px-6 py-6">
|
<main className="mx-auto max-w-2xl space-y-6 px-6 py-6">
|
||||||
<p className="text-muted text-sm">
|
<p className="text-muted text-sm">
|
||||||
One-way sync: {branding.name} stays the source of truth, and your confirmed appointments are pushed to
|
{/* {branding.name} */}
|
||||||
Google Calendar. Nothing is ever imported back from Google.
|
NextChair is where your bookings are managed. Connect your Google Calendar to automatically add confirmed appointments to your personal calendar.
|
||||||
</p>
|
</p>
|
||||||
<CalendarConnectionPanel
|
<CalendarConnectionPanel
|
||||||
configured={configured}
|
configured={configured}
|
||||||
|
|||||||
Reference in new issue
Block a user