diff --git a/package-lock.json b/package-lock.json index cc14303..16dc705 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "@hookform/resolvers": "^5.2.2", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.86.0", @@ -24,11 +26,13 @@ "date-fns": "^2.30.0", "lucide-react": "^0.555.0", "next": "16.0.7", + "next-themes": "^0.4.6", "react": "19.2.0", "react-big-calendar": "^1.19.4", "react-day-picker": "^9.11.3", "react-dom": "19.2.0", "react-hook-form": "^7.67.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zod": "^4.1.13" }, @@ -1624,6 +1628,90 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -2581,6 +2669,29 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", @@ -6952,6 +7063,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -7930,6 +8051,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 1552f5d..314c3f8 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "@hookform/resolvers": "^5.2.2", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.86.0", @@ -25,11 +27,13 @@ "date-fns": "^2.30.0", "lucide-react": "^0.555.0", "next": "16.0.7", + "next-themes": "^0.4.6", "react": "19.2.0", "react-big-calendar": "^1.19.4", "react-day-picker": "^9.11.3", "react-dom": "19.2.0", "react-hook-form": "^7.67.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zod": "^4.1.13" }, diff --git a/src/app/dashboard/calendar/page.tsx b/src/app/dashboard/calendar/page.tsx index 08b9916..fee7964 100644 --- a/src/app/dashboard/calendar/page.tsx +++ b/src/app/dashboard/calendar/page.tsx @@ -12,23 +12,30 @@ export default async function CalendarPage() { start_time, end_time, status, + hall_id, halls (name), - customers (full_name) + customers (full_name, phone) `) + .neq('status', 'cancelled') // Don't show cancelled events + + // Fetch halls for filter + const { data: halls } = await supabase + .from('halls') + .select('id, name') + .order('name') // Transform data for calendar const events = reservations?.map(res => ({ id: res.id, - title: `${res.halls?.name || 'Salon'} - ${res.customers?.full_name || 'Müşteri'}`, + title: `${res.customers?.full_name || 'Müşteri'} ${res.customers?.phone ? `(${res.customers.phone})` : ''}`, start: new Date(res.start_time), end: new Date(res.end_time), resource: res, })) || [] return ( -
-

Takvim

- +
+
) } diff --git a/src/app/dashboard/halls/new/hall-form.tsx b/src/app/dashboard/halls/new/hall-form.tsx index 1f1aff1..48ab476 100644 --- a/src/app/dashboard/halls/new/hall-form.tsx +++ b/src/app/dashboard/halls/new/hall-form.tsx @@ -18,7 +18,7 @@ import { Textarea } from "@/components/ui/textarea" import { createHall } from "./actions" import { useRouter } from "next/navigation" import { useState } from "react" -import { useToast } from "@/hooks/use-toast" // Note: Need to check if toast hook exists or create it +import { toast } from "sonner" const formSchema = z.object({ name: z.string().min(2, { @@ -49,9 +49,10 @@ export function HallForm() { await createHall(values) router.push('/dashboard/halls') router.refresh() + toast.success("Salon başarıyla oluşturuldu") } catch (error) { console.error(error) - alert("Bir hata oluştu") + toast.error("Bir hata oluştu") } finally { setLoading(false) } diff --git a/src/app/dashboard/reservations/new/reservation-form.tsx b/src/app/dashboard/reservations/new/reservation-form.tsx index 96165ee..8f95702 100644 --- a/src/app/dashboard/reservations/new/reservation-form.tsx +++ b/src/app/dashboard/reservations/new/reservation-form.tsx @@ -17,7 +17,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Textarea } from "@/components/ui/textarea" import { createReservation } from "./actions" import { useState } from "react" -import { useToast } from "@/hooks/use-toast" // Assuming we have this or will use alert +import { toast } from "sonner" const formSchema = z.object({ hall_id: z.string().min(1, "Salon seçmelisiniz."), diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..c6e1492 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Toaster } from "@/components/ui/sonner" const geistSans = Geist({ variable: "--font-geist-sans", @@ -28,6 +29,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > {children} + ); diff --git a/src/components/calendar-view.tsx b/src/components/calendar-view.tsx index 6a55a41..3eb3f15 100644 --- a/src/components/calendar-view.tsx +++ b/src/components/calendar-view.tsx @@ -7,8 +7,16 @@ import startOfWeek from 'date-fns/startOfWeek' import getDay from 'date-fns/getDay' import tr from 'date-fns/locale/tr' import "react-big-calendar/lib/css/react-big-calendar.css" -import { useState } from 'react' +import { useState, useMemo } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { useRouter } from 'next/navigation' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu" const locales = { 'tr': tr, @@ -24,43 +32,95 @@ const localizer = dateFnsLocalizer({ interface CalendarViewProps { events?: any[] + halls?: { id: string, name: string }[] } -export function CalendarView({ events = [] }: CalendarViewProps) { +export function CalendarView({ events = [], halls = [] }: CalendarViewProps) { + const router = useRouter() const [view, setView] = useState(Views.MONTH) const [date, setDate] = useState(new Date()) + const [selectedHallId, setSelectedHallId] = useState("all") + + const filteredEvents = useMemo(() => { + if (selectedHallId === "all") return events + return events.filter(event => event.resource.hall_id === selectedHallId) + }, [events, selectedHallId]) + + const handleDoubleClickEvent = (event: any) => { + router.push(`/dashboard/reservations/${event.id}`) + } + + const handleSelectSlot = ({ start, end }: { start: Date, end: Date }) => { + // Optional: Navigate to create page with pre-filled dates + // const params = new URLSearchParams() + // params.set('date', format(start, 'yyyy-MM-dd')) + // params.set('start', format(start, 'HH:mm')) + // params.set('end', format(end, 'HH:mm')) + // router.push(`/dashboard/reservations/new?${params.toString()}`) + } return ( - - + + Rezervasyon Takvimi +
+ +
- - + + + + ({ + className: "bg-primary text-primary-foreground text-xs rounded-md border-none px-2 py-1 shadow-sm hover:bg-primary/90 transition-colors cursor-pointer" + })} + /> + + + router.push('/dashboard/reservations/new')}> + Yeni Rezervasyon Ekle + + router.refresh()}> + Yenile + + +
) diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..f024a9c --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,252 @@ +"use client" + +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function ContextMenu({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function ContextMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function ContextMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function ContextMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..275381c --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..9b20afe --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,40 @@ +"use client" + +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster }