Files
Krow-workspace/apps/web/src/features/business/rates/ServiceRates.tsx

827 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Badge } from "@/common/components/ui/badge";
import { Button } from "@/common/components/ui/button";
import { Card, CardContent } from "@/common/components/ui/card";
import { Input } from "@/common/components/ui/input";
import { useToast } from "@/common/components/ui/use-toast";
import DashboardLayout from "@/features/layouts/DashboardLayout";
import { useQueryClient } from "@tanstack/react-query";
import {
BarChart3,
Briefcase,
DollarSign,
Download,
FileText,
Filter,
MapPin,
Pencil,
Plus,
Search,
Shield,
Sparkles,
Trash2
} from "lucide-react";
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import {
useListVendorRates,
useListCustomRateCards,
useCreateCustomRateCard,
useUpdateCustomRateCard,
useDeleteCustomRateCard,
useUpdateVendorRate,
useGetVendorByUserId
} from "@/dataconnect-generated/react";
import RateCardModal from "./components/RateCardModal";
// --- Constants & Helper Functions ---
function fmtCurrency(v: number | undefined | null) {
if (typeof v !== "number" || Number.isNaN(v)) return "—";
return v.toLocaleString(undefined, { style: "currency", currency: "USD" });
}
function downloadCSV(rows: any[], regionName: string, vendorName: string) {
const headers = [
"Role",
"Category",
"Employee Wage",
"Markup %",
"Vendor Fee %",
"Client Rate",
];
const lines = [headers.join(",")];
for (const r of rows) {
const cells = [
r.role_name,
r.category,
r.employee_wage,
r.markup_percentage,
r.vendor_fee_percentage,
r.client_rate,
];
lines.push(cells.join(","));
}
const blob = new Blob([lines.join("\n")], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${vendorName}_${regionName}_Rates_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
const parseRoleName = (roleName: string) => {
if (!roleName) return { position: "", region: "" };
if (roleName.includes(" - ")) {
const parts = roleName.split(" - ");
return {
position: parts[0].trim(),
region: parts[1].trim(),
};
}
return {
position: roleName,
region: "",
};
};
// --- Sub-Components ---
function VendorCompanyPricebookView({
vendorName,
}: {
vendorName: string;
}) {
const { toast } = useToast();
const queryClient = useQueryClient();
const { data: vendorRatesData } = useListVendorRates();
const vendorRates = vendorRatesData?.vendorRates || [];
const { data: customRateCardsData } = useListCustomRateCards();
const customRateCards = customRateCardsData?.customRateCards || [];
const { mutate: createCustomRateCard } = useCreateCustomRateCard();
const { mutate: updateCustomRateCard } = useUpdateCustomRateCard();
const { mutate: deleteCustomRateCard } = useDeleteCustomRateCard();
const { mutate: updateVendorRate } = useUpdateVendorRate();
const handleUpdateVendorRate = (vars: any) => {
updateVendorRate(vars, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listVendorRates"] });
}
});
};
const [pricebook, setPricebook] = useState("Standard");
const [search, setSearch] = useState("");
const [activeRegion, setActiveRegion] = useState("All");
const [activeCategory, setActiveCategory] = useState("All");
const [editing, setEditing] = useState<string | null>(null);
const [analyzingCompetitiveness, setAnalyzingCompetitiveness] =
useState(false);
const [competitivenessData, setCompetitivenessData] = useState<any[] | null>(
null,
);
const [showRateCardModal, setShowRateCardModal] = useState(false);
const [editingRateCard, setEditingRateCard] = useState<any | null>(null);
const [renamingCard, setRenamingCard] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState("");
const RATE_CARDS = customRateCards.map((c: any) => c.name);
const rates = useMemo(() => {
return vendorRates.filter((r) => r.vendor?.companyName === vendorName && r.isActive);
}, [vendorRates, vendorName]);
const CATEGORIES = useMemo(() => {
const categories = vendorRates.reduce<string[]>((acc, r) => {
if (r.category) {
acc.push(String(r.category));
}
return acc;
}, []);
return Array.from(new Set(categories));
}, [vendorRates]);
const handleSaveRateCard = (cardData: any) => {
if (editingRateCard) {
updateCustomRateCard({
id: editingRateCard.id,
name: cardData.name,
baseBook: cardData.baseBook,
discount: cardData.discount,
isDefault: editingRateCard.isDefault
}, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listCustomRateCards"] });
toast({ title: "Rate card updated successfully" });
}
});
} else {
createCustomRateCard({
name: cardData.name,
baseBook: cardData.baseBook,
discount: cardData.discount,
isDefault: false
}, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listCustomRateCards"] });
toast({ title: "Rate card saved successfully" });
}
});
}
setPricebook(cardData.name);
setEditingRateCard(null);
};
const handleDeleteRateCard = (cardId: string, cardName: string) => {
if (customRateCards.length <= 1) {
toast({
title: "Cannot delete the last rate card",
variant: "destructive",
});
return;
}
deleteCustomRateCard({ id: cardId }, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listCustomRateCards"] });
toast({ title: "Rate card deleted" });
if (pricebook === cardName) {
const nextCard = customRateCards.find((c: any) => c.id !== cardId);
setPricebook(nextCard ? nextCard.name : "Standard");
}
}
});
};
const handleRenameCard = (cardId: string, oldName: string) => {
if (!renameValue.trim() || renameValue === oldName) {
setRenamingCard(null);
return;
}
const currentCard = customRateCards.find((c: any) => c.id === cardId);
if (currentCard) {
updateCustomRateCard({
id: cardId,
name: renameValue.trim(),
baseBook: currentCard.baseBook,
discount: currentCard.discount,
isDefault: currentCard.isDefault
}, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listCustomRateCards"] });
if (pricebook === oldName) {
setPricebook(renameValue.trim());
}
}
});
}
setRenamingCard(null);
};
const scopedByBook = useMemo(() => {
const isCustomRateCard = customRateCards.find(c => c.name === pricebook);
const discount = isCustomRateCard?.discount || 0;
return rates.map((r: any) => {
const parsed = parseRoleName(r.roleName || "");
// Apply discount if it's a custom rate card
const proposedRate = r.clientRate * (1 - discount / 100);
const assignedRegion =
r.vendor?.region ||
parsed.region ||
(r.notes?.includes("Bay Area") ? "Bay Area" : "LA");
return {
...r,
client: pricebook,
region: assignedRegion,
approvedCap: r.clientRate,
proposedRate: proposedRate,
position: r.roleName,
markupPct: r.markupPercentage,
volDiscountPct: r.vendorFeePercentage,
overtime8Multiplier: r.overtime8Multiplier || 1.5,
overtime12Multiplier: r.overtime12Multiplier || 2.0,
holidayRate: r.holidayRate || proposedRate * 1.5
};
});
}, [rates, pricebook, customRateCards]);
const APPROVED_RATES = useMemo(() => {
return customRateCards.filter(c => c.isDefault).map(c => c.name);
}, [customRateCards]);
const isApprovedRate = APPROVED_RATES.includes(pricebook) || pricebook === "Standard";
const filtered = useMemo(() => {
return (scopedByBook as any[]).filter((r) => {
const regionMatch =
!isApprovedRate ||
activeRegion === "All" ||
r.region === activeRegion ||
parseRoleName(r.position).region === activeRegion;
const categoryMatch =
activeCategory === "All" || r.category === activeCategory;
const searchMatch =
search.trim() === "" ||
parseRoleName(r.position)
.position.toLowerCase()
.includes(search.toLowerCase());
return regionMatch && categoryMatch && searchMatch;
});
}, [scopedByBook, activeRegion, activeCategory, search, isApprovedRate]);
const kpis = useMemo(() => {
const rateValues = filtered.map((r) => r.proposedRate);
const avg = rateValues.length
? rateValues.reduce((a, b) => a + b, 0) / rateValues.length
: 0;
const min = rateValues.length ? Math.min(...rateValues) : 0;
const max = rateValues.length ? Math.max(...rateValues) : 0;
const total = filtered.length;
return { avg, min, max, total };
}, [filtered]);
async function analyzeCompetitiveness() {
setAnalyzingCompetitiveness(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1500));
const mockAnalysis = filtered.slice(0, 20).map((r) => ({
position: parseRoleName(r.position).position,
marketRate: r.proposedRate * (0.9 + Math.random() * 0.2),
score: Math.floor(60 + Math.random() * 40),
status: ["Highly Competitive", "Competitive", "Average", "Above Market"][
Math.floor(Math.random() * 4)
],
recommendation: "Consider slight adjustment.",
}));
setCompetitivenessData(mockAnalysis);
toast({ title: "Competitive analysis complete" });
} catch (error) {
console.error("Analysis error:", error);
toast({ title: "Analysis failed. Try again.", variant: "destructive" });
} finally {
setAnalyzingCompetitiveness(false);
}
}
return (
<DashboardLayout
title="Service Rate Management"
subtitle={`20252028 Pricing Structure for ${vendorName}`}
actions={
<>
<Button
onClick={analyzeCompetitiveness}
disabled={analyzingCompetitiveness}
className="bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
>
{analyzingCompetitiveness ? (
<>
<div className="w-4 h-4 mr-2 border-2 border-white border-t-transparent rounded-full animate-spin" />
Analyzing...
</>
) : (
<>
<Shield className="w-4 h-4 mr-2" />
AI Price Check
</>
)}
</Button>
<Button
onClick={() => downloadCSV(filtered, activeRegion, vendorName)}
variant="outline"
className="border-dashed border-border"
>
<Download className="w-4 h-4 mr-2" />
Export CSV
</Button>
</>
}
>
<div className="space-y-6">
{/* AI Analysis Result Block - Styled like Dispute Alert */}
{competitivenessData && competitivenessData.length > 0 && (
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 mb-6">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-emerald-100 rounded-full flex items-center justify-center flex-shrink-0 shadow-sm">
<Sparkles className="w-5 h-5 text-emerald-600" />
</div>
<div className="flex-1">
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-bold text-emerald-900">Market Intelligence Analysis Complete</h3>
<p className="text-sm text-emerald-700 mt-1">
Results based on current market data for {pricebook}.
</p>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => setCompetitivenessData(null)}
className="text-emerald-700 hover:bg-emerald-100 -mt-1 -mr-2"
>
Dismiss
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
{[{ "status": "Highly Competitive", "color": "bg-emerald-500", "textColor": "text-emerald-700" }, { "status": "Competitive", "color": "bg-blue-500", "textColor": "text-blue-700" }, { "status": "Average", "color": "bg-yellow-500", "textColor": "text-yellow-700" }, { "status": "Above Market", "color": "bg-red-500", "textColor": "text-red-700" }].map(({ status, color, textColor }) => {
const count = competitivenessData.filter((d) => d.status === status).length;
return (
<div key={status} className="bg-white/60 rounded-lg p-3 border border-emerald-200/50">
<div className="flex items-center gap-2 mb-1">
<div className={`w-2 h-2 rounded-full ${color}`} />
<span className={`text-xs font-bold uppercase ${textColor}`}>{status}</span>
</div>
<p className="text-2xl font-bold text-emerald-900">{count}</p>
</div>
);
})}
</div>
</div>
</div>
</div>
)}
{/* Rate Books Section Header */}
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center">
<FilesIcon className="w-6 h-6 text-primary" />
</div>
<div>
<h2 className="text-xl font-bold text-primary-text">Rate Books</h2>
<p className="text-secondary-text text-sm">Manage standard and custom client pricebooks</p>
</div>
</div>
{/* Book Selector Grid - Using bg-muted/20 like Client Selection */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Approved Enterprise Rates */}
<div className="bg-muted/10 p-4 rounded-xl border border-border/30">
<div className="flex items-center gap-3 mb-4 pb-3 border-b border-border/30">
<div className="w-8 h-8 bg-amber-500/10 rounded-lg flex items-center justify-center">
<Shield className="w-4 h-4 text-amber-600" />
</div>
<div>
<h3 className="text-sm font-bold text-primary-text uppercase tracking-wider">
Enterprise Books
</h3>
</div>
</div>
<div className="flex flex-wrap gap-2">
{APPROVED_RATES.map((tab) => (
<button
key={tab}
onClick={() => {
setPricebook(tab);
setActiveRegion("All");
}}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all ${pricebook === tab
? "bg-white shadow-sm text-amber-700 border-amber-200 border"
: "bg-muted/50 text-secondary-text hover:bg-muted"
}`}
>
{tab}
</button>
))}
</div>
</div>
{/* Custom Rate Cards */}
<div className="bg-muted/10 p-4 rounded-xl border border-border/30">
<div className="flex items-center justify-between mb-4 pb-3 border-b border-border/30">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500/10 rounded-lg flex items-center justify-center">
<Briefcase className="w-4 h-4 text-blue-600" />
</div>
<div>
<h3 className="text-sm font-bold text-primary-text uppercase tracking-wider">
Custom Cards
</h3>
</div>
</div>
<Button
onClick={() => {
setEditingRateCard(null);
setShowRateCardModal(true);
}}
size="sm"
variant="ghost"
className="h-7 text-xs hover:bg-primary/5 text-primary"
>
<Plus className="w-3.5 h-3.5 mr-1" /> New
</Button>
</div>
<div className="flex flex-wrap gap-2">
{RATE_CARDS.map((tab) => {
const cardData = customRateCards.find((c) => c.name === tab);
if (!cardData) {
return null;
}
const isRenaming = renamingCard === tab;
if (isRenaming) {
return (
<div key={tab} className="flex items-center gap-1">
<input
type="text"
value={renameValue}
onChange={(e) => 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"
/>
</div>
);
}
return (
<div key={tab} className="relative group">
<button
onClick={() => setPricebook(tab)}
onDoubleClick={(e) => {
e.stopPropagation();
setRenamingCard(tab);
setRenameValue(tab);
}}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center gap-2 ${pricebook === tab
? "bg-white shadow-sm text-blue-700 border-blue-200 border"
: "bg-muted/50 text-secondary-text hover:bg-muted"
}`}
title="Double-click to rename"
>
{tab}
{cardData?.discount && cardData.discount > 0 && (
<span className="text-xs opacity-80 px-1 py-0.5 bg-green-500/20 rounded">
-{cardData.discount}%
</span>
)}
</button>
{/* Hover actions simplified */}
<div className="absolute -top-1 -right-1 hidden group-hover:flex">
<button onClick={(e) => { e.stopPropagation(); handleDeleteRateCard(cardData.id, tab); }} className="p-1 bg-white rounded-full border border-red-200 text-red-500 hover:bg-red-50 shadow-sm"><Trash2 className="w-2 h-2" /></button>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* KPIs Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-border/50 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<DollarSign className="w-4 h-4 text-emerald-600" />
<h3 className="font-semibold text-primary-text text-sm">Average Rate</h3>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-primary-text">{fmtCurrency(kpis.avg)}</p>
<p className="text-xs text-secondary-text">Across {kpis.total} positions active in book</p>
</div>
</CardContent>
</Card>
<Card className="border-border/50 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<BarChart3 className="w-4 h-4 text-blue-600" />
<h3 className="font-semibold text-primary-text text-sm">Coverage</h3>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-primary-text">{kpis.total} <span className="text-sm font-normal text-secondary-text">roles</span></p>
<p className="text-xs text-secondary-text">Total defined positions</p>
</div>
</CardContent>
</Card>
<Card className="border-border/50 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<MapPin className="w-4 h-4 text-purple-600" />
<h3 className="font-semibold text-primary-text text-sm">Spread</h3>
</div>
<div className="space-y-1">
<p className="text-2xl font-bold text-primary-text">{fmtCurrency(kpis.min)} {fmtCurrency(kpis.max)}</p>
<p className="text-xs text-secondary-text">Rate range (Min - Max)</p>
</div>
</CardContent>
</Card>
</div>
{/* Filters - styled like the Search/Filter blocks */}
<div className="bg-muted/5 p-4 rounded-xl border border-border/50 flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="flex items-center gap-4 flex-1 w-full overflow-x-auto">
<div className="flex items-center gap-2 text-secondary-text min-w-fit">
<Filter className="w-4 h-4" />
<span className="text-sm font-semibold uppercase tracking-wider">Filters</span>
</div>
{/* Categories */}
<div className="flex gap-2">
{["All", ...CATEGORIES].map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all whitespace-nowrap ${activeCategory === cat
? "bg-primary text-primary-foreground shadow-sm"
: "bg-white border border-border/50 text-secondary-text hover:bg-muted"
}`}
>
{cat}
</button>
))}
</div>
</div>
<div className="relative w-full md:w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search positions..."
className="pl-9 h-9 bg-white border-border/50"
/>
</div>
</div>
{/* Main Rates Table */}
<div>
{/* Section Header */}
<div className="flex items-center justify-between mb-4 px-1">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white rounded-lg flex items-center justify-center border border-border/30 shadow-sm">
<span className="text-lg">💰</span>
</div>
<div>
<h3 className="font-bold text-primary-text">Rate Breakdown</h3>
<p className="text-xs text-muted-text">Detailed pricing for {pricebook}</p>
</div>
</div>
</div>
<Card className="border-border/50 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-muted/30 border-b border-border/50">
<tr className="text-muted-text text-[10px] font-bold uppercase tracking-wider">
<th className="px-6 py-4">Position</th>
<th className="px-6 py-4">Category</th>
<th className="px-6 py-4">Region</th>
<th className="px-6 py-4">Base Wage</th>
<th className="px-6 py-4">{pricebook} Rate</th>
<th className="px-6 py-4">OT 8h</th>
<th className="px-6 py-4">OT 12h</th>
<th className="px-6 py-4">Holiday</th>
<th className="px-6 py-4">Market Rate</th>
<th className="px-6 py-4">Comp. Score</th>
<th className="px-6 py-4 text-center">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border/20">
{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 (
<tr
key={r.id || idx}
className={`transition-all hover:bg-muted/10 bg-card`}
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
{/* Icon for position */}
<div className="w-8 h-8 rounded-lg bg-primary/5 flex items-center justify-center text-primary font-bold text-xs border border-primary/10">
{parsed.position.substring(0, 2).toUpperCase()}
</div>
<p className="font-bold text-primary-text text-sm">{parsed.position}</p>
</div>
</td>
<td className="px-6 py-4">
<Badge
variant="outline"
className="bg-muted/30 text-secondary-text border-border/50 text-[10px] font-bold px-2 py-0.5"
>
{r.category}
</Badge>
</td>
<td className="px-6 py-4">
<span className="text-xs text-secondary-text flex items-center gap-1 font-medium">
{r.region !== '—' && <MapPin className="w-3 h-3 text-muted-text" />}
{r.region}
</span>
</td>
<td className="px-6 py-4">
<span className="text-sm font-medium text-secondary-text">
{fmtCurrency(r.employee_wage)}
</span>
</td>
<td className="px-6 py-4">
{isEditing ? (
<input
type="number"
step="0.01"
defaultValue={r.proposedRate}
className="w-20 px-2 py-1 border border-primary rounded-md font-bold text-primary focus:outline-none text-xs"
onBlur={(e) => {
handleUpdateVendorRate({
id: r.id,
clientRate: parseFloat(e.target.value)
});
setEditing(null);
toast({ title: "Rate updated successfully" });
}}
autoFocus
/>
) : (
<span className="text-sm font-bold text-primary-text font-mono">
{fmtCurrency(r.proposedRate)}
</span>
)}
</td>
<td className="px-6 py-4">
{isEditing ? (
<input
type="number"
step="0.1"
defaultValue={r.overtime8Multiplier}
className="w-16 px-2 py-1 border border-primary rounded-md font-medium text-primary focus:outline-none text-xs"
onBlur={(e) => {
handleUpdateVendorRate({
id: r.id,
overtime8Multiplier: parseFloat(e.target.value)
});
}}
/>
) : (
<span className="text-xs font-medium text-secondary-text">{r.overtime8Multiplier}x</span>
)}
</td>
<td className="px-6 py-4">
{isEditing ? (
<input
type="number"
step="0.1"
defaultValue={r.overtime12Multiplier}
className="w-16 px-2 py-1 border border-primary rounded-md font-medium text-primary focus:outline-none text-xs"
onBlur={(e) => {
handleUpdateVendorRate({
id: r.id,
overtime12Multiplier: parseFloat(e.target.value)
});
}}
/>
) : (
<span className="text-xs font-medium text-secondary-text">{r.overtime12Multiplier}x</span>
)}
</td>
<td className="px-6 py-4">
{isEditing ? (
<input
type="number"
step="0.01"
defaultValue={r.holidayRate}
className="w-20 px-2 py-1 border border-primary rounded-md font-medium text-primary focus:outline-none text-xs"
onBlur={(e) => {
handleUpdateVendorRate({
id: r.id,
holidayRate: parseFloat(e.target.value)
});
}}
/>
) : (
<span className="text-xs font-medium text-secondary-text">{fmtCurrency(r.holidayRate)}</span>
)}
</td>
<td className="px-6 py-4">
{competitive ? (
<div className="flex flex-col">
<span className="text-xs font-medium text-primary-text">{fmtCurrency(competitive.marketRate)}</span>
<span className={`text-[10px] font-bold ${r.proposedRate < competitive.marketRate ? "text-emerald-600" : "text-red-600"}`}>
{r.proposedRate < competitive.marketRate ? "▼ Below" : "▲ Above"}
</span>
</div>
) : <span className="text-muted-text text-sm"></span>}
</td>
<td className="px-6 py-4">
{competitive ? (
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${competitive.status === "Highly Competitive" ? "bg-emerald-500" : "bg-blue-500"}`} />
<span className="text-xs font-medium text-primary-text">{competitive.score}/100</span>
</div>
) : <span className="text-muted-text text-sm"></span>}
</td>
<td className="px-6 py-4 text-center">
<Button
variant="ghost"
size="sm"
onClick={() => setEditing(isEditing ? null : r.id)}
className={`${isEditing
? "bg-emerald-50 text-emerald-600 hover:bg-emerald-100"
: "text-muted-text hover:text-primary hover:bg-primary/5"
} h-8 w-8 p-0`}
>
<Pencil className="w-3.5 h-3.5" />
</Button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</Card>
</div>
<RateCardModal
isOpen={showRateCardModal}
onClose={() => {
setShowRateCardModal(false);
setEditingRateCard(null);
}}
onSave={handleSaveRateCard}
editingCard={editingRateCard}
/>
</div>
</DashboardLayout>
);
}
// 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 <VendorCompanyPricebookView vendorName={vendorName} />;
}