mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Replace all 'any' types with proper TypeScript interfaces - Fix React hooks setState in useEffect issues with lazy initialization - Remove unused variables and imports across all files - Fix React Compiler memoization dependency issues - Add comprehensive i18n translation keys for admin interfaces - Apply consistent prettier formatting throughout codebase - Clean up unused bulk editing functionality - Improve type safety and code quality across frontend Files changed: 39 - ImpactMetrics.tsx: Fixed any types and interfaces - AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars - LocalizationUIPage.tsx: Fixed memoization, added translations - LocalizationDataPage.tsx: Added type safety and translations - And 35+ other files with various lint fixes
537 lines
19 KiB
TypeScript
537 lines
19 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import {
|
|
AlertTriangle,
|
|
ArrowDown,
|
|
ArrowUp,
|
|
BarChart3,
|
|
CheckCircle,
|
|
Filter,
|
|
Minus,
|
|
Target,
|
|
TrendingDown,
|
|
TrendingUp,
|
|
XCircle,
|
|
} from 'lucide-react';
|
|
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
|
import PageHeader from '@/components/layout/PageHeader.tsx';
|
|
import Badge from '@/components/ui/Badge.tsx';
|
|
import Button from '@/components/ui/Button.tsx';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
|
|
import { Container, Flex, Grid, Stack } from '@/components/ui/layout';
|
|
import Select from '@/components/ui/Select.tsx';
|
|
import Spinner from '@/components/ui/Spinner.tsx';
|
|
import { useSupplyDemandAnalysis } from '@/hooks/api/useAnalyticsAPI.ts';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import type { SupplyDemandAnalysis, ItemCount } from '@/services/analytics-api.ts';
|
|
import { useNavigation } from '@/hooks/useNavigation.tsx';
|
|
|
|
interface ResourceAnalysisItem {
|
|
resource: string;
|
|
sector: string;
|
|
demand: number;
|
|
supply: number;
|
|
gap: number;
|
|
gapPercentage: number;
|
|
status: string;
|
|
}
|
|
|
|
const SupplyDemandAnalysis = () => {
|
|
const { t } = useTranslation();
|
|
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
|
|
|
const { data: supplyDemandData, isLoading } = useSupplyDemandAnalysis();
|
|
const [selectedSector, setSelectedSector] = useState<string>('all');
|
|
const [sortBy, setSortBy] = useState<string>('gap');
|
|
|
|
// Process supply/demand data
|
|
const analysis = useMemo(() => {
|
|
const data: SupplyDemandAnalysis = supplyDemandData || {
|
|
top_needs: [],
|
|
top_offers: [],
|
|
};
|
|
|
|
const topNeeds = data.top_needs || [];
|
|
const topOffers = data.top_offers || [];
|
|
const marketGaps: ItemCount[] = []; // TODO: Add market_gaps to schema if needed
|
|
|
|
// Create combined analysis
|
|
const resourceAnalysis = new Map();
|
|
|
|
// Process needs
|
|
topNeeds.forEach((need: ItemCount) => {
|
|
if (!resourceAnalysis.has(need.item)) {
|
|
resourceAnalysis.set(need.item, {
|
|
resource: need.item,
|
|
sector: need.sector,
|
|
demand: 0,
|
|
supply: 0,
|
|
gap: 0,
|
|
gapPercentage: 0,
|
|
status: 'unknown',
|
|
});
|
|
}
|
|
resourceAnalysis.get(need.item).demand = need.count;
|
|
});
|
|
|
|
// Process offers
|
|
topOffers.forEach((offer: ItemCount) => {
|
|
if (!resourceAnalysis.has(offer.item)) {
|
|
resourceAnalysis.set(offer.item, {
|
|
resource: offer.item,
|
|
sector: offer.sector,
|
|
demand: 0,
|
|
supply: 0,
|
|
gap: 0,
|
|
gapPercentage: 0,
|
|
status: 'unknown',
|
|
});
|
|
}
|
|
resourceAnalysis.get(offer.item).supply = offer.count;
|
|
});
|
|
|
|
// Calculate gaps and status
|
|
const analysisArray = Array.from(resourceAnalysis.values()).map(
|
|
(item: ResourceAnalysisItem) => {
|
|
const gap = item.supply - item.demand;
|
|
const total = item.supply + item.demand;
|
|
const gapPercentage = total > 0 ? (gap / total) * 100 : 0;
|
|
|
|
let status = 'balanced';
|
|
if (gap > 10) status = 'surplus';
|
|
else if (gap < -10) status = 'shortage';
|
|
|
|
return {
|
|
...item,
|
|
gap,
|
|
gapPercentage: Math.abs(gapPercentage),
|
|
status,
|
|
};
|
|
}
|
|
);
|
|
|
|
// Filter by sector
|
|
const filteredAnalysis =
|
|
selectedSector === 'all'
|
|
? analysisArray
|
|
: analysisArray.filter((item: ResourceAnalysisItem) => item.sector === selectedSector);
|
|
|
|
// Sort
|
|
const sortedAnalysis = filteredAnalysis.sort(
|
|
(a: ResourceAnalysisItem, b: ResourceAnalysisItem) => {
|
|
switch (sortBy) {
|
|
case 'gap':
|
|
return Math.abs(b.gap) - Math.abs(a.gap);
|
|
case 'demand':
|
|
return b.demand - a.demand;
|
|
case 'supply':
|
|
return b.supply - a.supply;
|
|
case 'resource':
|
|
return a.resource.localeCompare(b.resource);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get unique sectors
|
|
const sectors = Array.from(
|
|
new Set(analysisArray.map((item: ResourceAnalysisItem) => item.sector))
|
|
);
|
|
|
|
return {
|
|
analysis: sortedAnalysis,
|
|
sectors,
|
|
marketGaps,
|
|
summary: {
|
|
totalResources: analysisArray.length,
|
|
surplusCount: analysisArray.filter(
|
|
(item: ResourceAnalysisItem) => item.status === 'surplus'
|
|
).length,
|
|
shortageCount: analysisArray.filter(
|
|
(item: ResourceAnalysisItem) => item.status === 'shortage'
|
|
).length,
|
|
balancedCount: analysisArray.filter(
|
|
(item: ResourceAnalysisItem) => item.status === 'balanced'
|
|
).length,
|
|
},
|
|
};
|
|
}, [supplyDemandData, selectedSector, sortBy]);
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'surplus':
|
|
return <ArrowUp className="h-4 text-current text-green-600 w-4" />;
|
|
case 'shortage':
|
|
return <ArrowDown className="h-4 text-current text-red-600 w-4" />;
|
|
case 'balanced':
|
|
return <Minus className="h-4 text-blue-600 text-current w-4" />;
|
|
default:
|
|
return <Target className="h-4 text-current text-muted-foreground w-4" />;
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'surplus':
|
|
return 'text-green-700 bg-green-50 border-green-200';
|
|
case 'shortage':
|
|
return 'text-red-700 bg-red-50 border-red-200';
|
|
case 'balanced':
|
|
return 'text-blue-700 bg-blue-50 border-blue-200';
|
|
default:
|
|
return 'text-muted-foreground bg-muted/50 border-muted';
|
|
}
|
|
};
|
|
|
|
const getStatusBadgeVariant = (status: string) => {
|
|
switch (status) {
|
|
case 'surplus':
|
|
return 'default';
|
|
case 'shortage':
|
|
return 'destructive';
|
|
case 'balanced':
|
|
return 'secondary';
|
|
default:
|
|
return 'outline';
|
|
}
|
|
};
|
|
|
|
// Simple bar chart for supply vs demand
|
|
const SupplyDemandBar = ({ supply, demand }: { supply: number; demand: number }) => {
|
|
const maxValue = Math.max(supply, demand, 1);
|
|
const supplyPercent = (supply / maxValue) * 100;
|
|
const demandPercent = (demand / maxValue) * 100;
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-green-600">
|
|
{t('common.supply')}: {supply}
|
|
</span>
|
|
<span className="text-red-600">
|
|
{t('common.demand')}: {demand}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-1 h-4">
|
|
<div
|
|
className="bg-green-500 rounded-l transition-all duration-500"
|
|
style={{ width: `${supplyPercent}%` }}
|
|
/>
|
|
<div
|
|
className="bg-red-500 rounded-r transition-all duration-500"
|
|
style={{ width: `${demandPercent}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const sectorOptions = [
|
|
{ value: 'all', label: t('supplyDemand.allSectors') },
|
|
...analysis.sectors.map((sector) => ({ value: sector, label: sector })),
|
|
];
|
|
|
|
const sortOptions = [
|
|
{ value: 'gap', label: t('supplyDemand.sortByGap') },
|
|
{ value: 'demand', label: t('supplyDemand.sortByDemand') },
|
|
{ value: 'supply', label: t('supplyDemand.sortBySupply') },
|
|
{ value: 'resource', label: t('supplyDemand.sortByResource') },
|
|
];
|
|
|
|
return (
|
|
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
|
<Container size="2xl" className="py-8 sm:py-12">
|
|
<PageHeader
|
|
title={t('supplyDemand.title')}
|
|
subtitle={t('supplyDemand.subtitle')}
|
|
onBack={handleBackNavigation}
|
|
/>
|
|
|
|
<Stack spacing="2xl">
|
|
{/* Summary Cards */}
|
|
<Grid cols={{ md: 2, lg: 4 }} gap="md">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<Flex align="center" gap="md" className="mb-4">
|
|
<div className="p-3 rounded-full bg-blue-100">
|
|
<BarChart3 className="h-4 h-6 text-blue-600 text-current w-4 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-blue-700">
|
|
{t('supplyDemand.totalResources')}
|
|
</p>
|
|
<p className="text-2xl font-bold text-blue-800">
|
|
{analysis.summary.totalResources}
|
|
</p>
|
|
</div>
|
|
</Flex>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<Flex align="center" gap="md" className="mb-4">
|
|
<div className="p-3 rounded-full bg-green-100">
|
|
<TrendingUp className="h-4 h-6 text-current text-green-600 w-4 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-green-700">
|
|
{t('supplyDemand.surplusResources')}
|
|
</p>
|
|
<p className="text-2xl font-bold text-green-800">
|
|
{analysis.summary.surplusCount}
|
|
</p>
|
|
</div>
|
|
</Flex>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<Flex align="center" gap="md" className="mb-4">
|
|
<div className="p-3 rounded-full bg-red-100">
|
|
<TrendingDown className="h-4 h-6 text-current text-red-600 w-4 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-red-700">
|
|
{t('supplyDemand.shortageResources')}
|
|
</p>
|
|
<p className="text-2xl font-bold text-red-800">
|
|
{analysis.summary.shortageCount}
|
|
</p>
|
|
</div>
|
|
</Flex>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<Flex align="center" gap="md" className="mb-4">
|
|
<div className="p-3 rounded-full bg-blue-100">
|
|
<CheckCircle className="h-4 h-6 text-blue-600 text-current w-4 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-blue-700">
|
|
{t('supplyDemand.balancedResources')}
|
|
</p>
|
|
<p className="text-2xl font-bold text-blue-800">
|
|
{analysis.summary.balancedCount}
|
|
</p>
|
|
</div>
|
|
</Flex>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Filters */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Filter className="h-4 h-5 text-current w-4 w-5" />
|
|
{t('supplyDemand.filters')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ md: 2 }} gap="md">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('supplyDemand.sector')}
|
|
</label>
|
|
<Select
|
|
value={selectedSector}
|
|
onValueChange={setSelectedSector}
|
|
options={sectorOptions}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('supplyDemand.sortBy')}
|
|
</label>
|
|
<Select value={sortBy} onValueChange={setSortBy} options={sortOptions} />
|
|
</div>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Supply/Demand Analysis Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('supplyDemand.resourceAnalysis')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Spinner className="h-8 w-8" />
|
|
</div>
|
|
) : analysis.analysis.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{analysis.analysis.map((item: ResourceAnalysisItem, index: number) => (
|
|
<div
|
|
key={index}
|
|
className={`p-4 border rounded-lg ${getStatusColor(item.status)}`}
|
|
>
|
|
<Flex align="start" justify="between" className="mb-3">
|
|
<div className="flex-1">
|
|
<Flex align="center" gap="sm" className="mb-2">
|
|
{getStatusIcon(item.status)}
|
|
<h4 className="font-semibold">{item.resource}</h4>
|
|
<Badge variant={getStatusBadgeVariant(item.status)}>
|
|
{item.status}
|
|
</Badge>
|
|
</Flex>
|
|
<p className="text-sm opacity-75">{item.sector}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-sm font-medium">
|
|
{t('common.gap')}: {item.gap > 0 ? '+' : ''}
|
|
{item.gap}
|
|
</div>
|
|
<div className="text-xs opacity-75">
|
|
{item.gapPercentage.toFixed(1)}
|
|
{t('common.percent')} {t('common.imbalance')}
|
|
</div>
|
|
</div>
|
|
</Flex>
|
|
|
|
<SupplyDemandBar supply={item.supply} demand={item.demand} />
|
|
|
|
<Flex gap="md" className="mt-3 text-xs">
|
|
<span>
|
|
{t('common.supply')}: {item.supply}
|
|
</span>
|
|
<span>
|
|
{t('common.demand')}: {item.demand}
|
|
</span>
|
|
<span>
|
|
{t('common.gap')}: {item.gap > 0 ? '+' : ''}
|
|
{item.gap}
|
|
</span>
|
|
</Flex>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<BarChart3 className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
|
|
<p>{t('supplyDemand.noAnalysisData')}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Market Gaps & Opportunities */}
|
|
{analysis.marketGaps.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertTriangle className="h-4 h-5 text-current w-4 w-5" />
|
|
{t('supplyDemand.marketGaps')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{analysis.marketGaps.map((gap: ItemCount, index: number) => (
|
|
<div key={index} className="p-4 border rounded-lg">
|
|
<Flex align="center" gap="sm" className="mb-3">
|
|
<AlertTriangle className="h-4 h-5 text-amber-600 text-current w-4 w-5" />
|
|
<div>
|
|
<h4 className="font-semibold">{gap.resource_type}</h4>
|
|
<p className="text-sm text-muted-foreground">{gap.sector}</p>
|
|
</div>
|
|
</Flex>
|
|
|
|
<p className="text-sm mb-3">{gap.description}</p>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span>{t('supplyDemand.potentialMatches')}:</span>
|
|
<span className="font-medium">{gap.potential_matches}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span>{t('supplyDemand.gapSeverity')}:</span>
|
|
<Badge
|
|
variant={
|
|
gap.severity === 'high'
|
|
? 'destructive'
|
|
: gap.severity === 'medium'
|
|
? 'secondary'
|
|
: 'outline'
|
|
}
|
|
>
|
|
{gap.severity}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-3 border-t">
|
|
<Button size="sm" className="w-full">
|
|
{t('supplyDemand.exploreOpportunities')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Recommendations */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Target className="h-4 h-5 text-current w-4 w-5" />
|
|
{t('supplyDemand.recommendations')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid cols={{ md: 2, lg: 3 }} gap="md">
|
|
{analysis.summary.shortageCount > 0 && (
|
|
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
<Flex align="center" gap="sm" className="mb-2">
|
|
<XCircle className="h-4 h-5 text-current text-red-600 w-4 w-5" />
|
|
<h4 className="font-semibold text-red-800">
|
|
{t('supplyDemand.addressShortages')}
|
|
</h4>
|
|
</Flex>
|
|
<p className="text-sm text-red-700">
|
|
{t('supplyDemand.shortageRecommendation')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{analysis.summary.surplusCount > 0 && (
|
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
<Flex align="center" gap="sm" className="mb-2">
|
|
<CheckCircle className="h-4 h-5 text-current text-green-600 w-4 w-5" />
|
|
<h4 className="font-semibold text-green-800">
|
|
{t('supplyDemand.optimizeSurplus')}
|
|
</h4>
|
|
</Flex>
|
|
<p className="text-sm text-green-700">
|
|
{t('supplyDemand.surplusRecommendation')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<Flex align="center" gap="sm" className="mb-2">
|
|
<BarChart3 className="h-4 h-5 text-blue-600 text-current w-4 w-5" />
|
|
<h4 className="font-semibold text-blue-800">
|
|
{t('supplyDemand.monitorTrends')}
|
|
</h4>
|
|
</Flex>
|
|
<p className="text-sm text-blue-700">
|
|
{t('supplyDemand.monitoringRecommendation')}
|
|
</p>
|
|
</div>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
</Stack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
export default SupplyDemandAnalysis;
|