Bernt Christian Egeland 7 месяцев назад
Родитель
Сommit
211ad3adac

+ 264 - 0
src/app/(authenticated)/vehicles/[id]/notes-table.tsx

@@ -0,0 +1,264 @@
+"use client";
+
+import { useRouter, usePathname, useSearchParams } from "next/navigation";
+import { useCallback, useTransition } from "react";
+import { useFormatDate } from "@/lib/use-format-date";
+import { Button } from "@/components/ui/button";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  ChevronLeft,
+  ChevronRight,
+  ChevronsLeft,
+  ChevronsRight,
+  Loader2,
+  MoreVertical,
+  Pin,
+  PinOff,
+  Plus,
+  Trash2,
+} from "lucide-react";
+
+interface NoteRow {
+  id: string;
+  title: string;
+  content: string;
+  isPinned: boolean;
+  createdAt: Date;
+}
+
+interface NotesTableProps {
+  vehicleId: string;
+  records: NoteRow[];
+  total: number;
+  page: number;
+  pageSize: number;
+  totalPages: number;
+  onSelectNote: (note: NoteRow) => void;
+  onAddNote: () => void;
+  onTogglePin: (id: string) => void;
+  onDeleteNote: (id: string) => void;
+}
+
+export function NotesTable({
+  vehicleId,
+  records,
+  total,
+  page,
+  pageSize,
+  totalPages,
+  onSelectNote,
+  onAddNote,
+  onTogglePin,
+  onDeleteNote,
+}: NotesTableProps) {
+  const router = useRouter();
+  const { formatDate } = useFormatDate();
+  const pathname = usePathname();
+  const searchParams = useSearchParams();
+  const [isPending, startTransition] = useTransition();
+
+  const createUrl = useCallback(
+    (params: Record<string, string | number | undefined>) => {
+      const newParams = new URLSearchParams(searchParams.toString());
+      newParams.set("tab", "notes");
+      for (const [key, value] of Object.entries(params)) {
+        if (value === undefined || value === "") {
+          newParams.delete(key);
+        } else {
+          newParams.set(key, String(value));
+        }
+      }
+      // Reset to page 1 when page size changes (unless explicitly setting page)
+      if (!("notesPage" in params) && "notesPageSize" in params) {
+        newParams.delete("notesPage");
+      }
+      return `${pathname}?${newParams.toString()}`;
+    },
+    [pathname, searchParams]
+  );
+
+  const navigate = useCallback(
+    (params: Record<string, string | number | undefined>) => {
+      startTransition(() => {
+        router.push(createUrl(params));
+      });
+    },
+    [router, createUrl]
+  );
+
+  const handlePageSizeChange = useCallback(
+    (newSize: string) => {
+      navigate({ notesPageSize: newSize, notesPage: 1 });
+    },
+    [navigate]
+  );
+
+  const startItem = (page - 1) * pageSize + 1;
+  const endItem = Math.min(page * pageSize, total);
+
+  return (
+    <div className="space-y-4">
+      {/* Toolbar */}
+      <div className="flex items-center justify-between">
+        {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
+        <div className="ml-auto">
+          <Button size="sm" onClick={onAddNote}>
+            <Plus className="mr-1 h-3.5 w-3.5" />
+            Add Note
+          </Button>
+        </div>
+      </div>
+
+      {/* Table */}
+      <div className="rounded-lg border">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-7.5"></TableHead>
+              <TableHead className="w-40">Title</TableHead>
+              <TableHead className="hidden sm:table-cell">Content</TableHead>
+              <TableHead className="w-30">Date</TableHead>
+              <TableHead className="w-12.5"></TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {records.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={5} className="h-32 text-center text-muted-foreground">
+                  No notes yet.
+                </TableCell>
+              </TableRow>
+            ) : (
+              records.map((n) => (
+                <TableRow key={n.id} className="cursor-pointer" onClick={() => onSelectNote(n)}>
+                  <TableCell className="w-[30px] px-2">
+                    {n.isPinned && <Pin className="h-3.5 w-3.5 text-primary" />}
+                  </TableCell>
+                  <TableCell className="font-medium">{n.title}</TableCell>
+                  <TableCell className="hidden max-w-0 sm:table-cell">
+                    <p className="truncate text-sm text-muted-foreground">
+                      {n.content.replace(/<[^>]*>/g, "")}
+                    </p>
+                  </TableCell>
+                  <TableCell className="font-mono text-xs text-muted-foreground">
+                    {formatDate(new Date(n.createdAt))}
+                  </TableCell>
+                  <TableCell className="px-2" onClick={(e) => e.stopPropagation()}>
+                    <DropdownMenu>
+                      <DropdownMenuTrigger asChild>
+                        <Button variant="ghost" size="icon" className="h-7 w-7">
+                          <MoreVertical className="h-3.5 w-3.5" />
+                        </Button>
+                      </DropdownMenuTrigger>
+                      <DropdownMenuContent align="end">
+                        <DropdownMenuItem onClick={() => onTogglePin(n.id)}>
+                          {n.isPinned ? (
+                            <PinOff className="mr-2 h-4 w-4" />
+                          ) : (
+                            <Pin className="mr-2 h-4 w-4" />
+                          )}
+                          {n.isPinned ? "Unpin" : "Pin"}
+                        </DropdownMenuItem>
+                        <DropdownMenuItem
+                          className="text-destructive"
+                          onClick={() => onDeleteNote(n.id)}
+                        >
+                          <Trash2 className="mr-2 h-4 w-4" />
+                          Delete
+                        </DropdownMenuItem>
+                      </DropdownMenuContent>
+                    </DropdownMenu>
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      {/* Pagination */}
+      {total > 0 && (
+        <div className="flex flex-col items-center justify-between gap-3 sm:flex-row">
+          <div className="flex items-center gap-2 text-sm text-muted-foreground">
+            <span>
+              Showing {startItem}-{endItem} of {total}
+            </span>
+            <Select value={String(pageSize)} onValueChange={handlePageSizeChange}>
+              <SelectTrigger className="h-8 w-[70px]">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="5">5</SelectItem>
+                <SelectItem value="10">10</SelectItem>
+                <SelectItem value="20">20</SelectItem>
+                <SelectItem value="50">50</SelectItem>
+              </SelectContent>
+            </Select>
+            <span>per page</span>
+          </div>
+          <div className="flex items-center gap-1">
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page <= 1}
+              onClick={() => navigate({ notesPage: 1 })}
+            >
+              <ChevronsLeft className="h-4 w-4" />
+            </Button>
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page <= 1}
+              onClick={() => navigate({ notesPage: page - 1 })}
+            >
+              <ChevronLeft className="h-4 w-4" />
+            </Button>
+            <span className="px-3 text-sm">
+              Page {page} of {totalPages}
+            </span>
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page >= totalPages}
+              onClick={() => navigate({ notesPage: page + 1 })}
+            >
+              <ChevronRight className="h-4 w-4" />
+            </Button>
+            <Button
+              variant="outline"
+              size="icon"
+              className="h-8 w-8"
+              disabled={page >= totalPages}
+              onClick={() => navigate({ notesPage: totalPages })}
+            >
+              <ChevronsRight className="h-4 w-4" />
+            </Button>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}

+ 11 - 1
src/app/(authenticated)/vehicles/[id]/page.tsx

@@ -1,5 +1,6 @@
 import { getVehicle } from "@/features/vehicles/Actions/vehicleActions";
 import { getServiceRecordsPaginated } from "@/features/vehicles/Actions/serviceActions";
+import { getNotesPaginated } from "@/features/vehicles/Actions/noteActions";
 import { getCustomersList } from "@/features/customers/Actions/customerActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
@@ -24,10 +25,14 @@ export default async function VehicleDetailPage({
   const search = typeof sp.search === "string" ? sp.search : "";
   const type = typeof sp.type === "string" ? sp.type : "all";
 
-  const [result, customersResult, serviceResult, settingsResult, maintenanceSettingsResult, inspectionsResult, templatesResult] = await Promise.all([
+  const notesPage = Number(sp.notesPage) || 1;
+  const notesPageSize = Number(sp.notesPageSize) || 10;
+
+  const [result, customersResult, serviceResult, notesResult, settingsResult, maintenanceSettingsResult, inspectionsResult, templatesResult] = await Promise.all([
     getVehicle(id),
     getCustomersList(),
     getServiceRecordsPaginated(id, { page, pageSize, search, type }),
+    getNotesPaginated(id, { page: notesPage, pageSize: notesPageSize }),
     getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
     getSettings([
       SETTING_KEYS.PREDICTED_MAINTENANCE_ENABLED,
@@ -55,6 +60,10 @@ export default async function VehicleDetailPage({
     ? serviceResult.data
     : { records: [], total: 0, page: 1, pageSize: 10, totalPages: 0 };
 
+  const paginatedNotes = notesResult.success && notesResult.data
+    ? notesResult.data
+    : { records: [], total: 0, page: 1, pageSize: 10, totalPages: 0 };
+
   const currencySettings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const currencyCode = currencySettings[SETTING_KEYS.CURRENCY_CODE] || "USD";
   const unitSystem = (currencySettings[SETTING_KEYS.UNIT_SYSTEM] || "imperial") as "metric" | "imperial";
@@ -112,6 +121,7 @@ export default async function VehicleDetailPage({
           vehicle={result.data}
           customers={customersResult.data ?? []}
           paginatedServices={paginatedServices}
+          paginatedNotes={paginatedNotes}
           serviceSearch={search}
           serviceType={type}
           currencyCode={currencyCode}

+ 93 - 83
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -1,7 +1,7 @@
 'use client'
 
-import { useState, useTransition } from 'react'
-import { useRouter } from 'next/navigation'
+import { useState, useTransition, useCallback } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
 import Link from 'next/link'
 import { useFormatDate } from '@/lib/use-format-date'
 import { Card, CardContent } from '@/components/ui/card'
@@ -16,11 +16,18 @@ import {
   TableHeader,
   TableRow,
 } from '@/components/ui/table'
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
 import { useGlassModal } from '@/components/glass-modal'
 import { VehicleForm } from '@/features/vehicles/Components/VehicleForm'
 import { NoteForm } from '@/features/vehicles/Components/NoteForm'
 import { ReminderForm } from '@/features/vehicles/Components/ReminderForm'
 import { ServiceRecordsTable } from './service-records-table'
+import { NotesTable } from './notes-table'
 import { toast } from 'sonner'
 import { deleteNote, toggleNotePin } from '@/features/vehicles/Actions/noteActions'
 import { toggleReminder, deleteReminder } from '@/features/vehicles/Actions/reminderActions'
@@ -46,7 +53,6 @@ import {
   MoreVertical,
   Pencil,
   Pin,
-  PinOff,
   Plus,
   StickyNote,
   Trash2,
@@ -102,6 +108,20 @@ interface PaginatedServices {
   totalPages: number
 }
 
+interface PaginatedNotes {
+  records: {
+    id: string
+    title: string
+    content: string
+    isPinned: boolean
+    createdAt: Date
+  }[]
+  total: number
+  page: number
+  pageSize: number
+  totalPages: number
+}
+
 interface VehicleDetail {
   id: string
   make: string
@@ -132,13 +152,6 @@ interface VehicleDetail {
     cost: number
     totalAmount: number
   }[]
-  notes: {
-    id: string
-    title: string
-    content: string
-    isPinned: boolean
-    createdAt: Date
-  }[]
   reminders: {
     id: string
     title: string
@@ -159,6 +172,7 @@ export function VehicleDetailClient({
   vehicle,
   customers,
   paginatedServices,
+  paginatedNotes,
   serviceSearch,
   serviceType,
   currencyCode = 'USD',
@@ -170,6 +184,7 @@ export function VehicleDetailClient({
   vehicle: VehicleDetail
   customers: CustomerOption[]
   paginatedServices: PaginatedServices
+  paginatedNotes: PaginatedNotes
   serviceSearch: string
   serviceType: string
   currencyCode?: string
@@ -196,16 +211,36 @@ export function VehicleDetailClient({
 }) {
   const distUnit = unitSystem === 'metric' ? 'km' : 'mi'
   const router = useRouter()
+  const searchParams = useSearchParams()
   const { formatDate } = useFormatDate()
+
+  const validTabs = ['services', 'inspections', 'notes', 'reminders'] as const
+  const tabParam = searchParams.get('tab')
+  const activeTab = validTabs.includes(tabParam as (typeof validTabs)[number])
+    ? (tabParam as string)
+    : 'services'
+
+  const handleTabChange = useCallback((value: string) => {
+    const params = new URLSearchParams(searchParams.toString())
+    if (value === 'services') {
+      params.delete('tab')
+    } else {
+      params.set('tab', value)
+    }
+    const query = params.toString()
+    router.replace(`/vehicles/${vehicle.id}${query ? `?${query}` : ''}`, { scroll: false })
+  }, [searchParams, router, vehicle.id])
   const modal = useGlassModal()
   const [showEditForm, setShowEditForm] = useState(false)
   const [showNoteForm, setShowNoteForm] = useState(false)
+  const [editingNote, setEditingNote] = useState<PaginatedNotes['records'][number] | undefined>()
   const [showReminderForm, setShowReminderForm] = useState(false)
   const [editingReminder, setEditingReminder] = useState<
     VehicleDetail['reminders'][number] | undefined
   >()
   const [reminderFilter, setReminderFilter] = useState<'active' | 'completed' | 'all'>('active')
   const [showImage, setShowImage] = useState(false)
+  const [selectedNote, setSelectedNote] = useState<PaginatedNotes['records'][number] | null>(null)
   const [showArchiveDialog, setShowArchiveDialog] = useState(false)
   const [showDeleteDialog, setShowDeleteDialog] = useState(false)
   const [showNewInspection, setShowNewInspection] = useState(false)
@@ -553,7 +588,7 @@ export function VehicleDetailClient({
       </div>
 
       {/* Tabs */}
-      <Tabs defaultValue="services" className="space-y-4">
+      <Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-4">
         <TabsList className="grid w-full grid-cols-4">
           <TabsTrigger value="services" className="gap-1.5">
             <Wrench className="h-3.5 w-3.5" />
@@ -668,77 +703,19 @@ export function VehicleDetailClient({
         </TabsContent>
 
         {/* Notes Tab */}
-        <TabsContent value="notes" className="space-y-4">
-          <div className="flex justify-end">
-            <Button size="sm" onClick={() => setShowNoteForm(true)}>
-              <Plus className="mr-1 h-3.5 w-3.5" />
-              Add Note
-            </Button>
-          </div>
-
-          <div className="rounded-lg border">
-            <Table>
-              <TableHeader>
-                <TableRow>
-                  <TableHead className="w-7.5"></TableHead>
-                  <TableHead>Title</TableHead>
-                  <TableHead className="hidden sm:table-cell">Content</TableHead>
-                  <TableHead className="w-30">Date</TableHead>
-                  <TableHead className="w-12.5"></TableHead>
-                </TableRow>
-              </TableHeader>
-              <TableBody>
-                {vehicle.notes.length === 0 ? (
-                  <TableRow>
-                    <TableCell colSpan={5} className="h-32 text-center text-muted-foreground">
-                      No notes yet.
-                    </TableCell>
-                  </TableRow>
-                ) : (
-                  vehicle.notes.map((n) => (
-                    <TableRow key={n.id}>
-                      <TableCell className="w-[30px] px-2">
-                        {n.isPinned && <Pin className="h-3.5 w-3.5 text-primary" />}
-                      </TableCell>
-                      <TableCell className="font-medium">{n.title}</TableCell>
-                      <TableCell className="hidden max-w-0 sm:table-cell">
-                        <p className="truncate text-sm text-muted-foreground">{n.content}</p>
-                      </TableCell>
-                      <TableCell className="font-mono text-xs text-muted-foreground">
-                        {formatDate(new Date(n.createdAt))}
-                      </TableCell>
-                      <TableCell className="px-2">
-                        <DropdownMenu>
-                          <DropdownMenuTrigger asChild>
-                            <Button variant="ghost" size="icon" className="h-7 w-7">
-                              <MoreVertical className="h-3.5 w-3.5" />
-                            </Button>
-                          </DropdownMenuTrigger>
-                          <DropdownMenuContent align="end">
-                            <DropdownMenuItem onClick={() => handleTogglePin(n.id)}>
-                              {n.isPinned ? (
-                                <PinOff className="mr-2 h-4 w-4" />
-                              ) : (
-                                <Pin className="mr-2 h-4 w-4" />
-                              )}
-                              {n.isPinned ? 'Unpin' : 'Pin'}
-                            </DropdownMenuItem>
-                            <DropdownMenuItem
-                              className="text-destructive"
-                              onClick={() => handleDeleteNote(n.id)}
-                            >
-                              <Trash2 className="mr-2 h-4 w-4" />
-                              Delete
-                            </DropdownMenuItem>
-                          </DropdownMenuContent>
-                        </DropdownMenu>
-                      </TableCell>
-                    </TableRow>
-                  ))
-                )}
-              </TableBody>
-            </Table>
-          </div>
+        <TabsContent value="notes">
+          <NotesTable
+            vehicleId={vehicle.id}
+            records={paginatedNotes.records}
+            total={paginatedNotes.total}
+            page={paginatedNotes.page}
+            pageSize={paginatedNotes.pageSize}
+            totalPages={paginatedNotes.totalPages}
+            onSelectNote={setSelectedNote}
+            onAddNote={() => { setEditingNote(undefined); setShowNoteForm(true); }}
+            onTogglePin={handleTogglePin}
+            onDeleteNote={handleDeleteNote}
+          />
         </TabsContent>
 
         {/* Reminders Tab */}
@@ -881,7 +858,40 @@ export function VehicleDetailClient({
         vehicle={vehicle}
         customers={customers}
       />
-      <NoteForm vehicleId={vehicle.id} open={showNoteForm} onOpenChange={setShowNoteForm} />
+      <NoteForm vehicleId={vehicle.id} open={showNoteForm} onOpenChange={setShowNoteForm} note={editingNote} />
+      <Dialog open={!!selectedNote} onOpenChange={(open) => { if (!open) setSelectedNote(null) }}>
+        <DialogContent className="sm:max-w-3xl">
+          <DialogHeader>
+            <DialogTitle className="flex items-center gap-2">
+              {selectedNote?.isPinned && <Pin className="h-4 w-4 text-primary" />}
+              {selectedNote?.title}
+            </DialogTitle>
+            <p className="text-xs text-muted-foreground">
+              {selectedNote?.createdAt && formatDate(new Date(selectedNote.createdAt))}
+            </p>
+          </DialogHeader>
+          <div
+            className="notes-content max-h-[60vh] overflow-y-auto text-sm break-all"
+            dangerouslySetInnerHTML={{ __html: selectedNote?.content ?? "" }}
+          />
+          <div className="flex justify-end pt-2">
+            <Button
+              size="sm"
+              variant="outline"
+              onClick={() => {
+                if (selectedNote) {
+                  setEditingNote(selectedNote)
+                  setSelectedNote(null)
+                  setShowNoteForm(true)
+                }
+              }}
+            >
+              <Pencil className="mr-1 h-3.5 w-3.5" />
+              Edit
+            </Button>
+          </div>
+        </DialogContent>
+      </Dialog>
       <ReminderForm
         vehicleId={vehicle.id}
         open={showReminderForm}

+ 1 - 1
src/components/ui/dialog.tsx

@@ -61,7 +61,7 @@ function DialogContent({
       <DialogPrimitive.Content
         data-slot="dialog-content"
         className={cn(
-          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-3rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-3rem)] max-h-[85vh] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
           className
         )}
         {...props}

+ 132 - 0
src/components/ui/rich-text-editor.tsx

@@ -0,0 +1,132 @@
+"use client";
+
+import { useEditor, EditorContent } from "@tiptap/react";
+import StarterKit from "@tiptap/starter-kit";
+import Placeholder from "@tiptap/extension-placeholder";
+import {
+  Bold,
+  Italic,
+  Underline as UnderlineIcon,
+  Strikethrough,
+  List,
+  ListOrdered,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { useEffect } from "react";
+
+interface RichTextEditorProps {
+  content: string;
+  onChange: (html: string) => void;
+  placeholder?: string;
+}
+
+function ToolbarButton({
+  onClick,
+  isActive,
+  children,
+}: {
+  onClick: () => void;
+  isActive: boolean;
+  children: React.ReactNode;
+}) {
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      className={cn(
+        "inline-flex h-7 w-7 items-center justify-center rounded-md text-sm transition-colors",
+        isActive
+          ? "bg-primary/15 text-primary"
+          : "text-muted-foreground hover:bg-muted hover:text-foreground"
+      )}
+    >
+      {children}
+    </button>
+  );
+}
+
+export function RichTextEditor({
+  content,
+  onChange,
+  placeholder = "Write something...",
+}: RichTextEditorProps) {
+  const editor = useEditor({
+    immediatelyRender: false,
+    extensions: [
+      StarterKit.configure({
+        bulletList: { HTMLAttributes: { class: "list-disc pl-4" } },
+        orderedList: { HTMLAttributes: { class: "list-decimal pl-4" } },
+      }),
+      Placeholder.configure({ placeholder }),
+    ],
+    content,
+    onUpdate: ({ editor }) => {
+      onChange(editor.getHTML());
+    },
+    editorProps: {
+      attributes: {
+        class:
+          "tiptap-content min-h-[120px] max-h-[40vh] overflow-y-auto overflow-x-hidden px-3 py-2 text-sm outline-none break-all",
+      },
+    },
+  });
+
+  // Sync external content changes (e.g. when switching between create/edit)
+  useEffect(() => {
+    if (editor && content !== editor.getHTML()) {
+      editor.commands.setContent(content);
+    }
+  }, [content, editor]);
+
+  if (!editor) return null;
+
+  return (
+    <div className="rounded-md border border-input shadow-xs focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50">
+      {/* Toolbar */}
+      <div className="flex items-center gap-0.5 border-b px-2 py-1">
+        <ToolbarButton
+          onClick={() => editor.chain().focus().toggleBold().run()}
+          isActive={editor.isActive("bold")}
+        >
+          <Bold className="h-3.5 w-3.5" />
+        </ToolbarButton>
+        <ToolbarButton
+          onClick={() => editor.chain().focus().toggleItalic().run()}
+          isActive={editor.isActive("italic")}
+        >
+          <Italic className="h-3.5 w-3.5" />
+        </ToolbarButton>
+        <ToolbarButton
+          onClick={() => editor.chain().focus().toggleUnderline().run()}
+          isActive={editor.isActive("underline")}
+        >
+          <UnderlineIcon className="h-3.5 w-3.5" />
+        </ToolbarButton>
+        <ToolbarButton
+          onClick={() => editor.chain().focus().toggleStrike().run()}
+          isActive={editor.isActive("strike")}
+        >
+          <Strikethrough className="h-3.5 w-3.5" />
+        </ToolbarButton>
+
+        <div className="mx-1 h-4 w-px bg-border" />
+
+        <ToolbarButton
+          onClick={() => editor.chain().focus().toggleBulletList().run()}
+          isActive={editor.isActive("bulletList")}
+        >
+          <List className="h-3.5 w-3.5" />
+        </ToolbarButton>
+        <ToolbarButton
+          onClick={() => editor.chain().focus().toggleOrderedList().run()}
+          isActive={editor.isActive("orderedList")}
+        >
+          <ListOrdered className="h-3.5 w-3.5" />
+        </ToolbarButton>
+      </div>
+
+      {/* Editor */}
+      <EditorContent editor={editor} />
+    </div>
+  );
+}

+ 47 - 1
src/features/vehicles/Actions/noteActions.ts

@@ -3,9 +3,43 @@
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
-import { createNoteSchema } from "../Schema/noteSchema";
+import { createNoteSchema, updateNoteSchema } from "../Schema/noteSchema";
 import { revalidatePath } from "next/cache";
 
+export async function getNotesPaginated(
+  vehicleId: string,
+  params: { page?: number; pageSize?: number }
+) {
+  return withAuth(async ({ organizationId }) => {
+    const vehicle = await db.vehicle.findFirst({
+      where: { id: vehicleId, organizationId },
+    });
+    if (!vehicle) throw new Error("Vehicle not found");
+
+    const page = params.page || 1;
+    const pageSize = params.pageSize || 10;
+    const skip = (page - 1) * pageSize;
+
+    const [records, total] = await Promise.all([
+      db.note.findMany({
+        where: { vehicleId },
+        orderBy: [{ isPinned: "desc" }, { createdAt: "desc" }],
+        skip,
+        take: pageSize,
+      }),
+      db.note.count({ where: { vehicleId } }),
+    ]);
+
+    return {
+      records,
+      total,
+      page,
+      pageSize,
+      totalPages: Math.ceil(total / pageSize),
+    };
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.VEHICLES }] });
+}
+
 export async function createNote(input: unknown) {
   return withAuth(async ({ organizationId }) => {
     const data = createNoteSchema.parse(input);
@@ -20,6 +54,18 @@ export async function createNote(input: unknown) {
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
 }
 
+export async function updateNote(input: unknown) {
+  return withAuth(async ({ organizationId }) => {
+    const data = updateNoteSchema.parse(input);
+    const note = await db.note.findFirst({
+      where: { id: data.id, vehicle: { organizationId } },
+    });
+    if (!note) throw new Error("Note not found");
+    await db.note.update({ where: { id: data.id }, data });
+    revalidatePath(`/vehicles/${note.vehicleId}`);
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });
+}
+
 export async function toggleNotePin(noteId: string) {
   return withAuth(async ({ organizationId }) => {
     const note = await db.note.findFirst({

+ 45 - 22
src/features/vehicles/Components/NoteForm.tsx

@@ -1,11 +1,10 @@
 "use client";
 
-import { useState } from "react";
+import { useState, useEffect } from "react";
 import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
 import {
   Dialog,
   DialogContent,
@@ -14,37 +13,57 @@ import {
 } from "@/components/ui/dialog";
 import { useGlassModal } from "@/components/glass-modal";
 import { toast } from "sonner";
-import { createNote } from "../Actions/noteActions";
+import { createNote, updateNote } from "../Actions/noteActions";
+import { RichTextEditor } from "@/components/ui/rich-text-editor";
 import { Loader2 } from "lucide-react";
 
 interface NoteFormProps {
   vehicleId: string;
   open: boolean;
   onOpenChange: (open: boolean) => void;
+  note?: {
+    id: string;
+    title: string;
+    content: string;
+    isPinned: boolean;
+  };
 }
 
-export function NoteForm({ vehicleId, open, onOpenChange }: NoteFormProps) {
+export function NoteForm({ vehicleId, open, onOpenChange, note }: NoteFormProps) {
   const router = useRouter();
   const modal = useGlassModal();
   const [loading, setLoading] = useState(false);
+  const [title, setTitle] = useState("");
+  const [content, setContent] = useState("");
+
+  const isEditing = !!note;
+
+  useEffect(() => {
+    if (open) {
+      setTitle(note?.title ?? "");
+      setContent(note?.content ?? "");
+    }
+  }, [open, note]);
 
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault();
+    if (!title.trim() || !content.trim() || content === "<p></p>") {
+      modal.open("error", "Error", "Title and content are required");
+      return;
+    }
+
     setLoading(true);
 
-    const formData = new FormData(e.currentTarget);
-    const result = await createNote({
-      vehicleId,
-      title: formData.get("title") as string,
-      content: formData.get("content") as string,
-    });
+    const result = isEditing
+      ? await updateNote({ id: note.id, title, content })
+      : await createNote({ vehicleId, title, content });
 
     if (result.success) {
-      toast.success("Note created");
+      toast.success(isEditing ? "Note updated" : "Note created");
       onOpenChange(false);
       router.refresh();
     } else {
-      modal.open("error", "Error", result.error || "Failed to create note");
+      modal.open("error", "Error", result.error || `Failed to ${isEditing ? "update" : "create"} note`);
     }
 
     setLoading(false);
@@ -52,25 +71,29 @@ export function NoteForm({ vehicleId, open, onOpenChange }: NoteFormProps) {
 
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="sm:max-w-md">
+      <DialogContent className="sm:max-w-3xl">
         <DialogHeader>
-          <DialogTitle>Add Note</DialogTitle>
+          <DialogTitle>{isEditing ? "Edit Note" : "Add Note"}</DialogTitle>
         </DialogHeader>
 
         <form onSubmit={handleSubmit} className="space-y-4">
           <div className="space-y-2">
             <Label htmlFor="title">Title *</Label>
-            <Input id="title" name="title" placeholder="Note title" required />
+            <Input
+              id="title"
+              placeholder="Note title"
+              value={title}
+              onChange={(e) => setTitle(e.target.value)}
+              required
+            />
           </div>
 
           <div className="space-y-2">
-            <Label htmlFor="content">Content *</Label>
-            <Textarea
-              id="content"
-              name="content"
+            <Label>Content *</Label>
+            <RichTextEditor
+              content={content}
+              onChange={setContent}
               placeholder="Write your note..."
-              rows={5}
-              required
             />
           </div>
 
@@ -84,7 +107,7 @@ export function NoteForm({ vehicleId, open, onOpenChange }: NoteFormProps) {
             </Button>
             <Button type="submit" disabled={loading}>
               {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-              Add Note
+              {isEditing ? "Save Changes" : "Add Note"}
             </Button>
           </div>
         </form>