Implemented responsive card layout for mobile devices across all admin pages: - Desktop (md+): Shows traditional table layout - Mobile (<md): Shows stacked card layout with better readability AdminProducts.tsx: - Mobile cards display title, type, price (with sale badge), status - Action buttons (edit/delete) in header AdminOrders.tsx: - Mobile cards display order ID, email, status badge, total, payment method, date - View detail button in header AdminMembers.tsx: - Mobile cards display name, email, role badge, join date - Action buttons (detail/toggle admin) at bottom with full width AdminConsulting.tsx (upcoming & past tabs): - Mobile cards display date, time, client, category, status, meet link - Action buttons (link/complete/cancel) stacked at bottom AdminEvents.tsx (events & availability tabs): - Mobile cards display title/event type or block type, dates, status, notes - Action buttons (edit/delete) at bottom This approach provides much better UX on mobile compared to horizontal scrolling, especially for complex cells like sale prices with badges and multiple action buttons.
561 lines
24 KiB
TypeScript
561 lines
24 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { AppLayout } from '@/components/AppLayout';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { toast } from '@/hooks/use-toast';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Plus, Pencil, Trash2, Calendar, Clock } from 'lucide-react';
|
|
import { formatDateTime } from '@/lib/format';
|
|
|
|
interface Event {
|
|
id: string;
|
|
type: string;
|
|
product_id: string | null;
|
|
title: string;
|
|
starts_at: string;
|
|
ends_at: string;
|
|
status: string;
|
|
}
|
|
|
|
interface AvailabilityBlock {
|
|
id: string;
|
|
kind: string;
|
|
starts_at: string;
|
|
ends_at: string;
|
|
note: string | null;
|
|
}
|
|
|
|
interface Product {
|
|
id: string;
|
|
title: string;
|
|
}
|
|
|
|
const emptyEvent = {
|
|
type: 'webinar',
|
|
product_id: '' as string,
|
|
title: '',
|
|
starts_at: '',
|
|
ends_at: '',
|
|
status: 'confirmed',
|
|
};
|
|
|
|
const emptyBlock = {
|
|
kind: 'blocked',
|
|
starts_at: '',
|
|
ends_at: '',
|
|
note: '',
|
|
};
|
|
|
|
export default function AdminEvents() {
|
|
const { user, isAdmin, loading: authLoading } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [blocks, setBlocks] = useState<AvailabilityBlock[]>([]);
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [eventDialogOpen, setEventDialogOpen] = useState(false);
|
|
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
|
const [eventForm, setEventForm] = useState(emptyEvent);
|
|
|
|
const [blockDialogOpen, setBlockDialogOpen] = useState(false);
|
|
const [editingBlock, setEditingBlock] = useState<AvailabilityBlock | null>(null);
|
|
const [blockForm, setBlockForm] = useState(emptyBlock);
|
|
|
|
useEffect(() => {
|
|
if (!authLoading) {
|
|
if (!user) navigate('/auth');
|
|
else if (!isAdmin) navigate('/dashboard');
|
|
else fetchData();
|
|
}
|
|
}, [user, isAdmin, authLoading]);
|
|
|
|
const fetchData = async () => {
|
|
const [eventsRes, blocksRes, productsRes] = await Promise.all([
|
|
supabase.from('events').select('*').order('starts_at', { ascending: false }),
|
|
supabase.from('availability_blocks').select('*').order('starts_at', { ascending: false }),
|
|
supabase.from('products').select('id, title').eq('is_active', true),
|
|
]);
|
|
|
|
if (eventsRes.data) setEvents(eventsRes.data);
|
|
if (blocksRes.data) setBlocks(blocksRes.data);
|
|
if (productsRes.data) setProducts(productsRes.data);
|
|
setLoading(false);
|
|
};
|
|
|
|
// Event handlers
|
|
const handleNewEvent = () => {
|
|
setEditingEvent(null);
|
|
setEventForm(emptyEvent);
|
|
setEventDialogOpen(true);
|
|
};
|
|
|
|
const handleEditEvent = (event: Event) => {
|
|
setEditingEvent(event);
|
|
setEventForm({
|
|
type: event.type,
|
|
product_id: event.product_id || '',
|
|
title: event.title,
|
|
starts_at: event.starts_at.slice(0, 16),
|
|
ends_at: event.ends_at.slice(0, 16),
|
|
status: event.status,
|
|
});
|
|
setEventDialogOpen(true);
|
|
};
|
|
|
|
const handleSaveEvent = async () => {
|
|
if (!eventForm.title || !eventForm.starts_at || !eventForm.ends_at) {
|
|
toast({ title: 'Error', description: 'Lengkapi semua field yang wajib diisi', variant: 'destructive' });
|
|
return;
|
|
}
|
|
|
|
const eventData = {
|
|
type: eventForm.type,
|
|
product_id: eventForm.product_id || null,
|
|
title: eventForm.title,
|
|
starts_at: new Date(eventForm.starts_at).toISOString(),
|
|
ends_at: new Date(eventForm.ends_at).toISOString(),
|
|
status: eventForm.status,
|
|
};
|
|
|
|
if (editingEvent) {
|
|
const { error } = await supabase.from('events').update(eventData).eq('id', editingEvent.id);
|
|
if (error) toast({ title: 'Error', description: 'Gagal mengupdate event', variant: 'destructive' });
|
|
else { toast({ title: 'Berhasil', description: 'Event diupdate' }); setEventDialogOpen(false); fetchData(); }
|
|
} else {
|
|
const { error } = await supabase.from('events').insert(eventData);
|
|
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
else { toast({ title: 'Berhasil', description: 'Event dibuat' }); setEventDialogOpen(false); fetchData(); }
|
|
}
|
|
};
|
|
|
|
const handleDeleteEvent = async (id: string) => {
|
|
if (!confirm('Hapus event ini?')) return;
|
|
const { error } = await supabase.from('events').delete().eq('id', id);
|
|
if (error) toast({ title: 'Error', description: 'Gagal menghapus event', variant: 'destructive' });
|
|
else { toast({ title: 'Berhasil', description: 'Event dihapus' }); fetchData(); }
|
|
};
|
|
|
|
// Block handlers
|
|
const handleNewBlock = () => {
|
|
setEditingBlock(null);
|
|
setBlockForm(emptyBlock);
|
|
setBlockDialogOpen(true);
|
|
};
|
|
|
|
const handleEditBlock = (block: AvailabilityBlock) => {
|
|
setEditingBlock(block);
|
|
setBlockForm({
|
|
kind: block.kind,
|
|
starts_at: block.starts_at.slice(0, 16),
|
|
ends_at: block.ends_at.slice(0, 16),
|
|
note: block.note || '',
|
|
});
|
|
setBlockDialogOpen(true);
|
|
};
|
|
|
|
const handleSaveBlock = async () => {
|
|
if (!blockForm.starts_at || !blockForm.ends_at) {
|
|
toast({ title: 'Error', description: 'Lengkapi waktu mulai dan selesai', variant: 'destructive' });
|
|
return;
|
|
}
|
|
|
|
const blockData = {
|
|
kind: blockForm.kind,
|
|
starts_at: new Date(blockForm.starts_at).toISOString(),
|
|
ends_at: new Date(blockForm.ends_at).toISOString(),
|
|
note: blockForm.note || null,
|
|
};
|
|
|
|
if (editingBlock) {
|
|
const { error } = await supabase.from('availability_blocks').update(blockData).eq('id', editingBlock.id);
|
|
if (error) toast({ title: 'Error', description: 'Gagal mengupdate', variant: 'destructive' });
|
|
else { toast({ title: 'Berhasil', description: 'Blok diupdate' }); setBlockDialogOpen(false); fetchData(); }
|
|
} else {
|
|
const { error } = await supabase.from('availability_blocks').insert(blockData);
|
|
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
else { toast({ title: 'Berhasil', description: 'Blok dibuat' }); setBlockDialogOpen(false); fetchData(); }
|
|
}
|
|
};
|
|
|
|
const handleDeleteBlock = async (id: string) => {
|
|
if (!confirm('Hapus blok waktu ini?')) return;
|
|
const { error } = await supabase.from('availability_blocks').delete().eq('id', id);
|
|
if (error) toast({ title: 'Error', description: 'Gagal menghapus', variant: 'destructive' });
|
|
else { toast({ title: 'Berhasil', description: 'Blok dihapus' }); fetchData(); }
|
|
};
|
|
|
|
if (authLoading || loading) {
|
|
return (
|
|
<AppLayout>
|
|
<div className="container mx-auto px-4 py-8">
|
|
<Skeleton className="h-10 w-1/3 mb-8" />
|
|
<Skeleton className="h-64 w-full" />
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="flex items-center gap-3 mb-8">
|
|
<Calendar className="w-8 h-8" />
|
|
<div>
|
|
<h1 className="text-4xl font-bold">Kalender & Jadwal</h1>
|
|
<p className="text-muted-foreground">Kelola event dan blok ketersediaan</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="events" className="space-y-6">
|
|
<TabsList className="border-2 border-border">
|
|
<TabsTrigger value="events">Event</TabsTrigger>
|
|
<TabsTrigger value="availability">Ketersediaan</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="events">
|
|
<Card className="border-2 border-border">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Daftar Event</CardTitle>
|
|
<Button onClick={handleNewEvent} className="shadow-sm">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Tambah Event
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{/* Desktop Table */}
|
|
<div className="hidden md:block overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="whitespace-nowrap">Judul</TableHead>
|
|
<TableHead className="whitespace-nowrap">Tipe</TableHead>
|
|
<TableHead className="whitespace-nowrap">Mulai</TableHead>
|
|
<TableHead className="whitespace-nowrap">Status</TableHead>
|
|
<TableHead className="text-right whitespace-nowrap">Aksi</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{events.map((event) => (
|
|
<TableRow key={event.id}>
|
|
<TableCell className="font-medium">{event.title}</TableCell>
|
|
<TableCell className="capitalize">{event.type}</TableCell>
|
|
<TableCell>{formatDateTime(event.starts_at)}</TableCell>
|
|
<TableCell>
|
|
<Badge className={event.status === 'confirmed' ? 'bg-accent' : 'bg-muted'}>
|
|
{event.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button variant="ghost" size="sm" onClick={() => handleEditEvent(event)}>
|
|
<Pencil className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteEvent(event.id)}>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{events.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
|
Belum ada event
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Mobile Card Layout */}
|
|
<div className="md:hidden space-y-3 p-4">
|
|
{events.map((event) => (
|
|
<Card key={event.id} className="border-2 border-border">
|
|
<CardContent className="p-4 space-y-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-base line-clamp-1">{event.title}</h3>
|
|
<p className="text-sm text-muted-foreground capitalize">{event.type}</p>
|
|
</div>
|
|
<Badge className={event.status === 'confirmed' ? 'bg-accent' : 'bg-muted shrink-0'}>
|
|
{event.status}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">Mulai:</span>
|
|
<span className="text-sm">{formatDateTime(event.starts_at)}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 pt-2 border-t border-border">
|
|
<Button variant="ghost" size="sm" onClick={() => handleEditEvent(event)} className="flex-1">
|
|
<Pencil className="w-4 h-4 mr-1" />
|
|
Edit
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteEvent(event.id)} className="flex-1 text-destructive">
|
|
<Trash2 className="w-4 h-4 mr-1" />
|
|
Hapus
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
{events.length === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Belum ada event
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="availability">
|
|
<Card className="border-2 border-border">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Blok Ketersediaan</CardTitle>
|
|
<Button onClick={handleNewBlock} className="shadow-sm">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Tambah Blok
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{/* Desktop Table */}
|
|
<div className="hidden md:block overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="whitespace-nowrap">Tipe</TableHead>
|
|
<TableHead className="whitespace-nowrap">Mulai</TableHead>
|
|
<TableHead className="whitespace-nowrap">Selesai</TableHead>
|
|
<TableHead className="whitespace-nowrap">Catatan</TableHead>
|
|
<TableHead className="text-right whitespace-nowrap">Aksi</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{blocks.map((block) => (
|
|
<TableRow key={block.id}>
|
|
<TableCell>
|
|
<Badge className={block.kind === 'available' ? 'bg-accent' : 'bg-destructive'}>
|
|
{block.kind === 'available' ? 'Tersedia' : 'Tidak Tersedia'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>{formatDateTime(block.starts_at)}</TableCell>
|
|
<TableCell>{formatDateTime(block.ends_at)}</TableCell>
|
|
<TableCell className="text-muted-foreground">{block.note || '-'}</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button variant="ghost" size="sm" onClick={() => handleEditBlock(block)}>
|
|
<Pencil className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteBlock(block.id)}>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{blocks.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
|
Belum ada blok ketersediaan
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Mobile Card Layout */}
|
|
<div className="md:hidden space-y-3 p-4">
|
|
{blocks.map((block) => (
|
|
<Card key={block.id} className="border-2 border-border">
|
|
<CardContent className="p-4 space-y-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-base">
|
|
{block.kind === 'available' ? 'Tersedia' : 'Tidak Tersedia'}
|
|
</h3>
|
|
</div>
|
|
<Badge className={block.kind === 'available' ? 'bg-accent' : 'bg-destructive shrink-0'}>
|
|
{block.kind === 'available' ? 'Tersedia' : 'Tidak'}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">Mulai:</span>
|
|
<span className="text-sm">{formatDateTime(block.starts_at)}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">Selesai:</span>
|
|
<span className="text-sm">{formatDateTime(block.ends_at)}</span>
|
|
</div>
|
|
{block.note && (
|
|
<div className="flex items-start justify-between">
|
|
<span className="text-sm text-muted-foreground">Catatan:</span>
|
|
<span className="text-sm text-right flex-1 ml-4">{block.note}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2 pt-2 border-t border-border">
|
|
<Button variant="ghost" size="sm" onClick={() => handleEditBlock(block)} className="flex-1">
|
|
<Pencil className="w-4 h-4 mr-1" />
|
|
Edit
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteBlock(block.id)} className="flex-1 text-destructive">
|
|
<Trash2 className="w-4 h-4 mr-1" />
|
|
Hapus
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
{blocks.length === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Belum ada blok ketersediaan
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Event Dialog */}
|
|
<Dialog open={eventDialogOpen} onOpenChange={setEventDialogOpen}>
|
|
<DialogContent className="max-w-md border-2 border-border">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingEvent ? 'Edit Event' : 'Buat Event Baru'}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label>Judul *</Label>
|
|
<Input
|
|
value={eventForm.title}
|
|
onChange={(e) => setEventForm({ ...eventForm, title: e.target.value })}
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Tipe</Label>
|
|
<Select value={eventForm.type} onValueChange={(v) => setEventForm({ ...eventForm, type: v })}>
|
|
<SelectTrigger className="border-2"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="webinar">Webinar</SelectItem>
|
|
<SelectItem value="bootcamp">Bootcamp</SelectItem>
|
|
<SelectItem value="consulting">Konsultasi</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Produk Terkait</Label>
|
|
<Select value={eventForm.product_id} onValueChange={(v) => setEventForm({ ...eventForm, product_id: v })}>
|
|
<SelectTrigger className="border-2"><SelectValue placeholder="Pilih produk (opsional)" /></SelectTrigger>
|
|
<SelectContent>
|
|
{products.map((p) => (
|
|
<SelectItem key={p.id} value={p.id}>{p.title}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Mulai *</Label>
|
|
<Input
|
|
type="datetime-local"
|
|
value={eventForm.starts_at}
|
|
onChange={(e) => setEventForm({ ...eventForm, starts_at: e.target.value })}
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Selesai *</Label>
|
|
<Input
|
|
type="datetime-local"
|
|
value={eventForm.ends_at}
|
|
onChange={(e) => setEventForm({ ...eventForm, ends_at: e.target.value })}
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Status</Label>
|
|
<Select value={eventForm.status} onValueChange={(v) => setEventForm({ ...eventForm, status: v })}>
|
|
<SelectTrigger className="border-2"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="confirmed">Confirmed</SelectItem>
|
|
<SelectItem value="cancelled">Cancelled</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button onClick={handleSaveEvent} className="w-full shadow-sm">Simpan</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Block Dialog */}
|
|
<Dialog open={blockDialogOpen} onOpenChange={setBlockDialogOpen}>
|
|
<DialogContent className="max-w-md border-2 border-border">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingBlock ? 'Edit Blok' : 'Tambah Blok Ketersediaan'}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label>Tipe</Label>
|
|
<Select value={blockForm.kind} onValueChange={(v) => setBlockForm({ ...blockForm, kind: v })}>
|
|
<SelectTrigger className="border-2"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="available">Tersedia</SelectItem>
|
|
<SelectItem value="blocked">Tidak Tersedia</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Mulai *</Label>
|
|
<Input
|
|
type="datetime-local"
|
|
value={blockForm.starts_at}
|
|
onChange={(e) => setBlockForm({ ...blockForm, starts_at: e.target.value })}
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Selesai *</Label>
|
|
<Input
|
|
type="datetime-local"
|
|
value={blockForm.ends_at}
|
|
onChange={(e) => setBlockForm({ ...blockForm, ends_at: e.target.value })}
|
|
className="border-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Catatan</Label>
|
|
<Textarea
|
|
value={blockForm.note}
|
|
onChange={(e) => setBlockForm({ ...blockForm, note: e.target.value })}
|
|
placeholder="Contoh: Libur nasional, sudah ada jadwal lain..."
|
|
className="border-2"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
<Button onClick={handleSaveBlock} className="w-full shadow-sm">Simpan</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|