token generation from profile added and cost item enhnced to support hsn
This commit is contained in:
parent
d2d75d93f7
commit
5e91b85854
11
src/App.tsx
11
src/App.tsx
@ -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"
|
||||||
|
|||||||
297
src/components/settings/ApiTokenManager.tsx
Normal file
297
src/components/settings/ApiTokenManager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
<div className="col-span-4">Description</div>
|
||||||
key={index}
|
<div className="col-span-2 text-right">Base</div>
|
||||||
className="flex items-center justify-between py-1.5 sm:py-2 px-2 sm:px-3 bg-gray-50 rounded border"
|
<div className="col-span-2 text-right">GST Rate</div>
|
||||||
>
|
<div className="col-span-2 text-right">GST Amt</div>
|
||||||
<div className="flex-1 min-w-0 pr-2">
|
<div className="col-span-2 text-right">Total</div>
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate">
|
|
||||||
{expense.description || `Expense ${index + 1}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="ml-2 flex-shrink-0">
|
|
||||||
<p className="text-xs sm:text-sm font-semibold text-gray-900">
|
|
||||||
{formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)}
|
|
||||||
</p>
|
|
||||||
</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="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
|
||||||
|
key={index}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{/* Mobile View: Stacked */}
|
||||||
|
<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}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block col-span-2 text-right text-gray-600">
|
||||||
|
{formatCurrency(amount)}
|
||||||
|
</div>
|
||||||
|
<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 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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,57 +856,87 @@ 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 gap-2 items-start w-full">
|
<div className="flex flex-col gap-3 w-full relative">
|
||||||
<div className="flex-1 min-w-0">
|
{/* Row 1: Description and Close Button */}
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Description</Label>
|
<div className="flex gap-2 items-start w-full">
|
||||||
<Input
|
<div className="flex-1">
|
||||||
placeholder="Item description"
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Description</Label>
|
||||||
value={item.description}
|
<Input
|
||||||
onChange={(e) =>
|
placeholder="Item description"
|
||||||
handleCostItemChange(item.id, 'description', e.target.value)
|
value={item.description}
|
||||||
}
|
onChange={(e) =>
|
||||||
className="w-full bg-white"
|
handleCostItemChange(item.id, 'description', e.target.value)
|
||||||
/>
|
}
|
||||||
|
className="w-full bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
<div className="w-24 lg:w-28 flex-shrink-0">
|
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label>
|
{/* Row 2: Numeric Fields */}
|
||||||
<Input
|
<div className="flex gap-2 w-full">
|
||||||
type="number"
|
<div className="flex-1">
|
||||||
placeholder="Amount"
|
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">Amount (Base)</Label>
|
||||||
min="0"
|
<Input
|
||||||
step="0.01"
|
type="number"
|
||||||
value={item.amount || ''}
|
placeholder="Amount"
|
||||||
onChange={(e) =>
|
min="0"
|
||||||
handleCostItemChange(item.id, 'amount', e.target.value)
|
step="0.01"
|
||||||
}
|
value={item.amount || ''}
|
||||||
className="w-full bg-white"
|
onChange={(e) =>
|
||||||
/>
|
handleCostItemChange(item.id, 'amount', 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">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>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="%"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.1"
|
||||||
|
value={item.gstRate || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleCostItemChange(item.id, 'gstRate', e.target.value)
|
||||||
|
}
|
||||||
|
className="w-full bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-20 lg:w-24 flex-shrink-0">
|
|
||||||
<Label className="text-[10px] uppercase text-gray-500 font-bold mb-1 block">GST %</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="%"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
step="0.1"
|
|
||||||
value={item.gstRate || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleCostItemChange(item.id, 'gstRate', e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full bg-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
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 && (
|
||||||
|
|||||||
@ -39,6 +39,7 @@ interface CostItem {
|
|||||||
id: string;
|
id: string;
|
||||||
description: string;
|
description: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
quantity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProposalData {
|
interface ProposalData {
|
||||||
@ -110,14 +111,18 @@ export function InitiatorProposalApprovalModal({
|
|||||||
const costBreakup = Array.isArray(proposalData.costBreakup)
|
const costBreakup = Array.isArray(proposalData.costBreakup)
|
||||||
? proposalData.costBreakup
|
? proposalData.costBreakup
|
||||||
: (typeof proposalData.costBreakup === 'string'
|
: (typeof proposalData.costBreakup === 'string'
|
||||||
? JSON.parse(proposalData.costBreakup)
|
? JSON.parse(proposalData.costBreakup)
|
||||||
: []);
|
: []);
|
||||||
|
|
||||||
if (!Array.isArray(costBreakup)) return 0;
|
if (!Array.isArray(costBreakup)) return 0;
|
||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
@ -141,11 +146,11 @@ export function InitiatorProposalApprovalModal({
|
|||||||
if (!doc.name) return false;
|
if (!doc.name) return false;
|
||||||
const name = doc.name.toLowerCase();
|
const name = doc.name.toLowerCase();
|
||||||
return name.endsWith('.pdf') ||
|
return name.endsWith('.pdf') ||
|
||||||
name.endsWith('.jpg') ||
|
name.endsWith('.jpg') ||
|
||||||
name.endsWith('.jpeg') ||
|
name.endsWith('.jpeg') ||
|
||||||
name.endsWith('.png') ||
|
name.endsWith('.png') ||
|
||||||
name.endsWith('.gif') ||
|
name.endsWith('.gif') ||
|
||||||
name.endsWith('.webp');
|
name.endsWith('.webp');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle document preview - leverage FilePreview's internal fetching
|
// Handle document preview - leverage FilePreview's internal fetching
|
||||||
@ -321,43 +326,43 @@ export function InitiatorProposalApprovalModal({
|
|||||||
<div className="px-4 pb-4 border-t border-amber-200 space-y-4 bg-white/50">
|
<div className="px-4 pb-4 border-t border-amber-200 space-y-4 bg-white/50">
|
||||||
{/* Header Info: Date & Document */}
|
{/* Header Info: Date & Document */}
|
||||||
<div className="flex flex-wrap gap-4 text-xs mt-3">
|
<div className="flex flex-wrap gap-4 text-xs mt-3">
|
||||||
{previousProposalData.expectedCompletionDate && (
|
{previousProposalData.expectedCompletionDate && (
|
||||||
<div className="flex items-center gap-1.5 text-gray-700">
|
<div className="flex items-center gap-1.5 text-gray-700">
|
||||||
<Calendar className="w-3.5 h-3.5 text-gray-500" />
|
<Calendar className="w-3.5 h-3.5 text-gray-500" />
|
||||||
<span className="font-medium">Expected Completion:</span>
|
<span className="font-medium">Expected Completion:</span>
|
||||||
<span>{new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')}</span>
|
<span>{new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{previousProposalData.documentUrl && (
|
{previousProposalData.documentUrl && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{canPreviewDocument({ name: previousProposalData.documentUrl }) ? (
|
{canPreviewDocument({ name: previousProposalData.documentUrl }) ? (
|
||||||
<>
|
<>
|
||||||
<Eye className="w-3.5 h-3.5 text-blue-500" />
|
<Eye className="w-3.5 h-3.5 text-blue-500" />
|
||||||
<a
|
<a
|
||||||
href={previousProposalData.documentUrl}
|
href={previousProposalData.documentUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline font-medium flex items-center gap-1"
|
className="text-blue-600 hover:underline font-medium flex items-center gap-1"
|
||||||
>
|
>
|
||||||
View Previous Document
|
View Previous Document
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Download className="w-3.5 h-3.5 text-blue-500" />
|
<Download className="w-3.5 h-3.5 text-blue-500" />
|
||||||
<a
|
<a
|
||||||
href={previousProposalData.documentUrl}
|
href={previousProposalData.documentUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:underline font-medium flex items-center gap-1"
|
className="text-blue-600 hover:underline font-medium flex items-center gap-1"
|
||||||
>
|
>
|
||||||
Download Previous Document
|
Download Previous Document
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cost Breakdown */}
|
{/* Cost Breakdown */}
|
||||||
@ -398,40 +403,40 @@ export function InitiatorProposalApprovalModal({
|
|||||||
|
|
||||||
{/* Additional/Supporting Documents */}
|
{/* Additional/Supporting Documents */}
|
||||||
{previousProposalData.otherDocuments && previousProposalData.otherDocuments.length > 0 && (
|
{previousProposalData.otherDocuments && previousProposalData.otherDocuments.length > 0 && (
|
||||||
<div className="w-full pt-2 border-t border-amber-200/50">
|
<div className="w-full pt-2 border-t border-amber-200/50">
|
||||||
<p className="text-[10px] font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
|
<p className="text-[10px] font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
|
||||||
<FileText className="w-3 h-3" />
|
<FileText className="w-3 h-3" />
|
||||||
Supporting Documents
|
Supporting Documents
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2 max-h-[150px] overflow-y-auto">
|
<div className="space-y-2 max-h-[150px] overflow-y-auto">
|
||||||
{previousProposalData.otherDocuments.map((doc: any, idx: number) => (
|
{previousProposalData.otherDocuments.map((doc: any, idx: number) => (
|
||||||
<DocumentCard
|
<DocumentCard
|
||||||
key={idx}
|
key={idx}
|
||||||
document={{
|
document={{
|
||||||
documentId: doc.documentId || doc.id || '',
|
documentId: doc.documentId || doc.id || '',
|
||||||
name: doc.originalFileName || doc.fileName || doc.name || 'Supporting Document',
|
name: doc.originalFileName || doc.fileName || doc.name || 'Supporting Document',
|
||||||
fileType: (doc.originalFileName || doc.fileName || doc.name || '').split('.').pop() || 'file',
|
fileType: (doc.originalFileName || doc.fileName || doc.name || '').split('.').pop() || 'file',
|
||||||
uploadedAt: doc.uploadedAt || new Date().toISOString()
|
uploadedAt: doc.uploadedAt || new Date().toISOString()
|
||||||
}}
|
}}
|
||||||
onPreview={canPreviewDocument({ name: doc.originalFileName || doc.fileName || doc.name || '' }) ? () => handlePreviewDocument(doc) : undefined}
|
onPreview={canPreviewDocument({ name: doc.originalFileName || doc.fileName || doc.name || '' }) ? () => handlePreviewDocument(doc) : undefined}
|
||||||
onDownload={async (id) => {
|
onDownload={async (id) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
await downloadDocument(id);
|
await downloadDocument(id);
|
||||||
} else {
|
} else {
|
||||||
let downloadUrl = doc.storageUrl || doc.documentUrl;
|
let downloadUrl = doc.storageUrl || doc.documentUrl;
|
||||||
if (downloadUrl && !downloadUrl.startsWith('http')) {
|
if (downloadUrl && !downloadUrl.startsWith('http')) {
|
||||||
const baseUrl = import.meta.env.VITE_BASE_URL || '';
|
const baseUrl = import.meta.env.VITE_BASE_URL || '';
|
||||||
const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
const cleanFileUrl = downloadUrl.startsWith('/') ? downloadUrl : `/${downloadUrl}`;
|
const cleanFileUrl = downloadUrl.startsWith('/') ? downloadUrl : `/${downloadUrl}`;
|
||||||
downloadUrl = `${cleanBaseUrl}${cleanFileUrl}`;
|
downloadUrl = `${cleanBaseUrl}${cleanFileUrl}`;
|
||||||
}
|
}
|
||||||
if (downloadUrl) window.open(downloadUrl, '_blank');
|
if (downloadUrl) window.open(downloadUrl, '_blank');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comments */}
|
{/* Comments */}
|
||||||
@ -453,247 +458,262 @@ export function InitiatorProposalApprovalModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6 lg:items-start lg:content-start">
|
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-6 lg:items-start lg:content-start">
|
||||||
{/* Left Column - Documents */}
|
{/* Left Column - Documents */}
|
||||||
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
||||||
{/* Proposal Document Section */}
|
{/* Proposal Document Section */}
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
|
||||||
<FileText className="w-4 h-4 text-blue-600" />
|
|
||||||
Proposal Document
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
{proposalData?.proposalDocument ? (
|
|
||||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
|
||||||
<FileText className="w-5 h-5 lg:w-6 lg:h-6 text-blue-600 flex-shrink-0" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={proposalData.proposalDocument.name}>
|
|
||||||
{proposalData.proposalDocument.name}
|
|
||||||
</p>
|
|
||||||
{proposalData?.submittedAt && (
|
|
||||||
<p className="text-xs text-gray-500 truncate">
|
|
||||||
Submitted on {formatDate(proposalData.submittedAt)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
|
||||||
{proposalData.proposalDocument.id && (
|
|
||||||
<>
|
|
||||||
{canPreviewDocument(proposalData.proposalDocument) && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handlePreviewDocument(proposalData.proposalDocument!)}
|
|
||||||
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
||||||
title="Preview document"
|
|
||||||
>
|
|
||||||
<Eye className="w-5 h-5 text-blue-600" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
if (proposalData.proposalDocument?.id) {
|
|
||||||
await downloadDocument(proposalData.proposalDocument.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to download document:', error);
|
|
||||||
toast.error('Failed to download document');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
||||||
title="Download document"
|
|
||||||
>
|
|
||||||
<Download className="w-5 h-5 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-gray-500 italic">No proposal document available</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Other Supporting Documents */}
|
|
||||||
{proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
|
||||||
<FileText className="w-4 h-4 text-gray-600" />
|
|
||||||
Other Supporting Documents
|
|
||||||
</h3>
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{proposalData.otherDocuments.length} file(s)
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
|
|
||||||
{proposalData.otherDocuments.map((doc, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="border rounded-lg p-2 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
|
||||||
<FileText className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600 flex-shrink-0" />
|
|
||||||
<p className="text-xs lg:text-sm font-medium text-gray-900 truncate" title={doc.name}>
|
|
||||||
{doc.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{doc.id && (
|
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
|
||||||
{canPreviewDocument(doc) && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handlePreviewDocument(doc)}
|
|
||||||
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
||||||
title="Preview document"
|
|
||||||
>
|
|
||||||
<Eye className="w-5 h-5 text-blue-600" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
if (doc.id) {
|
|
||||||
await downloadDocument(doc.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to download document:', error);
|
|
||||||
toast.error('Failed to download document');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
||||||
title="Download document"
|
|
||||||
>
|
|
||||||
<Download className="w-5 h-5 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column - Planning & Details */}
|
|
||||||
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
|
||||||
{/* Cost Breakup Section */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
|
||||||
<IndianRupee className="w-4 h-4 text-green-600" />
|
|
||||||
Cost Breakup
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
{(() => {
|
|
||||||
// Ensure costBreakup is an array
|
|
||||||
const costBreakup = proposalData?.costBreakup
|
|
||||||
? (Array.isArray(proposalData.costBreakup)
|
|
||||||
? proposalData.costBreakup
|
|
||||||
: (typeof proposalData.costBreakup === 'string'
|
|
||||||
? JSON.parse(proposalData.costBreakup)
|
|
||||||
: []))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<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="grid grid-cols-2 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
|
|
||||||
<div>Item Description</div>
|
|
||||||
<div className="text-right">Amount</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y">
|
|
||||||
{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 className="text-xs lg:text-sm text-gray-700">{item?.description || 'N/A'}</div>
|
|
||||||
<div className="text-xs lg:text-sm font-semibold text-gray-900 text-right">
|
|
||||||
₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="border-2 border-[--re-green] rounded-lg p-2.5 lg:p-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<IndianRupee className="w-4 h-4 text-[--re-green]" />
|
|
||||||
<span className="font-semibold text-xs lg:text-sm text-gray-700">Total Estimated Budget</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-lg lg:text-xl font-bold text-[--re-green]">
|
|
||||||
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-gray-500 italic">No cost breakdown available</p>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Section */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
|
||||||
<Calendar className="w-4 h-4 text-purple-600" />
|
|
||||||
Expected Completion Date
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50">
|
|
||||||
<p className="text-sm lg:text-base font-semibold text-gray-900">
|
|
||||||
{proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Comments Section - Side by Side */}
|
|
||||||
<div className="space-y-2 border-t pt-3 lg:pt-3 lg:col-span-2 mt-2">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
|
|
||||||
{/* Dealer Comments */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
<MessageSquare className="w-4 h-4 text-blue-600" />
|
<FileText className="w-4 h-4 text-blue-600" />
|
||||||
Dealer Comments
|
Proposal Document
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
|
{proposalData?.proposalDocument ? (
|
||||||
<p className="text-xs text-gray-700 whitespace-pre-wrap">
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2">
|
||||||
{proposalData?.dealerComments || 'No comments provided'}
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
|
<FileText className="w-5 h-5 lg:w-6 lg:h-6 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={proposalData.proposalDocument.name}>
|
||||||
|
{proposalData.proposalDocument.name}
|
||||||
|
</p>
|
||||||
|
{proposalData?.submittedAt && (
|
||||||
|
<p className="text-xs text-gray-500 truncate">
|
||||||
|
Submitted on {formatDate(proposalData.submittedAt)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{proposalData.proposalDocument.id && (
|
||||||
|
<>
|
||||||
|
{canPreviewDocument(proposalData.proposalDocument) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePreviewDocument(proposalData.proposalDocument!)}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Preview document"
|
||||||
|
>
|
||||||
|
<Eye className="w-5 h-5 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (proposalData.proposalDocument?.id) {
|
||||||
|
await downloadDocument(proposalData.proposalDocument.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download document:', error);
|
||||||
|
toast.error('Failed to download document');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Download document"
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-gray-500 italic">No proposal document available</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other Supporting Documents */}
|
||||||
|
{proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-gray-600" />
|
||||||
|
Other Supporting Documents
|
||||||
|
</h3>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{proposalData.otherDocuments.length} file(s)
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
|
||||||
|
{proposalData.otherDocuments.map((doc, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border rounded-lg p-2 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
|
<FileText className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600 flex-shrink-0" />
|
||||||
|
<p className="text-xs lg:text-sm font-medium text-gray-900 truncate" title={doc.name}>
|
||||||
|
{doc.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{doc.id && (
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
{canPreviewDocument(doc) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePreviewDocument(doc)}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Preview document"
|
||||||
|
>
|
||||||
|
<Eye className="w-5 h-5 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (doc.id) {
|
||||||
|
await downloadDocument(doc.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download document:', error);
|
||||||
|
toast.error('Failed to download document');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Download document"
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Planning & Details */}
|
||||||
|
<div className="space-y-4 lg:space-y-4 flex flex-col">
|
||||||
|
{/* Cost Breakup Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
|
<IndianRupee className="w-4 h-4 text-green-600" />
|
||||||
|
Cost Breakup
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
// Ensure costBreakup is an array
|
||||||
|
const costBreakup = proposalData?.costBreakup
|
||||||
|
? (Array.isArray(proposalData.costBreakup)
|
||||||
|
? proposalData.costBreakup
|
||||||
|
: (typeof proposalData.costBreakup === 'string'
|
||||||
|
? JSON.parse(proposalData.costBreakup)
|
||||||
|
: []))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<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="grid grid-cols-5 gap-4 text-xs lg:text-sm font-semibold text-gray-700">
|
||||||
|
<div className="col-span-1">Item Description</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 className="divide-y">
|
||||||
|
{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-5 gap-4">
|
||||||
|
<div className="col-span-1 text-xs lg:text-sm text-gray-700">
|
||||||
|
{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 })}
|
||||||
|
</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 className="border-2 border-[--re-green] rounded-lg p-2.5 lg:p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IndianRupee className="w-4 h-4 text-[--re-green]" />
|
||||||
|
<span className="font-semibold text-xs lg:text-sm text-gray-700">Total Estimated Budget</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg lg:text-xl font-bold text-[--re-green]">
|
||||||
|
₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-gray-500 italic">No cost breakdown available</p>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline Section */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
|
<Calendar className="w-4 h-4 text-purple-600" />
|
||||||
|
Expected Completion Date
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50">
|
||||||
|
<p className="text-sm lg:text-base font-semibold text-gray-900">
|
||||||
|
{proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Your Decision & Comments */}
|
{/* Comments Section - Side by Side */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 border-t pt-3 lg:pt-3 lg:col-span-2 mt-2">
|
||||||
<h3 className="font-semibold text-sm lg:text-base">Your Decision & Comments</h3>
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
|
||||||
<Textarea
|
{/* Dealer Comments */}
|
||||||
placeholder="Provide your evaluation comments, approval conditions, or rejection reasons..."
|
<div className="space-y-2">
|
||||||
value={comments}
|
<div className="flex items-center gap-2">
|
||||||
onChange={(e) => setComments(e.target.value)}
|
<h3 className="font-semibold text-sm lg:text-base flex items-center gap-2">
|
||||||
className="min-h-[150px] lg:min-h-[140px] text-xs lg:text-sm w-full"
|
<MessageSquare className="w-4 h-4 text-blue-600" />
|
||||||
/>
|
Dealer Comments
|
||||||
<p className="text-xs text-gray-500">{comments.length} characters</p>
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 max-h-[150px] lg:max-h-[140px] overflow-y-auto">
|
||||||
|
<p className="text-xs text-gray-700 whitespace-pre-wrap">
|
||||||
|
{proposalData?.dealerComments || 'No comments provided'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Your Decision & Comments */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-sm lg:text-base">Your Decision & Comments</h3>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Provide your evaluation comments, approval conditions, or rejection reasons..."
|
||||||
|
value={comments}
|
||||||
|
onChange={(e) => setComments(e.target.value)}
|
||||||
|
className="min-h-[150px] lg:min-h-[140px] text-xs lg:text-sm w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">{comments.length} characters</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warning for missing comments */}
|
{/* Warning for missing comments */}
|
||||||
{!comments.trim() && (
|
{!comments.trim() && (
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-2 flex items-start gap-2 lg:col-span-2">
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-2 flex items-start gap-2 lg:col-span-2">
|
||||||
<XCircle className="w-3.5 h-3.5 text-amber-600 flex-shrink-0 mt-0.5" />
|
<XCircle className="w-3.5 h-3.5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
<p className="text-xs text-amber-800">
|
<p className="text-xs text-amber-800">
|
||||||
Please provide comments before making a decision. Comments are required and will be visible to all participants.
|
Please provide comments before making a decision. Comments are required and will be visible to all participants.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
67
src/pages/Settings/SecuritySettings.tsx
Normal file
67
src/pages/Settings/SecuritySettings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -230,18 +230,29 @@ 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.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
|
? (proposalDetails.costItems || proposalDetails.cost_items).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,
|
||||||
gstRate: item.gstRate ?? item.gst_rate,
|
gstRate: Number(item.gstRate ?? item.gst_rate ?? 0),
|
||||||
gstAmt: item.gstAmt ?? item.gst_amt,
|
gstAmt: Number(item.gstAmt ?? item.gst_amt ?? 0),
|
||||||
cgstAmt: item.cgstAmt ?? item.cgst_amt,
|
cgstAmt: Number(item.cgstAmt ?? item.cgst_amt ?? 0),
|
||||||
sgstAmt: item.sgstAmt ?? item.sgst_amt,
|
sgstAmt: Number(item.sgstAmt ?? item.sgst_amt ?? 0),
|
||||||
igstAmt: item.igstAmt ?? item.igst_amt,
|
igstAmt: Number(item.igstAmt ?? item.igst_amt ?? 0),
|
||||||
totalAmt: item.totalAmt ?? item.total_amt
|
totalAmt: Number(item.totalAmt ?? item.total_amt ?? 0)
|
||||||
}))
|
}))
|
||||||
: [],
|
: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup)
|
||||||
|
? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
|
||||||
|
description: item.description || item.itemDescription || item.item_description || '',
|
||||||
|
amount: Number(item.amount) || 0,
|
||||||
|
gstRate: item.gstRate ?? item.gst_rate,
|
||||||
|
gstAmt: item.gstAmt ?? item.gst_amt,
|
||||||
|
cgstAmt: item.cgstAmt ?? item.cgst_amt,
|
||||||
|
sgstAmt: item.sgstAmt ?? item.sgst_amt,
|
||||||
|
igstAmt: item.igstAmt ?? item.igst_amt,
|
||||||
|
totalAmt: item.totalAmt ?? item.total_amt
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
||||||
timelineMode: proposalDetails.timelineMode || proposalDetails.timeline_mode,
|
timelineMode: proposalDetails.timelineMode || proposalDetails.timeline_mode,
|
||||||
expectedCompletionDate: expectedCompletionDate,
|
expectedCompletionDate: expectedCompletionDate,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user