Merge main: Guidad Felsökning flyttar ur roten till felsokning/

Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.

För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:

  felsokning/app        webbklienten (Vite, egen package.json och
                        eslint-/vitest-konfiguration)
  felsokning/services   plattformstjänsten och AI-orkestern
  felsokning/infra      Terraform och databasschemat
  felsokning/docs       vision, moduler, drift
  felsokning/supabase   edge-funktion och migrationer

Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.

Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.

CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.

Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-04 12:22:49 +00:00
298 changed files with 23491 additions and 6140 deletions
@@ -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,399 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Sparkles, RefreshCw, Building2, Users, MapPin, Mail, Search, Copy, Check, Send } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
interface Lead {
company_name: string;
industry: string;
employee_count: number;
district: string;
reason: string;
}
interface EmailContent {
subject: string;
body: string;
personalization_points: string[];
}
interface ResearchResult {
company_name: string;
research_summary: string;
email: EmailContent;
}
export function LeadsGenerator() {
const [leads, setLeads] = useState<Lead[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [researchingLead, setResearchingLead] = useState<string | null>(null);
const [emailDialog, setEmailDialog] = useState<ResearchResult | null>(null);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [recipient, setRecipient] = useState("");
const [sending, setSending] = useState(false);
const { toast } = useToast();
useEffect(() => {
setRecipient("");
}, [emailDialog?.company_name]);
const sendEmail = async () => {
if (!emailDialog || !recipient) return;
setSending(true);
try {
const res = await fetch(
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/send-outreach-email`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({
to_email: recipient,
to_name: emailDialog.company_name,
subject: emailDialog.email.subject,
body: emailDialog.email.body,
}),
},
);
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Kunde inte skicka mejl");
toast({
title: "Mejl skickat!",
description: `Skickat från konditorivaror@gmail.com till ${recipient}`,
});
setEmailDialog(null);
} catch (err) {
toast({
title: "Fel vid utskick",
description: err instanceof Error ? err.message : "Okänt fel",
variant: "destructive",
});
} finally {
setSending(false);
}
};
const generateLeads = async () => {
setIsLoading(true);
try {
const response = await fetch(
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/generate-leads`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`,
},
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Kunde inte generera leads");
}
const data = await response.json();
setLeads(data.leads || []);
toast({
title: "Leads genererade!",
description: `${data.leads?.length || 0} potentiella kunder hittades.`,
});
} catch (error) {
console.error("Error generating leads:", error);
toast({
title: "Fel",
description: error instanceof Error ? error.message : "Kunde inte generera leads",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
const researchCompany = async (lead: Lead) => {
setResearchingLead(lead.company_name);
try {
const response = await fetch(
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/research-company`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({
company_name: lead.company_name,
district: lead.district,
industry: lead.industry,
}),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Kunde inte researcha företaget");
}
const data: ResearchResult = await response.json();
setEmailDialog(data);
toast({
title: "Research klar!",
description: `Personligt mejl genererat för ${lead.company_name}`,
});
} catch (error) {
console.error("Error researching company:", error);
toast({
title: "Fel",
description: error instanceof Error ? error.message : "Kunde inte researcha företaget",
variant: "destructive",
});
} finally {
setResearchingLead(null);
}
};
const copyToClipboard = async (text: string, field: string) => {
await navigator.clipboard.writeText(text);
setCopiedField(field);
setTimeout(() => setCopiedField(null), 2000);
toast({
title: "Kopierat!",
description: `${field} kopierat till urklipp`,
});
};
const getEmployeeCountBadge = (count: number) => {
if (count >= 100) return "bg-green-100 text-green-800";
if (count >= 50) return "bg-blue-100 text-blue-800";
if (count >= 25) return "bg-yellow-100 text-yellow-800";
return "bg-gray-100 text-gray-800";
};
return (
<>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
AI Kundprospektering
</CardTitle>
<CardDescription>
Generera leads och skapa personliga mejl med AI-driven research
</CardDescription>
</div>
<Button onClick={generateLeads} disabled={isLoading}>
{isLoading ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
Genererar...
</>
) : (
<>
<Sparkles className="h-4 w-4 mr-2" />
Generera leads
</>
)}
</Button>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : leads.length > 0 ? (
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>
<div className="flex items-center gap-1">
<Building2 className="h-4 w-4" />
Företag
</div>
</TableHead>
<TableHead>Bransch</TableHead>
<TableHead>
<div className="flex items-center gap-1">
<Users className="h-4 w-4" />
Anställda
</div>
</TableHead>
<TableHead>
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
Stadsdel
</div>
</TableHead>
<TableHead>Varför bra kund</TableHead>
<TableHead className="text-right">Åtgärd</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{leads.map((lead, index) => (
<TableRow key={index}>
<TableCell className="font-medium">{lead.company_name}</TableCell>
<TableCell>
<Badge variant="outline">{lead.industry}</Badge>
</TableCell>
<TableCell>
<Badge className={getEmployeeCountBadge(lead.employee_count)}>
{lead.employee_count} st
</Badge>
</TableCell>
<TableCell>{lead.district}</TableCell>
<TableCell className="max-w-xs text-sm text-muted-foreground">
{lead.reason}
</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="outline"
onClick={() => researchCompany(lead)}
disabled={researchingLead === lead.company_name}
>
{researchingLead === lead.company_name ? (
<>
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
Researchar...
</>
) : (
<>
<Search className="h-3 w-3 mr-1" />
Research & Mejl
</>
)}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
<Sparkles className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Klicka "Generera leads" för att hitta potentiella kunder</p>
</div>
)}
</CardContent>
</Card>
<Dialog open={!!emailDialog} onOpenChange={() => setEmailDialog(null)}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Personligt mejl för {emailDialog?.company_name}
</DialogTitle>
<DialogDescription>
AI-genererat mejl baserat företagsresearch
</DialogDescription>
</DialogHeader>
{emailDialog && (
<div className="space-y-4">
{emailDialog.research_summary && (
<div className="p-3 bg-muted rounded-lg">
<p className="text-xs font-medium text-muted-foreground mb-1">Research-sammanfattning:</p>
<p className="text-sm">{emailDialog.research_summary.substring(0, 300)}...</p>
</div>
)}
<div className="space-y-3">
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-sm font-medium">Ämnesrad:</label>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(emailDialog.email.subject, "Ämnesrad")}
>
{copiedField === "Ämnesrad" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<div className="p-3 bg-background border rounded-md">
<p className="font-medium">{emailDialog.email.subject}</p>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-sm font-medium">Mejltext:</label>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(emailDialog.email.body, "Mejltext")}
>
{copiedField === "Mejltext" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<div className="p-3 bg-background border rounded-md whitespace-pre-wrap">
{emailDialog.email.body}
</div>
</div>
{emailDialog.email.personalization_points?.length > 0 && (
<div>
<label className="text-sm font-medium mb-2 block">Personaliseringspunkter:</label>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
{emailDialog.email.personalization_points.map((point, i) => (
<li key={i}>{point}</li>
))}
</ul>
</div>
)}
<div className="pt-4 border-t space-y-2">
<label className="text-sm font-medium">Skicka till (mottagarens mejl):</label>
<div className="flex gap-2">
<Input
type="email"
placeholder="kontakt@företag.se"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
disabled={sending}
/>
<Button
onClick={sendEmail}
disabled={sending || !recipient || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient)}
>
{sending ? (
<><RefreshCw className="h-4 w-4 mr-2 animate-spin" /> Skickar...</>
) : (
<><Send className="h-4 w-4 mr-2" /> Skicka mejl</>
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Skickas från <strong>konditorivaror@gmail.com</strong> som Tiffany.
</p>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
);
}
@@ -0,0 +1,162 @@
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>Leveransadress</TableHead>
<TableHead>Datum</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Belopp</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredOrders?.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
Inga ordrar hittades
</TableCell>
</TableRow>
) : (
filteredOrders?.map((order) => {
const item = Array.isArray(order.items) ? order.items[0] : null;
const lev = item?.leverans;
const phone = item?.telefon;
const comments = item?.kommentarer;
return (
<TableRow key={order.id}>
<TableCell className="font-medium align-top">{order.order_number}</TableCell>
<TableCell className="align-top">
<div>
<div className="font-medium">{order.customer_name || "—"}</div>
<div className="text-sm text-muted-foreground">{order.customer_email}</div>
{phone && <div className="text-sm text-muted-foreground">{phone}</div>}
</div>
</TableCell>
<TableCell className="align-top">
{lev ? (
<div className="text-sm">
<div>{lev.adress || "—"}</div>
<div className="text-muted-foreground">
{[lev.postnummer, lev.stad].filter(Boolean).join(" ")}
</div>
{comments && (
<div className="text-xs text-muted-foreground mt-1 italic">
"{comments}"
</div>
)}
</div>
) : (
<span className="text-muted-foreground text-sm"></span>
)}
</TableCell>
<TableCell className="align-top">
{format(new Date(order.created_at), "d MMM yyyy, HH:mm", { locale: sv })}
</TableCell>
<TableCell className="align-top">
<Badge variant="outline" className={statusColors[order.status] || ""}>
{statusLabels[order.status] || order.status}
</Badge>
</TableCell>
<TableCell className="text-right font-medium align-top">
{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>
);
}