Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a628ef3b7 | |||
| 15af4f0962 | |||
| 990bc4400c | |||
| c12935de9c | |||
| 9b49b6388d | |||
| fe5de5fec8 | |||
| 82f1e3856f | |||
| 526251b63a |
@@ -53,41 +53,6 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
e2e:
|
|
||||||
name: E2E Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [lint-typecheck, test]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: '9.15.4'
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
|
||||||
run: pnpm --filter @groombook/e2e exec playwright install --with-deps chromium
|
|
||||||
|
|
||||||
- name: Start Docker Compose stack
|
|
||||||
run: docker compose up -d --wait
|
|
||||||
timeout-minutes: 5
|
|
||||||
|
|
||||||
- name: Run E2E tests
|
|
||||||
run: pnpm --filter @groombook/e2e test
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_BASE_URL: http://host.docker.internal:8080
|
|
||||||
|
|
||||||
- name: Stop Docker Compose stack
|
|
||||||
if: always()
|
|
||||||
run: docker compose down
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -115,7 +80,7 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
name: Build & Push Docker Images
|
name: Build & Push Docker Images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build, e2e]
|
needs: [build]
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.version.outputs.tag }}
|
tag: ${{ steps.version.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { eq, and, gt, gte, lt, ne, or, asc } from "@groombook/db";
|
import { eq, and, gt, or, asc } from "@groombook/db";
|
||||||
import { appointments, clients, pets, services, staff, type Db } from "@groombook/db";
|
import { appointments, clients, pets, services, staff, type Db } from "@groombook/db";
|
||||||
import { resolveBufferMinutes } from "./buffer.js";
|
import { resolveBufferMinutes } from "./buffer.js";
|
||||||
import { sendEmail, buildRescheduleNotificationEmail } from "../services/email.js";
|
import { sendEmail, buildRescheduleNotificationEmail } from "../services/email.js";
|
||||||
@@ -53,12 +53,12 @@ export async function detectAndCascadeOverrun({
|
|||||||
db,
|
db,
|
||||||
overrunningAppointmentId,
|
overrunningAppointmentId,
|
||||||
newEndTime,
|
newEndTime,
|
||||||
originalEndTime,
|
_originalEndTime,
|
||||||
}: {
|
}: {
|
||||||
db: Db;
|
db: Db;
|
||||||
overrunningAppointmentId: string;
|
overrunningAppointmentId: string;
|
||||||
newEndTime: Date;
|
newEndTime: Date;
|
||||||
originalEndTime: Date;
|
_originalEndTime: Date;
|
||||||
}): Promise<CascadeResult> {
|
}): Promise<CascadeResult> {
|
||||||
const result: CascadeResult = { shifted: [], flaggedForReview: [] };
|
const result: CascadeResult = { shifted: [], flaggedForReview: [] };
|
||||||
|
|
||||||
@@ -178,16 +178,16 @@ export async function detectAndCascadeOverrun({
|
|||||||
export function isOverrun({
|
export function isOverrun({
|
||||||
originalEndTime,
|
originalEndTime,
|
||||||
newEndTime,
|
newEndTime,
|
||||||
originalStartTime,
|
_originalStartTime,
|
||||||
newStartTime,
|
_newStartTime,
|
||||||
status,
|
status,
|
||||||
currentTime,
|
currentTime,
|
||||||
bufferMinutes,
|
bufferMinutes,
|
||||||
}: {
|
}: {
|
||||||
originalEndTime: Date;
|
originalEndTime: Date;
|
||||||
newEndTime: Date;
|
newEndTime: Date;
|
||||||
originalStartTime: Date;
|
_originalStartTime: Date;
|
||||||
newStartTime?: Date;
|
_newStartTime?: Date;
|
||||||
status: string;
|
status: string;
|
||||||
currentTime: Date;
|
currentTime: Date;
|
||||||
bufferMinutes: number;
|
bufferMinutes: number;
|
||||||
|
|||||||
@@ -700,7 +700,7 @@ appointmentsRouter.patch(
|
|||||||
isOverrun({
|
isOverrun({
|
||||||
originalEndTime,
|
originalEndTime,
|
||||||
newEndTime: new Date(updateFields.endTime),
|
newEndTime: new Date(updateFields.endTime),
|
||||||
originalStartTime: row.startTime,
|
_originalStartTime: row.startTime,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
currentTime: new Date(),
|
currentTime: new Date(),
|
||||||
bufferMinutes: row.bufferMinutes ?? 0,
|
bufferMinutes: row.bufferMinutes ?? 0,
|
||||||
@@ -710,7 +710,7 @@ appointmentsRouter.patch(
|
|||||||
db,
|
db,
|
||||||
overrunningAppointmentId: id,
|
overrunningAppointmentId: id,
|
||||||
newEndTime: new Date(updateFields.endTime),
|
newEndTime: new Date(updateFields.endTime),
|
||||||
originalEndTime,
|
_originalEndTime: originalEndTime,
|
||||||
});
|
});
|
||||||
return c.json({ ...row, cascade: cascadeResult });
|
return c.json({ ...row, cascade: cascadeResult });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ bookRouter.get("/availability", async (c) => {
|
|||||||
const serviceId = c.req.query("serviceId");
|
const serviceId = c.req.query("serviceId");
|
||||||
const dateStr = c.req.query("date");
|
const dateStr = c.req.query("date");
|
||||||
const petSizeCategory = c.req.query("petSizeCategory") ?? undefined;
|
const petSizeCategory = c.req.query("petSizeCategory") ?? undefined;
|
||||||
const petCoatType = c.req.query("petCoatType") ?? undefined;
|
|
||||||
|
|
||||||
if (!serviceId || !dateStr) {
|
if (!serviceId || !dateStr) {
|
||||||
return c.json({ error: "serviceId and date are required" }, 400);
|
return c.json({ error: "serviceId and date are required" }, 400);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
|||||||
reporter: process.env.CI ? "github" : "list",
|
reporter: process.env.CI ? "github" : "list",
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
baseURL: "http://localhost:8080",
|
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:8080",
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
screenshot: "only-on-failure",
|
screenshot: "only-on-failure",
|
||||||
serviceWorkers: "block",
|
serviceWorkers: "block",
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ export function BookPage() {
|
|||||||
<option value="small">Small (under 15 lbs)</option>
|
<option value="small">Small (under 15 lbs)</option>
|
||||||
<option value="medium">Medium (15–40 lbs)</option>
|
<option value="medium">Medium (15–40 lbs)</option>
|
||||||
<option value="large">Large (40–80 lbs)</option>
|
<option value="large">Large (40–80 lbs)</option>
|
||||||
<option value="x-large">X-Large (over 80 lbs)</option>
|
<option value="xlarge">X-Large (over 80 lbs)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -568,7 +568,7 @@ export function BookPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Service</div>
|
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Service</div>
|
||||||
<div style={{ fontWeight: 600 }}>{selectedService.name}</div>
|
<div style={{ fontWeight: 600 }}>{selectedService.name}</div>
|
||||||
<div style={{ color: "#6b7280" }}>{fmtPrice(selectedService.basePriceCents)} · {fmtDuration(selectedService.durationMinutes + ((form.petSizeCategory === "large" || form.petSizeCategory === "x-large") ? (selectedService.defaultBufferMinutes ?? 0) : 0))}</div>
|
<div style={{ color: "#6b7280" }}>{fmtPrice(selectedService.basePriceCents)} · {fmtDuration(selectedService.durationMinutes + ((form.petSizeCategory === "large" || form.petSizeCategory === "xlarge") ? (selectedService.defaultBufferMinutes ?? 0) : 0))}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Date & Time</div>
|
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Date & Time</div>
|
||||||
|
|||||||
+16
-1
@@ -43,6 +43,12 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
@@ -50,8 +56,17 @@ services:
|
|||||||
dockerfile: apps/web/Dockerfile
|
dockerfile: apps/web/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:80 || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user