From 248afbb0ae5fd420276f794a55622479c3578875 Mon Sep 17 00:00:00 2001 From: dhinesh-m24 Date: Thu, 5 Feb 2026 12:18:55 +0530 Subject: [PATCH] fix: Verify functionality of Add Client and Client List --- apps/web/package.json | 1 + apps/web/pnpm-lock.yaml | 3 + apps/web/src/common/components/ui/dailog.tsx | 119 +++ .../src/common/components/ui/use-toast.tsx | 25 + .../features/business/clients/AddClient.tsx | 31 +- .../features/business/clients/ClientList.tsx | 2 +- .../features/business/rates/ServiceRates.tsx | 823 ++++++++++++++++++ .../rates/components/RateCardModal.tsx | 143 +++ apps/web/src/routes.tsx | 4 + 9 files changed, 1147 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/common/components/ui/dailog.tsx create mode 100644 apps/web/src/common/components/ui/use-toast.tsx create mode 100644 apps/web/src/features/business/rates/ServiceRates.tsx create mode 100644 apps/web/src/features/business/rates/components/RateCardModal.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 385c8b38..414b12ba 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,7 @@ "dependencies": { "@firebase/analytics": "^0.10.19", "@firebase/data-connect": "^0.3.12", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 9c831927..67f7397b 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@firebase/data-connect': specifier: ^0.3.12 version: 0.3.12(@firebase/app@0.14.7) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) diff --git a/apps/web/src/common/components/ui/dailog.tsx b/apps/web/src/common/components/ui/dailog.tsx new file mode 100644 index 00000000..b718cca1 --- /dev/null +++ b/apps/web/src/common/components/ui/dailog.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/apps/web/src/common/components/ui/use-toast.tsx b/apps/web/src/common/components/ui/use-toast.tsx new file mode 100644 index 00000000..dc710d79 --- /dev/null +++ b/apps/web/src/common/components/ui/use-toast.tsx @@ -0,0 +1,25 @@ +// Simplified use-toast hook +import { useState } from "react"; + +type ToastProps = { + title?: string; + description?: string; + variant?: "default" | "destructive"; +}; + +export const useToast = () => { + const [toasts, setToasts] = useState([]); + + const toast = ({ title, description, variant = "default" }: ToastProps) => { + const newToast = { title, description, variant }; + setToasts((prev) => [...prev, newToast]); + console.log("Toast:", title, description); + + // Auto dismiss after 3 seconds + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t !== newToast)); + }, 3000); + }; + + return { toast, toasts }; +}; diff --git a/apps/web/src/features/business/clients/AddClient.tsx b/apps/web/src/features/business/clients/AddClient.tsx index e277f72c..9313b9f6 100644 --- a/apps/web/src/features/business/clients/AddClient.tsx +++ b/apps/web/src/features/business/clients/AddClient.tsx @@ -18,13 +18,14 @@ import { useSelector } from "react-redux"; import type { RootState } from "@/store/store"; import { useCreateBusiness, - useCreateTeamHub + useCreateTeamHub, + useCreateTeam } from "@/dataconnect-generated/react"; import { BusinessArea, BusinessSector, BusinessStatus, - BusinessRateGroup + BusinessRateGroup, } from "@/dataconnect-generated"; import { dataConnect } from "@/features/auth/firebase"; import { motion, AnimatePresence } from "framer-motion"; @@ -55,6 +56,7 @@ export default function AddClient() { const { mutateAsync: createBusiness, isPending: isCreatingBusiness } = useCreateBusiness(dataConnect); const { mutateAsync: createHub, isPending: isCreatingHub } = useCreateTeamHub(dataConnect); + const { mutateAsync: createTeam, isPending: isCreatingTeam } = useCreateTeam(dataConnect); const handleChange = (field: string, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -82,12 +84,35 @@ export default function AddClient() { status: formData.status, notes: formData.notes }); + console.log("Business created:", businessResult); const businessId = businessResult.business_insert.id; + if (!businessId) { + throw new Error("Business creation failed — no ID returned."); + } + + // Create the team for this business + const teamResult = await createTeam({ + teamName: `${formData.businessName} Team`, + ownerId: businessId, + ownerName: formData.contactName, + ownerRole: "ADMIN", + email: formData.email, + companyLogo: formData.companyLogoUrl || null, + totalMembers: 0, + activeMembers: 0, + totalHubs: 0 + }); + + const teamId = teamResult.team_insert.id; + + if (!teamId) { + throw new Error("Team creation failed — no ID returned."); + } // 2. Automatically create the client's first "hub" or location await createHub({ - teamId: businessId, + teamId: teamId, hubName: `${formData.businessName} - Main Hub`, address: formData.address || "Main Office", city: formData.city, diff --git a/apps/web/src/features/business/clients/ClientList.tsx b/apps/web/src/features/business/clients/ClientList.tsx index 53a28cf0..de047721 100644 --- a/apps/web/src/features/business/clients/ClientList.tsx +++ b/apps/web/src/features/business/clients/ClientList.tsx @@ -172,7 +172,7 @@ export default function ClientList() { All Statuses Active Pending - Inactive + Suspended setRenameValue(e.target.value)} + onBlur={() => handleRenameCard(cardData.id, tab)} + onKeyDown={(e) => { + if (e.key === "Enter") handleRenameCard(cardData.id, tab); + if (e.key === "Escape") setRenamingCard(null); + }} + autoFocus + className="px-3 py-2 rounded-lg text-sm font-semibold border border-primary bg-white focus:outline-none w-40" + /> +
+ ); + } + + return ( +
+ + {/* Hover actions simplified */} +
+ +
+
+ ); + })} +
+ + + + {/* KPIs Section */} +
+ + +
+ +

