Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions __tests__/sanity.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { render, screen } from "@testing-library/react";
import DashboardPage from "@/app/(authenticated)/dashboard/page";
import { TestComponent } from "./test-component";

describe("jest setup", () => {
it("runs React + Testing Library", async () => {
const jsx = await DashboardPage();
render(jsx);
expect(screen.getByText("You are admin")).toBeInTheDocument();
});
it("runs React + Testing Library", () => {
render(<TestComponent />);
expect(screen.getByText("ok")).toBeInTheDocument();
});
});
3 changes: 3 additions & 0 deletions __tests__/test-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function TestComponent() {
return <div>ok</div>;
}
17 changes: 10 additions & 7 deletions app/(authenticated)/services/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { revalidatePath, updateTag } from "next/cache";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/lib/db";
import { services } from "@/lib/db/schema";
import { services, type ProgramSlot as DbProgramSlot } from "@/lib/db/schema";
import { requireAdmin } from "@/lib/auth/require-admin";
import { cadStringToCents } from "@/lib/money";
import {
Expand All @@ -19,10 +19,7 @@ export type ServiceActionState = {
message?: string;
} | null;

export type ProgramSlot = {
dayOfWeek: number;
time: string;
};
export type ProgramSlot = DbProgramSlot;

export type ProgramSchedule = {
startDate: string;
Expand Down Expand Up @@ -201,7 +198,9 @@ export async function createService(

await db.insert(services).values({
type,
scheduledAt: scheduledAtValue,
startDate: scheduledAtValue?.startDate ?? null,
endDate: scheduledAtValue?.endDate ?? null,
slots: scheduledAtValue?.slots ?? null,
durationMinutes: duration_minutes,
stripeProductId: productId,
status: "active",
Expand Down Expand Up @@ -330,7 +329,11 @@ export async function updateService(

const dbPatch: Partial<typeof services.$inferInsert> = {};
if (duration_minutes !== undefined) dbPatch.durationMinutes = duration_minutes;
if (scheduledAtValue !== undefined) dbPatch.scheduledAt = scheduledAtValue;
if (scheduledAtValue !== undefined) {
dbPatch.startDate = scheduledAtValue.startDate;
dbPatch.endDate = scheduledAtValue.endDate;
dbPatch.slots = scheduledAtValue.slots;
}

if (Object.keys(dbPatch).length > 0) {
dbPatch.updatedAt = new Date();
Expand Down
16 changes: 14 additions & 2 deletions app/(authenticated)/services/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { and, asc, desc, eq, inArray } from "drizzle-orm";
import { db } from "@/lib/db";
import { profiles, services } from "@/lib/db/schema";
import { getStripeServiceData } from "@/lib/stripe";
import type { ProgramSchedule } from "@/app/(authenticated)/services/actions";

const SERVICES_TAG = "services";
const COACHES_TAG = "coaches";
Expand All @@ -13,7 +14,7 @@ export type ServiceType = "private_lessons" | "programs";
export type ServiceView = {
id: string;
type: ServiceType;
scheduledAt: unknown;
scheduledAt: ProgramSchedule | null;
durationMinutes: number;
status: ServiceStatus;
stripeProductId: string;
Expand All @@ -25,12 +26,23 @@ export type ServiceView = {
priceCurrency: string | null;
};

function rowToSchedule(
row: typeof services.$inferSelect,
): ProgramSchedule | null {
if (!row.startDate || !row.endDate) return null;
return {
startDate: row.startDate,
endDate: row.endDate,
slots: row.slots ?? [],
};
}

async function buildServiceView(row: typeof services.$inferSelect): Promise<ServiceView> {
const stripeData = await getStripeServiceData(row.stripeProductId);
return {
id: row.id,
type: row.type,
scheduledAt: row.scheduledAt,
scheduledAt: rowToSchedule(row),
durationMinutes: row.durationMinutes,
status: row.status,
stripeProductId: row.stripeProductId,
Expand Down
18 changes: 1 addition & 17 deletions app/(authenticated)/services/service-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,6 @@ function fromISODate(value: string | undefined): Date | undefined {
return new Date(y, m - 1, d);
}

function isProgramSchedule(value: unknown): value is ProgramSchedule {
if (!value || typeof value !== "object") return false;
const v = value as Partial<ProgramSchedule>;
return (
typeof v.startDate === "string" &&
typeof v.endDate === "string" &&
Array.isArray(v.slots) &&
v.slots.every(
(s) => typeof s?.dayOfWeek === "number" && typeof s?.time === "string",
)
);
}

function DateRangePicker({
value,
onChange,
Expand Down Expand Up @@ -316,10 +303,7 @@ export function ServiceDialog(props: Props) {

const showForm = !isEdit || service !== null;

const initialSchedule =
service && isProgramSchedule(service.scheduledAt)
? service.scheduledAt
: null;
const initialSchedule = service?.scheduledAt ?? null;

return (
<Dialog {...dialogControl}>
Expand Down
14 changes: 3 additions & 11 deletions app/(authenticated)/services/services-data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,9 @@ import { DataTable } from "@/components/data-table";
import { formatDate } from "@/lib/format";
import { statusBadgeClass } from "@/lib/service-status";

import {
setServiceStatus,
type ProgramSchedule,
} from "@/app/(authenticated)/services/actions";
import { setServiceStatus } from "@/app/(authenticated)/services/actions";
import type { ServiceView } from "@/app/(authenticated)/services/queries";

function programSchedule(value: ServiceView["scheduledAt"]): ProgramSchedule | null {
if (!value) return null;
return value;
}

export function ServicesDataTable({
services,
onEdit,
Expand Down Expand Up @@ -73,15 +65,15 @@ export function ServicesDataTable({
id: "startDate",
header: "Start Date",
cell: ({ row }) => {
const s = programSchedule(row.original.scheduledAt);
const s = row.original.scheduledAt;
return s ? formatDate(s.startDate) : "—";
},
},
{
id: "endDate",
header: "End Date",
cell: ({ row }) => {
const s = programSchedule(row.original.scheduledAt);
const s = row.original.scheduledAt;
return s ? formatDate(s.endDate) : "—";
},
},
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const customJestConfig = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
testEnvironment: "jest-environment-jsdom",
modulePathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/"],
testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/*.test.[jt]s?(x)"],
testMatch: ["**/*.test.[jt]s?(x)"],
collectCoverageFrom: [
"app/**/*.{js,jsx,ts,tsx}",
"components/**/*.{js,jsx,ts,tsx}",
Expand Down
7 changes: 6 additions & 1 deletion lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {
integer,
boolean,
jsonb,
date,
} from "drizzle-orm/pg-core";

export type ProgramSlot = { dayOfWeek: number; time: string };

export const roleEnum = pgEnum("role", ["user", "admin", "coach"]);
export const serviceTypeEnum = pgEnum("service_type", [
"private_lessons",
Expand Down Expand Up @@ -46,7 +49,9 @@ export const profiles = pgTable("profiles", {
export const services = pgTable("services", {
id: uuid("id").primaryKey().defaultRandom(),
type: serviceTypeEnum("type").notNull(),
scheduledAt: jsonb("scheduled_at"),
startDate: date("start_date", { mode: "string" }),
endDate: date("end_date", { mode: "string" }),
slots: jsonb("slots").$type<ProgramSlot[]>(),
durationMinutes: integer("duration_minutes").notNull(),
stripeProductId: text("stripe_product_id").notNull(),
status: serviceStatusEnum("status").notNull().default("active"),
Expand Down
15 changes: 8 additions & 7 deletions lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export function formatDate(value: Date | string) {
const date = value instanceof Date ? value : new Date(value);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
const MONTHS_SHORT = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
] as const;

export function formatDate(value: string): string {
const [y, m, d] = value.split("-");
return `${MONTHS_SHORT[Number(m) - 1]} ${Number(d)}, ${y}`;
}
Loading