Karmashek commited on
Commit
fc1eb7c
·
verified ·
1 Parent(s): 638bc39

Upload 128 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +6 -0
  3. .replit +36 -0
  4. client/index.html +16 -0
  5. client/src/App.tsx +126 -0
  6. client/src/components/auth/login-form.tsx +134 -0
  7. client/src/components/auth/protected-route.tsx +41 -0
  8. client/src/components/auth/register-form.tsx +166 -0
  9. client/src/components/auth/role-guard.tsx +136 -0
  10. client/src/components/dashboard/campaign-performance.tsx +67 -0
  11. client/src/components/dashboard/metric-card.tsx +74 -0
  12. client/src/components/dashboard/role-access-control.tsx +111 -0
  13. client/src/components/dashboard/team-activity.tsx +122 -0
  14. client/src/components/dashboard/team-member-performance.tsx +167 -0
  15. client/src/components/dashboard/team-performance-chart.tsx +62 -0
  16. client/src/components/equipment-form.tsx +254 -0
  17. client/src/components/layout/header.tsx +89 -0
  18. client/src/components/layout/mobile-sidebar.tsx +151 -0
  19. client/src/components/layout/notifications.tsx +295 -0
  20. client/src/components/layout/sidebar.tsx +236 -0
  21. client/src/components/layout/user-status.tsx +163 -0
  22. client/src/components/providers.tsx +50 -0
  23. client/src/components/ui/accordion.tsx +56 -0
  24. client/src/components/ui/alert-dialog.tsx +139 -0
  25. client/src/components/ui/alert.tsx +59 -0
  26. client/src/components/ui/aspect-ratio.tsx +5 -0
  27. client/src/components/ui/avatar.tsx +50 -0
  28. client/src/components/ui/badge.tsx +36 -0
  29. client/src/components/ui/breadcrumb.tsx +115 -0
  30. client/src/components/ui/button.tsx +56 -0
  31. client/src/components/ui/calendar.tsx +68 -0
  32. client/src/components/ui/card.tsx +79 -0
  33. client/src/components/ui/carousel.tsx +260 -0
  34. client/src/components/ui/chart.tsx +365 -0
  35. client/src/components/ui/checkbox.tsx +28 -0
  36. client/src/components/ui/collapsible.tsx +11 -0
  37. client/src/components/ui/command.tsx +151 -0
  38. client/src/components/ui/context-menu.tsx +198 -0
  39. client/src/components/ui/data-table.tsx +69 -0
  40. client/src/components/ui/dialog.tsx +122 -0
  41. client/src/components/ui/drawer.tsx +118 -0
  42. client/src/components/ui/dropdown-menu.tsx +198 -0
  43. client/src/components/ui/form.tsx +178 -0
  44. client/src/components/ui/hover-card.tsx +29 -0
  45. client/src/components/ui/input-otp.tsx +69 -0
  46. client/src/components/ui/input.tsx +22 -0
  47. client/src/components/ui/label.tsx +24 -0
  48. client/src/components/ui/menubar.tsx +256 -0
  49. client/src/components/ui/navigation-menu.tsx +128 -0
  50. client/src/components/ui/pagination.tsx +117 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ generated-icon.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ server/public
