token generation from profile added and cost item enhnced to support hsn

This commit is contained in:
laxmanhalaki 2026-02-16 20:01:02 +05:30
parent d2d75d93f7
commit 5e91b85854
14 changed files with 1003 additions and 438 deletions

View File

@ -18,6 +18,7 @@ import { useAuth, hasManagementAccess } from '@/contexts/AuthContext';
import { ApproverPerformance } from '@/pages/ApproverPerformance/ApproverPerformance'; import { ApproverPerformance } from '@/pages/ApproverPerformance/ApproverPerformance';
import { Profile } from '@/pages/Profile'; import { Profile } from '@/pages/Profile';
import { Settings } from '@/pages/Settings'; import { Settings } from '@/pages/Settings';
import { SecuritySettings } from '@/pages/Settings/SecuritySettings';
import { Notifications } from '@/pages/Notifications'; import { Notifications } from '@/pages/Notifications';
import { DetailedReports } from '@/pages/DetailedReports'; import { DetailedReports } from '@/pages/DetailedReports';
@ -609,6 +610,16 @@ function AppRoutes({ onLogout }: AppProps) {
} }
/> />
{/* Security Settings */}
<Route
path="/settings/security"
element={
<PageLayout currentPage="settings" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
<SecuritySettings />
</PageLayout>
}
/>
{/* Notifications */} {/* Notifications */}
<Route <Route
path="/notifications" path="/notifications"

View File

