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
375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
import { Button } from '@/components/ui';
|
|
import { Avatar } from '@/components/ui';
|
|
import { Badge } from '@/components/ui';
|
|
import { useOrganizations } from '@/hooks/useOrganizations.ts';
|
|
import { Organization } from '@/types.ts';
|
|
import {
|
|
useVerifyOrganization,
|
|
useRejectVerification,
|
|
useBulkVerifyOrganizations,
|
|
} from '@/hooks/api/useAdminAPI.ts';
|
|
import { useToast } from '@/hooks/useToast.ts';
|
|
import { CheckCircle, XCircle, Eye, Clock, AlertTriangle, ArrowLeft } from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
const AdminVerificationQueuePage = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { organizations } = useOrganizations();
|
|
const { success, error: showError } = useToast();
|
|
|
|
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
|
|
const [rejectionReason, setRejectionReason] = useState('');
|
|
const [rejectionNotes, setRejectionNotes] = useState('');
|
|
|
|
// API hooks
|
|
const verifyOrganization = useVerifyOrganization();
|
|
const rejectVerification = useRejectVerification();
|
|
const bulkVerifyOrganizations = useBulkVerifyOrganizations();
|
|
|
|
// Filter organizations needing verification
|
|
const pendingOrganizations = useMemo(() => {
|
|
return organizations
|
|
.filter((org) => !org.Verified && (org.PendingVerification || !org.Verified))
|
|
.sort((a, b) => {
|
|
// Sort by creation date (newest first)
|
|
return new Date(b.CreatedAt || '').getTime() - new Date(a.CreatedAt || '').getTime();
|
|
});
|
|
}, [organizations]);
|
|
|
|
const handleVerify = async (org: Organization, notes?: string) => {
|
|
try {
|
|
await verifyOrganization.mutateAsync({ id: org.ID, notes });
|
|
success('Organization verified successfully');
|
|
setSelectedOrg(null);
|
|
} catch {
|
|
showError('Failed to verify organization');
|
|
}
|
|
};
|
|
|
|
const handleReject = async (org: Organization) => {
|
|
if (!rejectionReason) {
|
|
showError('Please provide a rejection reason');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await rejectVerification.mutateAsync({
|
|
id: org.ID,
|
|
reason: rejectionReason,
|
|
notes: rejectionNotes,
|
|
});
|
|
success('Organization verification rejected');
|
|
setSelectedOrg(null);
|
|
setRejectionReason('');
|
|
setRejectionNotes('');
|
|
} catch {
|
|
showError('Failed to reject verification');
|
|
}
|
|
};
|
|
|
|
const handleBulkVerify = async () => {
|
|
const orgIds = pendingOrganizations.slice(0, 10).map((org) => org.ID); // Verify first 10
|
|
try {
|
|
await bulkVerifyOrganizations.mutateAsync(orgIds);
|
|
success(`Verified ${orgIds.length} organizations successfully`);
|
|
} catch {
|
|
showError('Failed to bulk verify organizations');
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string | undefined) => {
|
|
if (!dateString) return 'Unknown';
|
|
return new Date(dateString).toLocaleDateString();
|
|
};
|
|
|
|
const getVerificationCriteria = (org: Organization) => {
|
|
return [
|
|
{
|
|
label: 'Valid contact information',
|
|
met: !!(org.Website || (org.Address && org.Address.includes('@'))),
|
|
},
|
|
{
|
|
label: 'Complete profile',
|
|
met: !!(org.Name && org.Description && org.Sector),
|
|
},
|
|
{
|
|
label: 'Appropriate content',
|
|
met: !!(org.Description && org.Description.length > 20),
|
|
},
|
|
{
|
|
label: 'Logo quality',
|
|
met: !!org.Logo,
|
|
},
|
|
];
|
|
};
|
|
|
|
if (pendingOrganizations.length === 0) {
|
|
return (
|
|
<div className="max-w-4xl mx-auto space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="sm" onClick={() => navigate('/admin/organizations')}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
{t('admin.verification.queue.backToOrganizations')}
|
|
</Button>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent className="text-center py-12">
|
|
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">
|
|
{t('admin.verification.queue.allCaughtUp')}
|
|
</h3>
|
|
<p className="text-muted-foreground">
|
|
{t('admin.verification.queue.noPendingMessage')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="sm" onClick={() => navigate('/admin/organizations')}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
{t('admin.verification.queue.backToOrganizations')}
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold">{t('admin.verification.queue.title')}</h1>
|
|
<p className="text-muted-foreground">
|
|
{t('admin.verification.queue.subtitle', { count: pendingOrganizations.length })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{pendingOrganizations.length > 0 && (
|
|
<Button onClick={handleBulkVerify} disabled={bulkVerifyOrganizations.isPending}>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
{t('admin.verification.queue.verifyNext10')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Queue List */}
|
|
<div className="lg:col-span-1">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Clock className="w-5 h-5" />
|
|
{t('admin.verification.queue.pendingOrganizations')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{pendingOrganizations.map((org, index) => (
|
|
<div
|
|
key={org.ID}
|
|
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
|
selectedOrg?.ID === org.ID
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border hover:bg-muted/50'
|
|
}`}
|
|
onClick={() => setSelectedOrg(org)}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-xs font-medium">
|
|
{org.Name?.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-medium text-sm truncate">{org.Name}</h4>
|
|
<p className="text-xs text-muted-foreground">{org.Sector}</p>
|
|
<p className="text-xs text-muted-foreground">{formatDate(org.CreatedAt)}</p>
|
|
</div>
|
|
{index < 10 && (
|
|
<Badge variant="secondary" size="sm">
|
|
{t('admin.verification.queue.priority')}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Organization Details */}
|
|
<div className="lg:col-span-2">
|
|
{selectedOrg ? (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-3">
|
|
<Avatar src={selectedOrg.Logo} name={selectedOrg.Name} size="md" />
|
|
<div>
|
|
<h3 className="text-lg font-semibold">{selectedOrg.Name}</h3>
|
|
<p className="text-sm text-muted-foreground">{selectedOrg.Sector}</p>
|
|
</div>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Organization Info */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 className="font-medium mb-2">
|
|
{t('admin.verification.queue.basicInformation')}
|
|
</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">
|
|
{t('admin.verification.queue.sector')}
|
|
</span>{' '}
|
|
{selectedOrg.Sector}
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">
|
|
{t('admin.verification.queue.type')}
|
|
</span>{' '}
|
|
{selectedOrg.Subtype || 'Not specified'}
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">
|
|
{t('admin.verification.queue.website')}
|
|
</span>{' '}
|
|
{selectedOrg.Website ? (
|
|
<a
|
|
href={selectedOrg.Website}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary hover:underline"
|
|
>
|
|
{selectedOrg.Website}
|
|
</a>
|
|
) : (
|
|
'Not provided'
|
|
)}
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">
|
|
{t('admin.verification.queue.created')}
|
|
</span>{' '}
|
|
{formatDate(selectedOrg.CreatedAt)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium mb-2">
|
|
{t('admin.verification.queue.description')}
|
|
</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
{selectedOrg.Description || 'No description provided'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Verification Criteria */}
|
|
<div>
|
|
<h4 className="font-medium mb-3">
|
|
{t('admin.verification.queue.verificationChecklist')}
|
|
</h4>
|
|
<div className="space-y-2">
|
|
{getVerificationCriteria(selectedOrg).map((criteria, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
{criteria.met ? (
|
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
|
) : (
|
|
<XCircle className="w-4 h-4 text-red-500" />
|
|
)}
|
|
<span
|
|
className={`text-sm ${criteria.met ? 'text-green-700' : 'text-red-700'}`}
|
|
>
|
|
{criteria.label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Resources */}
|
|
{(selectedOrg.Needs?.length > 0 || selectedOrg.Offers?.length > 0) && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{selectedOrg.Needs && selectedOrg.Needs.length > 0 && (
|
|
<div>
|
|
<h4 className="font-medium mb-2">{t('admin.verification.queue.needs')}</h4>
|
|
<div className="flex flex-wrap gap-1">
|
|
{selectedOrg.Needs.map((need, index) => (
|
|
<Badge key={index} variant="outline" size="sm">
|
|
{need}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedOrg.Offers && selectedOrg.Offers.length > 0 && (
|
|
<div>
|
|
<h4 className="font-medium mb-2">{t('admin.verification.queue.offers')}</h4>
|
|
<div className="flex flex-wrap gap-1">
|
|
{selectedOrg.Offers.map((offer, index) => (
|
|
<Badge key={index} variant="secondary" size="sm">
|
|
{offer}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-3 pt-4 border-t">
|
|
<Button
|
|
onClick={() => handleVerify(selectedOrg)}
|
|
disabled={verifyOrganization.isPending}
|
|
className="flex-1"
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
{t('admin.verification.queue.approve')}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate(`/admin/organizations/${selectedOrg.ID}/edit`)}
|
|
>
|
|
<Eye className="w-4 h-4 mr-2" />
|
|
{t('admin.verification.queue.edit')}
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() => {
|
|
// Show rejection modal/form
|
|
const reason = prompt('Rejection reason:');
|
|
if (reason) {
|
|
handleReject(selectedOrg);
|
|
}
|
|
}}
|
|
>
|
|
<XCircle className="w-4 h-4 mr-2" />
|
|
{t('admin.verification.queue.reject')}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="text-center py-12">
|
|
<AlertTriangle className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">
|
|
{t('admin.verification.queue.selectOrganization')}
|
|
</h3>
|
|
<p className="text-muted-foreground">
|
|
{t('admin.verification.queue.selectOrganizationDesc')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminVerificationQueuePage;
|