Răsfoiți Sursa

Fix service record save with inventory part (#71)

* fix but in inventory

* remove unused code

* improved the parts dialog, added infinite scroll

* show description in table
Bernt Christian Egeland 6 luni în urmă
părinte
comite
a89aaa7e30

+ 1 - 0
src/features/inventory/Actions/inventoryActions.ts

@@ -226,6 +226,7 @@ export async function getInventoryPartsList() {
         id: true,
         id: true,
         partNumber: true,
         partNumber: true,
         name: true,
         name: true,
+        description: true,
         unitCost: true,
         unitCost: true,
         sellPrice: true,
         sellPrice: true,
         quantity: true,
         quantity: true,

+ 4 - 4
src/features/vehicles/Actions/serviceActions.ts

@@ -399,10 +399,10 @@ export async function updateServiceRecord(input: unknown) {
         await tx.servicePart.deleteMany({ where: { serviceRecordId: id } });
         await tx.servicePart.deleteMany({ where: { serviceRecordId: id } });
         if (partItems.length > 0) {
         if (partItems.length > 0) {
           await tx.servicePart.createMany({
           await tx.servicePart.createMany({
-            data: partItems.map((p) => ({
-              ...p,
-              serviceRecordId: id,
-            })),
+            data: partItems.map((p) => {
+              const { inventoryPartId: _inv, ...partData } = p;
+              return { ...partData, serviceRecordId: id };
+            }),
           });
           });
         }
         }
       }
       }

+ 5 - 6
src/features/vehicles/Components/service-detail/ServiceDetailContent.tsx

@@ -3,7 +3,6 @@
 import { useEffect, useState } from 'react'
 import { useEffect, useState } from 'react'
 
 
 const SIDEBAR_MIN_W = 280
 const SIDEBAR_MIN_W = 280
-const SIDEBAR_DEFAULT_PCT = 0.22 // 22% of viewport
 
 
 interface ServiceDetailContentProps {
 interface ServiceDetailContentProps {
   leftColumn: React.ReactNode
   leftColumn: React.ReactNode
@@ -11,9 +10,7 @@ interface ServiceDetailContentProps {
 }
 }
 
 
 export function ServiceDetailContent({ leftColumn, rightColumn }: ServiceDetailContentProps) {
 export function ServiceDetailContent({ leftColumn, rightColumn }: ServiceDetailContentProps) {
-  const [sidebarWidth, setSidebarWidth] = useState(() =>
-    typeof window !== 'undefined' ? Math.max(SIDEBAR_MIN_W, window.innerWidth * SIDEBAR_DEFAULT_PCT) : 380
-  )
+  const [sidebarWidth, setSidebarWidth] = useState<number | null>(null)
   const [isDragging, setIsDragging] = useState(false)
   const [isDragging, setIsDragging] = useState(false)
 
 
   useEffect(() => {
   useEffect(() => {
@@ -55,12 +52,14 @@ export function ServiceDetailContent({ leftColumn, rightColumn }: ServiceDetailC
           onMouseDown={() => setIsDragging(true)}
           onMouseDown={() => setIsDragging(true)}
         >
         >
           <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-4 flex items-center justify-center rounded-sm border bg-background shadow-sm">
           <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-8 w-4 flex items-center justify-center rounded-sm border bg-background shadow-sm">
-            <svg width="6" height="14" viewBox="0 0 6 14" className="text-muted-foreground"><path d="M1 0v14M5 0v14" stroke="currentColor" strokeWidth="1" /></svg>
+            <svg width="6" height="14" viewBox="0 0 6 14" className="text-muted-foreground">
+              <path d="M1 0v14M5 0v14" stroke="currentColor" strokeWidth="1" />
+            </svg>
           </div>
           </div>
         </div>
         </div>
         <div
         <div
           className="shrink-0 overflow-y-auto overscroll-contain p-4 pl-2"
           className="shrink-0 overflow-y-auto overscroll-contain p-4 pl-2"
-          style={{ width: sidebarWidth }}
+          style={sidebarWidth != null ? { width: sidebarWidth } : { width: '22vw', minWidth: SIDEBAR_MIN_W }}
         >
         >
           <div className="space-y-3 pb-40">{rightColumn}</div>
           <div className="space-y-3 pb-40">{rightColumn}</div>
         </div>
         </div>

+ 47 - 18
src/features/vehicles/Components/service-edit/InventoryPickerDialog.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
@@ -9,6 +9,8 @@ import { formatCurrency } from '@/lib/format'
 import type { ServicePartInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { ServicePartInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { InventoryPartOption } from './form-types'
 import type { InventoryPartOption } from './form-types'
 
 
+const PAGE_SIZE = 100
+
 interface InventoryPickerDialogProps {
 interface InventoryPickerDialogProps {
   open: boolean
   open: boolean
   onOpenChange: (open: boolean) => void
   onOpenChange: (open: boolean) => void
@@ -26,6 +28,8 @@ export function InventoryPickerDialog({
 }: InventoryPickerDialogProps) {
 }: InventoryPickerDialogProps) {
   const t = useTranslations('service.parts')
   const t = useTranslations('service.parts')
   const [search, setSearch] = useState('')
   const [search, setSearch] = useState('')
+  const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
+  const listRef = useRef<HTMLDivElement>(null)
 
 
   const filtered = inventoryParts.filter((p) => {
   const filtered = inventoryParts.filter((p) => {
     if (!search) return true
     if (!search) return true
@@ -37,6 +41,24 @@ export function InventoryPickerDialog({
     )
     )
   })
   })
 
 
+  const visible = filtered.slice(0, visibleCount)
+
+  // Reset visible count when search changes or dialog opens
+  useEffect(() => {
+    setVisibleCount(PAGE_SIZE)
+  }, [search, open])
+
+  const handleScroll = useCallback(() => {
+    const el = listRef.current
+    if (!el) return
+    if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
+      setVisibleCount((prev) => {
+        if (prev >= filtered.length) return prev
+        return prev + PAGE_SIZE
+      })
+    }
+  }, [filtered.length])
+
   return (
   return (
     <Dialog
     <Dialog
       open={open}
       open={open}
@@ -45,7 +67,7 @@ export function InventoryPickerDialog({
         if (!v) setSearch('')
         if (!v) setSearch('')
       }}
       }}
     >
     >
-      <DialogContent className="sm:max-w-md">
+      <DialogContent className="sm:max-w-2xl">
         <DialogHeader>
         <DialogHeader>
           <DialogTitle>{t('inventoryTitle')}</DialogTitle>
           <DialogTitle>{t('inventoryTitle')}</DialogTitle>
         </DialogHeader>
         </DialogHeader>
@@ -58,14 +80,14 @@ export function InventoryPickerDialog({
             className="pl-9"
             className="pl-9"
           />
           />
         </div>
         </div>
-        <div className="max-h-75 overflow-y-auto space-y-1">
-          {filtered.map((ip) => (
+        <div ref={listRef} onScroll={handleScroll} className="max-h-75 overflow-y-auto space-y-1">
+          {visible.map((ip) => (
             <button
             <button
               key={ip.id}
               key={ip.id}
               type="button"
               type="button"
-              className="w-full text-left rounded-md p-2.5 hover:bg-accent transition-colors"
+              className="w-full text-left rounded-md px-2.5 py-1.5 hover:bg-accent transition-colors flex items-center justify-between gap-4"
               onClick={() => {
               onClick={() => {
-                const price = ip.sellPrice > 0 ? ip.sellPrice : ip.unitCost;
+                const price = ip.sellPrice > 0 ? ip.sellPrice : ip.unitCost
                 onSelectPart({
                 onSelectPart({
                   partNumber: ip.partNumber || '',
                   partNumber: ip.partNumber || '',
                   name: ip.name,
                   name: ip.name,
@@ -77,34 +99,41 @@ export function InventoryPickerDialog({
                 onOpenChange(false)
                 onOpenChange(false)
               }}
               }}
             >
             >
-              <div className="flex items-center justify-between">
-                <div>
-                  <span className="font-medium text-sm">{ip.name}</span>
+              <div className="min-w-0">
+                <div className="flex items-center gap-2">
+                  <span className="font-medium text-sm truncate">{ip.name}</span>
                   {ip.partNumber && (
                   {ip.partNumber && (
-                    <span className="ml-2 text-xs font-mono text-muted-foreground">
+                    <span className="text-xs font-mono text-muted-foreground shrink-0">
                       {ip.partNumber}
                       {ip.partNumber}
                     </span>
                     </span>
                   )}
                   )}
+                  {ip.category && (
+                    <span className="text-xs text-muted-foreground shrink-0">{ip.category}</span>
+                  )}
                 </div>
                 </div>
-                <span className="text-xs text-muted-foreground">{t('inStock', { quantity: ip.quantity })}</span>
+                {ip.description && (
+                  <p className="text-xs text-muted-foreground truncate">{ip.description}</p>
+                )}
               </div>
               </div>
-              <div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
-                {ip.category && <span>{ip.category}</span>}
+              <div className="flex items-center gap-3 shrink-0 text-xs text-muted-foreground">
                 {ip.sellPrice > 0 ? (
                 {ip.sellPrice > 0 ? (
                   <>
                   <>
-                    <span className="font-medium text-foreground">{formatCurrency(ip.sellPrice, currencyCode)}</span>
-                    <span className="line-through">{formatCurrency(ip.unitCost, currencyCode)}</span>
+                    <span className="font-medium text-foreground">
+                      {formatCurrency(ip.sellPrice, currencyCode)}
+                    </span>
+                    <span className="line-through">
+                      {formatCurrency(ip.unitCost, currencyCode)}
+                    </span>
                   </>
                   </>
                 ) : (
                 ) : (
                   <span>{formatCurrency(ip.unitCost, currencyCode)}</span>
                   <span>{formatCurrency(ip.unitCost, currencyCode)}</span>
                 )}
                 )}
+                <span>{t('inStock', { quantity: ip.quantity })}</span>
               </div>
               </div>
             </button>
             </button>
           ))}
           ))}
           {filtered.length === 0 && (
           {filtered.length === 0 && (
-            <p className="py-6 text-center text-sm text-muted-foreground">
-              {t('noInventory')}
-            </p>
+            <p className="py-6 text-center text-sm text-muted-foreground">{t('noInventory')}</p>
           )}
           )}
         </div>
         </div>
       </DialogContent>
       </DialogContent>

+ 1 - 0
src/features/vehicles/Components/service-edit/form-types.ts

@@ -8,6 +8,7 @@ export interface InventoryPartOption {
   id: string
   id: string
   partNumber: string | null
   partNumber: string | null
   name: string
   name: string
+  description: string | null
   unitCost: number
   unitCost: number
   sellPrice: number
   sellPrice: number
   quantity: number
   quantity: number