Fix email system and implement OTP confirmation flow
Email System Fixes: - Fix email sending after payment: handle-order-paid now calls send-notification instead of send-email-v2 directly, properly processing template variables - Fix order_created email timing: sent immediately after order creation, before payment QR code generation - Update email templates to use short order ID (8 chars) instead of full UUID - Add working "Akses Sekarang" buttons to payment_success and access_granted emails - Add platform_url column to platform_settings for email links OTP Verification Flow: - Create dedicated /confirm-otp page for users who close registration modal - Add link in checkout modal and email to dedicated OTP page - Update OTP email template with better copywriting and dedicated page link - Fix send-auth-otp to fetch platform settings for dynamic brand_name and platform_url - Auto-login users after OTP verification in checkout flow Admin Features: - Add delete user functionality with cascade deletion of all related data - Update IntegrasiTab to read/write email settings from platform_settings only - Add test email template for email configuration testing Cleanup: - Remove obsolete send-consultation-reminder and send-test-email functions - Update send-email-v2 to read email config from platform_settings - Remove footer links (Ubah Preferensi/Unsubscribe) from email templates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,18 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { Eye, Shield, ShieldOff, Search, X } from "lucide-react";
|
||||
import { Eye, Shield, ShieldOff, Search, X, Trash2 } from "lucide-react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface Member {
|
||||
id: string;
|
||||
@@ -39,6 +49,9 @@ export default function AdminMembers() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterRole, setFilterRole] = useState<string>('all');
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [memberToDelete, setMemberToDelete] = useState<Member | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading) {
|
||||
@@ -107,6 +120,83 @@ export default function AdminMembers() {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteMember = (member: Member) => {
|
||||
if (member.id === user?.id) {
|
||||
toast({ title: "Error", description: "Tidak bisa menghapus akun sendiri", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
setMemberToDelete(member);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const deleteMember = async () => {
|
||||
if (!memberToDelete) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const userId = memberToDelete.id;
|
||||
|
||||
// Step 1: Delete auth_otps
|
||||
await supabase.from("auth_otps").delete().eq("user_id", userId);
|
||||
|
||||
// Step 2: Delete order_items (first to avoid FK issues)
|
||||
const { data: orders } = await supabase.from("orders").select("id").eq("user_id", userId);
|
||||
if (orders && orders.length > 0) {
|
||||
const orderIds = orders.map(o => o.id);
|
||||
await supabase.from("order_items").delete().in("order_id", orderIds);
|
||||
}
|
||||
|
||||
// Step 3: Delete orders
|
||||
await supabase.from("orders").delete().eq("user_id", userId);
|
||||
|
||||
// Step 4: Delete user_access
|
||||
await supabase.from("user_access").delete().eq("user_id", userId);
|
||||
|
||||
// Step 5: Delete video_progress
|
||||
await supabase.from("video_progress").delete().eq("user_id", userId);
|
||||
|
||||
// Step 6: Delete consulting_slots
|
||||
await supabase.from("consulting_slots").delete().eq("user_id", userId);
|
||||
|
||||
// Step 7: Delete calendar_events
|
||||
await supabase.from("calendar_events").delete().eq("user_id", userId);
|
||||
|
||||
// Step 8: Delete user_roles
|
||||
await supabase.from("user_roles").delete().eq("user_id", userId);
|
||||
|
||||
// Step 9: Delete profile
|
||||
await supabase.from("profiles").delete().eq("id", userId);
|
||||
|
||||
// Step 10: Delete from auth.users using edge function
|
||||
const { error: deleteError } = await supabase.functions.invoke('delete-user', {
|
||||
body: { user_id: userId }
|
||||
});
|
||||
|
||||
if (deleteError) {
|
||||
console.error('Error deleting from auth.users:', deleteError);
|
||||
throw new Error(`Gagal menghapus user dari auth: ${deleteError.message}`);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Berhasil",
|
||||
description: `Member ${memberToDelete.email || memberToDelete.name} berhasil dihapus beserta semua data terkait`
|
||||
});
|
||||
|
||||
setDeleteDialogOpen(false);
|
||||
setMemberToDelete(null);
|
||||
fetchMembers();
|
||||
} catch (error: any) {
|
||||
console.error('Delete member error:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Gagal menghapus member",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (authLoading || loading) {
|
||||
return (
|
||||
<AppLayout>
|
||||
@@ -243,6 +333,15 @@ export default function AdminMembers() {
|
||||
>
|
||||
{adminIds.has(member.id) ? <ShieldOff className="w-4 h-4" /> : <Shield className="w-4 h-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => confirmDeleteMember(member)}
|
||||
disabled={member.id === user?.id}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -289,6 +388,16 @@ export default function AdminMembers() {
|
||||
{adminIds.has(member.id) ? <ShieldOff className="w-4 h-4 mr-1" /> : <Shield className="w-4 h-4 mr-1" />}
|
||||
{adminIds.has(member.id) ? "Hapus Admin" : "Jadikan Admin"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => confirmDeleteMember(member)}
|
||||
disabled={member.id === user?.id}
|
||||
className="flex-1 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -334,6 +443,57 @@ export default function AdminMembers() {
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent className="border-2 border-border">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Hapus Member?</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
Anda akan menghapus member <strong>{memberToDelete?.email || memberToDelete?.name}</strong>.
|
||||
</p>
|
||||
<p className="text-destructive font-medium">
|
||||
Tindakan ini akan menghapus SEMUA data terkait member ini:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
||||
<li>Order dan item order</li>
|
||||
<li>Akses produk</li>
|
||||
<li>Progress video</li>
|
||||
<li>Jadwal konsultasi</li>
|
||||
<li>Event kalender</li>
|
||||
<li>Role admin (jika ada)</li>
|
||||
<li>Profil user</li>
|
||||
<li>Akun autentikasi</li>
|
||||
</ul>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Tindakan ini <strong>TIDAK BISA dibatalkan</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Batal</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={deleteMember}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<span className="animate-spin mr-2">⏳</span>
|
||||
Menghapus...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Ya, Hapus Member
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user