From 7a14aa0f752902043bbf825d46d050a8fe661d2f Mon Sep 17 00:00:00 2001 From: DanielS Date: Wed, 24 Jun 2026 17:12:40 +0200 Subject: [PATCH] feat: add company management and user-company assignment in admin panel --- shop/app/admin/companies/page.tsx | 186 ++++++++++++++++++ shop/app/admin/einstellungen/page.tsx | 4 + shop/app/admin/layout.tsx | 6 +- shop/app/admin/users/page.tsx | 5 +- shop/components/admin/user-list.tsx | 45 ++++- shop/lib/actions/companies.ts | 49 +++++ shop/lib/actions/users.ts | 35 +++- .../20260624100000_create_companies_table.sql | 31 +++ 8 files changed, 346 insertions(+), 15 deletions(-) create mode 100644 shop/app/admin/companies/page.tsx create mode 100644 shop/lib/actions/companies.ts create mode 100644 shop/supabase/migrations/20260624100000_create_companies_table.sql diff --git a/shop/app/admin/companies/page.tsx b/shop/app/admin/companies/page.tsx new file mode 100644 index 0000000..59f9228 --- /dev/null +++ b/shop/app/admin/companies/page.tsx @@ -0,0 +1,186 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Building2, Plus, Trash2, Loader2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { getCompanies, createCompany, deleteCompany } from '@/lib/actions/companies' + +export default function CompaniesPage() { + const [companies, setCompanies] = useState([]) + const [loading, setLoading] = useState(true) + const [isCreateOpen, setIsCreateOpen] = useState(false) + const [name, setName] = useState('') + const [creating, setCreating] = useState(false) + const [createError, setCreateError] = useState(null) + const [deletingId, setDeletingId] = useState(null) + + const loadCompanies = async () => { + setLoading(true) + try { + const data = await getCompanies() + setCompanies(data) + } catch (err) { + console.error('Fehler beim Laden der Firmen:', err) + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadCompanies() + }, []) + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault() + setCreating(true) + setCreateError(null) + try { + await createCompany(name) + setIsCreateOpen(false) + setName('') + loadCompanies() + } catch (err: any) { + setCreateError(err.message || 'Fehler beim Anlegen.') + } finally { + setCreating(false) + } + } + + const handleDelete = async (id: string, firmName: string) => { + if (!confirm(`Firma "${firmName}" wirklich löschen? Alle zugewiesenen Partner werden getrennt.`)) return + setDeletingId(id) + try { + await deleteCompany(id) + loadCompanies() + } catch (err) { + console.error('Fehler beim Löschen:', err) + } finally { + setDeletingId(null) + } + } + + return ( +
+ {/* Header */} +
+
+

+ + Firmenverwaltung +

+

+ Verwalten Sie Firmen und weisen Sie Partner zu. +

+
+ +
+ + {/* Firmen-Tabelle */} +
+ {loading ? ( +
+ + Lade Firmen... +
+ ) : companies.length === 0 ? ( +
+ + Noch keine Firmen angelegt. +
+ ) : ( + + + + + + + + + + {companies.map((company) => ( + + + + + + ))} + +
FirmennameErstellt amAktionen
+
+ + {company.name} +
+
+ {company.created_at ? new Date(company.created_at).toLocaleDateString('de-DE') : '–'} + + +
+ )} +
+ + {/* Dialog: Neue Firma */} + + + + Neue Firma anlegen + + Geben Sie den Namen der Firma ein. Partner können danach zugewiesen werden. + + +
+
+ + setName(e.target.value)} + className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10" + /> +
+ {createError && ( +

{createError}

+ )} + + + + +
+
+
+
+ ) +} diff --git a/shop/app/admin/einstellungen/page.tsx b/shop/app/admin/einstellungen/page.tsx index aa2f53e..30917be 100644 --- a/shop/app/admin/einstellungen/page.tsx +++ b/shop/app/admin/einstellungen/page.tsx @@ -169,6 +169,7 @@ export default function AdminSettings() { Partner E-Mail + Firma Status Erstellt am Aktionen @@ -186,6 +187,9 @@ export default function AdminSettings() { {partner.email} + + {partner.company_name || '–'} + {isGesperrt ? ( diff --git a/shop/app/admin/layout.tsx b/shop/app/admin/layout.tsx index 92da7b3..db2ee24 100644 --- a/shop/app/admin/layout.tsx +++ b/shop/app/admin/layout.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import { LayoutDashboard, Package, Settings, Users, LayoutGrid, ShoppingCart, Wrench, Database } from 'lucide-react' +import { LayoutDashboard, Package, Settings, Users, LayoutGrid, ShoppingCart, Wrench, Database, Building2 } from 'lucide-react' import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' import { AdminLogoutButton } from '@/components/admin/logout-menu-item' @@ -58,6 +58,10 @@ export default async function AdminLayout({ Benutzer + + + Firmen +
Einstellungen diff --git a/shop/app/admin/users/page.tsx b/shop/app/admin/users/page.tsx index 157220d..cd5f40b 100644 --- a/shop/app/admin/users/page.tsx +++ b/shop/app/admin/users/page.tsx @@ -1,4 +1,5 @@ import { getUsers } from '@/lib/actions/users' +import { getCompanies } from '@/lib/actions/companies' import { UserList } from '@/components/admin/user-list' import { UserDialog } from '@/components/admin/user-dialog' import { Users } from 'lucide-react' @@ -32,8 +33,8 @@ export default async function AdminUsersPage() { async function UserDataWrapper() { try { - const users = await getUsers() - return + const [users, companies] = await Promise.all([getUsers(), getCompanies()]) + return } catch (error: any) { console.error('[UserDataWrapper] Error loading users:', error) return ( diff --git a/shop/components/admin/user-list.tsx b/shop/components/admin/user-list.tsx index 6499d9d..0ebd917 100644 --- a/shop/components/admin/user-list.tsx +++ b/shop/components/admin/user-list.tsx @@ -11,9 +11,10 @@ import { TableRow, } from '@/components/ui/table' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Key, Trash2, Mail, Loader2 } from 'lucide-react' +import { Key, Trash2, Mail, Loader2, Building2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { deleteUser, resetPassword, updateUserRole } from '@/lib/actions/users' +import { assignCompanyToUser } from '@/lib/actions/companies' import { Badge } from '@/components/ui/badge' type Role = 'admin' | 'partner' | 'gesperrt' @@ -30,7 +31,12 @@ const ROLE_BADGE_CLASSES: Record = { gesperrt: 'bg-red-500/20 text-red-400 border border-red-500/30', } -export function UserList({ initialUsers }: { initialUsers: any[] }) { +interface UserListProps { + initialUsers: any[] + companies: { id: string; name: string }[] +} + +export function UserList({ initialUsers, companies }: UserListProps) { const router = useRouter() const [loadingId, setLoadingId] = useState(null) @@ -55,6 +61,18 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { } } + const handleCompanyChange = async (userId: string, companyId: string) => { + setLoadingId(userId + '-company') + try { + await assignCompanyToUser(userId, companyId === '' ? null : companyId) + router.refresh() + } catch (e) { + console.error('Fehler beim Zuweisen der Firma:', e) + } finally { + setLoadingId(null) + } + } + const handleResetPassword = async (email: string) => { try { await resetPassword(email) @@ -69,7 +87,7 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { Benutzerverwaltung - Verwalten Sie Rollen, Passwörter und Benutzerkonten. + Rollen, Firmenzuweisungen, Passwörter und Benutzerkonten verwalten. @@ -78,6 +96,7 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { E-Mail Rolle + Firma Zuletzt angemeldet Aktionen @@ -86,6 +105,7 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { {initialUsers.map((user) => { const role: Role = (['admin', 'partner', 'gesperrt'].includes(user.role) ? user.role : 'partner') as Role const isLoadingRole = loadingId === user.id + '-role' + const isLoadingCompany = loadingId === user.id + '-company' const isLoadingDelete = loadingId === user.id return ( @@ -116,6 +136,25 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { )}
+ +
+ + {isLoadingCompany ? ( + + ) : ( + + )} +
+
{user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString('de-DE') : 'Noch nie'} diff --git a/shop/lib/actions/companies.ts b/shop/lib/actions/companies.ts new file mode 100644 index 0000000..9308ac4 --- /dev/null +++ b/shop/lib/actions/companies.ts @@ -0,0 +1,49 @@ +'use server' + +import { createAdminClient } from '@/lib/supabase/admin' +import { revalidatePath } from 'next/cache' + +export async function getCompanies() { + const admin = createAdminClient() + const { data, error } = await admin + .from('companies') + .select('id, name, created_at') + .order('name', { ascending: true }) + if (error) throw error + return data || [] +} + +export async function createCompany(name: string) { + const admin = createAdminClient() + const { data, error } = await admin + .from('companies') + .insert({ name: name.trim() }) + .select() + .single() + if (error) throw error + revalidatePath('/admin/companies') + revalidatePath('/admin/users') + return data +} + +export async function deleteCompany(id: string) { + const admin = createAdminClient() + const { error } = await admin + .from('companies') + .delete() + .eq('id', id) + if (error) throw error + revalidatePath('/admin/companies') + revalidatePath('/admin/users') +} + +export async function assignCompanyToUser(userId: string, companyId: string | null) { + const admin = createAdminClient() + const { error } = await admin + .from('users') + .update({ company_id: companyId }) + .eq('id', userId) + if (error) throw error + revalidatePath('/admin/users') + revalidatePath('/admin/einstellungen') +} diff --git a/shop/lib/actions/users.ts b/shop/lib/actions/users.ts index bb0839a..efa6769 100644 --- a/shop/lib/actions/users.ts +++ b/shop/lib/actions/users.ts @@ -10,18 +10,24 @@ export async function getUsers() { const { data: dbUsers, error: dbError } = await admin .from('users') - .select('id, role') + .select('id, role, company_id, companies(id, name)') - const roleMap: Record = {} + const userMap: Record = {} if (!dbError && dbUsers) { - dbUsers.forEach(u => { - roleMap[u.id] = u.role + dbUsers.forEach((u: any) => { + userMap[u.id] = { + role: u.role, + company_id: u.company_id || null, + company_name: u.companies?.name || null, + } }) } return users.map(u => ({ ...u, - role: roleMap[u.id] || 'partner' + role: userMap[u.id]?.role || 'partner', + company_id: userMap[u.id]?.company_id || null, + company_name: userMap[u.id]?.company_name || null, })) } @@ -90,15 +96,26 @@ export async function getPartners() { const { data: dbUsers, error: dbError } = await admin .from('users') - .select('id, role') + .select('id, role, company_id, companies(id, name)') .in('role', ['partner', 'gesperrt']) if (dbError) throw dbError - const roleMap: Record = {} - dbUsers.forEach(u => { roleMap[u.id] = u.role }) + const userMap: Record = {} + dbUsers.forEach((u: any) => { + userMap[u.id] = { + role: u.role, + company_id: u.company_id || null, + company_name: u.companies?.name || null, + } + }) const partnerIds = new Set(dbUsers.map(u => u.id)) return users .filter(u => partnerIds.has(u.id)) - .map(u => ({ ...u, role: roleMap[u.id] || 'partner' })) + .map(u => ({ + ...u, + role: userMap[u.id]?.role || 'partner', + company_id: userMap[u.id]?.company_id || null, + company_name: userMap[u.id]?.company_name || null, + })) } diff --git a/shop/supabase/migrations/20260624100000_create_companies_table.sql b/shop/supabase/migrations/20260624100000_create_companies_table.sql new file mode 100644 index 0000000..d41e85a --- /dev/null +++ b/shop/supabase/migrations/20260624100000_create_companies_table.sql @@ -0,0 +1,31 @@ +-- Migration: companies table + company_id in users + +-- 1. Tabelle companies erstellen +CREATE TABLE IF NOT EXISTS public.companies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- RLS aktivieren +ALTER TABLE public.companies ENABLE ROW LEVEL SECURITY; + +-- Policies +DROP POLICY IF EXISTS "Admins und Partner lesen Firmen" ON public.companies; +DROP POLICY IF EXISTS "Service role kann alles" ON public.companies; + +CREATE POLICY "Admins und Partner lesen Firmen" + ON public.companies FOR SELECT + USING (auth.role() = 'authenticated'); + +CREATE POLICY "Service role kann alles" + ON public.companies FOR ALL + USING (true) + WITH CHECK (true); + +-- 2. company_id zu public.users hinzufügen +ALTER TABLE public.users + ADD COLUMN IF NOT EXISTS company_id UUID REFERENCES public.companies(id) ON DELETE SET NULL; + +-- Reload Schema Cache +NOTIFY pgrst, 'reload schema';