Initial commit: ZiRA Classification Tool for Zuyderland CMDB
This commit is contained in:
682
frontend/src/components/ApplicationList.tsx
Normal file
682
frontend/src/components/ApplicationList.tsx
Normal file
@@ -0,0 +1,682 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import { clsx } from 'clsx';
|
||||
import { searchApplications, getReferenceData } from '../services/api';
|
||||
import { useSearchStore } from '../stores/searchStore';
|
||||
import { useNavigationStore } from '../stores/navigationStore';
|
||||
import type { ApplicationListItem, SearchResult, ReferenceValue, ApplicationStatus } from '../types';
|
||||
|
||||
const ALL_STATUSES: ApplicationStatus[] = [
|
||||
'In Production',
|
||||
'Implementation',
|
||||
'Proof of Concept',
|
||||
'End of support',
|
||||
'End of life',
|
||||
'Deprecated',
|
||||
'Shadow IT',
|
||||
'Closed',
|
||||
'Undefined',
|
||||
];
|
||||
|
||||
export default function ApplicationList() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const {
|
||||
filters,
|
||||
currentPage,
|
||||
pageSize,
|
||||
setSearchText,
|
||||
setStatuses,
|
||||
setApplicationFunction,
|
||||
setGovernanceModel,
|
||||
setApplicationCluster,
|
||||
setApplicationType,
|
||||
setOrganisation,
|
||||
setHostingType,
|
||||
setBusinessImportance,
|
||||
setCurrentPage,
|
||||
resetFilters,
|
||||
} = useSearchStore();
|
||||
const { setNavigationContext } = useNavigationStore();
|
||||
|
||||
const [result, setResult] = useState<SearchResult | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [organisations, setOrganisations] = useState<ReferenceValue[]>([]);
|
||||
const [hostingTypes, setHostingTypes] = useState<ReferenceValue[]>([]);
|
||||
const [businessImportanceOptions, setBusinessImportanceOptions] = useState<ReferenceValue[]>([]);
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
// Sync URL params with store on mount
|
||||
useEffect(() => {
|
||||
const pageParam = searchParams.get('page');
|
||||
if (pageParam) {
|
||||
const page = parseInt(pageParam, 10);
|
||||
if (!isNaN(page) && page > 0 && page !== currentPage) {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
}
|
||||
}, []); // Only run on mount
|
||||
|
||||
// Update URL when page changes
|
||||
useEffect(() => {
|
||||
const currentUrlPage = searchParams.get('page');
|
||||
const currentUrlPageNum = currentUrlPage ? parseInt(currentUrlPage, 10) : 1;
|
||||
|
||||
if (currentPage !== currentUrlPageNum) {
|
||||
if (currentPage === 1) {
|
||||
// Remove page param when on page 1
|
||||
searchParams.delete('page');
|
||||
} else {
|
||||
searchParams.set('page', currentPage.toString());
|
||||
}
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
}
|
||||
}, [currentPage, searchParams, setSearchParams]);
|
||||
|
||||
const fetchApplications = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await searchApplications(filters, currentPage, pageSize);
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load applications');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters, currentPage, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApplications();
|
||||
}, [fetchApplications]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadReferenceData() {
|
||||
try {
|
||||
const data = await getReferenceData();
|
||||
setOrganisations(data.organisations);
|
||||
setHostingTypes(data.hostingTypes);
|
||||
setBusinessImportanceOptions(data.businessImportance || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load reference data', err);
|
||||
}
|
||||
}
|
||||
loadReferenceData();
|
||||
}, []);
|
||||
|
||||
// Update navigation context whenever results change, so "Opslaan & Volgende" works
|
||||
// even when user opens an application in a new tab
|
||||
useEffect(() => {
|
||||
if (result && result.applications.length > 0) {
|
||||
const allIds = result.applications.map((a) => a.id);
|
||||
// Preserve current index if it's still valid, otherwise reset to 0
|
||||
setNavigationContext(allIds, filters, 0);
|
||||
}
|
||||
}, [result, filters, setNavigationContext]);
|
||||
|
||||
const handleRowClick = (app: ApplicationListItem, index: number, event: React.MouseEvent) => {
|
||||
// Update current index in navigation context
|
||||
if (result) {
|
||||
const allIds = result.applications.map((a) => a.id);
|
||||
setNavigationContext(allIds, filters, index);
|
||||
}
|
||||
|
||||
// Let the browser handle CTRL+click / CMD+click / middle-click natively for new tab
|
||||
// Only navigate programmatically for regular clicks
|
||||
if (!event.ctrlKey && !event.metaKey && !event.shiftKey && event.button === 0) {
|
||||
event.preventDefault();
|
||||
navigate(`/applications/${app.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleStatus = (status: ApplicationStatus) => {
|
||||
const current = filters.statuses || [];
|
||||
if (current.includes(status)) {
|
||||
setStatuses(current.filter((s) => s !== status));
|
||||
} else {
|
||||
setStatuses([...current, status]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Applicaties</h2>
|
||||
<p className="text-gray-600">Zoek en classificeer applicatiecomponenten</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{showFilters ? 'Verberg filters' : 'Toon filters'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search and filters */}
|
||||
<div className="card">
|
||||
{/* Search bar */}
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Zoeken op naam, beschrijving, leverancier..."
|
||||
value={filters.searchText || ''}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<svg
|
||||
className="absolute left-3 top-2.5 h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
{showFilters && (
|
||||
<div className="p-4 bg-gray-50 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-medium text-gray-900">Filters</h3>
|
||||
<button
|
||||
onClick={resetFilters}
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Wis alle filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Status filter */}
|
||||
<div>
|
||||
<label className="label mb-2">Status</label>
|
||||
<div className="space-y-1 max-h-48 overflow-y-auto">
|
||||
{ALL_STATUSES.map((status) => (
|
||||
<label key={status} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(filters.statuses || []).includes(status)}
|
||||
onChange={() => toggleStatus(status)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{status}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Classification filters */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="label mb-2">ApplicationFunction</label>
|
||||
<div className="space-y-1">
|
||||
{(['all', 'filled', 'empty'] as const).map((value) => (
|
||||
<label key={value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="applicationFunction"
|
||||
checked={filters.applicationFunction === value}
|
||||
onChange={() => setApplicationFunction(value)}
|
||||
className="border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{value === 'all' ? 'Alle' : value === 'filled' ? 'Ingevuld' : 'Leeg'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label mb-2">Governance Model</label>
|
||||
<div className="space-y-1">
|
||||
{(['all', 'filled', 'empty'] as const).map((value) => (
|
||||
<label key={value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="governanceModel"
|
||||
checked={filters.governanceModel === value}
|
||||
onChange={() => setGovernanceModel(value)}
|
||||
className="border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{value === 'all' ? 'Alle' : value === 'filled' ? 'Ingevuld' : 'Leeg'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label mb-2">Application Cluster</label>
|
||||
<div className="space-y-1">
|
||||
{(['all', 'filled', 'empty'] as const).map((value) => (
|
||||
<label key={value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="applicationCluster"
|
||||
checked={filters.applicationCluster === value}
|
||||
onChange={() => setApplicationCluster(value)}
|
||||
className="border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{value === 'all' ? 'Alle' : value === 'filled' ? 'Ingevuld' : 'Leeg'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label mb-2">Application Type</label>
|
||||
<div className="space-y-1">
|
||||
{(['all', 'filled', 'empty'] as const).map((value) => (
|
||||
<label key={value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="applicationType"
|
||||
checked={filters.applicationType === value}
|
||||
onChange={() => setApplicationType(value)}
|
||||
className="border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{value === 'all' ? 'Alle' : value === 'filled' ? 'Ingevuld' : 'Leeg'}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown filters */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="label mb-2">Organisatie</label>
|
||||
<select
|
||||
value={filters.organisation || ''}
|
||||
onChange={(e) => setOrganisation(e.target.value || undefined)}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Alle organisaties</option>
|
||||
{organisations.map((org) => (
|
||||
<option key={org.objectId} value={org.name}>
|
||||
{org.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label mb-2">Hosting Type</label>
|
||||
<select
|
||||
value={filters.hostingType || ''}
|
||||
onChange={(e) => setHostingType(e.target.value || undefined)}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Alle types</option>
|
||||
{hostingTypes.map((type) => (
|
||||
<option key={type.objectId} value={type.name}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label mb-2">Business Importance</label>
|
||||
<select
|
||||
value={filters.businessImportance || ''}
|
||||
onChange={(e) => setBusinessImportance(e.target.value || undefined)}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Alle</option>
|
||||
{businessImportanceOptions.map((importance) => (
|
||||
<option key={importance.objectId} value={importance.name}>
|
||||
{importance.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results count */}
|
||||
<div className="px-4 py-3 bg-white border-b border-gray-200 flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">
|
||||
{result ? (
|
||||
<>
|
||||
Resultaten: <strong>{result.totalCount}</strong> applicaties
|
||||
</>
|
||||
) : (
|
||||
'Laden...'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Results table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-4 text-red-600">{error}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
#
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Naam
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
AppFunctie
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Governance
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Benodigde inspanning
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{result?.applications.map((app, index) => (
|
||||
<tr
|
||||
key={app.id}
|
||||
className="hover:bg-blue-50 transition-colors group"
|
||||
>
|
||||
<td className="py-0">
|
||||
<Link
|
||||
to={`/applications/${app.id}`}
|
||||
onClick={(e) => handleRowClick(app, index, e)}
|
||||
className="block px-4 py-3 text-sm text-gray-500"
|
||||
>
|
||||
{(currentPage - 1) * pageSize + index + 1}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-0">
|
||||
<Link
|
||||
to={`/applications/${app.id}`}
|
||||
onClick={(e) => handleRowClick(app, index, e)}
|
||||
className="block px-4 py-3"
|
||||
>
|
||||
<div className="text-sm font-medium text-blue-600 group-hover:text-blue-800 group-hover:underline">
|
||||
{app.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{app.key}</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-0">
|
||||
<Link
|
||||
to={`/applications/${app.id}`}
|
||||
onClick={(e) => handleRowClick(app, index, e)}
|
||||
className="block px-4 py-3"
|
||||
>
|
||||
<StatusBadge status={app.status} />
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-0">
|
||||
<Link
|
||||
to={`/applications/${app.id}`}
|
||||
onClick={(e) => handleRowClick(app, index, e)}
|
||||
className="block px-4 py-3"
|
||||
>
|
||||
{app.applicationFunctions && app.applicationFunctions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{app.applicationFunctions.map((func) => (
|
||||
<span
|
||||
key={func.objectId}
|
||||
className="inline-block px-2 py-0.5 text-xs bg-blue-100 text-blue-800 rounded"
|
||||
title={func.description || func.name}
|
||||
>
|
||||
{func.name}{func.applicationFunctionCategory ? ` (${func.applicationFunctionCategory.name})` : ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-orange-600 font-medium">
|
||||
Leeg
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-0">
|
||||
<Link
|
||||
to={`/applications/${app.id}`}
|
||||
onClick={(e) => handleRowClick(app, index, e)}
|
||||
className="block px-4 py-3"
|
||||
>
|
||||
{app.governanceModel ? (
|
||||
<span className="text-sm text-gray-900">
|
||||
{app.governanceModel.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-orange-600 font-medium">
|
||||
Leeg
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-0">
|
||||
<Link
|
||||
to={`/applications/${app.id}`}
|
||||
onClick={(e) => handleRowClick(app, index, e)}
|
||||
className="block px-4 py-3 text-sm text-gray-900"
|
||||
>
|
||||
{app.requiredEffortApplicationManagement !== null ? (
|
||||
<span className="font-medium">
|
||||
{app.requiredEffortApplicationManagement.toFixed(2)} FTE
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{result && result.totalPages > 1 && (
|
||||
<div className="px-4 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
|
||||
{currentPage > 1 ? (
|
||||
<Link
|
||||
to={currentPage === 2 ? '/applications' : `/applications?page=${currentPage - 1}`}
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Vorige
|
||||
</Link>
|
||||
) : (
|
||||
<button disabled className="btn btn-secondary opacity-50 cursor-not-allowed">
|
||||
Vorige
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-600">
|
||||
Pagina {currentPage} van {result.totalPages}
|
||||
</span>
|
||||
{currentPage < result.totalPages ? (
|
||||
<Link
|
||||
to={`/applications?page=${currentPage + 1}`}
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Volgende
|
||||
</Link>
|
||||
) : (
|
||||
<button disabled className="btn btn-secondary opacity-50 cursor-not-allowed">
|
||||
Volgende
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatusBadge({ status }: { status: string | null }) {
|
||||
const statusColors: Record<string, string> = {
|
||||
'Closed': 'badge-dark-red',
|
||||
'Deprecated': 'badge-yellow',
|
||||
'End of life': 'badge-light-red',
|
||||
'End of support': 'badge-light-red',
|
||||
'Implementation': 'badge-blue',
|
||||
'In Production': 'badge-dark-green',
|
||||
'Proof of Concept': 'badge-light-green',
|
||||
'Shadow IT': 'badge-black',
|
||||
'Undefined': 'badge-gray',
|
||||
};
|
||||
|
||||
if (!status) return <span className="text-sm text-gray-400">-</span>;
|
||||
|
||||
return (
|
||||
<span className={clsx('badge', statusColors[status] || 'badge-gray')}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessImportanceBadge({ importance }: { importance: string | null }) {
|
||||
// Helper function to get the number prefix from the importance string
|
||||
const getImportanceNumber = (value: string | null): string | null => {
|
||||
if (!value) return null;
|
||||
// Match patterns like "0 - Critical Infrastructure" or just "0"
|
||||
const match = value.match(/^(\d+)/);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const importanceNumber = getImportanceNumber(importance);
|
||||
|
||||
// Map importance number to icon type and color
|
||||
const getImportanceConfig = (num: string | null) => {
|
||||
switch (num) {
|
||||
case '0':
|
||||
return {
|
||||
icon: 'warning',
|
||||
color: 'badge-darker-red',
|
||||
label: importance || '0 - Critical Infrastructure',
|
||||
};
|
||||
case '1':
|
||||
return {
|
||||
icon: 'exclamation',
|
||||
color: 'badge-dark-red',
|
||||
label: importance || '1 - Critical',
|
||||
};
|
||||
case '2':
|
||||
return {
|
||||
icon: 'exclamation',
|
||||
color: 'badge-red',
|
||||
label: importance || '2 - Highest',
|
||||
};
|
||||
case '3':
|
||||
return {
|
||||
icon: 'circle',
|
||||
color: 'badge-yellow-orange',
|
||||
label: importance || '3 - High',
|
||||
};
|
||||
case '4':
|
||||
return {
|
||||
icon: 'circle',
|
||||
color: 'badge-dark-blue',
|
||||
label: importance || '4 - Medium',
|
||||
};
|
||||
case '5':
|
||||
return {
|
||||
icon: 'circle',
|
||||
color: 'badge-light-blue',
|
||||
label: importance || '5 - Low',
|
||||
};
|
||||
case '6':
|
||||
return {
|
||||
icon: 'circle',
|
||||
color: 'badge-lighter-blue',
|
||||
label: importance || '6 - Lowest',
|
||||
};
|
||||
case '9':
|
||||
return {
|
||||
icon: 'question',
|
||||
color: 'badge-black',
|
||||
label: importance || '9 - Unknown',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: null,
|
||||
color: 'badge-gray',
|
||||
label: importance || '-',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (!importance) return <span className="text-sm text-gray-400">-</span>;
|
||||
|
||||
const config = getImportanceConfig(importanceNumber);
|
||||
|
||||
// Icon components
|
||||
const WarningIcon = () => (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ExclamationIcon = () => (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CircleIcon = () => (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="10" r="8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const QuestionIcon = () => (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const renderIcon = () => {
|
||||
switch (config.icon) {
|
||||
case 'warning':
|
||||
return <WarningIcon />;
|
||||
case 'exclamation':
|
||||
return <ExclamationIcon />;
|
||||
case 'circle':
|
||||
return <CircleIcon />;
|
||||
case 'question':
|
||||
return <QuestionIcon />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={clsx('badge inline-flex items-center gap-1.5', config.color)}>
|
||||
{renderIcon()}
|
||||
<span>{config.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user