Average Rate

+
+
+

{fmtCurrency(kpis.avg)}

+

Across {kpis.total} positions active in book

+
+
+
+ + + +
+ +

Coverage

+
+
+

{kpis.total} roles

+

Total defined positions

+
+
+
+ + + +
+ +

Spread

+
+
+

{fmtCurrency(kpis.min)} – {fmtCurrency(kpis.max)}

+

Rate range (Min - Max)

+
+
+
+
+ + {/* Filters - styled like the Search/Filter blocks */} +
+
+
+ + Filters +
+ + {/* Categories */} +
+ {["All", ...CATEGORIES].map((cat) => ( + + ))} +
+
+ +
+ + setSearch(e.target.value)} + placeholder="Search positions..." + className="pl-9 h-9 bg-white border-border/50" + /> +
+
+ + {/* Main Rates Table */} +
+ {/* Section Header */} +
+
+
+ 💰 +
+
+

Rate Breakdown

+

Detailed pricing for {pricebook}

+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + {filtered.map((r: any, idx) => { + const parsed = parseRoleName(r.position); + const competitive = competitivenessData?.find( + (c) => c.position === parsed.position, + ); + const isEditing = editing === r.id; + + return ( + + + + + + + + + + + + + + ); + })} + +
PositionCategoryRegionBase Wage{pricebook} RateOT 8hOT 12hHolidayMarket RateComp. ScoreActions
+
+ {/* Icon for position */} +
+ {parsed.position.substring(0, 2).toUpperCase()} +
+

{parsed.position}

