Changes
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user