mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
357 lines
14 KiB
TypeScript
357 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 (err) {
|
|
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 (err) {
|
|
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 (err) {
|
|
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" />
|
|
Back to Organizations
|
|
</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">All Caught Up!</h3>
|
|
<p className="text-muted-foreground">
|
|
There are no organizations pending verification at this time.
|
|
</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" />
|
|
Back to Organizations
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Verification Queue</h1>
|
|
<p className="text-muted-foreground">
|
|
{pendingOrganizations.length} organizations pending verification
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{pendingOrganizations.length > 0 && (
|
|
<Button onClick={handleBulkVerify} disabled={bulkVerifyOrganizations.isPending}>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Verify Next 10
|
|
</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" />
|
|
Pending Organizations
|
|
</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">
|
|
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">Basic Information</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">Sector:</span> {selectedOrg.Sector}
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Type:</span>{' '}
|
|
{selectedOrg.Subtype || 'Not specified'}
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">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">Created:</span>{' '}
|
|
{formatDate(selectedOrg.CreatedAt)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium mb-2">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">Verification Checklist</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">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">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" />
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate(`/admin/organizations/${selectedOrg.ID}/edit`)}
|
|
>
|
|
<Eye className="w-4 h-4 mr-2" />
|
|
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" />
|
|
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">Select an Organization</h3>
|
|
<p className="text-muted-foreground">
|
|
Choose an organization from the queue to review its details and verification
|
|
status.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminVerificationQueuePage;
|