Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useCallback } from 'react'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | |
| import { Button } from '../ui/button'; | |
| import { Badge } from '../ui/badge'; | |
| import { Textarea } from '../ui/textarea'; | |
| import { Input } from '../ui/input'; | |
| import { Label } from '../ui/label'; | |
| import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'; | |
| import { Alert, AlertDescription } from '../ui/alert'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; | |
| import { | |
| CheckCircle2, XCircle, Calendar, Award, MapPin, | |
| Loader2, RefreshCw, Clock, Video, | |
| } from 'lucide-react'; | |
| import adminApi, { type PendingSheikhDto } from '../../lib/api/adminApi'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { toast } from 'sonner'; | |
| import { useLanguage } from '../lib/languageContext'; | |
| import { translations } from '../../lib/translations'; | |
| // ─── Types ──────────────────────────────────────────────────────────────────── | |
| interface ScheduledInterview { | |
| sheikhId: number; | |
| name: string; | |
| country: string; | |
| specializations: string; | |
| experienceYears: number; | |
| scheduledAt: Date | null; | |
| } | |
| // ─── Helpers ────────────────────────────────────────────────────────────────── | |
| function formatCountdown(ms: number): string { | |
| if (ms <= 0) return 'Now'; | |
| const totalSecs = Math.floor(ms / 1000); | |
| const days = Math.floor(totalSecs / 86400); | |
| const hours = Math.floor((totalSecs % 86400) / 3600); | |
| const mins = Math.floor((totalSecs % 3600) / 60); | |
| const secs = totalSecs % 60; | |
| if (days > 0) return `${days}d ${hours}h ${mins}m`; | |
| if (hours > 0) return `${hours}h ${mins}m ${secs}s`; | |
| return `${mins}m ${secs}s`; | |
| } | |
| function isWithin5Mins(scheduledAt: Date | null, now: Date): boolean { | |
| if (!scheduledAt) return false; | |
| const diff = scheduledAt.getTime() - now.getTime(); | |
| return diff <= 5 * 60 * 1000 && diff > -30 * 60 * 1000; | |
| } | |
| // ─── Countdown badge ────────────────────────────────────────────────────────── | |
| function CountdownBadge({ scheduledAt, now }: { scheduledAt: Date | null; now: Date }) { | |
| if (!scheduledAt) { | |
| return ( | |
| <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-500 whitespace-nowrap"> | |
| <Clock className="h-3 w-3 shrink-0" /> No time set | |
| </span> | |
| ); | |
| } | |
| const diff = scheduledAt.getTime() - now.getTime(); | |
| const imminent = diff <= 5 * 60 * 1000 && diff > 0; | |
| const past = diff <= 0; | |
| if (past) { | |
| return ( | |
| <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700 whitespace-nowrap"> | |
| <Clock className="h-3 w-3 shrink-0" /> In progress | |
| </span> | |
| ); | |
| } | |
| if (imminent) { | |
| return ( | |
| <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-700 animate-pulse whitespace-nowrap"> | |
| <Clock className="h-3 w-3 shrink-0" /> {formatCountdown(diff)} | |
| </span> | |
| ); | |
| } | |
| return ( | |
| <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 whitespace-nowrap"> | |
| <Clock className="h-3 w-3 shrink-0" /> {formatCountdown(diff)} | |
| </span> | |
| ); | |
| } | |
| // ─── Under Review Card ──────────────────────────────────────────────────────── | |
| function UnderReviewCard({ | |
| interview, | |
| now, | |
| onApprove, | |
| onReject, | |
| onJoin, | |
| processing, | |
| isFocus = false, | |
| t, | |
| }: { | |
| interview: ScheduledInterview; | |
| now: Date; | |
| onApprove: () => void; | |
| onReject: () => void; | |
| onJoin: () => void; | |
| processing: boolean; | |
| isFocus?: boolean; | |
| t: ReturnType<typeof translations>[string]['sheikhApproval']; | |
| }) { | |
| const diff = interview.scheduledAt ? interview.scheduledAt.getTime() - now.getTime() : null; | |
| const isLive = diff !== null && diff <= 0 && diff > -30 * 60 * 1000; | |
| return ( | |
| <Card className={`transition-all duration-300 relative ${ | |
| isFocus | |
| ? 'border-2 border-emerald-500 shadow-xl shadow-emerald-100 ring-1 ring-emerald-400/30' | |
| : isLive | |
| ? 'ring-2 ring-emerald-500 shadow-lg shadow-emerald-100' | |
| : 'hover:shadow-md' | |
| }`}> | |
| {isFocus && ( | |
| <div className="bg-emerald-500 text-white text-xs font-bold py-1 px-3 rounded-full absolute -top-3 left-1/2 -translate-x-1/2 z-10 animate-bounce whitespace-nowrap"> | |
| {isLive ? t.interviewLiveNow : t.nextInterviewSoon} | |
| </div> | |
| )} | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-start justify-between gap-2 flex-wrap sm:flex-nowrap"> | |
| <div className="flex items-center gap-2 sm:gap-3 min-w-0"> | |
| <div className="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-xs sm:text-sm shrink-0"> | |
| {interview.name.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()} | |
| </div> | |
| <div> | |
| <CardTitle className="text-base">{interview.name}</CardTitle> | |
| <div className="flex items-center gap-1.5 mt-0.5 text-xs text-muted-foreground"> | |
| <MapPin className="h-3 w-3" />{interview.country} | |
| <span>·</span> | |
| <span className="flex items-center gap-0.5"> | |
| <Award className="h-3 w-3 shrink-0" /> | |
| {interview.experienceYears} {t.yearsExperience} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="shrink-0 mt-1 sm:mt-0"> | |
| <CountdownBadge scheduledAt={interview.scheduledAt} now={now} /> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-3 pt-0"> | |
| {/* Prominent countdown for focused card */} | |
| {isFocus && ( | |
| <div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 flex flex-col items-center justify-center space-y-1 mb-2"> | |
| {isLive ? ( | |
| <div className="text-3xl font-black text-emerald-600 tracking-tight animate-pulse">LIVE NOW</div> | |
| ) : ( | |
| <> | |
| <div className="text-emerald-600 text-[10px] uppercase tracking-wider font-semibold">Starts in</div> | |
| <div className="text-3xl font-black text-emerald-700 tracking-tight tabular-nums"> | |
| {diff !== null ? formatCountdown(diff) : '—'} | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| )} | |
| <p className="text-sm text-muted-foreground">{interview.specializations}</p> | |
| {/* Interview time row */} | |
| {interview.scheduledAt ? ( | |
| <div className="flex items-center gap-2 text-sm rounded-lg px-3 py-2 bg-blue-50 text-blue-800"> | |
| <Calendar className="h-4 w-4 shrink-0 text-blue-600" /> | |
| <span className="font-medium"> | |
| {interview.scheduledAt.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} | |
| {' at '} | |
| {interview.scheduledAt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })} | |
| </span> | |
| </div> | |
| ) : ( | |
| <div className="flex items-center gap-2 text-sm rounded-lg px-3 py-2 text-slate-500 bg-slate-50"> | |
| <Calendar className="h-4 w-4 shrink-0" /> | |
| <span>{t.noInterviewTime}</span> | |
| </div> | |
| )} | |
| {/* Action buttons — stack on mobile when 3 shown */} | |
| <div className={`flex gap-2 pt-1 ${isLive ? 'flex-col sm:flex-row' : 'flex-row'}`}> | |
| {isLive && ( | |
| <Button size="sm" className="w-full bg-emerald-600 hover:bg-emerald-700 font-bold" disabled={processing} onClick={onJoin}> | |
| <Video className="mr-1.5 h-3.5 w-3.5" /> | |
| Join Interview | |
| </Button> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |
| // ─── Main Component ─────────────────────────────────────────────────────────── | |
| export function SheikhApproval() { | |
| const navigate = useNavigate(); | |
| const { language, isRTL } = useLanguage(); | |
| const t = translations[language].sheikhApproval; | |
| const [pendingSheikhs, setPendingSheikhs] = useState<PendingSheikhDto[]>([]); | |
| const [scheduledInterviews, setScheduledInterviews] = useState<ScheduledInterview[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [now, setNow] = useState(new Date()); | |
| const [focusOverride, setFocusOverride] = useState(false); | |
| const [activeTab, setActiveTab] = useState<'pending' | 'under-review'>('pending'); | |
| const [selectedSheikh, setSelectedSheikh] = useState<number | null>(null); | |
| const [showDialog, setShowDialog] = useState(false); | |
| const [actionType, setActionType] = useState<'approve' | 'reject' | 'interview'>('approve'); | |
| const [rejectionReason, setRejectionReason] = useState(''); | |
| const [interviewDate, setInterviewDate] = useState(''); | |
| const [interviewTime, setInterviewTime] = useState(''); | |
| const [processing, setProcessing] = useState(false); | |
| // ── Tick every second ────────────────────────────────────────────────────── | |
| useEffect(() => { | |
| const timer = setInterval(() => setNow(new Date()), 1000); | |
| return () => clearInterval(timer); | |
| }, []); | |
| // ── Focus mode logic & auto-switch ───────────────────────────────────────── | |
| const imminentInterview = scheduledInterviews.find(i => isWithin5Mins(i.scheduledAt, now)); | |
| const showFocusMode = !!imminentInterview && !focusOverride; | |
| const [lastImminentId, setLastImminentId] = useState<number | null>(null); | |
| useEffect(() => { | |
| if (imminentInterview) { | |
| if (imminentInterview.sheikhId !== lastImminentId) { | |
| setLastImminentId(imminentInterview.sheikhId); | |
| if (!focusOverride) setActiveTab('under-review'); | |
| } | |
| } else { | |
| setLastImminentId(null); | |
| setFocusOverride(false); | |
| } | |
| }, [imminentInterview, lastImminentId, focusOverride]); | |
| // ── Load data ────────────────────────────────────────────────────────────── | |
| const loadData = useCallback(async () => { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| const [pending, underReview] = await Promise.all([ | |
| adminApi.fetchPendingSheikhs(), | |
| adminApi.fetchSheikhsByStatus('UNDER_REVIEW'), | |
| ]); | |
| setPendingSheikhs(pending); | |
| const interviews: ScheduledInterview[] = underReview | |
| .map(s => ({ | |
| sheikhId: s.id, | |
| name: s.name, | |
| country: s.country, | |
| specializations: s.specializations, | |
| experienceYears: s.experienceYears, | |
| scheduledAt: s.interviewDateTime ? new Date(s.interviewDateTime) : null, | |
| })) | |
| .sort((a, b) => { | |
| if (!a.scheduledAt && !b.scheduledAt) return 0; | |
| if (!a.scheduledAt) return 1; | |
| if (!b.scheduledAt) return -1; | |
| return a.scheduledAt.getTime() - b.scheduledAt.getTime(); | |
| }); | |
| setScheduledInterviews(interviews); | |
| } catch (err) { | |
| const message = err instanceof Error ? err.message : 'Unknown error'; | |
| setError(`${t.failedToLoad}: ${message}`); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, [t]); | |
| useEffect(() => { loadData(); }, [loadData]); | |
| // ── Actions ──────────────────────────────────────────────────────────────── | |
| const handleAction = (sheikhId: number, action: 'approve' | 'reject' | 'interview') => { | |
| setSelectedSheikh(sheikhId); | |
| setActionType(action); | |
| setShowDialog(true); | |
| }; | |
| const confirmAction = async () => { | |
| if (selectedSheikh === null) return; | |
| setProcessing(true); | |
| try { | |
| if (actionType === 'approve') { | |
| await adminApi.approveSheikh(selectedSheikh); | |
| } else if (actionType === 'reject') { | |
| await adminApi.rejectSheikh(selectedSheikh, rejectionReason); | |
| } else { | |
| await adminApi.scheduleInterview(selectedSheikh, interviewDate, interviewTime); | |
| } | |
| setShowDialog(false); | |
| setRejectionReason(''); | |
| setInterviewDate(''); | |
| setInterviewTime(''); | |
| await loadData(); | |
| } catch (err) { | |
| console.error(`Failed to ${actionType} sheikh:`, err); | |
| toast.error(`${t.failedToAction} ${actionType}. ${t.pleaseTryAgain}`); | |
| } finally { | |
| setProcessing(false); | |
| } | |
| }; | |
| // ── Pending card ─────────────────────────────────────────────────────────── | |
| const renderPendingCard = (sheikh: PendingSheikhDto) => ( | |
| <Card key={sheikh.id} className="hover:shadow-lg transition-shadow"> | |
| <CardHeader> | |
| <div className="flex items-start justify-between gap-2"> | |
| <div className="flex items-center gap-2 sm:gap-3 min-w-0"> | |
| <div className="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-emerald-600 flex items-center justify-center text-white font-semibold text-xs sm:text-sm shrink-0"> | |
| {sheikh.name.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()} | |
| </div> | |
| <div className="min-w-0"> | |
| <CardTitle className="text-sm sm:text-base truncate">{sheikh.name}</CardTitle> | |
| <div className="flex items-center gap-1 sm:gap-1.5 mt-0.5 text-xs text-muted-foreground flex-wrap"> | |
| <span className="flex items-center gap-0.5"> | |
| <MapPin className="h-3 w-3 shrink-0" /> | |
| <span className="truncate max-w-[80px] sm:max-w-none">{sheikh.County}</span> | |
| </span> | |
| <span>·</span> | |
| <span className="flex items-center gap-0.5"> | |
| <Award className="h-3 w-3 shrink-0" /> | |
| {sheikh.experienceYears} {t.yearsExperience} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <Badge variant="secondary" className="shrink-0 text-xs">{t.pending}</Badge> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-3 pt-0"> | |
| <p className="text-xs sm:text-sm text-muted-foreground line-clamp-2">{sheikh.specialization}</p> | |
| {/* Stacked buttons on mobile, row on sm+ */} | |
| <div className="flex flex-col sm:flex-row gap-2 pt-1"> | |
| <Button | |
| variant="outline" | |
| className="flex-1 text-xs sm:text-sm" | |
| onClick={() => handleAction(sheikh.id, 'interview')} | |
| > | |
| <Calendar className="mr-1.5 h-3.5 w-3.5 shrink-0" /> | |
| {t.scheduleInterview} | |
| </Button> | |
| <div className="flex gap-2 flex-1"> | |
| <Button | |
| variant="outline" | |
| className="flex-1 text-red-600 hover:bg-red-50 text-xs sm:text-sm" | |
| onClick={() => handleAction(sheikh.id, 'reject')} | |
| > | |
| <XCircle className="mr-1 sm:mr-1.5 h-3.5 w-3.5 shrink-0" /> | |
| {t.reject} | |
| </Button> | |
| <Button | |
| className="flex-1 text-xs sm:text-sm" | |
| onClick={() => handleAction(sheikh.id, 'approve')} | |
| > | |
| <CheckCircle2 className="mr-1 sm:mr-1.5 h-3.5 w-3.5 shrink-0" /> | |
| {t.approve} | |
| </Button> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| // ── Dialog title/description helpers ─────────────────────────────────────── | |
| const dialogTitle = actionType === 'approve' | |
| ? t.approveApplication | |
| : actionType === 'reject' | |
| ? t.rejectApplication | |
| : t.scheduleInterview; | |
| const dialogDescription = actionType === 'approve' | |
| ? t.approveDescription | |
| : actionType === 'reject' | |
| ? t.rejectDescription | |
| : t.scheduleDescription; | |
| // ── Render ───────────────────────────────────────────────────────────────── | |
| return ( | |
| <div className={`container mx-auto p-3 sm:p-6 ${isRTL ? 'rtl' : 'ltr'}`} dir={isRTL ? 'rtl' : 'ltr'}> | |
| {/* Page header */} | |
| <div className="mb-4 sm:mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | |
| <div> | |
| <h1 className="text-xl sm:text-2xl font-bold">{t.sheikhApprovalManagement}</h1> | |
| <p className="text-sm text-muted-foreground">{t.reviewManageApplications}</p> | |
| </div> | |
| {/* <Button variant="outline" onClick={loadData} disabled={loading} className="self-start sm:self-auto"> | |
| <RefreshCw className={`h-4 w-4 ${isRTL ? 'ml-2' : 'mr-2'} ${loading ? 'animate-spin' : ''}`} /> | |
| {t.refresh} | |
| </Button> */} | |
| </div> | |
| {error && ( | |
| <Alert variant="destructive" className="mb-4"> | |
| <AlertDescription className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2"> | |
| <span className="text-sm">{error}</span> | |
| <Button variant="outline" size="sm" onClick={loadData} className="shrink-0">{t.retry}</Button> | |
| </AlertDescription> | |
| </Alert> | |
| )} | |
| {loading ? ( | |
| <div className="flex justify-center items-center py-20"> | |
| <Loader2 className="h-10 w-10 animate-spin text-emerald-600" /> | |
| </div> | |
| ) : ( | |
| <Tabs value={activeTab} onValueChange={val => setActiveTab(val as 'pending' | 'under-review')}> | |
| <TabsList className="mb-4 w-full sm:w-auto"> | |
| <TabsTrigger value="pending" className="flex-1 sm:flex-none text-xs sm:text-sm"> | |
| {t.pending} | |
| {pendingSheikhs.length > 0 && ( | |
| <span className="ml-2 px-1.5 py-0.5 rounded-full text-orange-700 text-xs font-semibold"> | |
| {pendingSheikhs.length} | |
| </span> | |
| )} | |
| </TabsTrigger> | |
| <TabsTrigger value="under-review" className="flex-1 sm:flex-none text-xs sm:text-sm"> | |
| {t.interviewScheduled} | |
| {scheduledInterviews.length > 0 && ( | |
| <span className="ml-2 px-1.5 py-0.5 rounded-full text-blue-700 text-xs font-semibold"> | |
| {scheduledInterviews.length} | |
| </span> | |
| )} | |
| </TabsTrigger> | |
| </TabsList> | |
| {/* ── Pending tab ── */} | |
| <TabsContent value="pending"> | |
| {pendingSheikhs.length > 0 ? ( | |
| <div className="grid gap-3 sm:gap-4 grid-cols-1 md:grid-cols-2"> | |
| {pendingSheikhs.map(renderPendingCard)} | |
| </div> | |
| ) : ( | |
| <Card> | |
| <CardContent className="py-12 text-center text-muted-foreground text-sm"> | |
| {t.noPending} | |
| </CardContent> | |
| </Card> | |
| )} | |
| </TabsContent> | |
| {/* ── Under Review tab ── */} | |
| <TabsContent value="under-review"> | |
| {scheduledInterviews.length > 0 ? ( | |
| <div className="space-y-4 sm:space-y-6"> | |
| {showFocusMode && imminentInterview ? ( | |
| <div className="space-y-4 sm:space-y-6"> | |
| <div className="max-w-xl mx-auto px-0 sm:px-4"> | |
| <UnderReviewCard | |
| key={imminentInterview.sheikhId} | |
| interview={imminentInterview} | |
| now={now} | |
| processing={processing} | |
| onApprove={() => handleAction(imminentInterview.sheikhId, 'approve')} | |
| onReject={() => handleAction(imminentInterview.sheikhId, 'reject')} | |
| onJoin={() => navigate(`/admin/interview/${imminentInterview.sheikhId}`)} | |
| isFocus | |
| t={t} | |
| /> | |
| </div> | |
| <div className="text-center"> | |
| <Button | |
| variant="outline" | |
| onClick={() => setFocusOverride(true)} | |
| className="text-slate-500 border border-slate-200 text-sm" | |
| > | |
| {t.showAllInterviews} | |
| </Button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4 sm:space-y-6"> | |
| <div className="grid gap-3 sm:gap-4 grid-cols-1 md:grid-cols-2"> | |
| {scheduledInterviews.map(interview => ( | |
| <UnderReviewCard | |
| key={interview.sheikhId} | |
| interview={interview} | |
| now={now} | |
| processing={processing} | |
| onApprove={() => handleAction(interview.sheikhId, 'approve')} | |
| onReject={() => handleAction(interview.sheikhId, 'reject')} | |
| onJoin={() => navigate(`/admin/interview/${interview.sheikhId}`)} | |
| isFocus={interview.sheikhId === imminentInterview?.sheikhId} | |
| t={t} | |
| /> | |
| ))} | |
| </div> | |
| {imminentInterview && ( | |
| <div className="text-center pt-2"> | |
| <Button | |
| variant="ghost" | |
| onClick={() => setFocusOverride(false)} | |
| className="text-slate-500 text-sm" | |
| > | |
| {t.backToFocusMode} | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <Card> | |
| <CardContent className="py-12 text-center text-muted-foreground text-sm"> | |
| {t.noInterviewsScheduled} | |
| </CardContent> | |
| </Card> | |
| )} | |
| </TabsContent> | |
| </Tabs> | |
| )} | |
| {/* ── Action Dialog ── */} | |
| <Dialog open={showDialog} onOpenChange={setShowDialog}> | |
| <DialogContent className="w-[calc(100vw-2rem)] sm:max-w-md mx-auto" dir={isRTL ? 'rtl' : 'ltr'}> | |
| <DialogHeader> | |
| <DialogTitle className="text-base sm:text-lg">{dialogTitle}</DialogTitle> | |
| <DialogDescription className="text-xs sm:text-sm">{dialogDescription}</DialogDescription> | |
| </DialogHeader> | |
| <div className="space-y-4"> | |
| {actionType === 'reject' && ( | |
| <div className="space-y-2"> | |
| <Label htmlFor="reason" className="text-sm">{t.rejectionReason}</Label> | |
| <Textarea | |
| id="reason" | |
| placeholder={t.rejectionPlaceholder} | |
| value={rejectionReason} | |
| onChange={e => setRejectionReason(e.target.value)} | |
| rows={4} | |
| className="text-sm" | |
| /> | |
| </div> | |
| )} | |
| {actionType === 'interview' && ( | |
| <> | |
| <div className="space-y-2"> | |
| <Label htmlFor="date" className="text-sm">{t.interviewDate}</Label> | |
| <Input id="date" type="date" value={interviewDate} onChange={e => setInterviewDate(e.target.value)} className="text-sm" /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="time" className="text-sm">{t.interviewTime}</Label> | |
| <Input id="time" type="time" value={interviewTime} onChange={e => setInterviewTime(e.target.value)} className="text-sm" /> | |
| </div> | |
| </> | |
| )} | |
| {actionType === 'approve' && ( | |
| <Alert> | |
| <AlertDescription className="text-xs sm:text-sm">{t.approveAlert}</AlertDescription> | |
| </Alert> | |
| )} | |
| <div className="flex gap-2"> | |
| <Button | |
| variant="outline" | |
| onClick={() => setShowDialog(false)} | |
| className="flex-1 text-sm" | |
| disabled={processing} | |
| > | |
| {t.cancel} | |
| </Button> | |
| <Button | |
| onClick={confirmAction} | |
| className="flex-1 text-sm" | |
| variant={actionType === 'reject' ? 'destructive' : 'default'} | |
| disabled={ | |
| processing || | |
| (actionType === 'reject' && !rejectionReason) || | |
| (actionType === 'interview' && (!interviewDate || !interviewTime)) | |
| } | |
| > | |
| {processing ? <Loader2 className="h-4 w-4 animate-spin" /> : t.confirm} | |
| </Button> | |
| </div> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| </div> | |
| ); | |
| } |