primus-saas-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/DEMO.md +68 -0
  2. package/INTEGRATION.md +702 -0
  3. package/README.md +82 -0
  4. package/build_log.txt +0 -0
  5. package/dist/index.d.mts +446 -0
  6. package/dist/index.d.ts +446 -0
  7. package/dist/index.js +2513 -0
  8. package/dist/index.mjs +2445 -0
  9. package/package.json +37 -0
  10. package/postcss.config.js +6 -0
  11. package/src/components/ai/AICopilot.tsx +88 -0
  12. package/src/components/auth/PrimusLogin.tsx +149 -0
  13. package/src/components/auth/UserProfile.tsx +26 -0
  14. package/src/components/banking/accounts/AccountDashboard.tsx +67 -0
  15. package/src/components/banking/cards/CreditCardVisual.tsx +67 -0
  16. package/src/components/banking/credit/CreditScoreCard.tsx +80 -0
  17. package/src/components/banking/kyc/KYCVerification.tsx +76 -0
  18. package/src/components/banking/loans/LoanCalculator.tsx +106 -0
  19. package/src/components/banking/transactions/TransactionHistory.tsx +74 -0
  20. package/src/components/crud/PrimusDataTable.tsx +220 -0
  21. package/src/components/crud/PrimusModal.tsx +68 -0
  22. package/src/components/dashboard/PrimusDashboard.tsx +145 -0
  23. package/src/components/documents/DocumentViewer.tsx +107 -0
  24. package/src/components/featureflags/FeatureFlagToggle.tsx +64 -0
  25. package/src/components/insurance/agents/AgentDirectory.tsx +72 -0
  26. package/src/components/insurance/claims/ClaimStatusTracker.tsx +78 -0
  27. package/src/components/insurance/fraud/FraudDetectionDashboard.tsx +68 -0
  28. package/src/components/insurance/policies/PolicyCard.tsx +77 -0
  29. package/src/components/insurance/premium/PremiumCalculator.tsx +104 -0
  30. package/src/components/insurance/quotes/QuoteComparison.tsx +75 -0
  31. package/src/components/layout/PrimusHeader.tsx +75 -0
  32. package/src/components/layout/PrimusLayout.tsx +47 -0
  33. package/src/components/layout/PrimusSidebar.tsx +102 -0
  34. package/src/components/logging/LogViewer.tsx +90 -0
  35. package/src/components/notifications/NotificationFeed.tsx +106 -0
  36. package/src/components/notifications/PrimusNotificationCenter.tsx +282 -0
  37. package/src/components/payments/CheckoutForm.tsx +167 -0
  38. package/src/components/security/SecurityDashboard.tsx +83 -0
  39. package/src/components/shared/Button.tsx +36 -0
  40. package/src/components/shared/Input.tsx +36 -0
  41. package/src/components/storage/FileUploader.tsx +79 -0
  42. package/src/context/PrimusProvider.tsx +147 -0
  43. package/src/context/PrimusThemeProvider.tsx +160 -0
  44. package/src/hooks/useNotifications.ts +58 -0
  45. package/src/hooks/usePrimusAuth.ts +3 -0
  46. package/src/hooks/useRealtimeNotifications.ts +114 -0
  47. package/src/index.ts +42 -0
  48. package/tailwind.config.js +18 -0
  49. package/tsconfig.json +28 -0
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "primus-saas-react",
3
+ "version": "1.0.0",
4
+ "description": "Premium React UI Elements for Primus SaaS Framework",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --format cjs,esm --dts",
11
+ "lint": "eslint src/",
12
+ "dev": "tsup src/index.ts --format cjs,esm --watch --dts"
13
+ },
14
+ "peerDependencies": {
15
+ "react": ">=18",
16
+ "react-dom": ">=18"
17
+ },
18
+ "dependencies": {
19
+ "@headlessui/react": "^1.7.17",
20
+ "@heroicons/react": "^2.0.18",
21
+ "clsx": "^2.0.0",
22
+ "lucide-react": "^0.300.0",
23
+ "tailwind-merge": "^2.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^18.2.0",
27
+ "@types/react-dom": "^18.2.0",
28
+ "autoprefixer": "^10.4.16",
29
+ "postcss": "^8.4.31",
30
+ "tailwindcss": "^3.3.5",
31
+ "tsup": "^7.2.0",
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,88 @@
1
+ import React, { useState } from 'react';
2
+
3
+ export const AICopilot: React.FC<{ apiUrl?: string }> = ({
4
+ apiUrl = 'http://localhost:5221'
5
+ }) => {
6
+ const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]);
7
+ const [input, setInput] = useState('');
8
+ const [loading, setLoading] = useState(false);
9
+
10
+ const sendMessage = async () => {
11
+ if (!input.trim()) return;
12
+
13
+ const userMessage = { role: 'user', content: input };
14
+ setMessages(prev => [...prev, userMessage]);
15
+ setInput('');
16
+ setLoading(true);
17
+
18
+ try {
19
+ const response = await fetch(`${apiUrl}/api/ai/chat`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({ message: input })
23
+ });
24
+
25
+ if (response.ok) {
26
+ const data = await response.json();
27
+ setMessages(prev => [...prev, { role: 'assistant', content: data.message }]);
28
+ }
29
+ } catch (error) {
30
+ console.error('Failed to send message:', error);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className="bg-white rounded-lg border border-gray-200 shadow-sm flex flex-col h-[600px]">
38
+ <div className="p-4 border-b border-gray-200">
39
+ <h3 className="text-lg font-medium text-gray-900">AI Copilot</h3>
40
+ </div>
41
+
42
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
43
+ {messages.map((msg, index) => (
44
+ <div key={index} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
45
+ <div className={`max-w-[70%] p-3 rounded-lg ${msg.role === 'user'
46
+ ? 'bg-primus-600 text-white'
47
+ : 'bg-gray-100 text-gray-900'
48
+ }`}>
49
+ {msg.content}
50
+ </div>
51
+ </div>
52
+ ))}
53
+ {loading && (
54
+ <div className="flex justify-start">
55
+ <div className="bg-gray-100 p-3 rounded-lg">
56
+ <div className="flex gap-1">
57
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
58
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
59
+ <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ )}
64
+ </div>
65
+
66
+ <div className="p-4 border-t border-gray-200">
67
+ <div className="flex gap-2">
68
+ <input
69
+ type="text"
70
+ value={input}
71
+ onChange={(e) => setInput(e.target.value)}
72
+ onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
73
+ placeholder="Ask me anything..."
74
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primus-500"
75
+ disabled={loading}
76
+ />
77
+ <button
78
+ onClick={sendMessage}
79
+ disabled={loading || !input.trim()}
80
+ className="px-4 py-2 bg-primus-600 text-white rounded-lg hover:bg-primus-700 disabled:opacity-50"
81
+ >
82
+ Send
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ );
88
+ };
@@ -0,0 +1,149 @@
1
+ import React, { useState } from 'react';
2
+ import { usePrimusAuth } from '../../hooks/usePrimusAuth';
3
+ import { Button } from '../shared/Button';
4
+ import { Input } from '../shared/Input';
5
+
6
+ export interface PrimusLoginProps {
7
+ /** Custom logo URL */
8
+ logo?: string;
9
+ /** Login form title */
10
+ title?: string;
11
+ /** Show social login buttons */
12
+ allowSocial?: boolean;
13
+ /** Called after successful login */
14
+ onSuccess?: (user: any, token: string) => void;
15
+ /** Called on login failure */
16
+ onError?: (error: string) => void;
17
+ /** Show loading spinner overlay */
18
+ showLoadingOverlay?: boolean;
19
+ }
20
+
21
+ export const PrimusLogin: React.FC<PrimusLoginProps> = ({
22
+ logo,
23
+ title = "Sign in to Primus SaaS",
24
+ allowSocial = true,
25
+ onSuccess,
26
+ onError,
27
+ showLoadingOverlay = true,
28
+ }) => {
29
+ const { login, isLoading } = usePrimusAuth();
30
+ const [email, setEmail] = useState('');
31
+ const [password, setPassword] = useState('');
32
+ const [error, setError] = useState<string | null>(null);
33
+ const [localLoading, setLocalLoading] = useState(false);
34
+
35
+ const handleSubmit = async (e: React.FormEvent) => {
36
+ e.preventDefault();
37
+ setError(null);
38
+ setLocalLoading(true);
39
+
40
+ const result = await login({ email, password });
41
+
42
+ if (result.success) {
43
+ onSuccess?.({ email }, ''); // Token available via usePrimusAuth
44
+ } else {
45
+ setError(result.error || 'Login failed');
46
+ onError?.(result.error || 'Login failed');
47
+ }
48
+ setLocalLoading(false);
49
+ };
50
+
51
+ const handleSocialLogin = async (provider: 'google' | 'github' | 'auth0' | 'azure') => {
52
+ setError(null);
53
+ const result = await login({ provider });
54
+
55
+ if (result.success) {
56
+ onSuccess?.({ provider }, '');
57
+ } else {
58
+ setError(result.error || 'Social login failed');
59
+ onError?.(result.error || 'Social login failed');
60
+ }
61
+ };
62
+
63
+ const loading = isLoading || localLoading;
64
+
65
+ return (
66
+ <div className="w-full max-w-md mx-auto bg-white p-8 rounded-xl shadow-lg border border-gray-100 relative">
67
+ {/* Loading Overlay */}
68
+ {showLoadingOverlay && loading && (
69
+ <div className="absolute inset-0 bg-white/80 flex items-center justify-center rounded-xl z-10">
70
+ <div className="flex items-center gap-3">
71
+ <svg className="animate-spin h-5 w-5 text-blue-600" viewBox="0 0 24 24">
72
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
73
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
74
+ </svg>
75
+ <span className="text-gray-600">Signing in...</span>
76
+ </div>
77
+ </div>
78
+ )}
79
+
80
+ <div className="text-center mb-8">
81
+ {logo && <img src={logo} alt="Logo" className="h-12 w-12 mx-auto mb-4" />}
82
+ <h2 className="text-2xl font-bold text-gray-900">{title}</h2>
83
+ <p className="text-gray-500 mt-2">Welcome back! Please enter your details.</p>
84
+ </div>
85
+
86
+ {/* Error Message */}
87
+ {error && (
88
+ <div className="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
89
+ {error}
90
+ </div>
91
+ )}
92
+
93
+ <form onSubmit={handleSubmit} className="space-y-4">
94
+ <Input
95
+ label="Email"
96
+ type="email"
97
+ placeholder="Enter your email"
98
+ value={email}
99
+ onChange={(e) => setEmail(e.target.value)}
100
+ required
101
+ disabled={loading}
102
+ />
103
+ <Input
104
+ label="Password"
105
+ type="password"
106
+ placeholder="••••••••"
107
+ value={password}
108
+ onChange={(e) => setPassword(e.target.value)}
109
+ required
110
+ disabled={loading}
111
+ />
112
+
113
+ <div className="flex items-center justify-between text-sm">
114
+ <label className="flex items-center gap-2 text-gray-600 cursor-pointer">
115
+ <input type="checkbox" className="rounded border-gray-300 text-blue-600 focus:ring-blue-600" />
116
+ Remember me
117
+ </label>
118
+ <a href="#" className="text-blue-600 font-medium hover:underline">Forgot password?</a>
119
+ </div>
120
+
121
+ <Button type="submit" className="w-full" disabled={loading}>
122
+ {loading ? 'Signing in...' : 'Sign in'}
123
+ </Button>
124
+ </form>
125
+
126
+ {allowSocial && (
127
+ <div className="mt-6">
128
+ <div className="relative">
129
+ <div className="absolute inset-0 flex items-center">
130
+ <span className="w-full border-t border-gray-200" />
131
+ </div>
132
+ <div className="relative flex justify-center text-sm">
133
+ <span className="bg-white px-2 text-gray-500">Or continue with</span>
134
+ </div>
135
+ </div>
136
+
137
+ <div className="mt-6 grid grid-cols-2 gap-3">
138
+ <Button variant="outline" onClick={() => handleSocialLogin('google')} disabled={loading}>
139
+ Google
140
+ </Button>
141
+ <Button variant="outline" onClick={() => handleSocialLogin('github')} disabled={loading}>
142
+ GitHub
143
+ </Button>
144
+ </div>
145
+ </div>
146
+ )}
147
+ </div>
148
+ );
149
+ };
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { usePrimusAuth } from '../../hooks/usePrimusAuth';
3
+ import { Button } from '../shared/Button';
4
+
5
+ export const UserProfile: React.FC = () => {
6
+ const { user, logout, isAuthenticated } = usePrimusAuth();
7
+
8
+ if (!isAuthenticated || !user) {
9
+ return null;
10
+ }
11
+
12
+ return (
13
+ <div className="flex items-center gap-4 p-4 rounded-lg bg-gray-50 border border-gray-200">
14
+ <div className="h-10 w-10 rounded-full bg-primus-100 flex items-center justify-center text-primus-600 font-bold">
15
+ {user.name?.charAt(0) || 'U'}
16
+ </div>
17
+ <div className="flex-1">
18
+ <p className="text-sm font-medium text-gray-900">{user.name}</p>
19
+ <p className="text-xs text-gray-500">{user.email}</p>
20
+ </div>
21
+ <Button variant="outline" size="sm" onClick={() => logout()}>
22
+ Sign out
23
+ </Button>
24
+ </div>
25
+ );
26
+ };
@@ -0,0 +1,67 @@
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ interface Account {
4
+ id: string;
5
+ accountNumber: string;
6
+ type: string;
7
+ balance: number;
8
+ currency: string;
9
+ status: string;
10
+ }
11
+
12
+ export const AccountDashboard: React.FC<{ apiUrl?: string }> = ({
13
+ apiUrl = 'http://localhost:5221'
14
+ }) => {
15
+ const [accounts, setAccounts] = useState<Account[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ fetchAccounts();
20
+ }, []);
21
+
22
+ const fetchAccounts = async () => {
23
+ try {
24
+ const response = await fetch(`${apiUrl}/api/banking/accounts`);
25
+ if (response.ok) {
26
+ const data = await response.json();
27
+ setAccounts(data);
28
+ }
29
+ } catch (error) {
30
+ console.error('Failed to fetch accounts:', error);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ if (loading) return <div className="p-4">Loading accounts...</div>;
37
+
38
+ return (
39
+ <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
40
+ <h3 className="text-lg font-medium text-gray-900 mb-4">Bank Accounts</h3>
41
+ <div className="space-y-4">
42
+ {accounts.map((account) => (
43
+ <div key={account.id} className="p-4 bg-gray-50 rounded-lg border border-gray-200">
44
+ <div className="flex justify-between items-start">
45
+ <div>
46
+ <h4 className="font-medium text-gray-900">{account.type}</h4>
47
+ <p className="text-sm text-gray-500">{account.accountNumber}</p>
48
+ </div>
49
+ <div className="text-right">
50
+ <p className="text-2xl font-bold text-gray-900">
51
+ ${account.balance.toLocaleString()}
52
+ </p>
53
+ <p className="text-sm text-gray-500">{account.currency}</p>
54
+ </div>
55
+ </div>
56
+ <div className="mt-2">
57
+ <span className={`px-2 py-1 rounded text-xs font-medium ${account.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
58
+ }`}>
59
+ {account.status}
60
+ </span>
61
+ </div>
62
+ </div>
63
+ ))}
64
+ </div>
65
+ </div>
66
+ );
67
+ };
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { clsx } from 'clsx';
3
+ import { WifiIcon } from '@heroicons/react/24/outline'; // Approximate for contactless
4
+
5
+ export interface CreditCardProps {
6
+ cardHolder: string;
7
+ last4: string;
8
+ expiry: string;
9
+ brand?: 'visa' | 'mastercard' | 'amex';
10
+ variant?: 'black' | 'blue' | 'gold' | 'platinum';
11
+ }
12
+
13
+ export const CreditCardVisual: React.FC<CreditCardProps> = ({
14
+ cardHolder,
15
+ last4,
16
+ expiry,
17
+ brand = 'visa',
18
+ variant = 'black'
19
+ }) => {
20
+ const getBackground = () => {
21
+ switch (variant) {
22
+ case 'blue': return 'bg-gradient-to-br from-blue-600 to-blue-800';
23
+ case 'gold': return 'bg-gradient-to-br from-yellow-500 to-yellow-700';
24
+ case 'platinum': return 'bg-gradient-to-br from-gray-300 to-gray-500';
25
+ default: return 'bg-gradient-to-br from-gray-900 to-gray-800';
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div className={clsx(
31
+ "w-80 h-48 rounded-2xl p-6 text-white shadow-xl relative overflow-hidden transition-transform hover:scale-105 duration-300",
32
+ getBackground()
33
+ )}>
34
+ {/* Decorative circles */}
35
+ <div className="absolute top-0 right-0 -mr-10 -mt-10 w-40 h-40 bg-white opacity-5 rounded-full blur-xl" />
36
+ <div className="absolute bottom-0 left-0 -ml-10 -mb-10 w-40 h-40 bg-white opacity-5 rounded-full blur-xl" />
37
+
38
+ <div className="flex justify-between items-start mb-8">
39
+ {/* Chip */}
40
+ <div className="w-12 h-9 bg-yellow-400 rounded-md opacity-80 flex items-center justify-center">
41
+ <div className="w-8 h-6 border border-yellow-600 rounded opacity-50" />
42
+ </div>
43
+ <WifiIcon className="h-6 w-6 rotate-90 opacity-70" />
44
+ </div>
45
+
46
+ <div className="mb-6">
47
+ <p className="font-mono text-xl tracking-widest text-shadow">
48
+ •••• •••• •••• {last4}
49
+ </p>
50
+ </div>
51
+
52
+ <div className="flex justify-between items-end">
53
+ <div>
54
+ <p className="text-[10px] uppercase text-gray-300 tracking-wider mb-1">Card Holder</p>
55
+ <p className="font-medium tracking-wide uppercase text-sm truncate max-w-[160px]">{cardHolder}</p>
56
+ </div>
57
+ <div>
58
+ <p className="text-[10px] uppercase text-gray-300 tracking-wider mb-1">Expires</p>
59
+ <p className="font-medium tracking-wide text-sm">{expiry}</p>
60
+ </div>
61
+ <div className="font-bold text-lg italic opacity-90">
62
+ {brand.toUpperCase()}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ };
@@ -0,0 +1,80 @@
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ interface CreditScore {
4
+ score: number;
5
+ rating: string;
6
+ lastUpdated: string;
7
+ factors: Array<{ factor: string; impact: string; weight: number }>;
8
+ }
9
+
10
+ export const CreditScoreCard: React.FC<{ userId: string; apiUrl?: string }> = ({
11
+ userId,
12
+ apiUrl = 'http://localhost:5221'
13
+ }) => {
14
+ const [score, setScore] = useState<CreditScore | null>(null);
15
+ const [loading, setLoading] = useState(true);
16
+
17
+ useEffect(() => {
18
+ fetchScore();
19
+ }, [userId]);
20
+
21
+ const fetchScore = async () => {
22
+ try {
23
+ const response = await fetch(`${apiUrl}/api/banking/credit-score/${userId}`);
24
+ if (response.ok) {
25
+ const data = await response.json();
26
+ setScore(data);
27
+ }
28
+ } catch (error) {
29
+ console.error('Failed to fetch credit score:', error);
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ };
34
+
35
+ if (loading) return <div className="p-4">Loading credit score...</div>;
36
+ if (!score) return <div className="p-4">No credit score data</div>;
37
+
38
+ const getScoreColor = (score: number) => {
39
+ if (score >= 740) return 'text-green-600';
40
+ if (score >= 670) return 'text-blue-600';
41
+ if (score >= 580) return 'text-yellow-600';
42
+ return 'text-red-600';
43
+ };
44
+
45
+ return (
46
+ <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
47
+ <h3 className="text-lg font-medium text-gray-900 mb-4">Credit Score</h3>
48
+
49
+ <div className="text-center mb-6">
50
+ <div className={`text-6xl font-bold ${getScoreColor(score.score)}`}>
51
+ {score.score}
52
+ </div>
53
+ <div className="text-lg text-gray-600 mt-2">{score.rating}</div>
54
+ <div className="text-sm text-gray-400 mt-1">
55
+ Updated: {new Date(score.lastUpdated).toLocaleDateString()}
56
+ </div>
57
+ </div>
58
+
59
+ <div>
60
+ <h4 className="font-medium text-gray-900 mb-3">Score Factors</h4>
61
+ <div className="space-y-2">
62
+ {score.factors.map((factor, index) => (
63
+ <div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
64
+ <span className="text-sm text-gray-900">{factor.factor}</span>
65
+ <div className="flex items-center gap-2">
66
+ <span className="text-xs text-gray-500">{factor.weight}%</span>
67
+ <span className={`px-2 py-0.5 rounded text-xs font-medium ${factor.impact === 'positive' ? 'bg-green-100 text-green-800' :
68
+ factor.impact === 'negative' ? 'bg-red-100 text-red-800' :
69
+ 'bg-gray-100 text-gray-800'
70
+ }`}>
71
+ {factor.impact}
72
+ </span>
73
+ </div>
74
+ </div>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ };
@@ -0,0 +1,76 @@
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ interface KYCStatus {
4
+ userId: string;
5
+ status: string;
6
+ verificationDate: string;
7
+ documents: Array<{ type: string; status: string; uploadDate: string }>;
8
+ riskLevel: string;
9
+ }
10
+
11
+ export const KYCVerification: React.FC<{ userId: string; apiUrl?: string }> = ({
12
+ userId,
13
+ apiUrl = 'http://localhost:5221'
14
+ }) => {
15
+ const [status, setStatus] = useState<KYCStatus | null>(null);
16
+ const [loading, setLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ fetchStatus();
20
+ }, [userId]);
21
+
22
+ const fetchStatus = async () => {
23
+ try {
24
+ const response = await fetch(`${apiUrl}/api/banking/kyc/status/${userId}`);
25
+ if (response.ok) {
26
+ const data = await response.json();
27
+ setStatus(data);
28
+ }
29
+ } catch (error) {
30
+ console.error('Failed to fetch KYC status:', error);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ if (loading) return <div className="p-4">Loading KYC status...</div>;
37
+ if (!status) return <div className="p-4">No KYC data found</div>;
38
+
39
+ return (
40
+ <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
41
+ <h3 className="text-lg font-medium text-gray-900 mb-4">KYC Verification Status</h3>
42
+
43
+ <div className="mb-6">
44
+ <div className="flex items-center gap-2 mb-2">
45
+ <span className="text-sm text-gray-600">Status:</span>
46
+ <span className={`px-3 py-1 rounded-full text-sm font-medium ${status.status === 'verified' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
47
+ }`}>
48
+ {status.status}
49
+ </span>
50
+ </div>
51
+ <div className="flex items-center gap-2">
52
+ <span className="text-sm text-gray-600">Risk Level:</span>
53
+ <span className={`px-3 py-1 rounded-full text-sm font-medium ${status.riskLevel === 'low' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
54
+ }`}>
55
+ {status.riskLevel}
56
+ </span>
57
+ </div>
58
+ </div>
59
+
60
+ <div>
61
+ <h4 className="font-medium text-gray-900 mb-3">Documents</h4>
62
+ <div className="space-y-2">
63
+ {status.documents.map((doc, index) => (
64
+ <div key={index} className="flex justify-between items-center p-3 bg-gray-50 rounded">
65
+ <span className="text-sm text-gray-900">{doc.type}</span>
66
+ <span className={`px-2 py-1 rounded text-xs font-medium ${doc.status === 'approved' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
67
+ }`}>
68
+ {doc.status}
69
+ </span>
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ );
76
+ };