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