Fix calendar timezone and group consulting slots by order
Calendar Timezone Fix: - Add +07:00 timezone offset to date strings in create-google-meet-event - Fixes 13:00 appearing as 20:00 in Google Calendar - Now treats times as Asia/Jakarta time explicitly Single Calendar Event per Order: - handle-order-paid now creates ONE event for all slots in an order - Uses first slot's start time and last slot's end time - Updates all slots with the same meet_link - Prevents duplicate calendar events for multi-slot orders Admin Consulting Page Improvements: - Group consulting slots by order_id - Display as single row with continuous time range (start-end) - Show session count when multiple slots (e.g., "2 sesi") - Consistent with member-facing ConsultingHistory component - Updated both desktop table and mobile card layouts - Updated both upcoming and past tabs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,17 @@ interface ConsultingSlot {
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupedOrder {
|
||||
orderId: string | null;
|
||||
slots: ConsultingSlot[];
|
||||
firstDate: string;
|
||||
meetLink: string | null;
|
||||
profile: {
|
||||
name: string;
|
||||
email: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface PlatformSettings {
|
||||
integration_n8n_base_url?: string;
|
||||
integration_google_calendar_id?: string;
|
||||
@@ -245,10 +256,31 @@ export default function AdminConsulting() {
|
||||
);
|
||||
}
|
||||
|
||||
// Group slots by order_id
|
||||
const groupedOrders: GroupedOrder[] = (() => {
|
||||
const groups = new Map<string | null, ConsultingSlot[]>();
|
||||
|
||||
slots.forEach(slot => {
|
||||
const orderId = slot.order_id || 'no-order';
|
||||
if (!groups.has(orderId)) {
|
||||
groups.set(orderId, []);
|
||||
}
|
||||
groups.get(orderId)!.push(slot);
|
||||
});
|
||||
|
||||
return Array.from(groups.entries()).map(([orderId, slots]) => ({
|
||||
orderId: orderId === 'no-order' ? null : orderId,
|
||||
slots,
|
||||
firstDate: slots[0].date,
|
||||
meetLink: slots[0].meet_link,
|
||||
profile: slots[0].profiles || null,
|
||||
})).sort((a, b) => new Date(a.firstDate).getTime() - new Date(b.firstDate).getTime());
|
||||
})();
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const upcomingSlots = slots.filter(s => s.date >= today && (s.status === 'confirmed' || s.status === 'pending_payment'));
|
||||
const pastSlots = slots.filter(s => s.date < today || s.status === 'completed' || s.status === 'cancelled');
|
||||
const todaySlots = slots.filter(s => isToday(parseISO(s.date)) && s.status === 'confirmed');
|
||||
const upcomingOrders = groupedOrders.filter(o => o.firstDate >= today && o.slots.some(s => s.status === 'confirmed' || s.status === 'pending_payment'));
|
||||
const pastOrders = groupedOrders.filter(o => o.firstDate < today || o.slots.every(s => s.status === 'completed' || s.status === 'cancelled'));
|
||||
const todayOrders = groupedOrders.filter(o => isToday(parseISO(o.firstDate)) && o.slots.some(s => s.status === 'confirmed'));
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
@@ -262,30 +294,34 @@ export default function AdminConsulting() {
|
||||
</p>
|
||||
|
||||
{/* Today's Sessions Alert */}
|
||||
{todaySlots.length > 0 && (
|
||||
{todayOrders.length > 0 && (
|
||||
<Card className="border-2 border-primary bg-primary/5 mb-6">
|
||||
<CardContent className="py-4">
|
||||
<h3 className="font-bold flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5" />
|
||||
Sesi Hari Ini ({todaySlots.length})
|
||||
Sesi Hari Ini ({todayOrders.length})
|
||||
</h3>
|
||||
<div className="mt-2 space-y-2">
|
||||
{todaySlots.map(slot => (
|
||||
<div key={slot.id} className="flex items-center justify-between text-sm">
|
||||
<span>
|
||||
{slot.start_time.substring(0, 5)} - {slot.profiles?.name || 'N/A'} ({slot.topic_category})
|
||||
</span>
|
||||
{slot.meet_link ? (
|
||||
<a href={slot.meet_link} target="_blank" rel="noopener noreferrer" className="text-primary underline flex items-center gap-1">
|
||||
<ExternalLink className="w-3 h-3" /> Join
|
||||
</a>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" onClick={() => openMeetDialog(slot)}>
|
||||
Tambah Link
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{todayOrders.map((order) => {
|
||||
const firstSlot = order.slots[0];
|
||||
const lastSlot = order.slots[order.slots.length - 1];
|
||||
return (
|
||||
<div key={order.orderId || 'no-order'} className="flex items-center justify-between text-sm">
|
||||
<span>
|
||||
{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)} • {order.profile?.name || 'N/A'} ({firstSlot.topic_category})
|
||||
</span>
|
||||
{order.meetLink ? (
|
||||
<a href={order.meetLink} target="_blank" rel="noopener noreferrer" className="text-primary underline flex items-center gap-1">
|
||||
<ExternalLink className="w-3 h-3" /> Join
|
||||
</a>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" onClick={() => openMeetDialog(firstSlot)}>
|
||||
Tambah Link
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -295,25 +331,25 @@ export default function AdminConsulting() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{todaySlots.length}</div>
|
||||
<div className="text-2xl font-bold">{todayOrders.length}</div>
|
||||
<p className="text-sm text-muted-foreground">Hari Ini</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{upcomingSlots.filter(s => s.status === 'confirmed').length}</div>
|
||||
<div className="text-2xl font-bold">{upcomingOrders.filter(o => o.slots.some(s => s.status === 'confirmed')).length}</div>
|
||||
<p className="text-sm text-muted-foreground">Dikonfirmasi</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{upcomingSlots.filter(s => !s.meet_link && s.status === 'confirmed').length}</div>
|
||||
<div className="text-2xl font-bold">{upcomingOrders.filter(o => !o.meetLink && o.slots.some(s => s.status === 'confirmed')).length}</div>
|
||||
<p className="text-sm text-muted-foreground">Perlu Link Meet</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{pastSlots.filter(s => s.status === 'completed').length}</div>
|
||||
<div className="text-2xl font-bold">{pastOrders.filter(o => o.slots.every(s => s.status === 'completed')).length}</div>
|
||||
<p className="text-sm text-muted-foreground">Selesai</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -322,8 +358,8 @@ export default function AdminConsulting() {
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="upcoming">Mendatang ({upcomingSlots.length})</TabsTrigger>
|
||||
<TabsTrigger value="past">Riwayat ({pastSlots.length})</TabsTrigger>
|
||||
<TabsTrigger value="upcoming">Mendatang ({upcomingOrders.length})</TabsTrigger>
|
||||
<TabsTrigger value="past">Riwayat ({pastOrders.length})</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="upcoming">
|
||||
@@ -344,81 +380,91 @@ export default function AdminConsulting() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{upcomingSlots.map((slot) => (
|
||||
<TableRow key={slot.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}
|
||||
{isToday(parseISO(slot.date)) && <Badge className="ml-2 bg-primary">Hari Ini</Badge>}
|
||||
{isTomorrow(parseISO(slot.date)) && <Badge className="ml-2 bg-accent">Besok</Badge>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{slot.profiles?.name || '-'}</p>
|
||||
<p className="text-sm text-muted-foreground">{slot.profiles?.email}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{slot.topic_category}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[slot.status]?.label || slot.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{slot.meet_link ? (
|
||||
<a
|
||||
href={slot.meet_link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Buka
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
{slot.status === 'confirmed' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openMeetDialog(slot)}
|
||||
className="border-2"
|
||||
{upcomingOrders.map((order) => {
|
||||
const firstSlot = order.slots[0];
|
||||
const lastSlot = order.slots[order.slots.length - 1];
|
||||
const sessionCount = order.slots.length;
|
||||
return (
|
||||
<TableRow key={order.orderId || 'no-order'}>
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
{format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
|
||||
{isToday(parseISO(firstSlot.date)) && <Badge className="ml-2 bg-primary">Hari Ini</Badge>}
|
||||
{isTomorrow(parseISO(firstSlot.date)) && <Badge className="ml-2 bg-accent">Besok</Badge>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div>{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}</div>
|
||||
{sessionCount > 1 && (
|
||||
<div className="text-xs text-muted-foreground">{sessionCount} sesi</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{order.profile?.name || '-'}</p>
|
||||
<p className="text-sm text-muted-foreground">{order.profile?.email}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{firstSlot.topic_category}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[firstSlot.status]?.label || firstSlot.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.meetLink ? (
|
||||
<a
|
||||
href={order.meetLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline flex items-center gap-1"
|
||||
>
|
||||
<LinkIcon className="w-4 h-4 mr-1" />
|
||||
{slot.meet_link ? 'Edit' : 'Link'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(slot.id, 'completed')}
|
||||
className="border-2 text-green-600"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(slot.id, 'cancelled')}
|
||||
className="border-2 text-destructive"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{upcomingSlots.length === 0 && (
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Buka
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
{firstSlot.status === 'confirmed' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openMeetDialog(firstSlot)}
|
||||
className="border-2"
|
||||
>
|
||||
<LinkIcon className="w-4 h-4 mr-1" />
|
||||
{order.meetLink ? 'Edit' : 'Link'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(firstSlot.id, 'completed')}
|
||||
className="border-2 text-green-600"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(firstSlot.id, 'cancelled')}
|
||||
className="border-2 text-destructive"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{upcomingOrders.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||
Tidak ada jadwal mendatang
|
||||
@@ -433,90 +479,98 @@ export default function AdminConsulting() {
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{upcomingSlots.map((slot) => (
|
||||
<div key={slot.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
||||
<h3 className="font-semibold text-sm">
|
||||
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}
|
||||
</h3>
|
||||
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[slot.status]?.label || slot.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}
|
||||
</p>
|
||||
{upcomingOrders.map((order) => {
|
||||
const firstSlot = order.slots[0];
|
||||
const lastSlot = order.slots[order.slots.length - 1];
|
||||
const sessionCount = order.slots.length;
|
||||
return (
|
||||
<div key={order.orderId || 'no-order'} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
||||
<h3 className="font-semibold text-sm">
|
||||
{format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
|
||||
</h3>
|
||||
<Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[firstSlot.status]?.label || firstSlot.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}
|
||||
{sessionCount > 1 && (
|
||||
<span className="ml-2 text-xs">({sessionCount} sesi)</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Klien:</span>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium">{order.profile?.name || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Klien:</span>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium">{slot.profiles?.name || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Kategori:</span>
|
||||
<Badge variant="outline" className="text-xs">{slot.topic_category}</Badge>
|
||||
</div>
|
||||
{slot.meet_link && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Meet:</span>
|
||||
<a
|
||||
href={slot.meet_link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
Buka
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Kategori:</span>
|
||||
<Badge variant="outline" className="text-xs">{firstSlot.topic_category}</Badge>
|
||||
</div>
|
||||
{slot.status === 'confirmed' && (
|
||||
<div className="flex gap-2 pt-2 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openMeetDialog(slot)}
|
||||
className="flex-1 border-2 text-xs"
|
||||
{order.meetLink && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Meet:</span>
|
||||
<a
|
||||
href={order.meetLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
<LinkIcon className="w-3 h-3 mr-1" />
|
||||
{slot.meet_link ? 'Edit' : 'Link'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(slot.id, 'completed')}
|
||||
className="flex-1 border-2 text-green-600 text-xs"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Selesai
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(slot.id, 'cancelled')}
|
||||
className="flex-1 border-2 text-destructive text-xs"
|
||||
>
|
||||
<XCircle className="w-3 h-3 mr-1" />
|
||||
Batal
|
||||
</Button>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
Buka
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{firstSlot.status === 'confirmed' && (
|
||||
<div className="flex gap-2 pt-2 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openMeetDialog(firstSlot)}
|
||||
className="flex-1 border-2 text-xs"
|
||||
>
|
||||
<LinkIcon className="w-3 h-3 mr-1" />
|
||||
{order.meetLink ? 'Edit' : 'Link'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(firstSlot.id, 'completed')}
|
||||
className="flex-1 border-2 text-green-600 text-xs"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Selesai
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSlotStatus(firstSlot.id, 'cancelled')}
|
||||
className="flex-1 border-2 text-destructive text-xs"
|
||||
>
|
||||
<XCircle className="w-3 h-3 mr-1" />
|
||||
Batal
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{upcomingSlots.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Tidak ada jadwal mendatang
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{upcomingOrders.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Tidak ada jadwal mendatang
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="past">
|
||||
@@ -535,20 +589,32 @@ export default function AdminConsulting() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pastSlots.slice(0, 20).map((slot) => (
|
||||
<TableRow key={slot.id}>
|
||||
<TableCell>{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}</TableCell>
|
||||
<TableCell>{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}</TableCell>
|
||||
<TableCell>{slot.profiles?.name || '-'}</TableCell>
|
||||
<TableCell><Badge variant="outline">{slot.topic_category}</Badge></TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[slot.status]?.label || slot.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{pastSlots.length === 0 && (
|
||||
{pastOrders.slice(0, 20).map((order) => {
|
||||
const firstSlot = order.slots[0];
|
||||
const lastSlot = order.slots[order.slots.length - 1];
|
||||
const sessionCount = order.slots.length;
|
||||
return (
|
||||
<TableRow key={order.orderId || 'no-order'}>
|
||||
<TableCell>{format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div>{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}</div>
|
||||
{sessionCount > 1 && (
|
||||
<div className="text-xs text-muted-foreground">{sessionCount} sesi</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{order.profile?.name || '-'}</TableCell>
|
||||
<TableCell><Badge variant="outline">{firstSlot.topic_category}</Badge></TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[firstSlot.status]?.label || firstSlot.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{pastOrders.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||
Belum ada riwayat konsultasi
|
||||
@@ -563,40 +629,48 @@ export default function AdminConsulting() {
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{pastSlots.slice(0, 20).map((slot) => (
|
||||
<div key={slot.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm">
|
||||
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[slot.status]?.label || slot.status}
|
||||
</Badge>
|
||||
{pastOrders.slice(0, 20).map((order) => {
|
||||
const firstSlot = order.slots[0];
|
||||
const lastSlot = order.slots[order.slots.length - 1];
|
||||
const sessionCount = order.slots.length;
|
||||
return (
|
||||
<div key={order.orderId || 'no-order'} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm">
|
||||
{format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}
|
||||
{sessionCount > 1 && (
|
||||
<span className="ml-2 text-xs">({sessionCount} sesi)</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Klien:</span>
|
||||
<span className="text-sm">{slot.profiles?.name || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Kategori:</span>
|
||||
<Badge variant="outline" className="text-xs">{slot.topic_category}</Badge>
|
||||
</div>
|
||||
<Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
|
||||
{statusLabels[firstSlot.status]?.label || firstSlot.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Klien:</span>
|
||||
<span className="text-sm">{order.profile?.name || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Kategori:</span>
|
||||
<Badge variant="outline" className="text-xs">{firstSlot.topic_category}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{pastSlots.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Belum ada riwayat konsultasi
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{pastOrders.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Belum ada riwayat konsultasi
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user