@ -0,0 +1,297 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Key, Plus, Trash2, Copy, Check } from 'lucide-react';
import { format } from 'date-fns';
import axios from '@/services/authApi';
import { toast } from 'sonner';
interface ApiToken {
id: string;
name: string;
prefix: string;
lastUsedAt?: string;
expiresAt?: string;
createdAt: string;
isActive: boolean;
}
export function ApiTokenManager() {
const [tokens, setTokens] = useState<ApiToken[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newTokenName, setNewTokenName] = useState('');
const [newTokenExpiry, setNewTokenExpiry] = useState<number | ''>('');
const [generatedToken, setGeneratedToken] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [copied, setCopied] = useState(false);
const [tokenToRevoke, setTokenToRevoke] = useState<ApiToken | null>(null);
useEffect(() => {
fetchTokens();
}, []);
const fetchTokens = async () => {
try {
setIsLoading(true);
const response = await axios.get('/api-tokens');
setTokens(response.data.data.tokens);
} catch (error) {
console.error('Failed to fetch API tokens:', error);
toast.error('Failed to load API tokens');
} finally {
setIsLoading(false);
}
};
const handleCreateToken = async () => {
if (!newTokenName.trim()) return;
try {
setIsCreating(true);
const payload: any = { name: newTokenName };
if (newTokenExpiry) {
payload.expiresInDays = Number(newTokenExpiry);
}
const response = await axios.post('/api-tokens', payload);
setGeneratedToken(response.data.data.token);
toast.success('API Token created successfully');
fetchTokens(); // Refresh list
} catch (error) {
console.error('Failed to create token:', error);
toast.error('Failed to create API token');
} finally {
setIsCreating(false);
}
};
const handleRevokeToken = (token: ApiToken) => {
setTokenToRevoke(token);
};
const confirmRevokeToken = async () => {
if (!tokenToRevoke) return;
try {
await axios.delete(`/api-tokens/${tokenToRevoke.id}`);
toast.success('Token revoked successfully');
setTokens(tokens.filter(t => t.id !== tokenToRevoke.id));
setTokenToRevoke(null);
} catch (error) {
console.error('Failed to revoke token:', error);
toast.error('Failed to revoke token');
}
};
const copyToClipboard = () => {
if (generatedToken) {
navigator.clipboard.writeText(generatedToken);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
toast.success('Token copied to clipboard');
}
};
const resetCreateModal = () => {
setShowCreateModal(false);
setNewTokenName('');
setNewTokenExpiry('');
setGeneratedToken(null);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900">API Tokens</h3>
<p className="text-sm text-gray-500">Manage personal access tokens for external integrations</p>
</div>
<Button onClick={() => setShowCreateModal(true)} size="sm" className="bg-re-green hover:bg-re-green/90 text-white">
<Plus className="w-4 h-4 mr-2" />
Generate
</Button>
</div>
{isLoading ? (
<div className="text-center py-4 text-gray-500">Loading tokens...</div>
) : tokens.length === 0 ? (
<div className="text-center py-8 bg-gray-50 rounded-lg border border-dashed border-gray-300">
<Key className="w-10 h-10 text-gray-300 mx-auto mb-2" />
<p className="text-gray-500 font-medium">No API tokens found</p>
<p className="text-gray-400 text-sm mt-1">Generate a token to access the API programmatically</p>
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Prefix</TableHead>
<TableHead>Last Used</TableHead>
<TableHead>Expires</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tokens.map((token) => (
<TableRow key={token.id}>
<TableCell className="font-medium">{token.name}</TableCell>
<TableCell className="font-mono text-xs bg-slate-100 rounded px-2 py-1 w-fit">{token.prefix}...</TableCell>
<TableCell className="text-gray-500 text-sm">
{token.lastUsedAt ? format(new Date(token.lastUsedAt), 'MMM d, yyyy') : 'Never'}
</TableCell>
<TableCell className="text-gray-500 text-sm">
{token.expiresAt ? format(new Date(token.expiresAt), 'MMM d, yyyy') : 'No Expiry'}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={() => handleRevokeToken(token)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
<span className="sr-only">Revoke</span>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* Create Token Modal */}
<Dialog open={showCreateModal} onOpenChange={(open) => !open && resetCreateModal()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Generate API Token</DialogTitle>
<DialogDescription>
Create a new token to access the API. Treat this token like a password.
</DialogDescription>
</DialogHeader>
{!generatedToken ? (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="token-name">Token Name</Label>
<Input
id="token-name"
placeholder="e.g., CI/CD Pipeline, Prometheus"
value={newTokenName}
onChange={(e) => setNewTokenName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="token-expiry">Expiration (Days)</Label>
<Input
id="token-expiry"
type="number"
min="1"
placeholder="Leave empty for no expiry"
value={newTokenExpiry}
onChange={(e) => {
const val = e.target.value;
if (val === '') {
setNewTokenExpiry('');
} else {
const num = parseInt(val);
// Prevent negative numbers
if (!isNaN(num) && num >= 1) {
setNewTokenExpiry(num);
}
}
}}
/>
</div>
</div>
) : (
<div className="space-y-4 py-4">
<Alert className="bg-green-50 border-green-200">
<Check className="h-4 w-4 text-green-600" />
<AlertTitle className="text-green-800">Token Generated Successfully</AlertTitle>
<AlertDescription className="text-green-700">
Please copy your token now. You won't be able to see it again!
</AlertDescription>
</Alert>
<div className="relative">
<div className="p-4 bg-slate-900 rounded-md font-mono text-sm text-green-400 break-all pr-10">
{generatedToken}
</div>
<Button
size="icon"
variant="ghost"
className="absolute top-1 right-1 text-gray-400 hover:text-white hover:bg-slate-800"
onClick={copyToClipboard}
>
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
</div>
)}
<DialogFooter>
{!generatedToken ? (
<>
<Button variant="outline" onClick={resetCreateModal}>Cancel</Button>
<Button onClick={handleCreateToken} disabled={!newTokenName.trim() || isCreating}>
{isCreating ? 'Generating...' : 'Generate Token'}
</Button>
</>
) : (
<Button onClick={resetCreateModal}>Done</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={!!tokenToRevoke} onOpenChange={(open) => !open && setTokenToRevoke(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Revoke API Token</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to revoke the token <strong>{tokenToRevoke?.name}</strong>?
This action cannot be undone and any applications using this token will lose access immediately.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmRevokeToken} className="bg-red-600 hover:bg-red-700 text-white">
Revoke Token
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@ -904,7 +904,9 @@ export function DealerClaimWorkflowTab({
utgstAmt: item.utgstAmt, utgstAmt: item.utgstAmt,
cessRate: item.cessRate, cessRate: item.cessRate,
cessAmt: item.cessAmt, cessAmt: item.cessAmt,
totalAmt: item.totalAmt totalAmt: item.totalAmt,
quantity: item.quantity,
hsnCode: item.hsnCode
})), })),
totalEstimatedBudget: totalBudget, totalEstimatedBudget: totalBudget,
expectedCompletionDate: data.expectedCompletionDate, expectedCompletionDate: data.expectedCompletionDate,
@ -1175,7 +1177,9 @@ export function DealerClaimWorkflowTab({
utgstAmt: item.utgstAmt, utgstAmt: item.utgstAmt,
cessRate: item.cessRate, cessRate: item.cessRate,
cessAmt: item.cessAmt, cessAmt: item.cessAmt,
totalAmt: item.totalAmt totalAmt: item.totalAmt,
quantity: item.quantity,
hsnCode: item.hsnCode
})); }));
// Submit completion documents using dealer claim API // Submit completion documents using dealer claim API

View File

@ -123,7 +123,11 @@ export function ActivityInformationCard({
</label> </label>
<p className="text-sm text-gray-900 font-medium mt-1 flex items-center gap-2"> <p className="text-sm text-gray-900 font-medium mt-1 flex items-center gap-2">
<Receipt className="w-4 h-4 text-blue-600" /> <Receipt className="w-4 h-4 text-blue-600" />
{formatCurrency(activityInfo.closedExpenses)} {formatCurrency(
activityInfo.closedExpensesBreakdown && activityInfo.closedExpensesBreakdown.length > 0
? activityInfo.closedExpensesBreakdown.reduce((sum, item: any) => sum + (item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0))), 0)
: activityInfo.closedExpenses
)}
</p> </p>
</div> </div>
)} )}
@ -152,6 +156,7 @@ export function ActivityInformationCard({
<thead className="bg-blue-100/50"> <thead className="bg-blue-100/50">
<tr> <tr>
<th className="px-3 py-2 text-left font-semibold text-blue-900">Description</th> <th className="px-3 py-2 text-left font-semibold text-blue-900">Description</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-16">Qty</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-24">Base</th> <th className="px-3 py-2 text-right font-semibold text-blue-900 w-24">Base</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-24">GST</th> <th className="px-3 py-2 text-right font-semibold text-blue-900 w-24">GST</th>
<th className="px-3 py-2 text-right font-semibold text-blue-900 w-28">Total</th> <th className="px-3 py-2 text-right font-semibold text-blue-900 w-28">Total</th>
@ -164,18 +169,19 @@ export function ActivityInformationCard({
{item.description} {item.description}
{item.gstRate ? <span className="text-[10px] text-gray-400 block">{item.gstRate}% GST</span> : null} {item.gstRate ? <span className="text-[10px] text-gray-400 block">{item.gstRate}% GST</span> : null}
</td> </td>
<td className="px-3 py-2 text-right text-gray-900">{item.quantity || 1}</td>
<td className="px-3 py-2 text-right text-gray-900">{formatCurrency(item.amount)}</td> <td className="px-3 py-2 text-right text-gray-900">{formatCurrency(item.amount)}</td>
<td className="px-3 py-2 text-right text-gray-900">{formatCurrency(item.gstAmt || 0)}</td> <td className="px-3 py-2 text-right text-gray-900">{formatCurrency(item.gstAmt || 0)}</td>
<td className="px-3 py-2 text-right font-medium text-gray-900"> <td className="px-3 py-2 text-right font-medium text-gray-900">
{formatCurrency(item.totalAmt || (item.amount + (item.gstAmt || 0)))} {formatCurrency(item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0)))}
</td> </td>
</tr> </tr>
))} ))}
<tr className="bg-blue-100/50 font-bold"> <tr className="bg-blue-100/50 font-bold">
<td colSpan={3} className="px-3 py-2 text-blue-900">Final Claim Amount</td> <td colSpan={4} className="px-3 py-2 text-blue-900">Final Claim Amount</td>
<td className="px-3 py-2 text-right text-blue-700"> <td className="px-3 py-2 text-right text-blue-700">
{formatCurrency( {formatCurrency(
activityInfo.closedExpensesBreakdown.reduce((sum: number, item: any) => sum + (item.totalAmt || (item.amount + (item.gstAmt || 0))), 0) activityInfo.closedExpensesBreakdown.reduce((sum: number, item: any) => sum + (item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0))), 0)
)} )}
</td> </td>
</tr> </tr>