+
+
+ + {r.category} + + + + {r.region !== '—' && } + {r.region} + + + + {fmtCurrency(r.employee_wage)} + + + {isEditing ? ( + { + handleUpdateVendorRate({ + id: r.id, + clientRate: parseFloat(e.target.value) + }); + setEditing(null); + toast({ title: "Rate updated successfully" }); + }} + autoFocus + /> + ) : ( + + {fmtCurrency(r.proposedRate)} + + )} + + {isEditing ? ( + { + handleUpdateVendorRate({ + id: r.id, + overtime8Multiplier: parseFloat(e.target.value) + }); + }} + /> + ) : ( + {r.overtime8Multiplier}x + )} + + {isEditing ? ( + { + handleUpdateVendorRate({ + id: r.id, + overtime12Multiplier: parseFloat(e.target.value) + }); + }} + /> + ) : ( + {r.overtime12Multiplier}x + )} + + {isEditing ? ( + { + handleUpdateVendorRate({ + id: r.id, + holidayRate: parseFloat(e.target.value) + }); + }} + /> + ) : ( + {fmtCurrency(r.holidayRate)} + )} + + {competitive ? ( +
+ {fmtCurrency(competitive.marketRate)} + + {r.proposedRate < competitive.marketRate ? "▼ Below" : "▲ Above"} + +
+ ) : } +
+ {competitive ? ( +
+
+ {competitive.score}/100 +
+ ) : } +
+ +
+
+
+
+ + { + setShowRateCardModal(false); + setEditingRateCard(null); + }} + onSave={handleSaveRateCard} + editingCard={editingRateCard} + /> + + + ); +} + +// NOTE: Creating a dummy FilesIcon component to suppress errors if it's missing, or assumelucide import +const FilesIcon = FileText; + +// The user asked to match InvoiceEditor design. InvoiceEditor is primarily a form/management view. +// I will ensure the VendorCompanyPricebookView is the primary export and fully fleshed out. + +export default function ServiceRates() { + const { user } = useSelector((state: any) => state.auth); + const { data: vendorData } = useGetVendorByUserId({ userId: user?.uid || "" }, { enabled: !!user?.uid }); + + const vendorName = vendorData?.vendors[0]?.companyName || "Vendor"; + + return ; +} diff --git a/apps/web/src/features/business/rates/components/RateCardModal.tsx b/apps/web/src/features/business/rates/components/RateCardModal.tsx new file mode 100644 index 00000000..d4535736 --- /dev/null +++ b/apps/web/src/features/business/rates/components/RateCardModal.tsx @@ -0,0 +1,143 @@ +import { Badge } from "@/common/components/ui/badge"; +import { Button } from "@/common/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/common/components/ui/dailog"; +import { Input } from "@/common/components/ui/input"; +import { Label } from "@/common/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/common/components/ui/select"; +import { useEffect, useState } from "react"; +import { Briefcase } from "lucide-react"; + +interface RateCardModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (cardData: any) => void; + editingCard?: any; +} + +export default function RateCardModal({ + isOpen, + onClose, + onSave, + editingCard, +}: RateCardModalProps) { + const [formData, setFormData] = useState({ + name: "", + baseBook: "FoodBuy", + discount: 0, + }); + + useEffect(() => { + if (editingCard) { + setFormData({ + name: editingCard.name, + baseBook: editingCard.baseBook, + discount: editingCard.discount, + }); + } else { + setFormData({ + name: "", + baseBook: "FoodBuy", + discount: 0, + }); + } + }, [editingCard, isOpen]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave(formData); + onClose(); + }; + + return ( + + + + + + {editingCard ? "Edit Rate Card" : "New Rate Card"} + + +
+
+ + + setFormData({ ...formData, name: e.target.value }) + } + required + className="bg-card border-border/50 focus:border-primary" + /> +
+ +
+ + +

+ Starting rates will be pulled from this master book +

+
+ +
+ +
+ + setFormData({ + ...formData, + discount: parseFloat(e.target.value), + }) + } + className="w-24 bg-card border-border/50 focus:border-primary font-mono text-right" + /> + + Applied to all rates + +
+
+
+ + + + +
+
+ ); +} diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index 2355cfa8..c71d6d3c 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -15,6 +15,9 @@ import AddStaff from './features/workforce/directory/AddStaff'; import ClientList from './features/business/clients/ClientList'; import EditClient from './features/business/clients/EditClient'; import AddClient from './features/business/clients/AddClient'; +import ServiceRates from './features/business/rates/ServiceRates'; + + /** * AppRoutes Component * Defines the main routing structure of the application. @@ -87,6 +90,7 @@ const AppRoutes: React.FC = () => { } /> } /> } /> + } /> } />