Добавлен новый компонент TimesheetDemo и соответствующие вкладки в UIKitSection. Обновлены стили и структура кода для улучшения взаимодействия с пользователем. Оптимизированы импорты и форматирование кода в компоненте BusinessDemo.
This commit is contained in:
@ -16,6 +16,7 @@ import { StatesDemo } from './ui-kit/states-demo'
|
||||
import { MediaDemo } from './ui-kit/media-demo'
|
||||
import { InteractiveDemo } from './ui-kit/interactive-demo'
|
||||
import { BusinessDemo } from './ui-kit/business-demo'
|
||||
import { TimesheetDemo } from './ui-kit/timesheet-demo'
|
||||
|
||||
export function UIKitSection() {
|
||||
return (
|
||||
@ -69,6 +70,9 @@ export function UIKitSection() {
|
||||
<TabsTrigger value="business" className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2">
|
||||
Бизнес
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="timesheet" className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 text-xs px-3 py-2">
|
||||
Табель
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="buttons" className="space-y-6">
|
||||
@ -126,6 +130,10 @@ export function UIKitSection() {
|
||||
<TabsContent value="business" className="space-y-6">
|
||||
<BusinessDemo />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="timesheet" className="space-y-6">
|
||||
<TimesheetDemo />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,20 +1,21 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useState } from "react";
|
||||
import { TimesheetDemo } from "./timesheet-demo";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Calendar,
|
||||
Check,
|
||||
X,
|
||||
@ -43,69 +44,71 @@ import {
|
||||
ChevronRight,
|
||||
Hash,
|
||||
Package2,
|
||||
Truck
|
||||
} from 'lucide-react'
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
|
||||
export function BusinessDemo() {
|
||||
const [selectedProduct] = useState(null)
|
||||
const [cartQuantity, setCartQuantity] = useState(1)
|
||||
const [expandedSeller, setExpandedSeller] = useState(false)
|
||||
const [selectedProduct] = useState(null);
|
||||
const [cartQuantity, setCartQuantity] = useState(1);
|
||||
const [expandedSeller, setExpandedSeller] = useState(false);
|
||||
|
||||
// Данные для демонстрации
|
||||
const scheduleData = Array.from({ length: 30 }, (_, i) => ({
|
||||
day: i + 1,
|
||||
status: ['work', 'work', 'work', 'work', 'work', 'weekend', 'weekend'][i % 7],
|
||||
hours: [8, 8, 8, 8, 8, 0, 0][i % 7]
|
||||
}))
|
||||
status: ["work", "work", "work", "work", "work", "weekend", "weekend"][
|
||||
i % 7
|
||||
],
|
||||
hours: [8, 8, 8, 8, 8, 0, 0][i % 7],
|
||||
}));
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'iPhone 15 Pro Max 256GB',
|
||||
article: 'APL-IP15PM-256',
|
||||
id: "1",
|
||||
name: "iPhone 15 Pro Max 256GB",
|
||||
article: "APL-IP15PM-256",
|
||||
price: 89990,
|
||||
oldPrice: 99990,
|
||||
quantity: 45,
|
||||
category: 'Электроника',
|
||||
brand: 'Apple',
|
||||
category: "Электроника",
|
||||
brand: "Apple",
|
||||
rating: 4.8,
|
||||
reviews: 1234,
|
||||
image: '/placeholder-phone.jpg',
|
||||
seller: 'TechStore Moscow',
|
||||
image: "/placeholder-phone.jpg",
|
||||
seller: "TechStore Moscow",
|
||||
isNew: true,
|
||||
inStock: true
|
||||
inStock: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Беспроводные наушники AirPods Pro',
|
||||
article: 'APL-APP-PRO',
|
||||
id: "2",
|
||||
name: "Беспроводные наушники AirPods Pro",
|
||||
article: "APL-APP-PRO",
|
||||
price: 24990,
|
||||
quantity: 23,
|
||||
category: 'Аксессуары',
|
||||
brand: 'Apple',
|
||||
category: "Аксессуары",
|
||||
brand: "Apple",
|
||||
rating: 4.6,
|
||||
reviews: 856,
|
||||
image: '/placeholder-headphones.jpg',
|
||||
seller: 'Audio Expert',
|
||||
image: "/placeholder-headphones.jpg",
|
||||
seller: "Audio Expert",
|
||||
isNew: false,
|
||||
inStock: true
|
||||
inStock: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Ноутбук MacBook Air M2',
|
||||
article: 'APL-MBA-M2',
|
||||
id: "3",
|
||||
name: "Ноутбук MacBook Air M2",
|
||||
article: "APL-MBA-M2",
|
||||
price: 0,
|
||||
quantity: 0,
|
||||
category: 'Компьютеры',
|
||||
brand: 'Apple',
|
||||
category: "Компьютеры",
|
||||
brand: "Apple",
|
||||
rating: 4.9,
|
||||
reviews: 445,
|
||||
image: '/placeholder-laptop.jpg',
|
||||
seller: 'Digital World',
|
||||
image: "/placeholder-laptop.jpg",
|
||||
seller: "Digital World",
|
||||
isNew: false,
|
||||
inStock: false
|
||||
}
|
||||
]
|
||||
inStock: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Данные для поставки фулфилмента
|
||||
const fulfillmentSupply = {
|
||||
@ -128,106 +131,129 @@ export function BusinessDemo() {
|
||||
logisticsPartnerId: "log1",
|
||||
status: "planned",
|
||||
totalValue: 2500000,
|
||||
}
|
||||
};
|
||||
|
||||
const employees = [
|
||||
{ id: "emp1", firstName: "Иван", lastName: "Петров", position: "Менеджер склада" },
|
||||
{ id: "emp2", firstName: "Мария", lastName: "Сидорова", position: "Логист" }
|
||||
]
|
||||
{
|
||||
id: "emp1",
|
||||
firstName: "Иван",
|
||||
lastName: "Петров",
|
||||
position: "Менеджер склада",
|
||||
},
|
||||
{
|
||||
id: "emp2",
|
||||
firstName: "Мария",
|
||||
lastName: "Сидорова",
|
||||
position: "Логист",
|
||||
},
|
||||
];
|
||||
|
||||
const logisticsPartners = [
|
||||
{ id: "log1", name: "ТК Энергия", fullName: "ООО ТК Энергия" },
|
||||
{ id: "log2", name: "СДЭК", fullName: "ООО СДЭК" }
|
||||
]
|
||||
{ id: "log2", name: "СДЭК", fullName: "ООО СДЭК" },
|
||||
];
|
||||
|
||||
const wholesalers = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'ТехноОпт Москва',
|
||||
id: "1",
|
||||
name: "ТехноОпт Москва",
|
||||
fullName: 'ООО "Технологии Оптом"',
|
||||
inn: '7735123456',
|
||||
type: 'WHOLESALE',
|
||||
avatar: '/placeholder-company.jpg',
|
||||
inn: "7735123456",
|
||||
type: "WHOLESALE",
|
||||
avatar: "/placeholder-company.jpg",
|
||||
rating: 4.8,
|
||||
reviewsCount: 2345,
|
||||
productsCount: 15670,
|
||||
completedOrders: 8934,
|
||||
responseTime: '2 часа',
|
||||
categories: ['Электроника', 'Компьютеры', 'Аксессуары'],
|
||||
location: 'Москва, Россия',
|
||||
workingSince: '2018',
|
||||
verifiedBadges: ['verified', 'premium', 'fast-delivery'],
|
||||
description: 'Крупнейший поставщик электроники и компьютерной техники в России',
|
||||
responseTime: "2 часа",
|
||||
categories: ["Электроника", "Компьютеры", "Аксессуары"],
|
||||
location: "Москва, Россия",
|
||||
workingSince: "2018",
|
||||
verifiedBadges: ["verified", "premium", "fast-delivery"],
|
||||
description:
|
||||
"Крупнейший поставщик электроники и компьютерной техники в России",
|
||||
specialOffers: 3,
|
||||
minOrder: 50000
|
||||
minOrder: 50000,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'СтройБаза Регион',
|
||||
fullName: 'ИП Строительные материалы',
|
||||
inn: '7735987654',
|
||||
type: 'WHOLESALE',
|
||||
avatar: '/placeholder-construction.jpg',
|
||||
id: "2",
|
||||
name: "СтройБаза Регион",
|
||||
fullName: "ИП Строительные материалы",
|
||||
inn: "7735987654",
|
||||
type: "WHOLESALE",
|
||||
avatar: "/placeholder-construction.jpg",
|
||||
rating: 4.5,
|
||||
reviewsCount: 1876,
|
||||
productsCount: 8430,
|
||||
completedOrders: 5621,
|
||||
responseTime: '4 часа',
|
||||
categories: ['Стройматериалы', 'Инструменты', 'Сантехника'],
|
||||
location: 'Екатеринбург, Россия',
|
||||
workingSince: '2015',
|
||||
verifiedBadges: ['verified', 'eco-friendly'],
|
||||
description: 'Надежный поставщик строительных материалов по всей России',
|
||||
responseTime: "4 часа",
|
||||
categories: ["Стройматериалы", "Инструменты", "Сантехника"],
|
||||
location: "Екатеринбург, Россия",
|
||||
workingSince: "2015",
|
||||
verifiedBadges: ["verified", "eco-friendly"],
|
||||
description: "Надежный поставщик строительных материалов по всей России",
|
||||
specialOffers: 1,
|
||||
minOrder: 30000
|
||||
}
|
||||
]
|
||||
minOrder: 30000,
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'work': return 'bg-green-500'
|
||||
case 'weekend': return 'bg-gray-400'
|
||||
case 'vacation': return 'bg-blue-500'
|
||||
case 'sick': return 'bg-yellow-500'
|
||||
case 'absent': return 'bg-red-500'
|
||||
default: return 'bg-gray-400'
|
||||
case "work":
|
||||
return "bg-green-500";
|
||||
case "weekend":
|
||||
return "bg-gray-400";
|
||||
case "vacation":
|
||||
return "bg-blue-500";
|
||||
case "sick":
|
||||
return "bg-yellow-500";
|
||||
case "absent":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-400";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'work': return 'Работа'
|
||||
case 'weekend': return 'Выходной'
|
||||
case 'vacation': return 'Отпуск'
|
||||
case 'sick': return 'Больничный'
|
||||
case 'absent': return 'Прогул'
|
||||
default: return 'Неизвестно'
|
||||
case "work":
|
||||
return "Работа";
|
||||
case "weekend":
|
||||
return "Выходной";
|
||||
case "vacation":
|
||||
return "Отпуск";
|
||||
case "sick":
|
||||
return "Больничный";
|
||||
case "absent":
|
||||
return "Прогул";
|
||||
default:
|
||||
return "Неизвестно";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatPrice = (price: number) => {
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB',
|
||||
minimumFractionDigits: 0
|
||||
}).format(price)
|
||||
}
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
minimumFractionDigits: 0,
|
||||
}).format(price);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount)
|
||||
}
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
@ -247,17 +273,17 @@ export function BusinessDemo() {
|
||||
color: "text-purple-300 border-purple-400/30",
|
||||
label: "Обрабатывается",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -272,11 +298,15 @@ export function BusinessDemo() {
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src="/placeholder-employee.jpg" />
|
||||
<AvatarFallback className="bg-purple-600 text-white">ИИ</AvatarFallback>
|
||||
<AvatarFallback className="bg-purple-600 text-white">
|
||||
ИИ
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h4 className="text-white font-medium">Иванов Иван Иванович</h4>
|
||||
<p className="text-white/60 text-sm">Менеджер по продажам • Март 2024</p>
|
||||
<p className="text-white/60 text-sm">
|
||||
Менеджер по продажам • Март 2024
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@ -296,21 +326,29 @@ export function BusinessDemo() {
|
||||
<div>Сб</div>
|
||||
<div>Вс</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{scheduleData.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
relative p-3 rounded-lg border border-white/10 text-center transition-all hover:border-white/30
|
||||
${day.status === 'work' ? 'bg-green-500/20' : ''}
|
||||
${day.status === 'weekend' ? 'bg-gray-500/20' : ''}
|
||||
${day.status === "work" ? "bg-green-500/20" : ""}
|
||||
${day.status === "weekend" ? "bg-gray-500/20" : ""}
|
||||
`}
|
||||
>
|
||||
<div className="text-white text-sm font-medium">{day.day}</div>
|
||||
<div className={`w-2 h-2 rounded-full mx-auto mt-1 ${getStatusColor(day.status)}`}></div>
|
||||
<div className="text-white text-sm font-medium">
|
||||
{day.day}
|
||||
</div>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full mx-auto mt-1 ${getStatusColor(
|
||||
day.status
|
||||
)}`}
|
||||
></div>
|
||||
{day.hours > 0 && (
|
||||
<div className="text-white/60 text-xs mt-1">{day.hours}ч</div>
|
||||
<div className="text-white/60 text-xs mt-1">
|
||||
{day.hours}ч
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@ -371,29 +409,44 @@ export function BusinessDemo() {
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="glass-card p-4 rounded-lg border border-white/10 group hover:border-white/30 transition-all">
|
||||
<div
|
||||
key={product.id}
|
||||
className="glass-card p-4 rounded-lg border border-white/10 group hover:border-white/30 transition-all"
|
||||
>
|
||||
{/* Изображение товара */}
|
||||
<div className="relative mb-3">
|
||||
<div className="w-full h-40 bg-white/10 rounded-lg flex items-center justify-center">
|
||||
<Package className="h-16 w-16 text-white/40" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Бейджи */}
|
||||
<div className="absolute top-2 left-2 flex flex-col gap-1">
|
||||
{product.isNew && (
|
||||
<Badge className="bg-green-600 text-white text-xs">Новинка</Badge>
|
||||
<Badge className="bg-green-600 text-white text-xs">
|
||||
Новинка
|
||||
</Badge>
|
||||
)}
|
||||
{product.oldPrice && (
|
||||
<Badge className="bg-red-600 text-white text-xs">Скидка</Badge>
|
||||
<Badge className="bg-red-600 text-white text-xs">
|
||||
Скидка
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Кнопки действий */}
|
||||
<div className="absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button size="sm" variant="outline" className="bg-white/20 hover:bg-white/30 text-white border-white/30 h-8 w-8 p-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 h-8 w-8 p-0"
|
||||
>
|
||||
<Heart className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="bg-white/20 hover:bg-white/30 text-white border-white/30 h-8 w-8 p-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 h-8 w-8 p-0"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -402,25 +455,39 @@ export function BusinessDemo() {
|
||||
{/* Информация о товаре */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm line-clamp-2">{product.name}</h4>
|
||||
<p className="text-white/60 text-xs">Артикул: {product.article}</p>
|
||||
<h4 className="text-white font-medium text-sm line-clamp-2">
|
||||
{product.name}
|
||||
</h4>
|
||||
<p className="text-white/60 text-xs">
|
||||
Артикул: {product.article}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Рейтинг и отзывы */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
|
||||
<span className="text-white/80 text-xs">{product.rating}</span>
|
||||
<span className="text-white/80 text-xs">
|
||||
{product.rating}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-white/60 text-xs">({product.reviews} отзывов)</span>
|
||||
<span className="text-white/60 text-xs">
|
||||
({product.reviews} отзывов)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Категория и бренд */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="text-xs border-white/30 text-white/70">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs border-white/30 text-white/70"
|
||||
>
|
||||
{product.category}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs border-white/30 text-white/70">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs border-white/30 text-white/70"
|
||||
>
|
||||
{product.brand}
|
||||
</Badge>
|
||||
</div>
|
||||
@ -429,17 +496,25 @@ export function BusinessDemo() {
|
||||
<div className="space-y-1">
|
||||
{product.price > 0 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white font-bold">{formatPrice(product.price)}</span>
|
||||
<span className="text-white font-bold">
|
||||
{formatPrice(product.price)}
|
||||
</span>
|
||||
{product.oldPrice && (
|
||||
<span className="text-white/60 text-sm line-through">{formatPrice(product.oldPrice)}</span>
|
||||
<span className="text-white/60 text-sm line-through">
|
||||
{formatPrice(product.oldPrice)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-red-400 font-medium">Нет в наличии</span>
|
||||
<span className="text-red-400 font-medium">
|
||||
Нет в наличии
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
||||
{product.inStock && product.quantity > 0 && (
|
||||
<p className="text-green-400 text-xs">В наличии: {product.quantity} шт.</p>
|
||||
<p className="text-green-400 text-xs">
|
||||
В наличии: {product.quantity} шт.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -453,21 +528,37 @@ export function BusinessDemo() {
|
||||
{product.inStock && product.price > 0 ? (
|
||||
<>
|
||||
<div className="flex items-center border border-white/30 rounded">
|
||||
<Button size="sm" variant="ghost" className="h-7 w-7 p-0 text-white hover:bg-white/20">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0 text-white hover:bg-white/20"
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
<span className="px-2 text-white text-sm">{cartQuantity}</span>
|
||||
<Button size="sm" variant="ghost" className="h-7 w-7 p-0 text-white hover:bg-white/20">
|
||||
<span className="px-2 text-white text-sm">
|
||||
{cartQuantity}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0 text-white hover:bg-white/20"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button size="sm" className="flex-1 bg-purple-600 hover:bg-purple-700 text-white">
|
||||
<ShoppingCart className="h-3 w-3 mr-1" />
|
||||
В корзину
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
<ShoppingCart className="h-3 w-3 mr-1" />В корзину
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="sm" disabled className="w-full bg-gray-600 text-white/50">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full bg-gray-600 text-white/50"
|
||||
>
|
||||
Недоступно
|
||||
</Button>
|
||||
)}
|
||||
@ -482,7 +573,9 @@ export function BusinessDemo() {
|
||||
{/* Карточка поставки фулфилмента */}
|
||||
<Card className="glass-card border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Карточка поставки фулфилмента</CardTitle>
|
||||
<CardTitle className="text-white">
|
||||
Карточка поставки фулфилмента
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Card className="glass-card p-4 hover:bg-white/10 transition-colors">
|
||||
@ -641,10 +734,10 @@ export function BusinessDemo() {
|
||||
|
||||
{/* Ответственный сотрудник */}
|
||||
<div className="col-span-1">
|
||||
<p className="text-white/60 text-xs mb-1">
|
||||
Ответственный
|
||||
</p>
|
||||
<Select defaultValue={fulfillmentSupply.responsibleEmployeeId}>
|
||||
<p className="text-white/60 text-xs mb-1">Ответственный</p>
|
||||
<Select
|
||||
defaultValue={fulfillmentSupply.responsibleEmployeeId}
|
||||
>
|
||||
<SelectTrigger className="h-8 glass-input bg-white/10 border-white/20 text-white hover:bg-white/15 focus:bg-white/15 focus:ring-1 focus:ring-purple-400/50 text-xs">
|
||||
<SelectValue placeholder="Выберите" />
|
||||
</SelectTrigger>
|
||||
@ -685,7 +778,9 @@ export function BusinessDemo() {
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{partner.name || partner.fullName || "Без названия"}
|
||||
{partner.name ||
|
||||
partner.fullName ||
|
||||
"Без названия"}
|
||||
</span>
|
||||
<span className="text-xs text-white/60">
|
||||
Логистический партнер
|
||||
@ -719,7 +814,10 @@ export function BusinessDemo() {
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{wholesalers.map((wholesaler) => (
|
||||
<div key={wholesaler.id} className="glass-card p-6 rounded-lg border border-white/10 hover:border-white/30 transition-all">
|
||||
<div
|
||||
key={wholesaler.id}
|
||||
className="glass-card p-6 rounded-lg border border-white/10 hover:border-white/30 transition-all"
|
||||
>
|
||||
{/* Заголовок карточки */}
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
@ -728,27 +826,39 @@ export function BusinessDemo() {
|
||||
{wholesaler.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="text-white font-semibold text-lg truncate">{wholesaler.name}</h4>
|
||||
{wholesaler.verifiedBadges.includes('verified') && (
|
||||
<Badge className="bg-green-600 text-white text-xs">Проверен</Badge>
|
||||
<h4 className="text-white font-semibold text-lg truncate">
|
||||
{wholesaler.name}
|
||||
</h4>
|
||||
{wholesaler.verifiedBadges.includes("verified") && (
|
||||
<Badge className="bg-green-600 text-white text-xs">
|
||||
Проверен
|
||||
</Badge>
|
||||
)}
|
||||
{wholesaler.verifiedBadges.includes('premium') && (
|
||||
<Badge className="bg-yellow-600 text-white text-xs">Premium</Badge>
|
||||
{wholesaler.verifiedBadges.includes("premium") && (
|
||||
<Badge className="bg-yellow-600 text-white text-xs">
|
||||
Premium
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-white/70 text-sm mb-2">{wholesaler.fullName}</p>
|
||||
<p className="text-white/60 text-xs">ИНН: {wholesaler.inn}</p>
|
||||
|
||||
|
||||
<p className="text-white/70 text-sm mb-2">
|
||||
{wholesaler.fullName}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
ИНН: {wholesaler.inn}
|
||||
</p>
|
||||
|
||||
{/* Рейтинг и статистика */}
|
||||
<div className="flex items-center gap-4 mt-2 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
|
||||
<span className="text-white">{wholesaler.rating}</span>
|
||||
<span className="text-white/60">({wholesaler.reviewsCount})</span>
|
||||
<span className="text-white/60">
|
||||
({wholesaler.reviewsCount})
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-white/60">
|
||||
{wholesaler.completedOrders} заказов
|
||||
@ -769,15 +879,19 @@ export function BusinessDemo() {
|
||||
<Package className="h-4 w-4 text-purple-400" />
|
||||
<span className="text-white/70 text-xs">Товаров</span>
|
||||
</div>
|
||||
<div className="text-white font-bold">{wholesaler.productsCount.toLocaleString()}</div>
|
||||
<div className="text-white font-bold">
|
||||
{wholesaler.productsCount.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Clock className="h-4 w-4 text-green-400" />
|
||||
<span className="text-white/70 text-xs">Ответ</span>
|
||||
</div>
|
||||
<div className="text-white font-bold">{wholesaler.responseTime}</div>
|
||||
<div className="text-white font-bold">
|
||||
{wholesaler.responseTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -786,7 +900,11 @@ export function BusinessDemo() {
|
||||
<p className="text-white/70 text-xs mb-2">Категории:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{wholesaler.categories.map((category, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs border-white/30 text-white/70">
|
||||
<Badge
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="text-xs border-white/30 text-white/70"
|
||||
>
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
@ -827,10 +945,16 @@ export function BusinessDemo() {
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
Смотреть товары
|
||||
</Button>
|
||||
<Button variant="outline" className="bg-white/10 hover:bg-white/20 text-white border-white/30">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/30"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" className="bg-white/10 hover:bg-white/20 text-white border-white/30">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/30"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -839,6 +963,9 @@ export function BusinessDemo() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Космически-галактические табели рабочего времени */}
|
||||
<TimesheetDemo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
3751
src/components/admin/ui-kit/timesheet-demo.tsx
Normal file
3751
src/components/admin/ui-kit/timesheet-demo.tsx
Normal file
@ -0,0 +1,3751 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
interface CalendarDay {
|
||||
day: number;
|
||||
status: string;
|
||||
hours: number;
|
||||
overtime: number;
|
||||
workType: string | null;
|
||||
mood: string | null;
|
||||
efficiency: number | null;
|
||||
tasks: number;
|
||||
breaks: number;
|
||||
}
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Clock,
|
||||
Star,
|
||||
Award,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
Download,
|
||||
Filter,
|
||||
MoreHorizontal,
|
||||
MapPin,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Coffee,
|
||||
Home,
|
||||
Plane,
|
||||
Heart,
|
||||
Zap,
|
||||
Moon,
|
||||
Activity,
|
||||
Eye,
|
||||
Plus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
export function TimesheetDemo() {
|
||||
const [selectedVariant, setSelectedVariant] = useState<
|
||||
| "galaxy"
|
||||
| "cosmic"
|
||||
| "custom"
|
||||
| "compact"
|
||||
| "interactive"
|
||||
| "multi-employee"
|
||||
>("galaxy");
|
||||
const [selectedEmployee, setSelectedEmployee] = useState("employee1");
|
||||
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
|
||||
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
|
||||
const [animatedStats, setAnimatedStats] = useState(false);
|
||||
const [editableCalendarData, setEditableCalendarData] = useState<
|
||||
CalendarDay[]
|
||||
>([]);
|
||||
const [calendarData, setCalendarData] = useState<CalendarDay[]>([]);
|
||||
|
||||
// Данные сотрудников
|
||||
const employees = [
|
||||
{
|
||||
id: "employee1",
|
||||
name: "Алексей Космонавтов",
|
||||
position: "Senior Frontend Developer",
|
||||
avatar: "/placeholder-employee-1.jpg",
|
||||
department: "Отдел разработки",
|
||||
level: "Senior",
|
||||
experience: "5 лет",
|
||||
efficiency: 95,
|
||||
totalHours: 176,
|
||||
workDays: 22,
|
||||
overtime: 8,
|
||||
projects: 3,
|
||||
},
|
||||
{
|
||||
id: "employee2",
|
||||
name: "Мария Звездочетова",
|
||||
position: "UX/UI Designer",
|
||||
avatar: "/placeholder-employee-2.jpg",
|
||||
department: "Дизайн-студия",
|
||||
level: "Middle",
|
||||
experience: "3 года",
|
||||
efficiency: 88,
|
||||
totalHours: 168,
|
||||
workDays: 21,
|
||||
overtime: 4,
|
||||
projects: 5,
|
||||
},
|
||||
{
|
||||
id: "employee3",
|
||||
name: "Иван Галактический",
|
||||
position: "DevOps Engineer",
|
||||
avatar: "/placeholder-employee-3.jpg",
|
||||
department: "Инфраструктура",
|
||||
level: "Lead",
|
||||
experience: "7 лет",
|
||||
efficiency: 92,
|
||||
totalHours: 184,
|
||||
workDays: 23,
|
||||
overtime: 12,
|
||||
projects: 2,
|
||||
},
|
||||
];
|
||||
|
||||
// Состояние для универсального табеля
|
||||
const [employeesList, setEmployeesList] = useState(employees);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newEmployee, setNewEmployee] = useState({
|
||||
name: '',
|
||||
position: '',
|
||||
department: '',
|
||||
level: 'Junior'
|
||||
});
|
||||
|
||||
// Генерируем данные календаря для всех сотрудников
|
||||
const generateEmployeeCalendarData = () => {
|
||||
const daysInMonth = new Date(selectedYear, selectedMonth + 1, 0).getDate();
|
||||
const employeeData: { [key: string]: CalendarDay[] } = {};
|
||||
|
||||
employeesList.forEach(employee => {
|
||||
employeeData[employee.id] = Array.from({ length: daysInMonth }, (_, i) => {
|
||||
const dayOfWeek = (new Date(selectedYear, selectedMonth, 1).getDay() === 0 ? 6 : new Date(selectedYear, selectedMonth, 1).getDay() - 1 + i) % 7;
|
||||
const isWeekend = dayOfWeek >= 5;
|
||||
|
||||
return {
|
||||
day: i + 1,
|
||||
status: isWeekend
|
||||
? "weekend"
|
||||
: Math.random() > 0.95
|
||||
? "sick"
|
||||
: Math.random() > 0.9
|
||||
? "vacation"
|
||||
: "work",
|
||||
hours: isWeekend ? 0 : Math.floor(Math.random() * 3) + 7,
|
||||
overtime: Math.random() > 0.8 ? Math.floor(Math.random() * 3) + 1 : 0,
|
||||
workType: isWeekend ? null : ["office", "remote", "hybrid"][Math.floor(Math.random() * 3)],
|
||||
mood: isWeekend ? null : ["excellent", "good", "normal", "tired"][Math.floor(Math.random() * 4)],
|
||||
efficiency: isWeekend ? null : Math.floor(Math.random() * 30) + 70,
|
||||
tasks: isWeekend ? 0 : Math.floor(Math.random() * 8) + 2,
|
||||
breaks: isWeekend ? 0 : Math.floor(Math.random() * 3) + 1,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return employeeData;
|
||||
};
|
||||
|
||||
const [allEmployeesData, setAllEmployeesData] = useState(generateEmployeeCalendarData());
|
||||
|
||||
// Добавление нового сотрудника
|
||||
const handleAddEmployee = () => {
|
||||
if (newEmployee.name && newEmployee.position) {
|
||||
const newEmp = {
|
||||
id: `employee${Date.now()}`,
|
||||
name: newEmployee.name,
|
||||
position: newEmployee.position,
|
||||
department: newEmployee.department,
|
||||
level: newEmployee.level,
|
||||
avatar: `/placeholder-employee-${employeesList.length + 1}.jpg`,
|
||||
experience: "Новый сотрудник",
|
||||
efficiency: Math.floor(Math.random() * 20) + 80,
|
||||
totalHours: 0,
|
||||
workDays: 0,
|
||||
overtime: 0,
|
||||
projects: Math.floor(Math.random() * 5) + 1,
|
||||
};
|
||||
setEmployeesList([...employeesList, newEmp]);
|
||||
setNewEmployee({ name: '', position: '', department: '', level: 'Junior' });
|
||||
setShowAddForm(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Удаление сотрудника
|
||||
const handleRemoveEmployee = (employeeId: string) => {
|
||||
setEmployeesList(employeesList.filter(emp => emp.id !== employeeId));
|
||||
};
|
||||
|
||||
// Получение цвета для сотрудника
|
||||
const getEmployeeColor = (index: number) => {
|
||||
const colors = [
|
||||
'from-cyan-500 to-blue-500',
|
||||
'from-pink-500 to-purple-500',
|
||||
'from-emerald-500 to-teal-500',
|
||||
'from-orange-500 to-red-500',
|
||||
'from-yellow-500 to-amber-500',
|
||||
'from-indigo-500 to-purple-500',
|
||||
'from-green-500 to-lime-500',
|
||||
'from-rose-500 to-pink-500'
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
// Получение статуса дня для конкретного сотрудника
|
||||
const getDayStatus = (employeeId: string, dayIndex: number) => {
|
||||
return allEmployeesData[employeeId]?.[dayIndex] || null;
|
||||
};
|
||||
|
||||
// Подсчет работающих сотрудников в конкретный день
|
||||
const getWorkingEmployeesCount = (dayIndex: number) => {
|
||||
return employeesList.filter(emp => {
|
||||
const dayData = getDayStatus(emp.id, dayIndex);
|
||||
return dayData?.status === 'work';
|
||||
}).length;
|
||||
};
|
||||
|
||||
// Анимация статистики
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setAnimatedStats(true), 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Обновляем данные при изменении списка сотрудников или месяца
|
||||
useEffect(() => {
|
||||
setAllEmployeesData(generateEmployeeCalendarData());
|
||||
}, [employeesList, selectedMonth, selectedYear]);
|
||||
|
||||
// Инициализация данных календаря для интерактивного режима
|
||||
useEffect(() => {
|
||||
if (editableCalendarData.length === 0 && calendarData.length > 0) {
|
||||
setEditableCalendarData([...calendarData]);
|
||||
}
|
||||
}, [calendarData, editableCalendarData.length]);
|
||||
|
||||
// Подсчет статистики на основе редактируемых данных
|
||||
const interactiveStats = React.useMemo(() => {
|
||||
if (editableCalendarData.length === 0) {
|
||||
return {
|
||||
totalHours: 0,
|
||||
workDays: 0,
|
||||
vacation: 0,
|
||||
sick: 0,
|
||||
overtime: 0,
|
||||
avgEfficiency: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const workDays = editableCalendarData.filter(
|
||||
(day) => day.status === "work"
|
||||
).length;
|
||||
const totalHours = editableCalendarData.reduce(
|
||||
(sum, day) => sum + day.hours,
|
||||
0
|
||||
);
|
||||
const vacation = editableCalendarData.filter(
|
||||
(day) => day.status === "vacation"
|
||||
).length;
|
||||
const sick = editableCalendarData.filter(
|
||||
(day) => day.status === "sick"
|
||||
).length;
|
||||
const overtime = editableCalendarData.reduce(
|
||||
(sum, day) => sum + day.overtime,
|
||||
0
|
||||
);
|
||||
const avgEfficiency =
|
||||
workDays > 0
|
||||
? Math.round(
|
||||
editableCalendarData.reduce(
|
||||
(sum, day) => sum + (day.efficiency || 0),
|
||||
0
|
||||
) / workDays
|
||||
)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalHours,
|
||||
workDays,
|
||||
vacation,
|
||||
sick,
|
||||
overtime,
|
||||
avgEfficiency,
|
||||
};
|
||||
}, [editableCalendarData]);
|
||||
|
||||
// Функция для изменения статуса дня
|
||||
const toggleDayStatus = (dayIndex: number) => {
|
||||
const statuses = ["work", "weekend", "vacation", "sick", "absent"];
|
||||
const currentDay = editableCalendarData[dayIndex];
|
||||
if (!currentDay) return;
|
||||
|
||||
const currentStatusIndex = statuses.indexOf(currentDay.status);
|
||||
const nextStatusIndex = (currentStatusIndex + 1) % statuses.length;
|
||||
const newStatus = statuses[nextStatusIndex];
|
||||
|
||||
const updatedData = [...editableCalendarData];
|
||||
updatedData[dayIndex] = {
|
||||
...currentDay,
|
||||
status: newStatus,
|
||||
hours: newStatus === "work" ? 8 : 0,
|
||||
overtime: newStatus === "work" ? Math.floor(Math.random() * 3) : 0,
|
||||
};
|
||||
|
||||
setEditableCalendarData(updatedData);
|
||||
};
|
||||
|
||||
const currentEmployee =
|
||||
employees.find((emp) => emp.id === selectedEmployee) || employees[0];
|
||||
|
||||
// Обновление данных при изменении месяца/года
|
||||
useEffect(() => {
|
||||
const generateData = () => {
|
||||
const daysInMonth = new Date(
|
||||
selectedYear,
|
||||
selectedMonth + 1,
|
||||
0
|
||||
).getDate();
|
||||
const firstDay = new Date(selectedYear, selectedMonth, 1).getDay();
|
||||
const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1;
|
||||
|
||||
const workTypes = ["office", "remote", "hybrid"];
|
||||
const moods = ["excellent", "good", "normal", "tired"];
|
||||
|
||||
return Array.from({ length: daysInMonth }, (_, i) => {
|
||||
const dayOfWeek = (adjustedFirstDay + i) % 7;
|
||||
const isWeekend = dayOfWeek >= 5;
|
||||
|
||||
return {
|
||||
day: i + 1,
|
||||
status: isWeekend
|
||||
? "weekend"
|
||||
: Math.random() > 0.95
|
||||
? "sick"
|
||||
: Math.random() > 0.9
|
||||
? "vacation"
|
||||
: "work",
|
||||
hours: isWeekend ? 0 : Math.floor(Math.random() * 3) + 7,
|
||||
overtime: Math.random() > 0.8 ? Math.floor(Math.random() * 3) + 1 : 0,
|
||||
workType: isWeekend
|
||||
? null
|
||||
: workTypes[Math.floor(Math.random() * workTypes.length)],
|
||||
mood: isWeekend
|
||||
? null
|
||||
: moods[Math.floor(Math.random() * moods.length)],
|
||||
efficiency: isWeekend ? null : Math.floor(Math.random() * 30) + 70,
|
||||
tasks: isWeekend ? 0 : Math.floor(Math.random() * 8) + 2,
|
||||
breaks: isWeekend ? 0 : Math.floor(Math.random() * 3) + 1,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
setCalendarData(generateData());
|
||||
setAnimatedStats(false);
|
||||
const timer = setTimeout(() => setAnimatedStats(true), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [selectedMonth, selectedYear]);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "work":
|
||||
return "bg-gradient-to-r from-emerald-500 to-green-500";
|
||||
case "weekend":
|
||||
return "bg-gradient-to-r from-slate-500 to-gray-500";
|
||||
case "vacation":
|
||||
return "bg-gradient-to-r from-blue-500 to-cyan-500";
|
||||
case "sick":
|
||||
return "bg-gradient-to-r from-amber-500 to-orange-500";
|
||||
case "absent":
|
||||
return "bg-gradient-to-r from-red-500 to-rose-500";
|
||||
default:
|
||||
return "bg-gradient-to-r from-slate-500 to-gray-500";
|
||||
}
|
||||
};
|
||||
|
||||
const getWorkTypeIcon = (workType: string | null) => {
|
||||
switch (workType) {
|
||||
case "office":
|
||||
return <MapPin className="h-3 w-3" />;
|
||||
case "remote":
|
||||
return <Home className="h-3 w-3" />;
|
||||
case "hybrid":
|
||||
return <Zap className="h-3 w-3" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getMoodIcon = (mood: string | null) => {
|
||||
switch (mood) {
|
||||
case "excellent":
|
||||
return <Star className="h-3 w-3 text-yellow-400" />;
|
||||
case "good":
|
||||
return <CheckCircle className="h-3 w-3 text-green-400" />;
|
||||
case "normal":
|
||||
return <Clock className="h-3 w-3 text-blue-400" />;
|
||||
case "tired":
|
||||
return <Coffee className="h-3 w-3 text-orange-400" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const monthNames = [
|
||||
"Январь",
|
||||
"Февраль",
|
||||
"Март",
|
||||
"Апрель",
|
||||
"Май",
|
||||
"Июнь",
|
||||
"Июль",
|
||||
"Август",
|
||||
"Сентябрь",
|
||||
"Октябрь",
|
||||
"Ноябрь",
|
||||
"Декабрь",
|
||||
];
|
||||
|
||||
const dayNames = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||||
|
||||
// Статистика
|
||||
const stats = {
|
||||
totalHours: calendarData.reduce((sum, day) => sum + day.hours, 0),
|
||||
workDays: calendarData.filter((day) => day.status === "work").length,
|
||||
weekends: calendarData.filter((day) => day.status === "weekend").length,
|
||||
vacation: calendarData.filter((day) => day.status === "vacation").length,
|
||||
sick: calendarData.filter((day) => day.status === "sick").length,
|
||||
overtime: calendarData.reduce((sum, day) => sum + day.overtime, 0),
|
||||
avgEfficiency: Math.round(
|
||||
calendarData
|
||||
.filter((day) => day.efficiency)
|
||||
.reduce(
|
||||
(sum, day, _, arr) => sum + (day.efficiency || 0) / arr.length,
|
||||
0
|
||||
)
|
||||
),
|
||||
totalTasks: calendarData.reduce((sum, day) => sum + day.tasks, 0),
|
||||
};
|
||||
|
||||
const renderGalaxyVariant = () => (
|
||||
<Card className="glass-card border-white/10 overflow-hidden relative">
|
||||
{/* Космический фон с анимацией */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-blue-900/20 to-indigo-900/20">
|
||||
<div
|
||||
className="absolute inset-0 opacity-50"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="7" cy="7" r="1"/%3E%3Ccircle cx="27" cy="27" r="1"/%3E%3Ccircle cx="47" cy="47" r="1"/%3E%3Ccircle cx="17" cy="37" r="1"/%3E%3Ccircle cx="37" cy="17" r="1"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\')',
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Плавающие частицы */}
|
||||
<div className="absolute top-10 left-10 w-2 h-2 bg-purple-400/30 rounded-full animate-pulse"></div>
|
||||
<div className="absolute top-20 right-20 w-1 h-1 bg-blue-400/40 rounded-full animate-pulse delay-1000"></div>
|
||||
<div className="absolute bottom-20 left-20 w-1.5 h-1.5 bg-cyan-400/30 rounded-full animate-pulse delay-2000"></div>
|
||||
<div className="absolute bottom-10 right-10 w-1 h-1 bg-purple-300/40 rounded-full animate-pulse delay-500"></div>
|
||||
</div>
|
||||
|
||||
<CardHeader className="relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Avatar className="h-16 w-16 ring-2 ring-purple-500/50 ring-offset-2 ring-offset-gray-900">
|
||||
<AvatarImage src={currentEmployee.avatar} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-600 to-blue-600 text-white text-lg font-bold">
|
||||
{currentEmployee.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="absolute -top-1 -right-1 w-5 h-5 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white mb-1">
|
||||
{currentEmployee.name}
|
||||
</h3>
|
||||
<p className="text-purple-300 text-sm mb-1">
|
||||
{currentEmployee.position}
|
||||
</p>
|
||||
<div className="flex items-center space-x-3 text-xs text-white/70">
|
||||
<span>{currentEmployee.department}</span>
|
||||
<span>•</span>
|
||||
<Badge className="bg-purple-600/30 text-purple-200 border-purple-500/30">
|
||||
{currentEmployee.level}
|
||||
</Badge>
|
||||
<span>•</span>
|
||||
<span>{currentEmployee.experience}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-white mb-1 bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">
|
||||
{animatedStats ? stats.totalHours : 0}ч
|
||||
</div>
|
||||
<p className="text-purple-300 text-sm">
|
||||
Отработано в {monthNames[selectedMonth].toLowerCase()}
|
||||
</p>
|
||||
<div className="flex items-center justify-end mt-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-current" />
|
||||
<span className="text-white font-medium">
|
||||
{currentEmployee.efficiency}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Навигация по месяцам */}
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Select
|
||||
value={selectedEmployee}
|
||||
onValueChange={setSelectedEmployee}
|
||||
>
|
||||
<SelectTrigger className="w-64 glass-input bg-white/10 border-white/20 text-white hover:bg-white/15">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
|
||||
{employees.map((emp) => (
|
||||
<SelectItem
|
||||
key={emp.id}
|
||||
value={emp.id}
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={emp.avatar} />
|
||||
<AvatarFallback className="bg-purple-600 text-white text-xs">
|
||||
{emp.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{emp.name}</div>
|
||||
<div className="text-xs text-white/60">
|
||||
{emp.position}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 0) {
|
||||
setSelectedMonth(11);
|
||||
setSelectedYear(selectedYear - 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth - 1);
|
||||
}
|
||||
}}
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="text-white font-semibold text-lg min-w-[140px] text-center">
|
||||
{monthNames[selectedMonth]} {selectedYear}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 11) {
|
||||
setSelectedMonth(0);
|
||||
setSelectedYear(selectedYear + 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth + 1);
|
||||
}
|
||||
}}
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Экспорт
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6 relative z-10">
|
||||
{/* Статистические карты */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-6">
|
||||
<div className="glass-card p-4 rounded-xl border border-purple-500/30 bg-gradient-to-br from-purple-500/10 to-blue-500/10 hover:from-purple-500/20 hover:to-blue-500/20 transition-all duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Clock className="h-5 w-5 text-purple-400" />
|
||||
<div className="text-right">
|
||||
<div className="text-white text-lg font-bold">
|
||||
{animatedStats ? stats.totalHours : 0}
|
||||
</div>
|
||||
<div className="text-purple-300 text-xs">Часов</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={animatedStats ? (stats.totalHours / 200) * 100 : 0}
|
||||
className="h-2 bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl border border-green-500/30 bg-gradient-to-br from-green-500/10 to-emerald-500/10 hover:from-green-500/20 hover:to-emerald-500/20 transition-all duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
||||
<div className="text-right">
|
||||
<div className="text-white text-lg font-bold">
|
||||
{animatedStats ? stats.workDays : 0}
|
||||
</div>
|
||||
<div className="text-green-300 text-xs">Рабочих дней</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={animatedStats ? (stats.workDays / 25) * 100 : 0}
|
||||
className="h-2 bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl border border-blue-500/30 bg-gradient-to-br from-blue-500/10 to-cyan-500/10 hover:from-blue-500/20 hover:to-cyan-500/20 transition-all duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Plane className="h-5 w-5 text-blue-400" />
|
||||
<div className="text-right">
|
||||
<div className="text-white text-lg font-bold">
|
||||
{animatedStats ? stats.vacation : 0}
|
||||
</div>
|
||||
<div className="text-blue-300 text-xs">Отпуск</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={animatedStats ? (stats.vacation / 5) * 100 : 0}
|
||||
className="h-2 bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl border border-orange-500/30 bg-gradient-to-br from-orange-500/10 to-yellow-500/10 hover:from-orange-500/20 hover:to-yellow-500/20 transition-all duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Heart className="h-5 w-5 text-orange-400" />
|
||||
<div className="text-right">
|
||||
<div className="text-white text-lg font-bold">
|
||||
{animatedStats ? stats.sick : 0}
|
||||
</div>
|
||||
<div className="text-orange-300 text-xs">Больничный</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={animatedStats ? (stats.sick / 3) * 100 : 0}
|
||||
className="h-2 bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl border border-yellow-500/30 bg-gradient-to-br from-yellow-500/10 to-amber-500/10 hover:from-yellow-500/20 hover:to-amber-500/20 transition-all duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Zap className="h-5 w-5 text-yellow-400" />
|
||||
<div className="text-right">
|
||||
<div className="text-white text-lg font-bold">
|
||||
{animatedStats ? stats.overtime : 0}
|
||||
</div>
|
||||
<div className="text-yellow-300 text-xs">Переработка</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={animatedStats ? (stats.overtime / 20) * 100 : 0}
|
||||
className="h-2 bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 rounded-xl border border-pink-500/30 bg-gradient-to-br from-pink-500/10 to-rose-500/10 hover:from-pink-500/20 hover:to-rose-500/20 transition-all duration-300">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Activity className="h-5 w-5 text-pink-400" />
|
||||
<div className="text-right">
|
||||
<div className="text-white text-lg font-bold">
|
||||
{animatedStats ? stats.avgEfficiency : 0}%
|
||||
</div>
|
||||
<div className="text-pink-300 text-xs">Эффективность</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={animatedStats ? stats.avgEfficiency : 0}
|
||||
className="h-2 bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Календарь */}
|
||||
<div className="space-y-4">
|
||||
{/* Заголовки дней недели */}
|
||||
<div className="grid grid-cols-7 gap-2 text-center">
|
||||
{dayNames.map((day) => (
|
||||
<div key={day} className="text-white/70 font-medium text-sm py-2">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Дни месяца */}
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{/* Пустые ячейки для начала месяца */}
|
||||
{Array.from({
|
||||
length:
|
||||
new Date(selectedYear, selectedMonth, 1).getDay() === 0
|
||||
? 6
|
||||
: new Date(selectedYear, selectedMonth, 1).getDay() - 1,
|
||||
}).map((_, index) => (
|
||||
<div key={`empty-${index}`} className="aspect-square"></div>
|
||||
))}
|
||||
|
||||
{/* Дни месяца */}
|
||||
{calendarData.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
aspect-square p-2 rounded-xl border transition-all duration-300 hover:scale-105 cursor-pointer group
|
||||
${
|
||||
day.status === "work"
|
||||
? "border-green-500/30 bg-gradient-to-br from-green-500/10 to-emerald-500/10 hover:from-green-500/20 hover:to-emerald-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "weekend"
|
||||
? "border-gray-500/30 bg-gradient-to-br from-gray-500/10 to-slate-500/10"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "vacation"
|
||||
? "border-blue-500/30 bg-gradient-to-br from-blue-500/10 to-cyan-500/10"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "sick"
|
||||
? "border-orange-500/30 bg-gradient-to-br from-orange-500/10 to-yellow-500/10"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "absent"
|
||||
? "border-red-500/30 bg-gradient-to-br from-red-500/10 to-rose-500/10"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="h-full flex flex-col justify-between">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white font-medium text-sm">
|
||||
{day.day}
|
||||
</span>
|
||||
{day.workType && (
|
||||
<div className="text-white/60">
|
||||
{getWorkTypeIcon(day.workType)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{day.status === "work" && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/80 text-xs">
|
||||
{day.hours}ч
|
||||
</span>
|
||||
{day.overtime > 0 && (
|
||||
<span className="text-yellow-400 text-xs">
|
||||
+{day.overtime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{getMoodIcon(day.mood)}
|
||||
{day.efficiency && (
|
||||
<span className="text-white/60 text-xs">
|
||||
{day.efficiency}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{day.status !== "work" && day.status !== "weekend" && (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${getStatusColor(
|
||||
day.status
|
||||
)}`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Легенда */}
|
||||
<div className="flex flex-wrap gap-4 text-sm justify-center">
|
||||
<div className="flex items-center gap-2 bg-white/5 px-3 py-2 rounded-lg">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-emerald-500 to-green-500"></div>
|
||||
<span className="text-white/70">Работа</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/5 px-3 py-2 rounded-lg">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-slate-500 to-gray-500"></div>
|
||||
<span className="text-white/70">Выходной</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/5 px-3 py-2 rounded-lg">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500"></div>
|
||||
<span className="text-white/70">Отпуск</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/5 px-3 py-2 rounded-lg">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-amber-500 to-orange-500"></div>
|
||||
<span className="text-white/70">Больничный</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white/5 px-3 py-2 rounded-lg">
|
||||
<div className="w-3 h-3 rounded-full bg-gradient-to-r from-red-500 to-rose-500"></div>
|
||||
<span className="text-white/70">Прогул</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderCosmicVariant = () => (
|
||||
<Card className="glass-card border-white/10 overflow-hidden relative">
|
||||
{/* Космический фон с эффектом туманности */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-indigo-900/30 via-purple-900/30 to-pink-900/30">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-purple-600/10 via-blue-600/5 to-transparent"></div>
|
||||
<div
|
||||
className="absolute inset-0 bg-[conic-gradient(from_0deg_at_50%_50%,_var(--tw-gradient-stops))] from-transparent via-purple-500/5 to-transparent animate-spin"
|
||||
style={{ animationDuration: "20s" }}
|
||||
></div>
|
||||
|
||||
{/* Звездное поле */}
|
||||
<div className="absolute top-5 left-5 w-1 h-1 bg-white/60 rounded-full animate-pulse"></div>
|
||||
<div className="absolute top-12 right-12 w-0.5 h-0.5 bg-blue-300/70 rounded-full animate-pulse delay-300"></div>
|
||||
<div className="absolute bottom-20 left-8 w-1.5 h-1.5 bg-purple-300/50 rounded-full animate-pulse delay-700"></div>
|
||||
<div className="absolute bottom-8 right-20 w-1 h-1 bg-pink-300/60 rounded-full animate-pulse delay-1000"></div>
|
||||
<div className="absolute top-1/3 left-1/4 w-0.5 h-0.5 bg-cyan-300/80 rounded-full animate-pulse delay-500"></div>
|
||||
<div className="absolute top-2/3 right-1/3 w-1 h-1 bg-yellow-300/50 rounded-full animate-pulse delay-1200"></div>
|
||||
</div>
|
||||
|
||||
<CardHeader className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full animate-pulse opacity-50"></div>
|
||||
<Avatar className="h-20 w-20 relative z-10 ring-4 ring-gradient-to-r from-purple-400 to-pink-400 ring-offset-4 ring-offset-gray-900">
|
||||
<AvatarImage src={currentEmployee.avatar} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-600 via-indigo-600 to-pink-600 text-white text-xl font-bold">
|
||||
{currentEmployee.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{/* Орбитальные элементы */}
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full flex items-center justify-center animate-bounce">
|
||||
<CheckCircle className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
|
||||
<div className="absolute -bottom-1 -left-1 w-4 h-4 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full flex items-center justify-center">
|
||||
<Star className="h-2 w-2 text-white fill-current" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold bg-gradient-to-r from-white via-purple-200 to-pink-200 bg-clip-text text-transparent mb-2">
|
||||
{currentEmployee.name}
|
||||
</h3>
|
||||
<p className="text-purple-300 text-base mb-2 font-medium">
|
||||
{currentEmployee.position}
|
||||
</p>
|
||||
<div className="flex items-center space-x-4 text-sm">
|
||||
<Badge className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white border-none">
|
||||
{currentEmployee.department}
|
||||
</Badge>
|
||||
<Badge className="bg-gradient-to-r from-pink-600 to-rose-600 text-white border-none">
|
||||
{currentEmployee.level}
|
||||
</Badge>
|
||||
<span className="text-white/70">
|
||||
{currentEmployee.experience} опыта
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right space-y-2">
|
||||
<div className="text-4xl font-bold bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{animatedStats ? stats.totalHours : 0}
|
||||
</div>
|
||||
<p className="text-purple-300 font-medium">
|
||||
часов в {monthNames[selectedMonth].toLowerCase()}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-end space-x-4 mt-3">
|
||||
<div className="flex items-center space-x-2 bg-white/10 rounded-full px-3 py-1">
|
||||
<Activity className="h-4 w-4 text-cyan-400" />
|
||||
<span className="text-white font-bold">
|
||||
{currentEmployee.efficiency}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 bg-white/10 rounded-full px-3 py-1">
|
||||
<Award className="h-4 w-4 text-yellow-400" />
|
||||
<span className="text-white font-bold">
|
||||
{currentEmployee.projects}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Панель управления */}
|
||||
<div className="flex items-center justify-between bg-white/5 rounded-2xl p-4 backdrop-blur-sm border border-white/10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Select
|
||||
value={selectedEmployee}
|
||||
onValueChange={setSelectedEmployee}
|
||||
>
|
||||
<SelectTrigger className="w-72 glass-input bg-white/10 border-white/20 text-white hover:bg-white/15 rounded-xl">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white rounded-xl">
|
||||
{employees.map((emp) => (
|
||||
<SelectItem
|
||||
key={emp.id}
|
||||
value={emp.id}
|
||||
className="text-white hover:bg-white/10 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={emp.avatar} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-600 to-pink-600 text-white text-xs">
|
||||
{emp.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{emp.name}</div>
|
||||
<div className="text-xs text-white/60">
|
||||
{emp.position}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 0) {
|
||||
setSelectedMonth(11);
|
||||
setSelectedYear(selectedYear - 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth - 1);
|
||||
}
|
||||
}}
|
||||
className="text-white hover:bg-white/10 rounded-xl"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<div className="text-white font-bold text-xl min-w-[160px] text-center bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
{monthNames[selectedMonth]} {selectedYear}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 11) {
|
||||
setSelectedMonth(0);
|
||||
setSelectedYear(selectedYear + 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth + 1);
|
||||
}
|
||||
}}
|
||||
className="text-white hover:bg-white/10 rounded-xl"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10 rounded-xl"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Экспорт
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10 rounded-xl"
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Фильтр
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10 rounded-xl"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-8 relative z-10">
|
||||
{/* Круговая статистика */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6 mb-8">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-purple-500/30 hover:border-purple-400/50 transition-all duration-300">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-purple)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.totalHours / 200) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-2xl font-bold mb-1">
|
||||
{animatedStats ? stats.totalHours : 0}
|
||||
</div>
|
||||
<div className="text-purple-300 text-sm">Часов</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-green-500 to-emerald-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-green-500/30 hover:border-green-400/50 transition-all duration-300">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-green)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.workDays / 25) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<CheckCircle className="h-6 w-6 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-2xl font-bold mb-1">
|
||||
{animatedStats ? stats.workDays : 0}
|
||||
</div>
|
||||
<div className="text-green-300 text-sm">Рабочих</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-blue-500/30 hover:border-blue-400/50 transition-all duration-300">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-blue)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.vacation / 5) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Plane className="h-6 w-6 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-2xl font-bold mb-1">
|
||||
{animatedStats ? stats.vacation : 0}
|
||||
</div>
|
||||
<div className="text-blue-300 text-sm">Отпуск</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-orange-500 to-yellow-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-orange-500/30 hover:border-orange-400/50 transition-all duration-300">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-orange)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.sick / 3) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Heart className="h-6 w-6 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-2xl font-bold mb-1">
|
||||
{animatedStats ? stats.sick : 0}
|
||||
</div>
|
||||
<div className="text-orange-300 text-sm">Больничный</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-yellow-500 to-amber-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-yellow-500/30 hover:border-yellow-400/50 transition-all duration-300">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-yellow)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.overtime / 20) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Zap className="h-6 w-6 text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-2xl font-bold mb-1">
|
||||
{animatedStats ? stats.overtime : 0}
|
||||
</div>
|
||||
<div className="text-yellow-300 text-sm">Переработка</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-pink-500 to-rose-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-pink-500/30 hover:border-pink-400/50 transition-all duration-300">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-pink)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? stats.avgEfficiency : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-pink-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-2xl font-bold mb-1">
|
||||
{animatedStats ? stats.avgEfficiency : 0}%
|
||||
</div>
|
||||
<div className="text-pink-300 text-sm">КПД</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Календарь в виде гексагональной сетки */}
|
||||
<div className="space-y-6">
|
||||
{/* Заголовки дней недели */}
|
||||
<div className="flex justify-center">
|
||||
<div className="grid grid-cols-7 gap-4 text-center max-w-2xl">
|
||||
{dayNames.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="text-white/70 font-bold text-lg py-3 bg-white/5 rounded-xl"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Календарная сетка */}
|
||||
<div className="flex justify-center">
|
||||
<div className="grid grid-cols-7 gap-4 max-w-2xl">
|
||||
{/* Пустые ячейки для начала месяца */}
|
||||
{Array.from({
|
||||
length:
|
||||
new Date(selectedYear, selectedMonth, 1).getDay() === 0
|
||||
? 6
|
||||
: new Date(selectedYear, selectedMonth, 1).getDay() - 1,
|
||||
}).map((_, index) => (
|
||||
<div key={`empty-${index}`} className="aspect-square"></div>
|
||||
))}
|
||||
|
||||
{/* Дни месяца */}
|
||||
{calendarData.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
aspect-square p-3 rounded-2xl border-2 transition-all duration-500 hover:scale-110 cursor-pointer group relative overflow-hidden
|
||||
${
|
||||
day.status === "work"
|
||||
? "border-green-400/50 bg-gradient-to-br from-green-500/20 to-emerald-500/20 hover:from-green-500/30 hover:to-emerald-500/30"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "weekend"
|
||||
? "border-gray-400/50 bg-gradient-to-br from-gray-500/20 to-slate-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "vacation"
|
||||
? "border-blue-400/50 bg-gradient-to-br from-blue-500/20 to-cyan-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "sick"
|
||||
? "border-orange-400/50 bg-gradient-to-br from-orange-500/20 to-yellow-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "absent"
|
||||
? "border-red-400/50 bg-gradient-to-br from-red-500/20 to-rose-500/20"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Эффект свечения */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-2xl"></div>
|
||||
|
||||
<div className="relative h-full flex flex-col justify-between z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white font-bold text-lg">
|
||||
{day.day}
|
||||
</span>
|
||||
{day.workType && (
|
||||
<div className="text-white/80">
|
||||
{getWorkTypeIcon(day.workType)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{day.status === "work" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{day.hours}ч
|
||||
</span>
|
||||
{day.overtime > 0 && (
|
||||
<Badge className="bg-yellow-500/30 text-yellow-200 text-xs border-yellow-400/30">
|
||||
+{day.overtime}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{getMoodIcon(day.mood)}
|
||||
{day.efficiency && (
|
||||
<div className="text-right">
|
||||
<div className="text-white/80 text-xs font-medium">
|
||||
{day.efficiency}%
|
||||
</div>
|
||||
<div className="w-8 h-1 bg-white/20 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-purple-400 to-pink-400 transition-all duration-1000"
|
||||
style={{ width: `${day.efficiency}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{day.status !== "work" && day.status !== "weekend" && (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full ${getStatusColor(
|
||||
day.status
|
||||
)} animate-pulse`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Расширенная легенда */}
|
||||
<div className="bg-white/5 rounded-2xl p-6 backdrop-blur-sm border border-white/10">
|
||||
<h4 className="text-white font-semibold text-lg mb-4 text-center">
|
||||
Легенда статусов
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="flex flex-col items-center space-y-2 bg-white/5 p-4 rounded-xl">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-emerald-500 to-green-500 flex items-center justify-center">
|
||||
<CheckCircle className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-white/70 text-sm font-medium">Работа</span>
|
||||
<span className="text-white/50 text-xs text-center">
|
||||
Обычный рабочий день
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center space-y-2 bg-white/5 p-4 rounded-xl">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-slate-500 to-gray-500 flex items-center justify-center">
|
||||
<Moon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-white/70 text-sm font-medium">
|
||||
Выходной
|
||||
</span>
|
||||
<span className="text-white/50 text-xs text-center">
|
||||
Суббота/Воскресенье
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center space-y-2 bg-white/5 p-4 rounded-xl">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-center">
|
||||
<Plane className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-white/70 text-sm font-medium">Отпуск</span>
|
||||
<span className="text-white/50 text-xs text-center">
|
||||
Оплачиваемый отпуск
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center space-y-2 bg-white/5 p-4 rounded-xl">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 flex items-center justify-center">
|
||||
<Heart className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-white/70 text-sm font-medium">
|
||||
Больничный
|
||||
</span>
|
||||
<span className="text-white/50 text-xs text-center">
|
||||
По болезни
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center space-y-2 bg-white/5 p-4 rounded-xl">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-red-500 to-rose-500 flex items-center justify-center">
|
||||
<XCircle className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-white/70 text-sm font-medium">Прогул</span>
|
||||
<span className="text-white/50 text-xs text-center">
|
||||
Неявка без причины
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* SVG градиенты для круговых диаграмм */}
|
||||
<svg width="0" height="0" style={{ position: "absolute" }}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient-purple"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#8B5CF6" />
|
||||
<stop offset="100%" stopColor="#6366F1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-green"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#10B981" />
|
||||
<stop offset="100%" stopColor="#059669" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-blue"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#3B82F6" />
|
||||
<stop offset="100%" stopColor="#06B6D4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-orange"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#F59E0B" />
|
||||
<stop offset="100%" stopColor="#F97316" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-yellow"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#EAB308" />
|
||||
<stop offset="100%" stopColor="#F59E0B" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-pink"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#EC4899" />
|
||||
<stop offset="100%" stopColor="#F43F5E" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderCustomVariant = () => (
|
||||
<Card
|
||||
className="glass-card border-white/10 overflow-hidden relative"
|
||||
style={{ height: "800px" }}
|
||||
>
|
||||
{/* Космический фон с плавающими частицами и звездным полем (из Галактического) */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-blue-900/20 to-indigo-900/20">
|
||||
<div
|
||||
className="absolute inset-0 opacity-50"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="7" cy="7" r="1"/%3E%3Ccircle cx="27" cy="27" r="1"/%3E%3Ccircle cx="47" cy="47" r="1"/%3E%3Ccircle cx="17" cy="37" r="1"/%3E%3Ccircle cx="37" cy="17" r="1"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\')',
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Плавающие частицы */}
|
||||
<div className="absolute top-10 left-10 w-2 h-2 bg-purple-400/30 rounded-full animate-pulse"></div>
|
||||
<div className="absolute top-20 right-20 w-1 h-1 bg-blue-400/40 rounded-full animate-pulse delay-1000"></div>
|
||||
<div className="absolute bottom-20 left-20 w-1.5 h-1.5 bg-cyan-400/30 rounded-full animate-pulse delay-2000"></div>
|
||||
<div className="absolute bottom-32 right-32 w-1 h-1 bg-pink-400/40 rounded-full animate-pulse delay-3000"></div>
|
||||
<div className="absolute top-1/2 left-1/4 w-0.5 h-0.5 bg-white/60 rounded-full animate-pulse delay-4000"></div>
|
||||
<div className="absolute top-1/3 right-1/3 w-1 h-1 bg-indigo-400/50 rounded-full animate-pulse delay-5000"></div>
|
||||
|
||||
{/* Звездное поле */}
|
||||
<div className="absolute inset-0">
|
||||
{Array.from({ length: 20 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-0.5 h-0.5 bg-white/40 rounded-full animate-pulse"
|
||||
style={{
|
||||
top: `${Math.random() * 100}%`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 3}s`,
|
||||
animationDuration: `${2 + Math.random() * 2}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-6 relative z-10 p-6 h-full overflow-y-auto">
|
||||
{/* Заголовок сотрудника */}
|
||||
<div className="bg-white/5 rounded-2xl p-6 backdrop-blur-sm border border-white/10">
|
||||
<div className="flex items-center space-x-6">
|
||||
<Avatar className="h-20 w-20 ring-4 ring-purple-500/30">
|
||||
<AvatarImage src={currentEmployee.avatar} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-600 to-blue-600 text-white text-xl font-bold">
|
||||
{currentEmployee.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-white mb-1">
|
||||
{currentEmployee.name}
|
||||
</h3>
|
||||
<p className="text-purple-300 text-base mb-2">
|
||||
{currentEmployee.position}
|
||||
</p>
|
||||
<div className="flex items-center space-x-4 text-sm text-white/70">
|
||||
<span>{currentEmployee.department}</span>
|
||||
<span>•</span>
|
||||
<span>{currentEmployee.level}</span>
|
||||
<span>•</span>
|
||||
<span>{currentEmployee.experience}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Круговые диаграммы статистики (из Космического) */}
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-purple)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.totalHours / 200) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">
|
||||
{animatedStats ? stats.totalHours : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-purple-300 text-xs">Часов</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-pink)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? currentEmployee.efficiency : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">
|
||||
{currentEmployee.efficiency}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-pink-300 text-xs">Эффективность</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Навигация и управление */}
|
||||
<div className="flex items-center justify-between bg-white/5 rounded-2xl p-4 backdrop-blur-sm border border-white/10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Select
|
||||
value={selectedEmployee}
|
||||
onValueChange={setSelectedEmployee}
|
||||
>
|
||||
<SelectTrigger className="w-64 glass-input bg-white/10 border-white/20 text-white hover:bg-white/15">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
|
||||
{employees.map((emp) => (
|
||||
<SelectItem
|
||||
key={emp.id}
|
||||
value={emp.id}
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={emp.avatar} />
|
||||
<AvatarFallback className="bg-purple-600 text-white text-xs">
|
||||
{emp.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{emp.name}</div>
|
||||
<div className="text-xs text-white/60">
|
||||
{emp.position}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 0) {
|
||||
setSelectedMonth(11);
|
||||
setSelectedYear(selectedYear - 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="text-center min-w-[120px]">
|
||||
<div className="text-white font-bold text-lg">
|
||||
{monthNames[selectedMonth]} {selectedYear}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 11) {
|
||||
setSelectedMonth(0);
|
||||
setSelectedYear(selectedYear + 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Экспорт
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Статистика с круговыми диаграммами (из Космического) */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6 mb-8">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-purple-500/30 hover:border-purple-400/50 transition-all duration-300 text-center">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-purple)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.totalHours / 200) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-2xl mb-1">
|
||||
{animatedStats ? stats.totalHours : 0}
|
||||
</div>
|
||||
<p className="text-purple-300 text-sm font-medium">Часов</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-green-500 to-emerald-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-green-500/30 hover:border-green-400/50 transition-all duration-300 text-center">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-green)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.workDays / 25) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<CheckCircle className="h-6 w-6 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-2xl mb-1">
|
||||
{animatedStats ? stats.workDays : 0}
|
||||
</div>
|
||||
<p className="text-green-300 text-sm font-medium">Рабочих</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-blue-500/30 hover:border-blue-400/50 transition-all duration-300 text-center">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-blue)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.vacation / 5) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Plane className="h-6 w-6 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-2xl mb-1">
|
||||
{animatedStats ? stats.vacation : 0}
|
||||
</div>
|
||||
<p className="text-blue-300 text-sm font-medium">Отпуск</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-orange-500 to-red-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-orange-500/30 hover:border-orange-400/50 transition-all duration-300 text-center">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-orange)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.sick / 3) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Heart className="h-6 w-6 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-2xl mb-1">
|
||||
{animatedStats ? stats.sick : 0}
|
||||
</div>
|
||||
<p className="text-orange-300 text-sm font-medium">
|
||||
Больничный
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-yellow-500/30 hover:border-yellow-400/50 transition-all duration-300 text-center">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-yellow)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.overtime / 20) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Zap className="h-6 w-6 text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-2xl mb-1">
|
||||
{animatedStats ? stats.overtime : 0}
|
||||
</div>
|
||||
<p className="text-yellow-300 text-sm font-medium">
|
||||
Переработка
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-pink-500 to-purple-500 rounded-2xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-6 rounded-2xl border border-pink-500/30 hover:border-pink-400/50 transition-all duration-300 text-center">
|
||||
<div className="text-center">
|
||||
<div className="relative w-16 h-16 mx-auto mb-3">
|
||||
<svg
|
||||
className="w-16 h-16 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-pink)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? stats.avgEfficiency : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Activity className="h-6 w-6 text-pink-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-2xl mb-1">
|
||||
{animatedStats ? stats.avgEfficiency : 0}%
|
||||
</div>
|
||||
<p className="text-pink-300 text-sm font-medium">КПД</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Гексагональная календарная сетка */}
|
||||
<div className="space-y-4">
|
||||
{/* Заголовки дней недели */}
|
||||
<div className="grid grid-cols-7 gap-3 text-center">
|
||||
{dayNames.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="text-white/70 font-bold text-sm py-2 bg-white/5 rounded-xl border border-white/10"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Календарная сетка */}
|
||||
<div className="grid grid-cols-7 gap-3">
|
||||
{/* Пустые ячейки для начала месяца */}
|
||||
{Array.from({
|
||||
length:
|
||||
new Date(selectedYear, selectedMonth, 1).getDay() === 0
|
||||
? 6
|
||||
: new Date(selectedYear, selectedMonth, 1).getDay() - 1,
|
||||
}).map((_, index) => (
|
||||
<div key={`empty-${index}`} className="aspect-square"></div>
|
||||
))}
|
||||
|
||||
{/* Дни месяца */}
|
||||
{calendarData.map((day) => (
|
||||
<div
|
||||
key={day.day}
|
||||
className={`
|
||||
aspect-square p-3 rounded-2xl border-2 transition-all duration-500 hover:scale-110 cursor-pointer group relative overflow-hidden
|
||||
${
|
||||
day.status === "work"
|
||||
? "border-green-400/50 bg-gradient-to-br from-green-500/20 to-emerald-500/20 hover:from-green-500/30 hover:to-emerald-500/30 shadow-lg shadow-green-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "weekend"
|
||||
? "border-gray-400/50 bg-gradient-to-br from-gray-500/20 to-slate-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "vacation"
|
||||
? "border-blue-400/50 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 shadow-lg shadow-blue-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "sick"
|
||||
? "border-orange-400/50 bg-gradient-to-br from-orange-500/20 to-yellow-500/20 shadow-lg shadow-orange-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "absent"
|
||||
? "border-red-400/50 bg-gradient-to-br from-red-500/20 to-rose-500/20 shadow-lg shadow-red-500/20"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Эффект свечения */}
|
||||
<div className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-br from-white/10 to-transparent"></div>
|
||||
|
||||
<div className="relative h-full flex flex-col justify-between z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white font-bold text-base">
|
||||
{day.day}
|
||||
</span>
|
||||
{day.workType && (
|
||||
<div className="text-white/80">
|
||||
{getWorkTypeIcon(day.workType)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{day.status === "work" && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{day.hours}ч
|
||||
</span>
|
||||
{day.overtime > 0 && (
|
||||
<Badge className="bg-yellow-500/30 text-yellow-200 text-xs border-yellow-400/30">
|
||||
+{day.overtime}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{getMoodIcon(day.mood)}
|
||||
{day.efficiency && (
|
||||
<div className="text-right">
|
||||
<div className="text-white/80 text-xs font-medium">
|
||||
{day.efficiency}%
|
||||
</div>
|
||||
<div className="w-8 h-1 bg-white/20 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 transition-all duration-1000"
|
||||
style={{
|
||||
width: animatedStats
|
||||
? `${day.efficiency}%`
|
||||
: "0%",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{day.status !== "work" && day.status !== "weekend" && (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full ${getStatusColor(
|
||||
day.status
|
||||
)} animate-pulse shadow-lg`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* SVG градиенты для круговых диаграмм */}
|
||||
<svg width="0" height="0" style={{ position: "absolute" }}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient-purple"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#8B5CF6" />
|
||||
<stop offset="100%" stopColor="#6366F1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-green"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#10B981" />
|
||||
<stop offset="100%" stopColor="#059669" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-blue"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#3B82F6" />
|
||||
<stop offset="100%" stopColor="#06B6D4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-orange"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#F59E0B" />
|
||||
<stop offset="100%" stopColor="#F97316" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-yellow"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#EAB308" />
|
||||
<stop offset="100%" stopColor="#F59E0B" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-pink"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#EC4899" />
|
||||
<stop offset="100%" stopColor="#F43F5E" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// Компактный вариант для 13-дюймовых экранов
|
||||
const renderCompactVariant = () => (
|
||||
<Card
|
||||
className="glass-card border-white/10 overflow-hidden relative"
|
||||
style={{ height: "600px" }}
|
||||
>
|
||||
{/* Космический фон с плавающими частицами и звездным полем (из Галактического) */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-blue-900/20 to-indigo-900/20">
|
||||
<div
|
||||
className="absolute inset-0 opacity-50"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="7" cy="7" r="1"/%3E%3Ccircle cx="27" cy="27" r="1"/%3E%3Ccircle cx="47" cy="47" r="1"/%3E%3Ccircle cx="17" cy="37" r="1"/%3E%3Ccircle cx="37" cy="17" r="1"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\')',
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Плавающие частицы */}
|
||||
<div className="absolute top-10 left-10 w-2 h-2 bg-purple-400/30 rounded-full animate-pulse"></div>
|
||||
<div className="absolute top-20 right-20 w-1 h-1 bg-blue-400/40 rounded-full animate-pulse delay-1000"></div>
|
||||
<div className="absolute bottom-20 left-20 w-1.5 h-1.5 bg-cyan-400/30 rounded-full animate-pulse delay-2000"></div>
|
||||
<div className="absolute bottom-32 right-32 w-1 h-1 bg-pink-400/40 rounded-full animate-pulse delay-3000"></div>
|
||||
|
||||
{/* Звездное поле */}
|
||||
<div className="absolute inset-0">
|
||||
{Array.from({ length: 15 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-0.5 h-0.5 bg-white/40 rounded-full animate-pulse"
|
||||
style={{
|
||||
top: `${Math.random() * 100}%`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 3}s`,
|
||||
animationDuration: `${2 + Math.random() * 2}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-4 relative z-10 p-4 h-full overflow-y-auto">
|
||||
{/* Компактный заголовок сотрудника */}
|
||||
<div className="bg-white/5 rounded-xl p-4 backdrop-blur-sm border border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-12 w-12 ring-2 ring-purple-500/30">
|
||||
<AvatarImage src={currentEmployee.avatar} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-600 to-blue-600 text-white text-sm font-bold">
|
||||
{currentEmployee.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-white mb-1">
|
||||
{currentEmployee.name}
|
||||
</h3>
|
||||
<p className="text-purple-300 text-sm mb-1">
|
||||
{currentEmployee.position}
|
||||
</p>
|
||||
<div className="flex items-center space-x-3 text-xs text-white/70">
|
||||
<span>{currentEmployee.department}</span>
|
||||
<span>•</span>
|
||||
<span>{currentEmployee.level}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная навигация */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={selectedEmployee}
|
||||
onValueChange={setSelectedEmployee}
|
||||
>
|
||||
<SelectTrigger className="w-32 bg-white/10 border-white/30 text-white text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{employees.map((emp) => (
|
||||
<SelectItem key={emp.id} value={emp.id}>
|
||||
{emp.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white/70 hover:text-white hover:bg-white/10 h-8 w-8 p-0"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white/70 hover:text-white hover:bg-white/10 h-8 w-8 p-0"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная статистика в одну строку */}
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-purple-500/30 hover:border-purple-400/50 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-purple)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.totalHours / 200) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Clock className="h-4 w-4 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? stats.totalHours : 0}
|
||||
</div>
|
||||
<p className="text-purple-300 text-xs">Часов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-green-500 to-emerald-500 rounded-xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-green-500/30 hover:border-green-400/50 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-green)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.workDays / 25) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? stats.workDays : 0}
|
||||
</div>
|
||||
<p className="text-green-300 text-xs">Рабочих</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-blue-500/30 hover:border-blue-400/50 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-blue)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.vacation / 5) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Plane className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? stats.vacation : 0}
|
||||
</div>
|
||||
<p className="text-blue-300 text-xs">Отпуск</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-orange-500 to-red-500 rounded-xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-orange-500/30 hover:border-orange-400/50 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-orange)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.sick / 3) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Heart className="h-4 w-4 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? stats.sick : 0}
|
||||
</div>
|
||||
<p className="text-orange-300 text-xs">Больничный</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 rounded-xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-yellow-500/30 hover:border-yellow-400/50 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-yellow)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (stats.overtime / 20) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Zap className="h-4 w-4 text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? stats.overtime : 0}
|
||||
</div>
|
||||
<p className="text-yellow-300 text-xs">Переработка</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-pink-500 to-purple-500 rounded-xl opacity-20 group-hover:opacity-30 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-pink-500/30 hover:border-pink-400/50 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-pink)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? stats.avgEfficiency : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Activity className="h-4 w-4 text-pink-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? stats.avgEfficiency : 0}%
|
||||
</div>
|
||||
<p className="text-pink-300 text-xs">КПД</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная календарная сетка */}
|
||||
<div className="space-y-3">
|
||||
{/* Заголовки дней недели */}
|
||||
<div className="grid grid-cols-7 gap-2 text-center text-xs text-white/60 font-medium">
|
||||
<div>ПН</div>
|
||||
<div>ВТ</div>
|
||||
<div>СР</div>
|
||||
<div>ЧТ</div>
|
||||
<div>ПТ</div>
|
||||
<div>СБ</div>
|
||||
<div>ВС</div>
|
||||
</div>
|
||||
|
||||
{/* Календарная сетка */}
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{calendarData.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative group cursor-pointer transition-all duration-300 ${
|
||||
day.status === "work"
|
||||
? "bg-gradient-to-br from-emerald-500/20 to-green-500/20 border-emerald-500/30 hover:border-emerald-400/50"
|
||||
: day.status === "weekend"
|
||||
? "bg-gradient-to-br from-slate-500/20 to-gray-500/20 border-slate-500/30 hover:border-slate-400/50"
|
||||
: day.status === "vacation"
|
||||
? "bg-gradient-to-br from-blue-500/20 to-cyan-500/20 border-blue-500/30 hover:border-blue-400/50"
|
||||
: day.status === "sick"
|
||||
? "bg-gradient-to-br from-amber-500/20 to-orange-500/20 border-amber-500/30 hover:border-amber-400/50"
|
||||
: "bg-gradient-to-br from-red-500/20 to-rose-500/20 border-red-500/30 hover:border-red-400/50"
|
||||
} rounded-xl border backdrop-blur-sm p-2 h-16`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span className="text-white font-medium text-sm mb-1">
|
||||
{day.day}
|
||||
</span>
|
||||
|
||||
{day.status === "work" && (
|
||||
<div className="flex items-center space-x-1 text-xs">
|
||||
<span className="text-white/80">{day.hours}ч</span>
|
||||
{day.overtime > 0 && (
|
||||
<span className="text-yellow-400">+{day.overtime}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{day.status !== "work" && day.status !== "weekend" && (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
day.status === "vacation"
|
||||
? "bg-gradient-to-r from-blue-500 to-cyan-500"
|
||||
: day.status === "sick"
|
||||
? "bg-gradient-to-r from-amber-500 to-orange-500"
|
||||
: "bg-gradient-to-r from-red-500 to-rose-500"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная легенда */}
|
||||
<div className="flex flex-wrap gap-2 text-xs justify-center">
|
||||
<div className="flex items-center gap-1 bg-white/5 px-2 py-1 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-emerald-500 to-green-500"></div>
|
||||
<span className="text-white/70">Работа</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/5 px-2 py-1 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-slate-500 to-gray-500"></div>
|
||||
<span className="text-white/70">Выходной</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/5 px-2 py-1 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500"></div>
|
||||
<span className="text-white/70">Отпуск</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/5 px-2 py-1 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-amber-500 to-orange-500"></div>
|
||||
<span className="text-white/70">Больничный</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/5 px-2 py-1 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-red-500 to-rose-500"></div>
|
||||
<span className="text-white/70">Прогул</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* SVG градиенты */}
|
||||
<svg width="0" height="0">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient-purple"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#8B5CF6" />
|
||||
<stop offset="100%" stopColor="#6366F1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-green"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#10B981" />
|
||||
<stop offset="100%" stopColor="#059669" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-blue"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#3B82F6" />
|
||||
<stop offset="100%" stopColor="#06B6D4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-orange"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#F59E0B" />
|
||||
<stop offset="100%" stopColor="#F97316" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-yellow"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#EAB308" />
|
||||
<stop offset="100%" stopColor="#F59E0B" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-pink"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#EC4899" />
|
||||
<stop offset="100%" stopColor="#F43F5E" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// Интерактивный вариант с яркими цветами и кликабельными датами
|
||||
const renderInteractiveVariant = () => (
|
||||
<Card
|
||||
className="glass-card border-white/10 overflow-hidden relative"
|
||||
style={{ height: "600px" }}
|
||||
>
|
||||
{/* Космический фон с плавающими частицами и звездным полем */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-800/30 via-blue-800/30 to-indigo-800/30">
|
||||
<div
|
||||
className="absolute inset-0 opacity-60"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.08"%3E%3Ccircle cx="7" cy="7" r="1"/%3E%3Ccircle cx="27" cy="27" r="1"/%3E%3Ccircle cx="47" cy="47" r="1"/%3E%3Ccircle cx="17" cy="37" r="1"/%3E%3Ccircle cx="37" cy="17" r="1"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\')',
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Более яркие плавающие частицы */}
|
||||
<div className="absolute top-10 left-10 w-3 h-3 bg-purple-400/50 rounded-full animate-pulse"></div>
|
||||
<div className="absolute top-20 right-20 w-2 h-2 bg-blue-400/60 rounded-full animate-pulse delay-1000"></div>
|
||||
<div className="absolute bottom-20 left-20 w-2.5 h-2.5 bg-cyan-400/50 rounded-full animate-pulse delay-2000"></div>
|
||||
<div className="absolute bottom-32 right-32 w-2 h-2 bg-pink-400/60 rounded-full animate-pulse delay-3000"></div>
|
||||
|
||||
{/* Более яркое звездное поле */}
|
||||
<div className="absolute inset-0">
|
||||
{Array.from({ length: 20 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-1 h-1 bg-white/60 rounded-full animate-pulse"
|
||||
style={{
|
||||
top: `${Math.random() * 100}%`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 3}s`,
|
||||
animationDuration: `${2 + Math.random() * 2}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-4 relative z-10 p-4 h-full overflow-y-auto">
|
||||
{/* Компактный заголовок сотрудника с яркими цветами */}
|
||||
<div className="bg-white/10 rounded-xl p-4 backdrop-blur-sm border border-white/20 shadow-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-12 w-12 ring-3 ring-purple-400/50">
|
||||
<AvatarImage src={currentEmployee.avatar} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-blue-500 text-white text-sm font-bold">
|
||||
{currentEmployee.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-white mb-1">
|
||||
{currentEmployee.name}
|
||||
</h3>
|
||||
<p className="text-purple-200 text-sm mb-1">
|
||||
{currentEmployee.position}
|
||||
</p>
|
||||
<div className="flex items-center space-x-3 text-xs text-white/80">
|
||||
<span>{currentEmployee.department}</span>
|
||||
<span>•</span>
|
||||
<span>{currentEmployee.level}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Компактная навигация с яркими цветами */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={selectedEmployee}
|
||||
onValueChange={setSelectedEmployee}
|
||||
>
|
||||
<SelectTrigger className="w-32 bg-white/15 border-white/40 text-white text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{employees.map((emp) => (
|
||||
<SelectItem key={emp.id} value={emp.id}>
|
||||
{emp.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white/80 hover:text-white hover:bg-white/20 h-8 w-8 p-0"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white/80 hover:text-white hover:bg-white/20 h-8 w-8 p-0"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Яркая статистика в одну строку */}
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-400 to-indigo-400 rounded-xl opacity-30 group-hover:opacity-40 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-purple-400/50 hover:border-purple-300/70 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-purple-bright)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats
|
||||
? (interactiveStats.totalHours / 200) * 100
|
||||
: 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Clock className="h-4 w-4 text-purple-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? interactiveStats.totalHours : 0}
|
||||
</div>
|
||||
<p className="text-purple-200 text-xs">Часов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-green-400 to-emerald-400 rounded-xl opacity-30 group-hover:opacity-40 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-green-400/50 hover:border-green-300/70 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-green-bright)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (interactiveStats.workDays / 25) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<CheckCircle className="h-4 w-4 text-green-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? interactiveStats.workDays : 0}
|
||||
</div>
|
||||
<p className="text-green-200 text-xs">Рабочих</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-400 to-cyan-400 rounded-xl opacity-30 group-hover:opacity-40 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-blue-400/50 hover:border-blue-300/70 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-blue-bright)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (interactiveStats.vacation / 5) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Plane className="h-4 w-4 text-blue-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? interactiveStats.vacation : 0}
|
||||
</div>
|
||||
<p className="text-blue-200 text-xs">Отпуск</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-orange-400 to-red-400 rounded-xl opacity-30 group-hover:opacity-40 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-orange-400/50 hover:border-orange-300/70 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-orange-bright)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (interactiveStats.sick / 3) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Heart className="h-4 w-4 text-orange-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? interactiveStats.sick : 0}
|
||||
</div>
|
||||
<p className="text-orange-200 text-xs">Больничный</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-xl opacity-30 group-hover:opacity-40 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-yellow-400/50 hover:border-yellow-300/70 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-yellow-bright)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? (interactiveStats.overtime / 20) * 100 : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Zap className="h-4 w-4 text-yellow-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? interactiveStats.overtime : 0}
|
||||
</div>
|
||||
<p className="text-yellow-200 text-xs">Переработка</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-pink-400 to-purple-400 rounded-xl opacity-30 group-hover:opacity-40 transition-opacity blur-sm"></div>
|
||||
<div className="relative glass-card p-3 rounded-xl border border-pink-400/50 hover:border-pink-300/70 transition-all duration-300 text-center">
|
||||
<div className="relative w-10 h-10 mx-auto mb-2">
|
||||
<svg
|
||||
className="w-10 h-10 transform -rotate-90"
|
||||
viewBox="0 0 36 36"
|
||||
>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="url(#gradient-pink-bright)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={`${
|
||||
animatedStats ? interactiveStats.avgEfficiency : 0
|
||||
}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Activity className="h-4 w-4 text-pink-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg mb-1">
|
||||
{animatedStats ? interactiveStats.avgEfficiency : 0}%
|
||||
</div>
|
||||
<p className="text-pink-200 text-xs">КПД</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Интерактивная календарная сетка с яркими цветами */}
|
||||
<div className="space-y-3">
|
||||
{/* Заголовки дней недели */}
|
||||
<div className="grid grid-cols-7 gap-2 text-center text-xs text-white/80 font-medium">
|
||||
<div>ПН</div>
|
||||
<div>ВТ</div>
|
||||
<div>СР</div>
|
||||
<div>ЧТ</div>
|
||||
<div>ПТ</div>
|
||||
<div>СБ</div>
|
||||
<div>ВС</div>
|
||||
</div>
|
||||
|
||||
{/* Интерактивная календарная сетка */}
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{editableCalendarData.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => toggleDayStatus(index)}
|
||||
className={`relative group cursor-pointer transition-all duration-300 transform hover:scale-105 ${
|
||||
day.status === "work"
|
||||
? "bg-gradient-to-br from-emerald-400/30 to-green-400/30 border-emerald-400/50 hover:border-emerald-300/70 shadow-lg shadow-emerald-500/20"
|
||||
: day.status === "weekend"
|
||||
? "bg-gradient-to-br from-slate-400/30 to-gray-400/30 border-slate-400/50 hover:border-slate-300/70 shadow-lg shadow-slate-500/20"
|
||||
: day.status === "vacation"
|
||||
? "bg-gradient-to-br from-blue-400/30 to-cyan-400/30 border-blue-400/50 hover:border-blue-300/70 shadow-lg shadow-blue-500/20"
|
||||
: day.status === "sick"
|
||||
? "bg-gradient-to-br from-amber-400/30 to-orange-400/30 border-amber-400/50 hover:border-amber-300/70 shadow-lg shadow-amber-500/20"
|
||||
: "bg-gradient-to-br from-red-400/30 to-rose-400/30 border-red-400/50 hover:border-red-300/70 shadow-lg shadow-red-500/20"
|
||||
} rounded-xl border backdrop-blur-sm p-2 h-16`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span className="text-white font-medium text-sm mb-1">
|
||||
{day.day}
|
||||
</span>
|
||||
|
||||
{day.status === "work" && (
|
||||
<div className="flex items-center space-x-1 text-xs">
|
||||
<span className="text-white/90">{day.hours}ч</span>
|
||||
{day.overtime > 0 && (
|
||||
<span className="text-yellow-300">+{day.overtime}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{day.status !== "work" && day.status !== "weekend" && (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
day.status === "vacation"
|
||||
? "bg-gradient-to-r from-blue-400 to-cyan-400"
|
||||
: day.status === "sick"
|
||||
? "bg-gradient-to-r from-amber-400 to-orange-400"
|
||||
: "bg-gradient-to-r from-red-400 to-rose-400"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Индикатор интерактивности */}
|
||||
<div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-1.5 h-1.5 bg-white/60 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Яркая легенда с подсказкой */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2 text-xs justify-center">
|
||||
<div className="flex items-center gap-1 bg-white/10 px-2 py-1 rounded-lg border border-emerald-400/30">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-emerald-400 to-green-400"></div>
|
||||
<span className="text-white/80">Работа</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/10 px-2 py-1 rounded-lg border border-slate-400/30">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-slate-400 to-gray-400"></div>
|
||||
<span className="text-white/80">Выходной</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/10 px-2 py-1 rounded-lg border border-blue-400/30">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-blue-400 to-cyan-400"></div>
|
||||
<span className="text-white/80">Отпуск</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/10 px-2 py-1 rounded-lg border border-amber-400/30">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-amber-400 to-orange-400"></div>
|
||||
<span className="text-white/80">Больничный</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-white/10 px-2 py-1 rounded-lg border border-red-400/30">
|
||||
<div className="w-2 h-2 rounded-full bg-gradient-to-r from-red-400 to-rose-400"></div>
|
||||
<span className="text-white/80">Прогул</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-xs text-white/60">
|
||||
💡 Кликните на дату, чтобы изменить статус
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Яркие SVG градиенты */}
|
||||
<svg width="0" height="0">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient-purple-bright"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#A855F7" />
|
||||
<stop offset="100%" stopColor="#7C3AED" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-green-bright"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#34D399" />
|
||||
<stop offset="100%" stopColor="#10B981" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-blue-bright"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#60A5FA" />
|
||||
<stop offset="100%" stopColor="#22D3EE" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-orange-bright"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#FB923C" />
|
||||
<stop offset="100%" stopColor="#F87171" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-yellow-bright"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#FBBF24" />
|
||||
<stop offset="100%" stopColor="#FB923C" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gradient-pink-bright"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#F472B6" />
|
||||
<stop offset="100%" stopColor="#F87171" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// Интерактивный вариант для нескольких сотрудников с яркими цветами
|
||||
const renderMultiEmployeeInteractiveVariant = () => {
|
||||
|
||||
const daysInMonth = new Date(selectedYear, selectedMonth + 1, 0).getDate();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Заголовок */}
|
||||
<Card className="glass-card border-white/10 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/30 via-pink-900/30 to-cyan-900/30">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-purple-600/20 via-pink-600/10 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<CardHeader className="relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-cyan-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-2">
|
||||
Универсальный табель учета рабочего времени
|
||||
</h2>
|
||||
<p className="text-white/70 text-lg">
|
||||
{monthNames[selectedMonth]} {selectedYear} •{" "}
|
||||
{employeesList.length} сотрудников
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 0) {
|
||||
setSelectedMonth(11);
|
||||
setSelectedYear(selectedYear - 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth - 1);
|
||||
}
|
||||
}}
|
||||
className="text-white hover:bg-white/10 rounded-xl border border-cyan-400/30 hover:border-cyan-400/50"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<div className="text-white font-bold text-xl min-w-[180px] text-center bg-gradient-to-r from-cyan-400 to-pink-400 bg-clip-text text-transparent">
|
||||
{monthNames[selectedMonth]} {selectedYear}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (selectedMonth === 11) {
|
||||
setSelectedMonth(0);
|
||||
setSelectedYear(selectedYear + 1);
|
||||
} else {
|
||||
setSelectedMonth(selectedMonth + 1);
|
||||
}
|
||||
}}
|
||||
className="text-white hover:bg-white/10 rounded-xl border border-pink-400/30 hover:border-pink-400/50"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
className="text-white hover:bg-white/10 rounded-xl border border-green-400/30 hover:border-green-400/50"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Добавить сотрудника
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-white hover:bg-white/10 rounded-xl border border-purple-400/30 hover:border-purple-400/50"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Экспорт
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Форма добавления сотрудника */}
|
||||
{showAddForm && (
|
||||
<div className="mt-6 p-4 bg-white/5 rounded-xl border border-white/10">
|
||||
<h3 className="text-white font-semibold mb-4">
|
||||
Добавить нового сотрудника
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Имя Фамилия"
|
||||
value={newEmployee.name}
|
||||
onChange={(e) =>
|
||||
setNewEmployee({ ...newEmployee, name: e.target.value })
|
||||
}
|
||||
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-cyan-400/50"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Должность"
|
||||
value={newEmployee.position}
|
||||
onChange={(e) =>
|
||||
setNewEmployee({
|
||||
...newEmployee,
|
||||
position: e.target.value,
|
||||
})
|
||||
}
|
||||
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-cyan-400/50"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Отдел"
|
||||
value={newEmployee.department}
|
||||
onChange={(e) =>
|
||||
setNewEmployee({
|
||||
...newEmployee,
|
||||
department: e.target.value,
|
||||
})
|
||||
}
|
||||
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-cyan-400/50"
|
||||
/>
|
||||
<select
|
||||
value={newEmployee.level}
|
||||
onChange={(e) =>
|
||||
setNewEmployee({ ...newEmployee, level: e.target.value })
|
||||
}
|
||||
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-cyan-400/50"
|
||||
>
|
||||
<option value="Junior" className="bg-gray-900">
|
||||
Junior
|
||||
</option>
|
||||
<option value="Middle" className="bg-gray-900">
|
||||
Middle
|
||||
</option>
|
||||
<option value="Senior" className="bg-gray-900">
|
||||
Senior
|
||||
</option>
|
||||
<option value="Lead" className="bg-gray-900">
|
||||
Lead
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="text-white/70 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleAddEmployee}
|
||||
className="text-white hover:bg-green-500/20 border border-green-400/30"
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Основной табель */}
|
||||
<Card className="glass-card border-white/10 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-blue-900/20 to-indigo-900/20">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-purple-600/10 via-pink-600/5 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<CardContent className="relative z-10 p-6">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
{/* Заголовок таблицы */}
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left p-3 text-white font-semibold border-b border-white/10 sticky left-0 bg-gray-900/80 backdrop-blur min-w-[200px]">
|
||||
Сотрудник
|
||||
</th>
|
||||
{Array.from({ length: daysInMonth }, (_, i) => {
|
||||
const date = new Date(selectedYear, selectedMonth, i + 1);
|
||||
const dayOfWeek = date.getDay();
|
||||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||||
const workingCount = getWorkingEmployeesCount(i);
|
||||
|
||||
return (
|
||||
<th
|
||||
key={i + 1}
|
||||
className={`text-center p-2 text-sm border-b border-white/10 min-w-[60px] ${
|
||||
isWeekend ? "bg-gray-500/20" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="text-white/70 text-xs">
|
||||
{dayNames[dayOfWeek === 0 ? 6 : dayOfWeek - 1]}
|
||||
</div>
|
||||
<div className="text-white font-bold text-lg">
|
||||
{i + 1}
|
||||
</div>
|
||||
{workingCount > 0 && (
|
||||
<div className="text-green-400 text-xs font-semibold mt-1">
|
||||
{workingCount} чел.
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="text-center p-3 text-white font-semibold border-b border-white/10 min-w-[100px]">
|
||||
Итого
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Строки сотрудников */}
|
||||
<tbody>
|
||||
{employeesList.map((employee, employeeIndex) => {
|
||||
const employeeData = allEmployeesData[employee.id] || [];
|
||||
const totalHours = employeeData.reduce(
|
||||
(sum, day) => sum + day.hours,
|
||||
0
|
||||
);
|
||||
const workDays = employeeData.filter(
|
||||
(day) => day.status === "work"
|
||||
).length;
|
||||
const colorGradient = getEmployeeColor(employeeIndex);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={employee.id}
|
||||
className="hover:bg-white/5 transition-colors"
|
||||
>
|
||||
{/* Информация о сотруднике */}
|
||||
<td className="p-3 border-b border-white/5 sticky left-0 bg-gray-900/80 backdrop-blur">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar
|
||||
className={`h-10 w-10 ring-2 ring-offset-2 ring-offset-gray-900`}
|
||||
style={{
|
||||
borderColor: `rgb(${
|
||||
employeeIndex * 50 + 100
|
||||
}, ${200 - employeeIndex * 30}, ${
|
||||
150 + employeeIndex * 40
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
<AvatarImage src={employee.avatar} />
|
||||
<AvatarFallback
|
||||
className={`bg-gradient-to-br ${colorGradient} text-white text-sm font-bold`}
|
||||
>
|
||||
{employee.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-white font-medium text-sm">
|
||||
{employee.name}
|
||||
</div>
|
||||
<div className="text-white/60 text-xs">
|
||||
{employee.position}
|
||||
</div>
|
||||
<div className="text-white/40 text-xs">
|
||||
{employee.department}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveEmployee(employee.id)}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10 p-1 h-6 w-6"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Дни месяца */}
|
||||
{employeeData.map((day, dayIndex) => {
|
||||
const date = new Date(
|
||||
selectedYear,
|
||||
selectedMonth,
|
||||
day.day
|
||||
);
|
||||
const isWeekend =
|
||||
date.getDay() === 0 || date.getDay() === 6;
|
||||
|
||||
return (
|
||||
<td
|
||||
key={dayIndex}
|
||||
className={`p-1 border-b border-white/5 text-center ${
|
||||
isWeekend ? "bg-gray-500/10" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
w-12 h-12 mx-auto rounded-lg flex flex-col items-center justify-center text-xs font-semibold transition-all duration-300 hover:scale-110 cursor-pointer
|
||||
${
|
||||
day.status === "work"
|
||||
? "bg-gradient-to-br from-green-500/40 to-emerald-500/40 border border-green-400/50 text-white shadow-lg shadow-green-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "weekend"
|
||||
? "bg-gradient-to-br from-gray-500/30 to-slate-500/30 border border-gray-400/40 text-white/70"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "vacation"
|
||||
? "bg-gradient-to-br from-blue-500/40 to-cyan-500/40 border border-blue-400/50 text-white shadow-lg shadow-blue-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "sick"
|
||||
? "bg-gradient-to-br from-orange-500/40 to-red-500/40 border border-orange-400/50 text-white shadow-lg shadow-orange-500/20"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
day.status === "absent"
|
||||
? "bg-gradient-to-br from-red-500/40 to-rose-500/40 border border-red-400/50 text-white shadow-lg shadow-red-500/20"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
{day.status === "work" && (
|
||||
<>
|
||||
<span className="text-xs font-bold">
|
||||
{day.hours}ч
|
||||
</span>
|
||||
{day.overtime > 0 && (
|
||||
<span className="text-yellow-300 text-xs">
|
||||
+{day.overtime}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{day.status === "weekend" && (
|
||||
<span className="text-xs">Вых</span>
|
||||
)}
|
||||
{day.status === "vacation" && (
|
||||
<span className="text-xs">Отп</span>
|
||||
)}
|
||||
{day.status === "sick" && (
|
||||
<span className="text-xs">Б/Л</span>
|
||||
)}
|
||||
{day.status === "absent" && (
|
||||
<span className="text-xs">Пр</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Итого */}
|
||||
<td className="p-3 border-b border-white/5 text-center">
|
||||
<div className="text-white font-bold text-lg">
|
||||
{totalHours}ч
|
||||
</div>
|
||||
<div className="text-white/60 text-xs">
|
||||
{workDays} дней
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
{/* Итоговая строка */}
|
||||
<tfoot>
|
||||
<tr className="bg-white/5">
|
||||
<td className="p-3 text-white font-semibold border-t border-white/10 sticky left-0 bg-gray-800/80 backdrop-blur">
|
||||
Итого по дням:
|
||||
</td>
|
||||
{Array.from({ length: daysInMonth }, (_, dayIndex) => {
|
||||
const workingCount = getWorkingEmployeesCount(dayIndex);
|
||||
const totalHours = employeesList.reduce((sum, emp) => {
|
||||
const dayData = getDayStatus(emp.id, dayIndex);
|
||||
return sum + (dayData?.hours || 0);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<td
|
||||
key={dayIndex}
|
||||
className="p-2 text-center border-t border-white/10"
|
||||
>
|
||||
{workingCount > 0 && (
|
||||
<div className="text-white font-bold text-sm">
|
||||
{totalHours}ч
|
||||
</div>
|
||||
)}
|
||||
{workingCount > 0 && (
|
||||
<div className="text-green-400 text-xs">
|
||||
{workingCount} чел
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="p-3 text-center border-t border-white/10">
|
||||
<div className="text-white font-bold text-lg">
|
||||
{employeesList.reduce((sum, emp) => {
|
||||
const empData = allEmployeesData[emp.id] || [];
|
||||
return (
|
||||
sum +
|
||||
empData.reduce(
|
||||
(daySum, day) => daySum + day.hours,
|
||||
0
|
||||
)
|
||||
);
|
||||
}, 0)}
|
||||
ч
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Легенда */}
|
||||
<Card className="glass-card border-white/10">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-pink-900/20 to-cyan-900/20">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-purple-600/10 via-pink-600/5 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<CardContent className="relative z-10 p-6">
|
||||
<h4 className="text-white font-bold text-xl mb-6 text-center bg-gradient-to-r from-cyan-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Легенда статусов
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-6">
|
||||
<div className="flex items-center space-x-3 bg-white/5 p-4 rounded-xl border border-green-400/30">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-green-500/40 to-emerald-500/40 border border-green-400/50 flex items-center justify-center shadow-lg shadow-green-500/20">
|
||||
<span className="text-white text-xs font-bold">8ч</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white font-bold text-sm">Работа</span>
|
||||
<p className="text-green-300 text-xs">Рабочий день</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 bg-white/5 p-4 rounded-xl border border-gray-400/30">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-gray-500/30 to-slate-500/30 border border-gray-400/40 flex items-center justify-center">
|
||||
<span className="text-white/70 text-xs font-bold">Вых</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white font-bold text-sm">Выходной</span>
|
||||
<p className="text-gray-300 text-xs">Суббота/Воскресенье</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 bg-white/5 p-4 rounded-xl border border-blue-400/30">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500/40 to-cyan-500/40 border border-blue-400/50 flex items-center justify-center shadow-lg shadow-blue-500/20">
|
||||
<span className="text-white text-xs font-bold">Отп</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white font-bold text-sm">Отпуск</span>
|
||||
<p className="text-blue-300 text-xs">Оплачиваемый отпуск</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 bg-white/5 p-4 rounded-xl border border-orange-400/30">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-orange-500/40 to-red-500/40 border border-orange-400/50 flex items-center justify-center shadow-lg shadow-orange-500/20">
|
||||
<span className="text-white text-xs font-bold">Б/Л</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white font-bold text-sm">
|
||||
Больничный
|
||||
</span>
|
||||
<p className="text-orange-300 text-xs">По болезни</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 bg-white/5 p-4 rounded-xl border border-red-400/30">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-red-500/40 to-rose-500/40 border border-red-400/50 flex items-center justify-center shadow-lg shadow-red-500/20">
|
||||
<span className="text-white text-xs font-bold">Пр</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white font-bold text-sm">Прогул</span>
|
||||
<p className="text-red-300 text-xs">Неявка</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-white/60 text-sm">
|
||||
<p>
|
||||
💡 В заголовках дней показано количество работающих сотрудников
|
||||
</p>
|
||||
<p>
|
||||
📊 В итоговой строке показаны общие часы и количество
|
||||
сотрудников по дням
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Селектор вариантов */}
|
||||
<Card className="glass-card border-white/10">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-white">
|
||||
Табель учета рабочего времени
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Select
|
||||
value={selectedVariant}
|
||||
onValueChange={(
|
||||
value:
|
||||
| "galaxy"
|
||||
| "cosmic"
|
||||
| "custom"
|
||||
| "compact"
|
||||
| "interactive"
|
||||
| "multi-employee"
|
||||
) => setSelectedVariant(value)}
|
||||
>
|
||||
<SelectTrigger className="w-64 glass-input bg-white/10 border-white/20 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
|
||||
<SelectItem
|
||||
value="galaxy"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
Галактический стиль
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="cosmic"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
Космический стиль
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="custom"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
Кастомный стиль
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="compact"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
Компактный вид
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="interactive"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
Интерактивный режим
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="multi-employee"
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
Универсальный (несколько сотрудников)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Отображение выбранного варианта */}
|
||||
{selectedVariant === "galaxy" && renderGalaxyVariant()}
|
||||
{selectedVariant === "cosmic" && renderCosmicVariant()}
|
||||
{selectedVariant === "custom" && renderCustomVariant()}
|
||||
{selectedVariant === "compact" && renderCompactVariant()}
|
||||
{selectedVariant === "interactive" && renderInteractiveVariant()}
|
||||
{selectedVariant === "multi-employee" &&
|
||||
renderMultiEmployeeInteractiveVariant()}
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user