View File

@ -16,6 +16,7 @@ interface ProposalCostItem {
cgstAmt?: number; cgstAmt?: number;
sgstAmt?: number; sgstAmt?: number;
igstAmt?: number; igstAmt?: number;
quantity?: number;
totalAmt?: number; totalAmt?: number;
} }
@ -45,7 +46,11 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) { if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) {
const total = proposalDetails.costBreakup.reduce((sum, item) => { const total = proposalDetails.costBreakup.reduce((sum, item) => {
const amount = item.amount || 0; const amount = item.amount || 0;
return sum + (Number.isNaN(amount) ? 0 : amount); const quantity = item.quantity || 1;
const baseTotal = amount * quantity;
const gst = item.gstAmt || 0;
const lineTotal = item.totalAmt || (baseTotal + gst);
return sum + (Number.isNaN(lineTotal) ? 0 : lineTotal);
}, 0); }, 0);
return total; return total;
} }
@ -106,6 +111,9 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wide"> <th className="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wide">
Item Description Item Description
</th> </th>
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide">
Qty
</th>
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide"> <th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide">
Base Amount Base Amount
</th> </th>
@ -128,6 +136,9 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
</div> </div>
) : null} ) : null}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900 text-right">
{item.quantity || 1}
</td>
<td className="px-4 py-3 text-sm text-gray-900 text-right"> <td className="px-4 py-3 text-sm text-gray-900 text-right">
{formatCurrency(item.amount)} {formatCurrency(item.amount)}
</td> </td>
@ -135,12 +146,12 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta
{formatCurrency(item.gstAmt)} {formatCurrency(item.gstAmt)}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-900 text-right font-medium"> <td className="px-4 py-3 text-sm text-gray-900 text-right font-medium">
{formatCurrency(item.totalAmt || (item.amount || 0) + (item.gstAmt || 0))} {formatCurrency(item.totalAmt || ((item.amount || 0) * (item.quantity || 1)) + (item.gstAmt || 0))}
</td> </td>
</tr> </tr>
))} ))}
<tr className="bg-green-50 font-semibold"> <tr className="bg-green-50 font-semibold">
<td colSpan={3} className="px-4 py-3 text-sm text-gray-900"> <td colSpan={4} className="px-4 py-3 text-sm text-gray-900">
Estimated Budget (Total Inclusive of GST) Estimated Budget (Total Inclusive of GST)
</td> </td>
<td className="px-4 py-3 text-sm text-green-700 text-right"> <td className="px-4 py-3 text-sm text-green-700 text-right">

View File

