feat: Dark Mode mit Toggle, localStorage-Persistenz und System-Preference
- Sun/Moon Toggle im Top-Bar - Alle Seiten und Komponenten mit dark: Varianten - Tailwind darkMode: 'class' Strategie - Respektiert prefers-color-scheme als Default
This commit is contained in:
parent
9e3b431e7c
commit
3bad762329
@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Workflow Portal</title>
|
<title>Workflow Portal</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 text-gray-900 antialiased">
|
<body class="bg-gray-50 text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function ConfirmDialog({
|
|||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<div
|
<div
|
||||||
className={`flex h-12 w-12 items-center justify-center rounded-full ${
|
className={`flex h-12 w-12 items-center justify-center rounded-full ${
|
||||||
variant === 'danger' ? 'bg-red-100' : 'bg-amber-100'
|
variant === 'danger' ? 'bg-red-100 dark:bg-red-900/40' : 'bg-amber-100 dark:bg-amber-900/40'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<AlertTriangle
|
<AlertTriangle
|
||||||
@ -37,8 +37,8 @@ export default function ConfirmDialog({
|
|||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-3 text-lg font-semibold text-gray-900">{title}</h3>
|
<h3 className="mt-3 text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
|
||||||
<p className="mt-2 text-sm text-gray-500">{message}</p>
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{message}</p>
|
||||||
<div className="mt-6 flex w-full gap-3">
|
<div className="mt-6 flex w-full gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
@ -15,12 +15,12 @@ export default function EmptyState({
|
|||||||
}: EmptyStateProps) {
|
}: EmptyStateProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100">
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
<Icon className="text-gray-400" size={32} />
|
<Icon className="text-gray-400 dark:text-gray-500" size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-4 text-base font-semibold text-gray-900">{title}</h3>
|
<h3 className="mt-4 text-base font-semibold text-gray-900 dark:text-white">{title}</h3>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="mt-1 text-sm text-gray-500">{description}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
|
||||||
)}
|
)}
|
||||||
{action && <div className="mt-6">{action}</div>}
|
{action && <div className="mt-6">{action}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,13 +6,16 @@ import {
|
|||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
Menu,
|
Menu,
|
||||||
|
Moon,
|
||||||
Settings,
|
Settings,
|
||||||
|
Sun,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Menu as HeadlessMenu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
import { Menu as HeadlessMenu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
import { useDarkMode } from '../hooks/useDarkMode';
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||||
@ -29,6 +32,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const { isDark, toggle: toggleDark } = useDarkMode();
|
||||||
|
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
user?.role === 'SYSTEM_ADMIN' || user?.role === 'TECHNICIAN';
|
user?.role === 'SYSTEM_ADMIN' || user?.role === 'TECHNICIAN';
|
||||||
@ -41,22 +45,22 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
|
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
|
||||||
`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-primary-50 text-primary-700'
|
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-16 items-center gap-3 border-b border-gray-200 px-6">
|
<div className="flex h-16 items-center gap-3 border-b border-gray-200 px-6 dark:border-gray-700">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-600 text-sm font-bold text-white">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-600 text-sm font-bold text-white">
|
||||||
WP
|
WP
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-sm font-semibold text-gray-900">
|
<h1 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
Workflow Portal
|
Workflow Portal
|
||||||
</h1>
|
</h1>
|
||||||
{user?.tenant && (
|
{user?.tenant && (
|
||||||
<p className="text-xs text-gray-500">{user.tenant.name}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">{user.tenant.name}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,8 +81,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<div className="my-4 border-t border-gray-200" />
|
<div className="my-4 border-t border-gray-200 dark:border-gray-700" />
|
||||||
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
Administration
|
Administration
|
||||||
</p>
|
</p>
|
||||||
{adminNavigation.map((item) => (
|
{adminNavigation.map((item) => (
|
||||||
@ -96,17 +100,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 p-4">
|
<div className="border-t border-gray-200 p-4 dark:border-gray-700">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700 dark:bg-primary-900/40 dark:text-primary-400">
|
||||||
{user?.firstName?.[0]}
|
{user?.firstName?.[0]}
|
||||||
{user?.lastName?.[0]}
|
{user?.lastName?.[0]}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-gray-900">
|
<p className="truncate text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{user?.firstName} {user?.lastName}
|
{user?.firstName} {user?.lastName}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-gray-500">{user?.email}</p>
|
<p className="truncate text-xs text-gray-500 dark:text-gray-400">{user?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -114,7 +118,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-50">
|
<div className="flex h-screen overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||||
{/* Mobile sidebar overlay */}
|
{/* Mobile sidebar overlay */}
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div
|
<div
|
||||||
@ -125,13 +129,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
{/* Mobile sidebar */}
|
{/* Mobile sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-white shadow-xl transition-transform duration-200 lg:hidden ${
|
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-white shadow-xl transition-transform duration-200 lg:hidden dark:bg-gray-900 ${
|
||||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen(false)}
|
onClick={() => setSidebarOpen(false)}
|
||||||
className="absolute right-3 top-4 rounded-lg p-1 text-gray-400 hover:bg-gray-100"
|
className="absolute right-3 top-4 rounded-lg p-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
@ -139,25 +143,34 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Desktop sidebar */}
|
{/* Desktop sidebar */}
|
||||||
<aside className="hidden w-64 flex-col border-r border-gray-200 bg-white lg:flex">
|
<aside className="hidden w-64 flex-col border-r border-gray-200 bg-white lg:flex dark:border-gray-700 dark:bg-gray-900">
|
||||||
{sidebarContent}
|
{sidebarContent}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<header className="flex h-16 items-center justify-between border-b border-gray-200 bg-white px-4 lg:px-8">
|
<header className="flex h-16 items-center justify-between border-b border-gray-200 bg-white px-4 lg:px-8 dark:border-gray-700 dark:bg-gray-800">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen(true)}
|
onClick={() => setSidebarOpen(true)}
|
||||||
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 lg:hidden"
|
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 lg:hidden dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<Menu size={22} />
|
<Menu size={22} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Dark mode toggle */}
|
||||||
|
<button
|
||||||
|
onClick={toggleDark}
|
||||||
|
className="mr-2 rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
|
title={isDark ? 'Helles Design' : 'Dunkles Design'}
|
||||||
|
>
|
||||||
|
{isDark ? <Sun size={20} /> : <Moon size={20} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
<HeadlessMenu as="div" className="relative">
|
<HeadlessMenu as="div" className="relative">
|
||||||
<MenuButton className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
<MenuButton className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">
|
||||||
<User size={18} />
|
<User size={18} />
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{user?.firstName} {user?.lastName}
|
{user?.firstName} {user?.lastName}
|
||||||
@ -172,13 +185,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<MenuItems className="absolute right-0 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white py-1 shadow-lg focus:outline-none">
|
<MenuItems className="absolute right-0 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white py-1 shadow-lg focus:outline-none dark:border-gray-700 dark:bg-gray-800">
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ focus }) => (
|
{({ focus }) => (
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className={`flex items-center gap-2 px-4 py-2 text-sm ${
|
className={`flex items-center gap-2 px-4 py-2 text-sm ${
|
||||||
focus ? 'bg-gray-50 text-gray-900' : 'text-gray-700'
|
focus ? 'bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white' : 'text-gray-700 dark:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<User size={16} />
|
<User size={16} />
|
||||||
@ -191,7 +204,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className={`flex w-full items-center gap-2 px-4 py-2 text-sm ${
|
className={`flex w-full items-center gap-2 px-4 py-2 text-sm ${
|
||||||
focus ? 'bg-gray-50 text-gray-900' : 'text-gray-700'
|
focus ? 'bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white' : 'text-gray-700 dark:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<LogOut size={16} />
|
<LogOut size={16} />
|
||||||
|
|||||||
@ -51,17 +51,17 @@ export default function Modal({
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
className={`w-full ${sizeClasses[size]} transform rounded-xl bg-white p-6 shadow-xl transition-all`}
|
className={`w-full ${sizeClasses[size]} transform rounded-xl bg-white p-6 shadow-xl transition-all dark:bg-gray-800`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{title && (
|
{title && (
|
||||||
<DialogTitle className="text-lg font-semibold text-gray-900">
|
<DialogTitle className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="ml-auto rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
className="ml-auto rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -27,13 +27,13 @@ export default function Pagination({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onPageChange(page - 1)}
|
onClick={() => onPageChange(page - 1)}
|
||||||
disabled={page <= 1}
|
disabled={page <= 1}
|
||||||
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
|
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<ChevronLeft size={18} />
|
<ChevronLeft size={18} />
|
||||||
</button>
|
</button>
|
||||||
{pages.map((p, idx) =>
|
{pages.map((p, idx) =>
|
||||||
p === '...' ? (
|
p === '...' ? (
|
||||||
<span key={`dots-${idx}`} className="px-2 text-gray-400">
|
<span key={`dots-${idx}`} className="px-2 text-gray-400 dark:text-gray-500">
|
||||||
...
|
...
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@ -43,7 +43,7 @@ export default function Pagination({
|
|||||||
className={`min-w-[36px] rounded-lg px-3 py-1.5 text-sm font-medium ${
|
className={`min-w-[36px] rounded-lg px-3 py-1.5 text-sm font-medium ${
|
||||||
p === page
|
p === page
|
||||||
? 'bg-primary-600 text-white'
|
? 'bg-primary-600 text-white'
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{p}
|
{p}
|
||||||
@ -53,7 +53,7 @@ export default function Pagination({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onPageChange(page + 1)}
|
onClick={() => onPageChange(page + 1)}
|
||||||
disabled={page >= totalPages}
|
disabled={page >= totalPages}
|
||||||
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
|
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<ChevronRight size={18} />
|
<ChevronRight size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export default function SearchBar({
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search
|
<Search
|
||||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
||||||
size={18}
|
size={18}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@ -48,7 +48,7 @@ export default function SearchBar({
|
|||||||
{internal && (
|
{internal && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleChange('')}
|
onClick={() => handleChange('')}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -6,19 +6,19 @@ const statusConfig: Record<
|
|||||||
> = {
|
> = {
|
||||||
DRAFT: {
|
DRAFT: {
|
||||||
label: 'Entwurf',
|
label: 'Entwurf',
|
||||||
className: 'bg-gray-100 text-gray-700 ring-gray-300',
|
className: 'bg-gray-100 text-gray-700 ring-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:ring-gray-600',
|
||||||
},
|
},
|
||||||
REVIEW: {
|
REVIEW: {
|
||||||
label: 'In Prüfung',
|
label: 'In Prüfung',
|
||||||
className: 'bg-yellow-50 text-yellow-700 ring-yellow-300',
|
className: 'bg-yellow-50 text-yellow-700 ring-yellow-300 dark:bg-yellow-900/30 dark:text-yellow-400 dark:ring-yellow-700',
|
||||||
},
|
},
|
||||||
PUBLISHED: {
|
PUBLISHED: {
|
||||||
label: 'Veröffentlicht',
|
label: 'Veröffentlicht',
|
||||||
className: 'bg-green-50 text-green-700 ring-green-300',
|
className: 'bg-green-50 text-green-700 ring-green-300 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-700',
|
||||||
},
|
},
|
||||||
ARCHIVED: {
|
ARCHIVED: {
|
||||||
label: 'Archiviert',
|
label: 'Archiviert',
|
||||||
className: 'bg-red-50 text-red-700 ring-red-300',
|
className: 'bg-red-50 text-red-700 ring-red-300 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-700',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
38
frontend/src/hooks/useDarkMode.ts
Normal file
38
frontend/src/hooks/useDarkMode.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function getInitialTheme(): boolean {
|
||||||
|
const stored = localStorage.getItem('theme');
|
||||||
|
if (stored === 'dark') return true;
|
||||||
|
if (stored === 'light') return false;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDarkMode() {
|
||||||
|
const [isDark, setIsDark] = useState(getInitialTheme);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (isDark) {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
// Listen for system preference changes when no explicit choice stored
|
||||||
|
useEffect(() => {
|
||||||
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handler = (e: MediaQueryListEvent) => {
|
||||||
|
if (!localStorage.getItem('theme')) {
|
||||||
|
setIsDark(e.matches);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mq.addEventListener('change', handler);
|
||||||
|
return () => mq.removeEventListener('change', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggle = () => setIsDark((prev) => !prev);
|
||||||
|
|
||||||
|
return { isDark, toggle };
|
||||||
|
}
|
||||||
@ -7,24 +7,24 @@
|
|||||||
@apply scroll-smooth;
|
@apply scroll-smooth;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply min-h-screen bg-gray-50 text-gray-900;
|
@apply min-h-screen bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply inline-flex items-center justify-center rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
@apply inline-flex items-center justify-center rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-gray-900;
|
||||||
}
|
}
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@apply inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
@apply inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-900;
|
||||||
}
|
}
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@apply inline-flex items-center justify-center rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
@apply inline-flex items-center justify-center rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-gray-900;
|
||||||
}
|
}
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500;
|
@apply block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
@apply rounded-xl border border-gray-200 bg-white shadow-sm;
|
@apply rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,25 +77,25 @@ export default function Dashboard() {
|
|||||||
label: 'Gesamt',
|
label: 'Gesamt',
|
||||||
value: stats.total,
|
value: stats.total,
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
color: 'bg-primary-50 text-primary-600',
|
color: 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Veröffentlicht',
|
label: 'Veröffentlicht',
|
||||||
value: stats.published,
|
value: stats.published,
|
||||||
icon: FileCheck,
|
icon: FileCheck,
|
||||||
color: 'bg-green-50 text-green-600',
|
color: 'bg-green-50 text-green-600 dark:bg-green-900/30 dark:text-green-400',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'In Prüfung',
|
label: 'In Prüfung',
|
||||||
value: stats.review,
|
value: stats.review,
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
color: 'bg-yellow-50 text-yellow-600',
|
color: 'bg-yellow-50 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Entwurf',
|
label: 'Entwurf',
|
||||||
value: stats.draft,
|
value: stats.draft,
|
||||||
icon: Archive,
|
icon: Archive,
|
||||||
color: 'bg-gray-100 text-gray-600',
|
color: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -104,10 +104,10 @@ export default function Dashboard() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
Willkommen, {user?.firstName}!
|
Willkommen, {user?.firstName}!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Hier ist eine Übersicht Ihrer Workflows.
|
Hier ist eine Übersicht Ihrer Workflows.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -139,10 +139,10 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<p className="text-2xl font-bold text-gray-900">
|
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{loading ? '-' : card.value}
|
{loading ? '-' : card.value}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">{card.label}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">{card.label}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -154,12 +154,12 @@ export default function Dashboard() {
|
|||||||
to="/workflows/new"
|
to="/workflows/new"
|
||||||
className="card flex items-center gap-4 p-5 transition-shadow hover:shadow-md"
|
className="card flex items-center gap-4 p-5 transition-shadow hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary-50">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary-50 dark:bg-primary-900/30">
|
||||||
<Plus className="text-primary-600" size={24} />
|
<Plus className="text-primary-600 dark:text-primary-400" size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900">Neuer Workflow</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-white">Neuer Workflow</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Erstellen Sie einen neuen Workflow
|
Erstellen Sie einen neuen Workflow
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -168,12 +168,12 @@ export default function Dashboard() {
|
|||||||
to="/workflows?template=true"
|
to="/workflows?template=true"
|
||||||
className="card flex items-center gap-4 p-5 transition-shadow hover:shadow-md"
|
className="card flex items-center gap-4 p-5 transition-shadow hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-50">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-50 dark:bg-purple-900/30">
|
||||||
<Layers className="text-purple-600" size={24} />
|
<Layers className="text-purple-600 dark:text-purple-400" size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900">Vorlagen durchsuchen</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-white">Vorlagen durchsuchen</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Starten Sie mit einer bestehenden Vorlage
|
Starten Sie mit einer bestehenden Vorlage
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -182,13 +182,13 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Recent workflows */}
|
{/* Recent workflows */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-700">
|
||||||
<h2 className="text-base font-semibold text-gray-900">
|
<h2 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
Aktuelle Workflows
|
Aktuelle Workflows
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link
|
||||||
to="/workflows"
|
to="/workflows"
|
||||||
className="text-sm font-medium text-primary-600 hover:text-primary-700"
|
className="text-sm font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
|
||||||
>
|
>
|
||||||
Alle anzeigen
|
Alle anzeigen
|
||||||
</Link>
|
</Link>
|
||||||
@ -198,34 +198,34 @@ export default function Dashboard() {
|
|||||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary-600 border-t-transparent" />
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary-600 border-t-transparent" />
|
||||||
</div>
|
</div>
|
||||||
) : recent.length === 0 ? (
|
) : recent.length === 0 ? (
|
||||||
<div className="py-12 text-center text-sm text-gray-500">
|
<div className="py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
Noch keine Workflows vorhanden.
|
Noch keine Workflows vorhanden.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-gray-100">
|
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
{recent.map((wf) => (
|
{recent.map((wf) => (
|
||||||
<li key={wf.id}>
|
<li key={wf.id}>
|
||||||
<Link
|
<Link
|
||||||
to={`/workflows/${wf.id}`}
|
to={`/workflows/${wf.id}`}
|
||||||
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50"
|
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700">
|
||||||
<FilePlus className="text-gray-500" size={20} />
|
<FilePlus className="text-gray-500 dark:text-gray-400" size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate font-medium text-gray-900">
|
<p className="truncate font-medium text-gray-900 dark:text-white">
|
||||||
{wf.title}
|
{wf.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-sm text-gray-500">
|
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
|
||||||
{wf.tenant && (
|
{wf.tenant && (
|
||||||
<span className="font-medium text-primary-600">{wf.tenant.name}</span>
|
<span className="font-medium text-primary-600 dark:text-primary-400">{wf.tenant.name}</span>
|
||||||
)}
|
)}
|
||||||
{wf.tenant && <span> · </span>}
|
{wf.tenant && <span> · </span>}
|
||||||
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte · v{wf.version}
|
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte · v{wf.version}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge status={wf.status} />
|
<StatusBadge status={wf.status} />
|
||||||
<span className="hidden text-xs text-gray-400 sm:inline">
|
<span className="hidden text-xs text-gray-400 dark:text-gray-500 sm:inline">
|
||||||
{new Date(wf.updatedAt).toLocaleDateString('de-DE')}
|
{new Date(wf.updatedAt).toLocaleDateString('de-DE')}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -67,24 +67,24 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right login form */}
|
{/* Right login form */}
|
||||||
<div className="flex flex-1 items-center justify-center px-6 py-12">
|
<div className="flex flex-1 items-center justify-center bg-white px-6 py-12 dark:bg-gray-900">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
<div className="mb-8 text-center lg:hidden">
|
<div className="mb-8 text-center lg:hidden">
|
||||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-primary-600 text-xl font-bold text-white">
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-primary-600 text-xl font-bold text-white">
|
||||||
WP
|
WP
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mt-3 text-xl font-bold text-gray-900">
|
<h1 className="mt-3 text-xl font-bold text-gray-900 dark:text-white">
|
||||||
Workflow Portal
|
Workflow Portal
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Anmelden</h2>
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Anmelden</h2>
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Melden Sie sich mit Ihren Zugangsdaten an.
|
Melden Sie sich mit Ihren Zugangsdaten an.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -93,7 +93,7 @@ export default function LoginPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
E-Mail-Adresse
|
E-Mail-Adresse
|
||||||
</label>
|
</label>
|
||||||
@ -112,7 +112,7 @@ export default function LoginPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Passwort
|
Passwort
|
||||||
</label>
|
</label>
|
||||||
@ -130,7 +130,7 @@ export default function LoginPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -129,8 +129,8 @@ export default function TenantList() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Mandanten</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Mandanten</h1>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Verwalten Sie die Mandanten des Portals.
|
Verwalten Sie die Mandanten des Portals.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -172,47 +172,47 @@ export default function TenantList() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="card overflow-hidden">
|
<div className="card overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-700/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Slug
|
Slug
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Aktionen
|
Aktionen
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 bg-white">
|
<tbody className="divide-y divide-gray-100 bg-white dark:divide-gray-700 dark:bg-gray-800">
|
||||||
{tenants.map((tenant) => (
|
{tenants.map((tenant) => (
|
||||||
<tr key={tenant.id} className="hover:bg-gray-50">
|
<tr key={tenant.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<td className="whitespace-nowrap px-6 py-4">
|
<td className="whitespace-nowrap px-6 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-50 text-sm font-bold text-primary-700">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-50 text-sm font-bold text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
|
||||||
{tenant.name[0]}
|
{tenant.name[0]}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-gray-900">
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
{tenant.name}
|
{tenant.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{tenant.slug}
|
{tenant.slug}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-6 py-4">
|
<td className="whitespace-nowrap px-6 py-4">
|
||||||
{tenant.active ? (
|
{tenant.active ? (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-300">
|
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-300 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-700">
|
||||||
<Check size={12} />
|
<Check size={12} />
|
||||||
Aktiv
|
Aktiv
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-300">
|
<span className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:ring-gray-600">
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
Inaktiv
|
Inaktiv
|
||||||
</span>
|
</span>
|
||||||
@ -222,14 +222,14 @@ export default function TenantList() {
|
|||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => openEdit(tenant)}
|
onClick={() => openEdit(tenant)}
|
||||||
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||||
title="Bearbeiten"
|
title="Bearbeiten"
|
||||||
>
|
>
|
||||||
<Edit size={16} />
|
<Edit size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeactivateTarget(tenant)}
|
onClick={() => setDeactivateTarget(tenant)}
|
||||||
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||||
title={tenant.active ? 'Deaktivieren' : 'Aktivieren'}
|
title={tenant.active ? 'Deaktivieren' : 'Aktivieren'}
|
||||||
>
|
>
|
||||||
<Power size={16} />
|
<Power size={16} />
|
||||||
@ -256,7 +256,7 @@ export default function TenantList() {
|
|||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Name *
|
Name *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -278,7 +278,7 @@ export default function TenantList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Slug *
|
Slug *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -294,9 +294,9 @@ export default function TenantList() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formActive}
|
checked={formActive}
|
||||||
onChange={(e) => setFormActive(e.target.checked)}
|
onChange={(e) => setFormActive(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700">Aktiv</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Aktiv</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex justify-end gap-3 pt-2">
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -161,8 +161,8 @@ export default function UserList() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Benutzer</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Benutzer</h1>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Verwalten Sie die Benutzer und deren Rollen.
|
Verwalten Sie die Benutzer und deren Rollen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -236,63 +236,63 @@ export default function UserList() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="card overflow-hidden">
|
<div className="card overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-700/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
E-Mail
|
E-Mail
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Rolle
|
Rolle
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Mandant
|
Mandant
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500">
|
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||||
Aktionen
|
Aktionen
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 bg-white">
|
<tbody className="divide-y divide-gray-100 bg-white dark:divide-gray-700 dark:bg-gray-800">
|
||||||
{users.map((u) => (
|
{users.map((u) => (
|
||||||
<tr key={u.id} className="hover:bg-gray-50">
|
<tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<td className="whitespace-nowrap px-6 py-4">
|
<td className="whitespace-nowrap px-6 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700 dark:bg-primary-900/40 dark:text-primary-400">
|
||||||
{u.firstName[0]}
|
{u.firstName[0]}
|
||||||
{u.lastName[0]}
|
{u.lastName[0]}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-gray-900">
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
{u.firstName} {u.lastName}
|
{u.firstName} {u.lastName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{u.email}
|
{u.email}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-6 py-4">
|
<td className="whitespace-nowrap px-6 py-4">
|
||||||
<span className="inline-flex rounded-full bg-primary-50 px-2.5 py-0.5 text-xs font-medium text-primary-700">
|
<span className="inline-flex rounded-full bg-primary-50 px-2.5 py-0.5 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
|
||||||
{roleLabels[u.role]}
|
{roleLabels[u.role]}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{u.tenant?.name || '-'}
|
{u.tenant?.name || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-6 py-4 text-right">
|
<td className="whitespace-nowrap px-6 py-4 text-right">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => openEdit(u)}
|
onClick={() => openEdit(u)}
|
||||||
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||||
title="Bearbeiten"
|
title="Bearbeiten"
|
||||||
>
|
>
|
||||||
<Edit size={16} />
|
<Edit size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeactivateTarget(u)}
|
onClick={() => setDeactivateTarget(u)}
|
||||||
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-red-500"
|
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-red-500 dark:hover:bg-gray-700"
|
||||||
title="Deaktivieren"
|
title="Deaktivieren"
|
||||||
>
|
>
|
||||||
<Power size={16} />
|
<Power size={16} />
|
||||||
@ -321,7 +321,7 @@ export default function UserList() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Vorname *
|
Vorname *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -333,7 +333,7 @@ export default function UserList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Nachname *
|
Nachname *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -346,7 +346,7 @@ export default function UserList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
E-Mail *
|
E-Mail *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -358,7 +358,7 @@ export default function UserList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Passwort {editingUser ? '(leer lassen um beizubehalten)' : '*'}
|
Passwort {editingUser ? '(leer lassen um beizubehalten)' : '*'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -371,7 +371,7 @@ export default function UserList() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Rolle *
|
Rolle *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -387,7 +387,7 @@ export default function UserList() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Mandant
|
Mandant
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@ -280,24 +280,24 @@ export default function WorkflowEditor() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700"
|
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
Zurück
|
Zurück
|
||||||
</button>
|
</button>
|
||||||
{autoSaved && (
|
{autoSaved && (
|
||||||
<span className="text-xs text-green-600">Automatisch gespeichert</span>
|
<span className="text-xs text-green-600 dark:text-green-400">Automatisch gespeichert</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{isNew ? 'Neuer Workflow' : 'Workflow bearbeiten'}
|
{isNew ? 'Neuer Workflow' : 'Workflow bearbeiten'}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Metadata */}
|
{/* Metadata */}
|
||||||
<div className="card space-y-5 p-6">
|
<div className="card space-y-5 p-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Titel *
|
Titel *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -310,7 +310,7 @@ export default function WorkflowEditor() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Beschreibung
|
Beschreibung
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -324,7 +324,7 @@ export default function WorkflowEditor() {
|
|||||||
|
|
||||||
{isAdmin && isNew && (
|
{isAdmin && isNew && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Mandant *
|
Mandant *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -344,7 +344,7 @@ export default function WorkflowEditor() {
|
|||||||
|
|
||||||
<div className="grid gap-5 sm:grid-cols-2">
|
<div className="grid gap-5 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Kategorie
|
Kategorie
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -362,7 +362,7 @@ export default function WorkflowEditor() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700">
|
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Tags (kommagetrennt)
|
Tags (kommagetrennt)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -380,16 +380,16 @@ export default function WorkflowEditor() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isTemplate}
|
checked={isTemplate}
|
||||||
onChange={(e) => setIsTemplate(e.target.checked)}
|
onChange={(e) => setIsTemplate(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700">Als Vorlage speichern</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Als Vorlage speichern</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Schritte</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schritte</h2>
|
||||||
<button onClick={addStep} className="btn-secondary gap-1.5 text-sm">
|
<button onClick={addStep} className="btn-secondary gap-1.5 text-sm">
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
Schritt hinzufügen
|
Schritt hinzufügen
|
||||||
@ -418,21 +418,21 @@ export default function WorkflowEditor() {
|
|||||||
snapshot.isDragging ? 'shadow-lg ring-2 ring-primary-300' : ''
|
snapshot.isDragging ? 'shadow-lg ring-2 ring-primary-300' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 border-b border-gray-100 bg-gray-50 px-4 py-2">
|
<div className="flex items-center gap-2 border-b border-gray-100 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-700/50">
|
||||||
<div
|
<div
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
className="cursor-grab text-gray-400 hover:text-gray-600"
|
className="cursor-grab text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<GripVertical size={18} />
|
<GripVertical size={18} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-gray-500">
|
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Schritt {index + 1}
|
Schritt {index + 1}
|
||||||
</span>
|
</span>
|
||||||
<StepTypeIcon type={step.type} />
|
<StepTypeIcon type={step.type} />
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<button
|
<button
|
||||||
onClick={() => removeStep(index)}
|
onClick={() => removeStep(index)}
|
||||||
className="rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-500"
|
className="rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
@ -440,7 +440,7 @@ export default function WorkflowEditor() {
|
|||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-500">
|
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
Titel
|
Titel
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -455,7 +455,7 @@ export default function WorkflowEditor() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-500">
|
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
Typ
|
Typ
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -488,9 +488,9 @@ export default function WorkflowEditor() {
|
|||||||
e.target.checked
|
e.target.checked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-600">
|
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
Pflicht
|
Pflicht
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@ -498,7 +498,7 @@ export default function WorkflowEditor() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-500">
|
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
Inhalt
|
Inhalt
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -532,7 +532,7 @@ export default function WorkflowEditor() {
|
|||||||
|
|
||||||
{steps.length === 0 && (
|
{steps.length === 0 && (
|
||||||
<div className="card py-12 text-center">
|
<div className="card py-12 text-center">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Noch keine Schritte vorhanden.
|
Noch keine Schritte vorhanden.
|
||||||
</p>
|
</p>
|
||||||
<button onClick={addStep} className="btn-primary mt-4 gap-1.5">
|
<button onClick={addStep} className="btn-primary mt-4 gap-1.5">
|
||||||
|
|||||||
@ -83,10 +83,10 @@ export default function WorkflowList() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{isTemplate ? 'Vorlagen' : 'Workflows'}
|
{isTemplate ? 'Vorlagen' : 'Workflows'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{isTemplate
|
{isTemplate
|
||||||
? 'Wiederverwendbare Workflow-Vorlagen'
|
? 'Wiederverwendbare Workflow-Vorlagen'
|
||||||
: 'Alle Workflows Ihres Mandanten'}
|
: 'Alle Workflows Ihres Mandanten'}
|
||||||
@ -157,13 +157,13 @@ export default function WorkflowList() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<div className="flex rounded-lg border border-gray-300 bg-white">
|
<div className="flex rounded-lg border border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-800">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => setViewMode('grid')}
|
||||||
className={`rounded-l-lg p-2 ${
|
className={`rounded-l-lg p-2 ${
|
||||||
viewMode === 'grid'
|
viewMode === 'grid'
|
||||||
? 'bg-primary-50 text-primary-600'
|
? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400'
|
||||||
: 'text-gray-400 hover:text-gray-600'
|
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Grid3X3 size={18} />
|
<Grid3X3 size={18} />
|
||||||
@ -172,8 +172,8 @@ export default function WorkflowList() {
|
|||||||
onClick={() => setViewMode('list')}
|
onClick={() => setViewMode('list')}
|
||||||
className={`rounded-r-lg p-2 ${
|
className={`rounded-r-lg p-2 ${
|
||||||
viewMode === 'list'
|
viewMode === 'list'
|
||||||
? 'bg-primary-50 text-primary-600'
|
? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400'
|
||||||
: 'text-gray-400 hover:text-gray-600'
|
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<List size={18} />
|
<List size={18} />
|
||||||
@ -212,23 +212,23 @@ export default function WorkflowList() {
|
|||||||
className="card p-5 transition-shadow hover:shadow-md"
|
className="card p-5 transition-shadow hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<h3 className="line-clamp-1 font-semibold text-gray-900">
|
<h3 className="line-clamp-1 font-semibold text-gray-900 dark:text-white">
|
||||||
{wf.title}
|
{wf.title}
|
||||||
</h3>
|
</h3>
|
||||||
<StatusBadge status={wf.status} />
|
<StatusBadge status={wf.status} />
|
||||||
</div>
|
</div>
|
||||||
{wf.tenant && (
|
{wf.tenant && (
|
||||||
<div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-primary-600">
|
<div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-primary-600 dark:text-primary-400">
|
||||||
<Building2 size={12} />
|
<Building2 size={12} />
|
||||||
{wf.tenant.name}
|
{wf.tenant.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{wf.description && (
|
{wf.description && (
|
||||||
<p className="mt-1 line-clamp-2 text-sm text-gray-500">
|
<p className="mt-1 line-clamp-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{wf.description}
|
{wf.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 flex items-center justify-between text-xs text-gray-400">
|
<div className="mt-4 flex items-center justify-between text-xs text-gray-400 dark:text-gray-500">
|
||||||
<span>
|
<span>
|
||||||
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte · v{wf.version}
|
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte · v{wf.version}
|
||||||
</span>
|
</span>
|
||||||
@ -237,7 +237,7 @@ export default function WorkflowList() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{wf.category && (
|
{wf.category && (
|
||||||
<div className="mt-3 flex items-center gap-1.5 text-xs text-gray-500">
|
<div className="mt-3 flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
<Tag size={12} />
|
<Tag size={12} />
|
||||||
{wf.category.name}
|
{wf.category.name}
|
||||||
</div>
|
</div>
|
||||||
@ -247,13 +247,13 @@ export default function WorkflowList() {
|
|||||||
{wf.tags.slice(0, 3).map((tag) => (
|
{wf.tags.slice(0, 3).map((tag) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600"
|
className="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{wf.tags.length > 3 && (
|
{wf.tags.length > 3 && (
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
+{wf.tags.length - 3}
|
+{wf.tags.length - 3}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -263,36 +263,36 @@ export default function WorkflowList() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="card divide-y divide-gray-100">
|
<div className="card divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
{workflows.map((wf) => (
|
{workflows.map((wf) => (
|
||||||
<Link
|
<Link
|
||||||
key={wf.id}
|
key={wf.id}
|
||||||
to={`/workflows/${wf.id}`}
|
to={`/workflows/${wf.id}`}
|
||||||
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50"
|
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700">
|
||||||
<FileText className="text-gray-500" size={20} />
|
<FileText className="text-gray-500 dark:text-gray-400" size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate font-medium text-gray-900">{wf.title}</p>
|
<p className="truncate font-medium text-gray-900 dark:text-white">{wf.title}</p>
|
||||||
<p className="truncate text-sm text-gray-500">
|
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
|
||||||
{wf.tenant && (
|
{wf.tenant && (
|
||||||
<span className="font-medium text-primary-600">{wf.tenant.name}</span>
|
<span className="font-medium text-primary-600 dark:text-primary-400">{wf.tenant.name}</span>
|
||||||
)}
|
)}
|
||||||
{wf.tenant && wf.description && <span> · </span>}
|
{wf.tenant && wf.description && <span> · </span>}
|
||||||
{wf.description}
|
{wf.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{wf.category && (
|
{wf.category && (
|
||||||
<span className="hidden rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 md:inline">
|
<span className="hidden rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 md:inline dark:bg-gray-700 dark:text-gray-300">
|
||||||
{wf.category.name}
|
{wf.category.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="hidden text-sm text-gray-400 sm:inline">
|
<span className="hidden text-sm text-gray-400 dark:text-gray-500 sm:inline">
|
||||||
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte
|
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte
|
||||||
</span>
|
</span>
|
||||||
<StatusBadge status={wf.status} />
|
<StatusBadge status={wf.status} />
|
||||||
<span className="hidden text-xs text-gray-400 lg:inline">
|
<span className="hidden text-xs text-gray-400 dark:text-gray-500 lg:inline">
|
||||||
{new Date(wf.updatedAt).toLocaleDateString('de-DE')}
|
{new Date(wf.updatedAt).toLocaleDateString('de-DE')}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -23,15 +23,15 @@ import ConfirmDialog from '../components/ConfirmDialog';
|
|||||||
function getStepBgClass(type: WorkflowStep['type']): string {
|
function getStepBgClass(type: WorkflowStep['type']): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'WARNING':
|
case 'WARNING':
|
||||||
return 'border-l-4 border-l-amber-400 bg-amber-50';
|
return 'border-l-4 border-l-amber-400 bg-amber-50 dark:bg-amber-900/20';
|
||||||
case 'COMMAND':
|
case 'COMMAND':
|
||||||
return 'border-l-4 border-l-purple-400 bg-gray-900';
|
return 'border-l-4 border-l-purple-400 bg-gray-900 dark:bg-gray-950';
|
||||||
case 'INFO':
|
case 'INFO':
|
||||||
return 'border-l-4 border-l-cyan-400 bg-cyan-50';
|
return 'border-l-4 border-l-cyan-400 bg-cyan-50 dark:bg-cyan-900/20';
|
||||||
case 'CHECKLIST':
|
case 'CHECKLIST':
|
||||||
return 'border-l-4 border-l-green-400 bg-green-50';
|
return 'border-l-4 border-l-green-400 bg-green-50 dark:bg-green-900/20';
|
||||||
default:
|
default:
|
||||||
return 'border-l-4 border-l-primary-400 bg-white';
|
return 'border-l-4 border-l-primary-400 bg-white dark:bg-gray-800';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ export default function WorkflowView() {
|
|||||||
{/* Back */}
|
{/* Back */}
|
||||||
<Link
|
<Link
|
||||||
to="/workflows"
|
to="/workflows"
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700"
|
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
Zurück zu Workflows
|
Zurück zu Workflows
|
||||||
@ -126,15 +126,15 @@ export default function WorkflowView() {
|
|||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{workflow.title}
|
{workflow.title}
|
||||||
</h1>
|
</h1>
|
||||||
<StatusBadge status={workflow.status} />
|
<StatusBadge status={workflow.status} />
|
||||||
</div>
|
</div>
|
||||||
{workflow.description && (
|
{workflow.description && (
|
||||||
<p className="mt-2 text-gray-600">{workflow.description}</p>
|
<p className="mt-2 text-gray-600 dark:text-gray-300">{workflow.description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span>Version {workflow.version}</span>
|
<span>Version {workflow.version}</span>
|
||||||
<span>{sortedSteps.length} Schritte</span>
|
<span>{sortedSteps.length} Schritte</span>
|
||||||
{workflow.category && <span>{workflow.category.name}</span>}
|
{workflow.category && <span>{workflow.category.name}</span>}
|
||||||
@ -152,7 +152,7 @@ export default function WorkflowView() {
|
|||||||
{workflow.tags.map((tag) => (
|
{workflow.tags.map((tag) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="rounded-full bg-primary-50 px-3 py-0.5 text-xs font-medium text-primary-700"
|
className="rounded-full bg-primary-50 px-3 py-0.5 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-400"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
@ -181,7 +181,7 @@ export default function WorkflowView() {
|
|||||||
{workflow.status !== 'ARCHIVED' && (
|
{workflow.status !== 'ARCHIVED' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setArchiveOpen(true)}
|
onClick={() => setArchiveOpen(true)}
|
||||||
className="btn-secondary gap-1.5 text-red-600 hover:bg-red-50"
|
className="btn-secondary gap-1.5 text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
>
|
>
|
||||||
<Archive size={16} />
|
<Archive size={16} />
|
||||||
Archivieren
|
Archivieren
|
||||||
@ -193,9 +193,9 @@ export default function WorkflowView() {
|
|||||||
|
|
||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Schritte</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schritte</h2>
|
||||||
{sortedSteps.length === 0 ? (
|
{sortedSteps.length === 0 ? (
|
||||||
<div className="card py-12 text-center text-sm text-gray-500">
|
<div className="card py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
Keine Schritte vorhanden.
|
Keine Schritte vorhanden.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -208,21 +208,21 @@ export default function WorkflowView() {
|
|||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
{/* Step number */}
|
{/* Step number */}
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white text-sm font-bold text-gray-700 shadow-sm ring-1 ring-gray-200">
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white text-sm font-bold text-gray-700 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:ring-gray-600">
|
||||||
{idx + 1}
|
{idx + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StepTypeIcon type={step.type} showLabel />
|
<StepTypeIcon type={step.type} showLabel />
|
||||||
{step.isRequired && (
|
{step.isRequired && (
|
||||||
<span className="rounded bg-red-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-red-600">
|
<span className="rounded bg-red-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-red-600 dark:bg-red-900/40 dark:text-red-400">
|
||||||
Pflicht
|
Pflicht
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3
|
<h3
|
||||||
className={`mt-1 font-semibold ${
|
className={`mt-1 font-semibold ${
|
||||||
step.type === 'COMMAND' ? 'text-gray-100' : 'text-gray-900'
|
step.type === 'COMMAND' ? 'text-gray-100' : 'text-gray-900 dark:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step.title}
|
{step.title}
|
||||||
@ -235,10 +235,10 @@ export default function WorkflowView() {
|
|||||||
<div
|
<div
|
||||||
className={`mt-2 whitespace-pre-wrap text-sm ${
|
className={`mt-2 whitespace-pre-wrap text-sm ${
|
||||||
step.type === 'WARNING'
|
step.type === 'WARNING'
|
||||||
? 'text-amber-800'
|
? 'text-amber-800 dark:text-amber-300'
|
||||||
: step.type === 'INFO'
|
: step.type === 'INFO'
|
||||||
? 'text-cyan-800'
|
? 'text-cyan-800 dark:text-cyan-300'
|
||||||
: 'text-gray-600'
|
: 'text-gray-600 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step.content}
|
{step.content}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
|
darkMode: 'class',
|
||||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user