5
+ vite.config.ts.*
6
+ *.tar.gz
.replit ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modules = ["nodejs-20", "web", "postgresql-16"]
2
+ run = "npm run dev"
3
+ hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
4
+
5
+ [nix]
6
+ channel = "stable-24_05"
7
+
8
+ [deployment]
9
+ deploymentTarget = "autoscale"
10
+ build = ["npm", "run", "build"]
11
+ run = ["npm", "run", "start"]
12
+
13
+ [[ports]]
14
+ localPort = 5000
15
+ externalPort = 80
16
+
17
+ [workflows]
18
+ runButton = "Project"
19
+
20
+ [[workflows.workflow]]
21
+ name = "Project"
22
+ mode = "parallel"
23
+ author = "agent"
24
+
25
+ [[workflows.workflow.tasks]]
26
+ task = "workflow.run"
27
+ args = "Start application"
28
+
29
+ [[workflows.workflow]]
30
+ name = "Start application"
31
+ author = "agent"
32
+
33
+ [[workflows.workflow.tasks]]
34
+ task = "shell.exec"
35
+ args = "npm run dev"
36
+ waitForPort = 5000
client/index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
6
+ <title>MarketMetrics - Team Performance Dashboard</title>
7
+ <meta name="description" content="A comprehensive dashboard for monitoring marketing team performance metrics with role-based access controls." />
8
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjM2NmYxIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imx1Y2lkZSBsdWNpZGUtYmFyLWNoYXJ0LTIiPjxwYXRoIGQ9IE0xOCAyMFYxME0xMiAyMFYzTTYgMjBWMTQiLz48L3N2Zz4=" />
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ <!-- This is a replit script which adds a banner on the top of the page when opened in development mode outside the replit environment -->
14
+ <script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js"></script>
15
+ </body>
16
+ </html>
client/src/App.tsx ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Switch, Route } from "wouter";
2
+ import { queryClient } from "./lib/queryClient";
3
+ import { QueryClientProvider } from "@tanstack/react-query";
4
+ import { Toaster } from "@/components/ui/toaster";
5
+ import { TooltipProvider } from "@/components/ui/tooltip";
6
+ import NotFound from "@/pages/not-found";
7
+ import Login from "@/pages/login";
8
+ import Register from "@/pages/register";
9
+ import Dashboard from "@/pages/dashboard";
10
+ import Schedule from "@/pages/schedule";
11
+ import Equipment from "@/pages/equipment";
12
+ import Users from "@/pages/users";
13
+ import Maintenance from "@/pages/maintenance";
14
+ import Reports from "@/pages/reports";
15
+ import DailyInspection from "@/pages/daily-inspection-new";
16
+ import Remarks from "@/pages/remarks";
17
+ import Profile from "@/pages/profile";
18
+ import Tasks from "@/pages/tasks";
19
+ import { MobileSidebarProvider } from "@/hooks/use-mobile-sidebar";
20
+ import { AuthProvider } from "@/hooks/use-auth";
21
+ import { UserStatusProvider } from "@/hooks/use-user-status";
22
+ import { EquipmentProvider } from "@/hooks/use-equipment-data";
23
+ import { RemarksProvider } from "@/hooks/use-remarks-data";
24
+ import { MaintenanceProvider } from "@/hooks/use-maintenance-data";
25
+ import { InspectionChecklistProvider } from "@/hooks/use-inspection-checklists";
26
+ import { SidebarProvider } from "@/hooks/use-sidebar-state";
27
+ import { ProtectedRoute } from "@/components/auth/protected-route";
28
+
29
+ function Router() {
30
+ return (
31
+ <Switch>
32
+ <Route path="/login" component={Login} />
33
+ <Route path="/register" component={Register} />
34
+ <Route path="/dashboard">
35
+ <ProtectedRoute>
36
+ <Dashboard />
37
+ </ProtectedRoute>
38
+ </Route>
39
+ <Route path="/schedule">
40
+ <ProtectedRoute>
41
+ <Schedule />
42
+ </ProtectedRoute>
43
+ </Route>
44
+ <Route path="/equipment">
45
+ <ProtectedRoute>
46
+ <Equipment />
47
+ </ProtectedRoute>
48
+ </Route>
49
+ <Route path="/daily-inspection">
50
+ <ProtectedRoute>
51
+ <DailyInspection />
52
+ </ProtectedRoute>
53
+ </Route>
54
+ <Route path="/daily-inspection-new">
55
+ <ProtectedRoute>
56
+ <DailyInspection />
57
+ </ProtectedRoute>
58
+ </Route>
59
+ <Route path="/users">
60
+ <ProtectedRoute>
61
+ <Users />
62
+ </ProtectedRoute>
63
+ </Route>
64
+ <Route path="/maintenance">
65
+ <ProtectedRoute>
66
+ <Maintenance />
67
+ </ProtectedRoute>
68
+ </Route>
69
+ <Route path="/tasks">
70
+ <ProtectedRoute>
71
+ <Tasks />
72
+ </ProtectedRoute>
73
+ </Route>
74
+ <Route path="/remarks">
75
+ <ProtectedRoute>
76
+ <Remarks />
77
+ </ProtectedRoute>
78
+ </Route>
79
+ <Route path="/reports">
80
+ <ProtectedRoute>
81
+ <Reports />
82
+ </ProtectedRoute>
83
+ </Route>
84
+ <Route path="/profile">
85
+ <ProtectedRoute>
86
+ <Profile />
87
+ </ProtectedRoute>
88
+ </Route>
89
+ <Route path="/">
90
+ <ProtectedRoute>
91
+ <Dashboard />
92
+ </ProtectedRoute>
93
+ </Route>
94
+ <Route component={NotFound} />
95
+ </Switch>
96
+ );
97
+ }
98
+
99
+ function App() {
100
+ return (
101
+ <QueryClientProvider client={queryClient}>
102
+ <TooltipProvider>
103
+ <AuthProvider>
104
+ <SidebarProvider>
105
+ <EquipmentProvider>
106
+ <RemarksProvider>
107
+ <MaintenanceProvider>
108
+ <InspectionChecklistProvider>
109
+ <UserStatusProvider>
110
+ <MobileSidebarProvider>
111
+ <Toaster />
112
+ <Router />
113
+ </MobileSidebarProvider>
114
+ </UserStatusProvider>
115
+ </InspectionChecklistProvider>
116
+ </MaintenanceProvider>
117
+ </RemarksProvider>
118
+ </EquipmentProvider>
119
+ </SidebarProvider>
120
+ </AuthProvider>
121
+ </TooltipProvider>
122
+ </QueryClientProvider>
123
+ );
124
+ }
125
+
126
+ export default App;
client/src/components/auth/login-form.tsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { z } from "zod";
5
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { login } from "@/lib/auth";
10
+ import { useToast } from "@/hooks/use-toast";
11
+ import { useAuth } from "@/hooks/use-auth";
12
+ import { Link, useLocation } from "wouter";
13
+ import { Eye, EyeOff, LogIn } from "lucide-react";
14
+
15
+ const loginSchema = z.object({
16
+ email: z.string().email("Пожалуйста, введите корректный email"),
17
+ password: z.string().min(6, "Пароль должен содержать минимум 6 символов"),
18
+ });
19
+
20
+ type LoginFormValues = z.infer<typeof loginSchema>;
21
+
22
+ export function LoginForm() {
23
+ const [isLoading, setIsLoading] = useState(false);
24
+ const [showPassword, setShowPassword] = useState(false);
25
+ const { toast } = useToast();
26
+ const { refreshAuth } = useAuth();
27
+ const [_, setLocation] = useLocation();
28
+
29
+ const form = useForm<LoginFormValues>({
30
+ resolver: zodResolver(loginSchema),
31
+ defaultValues: {
32
+ email: "",
33
+ password: "",
34
+ },
35
+ });
36
+
37
+ const onSubmit = async (data: LoginFormValues) => {
38
+ setIsLoading(true);
39
+ try {
40
+ await login(data.email, data.password);
41
+ // Принудительно обновляем состояние аутентификации
42
+ refreshAuth();
43
+ toast({
44
+ title: "Вход выполнен",
45
+ description: "Вы успешно вошли в систему",
46
+ });
47
+ // Небольшая задержка для обновления состояния
48
+ setTimeout(() => {
49
+ setLocation("/dashboard");
50
+ }, 100);
51
+ } catch (error: any) {
52
+ console.error("Ошибка входа:", error);
53
+ toast({
54
+ title: "Ошибка входа",
55
+ description: error.message || "Пожалуйста, проверьте ваши данные и попробуйте снова",
56
+ variant: "destructive",
57
+ });
58
+ } finally {
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+
63
+ return (
64
+ <Card className="w-full max-w-md mx-auto">
65
+ <CardHeader>
66
+ <CardTitle className="text-2xl">Вход в систему</CardTitle>
67
+ <CardDescription>
68
+ Введите ваши данные для доступа к аккаунту
69
+ </CardDescription>
70
+ </CardHeader>
71
+ <CardContent>
72
+ <Form {...form}>
73
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
74
+ <FormField
75
+ control={form.control}
76
+ name="email"
77
+ render={({ field }) => (
78
+ <FormItem>
79
+ <FormLabel>Email</FormLabel>
80
+ <FormControl>
81
+ <Input placeholder="email@example.com" {...field} />
82
+ </FormControl>
83
+ <FormMessage />
84
+ </FormItem>
85
+ )}
86
+ />
87
+
88
+ <FormField
89
+ control={form.control}
90
+ name="password"
91
+ render={({ field }) => (
92
+ <FormItem>
93
+ <FormLabel>Пароль</FormLabel>
94
+ <FormControl>
95
+ <div className="relative">
96
+ <Input
97
+ type={showPassword ? "text" : "password"}
98
+ placeholder="••••••••"
99
+ {...field}
100
+ />
101
+ <Button
102
+ type="button"
103
+ variant="ghost"
104
+ size="sm"
105
+ className="absolute right-0 top-0 h-full px-3"
106
+ onClick={() => setShowPassword(!showPassword)}
107
+ >
108
+ {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
109
+ </Button>
110
+ </div>
111
+ </FormControl>
112
+ <FormMessage />
113
+ </FormItem>
114
+ )}
115
+ />
116
+
117
+ <Button type="submit" className="w-full mt-4" disabled={isLoading}>
118
+ {isLoading ? "Выполняется вход..." : "Войти"}
119
+ {!isLoading && <LogIn className="ml-2 h-4 w-4" />}
120
+ </Button>
121
+ </form>
122
+ </Form>
123
+ </CardContent>
124
+ <CardFooter className="flex justify-center">
125
+ <p className="text-sm text-gray-500">
126
+ Нет аккаунта?{" "}
127
+ <Link href="/register" className="text-primary-600 hover:underline">
128
+ Зарегистрироваться
129
+ </Link>
130
+ </p>
131
+ </CardFooter>
132
+ </Card>
133
+ );
134
+ }
client/src/components/auth/protected-route.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useAuth } from "@/hooks/use-auth";
2
+ import { useLocation } from "wouter";
3
+ import { useEffect } from "react";
4
+ import { getToken } from "@/lib/auth";
5
+
6
+ interface ProtectedRouteProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ export function ProtectedRoute({ children }: ProtectedRouteProps) {
11
+ const { user, isLoading } = useAuth();
12
+ const [, setLocation] = useLocation();
13
+ const token = getToken();
14
+
15
+ useEffect(() => {
16
+ // Простая проверка - есть токен или нет
17
+ if (!token) {
18
+ setLocation("/login");
19
+ }
20
+ }, [token, setLocation]);
21
+
22
+ // Если нет токена, перенаправляем
23
+ if (!token) {
24
+ return null;
25
+ }
26
+
27
+ // Показываем загрузку только при первоначальной загрузке пользователя
28
+ if (isLoading && !user) {
29
+ return (
30
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
31
+ <div className="text-center">
32
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
33
+ <p className="text-gray-600 dark:text-gray-400">Загрузка...</p>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ // Рендерим контент если есть токен
40
+ return <>{children}</>;
41
+ }
client/src/components/auth/register-form.tsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { z } from "zod";
5
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { register } from "@/lib/auth";
10
+ import { useToast } from "@/hooks/use-toast";
11
+ import { Link, useLocation } from "wouter";
12
+ import { Eye, EyeOff, UserPlus } from "lucide-react";
13
+
14
+ const registerSchema = z.object({
15
+ name: z.string().min(2, "Имя должно содержать минимум 2 символа"),
16
+ email: z.string().email("Пожалуйста, введите корректный email"),
17
+ password: z.string().min(6, "Пароль должен содержать минимум 6 символов"),
18
+ confirmPassword: z.string(),
19
+ }).refine(data => data.password === data.confirmPassword, {
20
+ message: "Пароли не совпадают",
21
+ path: ["confirmPassword"],
22
+ });
23
+
24
+ type RegisterFormValues = z.infer<typeof registerSchema>;
25
+
26
+ export function RegisterForm() {
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [showPassword, setShowPassword] = useState(false);
29
+ const { toast } = useToast();
30
+ const [_, setLocation] = useLocation();
31
+
32
+ const form = useForm<RegisterFormValues>({
33
+ resolver: zodResolver(registerSchema),
34
+ defaultValues: {
35
+ name: "",
36
+ email: "",
37
+ password: "",
38
+ confirmPassword: "",
39
+ },
40
+ });
41
+
42
+ const onSubmit = async (data: RegisterFormValues) => {
43
+ setIsLoading(true);
44
+ try {
45
+ await register(data.name, data.email, data.password);
46
+ toast({
47
+ title: "Регистрация успешна",
48
+ description: "Ваш аккаунт был создан",
49
+ });
50
+ setLocation("/dashboard");
51
+ } catch (error: any) {
52
+ console.error("Ошибка регистрации:", error);
53
+ toast({
54
+ title: "Ошибка регистрации",
55
+ description: error.message || "Пожалуйста, проверьте введенные данные и попробуйте снова",
56
+ variant: "destructive",
57
+ });
58
+ } finally {
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+
63
+ return (
64
+ <Card className="w-full max-w-md mx-auto">
65
+ <CardHeader>
66
+ <CardTitle className="text-2xl">Создание аккаунта</CardTitle>
67
+ <CardDescription>
68
+ Введите ваши данные для создания аккаунта
69
+ </CardDescription>
70
+ </CardHeader>
71
+ <CardContent>
72
+ <Form {...form}>
73
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
74
+ <FormField
75
+ control={form.control}
76
+ name="name"
77
+ render={({ field }) => (
78
+ <FormItem>
79
+ <FormLabel>Имя</FormLabel>
80
+ <FormControl>
81
+ <Input placeholder="Иван Иванов" {...field} />
82
+ </FormControl>
83
+ <FormMessage />
84
+ </FormItem>
85
+ )}
86
+ />
87
+
88
+ <FormField
89
+ control={form.control}
90
+ name="email"
91
+ render={({ field }) => (
92
+ <FormItem>
93
+ <FormLabel>Email</FormLabel>
94
+ <FormControl>
95
+ <Input placeholder="email@example.com" {...field} />
96
+ </FormControl>
97
+ <FormMessage />
98
+ </FormItem>
99
+ )}
100
+ />
101
+
102
+ <FormField
103
+ control={form.control}
104
+ name="password"
105
+ render={({ field }) => (
106
+ <FormItem>
107
+ <FormLabel>Пароль</FormLabel>
108
+ <FormControl>
109
+ <div className="relative">
110
+ <Input
111
+ type={showPassword ? "text" : "password"}
112
+ placeholder="••••••••"
113
+ {...field}
114
+ />
115
+ <Button
116
+ type="button"
117
+ variant="ghost"
118
+ size="sm"
119
+ className="absolute right-0 top-0 h-full px-3"
120
+ onClick={() => setShowPassword(!showPassword)}
121
+ >
122
+ {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
123
+ </Button>
124
+ </div>
125
+ </FormControl>
126
+ <FormMessage />
127
+ </FormItem>
128
+ )}
129
+ />
130
+
131
+ <FormField
132
+ control={form.control}
133
+ name="confirmPassword"
134
+ render={({ field }) => (
135
+ <FormItem>
136
+ <FormLabel>Подтверждение пароля</FormLabel>
137
+ <FormControl>
138
+ <Input
139
+ type={showPassword ? "text" : "password"}
140
+ placeholder="••••••••"
141
+ {...field}
142
+ />
143
+ </FormControl>
144
+ <FormMessage />
145
+ </FormItem>
146
+ )}
147
+ />
148
+
149
+ <Button type="submit" className="w-full mt-4" disabled={isLoading}>
150
+ {isLoading ? "Создание аккаунта..." : "Создать аккаунт"}
151
+ {!isLoading && <UserPlus className="ml-2 h-4 w-4" />}
152
+ </Button>
153
+ </form>
154
+ </Form>
155
+ </CardContent>
156
+ <CardFooter className="flex justify-center">
157
+ <p className="text-sm text-gray-500">
158
+ Уже есть аккаунт?{" "}
159
+ <Link href="/login" className="text-primary-600 hover:underline">
160
+ Войти
161
+ </Link>
162
+ </p>
163
+ </CardFooter>
164
+ </Card>
165
+ );
166
+ }
client/src/components/auth/role-guard.tsx ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useAuth } from "@/hooks/use-auth";
3
+ import { Alert, AlertDescription } from "@/components/ui/alert";
4
+ import { Shield, Lock } from "lucide-react";
5
+
6
+ interface RoleGuardProps {
7
+ allowedRoles: string[];
8
+ children: React.ReactNode;
9
+ fallback?: React.ReactNode;
10
+ showMessage?: boolean;
11
+ }
12
+
13
+ export function RoleGuard({ allowedRoles, children, fallback, showMessage = true }: RoleGuardProps) {
14
+ const { user } = useAuth();
15
+
16
+ if (!user) {
17
+ return fallback || (showMessage ? (
18
+ <Alert>
19
+ <Lock className="h-4 w-4" />
20
+ <AlertDescription>
21
+ Необходимо войти в систему для доступа к этому разделу.
22
+ </AlertDescription>
23
+ </Alert>
24
+ ) : null);
25
+ }
26
+
27
+ if (!allowedRoles.includes(user.role)) {
28
+ return fallback || (showMessage ? (
29
+ <Alert>
30
+ <Shield className="h-4 w-4" />
31
+ <AlertDescription>
32
+ У вас недостаточно прав для доступа к этому разделу.
33
+ Текущая роль: {getRoleDisplayName(user.role)}.
34
+ Обратитесь к администратору для получения дополнительных прав.
35
+ </AlertDescription>
36
+ </Alert>
37
+ ) : null);
38
+ }
39
+
40
+ return <>{children}</>;
41
+ }
42
+
43
+ // Компонент для скрытия элементов интерфейса на основе ролей
44
+ interface RoleBasedProps {
45
+ allowedRoles: string[];
46
+ children: React.ReactNode;
47
+ }
48
+
49
+ export function RoleBased({ allowedRoles, children }: RoleBasedProps) {
50
+ const { user } = useAuth();
51
+
52
+ if (!user || !allowedRoles.includes(user.role)) {
53
+ return null;
54
+ }
55
+
56
+ return <>{children}</>;
57
+ }
58
+
59
+ // Хук для проверки прав доступа
60
+ export function usePermissions() {
61
+ const { user } = useAuth();
62
+
63
+ const hasRole = (role: string) => {
64
+ return user?.role === role;
65
+ };
66
+
67
+ const hasAnyRole = (roles: string[]) => {
68
+ return user && roles.includes(user.role);
69
+ };
70
+
71
+ const canView = () => {
72
+ return hasAnyRole(['admin', 'operator', 'engineer', 'viewer']);
73
+ };
74
+
75
+ const canEdit = () => {
76
+ return hasAnyRole(['admin', 'operator', 'engineer']);
77
+ };
78
+
79
+ const canCreate = () => {
80
+ return hasAnyRole(['admin', 'operator', 'engineer']);
81
+ };
82
+
83
+ const canDelete = () => {
84
+ return hasAnyRole(['admin', 'operator']);
85
+ };
86
+
87
+ const canManageUsers = () => {
88
+ return hasRole('admin');
89
+ };
90
+
91
+ const canManageSystem = () => {
92
+ return hasRole('admin');
93
+ };
94
+
95
+ return {
96
+ hasRole,
97
+ hasAnyRole,
98
+ canView,
99
+ canEdit,
100
+ canCreate,
101
+ canDelete,
102
+ canManageUsers,
103
+ canManageSystem,
104
+ currentRole: user?.role || 'guest'
105
+ };
106
+ }
107
+
108
+ export function getRoleDisplayName(role: string): string {
109
+ switch (role) {
110
+ case 'admin':
111
+ return 'Администратор';
112
+ case 'operator':
113
+ return 'Оператор';
114
+ case 'engineer':
115
+ return 'Инженер';
116
+ case 'viewer':
117
+ return 'Просмотр';
118
+ default:
119
+ return 'Неизвестная роль';
120
+ }
121
+ }
122
+
123
+ export function getRoleDescription(role: string): string {
124
+ switch (role) {
125
+ case 'admin':
126
+ return 'Полный доступ ко всем функциям системы';
127
+ case 'operator':
128
+ return 'Может выполнять осмотры, создавать отчеты, управлять оборудованием';
129
+ case 'engineer':
130
+ return 'Может просматривать данные, выполнять ежедневные осмотры';
131
+ case 'viewer':
132
+ return 'Только просмотр данных без возможности изменений';
133
+ default:
134
+ return 'Описание роли недоступно';
135
+ }
136
+ }
client/src/components/dashboard/campaign-performance.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2
+ import { Button } from "@/components/ui/button";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ interface Campaign {
6
+ id: number;
7
+ name: string;
8
+ progress: number;
9
+ }
10
+
11
+ export function CampaignPerformance() {
12
+ const { data: campaigns } = useQuery<Campaign[]>({
13
+ queryKey: ["/api/campaigns"],
14
+ });
15
+
16
+ const getCampaignColorClass = (progress: number) => {
17
+ if (progress >= 85) return "bg-success-500 dark:bg-success-600";
18
+ if (progress >= 60) return "bg-primary-500 dark:bg-primary-600";
19
+ return "bg-warning-500 dark:bg-warning-600";
20
+ };
21
+
22
+ return (
23
+ <Card>
24
+ <CardHeader className="p-5 border-b border-gray-200 dark:border-gray-700">
25
+ <CardTitle className="font-semibold text-gray-800 dark:text-white">Эффективность кампаний</CardTitle>
26
+ </CardHeader>
27
+ <CardContent className="p-5">
28
+ <div className="space-y-4">
29
+ {campaigns ? (
30
+ campaigns.map((campaign) => (
31
+ <div key={campaign.id}>
32
+ <div className="flex justify-between items-center mb-1">
33
+ <span className="text-sm font-medium text-gray-700 dark:text-gray-300">{campaign.name}</span>
34
+ <span className="text-sm text-gray-500 dark:text-gray-400">{campaign.progress}%</span>
35
+ </div>
36
+ <div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
37
+ <div
38
+ className={`${getCampaignColorClass(campaign.progress)} h-2 rounded-full`}
39
+ style={{ width: `${campaign.progress}%` }}
40
+ ></div>
41
+ </div>
42
+ </div>
43
+ ))
44
+ ) : (
45
+ Array(4)
46
+ .fill(0)
47
+ .map((_, i) => (
48
+ <div key={i} className="animate-pulse">
49
+ <div className="flex justify-between items-center mb-1">
50
+ <div className="h-4 bg-gray-200 rounded w-1/2 dark:bg-gray-700"></div>
51
+ <div className="h-4 bg-gray-200 rounded w-12 dark:bg-gray-700"></div>
52
+ </div>
53
+ <div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700"></div>
54
+ </div>
55
+ ))
56
+ )}
57
+ </div>
58
+
59
+ <div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
60
+ <Button variant="outline" className="w-full py-2">
61
+ Просмотреть все кампании
62
+ </Button>
63
+ </div>
64
+ </CardContent>
65
+ </Card>
66
+ );
67
+ }
client/src/components/dashboard/metric-card.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BarChart2, Briefcase, CheckCircle, Wrench, AlertTriangle, Settings } from "lucide-react";
2
+
3
+ interface MetricCardProps {
4
+ title: string;
5
+ value: string | number;
6
+ change: number;
7
+ icon: string;
8
+ }
9
+
10
+ export function MetricCard({ title, value, change, icon }: MetricCardProps) {
11
+ const isPositive = change >= 0;
12
+ const iconClasses = {
13
+ equipment: {
14
+ bg: "bg-blue-100",
15
+ text: "text-blue-600",
16
+ icon: <Wrench />,
17
+ progress: 100,
18
+ },
19
+ completed: {
20
+ bg: "bg-green-100",
21
+ text: "text-green-600",
22
+ icon: <CheckCircle />,
23
+ progress: 67,
24
+ },
25
+ overdue: {
26
+ bg: "bg-yellow-100",
27
+ text: "text-yellow-600",
28
+ icon: <AlertTriangle />,
29
+ progress: 7,
30
+ },
31
+ repairs: {
32
+ bg: "bg-red-100",
33
+ text: "text-red-600",
34
+ icon: <Settings />,
35
+ progress: 12,
36
+ },
37
+ };
38
+
39
+ const iconType = icon in iconClasses ? icon : "equipment";
40
+ const { bg, text, icon: iconComponent, progress } = iconClasses[iconType as keyof typeof iconClasses];
41
+
42
+ return (
43
+ <div className="bg-white rounded-lg shadow p-5 transition-all hover:shadow-md dark:bg-gray-800">
44
+ <div className="flex justify-between items-start">
45
+ <div>
46
+ <h3 className="text-sm text-gray-500 uppercase tracking-wider dark:text-gray-400">{title}</h3>
47
+ <div className="mt-2 flex items-baseline">
48
+ <p className="text-2xl font-semibold text-gray-900 dark:text-white">{value}</p>
49
+ <p className={`ml-2 text-sm font-medium ${isPositive ? 'text-success-500 dark:text-success-400' : 'text-error-500 dark:text-error-400'}`}>
50
+ <i className={`fas fa-arrow-${isPositive ? 'up' : 'down'} text-xs mr-0.5`}></i>
51
+ <span>{Math.abs(change)}%</span>
52
+ </p>
53
+ </div>
54
+ <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">по сравнению с прошлым месяцем</p>
55
+ </div>
56
+ <div className={`${bg} rounded-full p-3 ${text} dark:bg-opacity-20`}>
57
+ {iconComponent}
58
+ </div>
59
+ </div>
60
+ <div className="mt-4">
61
+ <div className="flex items-center justify-between">
62
+ <div className="text-xs text-gray-500 dark:text-gray-400">Прогресс</div>
63
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">{progress}%</div>
64
+ </div>
65
+ <div className="mt-1 w-full bg-gray-200 rounded-full h-1.5 dark:bg-gray-700">
66
+ <div
67
+ className="bg-primary-600 h-1.5 rounded-full dark:bg-primary-500"
68
+ style={{ width: `${progress}%` }}
69
+ ></div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
client/src/components/dashboard/role-access-control.tsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2
+ import { Button } from "@/components/ui/button";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { CheckCircle, XCircle, Edit, PlusCircle } from "lucide-react";
5
+
6
+ interface Role {
7
+ id: number;
8
+ name: string;
9
+ description: string;
10
+ permissions: string[];
11
+ }
12
+
13
+ export function RoleAccessControl() {
14
+ const { data: roles } = useQuery<Role[]>({
15
+ queryKey: ["/api/roles"],
16
+ });
17
+
18
+ // Mock roles if API data not available yet
19
+ const mockRoles = [
20
+ {
21
+ id: 1,
22
+ name: "Администратор",
23
+ description: "Полный доступ ко всем функциям",
24
+ permissions: ["dashboard", "tasks", "team", "reports", "settings"],
25
+ },
26
+ {
27
+ id: 2,
28
+ name: "Маркетинг-менеджер",
29
+ description: "Может управлять кампаниями и просматривать отчеты",
30
+ permissions: ["dashboard", "tasks", "team", "reports"],
31
+ },
32
+ {
33
+ id: 3,
34
+ name: "Контент-создатель",
35
+ description: "Создает и редактирует контент для кампаний",
36
+ permissions: ["dashboard", "tasks"],
37
+ },
38
+ ];
39
+
40
+ const displayRoles = roles || mockRoles;
41
+ const allPermissions = ["dashboard", "tasks", "team", "reports", "settings"];
42
+
43
+ return (
44
+ <Card className="mb-6">
45
+ <CardHeader className="p-5">
46
+ <div className="flex justify-between items-center">
47
+ <CardTitle className="text-lg font-semibold text-gray-800 dark:text-white">Управление доступом на основе ролей</CardTitle>
48
+ <div className="flex items-center space-x-2">
49
+ <Button variant="outline" className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1.5 rounded-md text-sm font-medium flex items-center dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-300 dark:border-gray-600">
50
+ <Edit className="w-4 h-4 mr-1.5" />
51
+ Редактировать роли
52
+ </Button>
53
+ <Button variant="outline" className="bg-primary-100 hover:bg-primary-200 text-primary-700 px-3 py-1.5 rounded-md text-sm font-medium flex items-center dark:bg-primary-900/30 dark:hover:bg-primary-900/50 dark:text-primary-300 dark:border-primary-800">
54
+ <PlusCircle className="w-4 h-4 mr-1.5" />
55
+ Добавить роль
56
+ </Button>
57
+ </div>
58
+ </div>
59
+ </CardHeader>
60
+ <CardContent className="p-0">
61
+ <div className="overflow-x-auto">
62
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
63
+ <thead>
64
+ <tr>
65
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Роль</th>
66
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Описание</th>
67
+ {allPermissions.map((permission) => (
68
+ <th key={permission} className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">
69
+ {permission === "dashboard" ? "Панель" :
70
+ permission === "tasks" ? "Задачи" :
71
+ permission === "team" ? "Команда" :
72
+ permission === "reports" ? "Отчеты" :
73
+ permission === "settings" ? "Настройки" :
74
+ permission}
75
+ </th>
76
+ ))}
77
+ <th className="px-4 py-3 bg-gray-50 dark:bg-gray-800"></th>
78
+ </tr>
79
+ </thead>
80
+ <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
81
+ {displayRoles.map((role) => (
82
+ <tr key={role.id}>
83
+ <td className="px-4 py-3">
84
+ <div className="font-medium text-gray-900 dark:text-white">{role.name}</div>
85
+ </td>
86
+ <td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">{role.description}</td>
87
+ {allPermissions.map((permission) => (
88
+ <td key={permission} className="px-4 py-3 text-sm text-center">
89
+ {role.permissions.includes(permission) ? (
90
+ <CheckCircle className="mx-auto text-success-500 dark:text-success-400 w-5 h-5" />
91
+ ) : (
92
+ <XCircle className="mx-auto text-gray-300 dark:text-gray-600 w-5 h-5" />
93
+ )}
94
+ </td>
95
+ ))}
96
+ <td className="px-4 py-3 text-right text-sm">
97
+ <Button variant="ghost" size="sm" className="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400">
98
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5">
99
+ <path d="M3 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM8.5 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM15.5 8.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z" />
100
+ </svg>
101
+ </Button>
102
+ </td>
103
+ </tr>
104
+ ))}
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ </CardContent>
109
+ </Card>
110
+ );
111
+ }
client/src/components/dashboard/team-activity.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2
+ import { Button } from "@/components/ui/button";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { formatDistanceToNow } from "date-fns";
5
+
6
+ interface TeamMember {
7
+ id: number;
8
+ name: string;
9
+ avatar: string;
10
+ }
11
+
12
+ interface Activity {
13
+ id: number;
14
+ userId: number;
15
+ action: string;
16
+ timestamp: string;
17
+ user?: TeamMember;
18
+ }
19
+
20
+ export function TeamActivity() {
21
+ const { data: activities } = useQuery<Activity[]>({
22
+ queryKey: ["/api/activities"],
23
+ });
24
+
25
+ // Generate initial for avatar display
26
+ const getInitials = (name: string) => {
27
+ return name
28
+ .split(" ")
29
+ .map((n) => n[0])
30
+ .join("")
31
+ .toUpperCase();
32
+ };
33
+
34
+ // Get background color based on initials
35
+ const getAvatarColor = (initials: string) => {
36
+ const colors = {
37
+ JD: "bg-primary-100 text-primary-600 dark:bg-primary-900 dark:text-primary-300",
38
+ MS: "bg-secondary-100 text-secondary-500 dark:bg-secondary-900 dark:text-secondary-300",
39
+ AR: "bg-yellow-100 text-yellow-600 dark:bg-yellow-900 dark:text-yellow-300",
40
+ AM: "bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-300",
41
+ };
42
+ return initials in colors
43
+ ? colors[initials as keyof typeof colors]
44
+ : "bg-primary-100 text-primary-600 dark:bg-primary-900 dark:text-primary-300";
45
+ };
46
+
47
+ // Mock data in case API isn't available yet
48
+ const mockActivities = [
49
+ {
50
+ id: 1,
51
+ user: { id: 1, name: "Елена Иванова", avatar: "ЕИ" },
52
+ action: "Завершила настройку кампании \"Летняя акция\"",
53
+ timestamp: "2023-08-30T12:30:00Z",
54
+ },
55
+ {
56
+ id: 2,
57
+ user: { id: 2, name: "Максим Петров", avatar: "МП" },
58
+ action: "Добавил 3 новые задачи в кампанию \"Квартальная рассылка\"",
59
+ timestamp: "2023-08-30T10:15:00Z",
60
+ },
61
+ {
62
+ id: 3,
63
+ user: { id: 3, name: "Анна Смирнова", avatar: "АС" },
64
+ action: "Обновила стратегию социальных медиа",
65
+ timestamp: "2023-08-29T15:45:00Z",
66
+ },
67
+ {
68
+ id: 4,
69
+ user: { id: 4, name: "Алексей Морозов", avatar: "АМ" },
70
+ action: "Создал новый отчет о производительности за 3 квартал",
71
+ timestamp: "2023-08-29T09:12:00Z",
72
+ },
73
+ {
74
+ id: 5,
75
+ user: { id: 1, name: "Елена Иванова", avatar: "ЕИ" },
76
+ action: "Завершила 12 задач в кампании \"Запуск продукта\"",
77
+ timestamp: "2023-08-28T14:20:00Z",
78
+ },
79
+ ];
80
+
81
+ const displayActivities = activities || mockActivities;
82
+
83
+ return (
84
+ <Card>
85
+ <CardHeader className="p-5 border-b border-gray-200 flex justify-between items-center dark:border-gray-700">
86
+ <CardTitle className="font-semibold text-gray-800 dark:text-white">Активность команды</CardTitle>
87
+ <Button variant="ghost" size="sm" className="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400">
88
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5">
89
+ <path d="M3 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM8.5 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM15.5 8.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z" />
90
+ </svg>
91
+ </Button>
92
+ </CardHeader>
93
+ <div className="p-2 max-h-96 overflow-y-auto">
94
+ <ul className="divide-y divide-gray-200 dark:divide-gray-700">
95
+ {displayActivities.map((activity) => (
96
+ <li key={activity.id} className="p-3 hover:bg-gray-50 dark:hover:bg-gray-800/50">
97
+ <div className="flex space-x-3">
98
+ <div className="flex-shrink-0">
99
+ <div className={`w-9 h-9 rounded-full ${getAvatarColor(activity.user?.avatar || getInitials(activity.user?.name || ""))} flex items-center justify-center font-medium`}>
100
+ {activity.user?.avatar || getInitials(activity.user?.name || "")}
101
+ </div>
102
+ </div>
103
+ <div className="min-w-0 flex-1">
104
+ <div className="text-sm font-medium text-gray-900 dark:text-white">{activity.user?.name}</div>
105
+ <div className="text-sm text-gray-500 dark:text-gray-400">{activity.action}</div>
106
+ <div className="mt-1 text-xs text-gray-400 dark:text-gray-500">
107
+ {formatDistanceToNow(new Date(activity.timestamp), { addSuffix: true })}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </li>
112
+ ))}
113
+ </ul>
114
+ <div className="p-3 text-center">
115
+ <Button variant="link" className="text-sm text-primary-600 font-medium hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
116
+ Просмотреть всю активность
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ </Card>
121
+ );
122
+ }
client/src/components/dashboard/team-member-performance.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2
+ import { Button } from "@/components/ui/button";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { Link } from "wouter";
5
+ import { Download, Filter } from "lucide-react";
6
+
7
+ interface TeamMember {
8
+ id: number;
9
+ name: string;
10
+ email: string;
11
+ role: string;
12
+ avatar: string;
13
+ metrics: {
14
+ tasksCompleted: number;
15
+ tasksTotal: number;
16
+ onTimeRate: number;
17
+ productivityScore: number;
18
+ };
19
+ }
20
+
21
+ export function TeamMemberPerformance() {
22
+ const { data: teamMembers } = useQuery<TeamMember[]>({
23
+ queryKey: ["/api/users"],
24
+ });
25
+
26
+ const getRatingLabel = (score: number) => {
27
+ if (score >= 85) return { label: "Высокий", className: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" };
28
+ if (score >= 70) return { label: "Средний", className: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" };
29
+ return { label: "Низкий", className: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" };
30
+ };
31
+
32
+ const getProgressColor = (score: number) => {
33
+ if (score >= 85) return "bg-success-500 dark:bg-success-600";
34
+ if (score >= 70) return "bg-yellow-500 dark:bg-yellow-600";
35
+ return "bg-red-500 dark:bg-red-600";
36
+ };
37
+
38
+ // Use mock data if API data not available yet
39
+ const mockTeamMembers = [
40
+ {
41
+ id: 1,
42
+ name: "Jane Doe",
43
+ email: "jane.doe@example.com",
44
+ role: "Marketing Manager",
45
+ avatar: "JD",
46
+ metrics: {
47
+ tasksCompleted: 24,
48
+ tasksTotal: 30,
49
+ onTimeRate: 92,
50
+ productivityScore: 90,
51
+ },
52
+ },
53
+ {
54
+ id: 2,
55
+ name: "Mike Smith",
56
+ email: "mike.smith@example.com",
57
+ role: "Content Creator",
58
+ avatar: "MS",
59
+ metrics: {
60
+ tasksCompleted: 18,
61
+ tasksTotal: 25,
62
+ onTimeRate: 78,
63
+ productivityScore: 72,
64
+ },
65
+ },
66
+ {
67
+ id: 3,
68
+ name: "Anna Roberts",
69
+ email: "anna.roberts@example.com",
70
+ role: "Social Media Specialist",
71
+ avatar: "AR",
72
+ metrics: {
73
+ tasksCompleted: 32,
74
+ tasksTotal: 35,
75
+ onTimeRate: 94,
76
+ productivityScore: 95,
77
+ },
78
+ },
79
+ ];
80
+
81
+ const displayTeamMembers = teamMembers || mockTeamMembers;
82
+
83
+ return (
84
+ <Card className="col-span-1 lg:col-span-2">
85
+ <CardHeader className="p-5 border-b border-gray-200 flex justify-between items-center dark:border-gray-700">
86
+ <CardTitle className="font-semibold text-gray-800 dark:text-white">Эффективность сотрудников</CardTitle>
87
+ <div className="flex items-center space-x-2">
88
+ <Button variant="ghost" size="icon" className="text-gray-500 hover:text-gray-700 p-1 dark:text-gray-400 dark:hover:text-gray-300">
89
+ <Download className="w-5 h-5" />
90
+ </Button>
91
+ <Button variant="ghost" size="icon" className="text-gray-500 hover:text-gray-700 p-1 dark:text-gray-400 dark:hover:text-gray-300">
92
+ <Filter className="w-5 h-5" />
93
+ </Button>
94
+ </div>
95
+ </CardHeader>
96
+ <div className="overflow-x-auto">
97
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
98
+ <thead>
99
+ <tr>
100
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Сотрудник</th>
101
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Роль</th>
102
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Выполнено задач</th>
103
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Своевременность</th>
104
+ <th className="px-4 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400">Продуктивность</th>
105
+ <th className="px-4 py-3 bg-gray-50 dark:bg-gray-800"></th>
106
+ </tr>
107
+ </thead>
108
+ <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
109
+ {displayTeamMembers.map((member) => {
110
+ const rating = getRatingLabel(member.metrics.onTimeRate);
111
+ const progressColor = getProgressColor(member.metrics.productivityScore);
112
+
113
+ return (
114
+ <tr key={member.id}>
115
+ <td className="px-4 py-3 whitespace-nowrap">
116
+ <div className="flex items-center">
117
+ <div className="flex-shrink-0 h-8 w-8 rounded-full bg-primary-100 text-primary-600 flex items-center justify-center font-medium dark:bg-primary-900 dark:text-primary-300">
118
+ {member.avatar}
119
+ </div>
120
+ <div className="ml-3">
121
+ <div className="text-sm font-medium text-gray-900 dark:text-white">{member.name}</div>
122
+ <div className="text-xs text-gray-500 dark:text-gray-400">{member.email}</div>
123
+ </div>
124
+ </div>
125
+ </td>
126
+ <td className="px-4 py-3 whitespace-nowrap">
127
+ <div className="text-sm text-gray-900 dark:text-white">{member.role}</div>
128
+ </td>
129
+ <td className="px-4 py-3 whitespace-nowrap">
130
+ <div className="text-sm text-gray-900 dark:text-white">{member.metrics.tasksCompleted}/{member.metrics.tasksTotal}</div>
131
+ </td>
132
+ <td className="px-4 py-3 whitespace-nowrap">
133
+ <div className="flex items-center">
134
+ <div className="text-sm text-gray-900 mr-2 dark:text-white">{member.metrics.onTimeRate}%</div>
135
+ <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${rating.className}`}>
136
+ {rating.label}
137
+ </span>
138
+ </div>
139
+ </td>
140
+ <td className="px-4 py-3 whitespace-nowrap">
141
+ <div className="w-24 bg-gray-200 rounded-full h-1.5 dark:bg-gray-700">
142
+ <div
143
+ className={`${progressColor} h-1.5 rounded-full`}
144
+ style={{ width: `${member.metrics.productivityScore}%` }}
145
+ ></div>
146
+ </div>
147
+ <div className="text-xs mt-1 text-gray-500 dark:text-gray-400">{member.metrics.productivityScore}/100</div>
148
+ </td>
149
+ <td className="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
150
+ <Link href={`/team/${member.id}`}>
151
+ <a className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300">View</a>
152
+ </Link>
153
+ </td>
154
+ </tr>
155
+ );
156
+ })}
157
+ </tbody>
158
+ </table>
159
+ </div>
160
+ <div className="p-3 text-center border-t border-gray-200 dark:border-gray-700">
161
+ <Button variant="link" className="text-sm text-primary-600 font-medium hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
162
+ Просмотреть всех сотрудников
163
+ </Button>
164
+ </div>
165
+ </Card>
166
+ );
167
+ }
client/src/components/dashboard/team-performance-chart.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2
+ import { Button } from "@/components/ui/button";
3
+
4
+ export function TeamPerformanceChart() {
5
+ const months = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн"];
6
+ const heights = [40, 65, 50, 75, 60, 80];
7
+ const colors = [
8
+ "bg-primary-100 dark:bg-primary-900/30",
9
+ "bg-primary-200 dark:bg-primary-800/40",
10
+ "bg-primary-300 dark:bg-primary-700/50",
11
+ "bg-primary-400 dark:bg-primary-600/60",
12
+ "bg-primary-500 dark:bg-primary-500/70",
13
+ "bg-primary-600 dark:bg-primary-400/80",
14
+ ];
15
+
16
+ return (
17
+ <Card className="col-span-1 lg:col-span-2">
18
+ <CardHeader className="p-5 border-b border-gray-200 flex justify-between items-center dark:border-gray-700">
19
+ <CardTitle className="font-semibold text-gray-800 dark:text-white">Динамика эффективности команды</CardTitle>
20
+ <div className="flex space-x-2">
21
+ <Button
22
+ variant="outline"
23
+ size="sm"
24
+ className="px-3 py-1 text-xs font-medium rounded-md bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600"
25
+ >
26
+ Неделя
27
+ </Button>
28
+ <Button
29
+ size="sm"
30
+ className="px-3 py-1 text-xs font-medium rounded-md bg-primary-100 text-primary-700 dark:bg-primary-900/50 dark:text-primary-300"
31
+ >
32
+ Месяц
33
+ </Button>
34
+ <Button
35
+ variant="outline"
36
+ size="sm"
37
+ className="px-3 py-1 text-xs font-medium rounded-md bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600"
38
+ >
39
+ Квартал
40
+ </Button>
41
+ </div>
42
+ </CardHeader>
43
+ <CardContent className="p-5">
44
+ <div className="flex items-end h-64 space-x-6 px-2">
45
+ {months.map((month, index) => (
46
+ <div key={month} className="flex flex-col items-center space-y-2 flex-1">
47
+ <div
48
+ className={`w-full ${colors[index]} rounded-t animate-in fade-in-50 duration-500`}
49
+ style={{ height: `${heights[index]}%` }}
50
+ ></div>
51
+ <span className="text-xs text-gray-500 dark:text-gray-400">{month}</span>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ <div className="flex justify-between items-center mt-6 text-sm text-gray-500 dark:text-gray-400">
56
+ <div>Среднее: 75%</div>
57
+ <div>Цель: 90%</div>
58
+ </div>
59
+ </CardContent>
60
+ </Card>
61
+ );
62
+ }
client/src/components/equipment-form.tsx ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from "react";
2
+ import { Label } from "@/components/ui/label";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Checkbox } from "@/components/ui/checkbox";
7
+
8
+ interface EquipmentFormProps {
9
+ initialData?: any;
10
+ onSave: (data: any) => void;
11
+ onCancel: () => void;
12
+ isEdit?: boolean;
13
+ }
14
+
15
+ const equipmentTypes = [
16
+ "Фрезерный станок",
17
+ "Токарный станок",
18
+ "Шлифовальный станок",
19
+ "Сверлильный станок",
20
+ "Гравировальный станок",
21
+ "Плазменная резка",
22
+ "Лазерная резка",
23
+ "Гибочный станок",
24
+ "Прессовое оборудование",
25
+ "Сварочное оборудование",
26
+ "Другое"
27
+ ];
28
+
29
+ const responsibleOptions = [
30
+ "Иванов И.И.",
31
+ "Петров П.П.",
32
+ "Сидоров С.С.",
33
+ "Калюжный Никита"
34
+ ];
35
+
36
+ export default function EquipmentForm({ initialData, onSave, onCancel, isEdit = false }: EquipmentFormProps) {
37
+ const [formData, setFormData] = useState({
38
+ id: "",
39
+ name: "",
40
+ type: "",
41
+ description: "",
42
+ status: "active",
43
+ lastMaintenance: "",
44
+ nextMaintenance: "",
45
+ responsible: "",
46
+ maintenancePeriods: [] as string[]
47
+ });
48
+
49
+ const [customType, setCustomType] = useState("");
50
+ const [isCustomType, setIsCustomType] = useState(false);
51
+
52
+ useEffect(() => {
53
+ if (initialData) {
54
+ setFormData({
55
+ id: initialData.id || "",
56
+ name: initialData.name || "",
57
+ type: initialData.type || "",
58
+ description: initialData.description || "",
59
+ status: initialData.status || "active",
60
+ lastMaintenance: initialData.lastMaintenance || "",
61
+ nextMaintenance: initialData.nextMaintenance || "",
62
+ responsible: initialData.responsible || "",
63
+ maintenancePeriods: initialData.maintenancePeriods || []
64
+ });
65
+
66
+ const isCustom = !equipmentTypes.includes(initialData.type || "");
67
+ setIsCustomType(isCustom);
68
+ if (isCustom) {
69
+ setCustomType(initialData.type || "");
70
+ }
71
+ }
72
+ }, [initialData]);
73
+
74
+ const handleFieldChange = useCallback((field: string, value: string) => {
75
+ setFormData(prev => ({
76
+ ...prev,
77
+ [field]: value
78
+ }));
79
+ }, []);
80
+
81
+ const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
82
+ setFormData(prev => ({ ...prev, name: e.target.value }));
83
+ }, []);
84
+
85
+ const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
86
+ setFormData(prev => ({ ...prev, description: e.target.value }));
87
+ }, []);
88
+
89
+ const handleCustomTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
90
+ const value = e.target.value;
91
+ setCustomType(value);
92
+ setFormData(prev => ({ ...prev, type: value }));
93
+ }, []);
94
+
95
+ const handleMaintenancePeriodChange = useCallback((period: string, checked: boolean) => {
96
+ setFormData(prev => ({
97
+ ...prev,
98
+ maintenancePeriods: checked
99
+ ? [...prev.maintenancePeriods, period]
100
+ : prev.maintenancePeriods.filter(p => p !== period)
101
+ }));
102
+ }, []);
103
+
104
+ const handleSave = () => {
105
+ onSave(formData);
106
+ };
107
+
108
+ const maintenancePeriods = ["1М - ТО", "3М - ТО", "6М - ТО", "1Г - ТО"];
109
+
110
+ return (
111
+ <div className="space-y-4">
112
+ <div className="grid grid-cols-2 gap-4">
113
+ <div>
114
+ <Label htmlFor="name">Название оборудования *</Label>
115
+ <Input
116
+ id="name"
117
+ value={formData.name}
118
+ onChange={handleNameChange}
119
+ placeholder="Введите название"
120
+ />
121
+ </div>
122
+ <div>
123
+ <Label htmlFor="type">Тип оборудования *</Label>
124
+ <div className="space-y-2">
125
+ <div className="flex items-center space-x-2">
126
+ <input
127
+ type="checkbox"
128
+ id="customTypeCheck"
129
+ checked={isCustomType}
130
+ onChange={(e) => {
131
+ setIsCustomType(e.target.checked);
132
+ if (!e.target.checked) {
133
+ setCustomType("");
134
+ handleFieldChange('type', '');
135
+ }
136
+ }}
137
+ className="h-4 w-4 text-blue-600 border-gray-300 rounded"
138
+ />
139
+ <Label htmlFor="customTypeCheck" className="text-sm">Ввести тип вручную</Label>
140
+ </div>
141
+
142
+ {isCustomType ? (
143
+ <Input
144
+ placeholder="Введите тип оборудования"
145
+ value={customType}
146
+ onChange={handleCustomTypeChange}
147
+ />
148
+ ) : (
149
+ <Select value={formData.type} onValueChange={(value) => handleFieldChange('type', value)}>
150
+ <SelectTrigger>
151
+ <SelectValue placeholder="Выберите тип" />
152
+ </SelectTrigger>
153
+ <SelectContent>
154
+ {equipmentTypes.map((type) => (
155
+ <SelectItem key={type} value={type}>{type}</SelectItem>
156
+ ))}
157
+ </SelectContent>
158
+ </Select>
159
+ )}
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <div>
165
+ <Label htmlFor="description">Описание оборудования</Label>
166
+ <textarea
167
+ id="description"
168
+ className="w-full min-h-[80px] px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
169
+ value={formData.description}
170
+ onChange={handleDescriptionChange}
171
+ placeholder="Введите описание оборудования (опционально)"
172
+ />
173
+ </div>
174
+
175
+ <div className="grid grid-cols-2 gap-4">
176
+ <div>
177
+ <Label htmlFor="status">Статус</Label>
178
+ <Select value={formData.status} onValueChange={(value) => handleFieldChange('status', value)}>
179
+ <SelectTrigger>
180
+ <SelectValue />
181
+ </SelectTrigger>
182
+ <SelectContent>
183
+ <SelectItem value="active">Активно</SelectItem>
184
+ <SelectItem value="maintenance">ТО</SelectItem>
185
+ <SelectItem value="inactive">Неактивно</SelectItem>
186
+ <SelectItem value="decommissioned">Выведено из эксплуатации</SelectItem>
187
+ </SelectContent>
188
+ </Select>
189
+ </div>
190
+ <div>
191
+ <Label htmlFor="responsible">Ответственный *</Label>
192
+ <Select value={formData.responsible} onValueChange={(value) => handleFieldChange('responsible', value)}>
193
+ <SelectTrigger>
194
+ <SelectValue placeholder="Выберите ответственного" />
195
+ </SelectTrigger>
196
+ <SelectContent>
197
+ {responsibleOptions.map((person) => (
198
+ <SelectItem key={person} value={person}>{person}</SelectItem>
199
+ ))}
200
+ </SelectContent>
201
+ </Select>
202
+ </div>
203
+ </div>
204
+
205
+ <div className="grid grid-cols-2 gap-4">
206
+ <div>
207
+ <Label htmlFor="lastMaintenance">Последнее ТО</Label>
208
+ <Input
209
+ id="lastMaintenance"
210
+ type="date"
211
+ value={formData.lastMaintenance}
212
+ onChange={(e) => handleFieldChange('lastMaintenance', e.target.value)}
213
+ />
214
+ </div>
215
+ <div>
216
+ <Label htmlFor="nextMaintenance">Следующее ТО</Label>
217
+ <Input
218
+ id="nextMaintenance"
219
+ type="date"
220
+ value={formData.nextMaintenance}
221
+ onChange={(e) => handleFieldChange('nextMaintenance', e.target.value)}
222
+ />
223
+ </div>
224
+ </div>
225
+
226
+ <div>
227
+ <Label>Периодичность ТО</Label>
228
+ <div className="grid grid-cols-2 gap-2 mt-2">
229
+ {maintenancePeriods.map((period) => (
230
+ <div key={period} className="flex items-center space-x-2">
231
+ <Checkbox
232
+ id={`period-${period}`}
233
+ checked={formData.maintenancePeriods.includes(period)}
234
+ onCheckedChange={(checked) => handleMaintenancePeriodChange(period, !!checked)}
235
+ />
236
+ <Label htmlFor={`period-${period}`} className="text-sm">
237
+ {period}
238
+ </Label>
239
+ </div>
240
+ ))}
241
+ </div>
242
+ </div>
243
+
244
+ <div className="flex justify-end space-x-2 pt-4">
245
+ <Button variant="outline" onClick={onCancel}>
246
+ Отмена
247
+ </Button>
248
+ <Button onClick={handleSave}>
249
+ {isEdit ? 'Сохранить изменения' : 'Добавить оборудование'}
250
+ </Button>
251
+ </div>
252
+ </div>
253
+ );
254
+ }
client/src/components/layout/header.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Link } from "wouter";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
6
+ import { useAuth } from "@/hooks/use-auth";
7
+ import { useMobileSidebar } from "@/hooks/use-mobile-sidebar";
8
+ import { Menu, User, LogOut } from "lucide-react";
9
+ import { NotificationsDropdown } from "./notifications";
10
+
11
+ export function Header() {
12
+ const { user, logout } = useAuth();
13
+ const { setOpen } = useMobileSidebar();
14
+
15
+ const handleLogout = async () => {
16
+ await logout();
17
+ };
18
+
19
+ return (
20
+ <header className="bg-gray-900 border-b border-gray-700 shadow-sm">
21
+ <div className="flex items-center justify-between p-4">
22
+ {/* Mobile menu button */}
23
+ <Button
24
+ variant="ghost"
25
+ size="icon"
26
+ className="lg:hidden text-gray-300 hover:text-white focus:outline-none"
27
+ onClick={() => setOpen(true)}
28
+ >
29
+ <Menu className="w-6 h-6" />
30
+ </Button>
31
+
32
+ {/* Page title (mobile only) */}
33
+ <div className="lg:hidden flex items-center">
34
+ <div className="w-8 h-8 rounded-md bg-blue-600 flex items-center justify-center">
35
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5 text-white">
36
+ <path d="M15.5 2A1.5 1.5 0 0014 3.5v13a1.5 1.5 0 001.5 1.5h1a1.5 1.5 0 001.5-1.5v-13A1.5 1.5 0 0016.5 2h-1zM9.5 6A1.5 1.5 0 008 7.5v9A1.5 1.5 0 009.5 18h1a1.5 1.5 0 001.5-1.5v-9A1.5 1.5 0 0010.5 6h-1zM3.5 10A1.5 1.5 0 002 11.5v5A1.5 1.5 0 003.5 18h1A1.5 1.5 0 006 16.5v-5A1.5 1.5 0 004.5 10h-1z" />
37
+ </svg>
38
+ </div>
39
+ <h1 className="ml-2 text-lg font-semibold text-white">StarLine</h1>
40
+ </div>
41
+
42
+ {/* Spacer to push user menu to right */}
43
+ <div className="flex-1"></div>
44
+
45
+ {/* Header actions */}
46
+ <div className="flex items-center space-x-4">
47
+ {/* Notifications */}
48
+ <NotificationsDropdown />
49
+
50
+ <div className="hidden lg:block">
51
+ <DropdownMenu>
52
+ <DropdownMenuTrigger asChild>
53
+ <Button variant="ghost" className="relative h-10 w-10 p-0 rounded-full hover:bg-gray-700">
54
+ <div className="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center font-medium">
55
+ {user?.name?.split(' ').map(n => n[0]).join('').slice(0, 2) || "AM"}
56
+ </div>
57
+ </Button>
58
+ </DropdownMenuTrigger>
59
+ <DropdownMenuContent className="w-56" align="end" forceMount>
60
+ <div className="flex items-center justify-start gap-2 p-2">
61
+ <div className="flex flex-col space-y-1 leading-none">
62
+ <p className="font-medium">{user?.name || "Алекс Морган"}</p>
63
+ <p className="w-[200px] truncate text-sm text-muted-foreground">
64
+ {user?.email || "admin@example.com"}
65
+ </p>
66
+ </div>
67
+ </div>
68
+ <DropdownMenuSeparator />
69
+ <DropdownMenuItem asChild>
70
+ <Link href="/profile">
71
+ <User className="mr-2 h-4 w-4" />
72
+ <span>Профиль</span>
73
+ </Link>
74
+ </DropdownMenuItem>
75
+ <DropdownMenuSeparator />
76
+ <DropdownMenuItem onClick={handleLogout}>
77
+ <LogOut className="mr-2 h-4 w-4" />
78
+ <span>Выйти</span>
79
+ </DropdownMenuItem>
80
+ </DropdownMenuContent>
81
+ </DropdownMenu>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+
87
+ </header>
88
+ );
89
+ }
client/src/components/layout/mobile-sidebar.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Link, useLocation } from "wouter";
3
+ import { useMobileSidebar } from "@/hooks/use-mobile-sidebar";
4
+ import { useAuth } from "@/hooks/use-auth";
5
+ import { useUserStatus } from "@/hooks/use-user-status";
6
+ import { UserStatusSelector } from "@/components/layout/user-status";
7
+ import { BarChart2, Users, ChartBar, Wrench, Clipboard, Calendar, X, ClipboardCheck } from "lucide-react";
8
+ import { Button } from "@/components/ui/button";
9
+
10
+ export function MobileSidebar() {
11
+ const { open, setOpen } = useMobileSidebar();
12
+ const [location] = useLocation();
13
+ const { user } = useAuth();
14
+ const { getCurrentUserStatus, setCurrentUserStatus } = useUserStatus();
15
+
16
+ const navigation = [
17
+ {
18
+ section: "Основное",
19
+ items: [
20
+ {
21
+ name: "Панель управления",
22
+ href: "/dashboard",
23
+ icon: <BarChart2 className="h-6 w-6" />,
24
+ active: location === "/dashboard" || location === "/",
25
+ },
26
+ {
27
+ name: "График ТО",
28
+ href: "/schedule",
29
+ icon: <Calendar className="h-6 w-6" />,
30
+ active: location === "/schedule",
31
+ badge: 3
32
+ },
33
+ {
34
+ name: "Оборудование",
35
+ href: "/equipment",
36
+ icon: <Wrench className="h-6 w-6" />,
37
+ active: location === "/equipment",
38
+ },
39
+ {
40
+ name: "Ежедневные осмотры",
41
+ href: "/daily-inspection",
42
+ icon: <ClipboardCheck className="h-6 w-6" />,
43
+ active: location === "/daily-inspection",
44
+ },
45
+ {
46
+ name: "Техническое обслуживание",
47
+ href: "/maintenance",
48
+ icon: <Clipboard className="h-6 w-6" />,
49
+ active: location === "/maintenance",
50
+ }
51
+ ]
52
+ },
53
+ {
54
+ section: "Администрирование",
55
+ items: [
56
+ {
57
+ name: "Пользователи",
58
+ href: "/users",
59
+ icon: <Users className="h-6 w-6" />,
60
+ active: location === "/users",
61
+ },
62
+ {
63
+ name: "Отчеты",
64
+ href: "/reports",
65
+ icon: <ChartBar className="h-6 w-6" />,
66
+ active: location === "/reports",
67
+ }
68
+ ]
69
+ }
70
+ ];
71
+
72
+ if (!open) return null;
73
+
74
+ return (
75
+ <div
76
+ className="fixed inset-0 flex z-40 lg:hidden"
77
+ role="dialog"
78
+ aria-modal="true"
79
+ >
80
+ <div
81
+ className="fixed inset-0 bg-gray-600 bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-80"
82
+ aria-hidden="true"
83
+ onClick={() => setOpen(false)}
84
+ />
85
+
86
+ {/* Sidebar */}
87
+ <div className="relative flex-1 flex flex-col max-w-xs w-full bg-white dark:bg-gray-800">
88
+ <div className="absolute top-0 right-0 -mr-12 pt-2">
89
+ <Button
90
+ variant="ghost"
91
+ size="icon"
92
+ className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
93
+ onClick={() => setOpen(false)}
94
+ >
95
+ <span className="sr-only">Close sidebar</span>
96
+ <X className="h-6 w-6 text-white" />
97
+ </Button>
98
+ </div>
99
+
100
+ <div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
101
+ <div className="flex-shrink-0 flex items-center px-4">
102
+ <div className="w-8 h-8 rounded-md bg-primary-600 flex items-center justify-center">
103
+ <BarChart2 className="text-white text-sm" />
104
+ </div>
105
+ <h1 className="ml-3 text-xl font-semibold text-gray-800 dark:text-white">Победит 4</h1>
106
+ </div>
107
+ <nav className="mt-5 px-2">
108
+ {navigation.map((section, idx) => (
109
+ <div key={`mobile-section-${idx}`} className="mb-4">
110
+ <p className="uppercase text-xs font-semibold text-gray-500 px-2 mb-2 dark:text-gray-400">{section.section}</p>
111
+ <div className="space-y-1">
112
+ {section.items.map((item, itemIdx) => (
113
+ <Link key={`mobile-item-${idx}-${itemIdx}`} href={item.href}>
114
+ <a
115
+ className={`group flex items-center px-2 py-2 text-base font-medium rounded-md ${
116
+ item.active
117
+ ? "bg-primary-50 text-primary-600 dark:bg-primary-900/20 dark:text-primary-400"
118
+ : "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700/50"
119
+ }`}
120
+ onClick={() => setOpen(false)}
121
+ >
122
+ {item.icon}
123
+ <span className="ml-3">{item.name}</span>
124
+ {item.badge && (
125
+ <span className="ml-auto bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">
126
+ {item.badge}
127
+ </span>
128
+ )}
129
+ </a>
130
+ </Link>
131
+ ))}
132
+ </div>
133
+ </div>
134
+ ))}
135
+ </nav>
136
+ </div>
137
+
138
+ <div className="flex-shrink-0 border-t border-gray-200 p-4 dark:border-gray-700">
139
+ <UserStatusSelector
140
+ currentStatus={getCurrentUserStatus()}
141
+ onStatusChange={setCurrentUserStatus}
142
+ userName={user?.name || "Алекс Морган"}
143
+ />
144
+ </div>
145
+ </div>
146
+ <div className="flex-shrink-0 w-14" aria-hidden="true">
147
+ {/* Spacer */}
148
+ </div>
149
+ </div>
150
+ );
151
+ }
client/src/components/layout/notifications.tsx ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Bell, Calendar, Wrench, AlertTriangle, FileText, CheckSquare } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuLabel,
12
+ } from '@/components/ui/dropdown-menu';
13
+ import { useEquipmentData } from '@/hooks/use-equipment-data';
14
+ import { useRemarksData } from '@/hooks/use-remarks-data';
15
+ import { useMaintenanceData } from '@/hooks/use-maintenance-data';
16
+ import { useQuery } from '@tanstack/react-query';
17
+ import { Link } from 'wouter';
18
+
19
+ interface Notification {
20
+ id: string;
21
+ type: 'maintenance' | 'remark' | 'task' | 'warning' | 'info';
22
+ title: string;
23
+ description: string;
24
+ link?: string;
25
+ equipmentId?: string;
26
+ priority: 'high' | 'medium' | 'low';
27
+ createdAt: Date;
28
+ }
29
+
30
+ export function NotificationsDropdown() {
31
+ const { equipment } = useEquipmentData();
32
+ const { remarks } = useRemarksData();
33
+ const { maintenanceRecords, refreshData } = useMaintenanceData();
34
+ const [refreshKey, setRefreshKey] = useState(0);
35
+
36
+ // Загрузка задач
37
+ const { data: tasks = [] } = useQuery({
38
+ queryKey: ['/api/tasks']
39
+ });
40
+
41
+ // Слушаем события изменения данных ТО и замечаний
42
+ useEffect(() => {
43
+ const handleDataChange = () => {
44
+ refreshData();
45
+ setRefreshKey(prev => prev + 1);
46
+ };
47
+
48
+ window.addEventListener('maintenanceDataChanged', handleDataChange);
49
+ window.addEventListener('remarksUpdated', handleDataChange);
50
+ window.addEventListener('remarkStatusChanged', handleDataChange);
51
+
52
+ return () => {
53
+ window.removeEventListener('maintenanceDataChanged', handleDataChange);
54
+ window.removeEventListener('remarksUpdated', handleDataChange);
55
+ window.removeEventListener('remarkStatusChanged', handleDataChange);
56
+ };
57
+ }, [refreshData]);
58
+
59
+ const generateNotifications = (): Notification[] => {
60
+ const notifications: Notification[] = [];
61
+ const today = new Date();
62
+ const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
63
+
64
+ // Уведомления о запланированных ТО из записей обслуживания
65
+ maintenanceRecords.forEach(record => {
66
+ if (record.status !== 'scheduled') return;
67
+
68
+ const scheduledDate = new Date(record.scheduledDate);
69
+ const daysUntil = Math.ceil((scheduledDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
70
+
71
+ // Проверяем, что оборудование существует в системе
72
+ const equipmentItem = equipment.find(eq => eq.id === record.equipmentId);
73
+ if (!equipmentItem) return; // Пропускаем записи для несуществующего оборудования
74
+
75
+ const equipmentName = equipmentItem.name;
76
+
77
+ if (scheduledDate <= nextWeek && scheduledDate >= today) {
78
+ notifications.push({
79
+ id: `maintenance-${record.id}`,
80
+ type: 'maintenance',
81
+ title: 'Требуется ТО',
82
+ description: `${equipmentName} - ${record.maintenanceType} через ${daysUntil} дн.`,
83
+ link: '/maintenance',
84
+ equipmentId: record.equipmentId,
85
+ priority: daysUntil <= 3 ? 'high' : 'medium',
86
+ createdAt: today
87
+ });
88
+ } else if (scheduledDate < today) {
89
+ notifications.push({
90
+ id: `maintenance-overdue-${record.id}`,
91
+ type: 'warning',
92
+ title: 'Просрочено ТО',
93
+ description: `${equipmentName} - ${record.maintenanceType} просрочено на ${Math.abs(daysUntil)} дн.`,
94
+ link: '/maintenance',
95
+ equipmentId: record.equipmentId,
96
+ priority: 'high',
97
+ createdAt: today
98
+ });
99
+ }
100
+ });
101
+
102
+ // Уведомления о замечаниях (только открытые и в работе)
103
+ remarks.forEach(remark => {
104
+ if (remark.status === 'open' || remark.status === 'in_progress') {
105
+ const priority = remark.priority === 'critical' ? 'high' :
106
+ remark.priority === 'high' ? 'medium' : 'low';
107
+
108
+ notifications.push({
109
+ id: `remark-${remark.id}`,
110
+ type: 'remark',
111
+ title: remark.status === 'in_progress' ? 'Замечание в работе' : 'Открытое замечание',
112
+ description: `${remark.equipmentName} - ${remark.description.substring(0, 50)}...`,
113
+ link: '/tasks',
114
+ equipmentId: remark.equipmentId,
115
+ priority,
116
+ createdAt: new Date(remark.createdAt)
117
+ });
118
+ }
119
+ });
120
+
121
+ // Уведомления о задачах (только ожидающие и в работе)
122
+ tasks.forEach((task: any) => {
123
+ if (task.status === 'pending' || task.status === 'in_progress') {
124
+ const priority = task.priority === 'critical' ? 'high' :
125
+ task.priority === 'high' ? 'medium' : 'low';
126
+
127
+ const dueDate = task.dueDate ? new Date(task.dueDate) : null;
128
+ const isOverdue = dueDate && dueDate < today;
129
+ const daysDue = dueDate ? Math.ceil((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)) : null;
130
+
131
+ notifications.push({
132
+ id: `task-${task.id}`,
133
+ type: 'task',
134
+ title: task.status === 'in_progress' ? 'Задача в работе' : isOverdue ? 'Просроченная задача' : 'Новая задача',
135
+ description: daysDue !== null ?
136
+ (isOverdue ? `${task.title} - просрочена на ${Math.abs(daysDue)} дн.` :
137
+ daysDue <= 3 ? `${task.title} - до ${daysDue} дн.` : task.title) :
138
+ task.title,
139
+ link: '/tasks',
140
+ equipmentId: task.equipmentId,
141
+ priority: isOverdue ? 'high' : priority,
142
+ createdAt: new Date(task.createdAt)
143
+ });
144
+ }
145
+ });
146
+
147
+ // Оборудование требующее внимания (статус maintenance)
148
+ equipment.forEach(item => {
149
+ if (item.status === 'maintenance') {
150
+ notifications.push({
151
+ id: `equipment-maintenance-${item.id}`,
152
+ type: 'warning',
153
+ title: 'Оборудование на ТО',
154
+ description: `${item.name} - находится на техобслуживании`,
155
+ link: '/equipment',
156
+ equipmentId: item.id,
157
+ priority: 'medium',
158
+ createdAt: today
159
+ });
160
+ } else if (item.status === 'inactive') {
161
+ notifications.push({
162
+ id: `equipment-inactive-${item.id}`,
163
+ type: 'warning',
164
+ title: 'Оборудование не активно',
165
+ description: `${item.name} - требует проверки`,
166
+ link: '/equipment',
167
+ equipmentId: item.id,
168
+ priority: 'high',
169
+ createdAt: today
170
+ });
171
+ }
172
+ });
173
+
174
+ // Сортировка по приоритету и дате
175
+ return notifications.sort((a, b) => {
176
+ const priorityOrder = { high: 3, medium: 2, low: 1 };
177
+ if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
178
+ return priorityOrder[b.priority] - priorityOrder[a.priority];
179
+ }
180
+ return b.createdAt.getTime() - a.createdAt.getTime();
181
+ });
182
+ };
183
+
184
+ const notifications = generateNotifications();
185
+ const highPriorityCount = notifications.filter(n => n.priority === 'high').length;
186
+
187
+ const getNotificationIcon = (type: string) => {
188
+ switch (type) {
189
+ case 'maintenance':
190
+ return <Calendar className="w-4 h-4 text-blue-500" />;
191
+ case 'remark':
192
+ return <FileText className="w-4 h-4 text-yellow-500" />;
193
+ case 'task':
194
+ return <CheckSquare className="w-4 h-4 text-green-500" />;
195
+ case 'warning':
196
+ return <AlertTriangle className="w-4 h-4 text-red-500" />;
197
+ default:
198
+ return <Wrench className="w-4 h-4 text-gray-500" />;
199
+ }
200
+ };
201
+
202
+ const getPriorityColor = (priority: string) => {
203
+ switch (priority) {
204
+ case 'high':
205
+ return 'text-red-600 dark:text-red-400';
206
+ case 'medium':
207
+ return 'text-yellow-600 dark:text-yellow-400';
208
+ default:
209
+ return 'text-gray-600 dark:text-gray-400';
210
+ }
211
+ };
212
+
213
+ return (
214
+ <DropdownMenu>
215
+ <DropdownMenuTrigger asChild>
216
+ <Button variant="ghost" size="icon" className="relative text-gray-300 hover:text-white">
217
+ <Bell className="h-5 w-5" />
218
+ {notifications.length > 0 && (
219
+ <Badge
220
+ variant={highPriorityCount > 0 ? "destructive" : "secondary"}
221
+ className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
222
+ >
223
+ {notifications.length > 99 ? '99+' : notifications.length}
224
+ </Badge>
225
+ )}
226
+ </Button>
227
+ </DropdownMenuTrigger>
228
+ <DropdownMenuContent align="end" className="w-80 max-h-96 overflow-y-auto">
229
+ <DropdownMenuLabel className="flex items-center justify-between">
230
+ <span>Уведомления</span>
231
+ {notifications.length > 0 && (
232
+ <Badge variant="outline" className="ml-2">
233
+ {notifications.length}
234
+ </Badge>
235
+ )}
236
+ </DropdownMenuLabel>
237
+ <DropdownMenuSeparator />
238
+
239
+ {notifications.length === 0 ? (
240
+ <div className="p-4 text-center text-gray-500 dark:text-gray-400">
241
+ <Bell className="w-8 h-8 mx-auto mb-2 opacity-50" />
242
+ <p className="text-sm">Нет уведомлений</p>
243
+ </div>
244
+ ) : (
245
+ notifications.map((notification) => (
246
+ <DropdownMenuItem key={notification.id} className="p-0">
247
+ <Link
248
+ href={notification.link || '#'}
249
+ className="w-full p-3 flex items-start gap-3 hover:bg-gray-50 dark:hover:bg-gray-800"
250
+ >
251
+ <div className="flex-shrink-0 mt-1">
252
+ {getNotificationIcon(notification.type)}
253
+ </div>
254
+ <div className="flex-1 min-w-0">
255
+ <div className="flex items-center justify-between">
256
+ <p className={`text-sm font-medium ${getPriorityColor(notification.priority)}`}>
257
+ {notification.title}
258
+ </p>
259
+ <Badge
260
+ variant={notification.priority === 'high' ? 'destructive' :
261
+ notification.priority === 'medium' ? 'default' : 'secondary'}
262
+ className="ml-2 text-xs"
263
+ >
264
+ {notification.priority === 'high' ? 'Срочно' :
265
+ notification.priority === 'medium' ? 'Важно' : 'Обычное'}
266
+ </Badge>
267
+ </div>
268
+ <p className="text-xs text-gray-600 dark:text-gray-300 mt-1 truncate">
269
+ {notification.description}
270
+ </p>
271
+ {notification.equipmentId && (
272
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
273
+ ID: {notification.equipmentId}
274
+ </p>
275
+ )}
276
+ </div>
277
+ </Link>
278
+ </DropdownMenuItem>
279
+ ))
280
+ )}
281
+
282
+ {notifications.length > 0 && (
283
+ <>
284
+ <DropdownMenuSeparator />
285
+ <DropdownMenuItem asChild>
286
+ <Link href="/maintenance" className="w-full text-center p-3 text-sm text-blue-600 dark:text-blue-400">
287
+ Все уведомления →
288
+ </Link>
289
+ </DropdownMenuItem>
290
+ </>
291
+ )}
292
+ </DropdownMenuContent>
293
+ </DropdownMenu>
294
+ );
295
+ }
client/src/components/layout/sidebar.tsx ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Link, useLocation } from "wouter";
2
+ import { useAuth } from "@/hooks/use-auth";
3
+ import { useUserStatus } from "@/hooks/use-user-status";
4
+ import { useSidebarState } from "@/hooks/use-sidebar-state";
5
+ import { UserStatusSelector, getStatusBadge } from "@/components/layout/user-status";
6
+ import { Button } from "@/components/ui/button";
7
+ import { BarChart2, Users, Calendar, Wrench, FileText, Settings, CheckSquare, Clipboard, ChartBar, ClipboardCheck, CheckCircle } from "lucide-react";
8
+
9
+ export function Sidebar() {
10
+ const [location] = useLocation();
11
+ const { user } = useAuth();
12
+ const { users, getCurrentUserStatus, setCurrentUserStatus } = useUserStatus();
13
+ const { isCollapsed, toggleCollapsed } = useSidebarState();
14
+
15
+
16
+
17
+ const navigation = [
18
+ {
19
+ section: "Основное",
20
+ items: [
21
+ {
22
+ name: "Панель управления",
23
+ href: "/dashboard",
24
+ icon: <BarChart2 className="w-5 h-5" />,
25
+ active: location === "/dashboard" || location === "/",
26
+ },
27
+ {
28
+ name: "График ТО",
29
+ href: "/schedule",
30
+ icon: <Calendar className="w-5 h-5" />,
31
+ active: location === "/schedule",
32
+ },
33
+ {
34
+ name: "Оборудование",
35
+ href: "/equipment",
36
+ icon: <Wrench className="w-5 h-5" />,
37
+ active: location === "/equipment",
38
+ },
39
+ {
40
+ name: "Ежедневные осмотры",
41
+ href: "/daily-inspection",
42
+ icon: <ClipboardCheck className="w-5 h-5" />,
43
+ active: location === "/daily-inspection",
44
+ },
45
+ {
46
+ name: "Техническое обслуживание",
47
+ href: "/maintenance",
48
+ icon: <Clipboard className="w-5 h-5" />,
49
+ active: location === "/maintenance",
50
+ },
51
+ {
52
+ name: "Задачи",
53
+ href: "/tasks",
54
+ icon: <CheckSquare className="w-5 h-5" />,
55
+ active: location === "/tasks",
56
+ }
57
+ ]
58
+ },
59
+ {
60
+ section: "Администрирование",
61
+ items: [
62
+ {
63
+ name: "Пользователи",
64
+ href: "/users",
65
+ icon: <Users className="w-5 h-5" />,
66
+ active: location === "/users",
67
+ },
68
+ {
69
+ name: "Отчеты",
70
+ href: "/reports",
71
+ icon: <ChartBar className="w-5 h-5" />,
72
+ active: location === "/reports",
73
+ }
74
+ ]
75
+ }
76
+ ];
77
+
78
+ // Получаем только пользователей со статусом "на работе" для отображения в боковой панели
79
+ const workingUsers = users.filter(user => user.status === 'working');
80
+ const displayUsers = workingUsers.slice(0, 5);
81
+
82
+ return (
83
+ <aside className={`sidebar fixed left-0 top-0 z-40 hidden lg:flex flex-col h-full bg-gray-900 border-r border-gray-700 shadow-sm transition-all duration-300 ${
84
+ isCollapsed ? 'w-16' : 'w-64'
85
+ }`}>
86
+ <div className="p-5 border-b border-gray-700">
87
+ <div className="flex items-center justify-between">
88
+ <div className="flex items-center">
89
+ <div className="w-8 h-8 rounded-md bg-blue-600 flex items-center justify-center">
90
+ <BarChart2 className="text-white text-sm" />
91
+ </div>
92
+ {!isCollapsed && (
93
+ <div className="ml-3">
94
+ <h1 className="text-xl font-semibold text-white">StarLine</h1>
95
+ <p className="text-sm text-gray-400">Победит 4</p>
96
+ </div>
97
+ )}
98
+ </div>
99
+ <Button
100
+ variant="ghost"
101
+ size="icon"
102
+ onClick={toggleCollapsed}
103
+ className="text-gray-400 hover:text-gray-200"
104
+ >
105
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
106
+ <path fillRule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
107
+ </svg>
108
+ </Button>
109
+ </div>
110
+ </div>
111
+
112
+ <nav className="flex-grow overflow-y-auto p-4">
113
+ {navigation.map((section, idx) => (
114
+ <div key={`section-${idx}`} className="mb-6">
115
+ {!isCollapsed && (
116
+ <p className="uppercase text-xs font-semibold text-white mb-2">{section.section}</p>
117
+ )}
118
+ <ul className="space-y-2">
119
+ {section.items.map((item, itemIdx) => (
120
+ <li key={`item-${idx}-${itemIdx}`}>
121
+ <Link href={item.href}>
122
+ <div className={`flex items-center px-4 py-3 rounded-md font-medium cursor-pointer
123
+ ${item.active
124
+ ? "text-white bg-blue-900/20"
125
+ : "text-gray-300 hover:bg-gray-700/50 hover:text-white"}`}>
126
+ {item.icon}
127
+ {!isCollapsed && (
128
+ <>
129
+ <span className="ml-3">{item.name}</span>
130
+ {item.badge && item.badge > 0 && (
131
+ <span className="ml-auto bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">
132
+ {item.badge}
133
+ </span>
134
+ )}
135
+ </>
136
+ )}
137
+ {isCollapsed && item.badge && item.badge > 0 && (
138
+ <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold px-1.5 py-0.5 rounded-full">
139
+ {item.badge}
140
+ </span>
141
+ )}
142
+ </div>
143
+ </Link>
144
+ </li>
145
+ ))}
146
+ </ul>
147
+ </div>
148
+ ))}
149
+
150
+ {!isCollapsed && (
151
+ <div className="mt-8 pt-6 border-t border-gray-700">
152
+ <h3 className="px-4 text-xs font-semibold text-gray-400 uppercase tracking-wider flex items-center justify-between">
153
+ Сотрудники на работе
154
+ <span className="bg-green-900 text-green-300 text-xs px-2 py-1 rounded-full">
155
+ {workingUsers.length}
156
+ </span>
157
+ </h3>
158
+ <ul className="mt-3 space-y-2">
159
+ {displayUsers.length > 0 ? (
160
+ displayUsers.map((member) => (
161
+ <li key={member.id}>
162
+ <div className="flex items-center px-4 py-2 text-sm text-gray-300 hover:bg-gray-700/50 rounded-md">
163
+ <div className="w-7 h-7 rounded-full bg-gray-700 flex items-center justify-center text-xs font-medium text-white">
164
+ {member.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
165
+ </div>
166
+ <div className="ml-3 flex-1 min-w-0">
167
+ <p className="text-sm font-medium text-white truncate">
168
+ {member.name.split(' ')[1]} {member.name.split(' ')[2]?.[0]}.
169
+ </p>
170
+ <div className="mt-1">
171
+ {getStatusBadge(member.status, true)}
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </li>
176
+ ))
177
+ ) : (
178
+ <li className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center">
179
+ Нет сотрудников на работе
180
+ </li>
181
+ )}
182
+ </ul>
183
+ </div>
184
+ )}
185
+
186
+ {isCollapsed && displayUsers.length > 0 && (
187
+ <div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
188
+ <div className="flex flex-col items-center space-y-2 px-2">
189
+ {displayUsers.slice(0, 3).map((member) => (
190
+ <div
191
+ key={member.id}
192
+ className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-xs font-medium"
193
+ title={member.name}
194
+ >
195
+ {member.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
196
+ </div>
197
+ ))}
198
+ {workingUsers.length > 3 && (
199
+ <div className="w-6 h-6 rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300 flex items-center justify-center text-xs font-bold">
200
+ +{workingUsers.length - 3}
201
+ </div>
202
+ )}
203
+ </div>
204
+ </div>
205
+ )}
206
+ </nav>
207
+
208
+ {/* User profile with status */}
209
+ <div className="p-4 border-t border-gray-200 dark:border-gray-700">
210
+ {!isCollapsed ? (
211
+ <UserStatusSelector
212
+ currentStatus={getCurrentUserStatus()}
213
+ onStatusChange={setCurrentUserStatus}
214
+ userName={user?.name || "Алекс Морган"}
215
+ />
216
+ ) : (
217
+ <div className="flex justify-center">
218
+ <div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center relative">
219
+ <span className="text-xs font-medium text-white">
220
+ {user?.name?.split(' ').map(n => n[0]).join('').slice(0, 2) || "АМ"}
221
+ </span>
222
+ <div className={`absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-gray-900 ${
223
+ getCurrentUserStatus() === 'working' ? 'bg-blue-500' :
224
+ getCurrentUserStatus() === 'online' ? 'bg-green-500' :
225
+ getCurrentUserStatus() === 'break' ? 'bg-yellow-500' :
226
+ getCurrentUserStatus() === 'vacation' ? 'bg-purple-500' :
227
+ getCurrentUserStatus() === 'busy' ? 'bg-orange-500' :
228
+ 'bg-red-500'
229
+ }`}></div>
230
+ </div>
231
+ </div>
232
+ )}
233
+ </div>
234
+ </aside>
235
+ );
236
+ }
client/src/components/layout/user-status.tsx ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
4
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { User, Clock, Coffee, Plane, UserX, Wifi } from "lucide-react";
7
+ import { useToast } from "@/hooks/use-toast";
8
+
9
+ export interface UserStatus {
10
+ id: string;
11
+ name: string;
12
+ icon: React.ReactNode;
13
+ color: string;
14
+ bgColor: string;
15
+ }
16
+
17
+ const userStatuses: UserStatus[] = [
18
+ {
19
+ id: "online",
20
+ name: "Онлайн",
21
+ icon: <Wifi className="w-3 h-3" />,
22
+ color: "text-green-600",
23
+ bgColor: "bg-green-100 dark:bg-green-900/20"
24
+ },
25
+ {
26
+ id: "working",
27
+ name: "На работе",
28
+ icon: <User className="w-3 h-3" />,
29
+ color: "text-blue-600",
30
+ bgColor: "bg-blue-100 dark:bg-blue-900/20"
31
+ },
32
+ {
33
+ id: "break",
34
+ name: "На перерыве",
35
+ icon: <Coffee className="w-3 h-3" />,
36
+ color: "text-yellow-600",
37
+ bgColor: "bg-yellow-100 dark:bg-yellow-900/20"
38
+ },
39
+ {
40
+ id: "vacation",
41
+ name: "В отпуске",
42
+ icon: <Plane className="w-3 h-3" />,
43
+ color: "text-purple-600",
44
+ bgColor: "bg-purple-100 dark:bg-purple-900/20"
45
+ },
46
+ {
47
+ id: "absent",
48
+ name: "Отсутствует",
49
+ icon: <UserX className="w-3 h-3" />,
50
+ color: "text-red-600",
51
+ bgColor: "bg-red-100 dark:bg-red-900/20"
52
+ },
53
+ {
54
+ id: "busy",
55
+ name: "Занят",
56
+ icon: <Clock className="w-3 h-3" />,
57
+ color: "text-orange-600",
58
+ bgColor: "bg-orange-100 dark:bg-orange-900/20"
59
+ }
60
+ ];
61
+
62
+ interface UserStatusSelectorProps {
63
+ currentStatus: string;
64
+ onStatusChange: (statusId: string) => void;
65
+ userName: string;
66
+ }
67
+
68
+ export function UserStatusSelector({ currentStatus, onStatusChange, userName }: UserStatusSelectorProps) {
69
+ const [isOpen, setIsOpen] = useState(false);
70
+ const { toast } = useToast();
71
+
72
+ const handleStatusChange = (statusId: string) => {
73
+ onStatusChange(statusId);
74
+ setIsOpen(false);
75
+
76
+ const status = userStatuses.find(s => s.id === statusId);
77
+ toast({
78
+ title: "Статус обновлен",
79
+ description: `Ваш статус изменен на "${status?.name}"`,
80
+ });
81
+ };
82
+
83
+ const currentStatusObj = userStatuses.find(s => s.id === currentStatus) || userStatuses[0];
84
+
85
+ return (
86
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
87
+ <DialogTrigger asChild>
88
+ <Button variant="ghost" className="w-full justify-start p-2 h-auto hover:bg-gray-700/50">
89
+ <div className="flex items-center space-x-2 w-full">
90
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center ${currentStatusObj.bgColor}`}>
91
+ <span className="text-xs font-medium text-gray-700 dark:text-gray-300">
92
+ {userName.split(' ').map(n => n[0]).join('').slice(0, 2)}
93
+ </span>
94
+ </div>
95
+ <div className="flex-1 text-left">
96
+ <div className="text-sm font-medium text-white group-hover:text-gray-900 hover:text-gray-900">{userName}</div>
97
+ <div className="flex items-center space-x-1">
98
+ <span className={currentStatusObj.color}>{currentStatusObj.icon}</span>
99
+ <span className="text-xs text-white group-hover:text-gray-700 hover:text-gray-700">{currentStatusObj.name}</span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </Button>
104
+ </DialogTrigger>
105
+ <DialogContent className="sm:max-w-[425px]">
106
+ <DialogHeader>
107
+ <DialogTitle>Изменить статус</DialogTitle>
108
+ </DialogHeader>
109
+ <div className="grid gap-4 py-4">
110
+ <div className="space-y-2">
111
+ {userStatuses.map((status) => (
112
+ <Button
113
+ key={status.id}
114
+ variant={currentStatus === status.id ? "default" : "ghost"}
115
+ className="w-full justify-start"
116
+ onClick={() => handleStatusChange(status.id)}
117
+ >
118
+ <div className="flex items-center space-x-3">
119
+ <span className={status.color}>{status.icon}</span>
120
+ <span>{status.name}</span>
121
+ </div>
122
+ </Button>
123
+ ))}
124
+ </div>
125
+ </div>
126
+ </DialogContent>
127
+ </Dialog>
128
+ );
129
+ }
130
+
131
+ export function getStatusBadge(statusId: string, darkMode: boolean = false) {
132
+ const status = userStatuses.find(s => s.id === statusId) || userStatuses[0];
133
+
134
+ if (darkMode) {
135
+ // Цвета для темного фона боковой панели
136
+ const darkColors = {
137
+ 'online': 'bg-green-500/20 text-green-300',
138
+ 'working': 'bg-blue-500/20 text-blue-300',
139
+ 'break': 'bg-yellow-500/20 text-yellow-300',
140
+ 'vacation': 'bg-purple-500/20 text-purple-300',
141
+ 'absent': 'bg-red-500/20 text-red-300',
142
+ 'busy': 'bg-orange-500/20 text-orange-300'
143
+ };
144
+
145
+ const colorClass = darkColors[status.id as keyof typeof darkColors] || darkColors.online;
146
+
147
+ return (
148
+ <Badge className={`${colorClass} border-0 text-xs`}>
149
+ {status.icon}
150
+ <span className="ml-1">{status.name}</span>
151
+ </Badge>
152
+ );
153
+ }
154
+
155
+ return (
156
+ <Badge className={`${status.bgColor} ${status.color} border-0`}>
157
+ {status.icon}
158
+ <span className="ml-1">{status.name}</span>
159
+ </Badge>
160
+ );
161
+ }
162
+
163
+ export { userStatuses };
client/src/components/providers.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { QueryClientProvider } from "@tanstack/react-query";
2
+ import { queryClient } from "@/lib/queryClient";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import { Toaster } from "@/components/ui/toaster";
5
+ import { useState, useEffect, createContext } from "react";
6
+
7
+ type Theme = "light" | "dark";
8
+
9
+ interface ThemeContextType {
10
+ theme: Theme;
11
+ setTheme: (theme: Theme) => void;
12
+ }
13
+
14
+ export const ThemeContext = createContext<ThemeContextType>({
15
+ theme: "light",
16
+ setTheme: () => {},
17
+ });
18
+
19
+ export function Providers({ children }: { children: React.ReactNode }) {
20
+ const [theme, setTheme] = useState<Theme>("light");
21
+
22
+ useEffect(() => {
23
+ const storedTheme = localStorage.getItem("theme") as Theme | null;
24
+ if (storedTheme) {
25
+ setTheme(storedTheme);
26
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
27
+ setTheme("dark");
28
+ }
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ localStorage.setItem("theme", theme);
33
+ if (theme === "dark") {
34
+ document.documentElement.classList.add("dark");
35
+ } else {
36
+ document.documentElement.classList.remove("dark");
37
+ }
38
+ }, [theme]);
39
+
40
+ return (
41
+ <ThemeContext.Provider value={{ theme, setTheme }}>
42
+ <QueryClientProvider client={queryClient}>
43
+ <TooltipProvider>
44
+ <Toaster />
45
+ {children}
46
+ </TooltipProvider>
47
+ </QueryClientProvider>
48
+ </ThemeContext.Provider>
49
+ );
50
+ }
client/src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDown } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Accordion = AccordionPrimitive.Root
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item
14
+ ref={ref}
15
+ className={cn("border-b", className)}
16
+ {...props}
17
+ />
18
+ ))
19
+ AccordionItem.displayName = "AccordionItem"
20
+
21
+ const AccordionTrigger = React.forwardRef<
22
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
23
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
24
+ >(({ className, children, ...props }, ref) => (
25
+ <AccordionPrimitive.Header className="flex">
26
+ <AccordionPrimitive.Trigger
27
+ ref={ref}
28
+ className={cn(
29
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
36
+ </AccordionPrimitive.Trigger>
37
+ </AccordionPrimitive.Header>
38
+ ))
39
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40
+
41
+ const AccordionContent = React.forwardRef<
42
+ React.ElementRef<typeof AccordionPrimitive.Content>,
43
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
44
+ >(({ className, children, ...props }, ref) => (
45
+ <AccordionPrimitive.Content
46
+ ref={ref}
47
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
48
+ {...props}
49
+ >
50
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
51
+ </AccordionPrimitive.Content>
52
+ ))
53
+
54
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
55
+
56
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
client/src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { buttonVariants } from "@/components/ui/button"
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ))
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ))
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45
+
46
+ const AlertDialogHeader = ({
47
+ className,
48
+ ...props
49
+ }: React.HTMLAttributes<HTMLDivElement>) => (
50
+ <div
51
+ className={cn(
52
+ "flex flex-col space-y-2 text-center sm:text-left",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ AlertDialogHeader.displayName = "AlertDialogHeader"
59
+
60
+ const AlertDialogFooter = ({
61
+ className,
62
+ ...props
63
+ }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div
65
+ className={cn(
66
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ )
72
+ AlertDialogFooter.displayName = "AlertDialogFooter"
73
+
74
+ const AlertDialogTitle = React.forwardRef<
75
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
76
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
77
+ >(({ className, ...props }, ref) => (
78
+ <AlertDialogPrimitive.Title
79
+ ref={ref}
80
+ className={cn("text-lg font-semibold", className)}
81
+ {...props}
82
+ />
83
+ ))
84
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85
+
86
+ const AlertDialogDescription = React.forwardRef<
87
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
88
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
89
+ >(({ className, ...props }, ref) => (
90
+ <AlertDialogPrimitive.Description
91
+ ref={ref}
92
+ className={cn("text-sm text-muted-foreground", className)}
93
+ {...props}
94
+ />
95
+ ))
96
+ AlertDialogDescription.displayName =
97
+ AlertDialogPrimitive.Description.displayName
98
+
99
+ const AlertDialogAction = React.forwardRef<
100
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
101
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
102
+ >(({ className, ...props }, ref) => (
103
+ <AlertDialogPrimitive.Action
104
+ ref={ref}
105
+ className={cn(buttonVariants(), className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110
+
111
+ const AlertDialogCancel = React.forwardRef<
112
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
113
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114
+ >(({ className, ...props }, ref) => (
115
+ <AlertDialogPrimitive.Cancel
116
+ ref={ref}
117
+ className={cn(
118
+ buttonVariants({ variant: "outline" }),
119
+ "mt-2 sm:mt-0",
120
+ className
121
+ )}
122
+ {...props}
123
+ />
124
+ ))
125
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126
+
127
+ export {
128
+ AlertDialog,
129
+ AlertDialogPortal,
130
+ AlertDialogOverlay,
131
+ AlertDialogTrigger,
132
+ AlertDialogContent,
133
+ AlertDialogHeader,
134
+ AlertDialogFooter,
135
+ AlertDialogTitle,
136
+ AlertDialogDescription,
137
+ AlertDialogAction,
138
+ AlertDialogCancel,
139
+ }
client/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
client/src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root
4
+
5
+ export { AspectRatio }
client/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName
22
+
23
+ const AvatarImage = React.forwardRef<
24
+ React.ElementRef<typeof AvatarPrimitive.Image>,
25
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26
+ >(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Image
28
+ ref={ref}
29
+ className={cn("aspect-square h-full w-full", className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
34
+
35
+ const AvatarFallback = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
37
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Fallback
40
+ ref={ref}
41
+ className={cn(
42
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ ))
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49
+
50
+ export { Avatar, AvatarImage, AvatarFallback }
client/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
client/src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
13
+ Breadcrumb.displayName = "Breadcrumb"
14
+
15
+ const BreadcrumbList = React.forwardRef<
16
+ HTMLOListElement,
17
+ React.ComponentPropsWithoutRef<"ol">
18
+ >(({ className, ...props }, ref) => (
19
+ <ol
20
+ ref={ref}
21
+ className={cn(
22
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ BreadcrumbList.displayName = "BreadcrumbList"
29
+
30
+ const BreadcrumbItem = React.forwardRef<
31
+ HTMLLIElement,
32
+ React.ComponentPropsWithoutRef<"li">
33
+ >(({ className, ...props }, ref) => (
34
+ <li
35
+ ref={ref}
36
+ className={cn("inline-flex items-center gap-1.5", className)}
37
+ {...props}
38
+ />
39
+ ))
40
+ BreadcrumbItem.displayName = "BreadcrumbItem"
41
+
42
+ const BreadcrumbLink = React.forwardRef<
43
+ HTMLAnchorElement,
44
+ React.ComponentPropsWithoutRef<"a"> & {
45
+ asChild?: boolean
46
+ }
47
+ >(({ asChild, className, ...props }, ref) => {
48
+ const Comp = asChild ? Slot : "a"
49
+
50
+ return (
51
+ <Comp
52
+ ref={ref}
53
+ className={cn("transition-colors hover:text-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ })
58
+ BreadcrumbLink.displayName = "BreadcrumbLink"
59
+
60
+ const BreadcrumbPage = React.forwardRef<
61
+ HTMLSpanElement,
62
+ React.ComponentPropsWithoutRef<"span">
63
+ >(({ className, ...props }, ref) => (
64
+ <span
65
+ ref={ref}
66
+ role="link"
67
+ aria-disabled="true"
68
+ aria-current="page"
69
+ className={cn("font-normal text-foreground", className)}
70
+ {...props}
71
+ />
72
+ ))
73
+ BreadcrumbPage.displayName = "BreadcrumbPage"
74
+
75
+ const BreadcrumbSeparator = ({
76
+ children,
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<"li">) => (
80
+ <li
81
+ role="presentation"
82
+ aria-hidden="true"
83
+ className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
84
+ {...props}
85
+ >
86
+ {children ?? <ChevronRight />}
87
+ </li>
88
+ )
89
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90
+
91
+ const BreadcrumbEllipsis = ({
92
+ className,
93
+ ...props
94
+ }: React.ComponentProps<"span">) => (
95
+ <span
96
+ role="presentation"
97
+ aria-hidden="true"
98
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
99
+ {...props}
100
+ >
101
+ <MoreHorizontal className="h-4 w-4" />
102
+ <span className="sr-only">More</span>
103
+ </span>
104
+ )
105
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106
+
107
+ export {
108
+ Breadcrumb,
109
+ BreadcrumbList,
110
+ BreadcrumbItem,
111
+ BreadcrumbLink,
112
+ BreadcrumbPage,
113
+ BreadcrumbSeparator,
114
+ BreadcrumbEllipsis,
115
+ }
client/src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }
client/src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import { DayPicker } from "react-day-picker"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { buttonVariants } from "@/components/ui/button"
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: CalendarProps) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
22
+ month: "space-y-4",
23
+ caption: "flex justify-center pt-1 relative items-center",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "space-x-1 flex items-center",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-y-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
38
+ day: cn(
39
+ buttonVariants({ variant: "ghost" }),
40
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
41
+ ),
42
+ day_range_end: "day-range-end",
43
+ day_selected:
44
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
45
+ day_today: "bg-accent text-accent-foreground",
46
+ day_outside:
47
+ "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
48
+ day_disabled: "text-muted-foreground opacity-50",
49
+ day_range_middle:
50
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
51
+ day_hidden: "invisible",
52
+ ...classNames,
53
+ }}
54
+ components={{
55
+ IconLeft: ({ className, ...props }) => (
56
+ <ChevronLeft className={cn("h-4 w-4", className)} {...props} />
57
+ ),
58
+ IconRight: ({ className, ...props }) => (
59
+ <ChevronRight className={cn("h-4 w-4", className)} {...props} />
60
+ ),
61
+ }}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+ Calendar.displayName = "Calendar"
67
+
68
+ export { Calendar }
client/src/components/ui/card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLDivElement,
49
+ React.HTMLAttributes<HTMLDivElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
client/src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import useEmblaCarousel, {
3
+ type UseEmblaCarouselType,
4
+ } from "embla-carousel-react"
5
+ import { ArrowLeft, ArrowRight } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+
10
+ type CarouselApi = UseEmblaCarouselType[1]
11
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
12
+ type CarouselOptions = UseCarouselParameters[0]
13
+ type CarouselPlugin = UseCarouselParameters[1]
14
+
15
+ type CarouselProps = {
16
+ opts?: CarouselOptions
17
+ plugins?: CarouselPlugin
18
+ orientation?: "horizontal" | "vertical"
19
+ setApi?: (api: CarouselApi) => void
20
+ }
21
+
22
+ type CarouselContextProps = {
23
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
24
+ api: ReturnType<typeof useEmblaCarousel>[1]
25
+ scrollPrev: () => void
26
+ scrollNext: () => void
27
+ canScrollPrev: boolean
28
+ canScrollNext: boolean
29
+ } & CarouselProps
30
+
31
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null)
32
+
33
+ function useCarousel() {
34
+ const context = React.useContext(CarouselContext)
35
+
36
+ if (!context) {
37
+ throw new Error("useCarousel must be used within a <Carousel />")
38
+ }
39
+
40
+ return context
41
+ }
42
+
43
+ const Carousel = React.forwardRef<
44
+ HTMLDivElement,
45
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
46
+ >(
47
+ (
48
+ {
49
+ orientation = "horizontal",
50
+ opts,
51
+ setApi,
52
+ plugins,
53
+ className,
54
+ children,
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const [carouselRef, api] = useEmblaCarousel(
60
+ {
61
+ ...opts,
62
+ axis: orientation === "horizontal" ? "x" : "y",
63
+ },
64
+ plugins
65
+ )
66
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
68
+
69
+ const onSelect = React.useCallback((api: CarouselApi) => {
70
+ if (!api) {
71
+ return
72
+ }
73
+
74
+ setCanScrollPrev(api.canScrollPrev())
75
+ setCanScrollNext(api.canScrollNext())
76
+ }, [])
77
+
78
+ const scrollPrev = React.useCallback(() => {
79
+ api?.scrollPrev()
80
+ }, [api])
81
+
82
+ const scrollNext = React.useCallback(() => {
83
+ api?.scrollNext()
84
+ }, [api])
85
+
86
+ const handleKeyDown = React.useCallback(
87
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
88
+ if (event.key === "ArrowLeft") {
89
+ event.preventDefault()
90
+ scrollPrev()
91
+ } else if (event.key === "ArrowRight") {
92
+ event.preventDefault()
93
+ scrollNext()
94
+ }
95
+ },
96
+ [scrollPrev, scrollNext]
97
+ )
98
+
99
+ React.useEffect(() => {
100
+ if (!api || !setApi) {
101
+ return
102
+ }
103
+
104
+ setApi(api)
105
+ }, [api, setApi])
106
+
107
+ React.useEffect(() => {
108
+ if (!api) {
109
+ return
110
+ }
111
+
112
+ onSelect(api)
113
+ api.on("reInit", onSelect)
114
+ api.on("select", onSelect)
115
+
116
+ return () => {
117
+ api?.off("select", onSelect)
118
+ }
119
+ }, [api, onSelect])
120
+
121
+ return (
122
+ <CarouselContext.Provider
123
+ value={{
124
+ carouselRef,
125
+ api: api,
126
+ opts,
127
+ orientation:
128
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
129
+ scrollPrev,
130
+ scrollNext,
131
+ canScrollPrev,
132
+ canScrollNext,
133
+ }}
134
+ >
135
+ <div
136
+ ref={ref}
137
+ onKeyDownCapture={handleKeyDown}
138
+ className={cn("relative", className)}
139
+ role="region"
140
+ aria-roledescription="carousel"
141
+ {...props}
142
+ >
143
+ {children}
144
+ </div>
145
+ </CarouselContext.Provider>
146
+ )
147
+ }
148
+ )
149
+ Carousel.displayName = "Carousel"
150
+
151
+ const CarouselContent = React.forwardRef<
152
+ HTMLDivElement,
153
+ React.HTMLAttributes<HTMLDivElement>
154
+ >(({ className, ...props }, ref) => {
155
+ const { carouselRef, orientation } = useCarousel()
156
+
157
+ return (
158
+ <div ref={carouselRef} className="overflow-hidden">
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "flex",
163
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
164
+ className
165
+ )}
166
+ {...props}
167
+ />
168
+ </div>
169
+ )
170
+ })
171
+ CarouselContent.displayName = "CarouselContent"
172
+
173
+ const CarouselItem = React.forwardRef<
174
+ HTMLDivElement,
175
+ React.HTMLAttributes<HTMLDivElement>
176
+ >(({ className, ...props }, ref) => {
177
+ const { orientation } = useCarousel()
178
+
179
+ return (
180
+ <div
181
+ ref={ref}
182
+ role="group"
183
+ aria-roledescription="slide"
184
+ className={cn(
185
+ "min-w-0 shrink-0 grow-0 basis-full",
186
+ orientation === "horizontal" ? "pl-4" : "pt-4",
187
+ className
188
+ )}
189
+ {...props}
190
+ />
191
+ )
192
+ })
193
+ CarouselItem.displayName = "CarouselItem"
194
+
195
+ const CarouselPrevious = React.forwardRef<
196
+ HTMLButtonElement,
197
+ React.ComponentProps<typeof Button>
198
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200
+
201
+ return (
202
+ <Button
203
+ ref={ref}
204
+ variant={variant}
205
+ size={size}
206
+ className={cn(
207
+ "absolute h-8 w-8 rounded-full",
208
+ orientation === "horizontal"
209
+ ? "-left-12 top-1/2 -translate-y-1/2"
210
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
211
+ className
212
+ )}
213
+ disabled={!canScrollPrev}
214
+ onClick={scrollPrev}
215
+ {...props}
216
+ >
217
+ <ArrowLeft className="h-4 w-4" />
218
+ <span className="sr-only">Previous slide</span>
219
+ </Button>
220
+ )
221
+ })
222
+ CarouselPrevious.displayName = "CarouselPrevious"
223
+
224
+ const CarouselNext = React.forwardRef<
225
+ HTMLButtonElement,
226
+ React.ComponentProps<typeof Button>
227
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
229
+
230
+ return (
231
+ <Button
232
+ ref={ref}
233
+ variant={variant}
234
+ size={size}
235
+ className={cn(
236
+ "absolute h-8 w-8 rounded-full",
237
+ orientation === "horizontal"
238
+ ? "-right-12 top-1/2 -translate-y-1/2"
239
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
240
+ className
241
+ )}
242
+ disabled={!canScrollNext}
243
+ onClick={scrollNext}
244
+ {...props}
245
+ >
246
+ <ArrowRight className="h-4 w-4" />
247
+ <span className="sr-only">Next slide</span>
248
+ </Button>
249
+ )
250
+ })
251
+ CarouselNext.displayName = "CarouselNext"
252
+
253
+ export {
254
+ type CarouselApi,
255
+ Carousel,
256
+ CarouselContent,
257
+ CarouselItem,
258
+ CarouselPrevious,
259
+ CarouselNext,
260
+ }
client/src/components/ui/chart.tsx ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RechartsPrimitive from "recharts"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode
14
+ icon?: React.ComponentType
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ )
19
+ }
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig
23
+ }
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext)
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />")
32
+ }
33
+
34
+ return context
35
+ }
36
+
37
+ const ChartContainer = React.forwardRef<
38
+ HTMLDivElement,
39
+ React.ComponentProps<"div"> & {
40
+ config: ChartConfig
41
+ children: React.ComponentProps<
42
+ typeof RechartsPrimitive.ResponsiveContainer
43
+ >["children"]
44
+ }
45
+ >(({ id, className, children, config, ...props }, ref) => {
46
+ const uniqueId = React.useId()
47
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
48
+
49
+ return (
50
+ <ChartContext.Provider value={{ config }}>
51
+ <div
52
+ data-chart={chartId}
53
+ ref={ref}
54
+ className={cn(
55
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
56
+ className
57
+ )}
58
+ {...props}
59
+ >
60
+ <ChartStyle id={chartId} config={config} />
61
+ <RechartsPrimitive.ResponsiveContainer>
62
+ {children}
63
+ </RechartsPrimitive.ResponsiveContainer>
64
+ </div>
65
+ </ChartContext.Provider>
66
+ )
67
+ })
68
+ ChartContainer.displayName = "Chart"
69
+
70
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71
+ const colorConfig = Object.entries(config).filter(
72
+ ([, config]) => config.theme || config.color
73
+ )
74
+
75
+ if (!colorConfig.length) {
76
+ return null
77
+ }
78
+
79
+ return (
80
+ <style
81
+ dangerouslySetInnerHTML={{
82
+ __html: Object.entries(THEMES)
83
+ .map(
84
+ ([theme, prefix]) => `
85
+ ${prefix} [data-chart=${id}] {
86
+ ${colorConfig
87
+ .map(([key, itemConfig]) => {
88
+ const color =
89
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
90
+ itemConfig.color
91
+ return color ? ` --color-${key}: ${color};` : null
92
+ })
93
+ .join("\n")}
94
+ }
95
+ `
96
+ )
97
+ .join("\n"),
98
+ }}
99
+ />
100
+ )
101
+ }
102
+
103
+ const ChartTooltip = RechartsPrimitive.Tooltip
104
+
105
+ const ChartTooltipContent = React.forwardRef<
106
+ HTMLDivElement,
107
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
108
+ React.ComponentProps<"div"> & {
109
+ hideLabel?: boolean
110
+ hideIndicator?: boolean
111
+ indicator?: "line" | "dot" | "dashed"
112
+ nameKey?: string
113
+ labelKey?: string
114
+ }
115
+ >(
116
+ (
117
+ {
118
+ active,
119
+ payload,
120
+ className,
121
+ indicator = "dot",
122
+ hideLabel = false,
123
+ hideIndicator = false,
124
+ label,
125
+ labelFormatter,
126
+ labelClassName,
127
+ formatter,
128
+ color,
129
+ nameKey,
130
+ labelKey,
131
+ },
132
+ ref
133
+ ) => {
134
+ const { config } = useChart()
135
+
136
+ const tooltipLabel = React.useMemo(() => {
137
+ if (hideLabel || !payload?.length) {
138
+ return null
139
+ }
140
+
141
+ const [item] = payload
142
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`
143
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
144
+ const value =
145
+ !labelKey && typeof label === "string"
146
+ ? config[label as keyof typeof config]?.label || label
147
+ : itemConfig?.label
148
+
149
+ if (labelFormatter) {
150
+ return (
151
+ <div className={cn("font-medium", labelClassName)}>
152
+ {labelFormatter(value, payload)}
153
+ </div>
154
+ )
155
+ }
156
+
157
+ if (!value) {
158
+ return null
159
+ }
160
+
161
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
162
+ }, [
163
+ label,
164
+ labelFormatter,
165
+ payload,
166
+ hideLabel,
167
+ labelClassName,
168
+ config,
169
+ labelKey,
170
+ ])
171
+
172
+ if (!active || !payload?.length) {
173
+ return null
174
+ }
175
+
176
+ const nestLabel = payload.length === 1 && indicator !== "dot"
177
+
178
+ return (
179
+ <div
180
+ ref={ref}
181
+ className={cn(
182
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
183
+ className
184
+ )}
185
+ >
186
+ {!nestLabel ? tooltipLabel : null}
187
+ <div className="grid gap-1.5">
188
+ {payload.map((item, index) => {
189
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
190
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
191
+ const indicatorColor = color || item.payload.fill || item.color
192
+
193
+ return (
194
+ <div
195
+ key={item.dataKey}
196
+ className={cn(
197
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
198
+ indicator === "dot" && "items-center"
199
+ )}
200
+ >
201
+ {formatter && item?.value !== undefined && item.name ? (
202
+ formatter(item.value, item.name, item, index, item.payload)
203
+ ) : (
204
+ <>
205
+ {itemConfig?.icon ? (
206
+ <itemConfig.icon />
207
+ ) : (
208
+ !hideIndicator && (
209
+ <div
210
+ className={cn(
211
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
212
+ {
213
+ "h-2.5 w-2.5": indicator === "dot",
214
+ "w-1": indicator === "line",
215
+ "w-0 border-[1.5px] border-dashed bg-transparent":
216
+ indicator === "dashed",
217
+ "my-0.5": nestLabel && indicator === "dashed",
218
+ }
219
+ )}
220
+ style={
221
+ {
222
+ "--color-bg": indicatorColor,
223
+ "--color-border": indicatorColor,
224
+ } as React.CSSProperties
225
+ }
226
+ />
227
+ )
228
+ )}
229
+ <div
230
+ className={cn(
231
+ "flex flex-1 justify-between leading-none",
232
+ nestLabel ? "items-end" : "items-center"
233
+ )}
234
+ >
235
+ <div className="grid gap-1.5">
236
+ {nestLabel ? tooltipLabel : null}
237
+ <span className="text-muted-foreground">
238
+ {itemConfig?.label || item.name}
239
+ </span>
240
+ </div>
241
+ {item.value && (
242
+ <span className="font-mono font-medium tabular-nums text-foreground">
243
+ {item.value.toLocaleString()}
244
+ </span>
245
+ )}
246
+ </div>
247
+ </>
248
+ )}
249
+ </div>
250
+ )
251
+ })}
252
+ </div>
253
+ </div>
254
+ )
255
+ }
256
+ )
257
+ ChartTooltipContent.displayName = "ChartTooltip"
258
+
259
+ const ChartLegend = RechartsPrimitive.Legend
260
+
261
+ const ChartLegendContent = React.forwardRef<
262
+ HTMLDivElement,
263
+ React.ComponentProps<"div"> &
264
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
265
+ hideIcon?: boolean
266
+ nameKey?: string
267
+ }
268
+ >(
269
+ (
270
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
271
+ ref
272
+ ) => {
273
+ const { config } = useChart()
274
+
275
+ if (!payload?.length) {
276
+ return null
277
+ }
278
+
279
+ return (
280
+ <div
281
+ ref={ref}
282
+ className={cn(
283
+ "flex items-center justify-center gap-4",
284
+ verticalAlign === "top" ? "pb-3" : "pt-3",
285
+ className
286
+ )}
287
+ >
288
+ {payload.map((item) => {
289
+ const key = `${nameKey || item.dataKey || "value"}`
290
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
291
+
292
+ return (
293
+ <div
294
+ key={item.value}
295
+ className={cn(
296
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
297
+ )}
298
+ >
299
+ {itemConfig?.icon && !hideIcon ? (
300
+ <itemConfig.icon />
301
+ ) : (
302
+ <div
303
+ className="h-2 w-2 shrink-0 rounded-[2px]"
304
+ style={{
305
+ backgroundColor: item.color,
306
+ }}
307
+ />
308
+ )}
309
+ {itemConfig?.label}
310
+ </div>
311
+ )
312
+ })}
313
+ </div>
314
+ )
315
+ }
316
+ )
317
+ ChartLegendContent.displayName = "ChartLegend"
318
+
319
+ // Helper to extract item config from a payload.
320
+ function getPayloadConfigFromPayload(
321
+ config: ChartConfig,
322
+ payload: unknown,
323
+ key: string
324
+ ) {
325
+ if (typeof payload !== "object" || payload === null) {
326
+ return undefined
327
+ }
328
+
329
+ const payloadPayload =
330
+ "payload" in payload &&
331
+ typeof payload.payload === "object" &&
332
+ payload.payload !== null
333
+ ? payload.payload
334
+ : undefined
335
+
336
+ let configLabelKey: string = key
337
+
338
+ if (
339
+ key in payload &&
340
+ typeof payload[key as keyof typeof payload] === "string"
341
+ ) {
342
+ configLabelKey = payload[key as keyof typeof payload] as string
343
+ } else if (
344
+ payloadPayload &&
345
+ key in payloadPayload &&
346
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
347
+ ) {
348
+ configLabelKey = payloadPayload[
349
+ key as keyof typeof payloadPayload
350
+ ] as string
351
+ }
352
+
353
+ return configLabelKey in config
354
+ ? config[configLabelKey]
355
+ : config[key as keyof typeof config]
356
+ }
357
+
358
+ export {
359
+ ChartContainer,
360
+ ChartTooltip,
361
+ ChartTooltipContent,
362
+ ChartLegend,
363
+ ChartLegendContent,
364
+ ChartStyle,
365
+ }
client/src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("flex items-center justify-center text-current")}
21
+ >
22
+ <Check className="h-4 w-4" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ))
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
27
+
28
+ export { Checkbox }
client/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10
+
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
client/src/components/ui/command.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { type DialogProps } from "@radix-ui/react-dialog"
3
+ import { Command as CommandPrimitive } from "cmdk"
4
+ import { Search } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
8
+
9
+ const Command = React.forwardRef<
10
+ React.ElementRef<typeof CommandPrimitive>,
11
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
12
+ >(({ className, ...props }, ref) => (
13
+ <CommandPrimitive
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ ))
22
+ Command.displayName = CommandPrimitive.displayName
23
+
24
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
25
+ return (
26
+ <Dialog {...props}>
27
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
28
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
29
+ {children}
30
+ </Command>
31
+ </DialogContent>
32
+ </Dialog>
33
+ )
34
+ }
35
+
36
+ const CommandInput = React.forwardRef<
37
+ React.ElementRef<typeof CommandPrimitive.Input>,
38
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
39
+ >(({ className, ...props }, ref) => (
40
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
41
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
42
+ <CommandPrimitive.Input
43
+ ref={ref}
44
+ className={cn(
45
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </div>
51
+ ))
52
+
53
+ CommandInput.displayName = CommandPrimitive.Input.displayName
54
+
55
+ const CommandList = React.forwardRef<
56
+ React.ElementRef<typeof CommandPrimitive.List>,
57
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
58
+ >(({ className, ...props }, ref) => (
59
+ <CommandPrimitive.List
60
+ ref={ref}
61
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
62
+ {...props}
63
+ />
64
+ ))
65
+
66
+ CommandList.displayName = CommandPrimitive.List.displayName
67
+
68
+ const CommandEmpty = React.forwardRef<
69
+ React.ElementRef<typeof CommandPrimitive.Empty>,
70
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
71
+ >((props, ref) => (
72
+ <CommandPrimitive.Empty
73
+ ref={ref}
74
+ className="py-6 text-center text-sm"
75
+ {...props}
76
+ />
77
+ ))
78
+
79
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
80
+
81
+ const CommandGroup = React.forwardRef<
82
+ React.ElementRef<typeof CommandPrimitive.Group>,
83
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
84
+ >(({ className, ...props }, ref) => (
85
+ <CommandPrimitive.Group
86
+ ref={ref}
87
+ className={cn(
88
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ ))
94
+
95
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
96
+
97
+ const CommandSeparator = React.forwardRef<
98
+ React.ElementRef<typeof CommandPrimitive.Separator>,
99
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
100
+ >(({ className, ...props }, ref) => (
101
+ <CommandPrimitive.Separator
102
+ ref={ref}
103
+ className={cn("-mx-1 h-px bg-border", className)}
104
+ {...props}
105
+ />
106
+ ))
107
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
108
+
109
+ const CommandItem = React.forwardRef<
110
+ React.ElementRef<typeof CommandPrimitive.Item>,
111
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
112
+ >(({ className, ...props }, ref) => (
113
+ <CommandPrimitive.Item
114
+ ref={ref}
115
+ className={cn(
116
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
117
+ className
118
+ )}
119
+ {...props}
120
+ />
121
+ ))
122
+
123
+ CommandItem.displayName = CommandPrimitive.Item.displayName
124
+
125
+ const CommandShortcut = ({
126
+ className,
127
+ ...props
128
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
129
+ return (
130
+ <span
131
+ className={cn(
132
+ "ml-auto text-xs tracking-widest text-muted-foreground",
133
+ className
134
+ )}
135
+ {...props}
136
+ />
137
+ )
138
+ }
139
+ CommandShortcut.displayName = "CommandShortcut"
140
+
141
+ export {
142
+ Command,
143
+ CommandDialog,
144
+ CommandInput,
145
+ CommandList,
146
+ CommandEmpty,
147
+ CommandGroup,
148
+ CommandItem,
149
+ CommandShortcut,
150
+ CommandSeparator,
151
+ }
client/src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ))
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ))
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
+ inset && "pl-8",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ ))
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
+ className
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ))
111
+ ContextMenuCheckboxItem.displayName =
112
+ ContextMenuPrimitive.CheckboxItem.displayName
113
+
114
+ const ContextMenuRadioItem = React.forwardRef<
115
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
116
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <ContextMenuPrimitive.RadioItem
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <ContextMenuPrimitive.ItemIndicator>
128
+ <Circle className="h-2 w-2 fill-current" />
129
+ </ContextMenuPrimitive.ItemIndicator>
130
+ </span>
131
+ {children}
132
+ </ContextMenuPrimitive.RadioItem>
133
+ ))
134
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
135
+
136
+ const ContextMenuLabel = React.forwardRef<
137
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
138
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
139
+ inset?: boolean
140
+ }
141
+ >(({ className, inset, ...props }, ref) => (
142
+ <ContextMenuPrimitive.Label
143
+ ref={ref}
144
+ className={cn(
145
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
146
+ inset && "pl-8",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ ))
152
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
153
+
154
+ const ContextMenuSeparator = React.forwardRef<
155
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
156
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
157
+ >(({ className, ...props }, ref) => (
158
+ <ContextMenuPrimitive.Separator
159
+ ref={ref}
160
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
161
+ {...props}
162
+ />
163
+ ))
164
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
165
+
166
+ const ContextMenuShortcut = ({
167
+ className,
168
+ ...props
169
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
170
+ return (
171
+ <span
172
+ className={cn(
173
+ "ml-auto text-xs tracking-widest text-muted-foreground",
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
181
+
182
+ export {
183
+ ContextMenu,
184
+ ContextMenuTrigger,
185
+ ContextMenuContent,
186
+ ContextMenuItem,
187
+ ContextMenuCheckboxItem,
188
+ ContextMenuRadioItem,
189
+ ContextMenuLabel,
190
+ ContextMenuSeparator,
191
+ ContextMenuShortcut,
192
+ ContextMenuGroup,
193
+ ContextMenuPortal,
194
+ ContextMenuSub,
195
+ ContextMenuSubContent,
196
+ ContextMenuSubTrigger,
197
+ ContextMenuRadioGroup,
198
+ }
client/src/components/ui/data-table.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
3
+
4
+ interface DataTableProps<TData> {
5
+ data: TData[];
6
+ columns: {
7
+ accessorKey: keyof TData | ((row: TData) => any);
8
+ header: string;
9
+ cell?: (row: TData) => React.ReactNode;
10
+ }[];
11
+ onRowClick?: (row: TData) => void;
12
+ className?: string;
13
+ }
14
+
15
+ export function DataTable<TData>({
16
+ data,
17
+ columns,
18
+ onRowClick,
19
+ className,
20
+ }: DataTableProps<TData>) {
21
+ return (
22
+ <div className={`w-full overflow-auto ${className}`}>
23
+ <Table>
24
+ <TableHeader>
25
+ <TableRow>
26
+ {columns.map((column, i) => (
27
+ <TableHead key={i} className="text-xs font-medium text-gray-500 uppercase tracking-wider">
28
+ {column.header}
29
+ </TableHead>
30
+ ))}
31
+ </TableRow>
32
+ </TableHeader>
33
+ <TableBody>
34
+ {data.length === 0 ? (
35
+ <TableRow>
36
+ <TableCell colSpan={columns.length} className="text-center py-6 text-sm text-gray-500">
37
+ No data available
38
+ </TableCell>
39
+ </TableRow>
40
+ ) : (
41
+ data.map((row, i) => (
42
+ <TableRow
43
+ key={i}
44
+ onClick={() => onRowClick?.(row)}
45
+ className={onRowClick ? "cursor-pointer hover:bg-gray-50" : ""}
46
+ >
47
+ {columns.map((column, j) => (
48
+ <TableCell key={j} className="whitespace-nowrap">
49
+ {column.cell ? (
50
+ column.cell(row)
51
+ ) : (
52
+ <div className="text-sm text-gray-900">
53
+ {typeof column.accessorKey === "function"
54
+ ? column.accessorKey(row)
55
+ : row[column.accessorKey] !== undefined
56
+ ? String(row[column.accessorKey])
57
+ : ""}
58
+ </div>
59
+ )}
60
+ </TableCell>
61
+ ))}
62
+ </TableRow>
63
+ ))
64
+ )}
65
+ </TableBody>
66
+ </Table>
67
+ </div>
68
+ );
69
+ }
client/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ ))
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <X className="h-4 w-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ))
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName
55
+
56
+ const DialogHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn(
62
+ "flex flex-col space-y-1.5 text-center sm:text-left",
63
+ className
64
+ )}
65
+ {...props}
66
+ />
67
+ )
68
+ DialogHeader.displayName = "DialogHeader"
69
+
70
+ const DialogFooter = ({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) => (
74
+ <div
75
+ className={cn(
76
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ DialogFooter.displayName = "DialogFooter"
83
+
84
+ const DialogTitle = React.forwardRef<
85
+ React.ElementRef<typeof DialogPrimitive.Title>,
86
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87
+ >(({ className, ...props }, ref) => (
88
+ <DialogPrimitive.Title
89
+ ref={ref}
90
+ className={cn(
91
+ "text-lg font-semibold leading-none tracking-tight",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
98
+
99
+ const DialogDescription = React.forwardRef<
100
+ React.ElementRef<typeof DialogPrimitive.Description>,
101
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102
+ >(({ className, ...props }, ref) => (
103
+ <DialogPrimitive.Description
104
+ ref={ref}
105
+ className={cn("text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
110
+
111
+ export {
112
+ Dialog,
113
+ DialogPortal,
114
+ DialogOverlay,
115
+ DialogClose,
116
+ DialogTrigger,
117
+ DialogContent,
118
+ DialogHeader,
119
+ DialogFooter,
120
+ DialogTitle,
121
+ DialogDescription,
122
+ }
client/src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Drawer as DrawerPrimitive } from "vaul"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Drawer = ({
9
+ shouldScaleBackground = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
12
+ <DrawerPrimitive.Root
13
+ shouldScaleBackground={shouldScaleBackground}
14
+ {...props}
15
+ />
16
+ )
17
+ Drawer.displayName = "Drawer"
18
+
19
+ const DrawerTrigger = DrawerPrimitive.Trigger
20
+
21
+ const DrawerPortal = DrawerPrimitive.Portal
22
+
23
+ const DrawerClose = DrawerPrimitive.Close
24
+
25
+ const DrawerOverlay = React.forwardRef<
26
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
27
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
28
+ >(({ className, ...props }, ref) => (
29
+ <DrawerPrimitive.Overlay
30
+ ref={ref}
31
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
32
+ {...props}
33
+ />
34
+ ))
35
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36
+
37
+ const DrawerContent = React.forwardRef<
38
+ React.ElementRef<typeof DrawerPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <DrawerPortal>
42
+ <DrawerOverlay />
43
+ <DrawerPrimitive.Content
44
+ ref={ref}
45
+ className={cn(
46
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
52
+ {children}
53
+ </DrawerPrimitive.Content>
54
+ </DrawerPortal>
55
+ ))
56
+ DrawerContent.displayName = "DrawerContent"
57
+
58
+ const DrawerHeader = ({
59
+ className,
60
+ ...props
61
+ }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div
63
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
64
+ {...props}
65
+ />
66
+ )
67
+ DrawerHeader.displayName = "DrawerHeader"
68
+
69
+ const DrawerFooter = ({
70
+ className,
71
+ ...props
72
+ }: React.HTMLAttributes<HTMLDivElement>) => (
73
+ <div
74
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
75
+ {...props}
76
+ />
77
+ )
78
+ DrawerFooter.displayName = "DrawerFooter"
79
+
80
+ const DrawerTitle = React.forwardRef<
81
+ React.ElementRef<typeof DrawerPrimitive.Title>,
82
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
83
+ >(({ className, ...props }, ref) => (
84
+ <DrawerPrimitive.Title
85
+ ref={ref}
86
+ className={cn(
87
+ "text-lg font-semibold leading-none tracking-tight",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ ))
93
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94
+
95
+ const DrawerDescription = React.forwardRef<
96
+ React.ElementRef<typeof DrawerPrimitive.Description>,
97
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
98
+ >(({ className, ...props }, ref) => (
99
+ <DrawerPrimitive.Description
100
+ ref={ref}
101
+ className={cn("text-sm text-muted-foreground", className)}
102
+ {...props}
103
+ />
104
+ ))
105
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106
+
107
+ export {
108
+ Drawer,
109
+ DrawerPortal,
110
+ DrawerOverlay,
111
+ DrawerTrigger,
112
+ DrawerClose,
113
+ DrawerContent,
114
+ DrawerHeader,
115
+ DrawerFooter,
116
+ DrawerTitle,
117
+ DrawerDescription,
118
+ }
client/src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const DropdownMenu = DropdownMenuPrimitive.Root
8
+
9
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
10
+
11
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
12
+
13
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14
+
15
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
16
+
17
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
18
+
19
+ const DropdownMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <DropdownMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto" />
36
+ </DropdownMenuPrimitive.SubTrigger>
37
+ ))
38
+ DropdownMenuSubTrigger.displayName =
39
+ DropdownMenuPrimitive.SubTrigger.displayName
40
+
41
+ const DropdownMenuSubContent = React.forwardRef<
42
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
43
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44
+ >(({ className, ...props }, ref) => (
45
+ <DropdownMenuPrimitive.SubContent
46
+ ref={ref}
47
+ className={cn(
48
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ ))
54
+ DropdownMenuSubContent.displayName =
55
+ DropdownMenuPrimitive.SubContent.displayName
56
+
57
+ const DropdownMenuContent = React.forwardRef<
58
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
+ >(({ className, sideOffset = 4, ...props }, ref) => (
61
+ <DropdownMenuPrimitive.Portal>
62
+ <DropdownMenuPrimitive.Content
63
+ ref={ref}
64
+ sideOffset={sideOffset}
65
+ className={cn(
66
+ "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ </DropdownMenuPrimitive.Portal>
72
+ ))
73
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
74
+
75
+ const DropdownMenuItem = React.forwardRef<
76
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
77
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
78
+ inset?: boolean
79
+ }
80
+ >(({ className, inset, ...props }, ref) => (
81
+ <DropdownMenuPrimitive.Item
82
+ ref={ref}
83
+ className={cn(
84
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
85
+ inset && "pl-8",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ ))
91
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
92
+
93
+ const DropdownMenuCheckboxItem = React.forwardRef<
94
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
95
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
96
+ >(({ className, children, checked, ...props }, ref) => (
97
+ <DropdownMenuPrimitive.CheckboxItem
98
+ ref={ref}
99
+ className={cn(
100
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
101
+ className
102
+ )}
103
+ checked={checked}
104
+ {...props}
105
+ >
106
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
107
+ <DropdownMenuPrimitive.ItemIndicator>
108
+ <Check className="h-4 w-4" />
109
+ </DropdownMenuPrimitive.ItemIndicator>
110
+ </span>
111
+ {children}
112
+ </DropdownMenuPrimitive.CheckboxItem>
113
+ ))
114
+ DropdownMenuCheckboxItem.displayName =
115
+ DropdownMenuPrimitive.CheckboxItem.displayName
116
+
117
+ const DropdownMenuRadioItem = React.forwardRef<
118
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120
+ >(({ className, children, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ ref={ref}
123
+ className={cn(
124
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125
+ className
126
+ )}
127
+ {...props}
128
+ >
129
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130
+ <DropdownMenuPrimitive.ItemIndicator>
131
+ <Circle className="h-2 w-2 fill-current" />
132
+ </DropdownMenuPrimitive.ItemIndicator>
133
+ </span>
134
+ {children}
135
+ </DropdownMenuPrimitive.RadioItem>
136
+ ))
137
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
138
+
139
+ const DropdownMenuLabel = React.forwardRef<
140
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142
+ inset?: boolean
143
+ }
144
+ >(({ className, inset, ...props }, ref) => (
145
+ <DropdownMenuPrimitive.Label
146
+ ref={ref}
147
+ className={cn(
148
+ "px-2 py-1.5 text-sm font-semibold",
149
+ inset && "pl-8",
150
+ className
151
+ )}
152
+ {...props}
153
+ />
154
+ ))
155
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
156
+
157
+ const DropdownMenuSeparator = React.forwardRef<
158
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
159
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
160
+ >(({ className, ...props }, ref) => (
161
+ <DropdownMenuPrimitive.Separator
162
+ ref={ref}
163
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
164
+ {...props}
165
+ />
166
+ ))
167
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
168
+
169
+ const DropdownMenuShortcut = ({
170
+ className,
171
+ ...props
172
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
173
+ return (
174
+ <span
175
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
181
+
182
+ export {
183
+ DropdownMenu,
184
+ DropdownMenuTrigger,
185
+ DropdownMenuContent,
186
+ DropdownMenuItem,
187
+ DropdownMenuCheckboxItem,
188
+ DropdownMenuRadioItem,
189
+ DropdownMenuLabel,
190
+ DropdownMenuSeparator,
191
+ DropdownMenuShortcut,
192
+ DropdownMenuGroup,
193
+ DropdownMenuPortal,
194
+ DropdownMenuSub,
195
+ DropdownMenuSubContent,
196
+ DropdownMenuSubTrigger,
197
+ DropdownMenuRadioGroup,
198
+ }
client/src/components/ui/form.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+ import { Slot } from "@radix-ui/react-slot"
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ type ControllerProps,
11
+ type FieldPath,
12
+ type FieldValues,
13
+ } from "react-hook-form"
14
+
15
+ import { cn } from "@/lib/utils"
16
+ import { Label } from "@/components/ui/label"
17
+
18
+ const Form = FormProvider
19
+
20
+ type FormFieldContextValue<
21
+ TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
23
+ > = {
24
+ name: TName
25
+ }
26
+
27
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
28
+ {} as FormFieldContextValue
29
+ )
30
+
31
+ const FormField = <
32
+ TFieldValues extends FieldValues = FieldValues,
33
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34
+ >({
35
+ ...props
36
+ }: ControllerProps<TFieldValues, TName>) => {
37
+ return (
38
+ <FormFieldContext.Provider value={{ name: props.name }}>
39
+ <Controller {...props} />
40
+ </FormFieldContext.Provider>
41
+ )
42
+ }
43
+
44
+ const useFormField = () => {
45
+ const fieldContext = React.useContext(FormFieldContext)
46
+ const itemContext = React.useContext(FormItemContext)
47
+ const { getFieldState, formState } = useFormContext()
48
+
49
+ const fieldState = getFieldState(fieldContext.name, formState)
50
+
51
+ if (!fieldContext) {
52
+ throw new Error("useFormField should be used within <FormField>")
53
+ }
54
+
55
+ const { id } = itemContext
56
+
57
+ return {
58
+ id,
59
+ name: fieldContext.name,
60
+ formItemId: `${id}-form-item`,
61
+ formDescriptionId: `${id}-form-item-description`,
62
+ formMessageId: `${id}-form-item-message`,
63
+ ...fieldState,
64
+ }
65
+ }
66
+
67
+ type FormItemContextValue = {
68
+ id: string
69
+ }
70
+
71
+ const FormItemContext = React.createContext<FormItemContextValue>(
72
+ {} as FormItemContextValue
73
+ )
74
+
75
+ const FormItem = React.forwardRef<
76
+ HTMLDivElement,
77
+ React.HTMLAttributes<HTMLDivElement>
78
+ >(({ className, ...props }, ref) => {
79
+ const id = React.useId()
80
+
81
+ return (
82
+ <FormItemContext.Provider value={{ id }}>
83
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
84
+ </FormItemContext.Provider>
85
+ )
86
+ })
87
+ FormItem.displayName = "FormItem"
88
+
89
+ const FormLabel = React.forwardRef<
90
+ React.ElementRef<typeof LabelPrimitive.Root>,
91
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92
+ >(({ className, ...props }, ref) => {
93
+ const { error, formItemId } = useFormField()
94
+
95
+ return (
96
+ <Label
97
+ ref={ref}
98
+ className={cn(error && "text-destructive", className)}
99
+ htmlFor={formItemId}
100
+ {...props}
101
+ />
102
+ )
103
+ })
104
+ FormLabel.displayName = "FormLabel"
105
+
106
+ const FormControl = React.forwardRef<
107
+ React.ElementRef<typeof Slot>,
108
+ React.ComponentPropsWithoutRef<typeof Slot>
109
+ >(({ ...props }, ref) => {
110
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111
+
112
+ return (
113
+ <Slot
114
+ ref={ref}
115
+ id={formItemId}
116
+ aria-describedby={
117
+ !error
118
+ ? `${formDescriptionId}`
119
+ : `${formDescriptionId} ${formMessageId}`
120
+ }
121
+ aria-invalid={!!error}
122
+ {...props}
123
+ />
124
+ )
125
+ })
126
+ FormControl.displayName = "FormControl"
127
+
128
+ const FormDescription = React.forwardRef<
129
+ HTMLParagraphElement,
130
+ React.HTMLAttributes<HTMLParagraphElement>
131
+ >(({ className, ...props }, ref) => {
132
+ const { formDescriptionId } = useFormField()
133
+
134
+ return (
135
+ <p
136
+ ref={ref}
137
+ id={formDescriptionId}
138
+ className={cn("text-sm text-muted-foreground", className)}
139
+ {...props}
140
+ />
141
+ )
142
+ })
143
+ FormDescription.displayName = "FormDescription"
144
+
145
+ const FormMessage = React.forwardRef<
146
+ HTMLParagraphElement,
147
+ React.HTMLAttributes<HTMLParagraphElement>
148
+ >(({ className, children, ...props }, ref) => {
149
+ const { error, formMessageId } = useFormField()
150
+ const body = error ? String(error?.message ?? "") : children
151
+
152
+ if (!body) {
153
+ return null
154
+ }
155
+
156
+ return (
157
+ <p
158
+ ref={ref}
159
+ id={formMessageId}
160
+ className={cn("text-sm font-medium text-destructive", className)}
161
+ {...props}
162
+ >
163
+ {body}
164
+ </p>
165
+ )
166
+ })
167
+ FormMessage.displayName = "FormMessage"
168
+
169
+ export {
170
+ useFormField,
171
+ Form,
172
+ FormItem,
173
+ FormLabel,
174
+ FormControl,
175
+ FormDescription,
176
+ FormMessage,
177
+ FormField,
178
+ }
client/src/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const HoverCard = HoverCardPrimitive.Root
9
+
10
+ const HoverCardTrigger = HoverCardPrimitive.Trigger
11
+
12
+ const HoverCardContent = React.forwardRef<
13
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
15
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16
+ <HoverCardPrimitive.Content
17
+ ref={ref}
18
+ align={align}
19
+ sideOffset={sideOffset}
20
+ className={cn(
21
+ "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ ))
27
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28
+
29
+ export { HoverCard, HoverCardTrigger, HoverCardContent }
client/src/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { OTPInput, OTPInputContext } from "input-otp"
3
+ import { Dot } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const InputOTP = React.forwardRef<
8
+ React.ElementRef<typeof OTPInput>,
9
+ React.ComponentPropsWithoutRef<typeof OTPInput>
10
+ >(({ className, containerClassName, ...props }, ref) => (
11
+ <OTPInput
12
+ ref={ref}
13
+ containerClassName={cn(
14
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
15
+ containerClassName
16
+ )}
17
+ className={cn("disabled:cursor-not-allowed", className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ InputOTP.displayName = "InputOTP"
22
+
23
+ const InputOTPGroup = React.forwardRef<
24
+ React.ElementRef<"div">,
25
+ React.ComponentPropsWithoutRef<"div">
26
+ >(({ className, ...props }, ref) => (
27
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
28
+ ))
29
+ InputOTPGroup.displayName = "InputOTPGroup"
30
+
31
+ const InputOTPSlot = React.forwardRef<
32
+ React.ElementRef<"div">,
33
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
34
+ >(({ index, className, ...props }, ref) => {
35
+ const inputOTPContext = React.useContext(OTPInputContext)
36
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ className={cn(
42
+ "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
43
+ isActive && "z-10 ring-2 ring-ring ring-offset-background",
44
+ className
45
+ )}
46
+ {...props}
47
+ >
48
+ {char}
49
+ {hasFakeCaret && (
50
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ )
56
+ })
57
+ InputOTPSlot.displayName = "InputOTPSlot"
58
+
59
+ const InputOTPSeparator = React.forwardRef<
60
+ React.ElementRef<"div">,
61
+ React.ComponentPropsWithoutRef<"div">
62
+ >(({ ...props }, ref) => (
63
+ <div ref={ref} role="separator" {...props}>
64
+ <Dot />
65
+ </div>
66
+ ))
67
+ InputOTPSeparator.displayName = "InputOTPSeparator"
68
+
69
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
client/src/components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+ )
20
+ Input.displayName = "Input"
21
+
22
+ export { Input }
client/src/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as LabelPrimitive from "@radix-ui/react-label"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const labelVariants = cva(
8
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9
+ )
10
+
11
+ const Label = React.forwardRef<
12
+ React.ElementRef<typeof LabelPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
14
+ VariantProps<typeof labelVariants>
15
+ >(({ className, ...props }, ref) => (
16
+ <LabelPrimitive.Root
17
+ ref={ref}
18
+ className={cn(labelVariants(), className)}
19
+ {...props}
20
+ />
21
+ ))
22
+ Label.displayName = LabelPrimitive.Root.displayName
23
+
24
+ export { Label }
client/src/components/ui/menubar.tsx ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function MenubarMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
12
+ return <MenubarPrimitive.Menu {...props} />
13
+ }
14
+
15
+ function MenubarGroup({
16
+ ...props
17
+ }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
18
+ return <MenubarPrimitive.Group {...props} />
19
+ }
20
+
21
+ function MenubarPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
24
+ return <MenubarPrimitive.Portal {...props} />
25
+ }
26
+
27
+ function MenubarRadioGroup({
28
+ ...props
29
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
30
+ return <MenubarPrimitive.RadioGroup {...props} />
31
+ }
32
+
33
+ function MenubarSub({
34
+ ...props
35
+ }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
36
+ return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
37
+ }
38
+
39
+ const Menubar = React.forwardRef<
40
+ React.ElementRef<typeof MenubarPrimitive.Root>,
41
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
42
+ >(({ className, ...props }, ref) => (
43
+ <MenubarPrimitive.Root
44
+ ref={ref}
45
+ className={cn(
46
+ "flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ ))
52
+ Menubar.displayName = MenubarPrimitive.Root.displayName
53
+
54
+ const MenubarTrigger = React.forwardRef<
55
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
56
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
57
+ >(({ className, ...props }, ref) => (
58
+ <MenubarPrimitive.Trigger
59
+ ref={ref}
60
+ className={cn(
61
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ ))
67
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
68
+
69
+ const MenubarSubTrigger = React.forwardRef<
70
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
71
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
72
+ inset?: boolean
73
+ }
74
+ >(({ className, inset, children, ...props }, ref) => (
75
+ <MenubarPrimitive.SubTrigger
76
+ ref={ref}
77
+ className={cn(
78
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
79
+ inset && "pl-8",
80
+ className
81
+ )}
82
+ {...props}
83
+ >
84
+ {children}
85
+ <ChevronRight className="ml-auto h-4 w-4" />
86
+ </MenubarPrimitive.SubTrigger>
87
+ ))
88
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
89
+
90
+ const MenubarSubContent = React.forwardRef<
91
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
92
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
93
+ >(({ className, ...props }, ref) => (
94
+ <MenubarPrimitive.SubContent
95
+ ref={ref}
96
+ className={cn(
97
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ ))
103
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
104
+
105
+ const MenubarContent = React.forwardRef<
106
+ React.ElementRef<typeof MenubarPrimitive.Content>,
107
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
108
+ >(
109
+ (
110
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
111
+ ref
112
+ ) => (
113
+ <MenubarPrimitive.Portal>
114
+ <MenubarPrimitive.Content
115
+ ref={ref}
116
+ align={align}
117
+ alignOffset={alignOffset}
118
+ sideOffset={sideOffset}
119
+ className={cn(
120
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ </MenubarPrimitive.Portal>
126
+ )
127
+ )
128
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName
129
+
130
+ const MenubarItem = React.forwardRef<
131
+ React.ElementRef<typeof MenubarPrimitive.Item>,
132
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
133
+ inset?: boolean
134
+ }
135
+ >(({ className, inset, ...props }, ref) => (
136
+ <MenubarPrimitive.Item
137
+ ref={ref}
138
+ className={cn(
139
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
140
+ inset && "pl-8",
141
+ className
142
+ )}
143
+ {...props}
144
+ />
145
+ ))
146
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName
147
+
148
+ const MenubarCheckboxItem = React.forwardRef<
149
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
150
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
151
+ >(({ className, children, checked, ...props }, ref) => (
152
+ <MenubarPrimitive.CheckboxItem
153
+ ref={ref}
154
+ className={cn(
155
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
156
+ className
157
+ )}
158
+ checked={checked}
159
+ {...props}
160
+ >
161
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
162
+ <MenubarPrimitive.ItemIndicator>
163
+ <Check className="h-4 w-4" />
164
+ </MenubarPrimitive.ItemIndicator>
165
+ </span>
166
+ {children}
167
+ </MenubarPrimitive.CheckboxItem>
168
+ ))
169
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
170
+
171
+ const MenubarRadioItem = React.forwardRef<
172
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
173
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
174
+ >(({ className, children, ...props }, ref) => (
175
+ <MenubarPrimitive.RadioItem
176
+ ref={ref}
177
+ className={cn(
178
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
179
+ className
180
+ )}
181
+ {...props}
182
+ >
183
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
184
+ <MenubarPrimitive.ItemIndicator>
185
+ <Circle className="h-2 w-2 fill-current" />
186
+ </MenubarPrimitive.ItemIndicator>
187
+ </span>
188
+ {children}
189
+ </MenubarPrimitive.RadioItem>
190
+ ))
191
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
192
+
193
+ const MenubarLabel = React.forwardRef<
194
+ React.ElementRef<typeof MenubarPrimitive.Label>,
195
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
196
+ inset?: boolean
197
+ }
198
+ >(({ className, inset, ...props }, ref) => (
199
+ <MenubarPrimitive.Label
200
+ ref={ref}
201
+ className={cn(
202
+ "px-2 py-1.5 text-sm font-semibold",
203
+ inset && "pl-8",
204
+ className
205
+ )}
206
+ {...props}
207
+ />
208
+ ))
209
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName
210
+
211
+ const MenubarSeparator = React.forwardRef<
212
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
213
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
214
+ >(({ className, ...props }, ref) => (
215
+ <MenubarPrimitive.Separator
216
+ ref={ref}
217
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
218
+ {...props}
219
+ />
220
+ ))
221
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
222
+
223
+ const MenubarShortcut = ({
224
+ className,
225
+ ...props
226
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
227
+ return (
228
+ <span
229
+ className={cn(
230
+ "ml-auto text-xs tracking-widest text-muted-foreground",
231
+ className
232
+ )}
233
+ {...props}
234
+ />
235
+ )
236
+ }
237
+ MenubarShortcut.displayname = "MenubarShortcut"
238
+
239
+ export {
240
+ Menubar,
241
+ MenubarMenu,
242
+ MenubarTrigger,
243
+ MenubarContent,
244
+ MenubarItem,
245
+ MenubarSeparator,
246
+ MenubarLabel,
247
+ MenubarCheckboxItem,
248
+ MenubarRadioGroup,
249
+ MenubarRadioItem,
250
+ MenubarPortal,
251
+ MenubarSubContent,
252
+ MenubarSubTrigger,
253
+ MenubarGroup,
254
+ MenubarSub,
255
+ MenubarShortcut,
256
+ }
client/src/components/ui/navigation-menu.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3
+ import { cva } from "class-variance-authority"
4
+ import { ChevronDown } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const NavigationMenu = React.forwardRef<
9
+ React.ElementRef<typeof NavigationMenuPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
11
+ >(({ className, children, ...props }, ref) => (
12
+ <NavigationMenuPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative z-10 flex max-w-max flex-1 items-center justify-center",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ {children}
21
+ <NavigationMenuViewport />
22
+ </NavigationMenuPrimitive.Root>
23
+ ))
24
+ NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25
+
26
+ const NavigationMenuList = React.forwardRef<
27
+ React.ElementRef<typeof NavigationMenuPrimitive.List>,
28
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
29
+ >(({ className, ...props }, ref) => (
30
+ <NavigationMenuPrimitive.List
31
+ ref={ref}
32
+ className={cn(
33
+ "group flex flex-1 list-none items-center justify-center space-x-1",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ ))
39
+ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40
+
41
+ const NavigationMenuItem = NavigationMenuPrimitive.Item
42
+
43
+ const navigationMenuTriggerStyle = cva(
44
+ "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
45
+ )
46
+
47
+ const NavigationMenuTrigger = React.forwardRef<
48
+ React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
49
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
50
+ >(({ className, children, ...props }, ref) => (
51
+ <NavigationMenuPrimitive.Trigger
52
+ ref={ref}
53
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
54
+ {...props}
55
+ >
56
+ {children}{" "}
57
+ <ChevronDown
58
+ className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
59
+ aria-hidden="true"
60
+ />
61
+ </NavigationMenuPrimitive.Trigger>
62
+ ))
63
+ NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64
+
65
+ const NavigationMenuContent = React.forwardRef<
66
+ React.ElementRef<typeof NavigationMenuPrimitive.Content>,
67
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
68
+ >(({ className, ...props }, ref) => (
69
+ <NavigationMenuPrimitive.Content
70
+ ref={ref}
71
+ className={cn(
72
+ "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ ))
78
+ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79
+
80
+ const NavigationMenuLink = NavigationMenuPrimitive.Link
81
+
82
+ const NavigationMenuViewport = React.forwardRef<
83
+ React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
84
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
85
+ >(({ className, ...props }, ref) => (
86
+ <div className={cn("absolute left-0 top-full flex justify-center")}>
87
+ <NavigationMenuPrimitive.Viewport
88
+ className={cn(
89
+ "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
90
+ className
91
+ )}
92
+ ref={ref}
93
+ {...props}
94
+ />
95
+ </div>
96
+ ))
97
+ NavigationMenuViewport.displayName =
98
+ NavigationMenuPrimitive.Viewport.displayName
99
+
100
+ const NavigationMenuIndicator = React.forwardRef<
101
+ React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
102
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
103
+ >(({ className, ...props }, ref) => (
104
+ <NavigationMenuPrimitive.Indicator
105
+ ref={ref}
106
+ className={cn(
107
+ "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
108
+ className
109
+ )}
110
+ {...props}
111
+ >
112
+ <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
113
+ </NavigationMenuPrimitive.Indicator>
114
+ ))
115
+ NavigationMenuIndicator.displayName =
116
+ NavigationMenuPrimitive.Indicator.displayName
117
+
118
+ export {
119
+ navigationMenuTriggerStyle,
120
+ NavigationMenu,
121
+ NavigationMenuList,
122
+ NavigationMenuItem,
123
+ NavigationMenuContent,
124
+ NavigationMenuTrigger,
125
+ NavigationMenuLink,
126
+ NavigationMenuIndicator,
127
+ NavigationMenuViewport,
128
+ }
client/src/components/ui/pagination.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { ButtonProps, buttonVariants } from "@/components/ui/button"
6
+
7
+ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8
+ <nav
9
+ role="navigation"
10
+ aria-label="pagination"
11
+ className={cn("mx-auto flex w-full justify-center", className)}
12
+ {...props}
13
+ />
14
+ )
15
+ Pagination.displayName = "Pagination"
16
+
17
+ const PaginationContent = React.forwardRef<
18
+ HTMLUListElement,
19
+ React.ComponentProps<"ul">
20
+ >(({ className, ...props }, ref) => (
21
+ <ul
22
+ ref={ref}
23
+ className={cn("flex flex-row items-center gap-1", className)}
24
+ {...props}
25
+ />
26
+ ))
27
+ PaginationContent.displayName = "PaginationContent"
28
+
29
+ const PaginationItem = React.forwardRef<
30
+ HTMLLIElement,
31
+ React.ComponentProps<"li">
32
+ >(({ className, ...props }, ref) => (
33
+ <li ref={ref} className={cn("", className)} {...props} />
34
+ ))
35
+ PaginationItem.displayName = "PaginationItem"
36
+
37
+ type PaginationLinkProps = {
38
+ isActive?: boolean
39
+ } & Pick<ButtonProps, "size"> &
40
+ React.ComponentProps<"a">
41
+
42
+ const PaginationLink = ({
43
+ className,
44
+ isActive,
45
+ size = "icon",
46
+ ...props
47
+ }: PaginationLinkProps) => (
48
+ <a
49
+ aria-current={isActive ? "page" : undefined}
50
+ className={cn(
51
+ buttonVariants({
52
+ variant: isActive ? "outline" : "ghost",
53
+ size,
54
+ }),
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ )
60
+ PaginationLink.displayName = "PaginationLink"
61
+
62
+ const PaginationPrevious = ({
63
+ className,
64
+ ...props
65
+ }: React.ComponentProps<typeof PaginationLink>) => (
66
+ <PaginationLink
67
+ aria-label="Go to previous page"
68
+ size="default"
69
+ className={cn("gap-1 pl-2.5", className)}
70
+ {...props}
71
+ >
72
+ <ChevronLeft className="h-4 w-4" />
73
+ <span>Previous</span>
74
+ </PaginationLink>
75
+ )
76
+ PaginationPrevious.displayName = "PaginationPrevious"
77
+
78
+ const PaginationNext = ({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof PaginationLink>) => (
82
+ <PaginationLink
83
+ aria-label="Go to next page"
84
+ size="default"
85
+ className={cn("gap-1 pr-2.5", className)}
86
+ {...props}
87
+ >
88
+ <span>Next</span>
89
+ <ChevronRight className="h-4 w-4" />
90
+ </PaginationLink>
91
+ )
92
+ PaginationNext.displayName = "PaginationNext"
93
+
94
+ const PaginationEllipsis = ({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<"span">) => (
98
+ <span
99
+ aria-hidden
100
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
101
+ {...props}
102
+ >
103
+ <MoreHorizontal className="h-4 w-4" />
104
+ <span className="sr-only">More pages</span>
105
+ </span>
106
+ )
107
+ PaginationEllipsis.displayName = "PaginationEllipsis"
108
+
109
+ export {
110
+ Pagination,
111
+ PaginationContent,
112
+ PaginationEllipsis,
113
+ PaginationItem,
114
+ PaginationLink,
115
+ PaginationNext,
116
+ PaginationPrevious,
117
+ }