@ -118,7 +118,9 @@ export function DMSPushModal({
if (completionDetails?.closedExpenses && Array.isArray(completionDetails.closedExpenses)) { if (completionDetails?.closedExpenses && Array.isArray(completionDetails.closedExpenses)) {
return completionDetails.closedExpenses.reduce((sum, item) => { return completionDetails.closedExpenses.reduce((sum, item) => {
const amount = typeof item === 'object' ? (item.amount || 0) : 0; const amount = typeof item === 'object' ? (item.amount || 0) : 0;
return sum + (Number(amount) || 0); const gst = typeof item === 'object' ? ((item as any).gstAmt || 0) : 0;
const total = (item as any).totalAmt || (amount + gst);
return sum + (Number(total) || 0);
}, 0); }, 0);
} }
return 0; return 0;
@ -387,38 +389,73 @@ export function DMSPushModal({
{/* Expense Breakdown Card */} {/* Expense Breakdown Card */}
{completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && ( {completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && (
<Card> <Card className="md:col-span-2 lg:col-span-3">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg"> <CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<DollarSign className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" /> <DollarSign className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
Expense Breakdown Expense Breakdown
</CardTitle> </CardTitle>
<CardDescription className="text-xs sm:text-sm"> <CardDescription className="text-xs sm:text-sm">
Review closed expenses before generation Review closed expenses breakdown (Base + GST)
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-1.5 sm:space-y-2 max-h-[200px] overflow-y-auto"> {/* Table Header */}
{completionDetails.closedExpenses.map((expense, index) => ( <div className="grid grid-cols-12 gap-2 mb-2 px-3 text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:grid">
<div className="col-span-4">Description</div>
<div className="col-span-2 text-right">Base</div>
<div className="col-span-2 text-right">GST Rate</div>
<div className="col-span-2 text-right">GST Amt</div>
<div className="col-span-2 text-right">Total</div>
</div>
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{completionDetails.closedExpenses.map((expense, index) => {
const amount = typeof expense === 'object' ? (expense.amount || 0) : 0; // Base Amount
const gstRate = typeof expense === 'object' ? ((expense as any).gstRate || 0) : 0;
const gstAmt = typeof expense === 'object' ? ((expense as any).gstAmt || 0) : 0; // GST Amount
const total = typeof expense === 'object' ? ((expense as any).totalAmt || (amount + gstAmt)) : 0; // Total Amount
return (
<div <div
key={index} key={index}
className="flex items-center justify-between py-1.5 sm:py-2 px-2 sm:px-3 bg-gray-50 rounded border" className="grid grid-cols-1 sm:grid-cols-12 gap-2 items-center py-2 px-3 bg-gray-50 rounded border text-xs sm:text-sm"
> >
<div className="flex-1 min-w-0 pr-2"> {/* Mobile View: Stacked */}
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate"> <div className="sm:hidden flex justify-between w-full mb-1">
<span className="font-semibold text-gray-900">{expense.description || `Expense ${index + 1}`}</span>
<span className="font-bold text-gray-900">{formatCurrency(total)}</span>
</div>
<div className="sm:hidden flex justify-between w-full text-xs text-gray-500">
<span>Base: {formatCurrency(amount)}</span>
<span>GST: {gstRate}% ({formatCurrency(gstAmt)})</span>
</div>
{/* Desktop View: Grid */}
<div className="hidden sm:block col-span-4 min-w-0">
<p className="font-medium text-gray-900 truncate" title={expense.description}>
{expense.description || `Expense ${index + 1}`} {expense.description || `Expense ${index + 1}`}
</p> </p>
</div> </div>
<div className="ml-2 flex-shrink-0"> <div className="hidden sm:block col-span-2 text-right text-gray-600">
<p className="text-xs sm:text-sm font-semibold text-gray-900"> {formatCurrency(amount)}
{formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)} </div>
</p> <div className="hidden sm:block col-span-2 text-right text-gray-600">
{gstRate}%
</div>
<div className="hidden sm:block col-span-2 text-right text-gray-600">
{formatCurrency(gstAmt)}
</div>
<div className="hidden sm:block col-span-2 text-right font-semibold text-gray-900">
{formatCurrency(total)}
</div> </div>
</div> </div>
))} );
})}
</div> </div>
<div className="flex items-center justify-between py-2 sm:py-3 px-2 sm:px-3 bg-blue-50 rounded border-2 border-blue-200 mt-2 sm:mt-3">
<span className="text-xs sm:text-sm font-semibold text-gray-900">Total:</span> <div className="flex items-center justify-between py-2 sm:py-3 px-3 sm:px-4 bg-blue-50 rounded border-2 border-blue-200 mt-3 sm:mt-4">
<span className="text-xs sm:text-sm font-semibold text-gray-900">Total Closed Expenses:</span>
<span className="text-sm sm:text-base font-bold text-blue-700"> <span className="text-sm sm:text-base font-bold text-blue-700">
{formatCurrency(totalClosedExpenses)} {formatCurrency(totalClosedExpenses)}
</span> </span>

View File

@ -25,7 +25,7 @@
@media (min-width: 1024px) { @media (min-width: 1024px) {
.dealer-completion-documents-modal { .dealer-completion-documents-modal {
width: 90vw !important; width: 90vw !important;
max-width: 1000px !important; max-width: 1200px !important;
} }
} }
@ -33,7 +33,7 @@
@media (min-width: 1536px) { @media (min-width: 1536px) {
.dealer-completion-documents-modal { .dealer-completion-documents-modal {
width: 90vw !important; width: 90vw !important;
max-width: 1000px !important; max-width: 1200px !important;
} }
} }
@ -65,4 +65,3 @@
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
} }

View File

@ -30,6 +30,8 @@ interface ExpenseItem {
amount: number; amount: number;
gstRate: number; gstRate: number;
gstAmt: number; gstAmt: number;
quantity: number;
hsnCode: string;
cgstRate: number; cgstRate: number;
cgstAmt: number; cgstAmt: number;
sgstRate: number; sgstRate: number;
@ -148,9 +150,10 @@ export function DealerCompletionDocumentsModal({
}, [expenseItems]); }, [expenseItems]);
// GST Calculation Helper // GST Calculation Helper
const calculateGST = (amount: number, rate: number, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => { const calculateGST = (amount: number, rate: number, quantity: number = 1, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => {
const gstAmt = (amount * rate) / 100; const baseTotal = amount * quantity;
const totalAmt = amount + gstAmt; const gstAmt = (baseTotal * rate) / 100;
const totalAmt = baseTotal + gstAmt;
if (type === 'IGST') { if (type === 'IGST') {
return { return {
@ -201,6 +204,8 @@ export function DealerCompletionDocumentsModal({
amount: 0, amount: 0,
gstRate: 0, gstRate: 0,
gstAmt: 0, gstAmt: 0,
quantity: 1,
hsnCode: '',
cgstRate: 0, cgstRate: 0,
cgstAmt: 0, cgstAmt: 0,
sgstRate: 0, sgstRate: 0,
@ -222,16 +227,18 @@ export function DealerCompletionDocumentsModal({
if (item.id === id) { if (item.id === id) {
const updatedItem = { ...item, [field]: value }; const updatedItem = { ...item, [field]: value };
// Re-calculate GST if amount or rate changes // Re-calculate GST if amount, rate or quantity changes
if (field === 'amount' || field === 'gstRate') { if (field === 'amount' || field === 'gstRate' || field === 'quantity') {
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount; const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate; const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate;
const gst = calculateGST(amount, rate); const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
const gst = calculateGST(amount, rate, quantity);
return { return {
...updatedItem, ...updatedItem,
amount, amount,
gstRate: rate, gstRate: rate,
quantity,
...gst ...gst
}; };
} }
@ -458,6 +465,8 @@ export function DealerCompletionDocumentsModal({
amount: 0, amount: 0,
gstRate: 0, gstRate: 0,
gstAmt: 0, gstAmt: 0,
quantity: 1,
hsnCode: '',
cgstRate: 0, cgstRate: 0,
cgstAmt: 0, cgstAmt: 0,
sgstRate: 0, sgstRate: 0,
@ -580,6 +589,29 @@ export function DealerCompletionDocumentsModal({
/> />
</div> </div>
</div> </div>
<div className="w-20 sm:w-24 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN Code</Label>
<Input
placeholder="HSN"
value={item.hsnCode || ''}
onChange={(e) =>
handleExpenseChange(item.id, 'hsnCode', e.target.value)
}
className="w-full bg-white text-sm"
/>
</div>
<div className="w-16 sm:w-20 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Qty</Label>
<Input
type="number"
min="1"
value={item.quantity || 1}
onChange={(e) =>
handleExpenseChange(item.id, 'quantity', parseInt(e.target.value) || 1)
}
className="w-full bg-white text-sm"
/>
</div>
<div className="w-20 sm:w-24 flex-shrink-0"> <div className="w-20 sm:w-24 flex-shrink-0">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label> <Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
<Input <Input

View File

@ -25,7 +25,7 @@
@media (min-width: 1024px) { @media (min-width: 1024px) {
.dealer-proposal-modal { .dealer-proposal-modal {
width: 90vw !important; width: 90vw !important;
max-width: 1000px !important; max-width: 1200px !important;
} }
} }
@ -33,7 +33,7 @@
@media (min-width: 1536px) { @media (min-width: 1536px) {
.dealer-proposal-modal { .dealer-proposal-modal {
width: 90vw !important; width: 90vw !important;
max-width: 1000px !important; max-width: 1200px !important;
} }
} }
@ -65,4 +65,3 @@
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
} }

View File

@ -33,6 +33,8 @@ interface CostItem {
amount: number; amount: number;
gstRate: number; gstRate: number;
gstAmt: number; gstAmt: number;
quantity: number;
hsnCode: string;
cgstRate: number; cgstRate: number;
cgstAmt: number; cgstAmt: number;
sgstRate: number; sgstRate: number;
@ -84,6 +86,8 @@ export function DealerProposalSubmissionModal({
amount: 0, amount: 0,
gstRate: 0, gstRate: 0,
gstAmt: 0, gstAmt: 0,
quantity: 1,
hsnCode: '',
cgstRate: 0, cgstRate: 0,
cgstAmt: 0, cgstAmt: 0,
sgstRate: 0, sgstRate: 0,
@ -99,9 +103,10 @@ export function DealerProposalSubmissionModal({
]); ]);
// GST Calculation Helper // GST Calculation Helper
const calculateGST = (amount: number, rate: number, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => { const calculateGST = (amount: number, rate: number, quantity: number = 1, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => {
const gstAmt = (amount * rate) / 100; const baseTotal = amount * quantity;
const totalAmt = amount + gstAmt; const gstAmt = (baseTotal * rate) / 100;
const totalAmt = baseTotal + gstAmt;
if (type === 'IGST') { if (type === 'IGST') {
return { return {
@ -313,6 +318,8 @@ export function DealerProposalSubmissionModal({
amount: 0, amount: 0,
gstRate: 0, gstRate: 0,
gstAmt: 0, gstAmt: 0,
quantity: 1,
hsnCode: '',
cgstRate: 0, cgstRate: 0,
cgstAmt: 0, cgstAmt: 0,
sgstRate: 0, sgstRate: 0,
@ -340,16 +347,18 @@ export function DealerProposalSubmissionModal({
if (item.id === id) { if (item.id === id) {
const updatedItem = { ...item, [field]: value }; const updatedItem = { ...item, [field]: value };
// Re-calculate GST if amount or rate changes // Re-calculate GST if amount, rate or quantity changes
if (field === 'amount' || field === 'gstRate') { if (field === 'amount' || field === 'gstRate' || field === 'quantity') {
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount; const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate; const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate;
const gst = calculateGST(amount, rate); const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
const gst = calculateGST(amount, rate, quantity);
return { return {
...updatedItem, ...updatedItem,
amount, amount,
gstRate: rate, gstRate: rate,
quantity,
...gst ...gst
}; };
} }
@ -413,6 +422,8 @@ export function DealerProposalSubmissionModal({
amount: 0, amount: 0,
gstRate: 0, gstRate: 0,
gstAmt: 0, gstAmt: 0,
quantity: 1,
hsnCode: '',
cgstRate: 0, cgstRate: 0,
cgstAmt: 0, cgstAmt: 0,
sgstRate: 0, sgstRate: 0,
@ -581,6 +592,7 @@ export function DealerProposalSubmissionModal({
<thead className="bg-gray-50 text-gray-600"> <thead className="bg-gray-50 text-gray-600">
<tr> <tr>
<th className="p-2 font-medium">Description</th> <th className="p-2 font-medium">Description</th>
<th className="p-2 font-medium text-right">Qty</th>
<th className="p-2 font-medium text-right">Amount</th> <th className="p-2 font-medium text-right">Amount</th>
</tr> </tr>
</thead> </thead>
@ -588,13 +600,16 @@ export function DealerProposalSubmissionModal({
{(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => ( {(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => (
<tr key={idx} className="bg-white"> <tr key={idx} className="bg-white">
<td className="p-2 text-gray-800">{item.description}</td> <td className="p-2 text-gray-800">{item.description}</td>
<td className="p-2 text-right text-gray-800 font-medium">
{item.quantity || 1}
</td>
<td className="p-2 text-right text-gray-800 font-medium"> <td className="p-2 text-right text-gray-800 font-medium">
{Number(item.amount).toLocaleString('en-IN')} {Number(item.amount).toLocaleString('en-IN')}
</td> </td>
</tr> </tr>
))} ))}
<tr className="bg-gray-50 font-bold"> <tr className="bg-gray-50 font-bold">
<td className="p-2 text-gray-900">Total</td> <td colSpan={2} className="p-2 text-gray-900">Total</td>
<td className="p-2 text-right text-gray-900"> <td className="p-2 text-right text-gray-900">
{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')} {Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')}
</td> </td>
@ -841,8 +856,10 @@ export function DealerProposalSubmissionModal({
<div className="space-y-3 lg:space-y-4 max-h-[300px] lg:max-h-[280px] overflow-y-auto pr-1"> <div className="space-y-3 lg:space-y-4 max-h-[300px] lg:max-h-[280px] overflow-y-auto pr-1">
{costItems.map((item) => ( {costItems.map((item) => (
<div key={item.id} className="p-3 border rounded-lg bg-gray-50/50 space-y-3 relative group"> <div key={item.id} className="p-3 border rounded-lg bg-gray-50/50 space-y-3 relative group">
<div className="flex flex-col gap-3 w-full relative">
{/* Row 1: Description and Close Button */}
<div className="flex gap-2 items-start w-full"> <div className="flex gap-2 items-start w-full">
<div className="flex-1 min-w-0"> <div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Description</Label> <Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Description</Label>
<Input <Input
placeholder="Item description" placeholder="Item description"
@ -853,7 +870,21 @@ export function DealerProposalSubmissionModal({
className="w-full bg-white" className="w-full bg-white"
/> />
</div> </div>
<div className="w-24 lg:w-28 flex-shrink-0"> <Button
type="button"
variant="ghost"
size="sm"
className="mt-6 hover:bg-red-100 hover:text-red-700 flex-shrink-0 h-9 w-9 p-0"
onClick={() => handleRemoveCostItem(item.id)}
disabled={costItems.length === 1}
>
<X className="w-4 h-4" />
</Button>
</div>
{/* Row 2: Numeric Fields */}
<div className="flex gap-2 w-full">
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label> <Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label>
<Input <Input
type="number" type="number"
@ -867,7 +898,30 @@ export function DealerProposalSubmissionModal({
className="w-full bg-white" className="w-full bg-white"
/> />
</div> </div>
<div className="w-20 lg:w-24 flex-shrink-0"> <div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">HSN Code</Label>
<Input
placeholder="HSN"
value={item.hsnCode || ''}
onChange={(e) =>
handleCostItemChange(item.id, 'hsnCode', e.target.value)
}
className="w-full bg-white"
/>
</div>
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Qty</Label>
<Input
type="number"
min="1"
value={item.quantity || 1}
onChange={(e) =>
handleCostItemChange(item.id, 'quantity', parseInt(e.target.value) || 1)
}
className="w-full bg-white"
/>
</div>
<div className="flex-1">
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label> <Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
<Input <Input
type="number" type="number"
@ -882,16 +936,7 @@ export function DealerProposalSubmissionModal({
className="w-full bg-white" className="w-full bg-white"
/> />
</div> </div>
<Button </div>
type="button"
variant="ghost"
size="sm"
className="mt-5 hover:bg-red-100 hover:text-red-700 flex-shrink-0 h-9 w-9 p-0"
onClick={() => handleRemoveCostItem(item.id)}
disabled={costItems.length === 1}
>
<X className="w-4 h-4" />
</Button>
</div> </div>
{item.gstAmt > 0 && ( {item.gstAmt > 0 && (

View File

@ -39,6 +39,7 @@ interface CostItem {
id: string; id: string;
description: string; description: string;
amount: number; amount: number;
quantity?: number;
} }
interface ProposalData { interface ProposalData {
@ -117,7 +118,11 @@ export function InitiatorProposalApprovalModal({
return costBreakup.reduce((sum: number, item: any) => { return costBreakup.reduce((sum: number, item: any) => {
const amount = typeof item === 'object' ? (item.amount || 0) : 0; const amount = typeof item === 'object' ? (item.amount || 0) : 0;
return sum + (Number(amount) || 0); const quantity = typeof item === 'object' ? (item.quantity || 1) : 1;
const baseTotal = amount * quantity;
const gst = typeof item === 'object' ? (item.gstAmt || 0) : 0;
const total = item.totalAmt || (baseTotal + gst);
return sum + (Number(total) || 0);
}, 0); }, 0);
}, [proposalData]); }, [proposalData]);
@ -603,18 +608,33 @@ export function InitiatorProposalApprovalModal({
<> <>
<div className="border rounded-lg overflow-hidden max-h-[200px] lg:max-h-[180px] overflow-y-auto"> <div className="border rounded-lg overflow-hidden max-h-[200px] lg:max-h-[180px] overflow-y-auto">
<div className="bg-gray-50 px-3 lg:px-4 py-2 border-b sticky top-0"> <div className="bg-gray-50 px-3 lg:px-4 py-2 border-b sticky top-0">
<div className="grid grid-cols-2 gap-4 text-xs lg:text-sm font-semibold text-gray-700"> <div className="grid grid-cols-5 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
<div>Item Description</div> <div className="col-span-1">Item Description</div>
<div className="text-right">Amount</div> <div className="text-right">Qty</div>
<div className="text-right">Base</div>
<div className="text-right">GST</div>
<div className="text-right">Total</div>
</div> </div>
</div> </div>
<div className="divide-y"> <div className="divide-y">
{costBreakup.map((item: any, index: number) => ( {costBreakup.map((item: any, index: number) => (
<div key={item?.id || item?.description || index} className="px-3 lg:px-4 py-2 lg:py-3 grid grid-cols-2 gap-4"> <div key={item?.id || item?.description || index} className="px-3 lg:px-4 py-2 lg:py-3 grid grid-cols-5 gap-4">
<div className="text-xs lg:text-sm text-gray-700">{item?.description || 'N/A'}</div> <div className="col-span-1 text-xs lg:text-sm text-gray-700">
<div className="text-xs lg:text-sm font-semibold text-gray-900 text-right"> {item?.description || 'N/A'}
{item?.gstRate ? <span className="block text-[10px] text-gray-400">{item.gstRate}% GST</span> : null}
</div>
<div className="text-xs lg:text-sm text-gray-900 text-right">
{item?.quantity || 1}
</div>
<div className="text-xs lg:text-sm text-gray-900 text-right">
{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div> </div>
<div className="text-xs lg:text-sm text-gray-900 text-right">
{(Number(item?.gstAmt) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<div className="text-xs lg:text-sm font-semibold text-gray-900 text-right">
{(Number(item?.totalAmt || ((item?.amount || 0) * (item?.quantity || 1) + (item?.gstAmt || 0))) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -0,0 +1,67 @@
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ArrowLeft, Lock, Key } from 'lucide-react';
import { ApiTokenManager } from '@/components/settings/ApiTokenManager';
export function SecuritySettings() {
const navigate = useNavigate();
return (
<div className="max-w-7xl mx-auto space-y-6 pb-8">
{/* Header */}
<div className="flex items-center gap-4 mb-6">
<Button variant="ghost" size="icon" onClick={() => navigate('/settings')}>
<ArrowLeft className="w-5 h-5" />
</Button>
<div>
<h1 className="text-2xl font-bold text-gray-900">Security Settings</h1>
<p className="text-gray-500">Manage your account security and access tokens</p>
</div>
</div>
<div className="grid grid-cols-1 gap-6">
{/* Password Section */}
<Card className="shadow-md">
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-md">
<Lock className="w-5 h-5 text-blue-600" />
</div>
<div>
<CardTitle>Password</CardTitle>
<CardDescription>Manage your sign-in password</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="p-4 bg-gray-50 rounded-md border border-gray-200">
<p className="text-sm text-gray-600">
Your password is managed through your organization's Single Sign-On (SSO) provider.
Please contact your IT administrator to reset or change your password.
</p>
</div>
</CardContent>
</Card>
{/* API Tokens Section */}
<Card className="shadow-md">
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-md">
<Key className="w-5 h-5 text-purple-600" />
</div>
<div>
<CardTitle>API Tokens</CardTitle>
<CardDescription>Manage personal access tokens for external integrations</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<ApiTokenManager />
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -19,10 +19,13 @@ import { UserRoleManager } from '@/components/admin/UserRoleManager';
import { ActivityTypeManager } from '@/components/admin/ActivityTypeManager'; import { ActivityTypeManager } from '@/components/admin/ActivityTypeManager';
import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal'; import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal';
import { NotificationPreferencesModal } from '@/components/settings/NotificationPreferencesModal'; import { NotificationPreferencesModal } from '@/components/settings/NotificationPreferencesModal';
// import { ApiTokenManager } from '@/components/settings/ApiTokenManager'; // Removed: Moved to dedicated page
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getUserSubscriptions } from '@/services/notificationApi'; import { getUserSubscriptions } from '@/services/notificationApi';
export function Settings() { export function Settings() {
const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const isAdmin = checkIsAdmin(user); const isAdmin = checkIsAdmin(user);
const [showNotificationModal, setShowNotificationModal] = useState(false); const [showNotificationModal, setShowNotificationModal] = useState(false);
@ -271,9 +274,18 @@ export function Settings() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-6">
<div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200"> <div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200">
<p className="text-sm text-gray-600 text-center">Security settings will be available soon</p> <p className="text-sm text-gray-600 mb-4">
Manage your password, API tokens, and other security preferences.
</p>
<Button
onClick={() => navigate('/settings/security')}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
>
<Lock className="w-4 h-4 mr-2" />
Manage Security
</Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -324,6 +336,9 @@ export function Settings() {
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
{/* Additional Settings if needed */}
</div> </div>
</TabsContent> </TabsContent>
@ -491,9 +506,18 @@ export function Settings() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-6">
<div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200"> <div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200">
<p className="text-sm text-gray-600 text-center">Security settings will be available soon</p> <p className="text-sm text-gray-600 mb-4">
Manage your password, API tokens, and other security preferences.
</p>
<Button
onClick={() => navigate('/settings/security')}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
>
<Lock className="w-4 h-4 mr-2" />
Manage Security
</Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -545,6 +569,8 @@ export function Settings() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Additional sections if needed */}
</> </>
)} )}
</div> </div>

View File

@ -230,7 +230,18 @@ export function mapToClaimManagementRequest(
const expectedCompletionDate = proposalDetails?.expectedCompletionDate || proposalDetails?.expected_completion_date; const expectedCompletionDate = proposalDetails?.expectedCompletionDate || proposalDetails?.expected_completion_date;
const proposal = proposalDetails ? { const proposal = proposalDetails ? {
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url, proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
costBreakup: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup) costBreakup: Array.isArray(proposalDetails.costItems || proposalDetails.cost_items)
? (proposalDetails.costItems || proposalDetails.cost_items).map((item: any) => ({
description: item.description || item.itemDescription || item.item_description || '',
amount: Number(item.amount) || 0,
gstRate: Number(item.gstRate ?? item.gst_rate ?? 0),
gstAmt: Number(item.gstAmt ?? item.gst_amt ?? 0),
cgstAmt: Number(item.cgstAmt ?? item.cgst_amt ?? 0),
sgstAmt: Number(item.sgstAmt ?? item.sgst_amt ?? 0),
igstAmt: Number(item.igstAmt ?? item.igst_amt ?? 0),
totalAmt: Number(item.totalAmt ?? item.total_amt ?? 0)
}))
: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup)
? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({ ? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
description: item.description || item.itemDescription || item.item_description || '', description: item.description || item.itemDescription || item.item_description || '',
amount: Number(item.amount) || 0, amount: Number(item.amount) || 0,