diff --git a/.gitignore b/.gitignore index a9fe888..904932f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ # testing /coverage +*.cer + # next.js /.next/ /out/ @@ -79,4 +81,7 @@ public/workbox-*.js public/workbox-*.js.map # Local scripts -.LOCAL \ No newline at end of file +.LOCAL + +.npmrc +*.cer diff --git a/app/api/external-syncs/[id]/sync/route.ts b/app/api/external-syncs/[id]/sync/route.ts index b09f0f6..1455023 100644 --- a/app/api/external-syncs/[id]/sync/route.ts +++ b/app/api/external-syncs/[id]/sync/route.ts @@ -117,11 +117,13 @@ export async function syncExternalCalendar( string, typeof shifts.$inferSelect >(); + for (const shift of existingShifts) { const fingerprint = createEventFingerprint( shift.date, shift.startTime, shift.endTime, + shift.number, shift.title, undefined, // Only use externalEventId for iCloud/Google (stable UIDs) @@ -175,6 +177,7 @@ export async function syncExternalCalendar( : baseEventId; const title = event.summary || "Untitled Event"; + const number = event.organizer || ""; // Create fingerprint based on event content // For iCloud/Google: include eventId for stable UID-based matching @@ -184,6 +187,7 @@ export async function syncExternalCalendar( dayEntry.startTime, dayEntry.endTime, title, + number, undefined, externalSync.syncType !== "custom" ? eventId : undefined ); @@ -196,6 +200,7 @@ export async function syncExternalCalendar( startTime: dayEntry.startTime, endTime: dayEntry.endTime, title, + number: "", color: externalSync.color, notes: event.description || null, isAllDay, @@ -255,6 +260,7 @@ export async function syncExternalCalendar( todoData.date, todoData.startTime, todoData.endTime, + todoData.number, title, undefined, externalSync.syncType !== "custom" ? eventId : undefined @@ -268,6 +274,7 @@ export async function syncExternalCalendar( startTime: todoData.startTime, endTime: todoData.endTime, title, + number: todoData.number, color: externalSync.color, notes: todoData.notes, isAllDay: todoData.isAllDay, @@ -310,6 +317,7 @@ export async function syncExternalCalendar( shift.date, shift.startTime, shift.endTime, + shift.number, shift.title, undefined, // Only use externalEventId for iCloud/Google (stable UIDs) diff --git a/app/api/presets/[id]/route.ts b/app/api/presets/[id]/route.ts index 5cb6642..b130db4 100644 --- a/app/api/presets/[id]/route.ts +++ b/app/api/presets/[id]/route.ts @@ -67,6 +67,7 @@ export async function PATCH( title, startTime, endTime, + number, color, notes, isSecondary, @@ -117,6 +118,7 @@ export async function PATCH( title, startTime: isAllDay ? "00:00" : startTime, endTime: isAllDay ? "23:59" : endTime, + number: number !== undefined ? number : "", color, notes: notes || null, isSecondary: isSecondary !== undefined ? isSecondary : undefined, @@ -136,6 +138,7 @@ export async function PATCH( endTime: isAllDay ? "23:59" : endTime, color, notes: notes || null, + number: number !== undefined ? number : "", isAllDay: isAllDay !== undefined ? isAllDay : undefined, updatedAt: new Date(), }) diff --git a/app/api/presets/route.ts b/app/api/presets/route.ts index 5c13ccc..762af5a 100644 --- a/app/api/presets/route.ts +++ b/app/api/presets/route.ts @@ -66,6 +66,7 @@ export async function POST(request: NextRequest) { title, startTime, endTime, + number, color, notes, isSecondary, @@ -122,6 +123,7 @@ export async function POST(request: NextRequest) { title, startTime: isAllDay ? "00:00" : startTime, endTime: isAllDay ? "23:59" : endTime, + number, color: color || "#3b82f6", notes: notes || null, isSecondary: isSecondary || false, diff --git a/app/api/shifts/[id]/route.ts b/app/api/shifts/[id]/route.ts index cdc07a2..4e9a780 100644 --- a/app/api/shifts/[id]/route.ts +++ b/app/api/shifts/[id]/route.ts @@ -4,6 +4,7 @@ import { calendars, shifts } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; import { getSessionUser } from "@/lib/auth/sessions"; import { canViewCalendar, canEditCalendar } from "@/lib/auth/permissions"; +import { number } from "better-auth"; // GET single shift export async function GET( @@ -21,6 +22,7 @@ export async function GET( date: shifts.date, startTime: shifts.startTime, endTime: shifts.endTime, + number: shifts.number, title: shifts.title, color: shifts.color, notes: shifts.notes, @@ -139,6 +141,7 @@ export async function PUT( date: body.date ? new Date(body.date) : existingShift.date, startTime: body.startTime ?? existingShift.startTime, endTime: body.endTime ?? existingShift.endTime, + number: body.number ?? existingShift.number, title: body.title ?? existingShift.title, color: body.color ?? existingShift.color, notes: body.notes ?? existingShift.notes, diff --git a/app/api/shifts/route.ts b/app/api/shifts/route.ts index 4292fd0..187e49b 100644 --- a/app/api/shifts/route.ts +++ b/app/api/shifts/route.ts @@ -5,6 +5,7 @@ import { eq, and, gte, lte, or, isNull } from "drizzle-orm"; import { getSessionUser } from "@/lib/auth/sessions"; import { canViewCalendar, canEditCalendar } from "@/lib/auth/permissions"; import { parseLocalDate } from "@/lib/date-utils"; +import { number } from "better-auth"; // GET shifts for a calendar (with optional date filter) export async function GET(request: Request) { @@ -51,6 +52,7 @@ export async function GET(request: Request) { date: shifts.date, startTime: shifts.startTime, endTime: shifts.endTime, + number: shifts.number, title: shifts.title, color: shifts.color, notes: shifts.notes, @@ -124,6 +126,7 @@ export async function POST(request: Request) { startTime, endTime, title, + number, color, notes, presetId, @@ -181,6 +184,7 @@ export async function POST(request: Request) { startTime: isAllDay ? "00:00" : startTime, endTime: isAllDay ? "23:59" : endTime, title, + number: number || "", color: color || "#3b82f6", notes: notes || null, isAllDay: isAllDay || false, diff --git a/app/api/shifts/stats/route.ts b/app/api/shifts/stats/route.ts index a06d0f3..0e8c0ae 100644 --- a/app/api/shifts/stats/route.ts +++ b/app/api/shifts/stats/route.ts @@ -18,6 +18,7 @@ import { startOfYear, endOfYear, } from "date-fns"; +import { number } from "better-auth"; // GET shift statistics for a calendar export async function GET(request: Request) { @@ -88,6 +89,7 @@ export async function GET(request: Request) { date: shifts.date, startTime: shifts.startTime, endTime: shifts.endTime, + number: shifts.number, isAllDay: shifts.isAllDay, }) .from(shifts) diff --git a/app/docs/ApiDocs.tsx b/app/docs/ApiDocs.tsx new file mode 100644 index 0000000..b7ca49a --- /dev/null +++ b/app/docs/ApiDocs.tsx @@ -0,0 +1,11 @@ +"use client"; + +import SwaggerUI from "swagger-ui-react"; + +export default function ApiDocs() { + return ( + + ); +} \ No newline at end of file diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx new file mode 100644 index 0000000..288c1f2 --- /dev/null +++ b/app/docs/layout.tsx @@ -0,0 +1,104 @@ +import type { Metadata, Viewport } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages, getLocale } from "next-intl/server"; +import { ThemeProvider } from "@/components/theme-provider"; +import { ThemedToaster } from "@/components/themed-toaster"; +import { AuthProvider } from "@/components/auth-provider"; +import { PublicConfigProvider } from "@/components/public-config-provider"; +import { QueryProvider } from "@/components/query-provider"; +import { getPublicConfig } from "@/lib/public-config"; +import "../globals.css"; +import "swagger-ui-react/swagger-ui.css"; +import "swagger-ui-themes/themes/3.x/theme-material.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "BetterShift", + description: "Your favorite shift management application!", + applicationName: "BetterShift", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "BetterShift", + }, + formatDetection: { + telephone: false, + }, + icons: { + icon: [ + { url: "/ios/32.png", sizes: "32x32", type: "image/png" }, + { + url: "/android/android-launchericon-192-192.png", + sizes: "192x192", + type: "image/png", + }, + { + url: "/android/android-launchericon-512-512.png", + sizes: "512x512", + type: "image/png", + }, + ], + apple: [{ url: "/ios/180.png", sizes: "180x180", type: "image/png" }], + }, +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, + themeColor: [{ media: "(prefers-color-scheme: dark)", color: "#0a0a0a" }], +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const locale = await getLocale(); + const messages = await getMessages(); + const publicConfig = getPublicConfig(); + + return ( + + + {/* Inject public config for immediate client-side access (zero latency) */} +