Improve the frontend and repository layer
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardHeader, CardBody } from '../../ui/Card';
|
||||
import { Badge } from '../../ui/Badge';
|
||||
import { SeverityBadge } from '../../ui/Badge';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { useNotifications } from '../../../hooks/useNotifications';
|
||||
import { useAlertFilters } from '../../../hooks/useAlertFilters';
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
AlertTriangle,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import AlertFilters from './AlertFilters';
|
||||
import AlertGroupHeader from './AlertGroupHeader';
|
||||
@@ -61,6 +63,10 @@ const RealTimeAlerts: React.FC<RealTimeAlertsProps> = ({
|
||||
const [showBulkActions, setShowBulkActions] = useState(false);
|
||||
const [showAnalyticsPanel, setShowAnalyticsPanel] = useState(false);
|
||||
|
||||
// Pagination state
|
||||
const ALERTS_PER_PAGE = 3;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const {
|
||||
notifications,
|
||||
isConnected,
|
||||
@@ -121,6 +127,32 @@ const RealTimeAlerts: React.FC<RealTimeAlertsProps> = ({
|
||||
);
|
||||
}, [groupedAlerts, isGroupCollapsed]);
|
||||
|
||||
// Reset pagination when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [filters, groupingMode]);
|
||||
|
||||
// Pagination calculations
|
||||
const totalAlerts = flatAlerts.length;
|
||||
const totalPages = Math.ceil(totalAlerts / ALERTS_PER_PAGE);
|
||||
const startIndex = (currentPage - 1) * ALERTS_PER_PAGE;
|
||||
const endIndex = startIndex + ALERTS_PER_PAGE;
|
||||
|
||||
// Paginated alerts - slice the flat alerts for current page
|
||||
const paginatedAlerts = useMemo(() => {
|
||||
const alertsToShow = flatAlerts.slice(startIndex, endIndex);
|
||||
const alertIds = new Set(alertsToShow.map(a => a.id));
|
||||
|
||||
// Filter groups to only show alerts on current page
|
||||
return groupedAlerts
|
||||
.map(group => ({
|
||||
...group,
|
||||
alerts: group.alerts.filter(alert => alertIds.has(alert.id)),
|
||||
count: group.alerts.filter(alert => alertIds.has(alert.id)).length,
|
||||
}))
|
||||
.filter(group => group.alerts.length > 0);
|
||||
}, [groupedAlerts, flatAlerts, startIndex, endIndex]);
|
||||
|
||||
const { focusedIndex } = useKeyboardNavigation(
|
||||
flatAlerts.length,
|
||||
{
|
||||
@@ -296,22 +328,18 @@ const RealTimeAlerts: React.FC<RealTimeAlertsProps> = ({
|
||||
{/* Alert count badges */}
|
||||
<div className="flex items-center gap-2">
|
||||
{urgentCount > 0 && (
|
||||
<Badge
|
||||
variant="error"
|
||||
<SeverityBadge
|
||||
severity="high"
|
||||
count={urgentCount}
|
||||
size="sm"
|
||||
icon={<AlertTriangle className="w-4 h-4" />}
|
||||
>
|
||||
{urgentCount} Alto
|
||||
</Badge>
|
||||
/>
|
||||
)}
|
||||
{highCount > 0 && (
|
||||
<Badge
|
||||
variant="warning"
|
||||
<SeverityBadge
|
||||
severity="medium"
|
||||
count={highCount}
|
||||
size="sm"
|
||||
icon={<AlertCircle className="w-4 h-4" />}
|
||||
>
|
||||
{highCount} Medio
|
||||
</Badge>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -402,7 +430,7 @@ const RealTimeAlerts: React.FC<RealTimeAlertsProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 p-4">
|
||||
{groupedAlerts.map((group) => (
|
||||
{paginatedAlerts.map((group) => (
|
||||
<div key={group.id}>
|
||||
{(group.count > 1 || groupingMode !== 'none') && (
|
||||
<div className="mb-3">
|
||||
@@ -448,24 +476,58 @@ const RealTimeAlerts: React.FC<RealTimeAlertsProps> = ({
|
||||
backgroundColor: 'var(--bg-secondary)/50',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<span className="font-medium">
|
||||
Mostrando <span className="font-bold text-[var(--text-primary)]">{filteredNotifications.length}</span> de <span className="font-bold text-[var(--text-primary)]">{notifications.length}</span> alertas
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
{stats.unread > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--color-info)] animate-pulse" />
|
||||
<span className="font-semibold text-[var(--text-primary)]">{stats.unread}</span> sin leer
|
||||
</span>
|
||||
)}
|
||||
{stats.snoozed > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span className="font-semibold text-[var(--text-primary)]">{stats.snoozed}</span> pospuestas
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Stats row */}
|
||||
<div className="flex items-center justify-between text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<span className="font-medium">
|
||||
Mostrando <span className="font-bold text-[var(--text-primary)]">{startIndex + 1}-{Math.min(endIndex, totalAlerts)}</span> de <span className="font-bold text-[var(--text-primary)]">{totalAlerts}</span> alertas
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
{stats.unread > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--color-info)] animate-pulse" />
|
||||
<span className="font-semibold text-[var(--text-primary)]">{stats.unread}</span> sin leer
|
||||
</span>
|
||||
)}
|
||||
{stats.snoozed > 0 && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span className="font-semibold text-[var(--text-primary)]">{stats.snoozed}</span> pospuestas
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="h-8 px-3"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<span className="text-sm font-medium px-3" style={{ color: 'var(--text-primary)' }}>
|
||||
Página <span className="font-bold">{currentPage}</span> de <span className="font-bold">{totalPages}</span>
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="h-8 px-3"
|
||||
aria-label="Next page"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -31,6 +31,15 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
const [selectedProvider, setSelectedProvider] = useState<POSSystem | ''>('');
|
||||
const { addToast } = useToast();
|
||||
|
||||
// Initialize selectedProvider in edit mode
|
||||
React.useEffect(() => {
|
||||
if (mode === 'edit' && existingConfig) {
|
||||
setSelectedProvider(existingConfig.pos_system as POSSystem);
|
||||
} else {
|
||||
setSelectedProvider('');
|
||||
}
|
||||
}, [mode, existingConfig]);
|
||||
|
||||
// Supported POS providers configuration
|
||||
const supportedProviders: POSProviderConfig[] = [
|
||||
{
|
||||
@@ -160,7 +169,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
const credentialFields: AddModalField[] = provider.required_fields.map(field => ({
|
||||
label: field.label,
|
||||
name: `credential_${field.field}`,
|
||||
type: field.type === 'select' ? 'select' : (field.type === 'password' ? 'text' : field.type),
|
||||
type: field.type === 'select' ? 'select' : 'text', // Map password/url to text
|
||||
required: field.required,
|
||||
placeholder: field.placeholder || `Ingresa ${field.label}`,
|
||||
helpText: field.help_text,
|
||||
@@ -245,20 +254,33 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract credentials
|
||||
// Extract credentials and separate top-level fields
|
||||
const credentials: Record<string, any> = {};
|
||||
let environment: string | undefined;
|
||||
let location_id: string | undefined;
|
||||
|
||||
provider.required_fields.forEach(field => {
|
||||
const credKey = `credential_${field.field}`;
|
||||
if (formData[credKey]) {
|
||||
credentials[field.field] = formData[credKey];
|
||||
const value = formData[credKey];
|
||||
|
||||
// Extract environment and location_id to top level, but keep in credentials too
|
||||
if (field.field === 'environment') {
|
||||
environment = value;
|
||||
} else if (field.field === 'location_id') {
|
||||
location_id = value;
|
||||
}
|
||||
|
||||
credentials[field.field] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Build request payload
|
||||
const payload = {
|
||||
// Build request payload with correct field names
|
||||
const payload: any = {
|
||||
tenant_id: tenantId,
|
||||
provider: formData.provider,
|
||||
config_name: formData.config_name,
|
||||
pos_system: formData.provider as POSSystem, // FIXED: was 'provider'
|
||||
provider_name: formData.config_name as string, // FIXED: was 'config_name'
|
||||
environment: (environment || 'sandbox') as POSEnvironment, // FIXED: extract from credentials
|
||||
credentials,
|
||||
sync_settings: {
|
||||
auto_sync_enabled: formData.auto_sync_enabled === 'true' || formData.auto_sync_enabled === true,
|
||||
@@ -266,7 +288,8 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
sync_sales: formData.sync_sales === 'true' || formData.sync_sales === true,
|
||||
sync_inventory: formData.sync_inventory === 'true' || formData.sync_inventory === true,
|
||||
sync_customers: false
|
||||
}
|
||||
},
|
||||
...(location_id && { location_id }) // FIXED: add location_id if present
|
||||
};
|
||||
|
||||
// Create or update configuration
|
||||
@@ -292,6 +315,13 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle field changes to update selectedProvider dynamically
|
||||
const handleFieldChange = (fieldName: string, value: any) => {
|
||||
if (fieldName === 'provider') {
|
||||
setSelectedProvider(value as POSSystem | '');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AddModal
|
||||
isOpen={isOpen}
|
||||
@@ -318,6 +348,7 @@ export const CreatePOSConfigModal: React.FC<CreatePOSConfigModalProps> = ({
|
||||
addToast(firstError, { type: 'error' });
|
||||
}
|
||||
}}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Leaf,
|
||||
Droplets,
|
||||
TreeDeciduous,
|
||||
TrendingDown,
|
||||
Award,
|
||||
FileText,
|
||||
ChevronRight,
|
||||
Download,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import Card from '../../ui/Card/Card';
|
||||
import { Button, Badge } from '../../ui';
|
||||
import { useSustainabilityWidget } from '../../../api/hooks/sustainability';
|
||||
import { useCurrentTenant } from '../../../stores/tenant.store';
|
||||
|
||||
interface SustainabilityWidgetProps {
|
||||
days?: number;
|
||||
onViewDetails?: () => void;
|
||||
onExportReport?: () => void;
|
||||
}
|
||||
|
||||
export const SustainabilityWidget: React.FC<SustainabilityWidgetProps> = ({
|
||||
days = 30,
|
||||
onViewDetails,
|
||||
onExportReport
|
||||
}) => {
|
||||
const { t } = useTranslation(['sustainability', 'common']);
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
const { data, isLoading, error } = useSustainabilityWidget(tenantId, days, {
|
||||
enabled: !!tenantId
|
||||
});
|
||||
|
||||
const getSDGStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'sdg_compliant':
|
||||
return 'bg-green-500/10 text-green-600 border-green-500/20';
|
||||
case 'on_track':
|
||||
return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
|
||||
case 'progressing':
|
||||
return 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20';
|
||||
default:
|
||||
return 'bg-gray-500/10 text-gray-600 border-gray-500/20';
|
||||
}
|
||||
};
|
||||
|
||||
const getSDGStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
sdg_compliant: t('sustainability:sdg.status.compliant', 'SDG Compliant'),
|
||||
on_track: t('sustainability:sdg.status.on_track', 'On Track'),
|
||||
progressing: t('sustainability:sdg.status.progressing', 'Progressing'),
|
||||
baseline: t('sustainability:sdg.status.baseline', 'Baseline')
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-6 bg-[var(--bg-secondary)] rounded w-1/3"></div>
|
||||
<div className="h-32 bg-[var(--bg-secondary)] rounded"></div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="text-center py-8">
|
||||
<Leaf className="w-12 h-12 mx-auto mb-3 text-[var(--text-secondary)] opacity-50" />
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:errors.load_failed', 'Unable to load sustainability metrics')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="p-6 pb-4 border-b border-[var(--border-primary)] bg-gradient-to-r from-green-50/50 to-blue-50/50 dark:from-green-900/10 dark:to-blue-900/10">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500/10 rounded-lg">
|
||||
<Leaf className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sustainability:widget.title', 'Sustainability Impact')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('sustainability:widget.subtitle', 'Environmental & SDG 12.3 Compliance')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full border text-xs font-medium ${getSDGStatusColor(data.sdg_status)}`}>
|
||||
{getSDGStatusLabel(data.sdg_status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SDG Progress Bar */}
|
||||
<div className="p-6 pb-4 border-b border-[var(--border-primary)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{t('sustainability:sdg.progress_label', 'SDG 12.3 Target Progress')}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-[var(--color-primary)]">
|
||||
{Math.round(data.sdg_progress)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-green-500 to-emerald-600 rounded-full transition-all duration-500 relative overflow-hidden"
|
||||
style={{ width: `${Math.min(data.sdg_progress, 100)}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white/20 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-2">
|
||||
{t('sustainability:sdg.target_note', 'Target: 50% food waste reduction by 2030')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics Grid */}
|
||||
<div className="p-6 grid grid-cols-2 gap-4">
|
||||
{/* Waste Reduction */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingDown className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.waste_reduction', 'Waste Reduction')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{Math.abs(data.waste_reduction_percentage).toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{data.total_waste_kg.toFixed(0)} kg {t('common:saved', 'saved')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CO2 Impact */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Leaf className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.co2_avoided', 'CO₂ Avoided')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{data.co2_saved_kg.toFixed(0)} kg
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
≈ {data.trees_equivalent.toFixed(1)} {t('sustainability:metrics.trees', 'trees')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Water Saved */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Droplets className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.water_saved', 'Water Saved')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{(data.water_saved_liters / 1000).toFixed(1)} m³
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{data.water_saved_liters.toFixed(0)} {t('common:liters', 'liters')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grant Programs */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Award className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{t('sustainability:metrics.grants_eligible', 'Grants Eligible')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{data.grant_programs_ready}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{t('sustainability:metrics.programs', 'programs')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Financial Impact */}
|
||||
<div className="px-6 pb-4">
|
||||
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-green-700 dark:text-green-400 mb-1">
|
||||
{t('sustainability:financial.potential_savings', 'Potential Monthly Savings')}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
€{data.financial_savings_eur.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<TreeDeciduous className="w-10 h-10 text-green-600/30 dark:text-green-400/30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="p-6 pt-4 border-t border-[var(--border-primary)] bg-[var(--bg-secondary)]/30">
|
||||
<div className="flex items-center gap-2">
|
||||
{onViewDetails && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onViewDetails}
|
||||
className="flex-1"
|
||||
>
|
||||
<Info className="w-4 h-4 mr-1" />
|
||||
{t('sustainability:actions.view_details', 'View Details')}
|
||||
</Button>
|
||||
)}
|
||||
{onExportReport && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={onExportReport}
|
||||
className="flex-1"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{t('sustainability:actions.export_report', 'Export Report')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-secondary)] text-center mt-3">
|
||||
{t('sustainability:widget.footer', 'Aligned with UN SDG 12.3 & EU Green Deal')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SustainabilityWidget;
|
||||
Reference in New Issue
Block a user