Phase 2
Base app architecture:
Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
This commit is contained in:
201 files changed
+31984
No files matched your search
+251
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Dev/demo fixtures. Run with `npm run prisma:seed` (requires a reachable
|
||||
* DATABASE_URL — see .env.example). Intentionally small: enough to exercise
|
||||
* every relation in the schema once, not a full demo dataset.
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import { PrismaClient, Permission, Weekday, RewardType } from "../generated/prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const OWNER_PERMISSIONS: Permission[] = [
|
||||
Permission.MANAGE_BUSINESS_PROFILE,
|
||||
Permission.MANAGE_BOOKING_RULES,
|
||||
Permission.MANAGE_SERVICES,
|
||||
Permission.MANAGE_STAFF,
|
||||
Permission.MANAGE_LOYALTY_PROGRAMME,
|
||||
Permission.MANAGE_NOTIFICATION_SETTINGS,
|
||||
Permission.VIEW_ALL_STAFF_APPOINTMENTS,
|
||||
Permission.MANAGE_OWN_APPOINTMENTS,
|
||||
Permission.CHECK_IN_CUSTOMERS,
|
||||
Permission.MANAGE_LOYALTY_STAMPS,
|
||||
Permission.MANAGE_OWN_AVAILABILITY,
|
||||
Permission.VIEW_CUSTOMER_DIRECTORY,
|
||||
Permission.BOOK_ON_BEHALF_OF_CUSTOMER,
|
||||
];
|
||||
|
||||
const BARBER_PERMISSIONS: Permission[] = [
|
||||
Permission.MANAGE_OWN_APPOINTMENTS,
|
||||
Permission.CHECK_IN_CUSTOMERS,
|
||||
Permission.MANAGE_LOYALTY_STAMPS,
|
||||
Permission.MANAGE_OWN_AVAILABILITY,
|
||||
Permission.VIEW_CUSTOMER_DIRECTORY,
|
||||
Permission.BOOK_ON_BEHALF_OF_CUSTOMER,
|
||||
];
|
||||
|
||||
const WEEKDAYS: Weekday[] = [
|
||||
Weekday.MONDAY,
|
||||
Weekday.TUESDAY,
|
||||
Weekday.WEDNESDAY,
|
||||
Weekday.THURSDAY,
|
||||
Weekday.FRIDAY,
|
||||
Weekday.SATURDAY,
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const business = await prisma.business.upsert({
|
||||
where: { slug: "blake-heritage-barbershop" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "blake-heritage-barbershop",
|
||||
name: "Blake Heritage Barbershop",
|
||||
description:
|
||||
"Blake Heritage has come to Northwood! Check our Instagram @Blake.Heritage.Barbershop for more info.",
|
||||
phone: "+44 1923 000000",
|
||||
email: "hello@blakeheritage.co.uk",
|
||||
timezone: "Europe/London",
|
||||
currency: "GBP",
|
||||
locale: "en-GB",
|
||||
locations: {
|
||||
create: {
|
||||
addressLine1: "79 Joel Street",
|
||||
city: "Northwood",
|
||||
postalCode: "HA6 1LW",
|
||||
country: "GB",
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
create: {
|
||||
cancellationPolicy: "Free cancellation up to 24 hours before your appointment.",
|
||||
lateArrivalPolicy: "Arrivals more than 10 minutes late may need to be rescheduled.",
|
||||
},
|
||||
},
|
||||
bookingRule: { create: {} }, // all defaults from the schema
|
||||
hours: {
|
||||
create: [
|
||||
...WEEKDAYS.map((dayOfWeek) => ({
|
||||
dayOfWeek,
|
||||
opensAtMinutes: 10 * 60,
|
||||
closesAtMinutes: 19 * 60,
|
||||
})),
|
||||
{ dayOfWeek: Weekday.SUNDAY, isClosed: true },
|
||||
],
|
||||
},
|
||||
loyaltyProgramme: {
|
||||
create: {
|
||||
isEnabled: true,
|
||||
stampsRequired: 10,
|
||||
rewardType: RewardType.PERCENTAGE,
|
||||
rewardValue: 50,
|
||||
rewardLabel: "50% Off Any Service",
|
||||
termsLine: "Valid for 12 months. One reward per visit.",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ownerRole = await prisma.role.upsert({
|
||||
where: { businessId_name: { businessId: business.id, name: "Owner" } },
|
||||
update: {},
|
||||
create: {
|
||||
businessId: business.id,
|
||||
name: "Owner",
|
||||
isSystem: true,
|
||||
permissions: { create: OWNER_PERMISSIONS.map((permission) => ({ permission })) },
|
||||
},
|
||||
});
|
||||
|
||||
const barberRole = await prisma.role.upsert({
|
||||
where: { businessId_name: { businessId: business.id, name: "Barber" } },
|
||||
update: {},
|
||||
create: {
|
||||
businessId: business.id,
|
||||
name: "Barber",
|
||||
isSystem: true,
|
||||
permissions: { create: BARBER_PERMISSIONS.map((permission) => ({ permission })) },
|
||||
},
|
||||
});
|
||||
|
||||
const category = await prisma.serviceCategory.upsert({
|
||||
where: { id: `${business.id}-senior-barber` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `${business.id}-senior-barber`,
|
||||
businessId: business.id,
|
||||
name: "Hair & Beard Services — Senior Barber",
|
||||
},
|
||||
});
|
||||
|
||||
const [haircut, skinfade] = await Promise.all([
|
||||
prisma.service.upsert({
|
||||
where: { id: `${business.id}-haircut-style` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `${business.id}-haircut-style`,
|
||||
businessId: business.id,
|
||||
categoryId: category.id,
|
||||
name: "Haircut & Style",
|
||||
durationMinutes: 30,
|
||||
price: 22,
|
||||
},
|
||||
}),
|
||||
prisma.service.upsert({
|
||||
where: { id: `${business.id}-skinfade-style` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `${business.id}-skinfade-style`,
|
||||
businessId: business.id,
|
||||
categoryId: category.id,
|
||||
name: "Skinfade / Taper & Style",
|
||||
durationMinutes: 30,
|
||||
price: 25,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const ownerPasswordHash = await bcrypt.hash("password123", 12);
|
||||
const ownerUser = await prisma.user.upsert({
|
||||
where: { email: "marcus@blakeheritage.co.uk" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "marcus@blakeheritage.co.uk",
|
||||
name: "Marcus Reed",
|
||||
passwordHash: ownerPasswordHash,
|
||||
emailVerified: new Date(),
|
||||
staffProfile: {
|
||||
create: {
|
||||
businessId: business.id,
|
||||
roleId: ownerRole.id,
|
||||
bio: "Master barber, owner.",
|
||||
workingHours: {
|
||||
create: WEEKDAYS.map((dayOfWeek) => ({
|
||||
dayOfWeek,
|
||||
startMinutes: 10 * 60,
|
||||
endMinutes: 19 * 60,
|
||||
})),
|
||||
},
|
||||
services: {
|
||||
create: [{ serviceId: haircut.id }, { serviceId: skinfade.id }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { staffProfile: true },
|
||||
});
|
||||
|
||||
const barberPasswordHash = await bcrypt.hash("password123", 12);
|
||||
await prisma.user.upsert({
|
||||
where: { email: "leo@blakeheritage.co.uk" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "leo@blakeheritage.co.uk",
|
||||
name: "Leo Vance",
|
||||
passwordHash: barberPasswordHash,
|
||||
emailVerified: new Date(),
|
||||
staffProfile: {
|
||||
create: {
|
||||
businessId: business.id,
|
||||
roleId: barberRole.id,
|
||||
bio: "Senior specialist.",
|
||||
workingHours: {
|
||||
create: WEEKDAYS.map((dayOfWeek) => ({
|
||||
dayOfWeek,
|
||||
startMinutes: 10 * 60,
|
||||
endMinutes: 19 * 60,
|
||||
})),
|
||||
},
|
||||
services: { create: [{ serviceId: haircut.id }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customerPasswordHash = await bcrypt.hash("password123", 12);
|
||||
await prisma.user.upsert({
|
||||
where: { email: "alex.chen@example.com" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "alex.chen@example.com",
|
||||
name: "Alex Chen",
|
||||
passwordHash: customerPasswordHash,
|
||||
emailVerified: new Date(),
|
||||
customerProfile: {
|
||||
create: {
|
||||
firstName: "Alex",
|
||||
lastName: "Chen",
|
||||
email: "alex.chen@example.com",
|
||||
isGuest: false,
|
||||
preferredBarberId: ownerUser.staffProfile?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Seed complete:", {
|
||||
business: business.name,
|
||||
roles: [ownerRole.name, barberRole.name],
|
||||
services: [haircut.name, skinfade.name],
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in new issue
Block a user