diff --git a/src/pages/admin/AdminOrders.tsx b/src/pages/admin/AdminOrders.tsx index a70a68e..9aae078 100644 --- a/src/pages/admin/AdminOrders.tsx +++ b/src/pages/admin/AdminOrders.tsx @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { formatIDR, formatDateTime } from "@/lib/format"; -import { Eye, CheckCircle, XCircle, Video, ExternalLink } from "lucide-react"; +import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle } from "lucide-react"; import { toast } from "@/hooks/use-toast"; interface Order { @@ -117,6 +117,35 @@ export default function AdminOrders() { } }; + const deleteOrder = async (orderId: string) => { + // Confirm deletion + const confirmed = window.confirm( + "Apakah Anda yakin ingin menghapus order ini? Semua data terkait (review, slot konsultasi, akses produk) akan dihapus secara permanen." + ); + + if (!confirmed) return; + + try { + const { data, error } = await supabase.functions.invoke("delete-order", { + body: { order_id: orderId }, + }); + + if (error || !data?.success) { + throw new Error(data?.error || error?.message || "Gagal menghapus order"); + } + + toast({ title: "Berhasil", description: "Order dan semua data terkait berhasil dihapus" }); + fetchOrders(); + setDialogOpen(false); + } catch (error: any) { + toast({ + title: "Error", + description: error.message || "Gagal menghapus order", + variant: "destructive", + }); + } + }; + const getStatusBadge = (status: string | null) => { switch (status) { case "paid": @@ -290,6 +319,15 @@ export default function AdminOrders() { Batalkan )} + )} diff --git a/supabase/config.toml b/supabase/config.toml index 5a9e9f2..cb068d4 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -41,3 +41,6 @@ verify_jwt = false [functions.send-email-v2] verify_jwt = false + +[functions.delete-order] +verify_jwt = false diff --git a/supabase/functions/delete-order/index.ts b/supabase/functions/delete-order/index.ts new file mode 100644 index 0000000..05372e4 --- /dev/null +++ b/supabase/functions/delete-order/index.ts @@ -0,0 +1,64 @@ +import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", +}; + +interface DeleteOrderRequest { + order_id: string; +} + +serve(async (req: Request): Promise => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const body: DeleteOrderRequest = await req.json(); + const { order_id } = body; + + if (!order_id) { + return new Response( + JSON.stringify({ success: false, error: "order_id is required" }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + console.log("[DELETE-ORDER] Deleting order:", order_id); + + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + // Call the database function to delete the order + const { data, error } = await supabase + .rpc("delete_order", { order_uuid: order_id }); + + if (error) { + console.error("[DELETE-ORDER] Error:", error); + return new Response( + JSON.stringify({ success: false, error: error.message }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + console.log("[DELETE-ORDER] Success:", data); + + return new Response( + JSON.stringify(data), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + + } catch (error: any) { + console.error("[DELETE-ORDER] Unexpected error:", error); + return new Response( + JSON.stringify({ + success: false, + error: error.message || "Internal server error" + }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +}); diff --git a/supabase/migrations/20241223_order_deletion_function.sql b/supabase/migrations/20241223_order_deletion_function.sql new file mode 100644 index 0000000..6330017 --- /dev/null +++ b/supabase/migrations/20241223_order_deletion_function.sql @@ -0,0 +1,85 @@ +-- ============================================================================ +-- Order Deletion Function +-- ============================================================================ +-- Safely deletes an order and all related data in the correct sequence +-- to maintain database integrity and avoid orphaned records. +-- ============================================================================ + +CREATE OR REPLACE FUNCTION delete_order(order_uuid UUID) +RETURNS JSON AS $$ +DECLARE + order_user_id UUID; + order_payment_status TEXT; + deleted_counts JSON; +BEGIN + -- Get order details first + SELECT user_id, payment_status + INTO order_user_id, order_payment_status + FROM orders + WHERE id = order_uuid; + + IF NOT FOUND THEN + RETURN jsonb_build_object( + 'success', false, + 'error', 'Order not found' + ); + END IF; + + -- Log the deletion + RAISE NOTICE 'Deleting order: %', order_uuid; + + -- Step 1: Delete consulting-related reviews + DELETE FROM reviews + WHERE order_id = order_uuid + AND type = 'consulting'; + RAISE NOTICE 'Deleted consulting reviews for order %', order_uuid; + + -- Step 2: Delete consulting slots (this also removes meet_link references) + DELETE FROM consulting_slots + WHERE order_id = order_uuid; + RAISE NOTICE 'Deleted consulting slots for order %', order_uuid; + + -- Step 3: Delete order items + DELETE FROM order_items + WHERE order_id = order_uuid; + RAISE NOTICE 'Deleted order items for order %', order_uuid; + + -- Step 4: Delete user access records (only if order was paid) + IF order_payment_status = 'paid' THEN + DELETE FROM user_access + WHERE user_id = order_user_id + AND product_id IN ( + SELECT product_id FROM order_items WHERE order_id = order_uuid + ); + RAISE NOTICE 'Deleted user access for order %', order_uuid; + END IF; + + -- Step 5: Finally delete the order itself + DELETE FROM orders + WHERE id = order_uuid; + RAISE NOTICE 'Deleted order %', order_uuid; + + -- Build success response with counts + deleted_counts := jsonb_build_object( + 'success', true, + 'order_id', order_uuid, + 'message', 'Order and all related data deleted successfully' + ); + + RETURN deleted_counts; +EXCEPTION + WHEN OTHERS THEN + RETURN jsonb_build_object( + 'success', false, + 'error', SQLERRM, + 'detail', 'Failed to delete order: ' || order_uuid + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission +GRANT EXECUTE ON FUNCTION delete_order(UUID) TO postgres; +GRANT EXECUTE ON FUNCTION delete_order(UUID) TO authenticated; + +-- Add comment +COMMENT ON FUNCTION delete_order(UUID) IS 'Safely deletes an order and all related data (reviews, consulting slots, order items, user access) in the correct sequence to maintain data integrity.';