Changes
This commit is contained in:
@@ -10,6 +10,7 @@ import Profile from "./pages/Profile";
|
|||||||
import Account from "./pages/Account";
|
import Account from "./pages/Account";
|
||||||
import AccountConfirmation from "./pages/AccountConfirmation";
|
import AccountConfirmation from "./pages/AccountConfirmation";
|
||||||
import Contact from "./pages/Contact";
|
import Contact from "./pages/Contact";
|
||||||
|
import Admin from "./pages/Admin";
|
||||||
import { useCartSync } from "./hooks/useCartSync";
|
import { useCartSync } from "./hooks/useCartSync";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -26,6 +27,7 @@ function AppContent() {
|
|||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/account" element={<Account />} />
|
<Route path="/account" element={<Account />} />
|
||||||
<Route path="/account/confirmation" element={<AccountConfirmation />} />
|
<Route path="/account/confirmation" element={<AccountConfirmation />} />
|
||||||
|
<Route path="/admin" element={<Admin />} />
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Skeleton className="h-8 w-16" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{statCards.map((stat) => (
|
||||||
|
<Card key={stat.title}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
|
||||||
|
<stat.icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stat.value}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{stat.description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Sök email, namn eller företag..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="pl-9 max-w-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Kund</TableHead>
|
||||||
|
<TableHead>Företag</TableHead>
|
||||||
|
<TableHead>Telefon</TableHead>
|
||||||
|
<TableHead>Ordrar</TableHead>
|
||||||
|
<TableHead className="text-right">Totalt spenderat</TableHead>
|
||||||
|
<TableHead>Registrerad</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredCustomers?.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||||
|
Inga kunder hittades
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredCustomers?.map((customer) => (
|
||||||
|
<TableRow key={customer.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{customer.name || "—"}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{customer.email}</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{customer.company || "—"}</TableCell>
|
||||||
|
<TableCell>{customer.phone || "—"}</TableCell>
|
||||||
|
<TableCell>{customer.total_orders}</TableCell>
|
||||||
|
<TableCell className="text-right font-medium">
|
||||||
|
{Number(customer.total_spent).toLocaleString("sv-SE")} kr
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(customer.created_at), "d MMM yyyy", { locale: sv })}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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<string>("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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Sök ordernummer, email eller namn..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
|
<SelectValue placeholder="Filtrera status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Alla statusar</SelectItem>
|
||||||
|
<SelectItem value="pending">Väntande</SelectItem>
|
||||||
|
<SelectItem value="processing">Behandlas</SelectItem>
|
||||||
|
<SelectItem value="completed">Slutförd</SelectItem>
|
||||||
|
<SelectItem value="cancelled">Avbruten</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Ordernummer</TableHead>
|
||||||
|
<TableHead>Kund</TableHead>
|
||||||
|
<TableHead>Datum</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Belopp</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredOrders?.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||||
|
Inga ordrar hittades
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredOrders?.map((order) => (
|
||||||
|
<TableRow key={order.id}>
|
||||||
|
<TableCell className="font-medium">{order.order_number}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{order.customer_name || "—"}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{order.customer_email}</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(order.created_at), "d MMM yyyy, HH:mm", { locale: sv })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className={statusColors[order.status] || ""}>
|
||||||
|
{statusLabels[order.status] || order.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium">
|
||||||
|
{Number(order.total_amount).toLocaleString("sv-SE")} {order.currency}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
pending: "Väntande",
|
||||||
|
completed: "Genomförd",
|
||||||
|
failed: "Misslyckad",
|
||||||
|
refunded: "Återbetald",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PaymentsTable() {
|
||||||
|
const { data: payments, isLoading } = usePayments();
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||||
|
|
||||||
|
const filteredPayments = payments?.filter((payment) => {
|
||||||
|
return statusFilter === "all" || payment.status === statusFilter;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex">
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Filtrera status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Alla statusar</SelectItem>
|
||||||
|
<SelectItem value="pending">Väntande</SelectItem>
|
||||||
|
<SelectItem value="completed">Genomförd</SelectItem>
|
||||||
|
<SelectItem value="failed">Misslyckad</SelectItem>
|
||||||
|
<SelectItem value="refunded">Återbetald</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Betalnings-ID</TableHead>
|
||||||
|
<TableHead>Betalmetod</TableHead>
|
||||||
|
<TableHead>Datum</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Belopp</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredPayments?.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||||
|
Inga betalningar hittades
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredPayments?.map((payment) => (
|
||||||
|
<TableRow key={payment.id}>
|
||||||
|
<TableCell className="font-mono text-sm">
|
||||||
|
{payment.id.slice(0, 8)}...
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{payment.payment_method || "—"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(payment.created_at), "d MMM yyyy, HH:mm", { locale: sv })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className={statusColors[payment.status] || ""}>
|
||||||
|
{statusLabels[payment.status] || payment.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-medium">
|
||||||
|
{Number(payment.amount).toLocaleString("sv-SE")} {payment.currency}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -14,11 +14,48 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
public: {
|
public: {
|
||||||
Tables: {
|
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: {
|
orders: {
|
||||||
Row: {
|
Row: {
|
||||||
created_at: string
|
created_at: string
|
||||||
currency: string
|
currency: string
|
||||||
customer_email: string
|
customer_email: string
|
||||||
|
customer_id: string | null
|
||||||
customer_name: string | null
|
customer_name: string | null
|
||||||
id: string
|
id: string
|
||||||
items: Json
|
items: Json
|
||||||
@@ -32,6 +69,7 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
currency?: string
|
currency?: string
|
||||||
customer_email: string
|
customer_email: string
|
||||||
|
customer_id?: string | null
|
||||||
customer_name?: string | null
|
customer_name?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
items?: Json
|
items?: Json
|
||||||
@@ -45,6 +83,7 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
currency?: string
|
currency?: string
|
||||||
customer_email?: string
|
customer_email?: string
|
||||||
|
customer_id?: string | null
|
||||||
customer_name?: string | null
|
customer_name?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
items?: Json
|
items?: Json
|
||||||
@@ -54,7 +93,56 @@ export type Database = {
|
|||||||
total_amount?: number
|
total_amount?: number
|
||||||
updated_at?: string
|
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: {
|
profiles: {
|
||||||
Row: {
|
Row: {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="min-h-screen bg-background p-8">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-8">
|
||||||
|
<Skeleton className="h-10 w-48" />
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-32" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-96" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="border-b">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => navigate("/")}>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-2xl font-bold">Admin Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={handleLogout}>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Logga ut
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AdminStats />
|
||||||
|
|
||||||
|
<Tabs defaultValue="orders" className="space-y-6">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="orders" className="gap-2">
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
Ordrar
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="customers" className="gap-2">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
Kunder
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="payments" className="gap-2">
|
||||||
|
<CreditCard className="h-4 w-4" />
|
||||||
|
Betalningar
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="orders">
|
||||||
|
<OrdersTable />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="customers">
|
||||||
|
<CustomersTable />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="payments">
|
||||||
|
<PaymentsTable />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user