mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
481 lines
18 KiB
TypeScript
481 lines
18 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 { useNavigation } from '@/hooks/useNavigation.tsx';
|
|
|
|
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 = supplyDemandData || {};
|
|
|
|
const topNeeds = (data as any).top_needs || [];
|
|
const topOffers = (data as any).top_offers || [];
|
|
const marketGaps = (data as any).market_gaps || [];
|
|
|
|
// Create combined analysis
|
|
const resourceAnalysis = new Map();
|
|
|
|
// Process needs
|
|
topNeeds.forEach((need: any) => {
|
|
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: any) => {
|
|
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: any) => {
|
|
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: any) => item.sector === selectedSector);
|
|
|
|
// Sort
|
|
const sortedAnalysis = filteredAnalysis.sort((a: any, b: any) => {
|
|
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: any) => item.sector)));
|
|
|
|
return {
|
|
analysis: sortedAnalysis,
|
|
sectors,
|
|
marketGaps,
|
|
summary: {
|
|
totalResources: analysisArray.length,
|
|
surplusCount: analysisArray.filter((item: any) => item.status === 'surplus').length,
|
|
shortageCount: analysisArray.filter((item: any) => item.status === 'shortage').length,
|
|
balancedCount: analysisArray.filter((item: any) => 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">Supply: {supply}</span>
|
|
<span className="text-red-600">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: any, 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">
|
|
Gap: {item.gap > 0 ? '+' : ''}{item.gap}
|
|
</div>
|
|
<div className="text-xs opacity-75">
|
|
{item.gapPercentage.toFixed(1)}% imbalance
|
|
</div>
|
|
</div>
|
|
</Flex>
|
|
|
|
<SupplyDemandBar supply={item.supply} demand={item.demand} />
|
|
|
|
<Flex gap="md" className="mt-3 text-xs">
|
|
<span>Supply: {item.supply}</span>
|
|
<span>Demand: {item.demand}</span>
|
|
<span>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: any, 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;
|