소스 검색

user releation

Bernt Christian Egeland 7 달 전
부모
커밋
c40f40f54b

+ 2 - 0
prisma/migrations/20260216180745_organization_user_relation/migration.sql

@@ -0,0 +1,2 @@
+-- AddForeignKey
+ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 2 - 0
prisma/schema.prisma

@@ -28,6 +28,7 @@ model User {
   quotes                 Quote[]
   customFieldDefinitions CustomFieldDefinition[]
   twoFactor              TwoFactor?
+  organizationMembers    OrganizationMember[]
 
   @@map("users")
 }
@@ -485,6 +486,7 @@ model OrganizationMember {
   role String @default("member")
 
   userId         String
+  user           User         @relation(fields: [userId], references: [id], onDelete: Cascade)
   organizationId String
   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 

+ 23 - 1
src/app/(authenticated)/vehicles/[id]/service/[serviceId]/edit/page.tsx

@@ -4,6 +4,8 @@ import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { getInventoryPartsList } from "@/features/inventory/Actions/inventoryActions";
 import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
 import { ServiceRecordForm } from "../../new/service-record-form";
+import { db } from "@/lib/db";
+import { getCachedSession, getCachedMembership } from "@/lib/cached-session";
 
 export default async function EditServicePage({
   params,
@@ -11,11 +13,12 @@ export default async function EditServicePage({
   params: Promise<{ id: string; serviceId: string }>;
 }) {
   const { id, serviceId } = await params;
-  const [result, settingsResult, inventoryResult, vehiclesResult] = await Promise.all([
+  const [result, settingsResult, inventoryResult, vehiclesResult, session] = await Promise.all([
     getServiceRecord(serviceId),
     getSettings([SETTING_KEYS.DEFAULT_TAX_RATE, SETTING_KEYS.TAX_ENABLED, SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.DEFAULT_LABOR_RATE]),
     getInventoryPartsList(),
     getVehicles(),
+    getCachedSession(),
   ]);
 
   if (!result.success || !result.data) {
@@ -74,6 +77,23 @@ export default async function EditServicePage({
 
   const vehicleName = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`;
 
+  // Fetch team members and current user name for technician dropdown
+  const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null;
+  const orgId = membership?.organizationId;
+  const [members, currentUser] = await Promise.all([
+    orgId
+      ? db.organizationMember.findMany({
+          where: { organizationId: orgId },
+          select: { id: true, user: { select: { name: true } } },
+        })
+      : Promise.resolve([]),
+    session?.user?.id
+      ? db.user.findUnique({ where: { id: session.user.id }, select: { name: true } })
+      : Promise.resolve(null),
+  ]);
+  const teamMembers = members.map((m) => ({ id: m.id, name: m.user.name }));
+  const currentUserName = currentUser?.name || "";
+
   return (
     <ServiceRecordForm
       vehicleId={id}
@@ -85,6 +105,8 @@ export default async function EditServicePage({
       initialData={initialData}
       inventoryParts={inventoryParts}
       vehicles={vehicles}
+      teamMembers={teamMembers}
+      currentUserName={currentUserName}
     />
   );
 }

+ 71 - 8
src/app/(authenticated)/vehicles/[id]/service/new/service-record-form.tsx

@@ -89,6 +89,11 @@ interface VehicleOption {
   label: string
 }
 
+interface TeamMemberOption {
+  id: string
+  name: string
+}
+
 interface ServiceRecordFormProps {
   vehicleId: string
   vehicleName: string
@@ -99,6 +104,8 @@ interface ServiceRecordFormProps {
   initialData?: InitialData
   inventoryParts?: InventoryPartOption[]
   vehicles?: VehicleOption[]
+  teamMembers?: TeamMemberOption[]
+  currentUserName?: string
 }
 
 const emptyPart = (): ServicePartInput => ({
@@ -126,6 +133,8 @@ export function ServiceRecordForm({
   initialData,
   inventoryParts = [],
   vehicles = [],
+  teamMembers = [],
+  currentUserName = '',
 }: ServiceRecordFormProps) {
   const cs = getCurrencySymbol(currencyCode)
   const isEdit = !!initialData
@@ -139,6 +148,8 @@ export function ServiceRecordForm({
     const v = vehicles.find((v) => v.id === selectedVehicleId)
     return v?.label || vehicleName
   }, [selectedVehicleId, vehicles, vehicleName, isEdit])
+  const [techName, setTechName] = useState(initialData?.techName || currentUserName)
+  const [techOpen, setTechOpen] = useState(false)
   const [type, setType] = useState(initialData?.type || 'maintenance')
   const [status, setStatus] = useState(initialData?.status || 'completed')
   const [partItems, setPartItems] = useState<ServicePartInput[]>(initialData?.partItems || [])
@@ -423,7 +434,7 @@ export function ServiceRecordForm({
       cost: totalAmount,
       mileage: Number(formData.get('mileage')) || undefined,
       serviceDate: (formData.get('serviceDate') as string) || new Date().toISOString(),
-      techName: (formData.get('techName') as string) || undefined,
+      techName: techName || undefined,
       diagnosticNotes: (formData.get('diagnosticNotes') as string) || undefined,
       invoiceNotes: (formData.get('invoiceNotes') as string) || undefined,
       ...(isEdit && { invoiceNumber: (formData.get('invoiceNumber') as string) || undefined }),
@@ -592,13 +603,65 @@ export function ServiceRecordForm({
                 </div>
               </div>
               <div className="space-y-2">
-                <Label htmlFor="techName">Technician</Label>
-                <Input
-                  id="techName"
-                  name="techName"
-                  placeholder="Mike Johnson"
-                  defaultValue={initialData?.techName || ''}
-                />
+                <Label>Technician</Label>
+                <Popover open={techOpen} onOpenChange={setTechOpen}>
+                  <PopoverTrigger asChild>
+                    <Button
+                      variant="outline"
+                      role="combobox"
+                      aria-expanded={techOpen}
+                      className="w-full justify-between font-normal"
+                    >
+                      {techName || <span className="text-muted-foreground">Select or type a name...</span>}
+                      <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                    </Button>
+                  </PopoverTrigger>
+                  <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
+                    <Command>
+                      <CommandInput
+                        placeholder="Search or type name..."
+                        value={techName}
+                        onValueChange={setTechName}
+                      />
+                      <CommandList>
+                        <CommandEmpty>
+                          {techName ? (
+                            <button
+                              type="button"
+                              className="w-full px-2 py-1.5 text-sm text-left"
+                              onClick={() => setTechOpen(false)}
+                            >
+                              Use &quot;{techName}&quot;
+                            </button>
+                          ) : (
+                            'Type a name...'
+                          )}
+                        </CommandEmpty>
+                        {teamMembers.length > 0 && (
+                          <CommandGroup heading="Team Members">
+                            {teamMembers.map((member) => (
+                              <CommandItem
+                                key={member.id}
+                                value={member.name}
+                                onSelect={() => {
+                                  setTechName(member.name)
+                                  setTechOpen(false)
+                                }}
+                              >
+                                <Check
+                                  className={`mr-2 h-4 w-4 ${
+                                    techName === member.name ? 'opacity-100' : 'opacity-0'
+                                  }`}
+                                />
+                                {member.name}
+                              </CommandItem>
+                            ))}
+                          </CommandGroup>
+                        )}
+                      </CommandList>
+                    </Command>
+                  </PopoverContent>
+                </Popover>
               </div>
               {isEdit && (
                 <div className="space-y-2">

+ 8 - 2
src/features/vehicles/Actions/createDraftServiceRecord.ts

@@ -6,14 +6,14 @@ import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 export async function createDraftServiceRecord(vehicleId: string, serviceDate?: Date) {
   return withAuth(
-    async ({ organizationId }) => {
+    async ({ organizationId, userId }) => {
       const vehicle = await db.vehicle.findFirst({
         where: { id: vehicleId, organizationId },
       });
       if (!vehicle) throw new Error("Vehicle not found");
 
       // Auto-populate shop name and invoice number
-      const [settings, org] = await Promise.all([
+      const [settings, org, currentUser] = await Promise.all([
         db.appSetting.findMany({
           where: {
             organizationId,
@@ -26,11 +26,16 @@ export async function createDraftServiceRecord(vehicleId: string, serviceDate?:
           where: { id: organizationId },
           select: { name: true },
         }),
+        db.user.findUnique({
+          where: { id: userId },
+          select: { name: true },
+        }),
       ]);
       const settingsMap: Record<string, string> = {};
       for (const s of settings) settingsMap[s.key] = s.value;
 
       const shopName = org?.name || undefined;
+      const techName = currentUser?.name || undefined;
 
       // Resolve invoice prefix
       const rawPrefix = settingsMap["workshop.invoicePrefix"] || "{year}-";
@@ -73,6 +78,7 @@ export async function createDraftServiceRecord(vehicleId: string, serviceDate?:
           status: "pending",
           vehicleId,
           shopName,
+          techName,
           invoiceNumber,
           serviceDate: serviceDate ?? now,
         },