feat: complete tOS project with HR, LEAN, Dashboard and Integrations modules
Full enterprise web operating system including: - Next.js 14 frontend with App Router, i18n (DE/EN), shadcn/ui - NestJS 10 backend with Prisma, JWT auth, Swagger docs - Keycloak 24 SSO with role-based access control - HR module (employees, time tracking, absences, org chart) - LEAN module (3S planning, morning meeting SQCDM, skill matrix) - Integrations module (PlentyONE, Zulip, Todoist, FreeScout, Nextcloud, ecoDMS, GembaDocs) - Dashboard with customizable drag & drop widget grid - Docker Compose infrastructure (PostgreSQL 16, Redis 7, Keycloak 24) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, Sun, Moon, Monitor, Languages, Check } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface PreferencesContentProps {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const themes = [
|
||||
{ value: 'light', icon: Sun, labelKey: 'lightMode' },
|
||||
{ value: 'dark', icon: Moon, labelKey: 'darkMode' },
|
||||
{ value: 'system', icon: Monitor, labelKey: 'systemDefault' },
|
||||
] as const;
|
||||
|
||||
const languages = [
|
||||
{ value: 'de', label: 'Deutsch', flag: 'DE' },
|
||||
{ value: 'en', label: 'English', flag: 'EN' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Preferences settings content - Theme and language settings
|
||||
*/
|
||||
export function PreferencesContent({ locale }: PreferencesContentProps) {
|
||||
const t = useTranslations('settings');
|
||||
const { theme, setTheme } = useTheme();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const handleLanguageChange = (newLocale: string) => {
|
||||
if (newLocale === locale) return;
|
||||
|
||||
// Replace the locale in the current path
|
||||
const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
|
||||
router.push(newPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header with back button */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<Link href={`/${locale}/settings`}>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('preferencesTitle')}</h1>
|
||||
<p className="text-muted-foreground">{t('preferencesDescription')}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Theme Selection */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('theme')}</CardTitle>
|
||||
<CardDescription>Waehlen Sie Ihr bevorzugtes Farbschema</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{themes.map((themeOption) => {
|
||||
const Icon = themeOption.icon;
|
||||
const isSelected = theme === themeOption.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={themeOption.value}
|
||||
onClick={() => setTheme(themeOption.value)}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center gap-3 rounded-lg border-2 p-4 transition-colors',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-muted hover:border-muted-foreground/50'
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="absolute right-2 top-2">
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-12 w-12 items-center justify-center rounded-full',
|
||||
isSelected ? 'bg-primary/20' : 'bg-muted'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('h-6 w-6', isSelected ? 'text-primary' : 'text-muted-foreground')} />
|
||||
</div>
|
||||
<span className={cn('text-sm font-medium', isSelected && 'text-primary')}>
|
||||
{t(themeOption.labelKey)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Language Selection */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Languages className="h-5 w-5" />
|
||||
{t('language')}
|
||||
</CardTitle>
|
||||
<CardDescription>Waehlen Sie Ihre bevorzugte Sprache</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{languages.map((lang) => {
|
||||
const isSelected = locale === lang.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={lang.value}
|
||||
onClick={() => handleLanguageChange(lang.value)}
|
||||
className={cn(
|
||||
'relative flex items-center gap-3 rounded-lg border-2 p-4 transition-colors',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-muted hover:border-muted-foreground/50'
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="absolute right-2 top-2">
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-10 w-10 items-center justify-center rounded-lg font-bold text-sm',
|
||||
isSelected ? 'bg-primary text-primary-foreground' : 'bg-muted'
|
||||
)}
|
||||
>
|
||||
{lang.flag}
|
||||
</div>
|
||||
<span className={cn('font-medium', isSelected && 'text-primary')}>
|
||||
{lang.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Dashboard Preferences */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dashboard</CardTitle>
|
||||
<CardDescription>Einstellungen fuer Ihr Dashboard</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>Widgets zuruecksetzen</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Setzen Sie Ihr Dashboard auf die Standardkonfiguration zurueck
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('tos-dashboard-layout');
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Zuruecksetzen
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user