From 3ce5a730d5bb08eb445553dafbb846a1901321c1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:26:48 +0000 Subject: [PATCH] Changes --- src/App.tsx | 2 + src/components/admin/AdminStats.tsx | 69 +++++++++ src/components/admin/CustomersTable.tsx | 95 ++++++++++++ src/components/admin/OrdersTable.tsx | 137 ++++++++++++++++++ src/components/admin/PaymentsTable.tsx | 115 +++++++++++++++ src/hooks/useAdminData.ts | 110 ++++++++++++++ src/integrations/supabase/types.ts | 90 +++++++++++- src/pages/Admin.tsx | 120 +++++++++++++++ ...3_9ff3ec88-8d5a-4590-ab11-a68aebc1a3f9.sql | 50 +++++++ 9 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 src/components/admin/AdminStats.tsx create mode 100644 src/components/admin/CustomersTable.tsx create mode 100644 src/components/admin/OrdersTable.tsx create mode 100644 src/components/admin/PaymentsTable.tsx create mode 100644 src/hooks/useAdminData.ts create mode 100644 src/pages/Admin.tsx create mode 100644 supabase/migrations/20260128002513_9ff3ec88-8d5a-4590-ab11-a68aebc1a3f9.sql diff --git a/src/App.tsx b/src/App.tsx index 65f0c5e..00f4eac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import Profile from "./pages/Profile"; import Account from "./pages/Account"; import AccountConfirmation from "./pages/AccountConfirmation"; import Contact from "./pages/Contact"; +import Admin from "./pages/Admin"; import { useCartSync } from "./hooks/useCartSync"; const queryClient = new QueryClient(); @@ -26,6 +27,7 @@ function AppContent() { } /> } /> } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/admin/AdminStats.tsx b/src/components/admin/AdminStats.tsx new file mode 100644 index 0000000..4a93339 --- /dev/null +++ b/src/components/admin/AdminStats.tsx @@ -0,0 +1,69 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Package, Users, CreditCard, TrendingUp } from "lucide-react"; +import { useAdminStats } from "@/hooks/useAdminData"; +import { Skeleton } from "@/components/ui/skeleton"; + +export function AdminStats() { + const { data: stats, isLoading } = useAdminStats(); + + if (isLoading) { + return ( +
+ {[...Array(4)].map((_, i) => ( + + + + + + + + + ))} +
+ ); + } + + const statCards = [ + { + title: "Totala Ordrar", + value: stats?.totalOrders || 0, + icon: Package, + description: `${stats?.pendingOrders || 0} väntande`, + }, + { + title: "Kunder", + value: stats?.totalCustomers || 0, + icon: Users, + description: "Registrerade kunder", + }, + { + title: "Betalningar", + value: stats?.totalPayments || 0, + icon: CreditCard, + description: `${stats?.completedPayments || 0} genomförda`, + }, + { + title: "Total Omsättning", + value: `${(stats?.totalRevenue || 0).toLocaleString("sv-SE")} kr`, + icon: TrendingUp, + description: "Totalt intjänat", + }, + ]; + + return ( +
+ {statCards.map((stat) => ( + + + {stat.title} + + + +
{stat.value}
+

{stat.description}

+
+
+ ))} +
+ ); +} diff --git a/src/components/admin/CustomersTable.tsx b/src/components/admin/CustomersTable.tsx new file mode 100644 index 0000000..d720d56 --- /dev/null +++ b/src/components/admin/CustomersTable.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { useCustomers } from "@/hooks/useAdminData"; +import { Skeleton } from "@/components/ui/skeleton"; +import { format } from "date-fns"; +import { sv } from "date-fns/locale"; +import { Search } from "lucide-react"; + +export function CustomersTable() { + const { data: customers, isLoading } = useCustomers(); + const [search, setSearch] = useState(""); + + const filteredCustomers = customers?.filter((customer) => { + return ( + customer.email.toLowerCase().includes(search.toLowerCase()) || + customer.name?.toLowerCase().includes(search.toLowerCase()) || + customer.company?.toLowerCase().includes(search.toLowerCase()) + ); + }); + + if (isLoading) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ + setSearch(e.target.value)} + className="pl-9 max-w-md" + /> +
+ +
+ + + + Kund + Företag + Telefon + Ordrar + Totalt spenderat + Registrerad + + + + {filteredCustomers?.length === 0 ? ( + + + Inga kunder hittades + + + ) : ( + filteredCustomers?.map((customer) => ( + + +
+
{customer.name || "—"}
+
{customer.email}
+
+
+ {customer.company || "—"} + {customer.phone || "—"} + {customer.total_orders} + + {Number(customer.total_spent).toLocaleString("sv-SE")} kr + + + {format(new Date(customer.created_at), "d MMM yyyy", { locale: sv })} + +
+ )) + )} +
+
+
+
+ ); +} diff --git a/src/components/admin/OrdersTable.tsx b/src/components/admin/OrdersTable.tsx new file mode 100644 index 0000000..d1b0613 --- /dev/null +++ b/src/components/admin/OrdersTable.tsx @@ -0,0 +1,137 @@ +import { useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useOrders, Order } from "@/hooks/useAdminData"; +import { Skeleton } from "@/components/ui/skeleton"; +import { format } from "date-fns"; +import { sv } from "date-fns/locale"; +import { Search } from "lucide-react"; + +const statusColors: Record = { + pending: "bg-yellow-500/20 text-yellow-700 border-yellow-500/30", + processing: "bg-blue-500/20 text-blue-700 border-blue-500/30", + completed: "bg-green-500/20 text-green-700 border-green-500/30", + cancelled: "bg-red-500/20 text-red-700 border-red-500/30", +}; + +const statusLabels: Record = { + pending: "Väntande", + processing: "Behandlas", + completed: "Slutförd", + cancelled: "Avbruten", +}; + +export function OrdersTable() { + const { data: orders, isLoading } = useOrders(); + const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + const filteredOrders = orders?.filter((order) => { + const matchesSearch = + order.order_number.toLowerCase().includes(search.toLowerCase()) || + order.customer_email.toLowerCase().includes(search.toLowerCase()) || + order.customer_name?.toLowerCase().includes(search.toLowerCase()); + + const matchesStatus = statusFilter === "all" || order.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + if (isLoading) { + return ( +
+ + +
+ ); + } + + return ( +
+
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ +
+ +
+ + + + Ordernummer + Kund + Datum + Status + Belopp + + + + {filteredOrders?.length === 0 ? ( + + + Inga ordrar hittades + + + ) : ( + filteredOrders?.map((order) => ( + + {order.order_number} + +
+
{order.customer_name || "—"}
+
{order.customer_email}
+
+
+ + {format(new Date(order.created_at), "d MMM yyyy, HH:mm", { locale: sv })} + + + + {statusLabels[order.status] || order.status} + + + + {Number(order.total_amount).toLocaleString("sv-SE")} {order.currency} + +
+ )) + )} +
+
+
+
+ ); +} diff --git a/src/components/admin/PaymentsTable.tsx b/src/components/admin/PaymentsTable.tsx new file mode 100644 index 0000000..550e832 --- /dev/null +++ b/src/components/admin/PaymentsTable.tsx @@ -0,0 +1,115 @@ +import { useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { usePayments } from "@/hooks/useAdminData"; +import { Skeleton } from "@/components/ui/skeleton"; +import { format } from "date-fns"; +import { sv } from "date-fns/locale"; + +const statusColors: Record = { + pending: "bg-yellow-500/20 text-yellow-700 border-yellow-500/30", + completed: "bg-green-500/20 text-green-700 border-green-500/30", + failed: "bg-red-500/20 text-red-700 border-red-500/30", + refunded: "bg-purple-500/20 text-purple-700 border-purple-500/30", +}; + +const statusLabels: Record = { + pending: "Väntande", + completed: "Genomförd", + failed: "Misslyckad", + refunded: "Återbetald", +}; + +export function PaymentsTable() { + const { data: payments, isLoading } = usePayments(); + const [statusFilter, setStatusFilter] = useState("all"); + + const filteredPayments = payments?.filter((payment) => { + return statusFilter === "all" || payment.status === statusFilter; + }); + + if (isLoading) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ +
+ +
+ + + + Betalnings-ID + Betalmetod + Datum + Status + Belopp + + + + {filteredPayments?.length === 0 ? ( + + + Inga betalningar hittades + + + ) : ( + filteredPayments?.map((payment) => ( + + + {payment.id.slice(0, 8)}... + + {payment.payment_method || "—"} + + {format(new Date(payment.created_at), "d MMM yyyy, HH:mm", { locale: sv })} + + + + {statusLabels[payment.status] || payment.status} + + + + {Number(payment.amount).toLocaleString("sv-SE")} {payment.currency} + + + )) + )} + +
+
+
+ ); +} diff --git a/src/hooks/useAdminData.ts b/src/hooks/useAdminData.ts new file mode 100644 index 0000000..23f30d0 --- /dev/null +++ b/src/hooks/useAdminData.ts @@ -0,0 +1,110 @@ +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; + +export interface Order { + id: string; + order_number: string; + customer_email: string; + customer_name: string | null; + items: any[]; + total_amount: number; + currency: string; + status: string; + created_at: string; +} + +export interface Customer { + id: string; + email: string; + name: string | null; + phone: string | null; + company: string | null; + total_orders: number; + total_spent: number; + created_at: string; +} + +export interface Payment { + id: string; + order_id: string | null; + amount: number; + currency: string; + status: string; + payment_method: string | null; + created_at: string; +} + +export function useOrders() { + return useQuery({ + queryKey: ["admin-orders"], + queryFn: async () => { + const { data, error } = await supabase + .from("orders") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + return data as Order[]; + }, + }); +} + +export function useCustomers() { + return useQuery({ + queryKey: ["admin-customers"], + queryFn: async () => { + const { data, error } = await supabase + .from("customers") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + return data as Customer[]; + }, + }); +} + +export function usePayments() { + return useQuery({ + queryKey: ["admin-payments"], + queryFn: async () => { + const { data, error } = await supabase + .from("payments") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + return data as Payment[]; + }, + }); +} + +export function useAdminStats() { + return useQuery({ + queryKey: ["admin-stats"], + queryFn: async () => { + const [ordersRes, customersRes, paymentsRes] = await Promise.all([ + supabase.from("orders").select("total_amount, status"), + supabase.from("customers").select("id"), + supabase.from("payments").select("amount, status"), + ]); + + const orders = ordersRes.data || []; + const customers = customersRes.data || []; + const payments = paymentsRes.data || []; + + const totalRevenue = orders.reduce((sum, o) => sum + Number(o.total_amount), 0); + const pendingOrders = orders.filter(o => o.status === "pending").length; + const completedPayments = payments.filter(p => p.status === "completed").length; + + return { + totalOrders: orders.length, + totalCustomers: customers.length, + totalRevenue, + pendingOrders, + totalPayments: payments.length, + completedPayments, + }; + }, + }); +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 59198fc..f347d25 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -14,11 +14,48 @@ export type Database = { } public: { Tables: { + customers: { + Row: { + company: string | null + created_at: string + email: string + id: string + name: string | null + phone: string | null + total_orders: number | null + total_spent: number | null + updated_at: string + } + Insert: { + company?: string | null + created_at?: string + email: string + id?: string + name?: string | null + phone?: string | null + total_orders?: number | null + total_spent?: number | null + updated_at?: string + } + Update: { + company?: string | null + created_at?: string + email?: string + id?: string + name?: string | null + phone?: string | null + total_orders?: number | null + total_spent?: number | null + updated_at?: string + } + Relationships: [] + } orders: { Row: { created_at: string currency: string customer_email: string + customer_id: string | null customer_name: string | null id: string items: Json @@ -32,6 +69,7 @@ export type Database = { created_at?: string currency?: string customer_email: string + customer_id?: string | null customer_name?: string | null id?: string items?: Json @@ -45,6 +83,7 @@ export type Database = { created_at?: string currency?: string customer_email?: string + customer_id?: string | null customer_name?: string | null id?: string items?: Json @@ -54,7 +93,56 @@ export type Database = { total_amount?: number updated_at?: string } - Relationships: [] + Relationships: [ + { + foreignKeyName: "orders_customer_id_fkey" + columns: ["customer_id"] + isOneToOne: false + referencedRelation: "customers" + referencedColumns: ["id"] + }, + ] + } + payments: { + Row: { + amount: number + created_at: string + currency: string + id: string + order_id: string | null + payment_method: string | null + shopify_payment_id: string | null + status: string + } + Insert: { + amount: number + created_at?: string + currency?: string + id?: string + order_id?: string | null + payment_method?: string | null + shopify_payment_id?: string | null + status?: string + } + Update: { + amount?: number + created_at?: string + currency?: string + id?: string + order_id?: string | null + payment_method?: string | null + shopify_payment_id?: string | null + status?: string + } + Relationships: [ + { + foreignKeyName: "payments_order_id_fkey" + columns: ["order_id"] + isOneToOne: false + referencedRelation: "orders" + referencedColumns: ["id"] + }, + ] } profiles: { Row: { diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx new file mode 100644 index 0000000..83370f5 --- /dev/null +++ b/src/pages/Admin.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { supabase } from "@/integrations/supabase/client"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { AdminStats } from "@/components/admin/AdminStats"; +import { OrdersTable } from "@/components/admin/OrdersTable"; +import { CustomersTable } from "@/components/admin/CustomersTable"; +import { PaymentsTable } from "@/components/admin/PaymentsTable"; +import { Package, Users, CreditCard, LogOut, ArrowLeft } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Admin() { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const checkAuth = async () => { + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + navigate("/"); + return; + } + setIsAuthenticated(true); + setLoading(false); + }; + + const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { + if (!session) { + navigate("/"); + } + }); + + checkAuth(); + + return () => subscription.unsubscribe(); + }, [navigate]); + + const handleLogout = async () => { + await supabase.auth.signOut(); + navigate("/"); + }; + + if (loading) { + return ( +
+
+ +
+ {[...Array(4)].map((_, i) => ( + + ))} +
+ +
+
+ ); + } + + if (!isAuthenticated) { + return null; + } + + return ( +
+
+
+
+
+ +

Admin Dashboard

+
+ +
+
+
+ +
+
+ + + + + + + Ordrar + + + + Kunder + + + + Betalningar + + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/supabase/migrations/20260128002513_9ff3ec88-8d5a-4590-ab11-a68aebc1a3f9.sql b/supabase/migrations/20260128002513_9ff3ec88-8d5a-4590-ab11-a68aebc1a3f9.sql new file mode 100644 index 0000000..ac85201 --- /dev/null +++ b/supabase/migrations/20260128002513_9ff3ec88-8d5a-4590-ab11-a68aebc1a3f9.sql @@ -0,0 +1,50 @@ +-- Create customers table +CREATE TABLE public.customers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + name TEXT, + phone TEXT, + company TEXT, + total_orders INTEGER DEFAULT 0, + total_spent DECIMAL(10,2) DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Create payments table +CREATE TABLE public.payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID REFERENCES public.orders(id) ON DELETE CASCADE, + amount DECIMAL(10,2) NOT NULL, + currency TEXT NOT NULL DEFAULT 'SEK', + status TEXT NOT NULL DEFAULT 'pending', + payment_method TEXT, + shopify_payment_id TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Add customer reference to orders +ALTER TABLE public.orders ADD COLUMN customer_id UUID REFERENCES public.customers(id); + +-- Enable RLS +ALTER TABLE public.customers ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.payments ENABLE ROW LEVEL SECURITY; + +-- RLS policies for customers (only authenticated admins) +CREATE POLICY "Authenticated users can view customers" +ON public.customers FOR SELECT TO authenticated USING (true); + +CREATE POLICY "Authenticated users can manage customers" +ON public.customers FOR ALL TO authenticated USING (true) WITH CHECK (true); + +-- RLS policies for payments +CREATE POLICY "Authenticated users can view payments" +ON public.payments FOR SELECT TO authenticated USING (true); + +CREATE POLICY "Service can insert payments" +ON public.payments FOR INSERT WITH CHECK (true); + +-- Triggers for updated_at +CREATE TRIGGER update_customers_updated_at +BEFORE UPDATE ON public.customers +FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); \ No newline at end of file