landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
This commit is contained in:
@@ -8,6 +8,7 @@ import HealthDashboard from './pages/HealthDashboard';
|
||||
import PilotChecklist from './pages/PilotChecklist';
|
||||
import ReadinessDashboard from './pages/ReadinessDashboard';
|
||||
import KillList from './pages/KillList';
|
||||
import GovernmentPortal from './pages/GovernmentPortal';
|
||||
|
||||
function App() {
|
||||
const [developerMode, setDeveloperMode] = useState(false);
|
||||
@@ -68,6 +69,7 @@ function App() {
|
||||
<Link to="/pilot" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>✅ Pilot</Link>
|
||||
<Link to="/readiness" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>📋 Ready</Link>
|
||||
<Link to="/kill" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>🔥 Kill</Link>
|
||||
<Link to="/government" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#333' }}>🏛️ Gov</Link>
|
||||
{developerMode && (
|
||||
<>
|
||||
<Link to="/datasets" onClick={() => setMenuOpen(false)} style={{ padding: '8px 0', textDecoration: 'none', color: '#666' }}>🗂️ Data</Link>
|
||||
@@ -88,6 +90,7 @@ function App() {
|
||||
<Route path="/pilot" element={<PilotChecklist />} />
|
||||
<Route path="/readiness" element={<ReadinessDashboard />} />
|
||||
<Route path="/kill" element={<KillList />} />
|
||||
<Route path="/government" element={<GovernmentPortal />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface GovernmentProfile {
|
||||
name: string;
|
||||
orgNumber: string;
|
||||
address: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
}
|
||||
|
||||
interface BudgetOverview {
|
||||
monthlyLimit: number;
|
||||
yearlyLimit: number;
|
||||
currentSpend: number;
|
||||
forecast: number;
|
||||
alerts: string[];
|
||||
}
|
||||
|
||||
interface PaymentSettings {
|
||||
method: 'card' | 'invoice' | 'autogiro';
|
||||
cardLast4?: string;
|
||||
invoiceAddress?: string;
|
||||
invoiceReference?: string;
|
||||
}
|
||||
|
||||
const API_BASE = (import.meta as any).env?.VITE_API_URL || 'https://pilot.landvex.com';
|
||||
|
||||
export default function GovernmentPortal() {
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'budget' | 'payment' | 'areas' | 'users'>('profile');
|
||||
const [profile, setProfile] = useState<GovernmentProfile | null>(null);
|
||||
const [budget, setBudget] = useState<BudgetOverview | null>(null);
|
||||
const [payment, setPayment] = useState<PaymentSettings>({ method: 'invoice' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile();
|
||||
}, []);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/government/me`);
|
||||
if (!res.ok) throw new Error('Failed to load profile');
|
||||
const data = await res.json();
|
||||
setProfile(data);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBudget = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/government/budget`);
|
||||
if (!res.ok) throw new Error('Failed to load budget');
|
||||
const data = await res.json();
|
||||
setBudget(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
const updatePayment = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/government/payment`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payment),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update payment');
|
||||
alert('Payment settings updated');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ padding: 40, textAlign: 'center' }}>Loading...</div>;
|
||||
if (error) return <div style={{ padding: 40, color: '#ef4444' }}>Error: {error}</div>;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'profile' as const, label: 'Profile' },
|
||||
{ id: 'budget' as const, label: 'Budget' },
|
||||
{ id: 'payment' as const, label: 'Payment' },
|
||||
{ id: 'areas' as const, label: 'Watch Areas' },
|
||||
{ id: 'users' as const, label: 'Users' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>
|
||||
<h1 style={{ fontSize: 28, marginBottom: 8 }}>Government Portal</h1>
|
||||
<p style={{ color: '#64748b', marginBottom: 24 }}>
|
||||
Manage your organization's settings, budget, and users
|
||||
</p>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', gap: 4, marginBottom: 24, borderBottom: '1px solid #e2e8f0' }}>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.id);
|
||||
if (tab.id === 'budget') fetchBudget();
|
||||
}}
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
borderBottom: activeTab === tab.id ? '2px solid #3b82f6' : '2px solid transparent',
|
||||
color: activeTab === tab.id ? '#3b82f6' : '#64748b',
|
||||
fontWeight: activeTab === tab.id ? 600 : 400,
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Profile Tab */}
|
||||
{activeTab === 'profile' && profile && (
|
||||
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Organization Profile</h2>
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Organization Name</label>
|
||||
<div style={{ fontSize: 16, fontWeight: 600 }}>{profile.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Organization Number</label>
|
||||
<div style={{ fontSize: 16, fontFamily: 'monospace' }}>{profile.orgNumber}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Address</label>
|
||||
<div>{profile.address}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Contact Email</label>
|
||||
<div>{profile.contactEmail}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Contact Phone</label>
|
||||
<div>{profile.contactPhone}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Budget Tab */}
|
||||
{activeTab === 'budget' && (
|
||||
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Budget Overview</h2>
|
||||
{budget ? (
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div style={{ background: '#fff', padding: 16, borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#64748b' }}>Monthly Limit</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700 }}>${budget.monthlyLimit.toLocaleString()}</div>
|
||||
</div>
|
||||
<div style={{ background: '#fff', padding: 16, borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#64748b' }}>Yearly Limit</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700 }}>${budget.yearlyLimit.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ background: '#fff', padding: 16, borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#64748b' }}>Current Spend</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700, color: budget.currentSpend > budget.monthlyLimit * 0.8 ? '#ef4444' : '#22c55e' }}>
|
||||
${budget.currentSpend.toLocaleString()}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, height: 8, background: '#e2e8f0', borderRadius: 4 }}>
|
||||
<div style={{
|
||||
width: `${Math.min((budget.currentSpend / budget.monthlyLimit) * 100, 100)}%`,
|
||||
height: '100%',
|
||||
background: budget.currentSpend > budget.monthlyLimit * 0.8 ? '#ef4444' : '#3b82f6',
|
||||
borderRadius: 4,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
{budget.alerts.length > 0 && (
|
||||
<div style={{ background: '#fef3c7', padding: 16, borderRadius: 8 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Alerts</div>
|
||||
{budget.alerts.map((alert, i) => (
|
||||
<div key={i} style={{ fontSize: 14, color: '#92400e' }}>{alert}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>Loading budget...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Tab */}
|
||||
{activeTab === 'payment' && (
|
||||
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Payment Settings</h2>
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Payment Method</label>
|
||||
<select
|
||||
value={payment.method}
|
||||
onChange={e => setPayment({ ...payment, method: e.target.value as any })}
|
||||
style={{ width: '100%', padding: 10, borderRadius: 6, border: '1px solid #e2e8f0' }}
|
||||
>
|
||||
<option value="card">Credit Card</option>
|
||||
<option value="invoice">Invoice</option>
|
||||
<option value="autogiro">Autogiro</option>
|
||||
</select>
|
||||
</div>
|
||||
{payment.method === 'invoice' && (
|
||||
<>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Invoice Address</label>
|
||||
<textarea
|
||||
value={payment.invoiceAddress || ''}
|
||||
onChange={e => setPayment({ ...payment, invoiceAddress: e.target.value })}
|
||||
style={{ width: '100%', padding: 10, borderRadius: 6, border: '1px solid #e2e8f0', minHeight: 80 }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12, color: '#64748b', marginBottom: 4 }}>Invoice Reference</label>
|
||||
<input
|
||||
type="text"
|
||||
value={payment.invoiceReference || ''}
|
||||
onChange={e => setPayment({ ...payment, invoiceReference: e.target.value })}
|
||||
style={{ width: '100%', padding: 10, borderRadius: 6, border: '1px solid #e2e8f0' }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={updatePayment}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
background: '#3b82f6',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Save Payment Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Watch Areas Tab */}
|
||||
{activeTab === 'areas' && (
|
||||
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Watch Areas</h2>
|
||||
<p style={{ color: '#64748b' }}>Define geographic areas for automatic mission monitoring</p>
|
||||
<div style={{ marginTop: 16, padding: 40, textAlign: 'center', background: '#fff', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="1 6 1 22 8 18 16 22 21 18 21 2 16 6 8 2 1 6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="18"/>
|
||||
<line x1="16" y1="6" x2="16" y2="22"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Map integration coming soon</p>
|
||||
<p style={{ fontSize: 12, color: '#64748b' }}>Draw polygons on map to define watch areas</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Tab */}
|
||||
{activeTab === 'users' && (
|
||||
<div style={{ background: '#f8fafc', padding: 24, borderRadius: 12 }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Users & Permissions</h2>
|
||||
<p style={{ color: '#64748b' }}>Manage team members and their access levels</p>
|
||||
<div style={{ marginTop: 16, padding: 40, textAlign: 'center', background: '#fff', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p>User management coming soon</p>
|
||||
<p style={{ fontSize: 12, color: '#64748b' }}>Invite users, assign roles, manage permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user