Initial import
This commit is contained in:
commit
725af8a3ea
30
Migdalor-main/.gitignore
vendored
Normal file
30
Migdalor-main/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
#env
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
1
Migdalor-main/README.md
Normal file
1
Migdalor-main/README.md
Normal file
@ -0,0 +1 @@
|
||||
Migdalor
|
||||
21
Migdalor-main/components.json
Normal file
21
Migdalor-main/components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
60
Migdalor-main/eslint.config.js
Normal file
60
Migdalor-main/eslint.config.js
Normal file
@ -0,0 +1,60 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
||||
|
||||
export default [
|
||||
{
|
||||
files: [
|
||||
"src/components/**/*.{js,mjs,cjs,jsx}",
|
||||
"src/pages/**/*.{js,mjs,cjs,jsx}",
|
||||
"src/Layout.jsx",
|
||||
],
|
||||
ignores: ["src/lib/**/*", "src/components/ui/**/*"],
|
||||
...pluginJs.configs.recommended,
|
||||
...pluginReact.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
react: pluginReact,
|
||||
"react-hooks": pluginReactHooks,
|
||||
"unused-imports": pluginUnusedImports,
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"react/jsx-uses-react": "error",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
varsIgnorePattern: "^_",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/no-unknown-property": [
|
||||
"error",
|
||||
{ ignore: ["cmdk-input-wrapper", "toast-close"] },
|
||||
],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
14
Migdalor-main/index.html
Normal file
14
Migdalor-main/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="https://base44.com/logo_v2.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Base44 APP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
Migdalor-main/jsconfig.json
Normal file
21
Migdalor-main/jsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["esnext", "dom"],
|
||||
"target": "esnext",
|
||||
"checkJs": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/components/**/*.js", "src/pages/**/*.jsx", "src/Layout.jsx"],
|
||||
"exclude": ["node_modules", "dist", "src/vite-plugins", "src/components/ui", "src/api", "src/lib"]
|
||||
}
|
||||
10133
Migdalor-main/package-lock.json
generated
Normal file
10133
Migdalor-main/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
100
Migdalor-main/package.json
Normal file
100
Migdalor-main/package.json
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "base44-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "tsc -p ./jsconfig.json",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base44/sdk": "^0.8.3",
|
||||
"@base44/vite-plugin": "^0.2.9",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@stripe/react-stripe-js": "^3.0.0",
|
||||
"@stripe/stripe-js": "^5.2.0",
|
||||
"@tanstack/react-query": "^5.84.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"framer-motion": "^11.16.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"jspdf": "^2.5.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.475.0",
|
||||
"moment": "^2.30.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.171.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"baseline-browser-mapping": "^2.8.32",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
6
Migdalor-main/postcss.config.js
Normal file
6
Migdalor-main/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
0
Migdalor-main/src/App.css
Normal file
0
Migdalor-main/src/App.css
Normal file
85
Migdalor-main/src/App.jsx
Normal file
85
Migdalor-main/src/App.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
import './App.css'
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { queryClientInstance } from '@/lib/query-client'
|
||||
import VisualEditAgent from '@/lib/VisualEditAgent'
|
||||
import NavigationTracker from '@/lib/NavigationTracker'
|
||||
import { pagesConfig } from './pages.config'
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import PageNotFound from './lib/PageNotFound';
|
||||
import { AuthProvider, useAuth } from '@/lib/AuthContext';
|
||||
import UserNotRegisteredError from '@/components/UserNotRegisteredError';
|
||||
|
||||
const { Pages, Layout, mainPage } = pagesConfig;
|
||||
const mainPageKey = mainPage ?? Object.keys(Pages)[0];
|
||||
const MainPage = mainPageKey ? Pages[mainPageKey] : <></>;
|
||||
|
||||
const LayoutWrapper = ({ children, currentPageName }) => Layout ?
|
||||
<Layout currentPageName={currentPageName}>{children}</Layout>
|
||||
: <>{children}</>;
|
||||
|
||||
const AuthenticatedApp = () => {
|
||||
const { isLoadingAuth, isLoadingPublicSettings, authError, isAuthenticated, navigateToLogin } = useAuth();
|
||||
|
||||
// Show loading spinner while checking app public settings or auth
|
||||
if (isLoadingPublicSettings || isLoadingAuth) {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-4 border-slate-200 border-t-slate-800 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
if (authError) {
|
||||
if (authError.type === 'user_not_registered') {
|
||||
return <UserNotRegisteredError />;
|
||||
} else if (authError.type === 'auth_required') {
|
||||
// Redirect to login automatically
|
||||
navigateToLogin();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the main app
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
<LayoutWrapper currentPageName={mainPageKey}>
|
||||
<MainPage />
|
||||
</LayoutWrapper>
|
||||
} />
|
||||
{Object.entries(Pages).map(([path, Page]) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={`/${path}`}
|
||||
element={
|
||||
<LayoutWrapper currentPageName={path}>
|
||||
<Page />
|
||||
</LayoutWrapper>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClientInstance}>
|
||||
<Router>
|
||||
<NavigationTracker />
|
||||
<AuthenticatedApp />
|
||||
</Router>
|
||||
<Toaster />
|
||||
<VisualEditAgent />
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
293
Migdalor-main/src/Layout.jsx
Normal file
293
Migdalor-main/src/Layout.jsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createPageUrl } from './utils';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { Key, Users, Settings, LayoutDashboard, Calendar, Target, LogOut, ChevronDown, Shield, Briefcase, MapPin, Image, Database } from 'lucide-react';
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export default function Layout({ children, currentPageName }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userPermissions, setUserPermissions] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const user = await base44.auth.me();
|
||||
setUser(user);
|
||||
|
||||
// Redirect to onboarding if not completed
|
||||
if (!user.onboarding_completed && currentPageName !== 'Onboarding') {
|
||||
window.location.href = createPageUrl('Onboarding');
|
||||
}
|
||||
|
||||
// Load user permissions based on positions
|
||||
if (user.positions && user.positions.length > 0) {
|
||||
const allPermissions = await base44.entities.PositionPermission.list();
|
||||
console.log('🔍 All permissions:', allPermissions);
|
||||
console.log('👤 User positions:', user.positions);
|
||||
|
||||
const userPositionPerms = allPermissions.filter(p =>
|
||||
user.positions.includes(p.position_name)
|
||||
);
|
||||
console.log('✅ Matched permissions:', userPositionPerms);
|
||||
|
||||
// Merge all permissions
|
||||
const mergedPerms = {
|
||||
has_classroom_management_access: userPositionPerms.some(p => p.has_classroom_management_access),
|
||||
pages_access: [...new Set(userPositionPerms.flatMap(p => p.pages_access || []))]
|
||||
};
|
||||
console.log('📋 Final permissions:', mergedPerms);
|
||||
|
||||
setUserPermissions(mergedPerms);
|
||||
} else {
|
||||
console.log('⚠️ No positions found for user');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserData();
|
||||
}, [currentPageName]);
|
||||
|
||||
// Show loading while checking onboarding status
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-800"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Hide navigation for onboarding and MyProfile pages
|
||||
if (currentPageName === 'Onboarding' || currentPageName === 'MyProfile') {
|
||||
return <div className="min-h-screen bg-slate-50">{children}</div>;
|
||||
}
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
// Check if we're in the "User Management" area
|
||||
const isUserManagementArea = currentPageName === 'ManageUsers' || currentPageName === 'ManagePermissions' || currentPageName === 'ManagePositions';
|
||||
const isHackalonArea = ['HackalonSchedule', 'HackalonOverview', 'HackalonAssignment', 'HackalonTeamArea', 'HackalonManageProblems', 'HackalonStatus'].includes(currentPageName);
|
||||
|
||||
// Redirect to HackalonSchedule when clicking on any Hackalon nav item if not already in Hackalon area
|
||||
if (isHackalonArea && currentPageName !== 'HackalonSchedule' && !window.location.search) {
|
||||
// This allows direct navigation to specific Hackalon pages via URL
|
||||
}
|
||||
|
||||
// Classroom Management Navigation
|
||||
const allNavItems = [
|
||||
{ name: 'לוח בקרה', icon: LayoutDashboard, page: 'Dashboard', tooltip: 'לוח בקרה' },
|
||||
{ name: 'תמונת מצב', icon: Image, page: 'DailyOverview', tooltip: 'תמונת מצב' },
|
||||
{ name: 'לוח הזמנים שלי', icon: Calendar, page: 'MySchedule', tooltip: 'לוח זמנים' },
|
||||
{ name: 'הקצאת מפתחות', icon: Target, page: 'KeyAllocation', tooltip: 'הקצאה' },
|
||||
{ name: 'מפתחות', icon: Key, page: 'ManageKeys', tooltip: 'מפתחות' },
|
||||
];
|
||||
|
||||
// Filter nav items based on permissions
|
||||
const getFilteredNavItems = () => {
|
||||
if (isAdmin) {
|
||||
return allNavItems;
|
||||
}
|
||||
|
||||
if (!userPermissions?.has_classroom_management_access) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If user has classroom management access, show all non-admin items
|
||||
// Or filter by specific pages if pages_access is defined and not empty
|
||||
if (userPermissions.pages_access && userPermissions.pages_access.length > 0) {
|
||||
return allNavItems.filter(item => {
|
||||
if (item.adminOnly) return false;
|
||||
return userPermissions.pages_access.includes(item.page);
|
||||
});
|
||||
} else {
|
||||
// Show all non-admin items if pages_access is not specified
|
||||
return allNavItems.filter(item => !item.adminOnly);
|
||||
}
|
||||
};
|
||||
|
||||
const adminNavItems = isAdmin ? allNavItems : [];
|
||||
const userNavItems = getFilteredNavItems();
|
||||
|
||||
// User Management Navigation (for admins only)
|
||||
const userManagementNavItems = [
|
||||
{ name: 'משתמשים', icon: Users, page: 'ManageUsers', tooltip: 'ניהול משתמשים' },
|
||||
{ name: 'תפקידים', icon: Briefcase, page: 'ManagePositions', tooltip: 'ניהול תפקידים' },
|
||||
{ name: 'הרשאות', icon: Shield, page: 'ManagePermissions', tooltip: 'ניהול הרשאות תפקידים' },
|
||||
];
|
||||
|
||||
// HackAlon Navigation - Default page is HackalonSchedule
|
||||
const allHackalonNavItems = [
|
||||
{ name: 'לוח זמנים', icon: Calendar, page: 'HackalonSchedule', tooltip: 'לוח זמנים ואירועים' },
|
||||
{ name: 'אזור הצוות', icon: Users, page: 'HackalonTeamArea', tooltip: 'אזור הצוות שלי' },
|
||||
{ name: 'סקירה', icon: LayoutDashboard, page: 'HackalonOverview', tooltip: 'סקירה כללית' },
|
||||
{ name: 'תמונת מצב', icon: Image, page: 'HackalonStatus', tooltip: 'מעקב אחר העלאות'},
|
||||
{ name: 'שיבוץ צוערים', icon: Users, page: 'HackalonAssignment', tooltip: 'שיבוץ למדורים וצוותים', adminOnly: true },
|
||||
{ name: 'ניהול בעיות', icon: Settings, page: 'HackalonManageProblems', tooltip: 'הגדרת בעיות לצוותים', adminOnly: true },
|
||||
];
|
||||
|
||||
// Filter HackAlon nav items based on permissions
|
||||
const getFilteredHackalonItems = () => {
|
||||
if (isAdmin) {
|
||||
return allHackalonNavItems;
|
||||
}
|
||||
|
||||
const allowedPositions = ['מפק״ץ', 'מנהל האקתון'];
|
||||
const hasAllowedPosition = user?.positions?.some(pos => allowedPositions.includes(pos));
|
||||
|
||||
return allHackalonNavItems.filter(item => {
|
||||
// Always show non-admin items
|
||||
if (!item.adminOnly) {
|
||||
// For Overview and Status pages, check special permissions
|
||||
if (item.page === 'HackalonOverview' || item.page === 'HackalonStatus') {
|
||||
return hasAllowedPosition;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// For admin-only items, check permissions
|
||||
if (!userPermissions?.pages_access) return false;
|
||||
return userPermissions.pages_access.includes(item.page);
|
||||
});
|
||||
};
|
||||
|
||||
const hackalonNavItems = getFilteredHackalonItems();
|
||||
|
||||
const allManagementItems = [
|
||||
{ name: 'פלוגות', page: 'ManageCrews', icon: Shield },
|
||||
{ name: 'צוותים', page: 'ManageSquads', icon: Users },
|
||||
{ name: 'אזורים', page: 'ManageZones', icon: MapPin, adminOnly: true },
|
||||
];
|
||||
|
||||
// Filter management items based on permissions
|
||||
const managementItems = isAdmin
|
||||
? allManagementItems
|
||||
: allManagementItems.filter(item => {
|
||||
if (item.adminOnly) return false;
|
||||
if (!userPermissions?.has_classroom_management_access) return false;
|
||||
// If pages_access exists and has items, filter by it; otherwise show all non-admin items
|
||||
if (userPermissions.pages_access && userPermissions.pages_access.length > 0) {
|
||||
return userPermissions.pages_access.includes(item.page);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const managementPages = managementItems.map(item => item.page);
|
||||
|
||||
const navItems = isUserManagementArea ? userManagementNavItems :
|
||||
isHackalonArea ? hackalonNavItems :
|
||||
(isAdmin ? adminNavItems : userNavItems);
|
||||
const isManagementPage = managementPages.includes(currentPageName);
|
||||
|
||||
// Show management dropdown only in classroom area
|
||||
const showManagementDropdown = !isUserManagementArea && !isHackalonArea;
|
||||
|
||||
// Hide navigation for Home page
|
||||
if (currentPageName === 'Home') {
|
||||
return <div className="min-h-screen bg-slate-50">{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50" dir="rtl">
|
||||
{/* Top Navigation */}
|
||||
<nav className="bg-white border-b border-slate-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-start h-16">
|
||||
{/* Logo */}
|
||||
<Link to={createPageUrl('Home')} className="flex items-center gap-3">
|
||||
<img
|
||||
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/693b00a201212578d09f8396/9732960ed_8.png"
|
||||
alt="מגדלור לוגו"
|
||||
className="w-12 h-12 object-contain"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Nav Links */}
|
||||
<div className="flex items-center gap-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentPageName === item.page;
|
||||
return (
|
||||
<Link
|
||||
key={item.page}
|
||||
to={createPageUrl(item.page)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all group relative ${
|
||||
isActive
|
||||
? 'bg-slate-100 text-slate-900'
|
||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'
|
||||
}`}
|
||||
title={item.tooltip}
|
||||
>
|
||||
<item.icon className="w-4 h-4" />
|
||||
<span className="hidden sm:block text-sm font-medium">{item.name}</span>
|
||||
{/* Mobile tooltip */}
|
||||
<span className="sm:hidden absolute bottom-full mb-2 left-1/2 -translate-x-1/2 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
|
||||
{item.tooltip}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Management Dropdown - Only show in classroom area */}
|
||||
{showManagementDropdown && (
|
||||
<DropdownMenu dir="rtl">
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all group relative ${
|
||||
isManagementPage
|
||||
? 'bg-slate-100 text-slate-900'
|
||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'
|
||||
}`}
|
||||
title="ניהול"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="hidden sm:flex items-center gap-1 text-sm font-medium">
|
||||
ניהול
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</span>
|
||||
{/* Mobile tooltip */}
|
||||
<span className="sm:hidden absolute bottom-full mb-2 left-1/2 -translate-x-1/2 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
|
||||
ניהול
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
{managementItems.map((item) => (
|
||||
<DropdownMenuItem key={item.page} asChild>
|
||||
<Link
|
||||
to={createPageUrl(item.page)}
|
||||
className={`cursor-pointer flex items-center gap-2 ${currentPageName === item.page ? 'bg-slate-100' : ''}`}
|
||||
>
|
||||
<item.icon className="w-4 h-4" />
|
||||
{item.name}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main>{children}</main>
|
||||
|
||||
{/* Toast notifications */}
|
||||
<Toaster position="top-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
Migdalor-main/src/api/base44Client.js
Normal file
13
Migdalor-main/src/api/base44Client.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { createClient } from '@base44/sdk';
|
||||
import { appParams } from '@/lib/app-params';
|
||||
|
||||
const { appId, serverUrl, token, functionsVersion } = appParams;
|
||||
|
||||
//Create a client with authentication required
|
||||
export const base44 = createClient({
|
||||
appId,
|
||||
serverUrl,
|
||||
token,
|
||||
functionsVersion,
|
||||
requiresAuth: false
|
||||
});
|
||||
9
Migdalor-main/src/api/entities.js
Normal file
9
Migdalor-main/src/api/entities.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { base44 } from './base44Client';
|
||||
|
||||
|
||||
export const Query = base44.entities.Query;
|
||||
|
||||
|
||||
|
||||
// auth sdk:
|
||||
export const User = base44.auth;
|
||||
24
Migdalor-main/src/api/integrations.js
Normal file
24
Migdalor-main/src/api/integrations.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { base44 } from './base44Client';
|
||||
|
||||
|
||||
|
||||
|
||||
export const Core = base44.integrations.Core;
|
||||
|
||||
export const InvokeLLM = base44.integrations.Core.InvokeLLM;
|
||||
|
||||
export const SendEmail = base44.integrations.Core.SendEmail;
|
||||
|
||||
export const SendSMS = base44.integrations.Core.SendSMS;
|
||||
|
||||
export const UploadFile = base44.integrations.Core.UploadFile;
|
||||
|
||||
export const GenerateImage = base44.integrations.Core.GenerateImage;
|
||||
|
||||
export const ExtractDataFromUploadedFile = base44.integrations.Core.ExtractDataFromUploadedFile;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
Migdalor-main/src/assets/react.svg
Normal file
1
Migdalor-main/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
31
Migdalor-main/src/components/UserNotRegisteredError.jsx
Normal file
31
Migdalor-main/src/components/UserNotRegisteredError.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
const UserNotRegisteredError = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-white to-slate-50">
|
||||
<div className="max-w-md w-full p-8 bg-white rounded-lg shadow-lg border border-slate-100">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 mb-6 rounded-full bg-orange-100">
|
||||
<svg className="w-8 h-8 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-4">Access Restricted</h1>
|
||||
<p className="text-slate-600 mb-8">
|
||||
You are not registered to use this application. Please contact the app administrator to request access.
|
||||
</p>
|
||||
<div className="p-4 bg-slate-50 rounded-md text-sm text-slate-600">
|
||||
<p>If you believe this is an error, you can:</p>
|
||||
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||
<li>Verify you are logged in with the correct account</li>
|
||||
<li>Contact the app administrator for access</li>
|
||||
<li>Try logging out and back in again</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserNotRegisteredError;
|
||||
200
Migdalor-main/src/components/keys/CheckoutModal.jsx
Normal file
200
Migdalor-main/src/components/keys/CheckoutModal.jsx
Normal file
@ -0,0 +1,200 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription } from
|
||||
"@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue } from
|
||||
"@/components/ui/select";
|
||||
import { Key, Users, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function CheckoutModal({ open, onClose, keyItem, crews, squads, currentUser, onConfirm, selectedDate }) {
|
||||
const [selectedCrew, setSelectedCrew] = useState('');
|
||||
const [platoonName, setPlatoonName] = useState('');
|
||||
const [customName, setCustomName] = useState('');
|
||||
const [useCustom, setUseCustom] = useState(false);
|
||||
const [startTime, setStartTime] = useState('');
|
||||
const [endTime, setEndTime] = useState('23:59');
|
||||
|
||||
|
||||
|
||||
// Prefill times if provided
|
||||
React.useEffect(() => {
|
||||
if (keyItem?.prefilledTimes) {
|
||||
setStartTime(keyItem.prefilledTimes.start);
|
||||
setEndTime(keyItem.prefilledTimes.end);
|
||||
}
|
||||
}, [keyItem]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
const holderName = useCustom ? customName : selectedCrew;
|
||||
if (holderName && startTime && endTime) {
|
||||
// Check if time has already passed (only for today)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (selectedDate === today) {
|
||||
const now = new Date();
|
||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
if (startTime < currentTime) {
|
||||
toast.error('לא ניתן למשוך מפתח בזמן שכבר חלף');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onConfirm(keyItem, holderName, startTime, endTime, platoonName);
|
||||
setSelectedCrew('');
|
||||
setPlatoonName('');
|
||||
setCustomName('');
|
||||
setUseCustom(false);
|
||||
setStartTime('');
|
||||
setEndTime('23:59');
|
||||
}
|
||||
};
|
||||
|
||||
if (!keyItem) return null;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
dir="rtl"
|
||||
className="sm:max-w-md text-right">
|
||||
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="text-lg font-semibold leading-none tracking-tight flex flex-row-reverse items-center gap-2 justify-end">משוך מפתח
|
||||
|
||||
|
||||
|
||||
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="text-right">
|
||||
משיכת מפתח לחדר {keyItem.room_number} ({keyItem.room_type})
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{!useCustom &&
|
||||
<div className="space-y-2">
|
||||
<Label className="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium">החדר עבור *</Label>
|
||||
<select
|
||||
value={selectedCrew}
|
||||
onChange={(e) => {
|
||||
const selectedValue = e.target.value;
|
||||
setSelectedCrew(selectedValue);
|
||||
|
||||
// Check if a squad was selected and auto-fill platoon name
|
||||
const selectedSquad = squads?.find((s) => s.squad_number === selectedValue);
|
||||
setPlatoonName(selectedSquad ? selectedSquad.platoon_name : '');
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right">
|
||||
|
||||
<option value="">בחר פלוגה או צוות...</option>
|
||||
|
||||
{crews && crews.length > 0 && (
|
||||
<optgroup label="פלוגות">
|
||||
{crews.map((crew) => (
|
||||
<option key={crew.id} value={crew.name}>{crew.name}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
|
||||
{squads && squads.length > 0 && (
|
||||
<optgroup label="צוותים">
|
||||
{squads.map((squad) => (
|
||||
<option key={squad.id} value={squad.squad_number}>
|
||||
{squad.squad_number} {squad.platoon_name ? `(${squad.platoon_name})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm" className="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3 text-slate-500 text-xs w-full"
|
||||
|
||||
onClick={() => setUseCustom(true)}>
|
||||
|
||||
הזן שם ידנית
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
{useCustom &&
|
||||
<div className="space-y-2">
|
||||
<Label className="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium">החדר עבור *</Label>
|
||||
<Input
|
||||
placeholder="למשל, סגל"
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)} />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm" className="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3 text-slate-500 text-xs w-full"
|
||||
|
||||
onClick={() => setUseCustom(false)}>
|
||||
|
||||
בחר מאפשרויות קיימות
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="space-y-4 border-t pt-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">שעת התחלה *</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)} />
|
||||
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">שעת סיום *</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{endTime === '23:59' &&
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-orange-600 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-xs text-orange-800">
|
||||
<strong>חשוב:</strong> אם לא ציינת שעת סיום מדויקת, יש לסמן את המפתח כהוחזר בעצמך כאשר מחזירים אותו.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={(useCustom ? !customName : !selectedCrew) || !startTime || !endTime}
|
||||
className="flex-1 bg-emerald-600 hover:bg-emerald-700">
|
||||
|
||||
אשר משיכה
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClose} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>);
|
||||
|
||||
}
|
||||
97
Migdalor-main/src/components/keys/KeyCard.jsx
Normal file
97
Migdalor-main/src/components/keys/KeyCard.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Key, User, Clock, ArrowRight, Monitor } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function KeyCard({ keyItem, onCheckout, onReturn, crews, currentUser, currentHolder, isAvailableInTimeRange, timeFilterActive }) {
|
||||
// If time filter is active, use time-range availability, otherwise use current status
|
||||
const isAvailable = timeFilterActive ? isAvailableInTimeRange : (keyItem.status === 'available' && !currentHolder);
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
const canReturn = !timeFilterActive && (!isAvailable && (isAdmin || keyItem.checked_out_by === currentUser?.email));
|
||||
const displayHolder = currentHolder || keyItem.current_holder;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Card className={`p-5 border-2 transition-all duration-300 hover:shadow-lg ${
|
||||
isAvailable
|
||||
? 'border-emerald-200 bg-gradient-to-br from-emerald-50/50 to-white'
|
||||
: 'border-amber-200 bg-gradient-to-br from-amber-50/50 to-white'
|
||||
}`}>
|
||||
<div dir="ltr" className="flex flex-row items-start justify-between mb-4">
|
||||
<Badge className={`${
|
||||
isAvailable
|
||||
? 'bg-emerald-500 hover:bg-emerald-600'
|
||||
: 'bg-amber-500 hover:bg-amber-600'
|
||||
}`}>
|
||||
{isAvailable ? 'זמין' : 'תפוס'}
|
||||
</Badge>
|
||||
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div className="text-right">
|
||||
<h3 className="font-semibold text-slate-800 text-lg flex items-center gap-2 justify-end">
|
||||
{keyItem.has_computers && (
|
||||
<Monitor className="w-4 h-4 text-blue-600" />
|
||||
)}
|
||||
חדר {keyItem.room_number}
|
||||
</h3>
|
||||
<div className="flex justify-end">
|
||||
<Badge variant="outline" className={`mt-1 ${
|
||||
keyItem.room_type === 'פלוגתי'
|
||||
? 'border-purple-300 text-purple-700 bg-purple-50'
|
||||
: 'border-blue-300 text-blue-700 bg-blue-50'
|
||||
}`}>
|
||||
{keyItem.room_type === 'פלוגתי' ? '🏢 פלוגתי' : '🏠 צוותי'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl ${
|
||||
isAvailable ? 'bg-emerald-100' : 'bg-amber-100'
|
||||
}`}>
|
||||
<Key className={`w-5 h-5 ${
|
||||
isAvailable ? 'text-emerald-600' : 'text-amber-600'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isAvailable && displayHolder && (
|
||||
<div className="mb-4 p-3 bg-white/80 rounded-lg border border-slate-100">
|
||||
<div className="flex items-center gap-2 text-slate-600 mb-1">
|
||||
<User className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{displayHolder}</span>
|
||||
</div>
|
||||
{keyItem.checkout_start_time && keyItem.checkout_end_time && (
|
||||
<div className="flex items-center gap-2 text-slate-400 text-xs">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{keyItem.checkout_end_time} - {keyItem.checkout_start_time}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAvailable ? (
|
||||
<Button
|
||||
onClick={() => onCheckout(keyItem)}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 transition-all"
|
||||
>
|
||||
משוך מפתח <ArrowRight className="w-4 h-4 mr-2" />
|
||||
</Button>
|
||||
) : canReturn ? (
|
||||
<Button
|
||||
onClick={() => onReturn(keyItem)}
|
||||
variant="outline"
|
||||
className="w-full border-amber-300 text-amber-700 hover:bg-amber-50"
|
||||
>
|
||||
החזר מפתח
|
||||
</Button>
|
||||
) : null}
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
222
Migdalor-main/src/components/queue/AddToQueueModal.jsx
Normal file
222
Migdalor-main/src/components/queue/AddToQueueModal.jsx
Normal file
@ -0,0 +1,222 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription } from
|
||||
"@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue } from
|
||||
"@/components/ui/select";
|
||||
import { Clock, Users } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function AddToQueueModal({ open, onClose, crews, squads, currentUser, onConfirm }) {
|
||||
const [crewName, setCrewName] = useState('');
|
||||
const [platoonName, setPlatoonName] = useState('');
|
||||
const [preferredType, setPreferredType] = useState('any');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [useExisting, setUseExisting] = useState(false);
|
||||
const [startTime, setStartTime] = useState('');
|
||||
const [endTime, setEndTime] = useState('');
|
||||
|
||||
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (crewName && startTime && endTime) {
|
||||
// Check if time has already passed
|
||||
const now = new Date();
|
||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
if (startTime < currentTime) {
|
||||
toast.error('לא ניתן להוסיף בקשה בזמן שכבר חלף');
|
||||
return;
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
toast.error('שעת הסיום חייבת להיות מאוחרת משעת ההתחלה');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get today's date
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
onConfirm({
|
||||
crew_name: crewName,
|
||||
platoon_name: platoonName,
|
||||
preferred_type: preferredType,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
notes: notes,
|
||||
date: today
|
||||
});
|
||||
setCrewName('');
|
||||
setPlatoonName('');
|
||||
setPreferredType('any');
|
||||
setStartTime('');
|
||||
setEndTime('');
|
||||
setNotes('');
|
||||
setUseExisting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent dir="rtl" className="text-right">
|
||||
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="w-full text-right">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<span className="text-right">בקשה מיוחדת למפתח</span>
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
|
||||
|
||||
|
||||
|
||||
<DialogDescription className="text-right">
|
||||
הוסף בקשה מיוחדת למפתח
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{!useExisting ?
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-right block">החדר עבור *</Label>
|
||||
<select
|
||||
value={crewName}
|
||||
onChange={(e) => {
|
||||
const selectedValue = e.target.value;
|
||||
setCrewName(selectedValue);
|
||||
|
||||
// Check if a squad was selected and auto-fill platoon name
|
||||
const selectedSquad = squads?.find((s) => s.squad_number === selectedValue);
|
||||
setPlatoonName(selectedSquad ? selectedSquad.platoon_name : '');
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right">
|
||||
|
||||
<option value="">בחר פלוגה או צוות...</option>
|
||||
|
||||
{crews && crews.length > 0 && (
|
||||
<optgroup label="פלוגות">
|
||||
{crews.map((crew) => (
|
||||
<option key={crew.id} value={crew.name}>{crew.name}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
|
||||
{squads && squads.length > 0 && (
|
||||
<optgroup label="צוותים">
|
||||
{squads.map((squad) => (
|
||||
<option key={squad.id} value={squad.squad_number}>
|
||||
{squad.squad_number} {squad.platoon_name ? `(${squad.platoon_name})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-500 text-xs w-full"
|
||||
onClick={() => setUseExisting(true)}>
|
||||
הזן שם ידנית
|
||||
</Button>
|
||||
</div> :
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-right block">החדר עבור *</Label>
|
||||
<Input
|
||||
placeholder="למשל, סגל"
|
||||
value={crewName}
|
||||
onChange={(e) => setCrewName(e.target.value)}
|
||||
className="text-right" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-500 text-xs w-full"
|
||||
onClick={() => setUseExisting(false)}>
|
||||
בחר מאפשרויות קיימות
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-right block">שעת התחלה</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
className="text-right" />
|
||||
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-right block">שעת סיום</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
className="text-right" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-right block">סוג חדר מועדף</Label>
|
||||
<Select value={preferredType} onValueChange={setPreferredType}>
|
||||
<SelectTrigger className="text-right" dir="rtl">
|
||||
<SelectValue className="text-right" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" dir="rtl">
|
||||
<SelectItem value="any">🔄 כל חדר זמין</SelectItem>
|
||||
<SelectItem value="צוותי">🏠 צוותי</SelectItem>
|
||||
<SelectItem value="פלוגתי">🏢 פלוגתי</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-right block">הערות (אופציונלי)</Label>
|
||||
<Textarea
|
||||
placeholder="דרישות מיוחדות..."
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="h-20 text-right" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button variant="outline" onClick={onClose} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={!crewName || !startTime || !endTime}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700">
|
||||
הוסף לתור
|
||||
</Button>
|
||||
|
||||
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>);
|
||||
|
||||
|
||||
}
|
||||
72
Migdalor-main/src/components/queue/WaitingQueueCard.jsx
Normal file
72
Migdalor-main/src/components/queue/WaitingQueueCard.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Users, Clock, Trash2, ArrowUp } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function WaitingQueueCard({ item, position, onRemove, onMoveUp, isAdmin }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Card className="p-4 border border-slate-200 hover:border-slate-300 transition-all" dir="rtl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center">
|
||||
<span className="font-bold text-slate-600">#{position}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-slate-400" />
|
||||
<span className="font-medium text-slate-800">{item.crew_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.preferred_type === 'any' ? '🔄 הכל' :
|
||||
item.preferred_type === 'פלוגתי' ? '🏢 פלוגתי' : '🏠 צוותי'}
|
||||
</Badge>
|
||||
{item.start_time && item.end_time && (
|
||||
<span className="text-xs text-slate-500">
|
||||
⏰ {item.start_time}-{item.end_time}
|
||||
</span>
|
||||
)}
|
||||
{item.notes && (
|
||||
<span className="text-xs text-slate-400">{item.notes}</span>
|
||||
)}
|
||||
</div>
|
||||
{item.created_by && (
|
||||
<span className="text-xs text-slate-400 mt-1 block">
|
||||
נשלח ע״י {item.created_by}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{position > 1 && isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onMoveUp(item)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(item)}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
66
Migdalor-main/src/components/stats/StatsBar.jsx
Normal file
66
Migdalor-main/src/components/stats/StatsBar.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Key, Clock, Users } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function StatsBar({ keys, queueCount }) {
|
||||
const availableKeys = keys.filter(k => k.status === 'available').length;
|
||||
const takenKeys = keys.filter(k => k.status === 'taken').length;
|
||||
const smallKeys = keys.filter(k => k.room_type === 'צוותי');
|
||||
const largeKeys = keys.filter(k => k.room_type === 'פלוגתי');
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: 'זמינים',
|
||||
value: availableKeys,
|
||||
total: keys.length,
|
||||
icon: Key,
|
||||
color: 'emerald',
|
||||
gradient: 'from-emerald-500 to-teal-500',
|
||||
},
|
||||
{
|
||||
label: 'בשימוש',
|
||||
value: takenKeys,
|
||||
total: keys.length,
|
||||
icon: Users,
|
||||
color: 'amber',
|
||||
gradient: 'from-amber-500 to-orange-500',
|
||||
},
|
||||
{
|
||||
label: 'ממתינים',
|
||||
value: queueCount,
|
||||
icon: Clock,
|
||||
color: 'blue',
|
||||
gradient: 'from-blue-500 to-indigo-500',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="relative overflow-hidden rounded-2xl bg-white border border-slate-200 p-6 shadow-sm"
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-br ${stat.gradient} opacity-10 rounded-full -translate-y-1/2 translate-x-1/2`} />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 mb-1">{stat.label}</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
{stat.total && (
|
||||
<span className="text-slate-400 text-lg">{stat.total} /</span>
|
||||
)}
|
||||
<span className="text-3xl font-bold text-slate-800">{stat.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl bg-${stat.color}-100`}>
|
||||
<stat.icon className={`w-6 h-6 text-${stat.color}-600`} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
Migdalor-main/src/components/ui/accordion.jsx
Normal file
41
Migdalor-main/src/components/ui/accordion.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronDown
|
||||
className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
97
Migdalor-main/src/components/ui/alert-dialog.jsx
Normal file
97
Migdalor-main/src/components/ui/alert-dialog.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref} />
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
47
Migdalor-main/src/components/ui/alert.jsx
Normal file
47
Migdalor-main/src/components/ui/alert.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props} />
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
5
Migdalor-main/src/components/ui/aspect-ratio.jsx
Normal file
5
Migdalor-main/src/components/ui/aspect-ratio.jsx
Normal file
@ -0,0 +1,5 @@
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
35
Migdalor-main/src/components/ui/avatar.jsx
Normal file
35
Migdalor-main/src/components/ui/avatar.jsx
Normal file
@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...props} />
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props} />
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
34
Migdalor-main/src/components/ui/badge.jsx
Normal file
34
Migdalor-main/src/components/ui/badge.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md 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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}) {
|
||||
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
92
Migdalor-main/src/components/ui/breadcrumb.jsx
Normal file
92
Migdalor-main/src/components/ui/breadcrumb.jsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Breadcrumb = React.forwardRef(
|
||||
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
|
||||
)
|
||||
Breadcrumb.displayName = "Breadcrumb"
|
||||
|
||||
const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
|
||||
const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props} />
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
|
||||
const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
(<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
|
||||
const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
||||
{...props}>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
48
Migdalor-main/src/components/ui/button.jsx
Normal file
48
Migdalor-main/src/components/ui/button.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
(<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
71
Migdalor-main/src/components/ui/calendar.jsx
Normal file
71
Migdalor-main/src/components/ui/calendar.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
(<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start: "day-range-start",
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
export { Calendar }
|
||||
50
Migdalor-main/src/components/ui/card.jsx
Normal file
50
Migdalor-main/src/components/ui/card.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
193
Migdalor-main/src/components/ui/carousel.jsx
Normal file
193
Migdalor-main/src/components/ui/carousel.jsx
Normal file
@ -0,0 +1,193 @@
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
const CarouselContext = React.createContext(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef((
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel({
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
}, plugins)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api) => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback((event) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
}, [scrollPrev, scrollNext])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
};
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
(<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>)
|
||||
);
|
||||
})
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
(<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</div>)
|
||||
);
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
(<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>)
|
||||
);
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
(<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>)
|
||||
);
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
|
||||
309
Migdalor-main/src/components/ui/chart.jsx
Normal file
309
Migdalor-main/src/components/ui/chart.jsx
Normal file
@ -0,0 +1,309 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = {
|
||||
light: "",
|
||||
dark: ".dark"
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
(<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>)
|
||||
);
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({
|
||||
id,
|
||||
config
|
||||
}) => {
|
||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
(<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`)
|
||||
.join("\n"),
|
||||
}} />)
|
||||
);
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef((
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
(<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>)
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
(<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
})}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor
|
||||
}
|
||||
} />
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>)
|
||||
);
|
||||
})
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef((
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
(<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}} />
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>)
|
||||
);
|
||||
})}
|
||||
</div>)
|
||||
);
|
||||
})
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config,
|
||||
payload,
|
||||
key
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key]
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[key]
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
22
Migdalor-main/src/components/ui/checkbox.jsx
Normal file
22
Migdalor-main/src/components/ui/checkbox.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
11
Migdalor-main/src/components/ui/collapsible.jsx
Normal file
11
Migdalor-main/src/components/ui/collapsible.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
116
Migdalor-main/src/components/ui/command.jsx
Normal file
116
Migdalor-main/src/components/ui/command.jsx
Normal file
@ -0,0 +1,116 @@
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
(<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<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">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>)
|
||||
);
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props} />
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef((props, ref) => (
|
||||
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
(<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
156
Migdalor-main/src/components/ui/context-menu.jsx
Normal file
156
Migdalor-main/src/components/ui/context-menu.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
))
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
||||
|
||||
const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</ContextMenuPrimitive.Portal>
|
||||
))
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
||||
|
||||
const ContextMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
ContextMenuCheckboxItem.displayName =
|
||||
ContextMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
))
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const ContextMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
||||
|
||||
const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
(<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
96
Migdalor-main/src/components/ui/dialog.jsx
Normal file
96
Migdalor-main/src/components/ui/dialog.jsx
Normal file
@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className="absolute left-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">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
92
Migdalor-main/src/components/ui/drawer.jsx
Normal file
92
Migdalor-main/src/components/ui/drawer.jsx
Normal file
@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}) => (
|
||||
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
|
||||
const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props} />
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
|
||||
const DrawerContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
|
||||
const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
|
||||
const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
156
Migdalor-main/src/components/ui/dropdown-menu.jsx
Normal file
156
Migdalor-main/src/components/ui/dropdown-menu.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default gap-2 select-none items-center 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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
(<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
134
Migdalor-main/src/components/ui/form.jsx
Normal file
134
Migdalor-main/src/components/ui/form.jsx
Normal file
@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Controller, FormProvider, useFormContext } from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
const FormFieldContext = React.createContext({})
|
||||
|
||||
const FormField = (
|
||||
{
|
||||
...props
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
(<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>)
|
||||
);
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext({})
|
||||
|
||||
const FormItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
(<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>)
|
||||
);
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
(<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
(<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
(<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
(<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}>
|
||||
{body}
|
||||
</p>)
|
||||
);
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
25
Migdalor-main/src/components/ui/hover-card.jsx
Normal file
25
Migdalor-main/src/components/ui/hover-card.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
53
Migdalor-main/src/components/ui/input-otp.jsx
Normal file
53
Migdalor-main/src/components/ui/input-otp.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { Minus } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props} />
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-1 ring-ring",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>)
|
||||
);
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Minus />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
19
Migdalor-main/src/components/ui/input.jsx
Normal file
19
Migdalor-main/src/components/ui/input.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
(<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors 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-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
16
Migdalor-main/src/components/ui/label.jsx
Normal file
16
Migdalor-main/src/components/ui/label.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
200
Migdalor-main/src/components/ui/menubar.jsx
Normal file
200
Migdalor-main/src/components/ui/menubar.jsx
Normal file
@ -0,0 +1,200 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Menu {...props} />;
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Group {...props} />;
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Portal {...props} />;
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.RadioGroup {...props} />;
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||
}
|
||||
|
||||
const Menubar = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
||||
|
||||
const MenubarTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
))
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
||||
|
||||
const MenubarSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
||||
|
||||
const MenubarContent = React.forwardRef((
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</MenubarPrimitive.Portal>
|
||||
))
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
||||
|
||||
const MenubarItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
))
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
||||
|
||||
const MenubarRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
))
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
||||
|
||||
const MenubarLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
||||
|
||||
const MenubarSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
(<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
MenubarShortcut.displayname = "MenubarShortcut"
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
}
|
||||
104
Migdalor-main/src/components/ui/navigation-menu.jsx
Normal file
104
Migdalor-main/src/components/ui/navigation-menu.jsx
Normal file
@ -0,0 +1,104 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const NavigationMenu = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
|
||||
const NavigationMenuList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 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-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||
)
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true" />
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
|
||||
const NavigationMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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 ",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"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 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)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
</div>
|
||||
))
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<div
|
||||
className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
100
Migdalor-main/src/components/ui/pagination.jsx
Normal file
100
Migdalor-main/src/components/ui/pagination.jsx
Normal file
@ -0,0 +1,100 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
const Pagination = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props} />
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
|
||||
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props} />
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
|
||||
const PaginationItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}), className)}
|
||||
{...props} />
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
27
Migdalor-main/src/components/ui/popover.jsx
Normal file
27
Migdalor-main/src/components/ui/popover.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
23
Migdalor-main/src/components/ui/progress.jsx
Normal file
23
Migdalor-main/src/components/ui/progress.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
29
Migdalor-main/src/components/ui/radio-group.jsx
Normal file
29
Migdalor-main/src/components/ui/radio-group.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const RadioGroup = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (<RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />);
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>)
|
||||
);
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
42
Migdalor-main/src/components/ui/resizable.jsx
Normal file
42
Migdalor-main/src/components/ui/resizable.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import { GripVertical } from "lucide-react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
)
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{withHandle && (
|
||||
<div
|
||||
className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<GripVertical className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
38
Migdalor-main/src/components/ui/scroll-area.jsx
Normal file
38
Migdalor-main/src/components/ui/scroll-area.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
121
Migdalor-main/src/components/ui/select.jsx
Normal file
121
Migdalor-main/src/components/ui/select.jsx
Normal file
@ -0,0 +1,121 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn("p-1", position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props} />
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props} />
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
23
Migdalor-main/src/components/ui/separator.jsx
Normal file
23
Migdalor-main/src/components/ui/separator.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef((
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
109
Migdalor-main/src/components/ui/sheet.jsx
Normal file
109
Migdalor-main/src/components/ui/sheet.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref} />
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
<SheetPrimitive.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-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
626
Migdalor-main/src/components/ui/sidebar.jsx
Normal file
626
Migdalor-main/src/components/ui/sidebar.jsx
Normal file
@ -0,0 +1,626 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
const SidebarContext = React.createContext(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef((
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback((value) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
}, [setOpenProp, open])
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo(() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar])
|
||||
|
||||
return (
|
||||
(<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style
|
||||
}
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>)
|
||||
);
|
||||
})
|
||||
SidebarProvider.displayName = "SidebarProvider"
|
||||
|
||||
const Sidebar = React.forwardRef((
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
(<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>)
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
(<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
|
||||
}
|
||||
}
|
||||
side={side}>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
className="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
)} />
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
);
|
||||
})
|
||||
Sidebar.displayName = "Sidebar"
|
||||
|
||||
const SidebarTrigger = React.forwardRef(({ className, onClick, asChild = false, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
(<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
asChild={asChild}
|
||||
{...props}>
|
||||
{asChild ? (
|
||||
<PanelLeft />
|
||||
) : (
|
||||
<>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</>
|
||||
)}
|
||||
</Button>)
|
||||
);
|
||||
})
|
||||
SidebarTrigger.displayName = "SidebarTrigger"
|
||||
|
||||
const SidebarRail = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
(<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarRail.displayName = "SidebarRail"
|
||||
|
||||
const SidebarInset = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarInset.displayName = "SidebarInset"
|
||||
|
||||
const SidebarInput = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarInput.displayName = "SidebarInput"
|
||||
|
||||
const SidebarHeader = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarHeader.displayName = "SidebarHeader"
|
||||
|
||||
const SidebarFooter = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarFooter.displayName = "SidebarFooter"
|
||||
|
||||
const SidebarSeparator = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarSeparator.displayName = "SidebarSeparator"
|
||||
|
||||
const SidebarContent = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarContent.displayName = "SidebarContent"
|
||||
|
||||
const SidebarGroup = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarGroup.displayName = "SidebarGroup"
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
(<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
||||
|
||||
const SidebarGroupAction = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
(<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
||||
|
||||
const SidebarGroupContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
||||
|
||||
const SidebarMenu = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenu.displayName = "SidebarMenu"
|
||||
|
||||
const SidebarMenuItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const SidebarMenuButton = React.forwardRef((
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props} />
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip} />
|
||||
</Tooltip>)
|
||||
);
|
||||
})
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
||||
|
||||
const SidebarMenuAction = React.forwardRef(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
(<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, [])
|
||||
|
||||
return (
|
||||
(<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}>
|
||||
{showIcon && (
|
||||
<Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width
|
||||
}
|
||||
} />
|
||||
</div>)
|
||||
);
|
||||
})
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
||||
|
||||
const SidebarMenuSub = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef(({ ...props }, ref) => <li ref={ref} {...props} />)
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef(
|
||||
({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
(<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
)
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
14
Migdalor-main/src/components/ui/skeleton.jsx
Normal file
14
Migdalor-main/src/components/ui/skeleton.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
(<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
21
Migdalor-main/src/components/ui/slider.jsx
Normal file
21
Migdalor-main/src/components/ui/slider.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
{...props}>
|
||||
<SliderPrimitive.Track
|
||||
className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb
|
||||
className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
29
Migdalor-main/src/components/ui/sonner.jsx
Normal file
29
Migdalor-main/src/components/ui/sonner.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
const Toaster = ({
|
||||
...props
|
||||
}) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
(<Sonner
|
||||
theme={theme}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props} />)
|
||||
);
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
22
Migdalor-main/src/components/ui/switch.jsx
Normal file
22
Migdalor-main/src/components/ui/switch.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)} />
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
86
Migdalor-main/src/components/ui/table.jsx
Normal file
86
Migdalor-main/src/components/ui/table.jsx
Normal file
@ -0,0 +1,86 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
41
Migdalor-main/src/components/ui/tabs.jsx
Normal file
41
Migdalor-main/src/components/ui/tabs.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
18
Migdalor-main/src/components/ui/textarea.jsx
Normal file
18
Migdalor-main/src/components/ui/textarea.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
104
Migdalor-main/src/components/ui/toast.jsx
Normal file
104
Migdalor-main/src/components/ui/toast.jsx
Normal file
@ -0,0 +1,104 @@
|
||||
import * as React from "react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ToastProvider = React.forwardRef(({ ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastProvider.displayName = "ToastProvider";
|
||||
|
||||
const ToastViewport = React.forwardRef(({ ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = "ToastViewport";
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = "Toast";
|
||||
|
||||
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = "ToastAction";
|
||||
|
||||
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
));
|
||||
ToastClose.displayName = "ToastClose";
|
||||
|
||||
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = "ToastTitle";
|
||||
|
||||
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = "ToastDescription";
|
||||
|
||||
export {
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
33
Migdalor-main/src/components/ui/toaster.jsx
Normal file
33
Migdalor-main/src/components/ui/toaster.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
44
Migdalor-main/src/components/ui/toggle-group.jsx
Normal file
44
Migdalor-main/src/components/ui/toggle-group.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
))
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||
|
||||
const ToggleGroupItem = React.forwardRef(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
(<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}), className)}
|
||||
{...props}>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>)
|
||||
);
|
||||
})
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
38
Migdalor-main/src/components/ui/toggle.jsx
Normal file
38
Migdalor-main/src/components/ui/toggle.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props} />
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
28
Migdalor-main/src/components/ui/tooltip.jsx
Normal file
28
Migdalor-main/src/components/ui/tooltip.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
164
Migdalor-main/src/components/ui/use-toast.jsx
Normal file
164
Migdalor-main/src/components/ui/use-toast.jsx
Normal file
@ -0,0 +1,164 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const TOAST_LIMIT = 20;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map();
|
||||
|
||||
const addToRemoveQueue = (toastId) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: actionTypes.REMOVE_TOAST,
|
||||
toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
const _clearFromRemoveQueue = (toastId) => {
|
||||
const timeout = toastTimeouts.get(toastId);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
toastTimeouts.delete(toastId);
|
||||
}
|
||||
};
|
||||
|
||||
export const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case actionTypes.ADD_TOAST:
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case actionTypes.UPDATE_TOAST:
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
};
|
||||
|
||||
case actionTypes.DISMISS_TOAST: {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
};
|
||||
}
|
||||
case actionTypes.REMOVE_TOAST:
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners = [];
|
||||
|
||||
let memoryState = { toasts: [] };
|
||||
|
||||
function dispatch(action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
function toast({ ...props }) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props) =>
|
||||
dispatch({
|
||||
type: actionTypes.UPDATE_TOAST,
|
||||
toast: { ...props, id },
|
||||
});
|
||||
|
||||
const dismiss = () =>
|
||||
dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: actionTypes.ADD_TOAST,
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = useState(memoryState);
|
||||
|
||||
useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
19
Migdalor-main/src/hooks/use-mobile.jsx
Normal file
19
Migdalor-main/src/hooks/use-mobile.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
158
Migdalor-main/src/index.css
Normal file
158
Migdalor-main/src/index.css
Normal file
@ -0,0 +1,158 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
/* :root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
154
Migdalor-main/src/lib/AuthContext.jsx
Normal file
154
Migdalor-main/src/lib/AuthContext.jsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { appParams } from '@/lib/app-params';
|
||||
import { createAxiosClient } from '@base44/sdk/dist/utils/axios-client';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoadingAuth, setIsLoadingAuth] = useState(true);
|
||||
const [isLoadingPublicSettings, setIsLoadingPublicSettings] = useState(true);
|
||||
const [authError, setAuthError] = useState(null);
|
||||
const [appPublicSettings, setAppPublicSettings] = useState(null); // Contains only { id, public_settings }
|
||||
|
||||
useEffect(() => {
|
||||
checkAppState();
|
||||
}, []);
|
||||
|
||||
const checkAppState = async () => {
|
||||
try {
|
||||
setIsLoadingPublicSettings(true);
|
||||
setAuthError(null);
|
||||
|
||||
// First, check app public settings (with token if available)
|
||||
// This will tell us if auth is required, user not registered, etc.
|
||||
const appClient = createAxiosClient({
|
||||
baseURL: `${appParams.serverUrl}/api/apps/public`,
|
||||
headers: {
|
||||
'X-App-Id': appParams.appId
|
||||
},
|
||||
token: appParams.token, // Include token if available
|
||||
interceptResponses: true
|
||||
});
|
||||
|
||||
try {
|
||||
const publicSettings = await appClient.get(`/prod/public-settings/by-id/${appParams.appId}`);
|
||||
setAppPublicSettings(publicSettings);
|
||||
|
||||
// If we got the app public settings successfully, check if user is authenticated
|
||||
if (appParams.token) {
|
||||
await checkUserAuth();
|
||||
} else {
|
||||
setIsLoadingAuth(false);
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
setIsLoadingPublicSettings(false);
|
||||
} catch (appError) {
|
||||
console.error('App state check failed:', appError);
|
||||
|
||||
// Handle app-level errors
|
||||
if (appError.status === 403 && appError.data?.extra_data?.reason) {
|
||||
const reason = appError.data.extra_data.reason;
|
||||
if (reason === 'auth_required') {
|
||||
setAuthError({
|
||||
type: 'auth_required',
|
||||
message: 'Authentication required'
|
||||
});
|
||||
} else if (reason === 'user_not_registered') {
|
||||
setAuthError({
|
||||
type: 'user_not_registered',
|
||||
message: 'User not registered for this app'
|
||||
});
|
||||
} else {
|
||||
setAuthError({
|
||||
type: reason,
|
||||
message: appError.message
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setAuthError({
|
||||
type: 'unknown',
|
||||
message: appError.message || 'Failed to load app'
|
||||
});
|
||||
}
|
||||
setIsLoadingPublicSettings(false);
|
||||
setIsLoadingAuth(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unexpected error:', error);
|
||||
setAuthError({
|
||||
type: 'unknown',
|
||||
message: error.message || 'An unexpected error occurred'
|
||||
});
|
||||
setIsLoadingPublicSettings(false);
|
||||
setIsLoadingAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkUserAuth = async () => {
|
||||
try {
|
||||
// Now check if the user is authenticated
|
||||
setIsLoadingAuth(true);
|
||||
const currentUser = await base44.auth.me();
|
||||
setUser(currentUser);
|
||||
setIsAuthenticated(true);
|
||||
setIsLoadingAuth(false);
|
||||
} catch (error) {
|
||||
console.error('User auth check failed:', error);
|
||||
setIsLoadingAuth(false);
|
||||
setIsAuthenticated(false);
|
||||
|
||||
// If user auth fails, it might be an expired token
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
setAuthError({
|
||||
type: 'auth_required',
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const logout = (shouldRedirect = true) => {
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
|
||||
if (shouldRedirect) {
|
||||
// Use the SDK's logout method which handles token cleanup and redirect
|
||||
base44.auth.logout(window.location.href);
|
||||
} else {
|
||||
// Just remove the token without redirect
|
||||
base44.auth.logout();
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToLogin = () => {
|
||||
// Use the SDK's redirectToLogin method
|
||||
base44.auth.redirectToLogin(window.location.href);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
isAuthenticated,
|
||||
isLoadingAuth,
|
||||
isLoadingPublicSettings,
|
||||
authError,
|
||||
appPublicSettings,
|
||||
logout,
|
||||
navigateToLogin,
|
||||
checkAppState
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
50
Migdalor-main/src/lib/NavigationTracker.jsx
Normal file
50
Migdalor-main/src/lib/NavigationTracker.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { pagesConfig } from '@/pages.config';
|
||||
|
||||
export default function NavigationTracker() {
|
||||
const location = useLocation();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { Pages, mainPage } = pagesConfig;
|
||||
const mainPageKey = mainPage ?? Object.keys(Pages)[0];
|
||||
|
||||
// Post navigation changes to parent window
|
||||
useEffect(() => {
|
||||
window.parent?.postMessage({
|
||||
type: "app_changed_url",
|
||||
url: window.location.href
|
||||
}, '*');
|
||||
}, [location]);
|
||||
|
||||
// Log user activity when navigating to a page
|
||||
useEffect(() => {
|
||||
// Extract page name from pathname
|
||||
const pathname = location.pathname;
|
||||
let pageName;
|
||||
|
||||
if (pathname === '/' || pathname === '') {
|
||||
pageName = mainPageKey;
|
||||
} else {
|
||||
// Remove leading slash and get the first segment
|
||||
const pathSegment = pathname.replace(/^\//, '').split('/')[0];
|
||||
|
||||
// Try case-insensitive lookup in Pages config
|
||||
const pageKeys = Object.keys(Pages);
|
||||
const matchedKey = pageKeys.find(
|
||||
key => key.toLowerCase() === pathSegment.toLowerCase()
|
||||
);
|
||||
|
||||
pageName = matchedKey || null;
|
||||
}
|
||||
|
||||
if (isAuthenticated && pageName) {
|
||||
base44.appLogs.logUserInApp(pageName).catch(() => {
|
||||
// Silently fail - logging shouldn't break the app
|
||||
});
|
||||
}
|
||||
}, [location, isAuthenticated, Pages, mainPageKey]);
|
||||
|
||||
return null;
|
||||
}
|
||||
75
Migdalor-main/src/lib/PageNotFound.jsx
Normal file
75
Migdalor-main/src/lib/PageNotFound.jsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
|
||||
export default function PageNotFound({}) {
|
||||
const location = useLocation();
|
||||
const pageName = location.pathname.substring(1);
|
||||
|
||||
const { data: authData, isFetched } = useQuery({
|
||||
queryKey: ['user'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const user = await base44.auth.me();
|
||||
return { user, isAuthenticated: true };
|
||||
} catch (error) {
|
||||
return { user: null, isAuthenticated: false };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center space-y-6">
|
||||
{/* 404 Error Code */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-7xl font-light text-slate-300">404</h1>
|
||||
<div className="h-0.5 w-16 bg-slate-200 mx-auto"></div>
|
||||
</div>
|
||||
|
||||
{/* Main Message */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-2xl font-medium text-slate-800">
|
||||
Page Not Found
|
||||
</h2>
|
||||
<p className="text-slate-600 leading-relaxed">
|
||||
The page <span className="font-medium text-slate-700">"{pageName}"</span> could not be found in this application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Admin Note */}
|
||||
{isFetched && authData.isAuthenticated && authData.user?.role === 'admin' && (
|
||||
<div className="mt-8 p-4 bg-slate-100 rounded-lg border border-slate-200">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-orange-100 flex items-center justify-center mt-0.5">
|
||||
<div className="w-2 h-2 rounded-full bg-orange-400"></div>
|
||||
</div>
|
||||
<div className="text-left space-y-1">
|
||||
<p className="text-sm font-medium text-slate-700">Admin Note</p>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">
|
||||
This could mean that the AI hasn't implemented this page yet. Ask it to implement it in the chat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="pt-6">
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 hover:border-slate-300 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Go Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
648
Migdalor-main/src/lib/VisualEditAgent.jsx
Normal file
648
Migdalor-main/src/lib/VisualEditAgent.jsx
Normal file
@ -0,0 +1,648 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export default function VisualEditAgent() {
|
||||
// this functions job is to receive first a message from the parent window, to set or unset visual edits mode.
|
||||
// once in visual edits mode, every hover over an elelmnt that has linenumbers should show an overlay, when clicked - it should stick the overlay and send a message to the parent window with the selected element
|
||||
// then, the parent window will have an editor, allow for changes to the tailwind css classes of the selected element, and send the updated css classes back to the iframe.
|
||||
// the iframe will then update the css classes of the selected element.
|
||||
|
||||
// State and refs
|
||||
const [isVisualEditMode, setIsVisualEditMode] = useState(false);
|
||||
const isVisualEditModeRef = useRef(false);
|
||||
const [isPopoverDragging, setIsPopoverDragging] = useState(false);
|
||||
const isPopoverDraggingRef = useRef(false);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const isDropdownOpenRef = useRef(false);
|
||||
const hoverOverlaysRef = useRef([]); // Multiple overlays for hover
|
||||
const selectedOverlaysRef = useRef([]); // Multiple overlays for selection
|
||||
const currentHighlightedElementsRef = useRef([]); // Multiple elements for hover
|
||||
const selectedElementIdRef = useRef(null); // Store the visual selector ID
|
||||
|
||||
// Create overlay element
|
||||
const createOverlay = (isSelected = false) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'absolute';
|
||||
overlay.style.pointerEvents = 'none';
|
||||
overlay.style.transition = 'all 0.1s ease-in-out';
|
||||
overlay.style.zIndex = '9999';
|
||||
|
||||
// Use different styles for hover vs selected
|
||||
if (isSelected) {
|
||||
overlay.style.border = '2px solid #2563EB';
|
||||
} else {
|
||||
overlay.style.border = '2px solid #95a5fc';
|
||||
overlay.style.backgroundColor = 'rgba(99, 102, 241, 0.05)';
|
||||
}
|
||||
|
||||
return overlay;
|
||||
};
|
||||
|
||||
// Position overlay relative to element
|
||||
const positionOverlay = (overlay, element, isSelected = false) => {
|
||||
if (!element || !isVisualEditModeRef.current) return;
|
||||
|
||||
// Force layout recalculation
|
||||
void element.offsetWidth;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.top = `${rect.top + window.scrollY}px`;
|
||||
overlay.style.left = `${rect.left + window.scrollX}px`; // weird bug with the offset
|
||||
overlay.style.width = `${rect.width}px`;
|
||||
overlay.style.height = `${rect.height}px`;
|
||||
|
||||
// Check if label already exists in overlay
|
||||
let label = overlay.querySelector('div');
|
||||
|
||||
if (!label) {
|
||||
// Create new label if it doesn't exist
|
||||
label = document.createElement('div');
|
||||
label.textContent = element.tagName.toLowerCase();
|
||||
label.style.position = 'absolute';
|
||||
label.style.top = '-27px';
|
||||
label.style.left = '-2px';
|
||||
label.style.padding = '2px 8px';
|
||||
label.style.fontSize = '11px';
|
||||
label.style.fontWeight = isSelected ? '500' : '400';
|
||||
label.style.color = isSelected ? '#ffffff' : '#526cff';
|
||||
label.style.backgroundColor = isSelected ? '#526cff' : '#DBEAFE';
|
||||
label.style.borderRadius = '3px';
|
||||
label.style.boxShadow = isSelected ? 'none' : 'none';
|
||||
label.style.minWidth = '24px';
|
||||
label.style.textAlign = 'center';
|
||||
overlay.appendChild(label);
|
||||
}
|
||||
// If label exists, we preserve its existing styling (don't recreate or modify)
|
||||
};
|
||||
|
||||
// Find elements by ID - first try data-source-location, fallback to data-visual-selector-id
|
||||
const findElementsById = (id) => {
|
||||
if (!id) return [];
|
||||
const sourceElements = [...document.querySelectorAll(`[data-source-location="${id}"]`)];
|
||||
if (sourceElements.length > 0) {
|
||||
return sourceElements;
|
||||
}
|
||||
return [...document.querySelectorAll(`[data-visual-selector-id="${id}"]`)];
|
||||
};
|
||||
|
||||
// Clear hover overlays
|
||||
const clearHoverOverlays = () => {
|
||||
hoverOverlaysRef.current.forEach(overlay => {
|
||||
if (overlay && overlay.parentNode) {
|
||||
overlay.remove();
|
||||
}
|
||||
});
|
||||
hoverOverlaysRef.current = [];
|
||||
currentHighlightedElementsRef.current = [];
|
||||
};
|
||||
|
||||
// Handle mouse over event
|
||||
const handleMouseOver = (e) => {
|
||||
if (!isVisualEditModeRef.current || isPopoverDraggingRef.current) return;
|
||||
|
||||
// Prevent hover effects when a dropdown is open
|
||||
if (isDropdownOpenRef.current) {
|
||||
clearHoverOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent hover effects on SVG path elements
|
||||
if (e.target.tagName.toLowerCase() === 'path') {
|
||||
clearHoverOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both data-source-location and data-visual-selector-id
|
||||
const element = e.target.closest('[data-source-location], [data-visual-selector-id]');
|
||||
if (!element) {
|
||||
clearHoverOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer data-source-location, fallback to data-visual-selector-id
|
||||
const selectorId = element.dataset.sourceLocation || element.dataset.visualSelectorId;
|
||||
const useSourceLocation = !!element.dataset.sourceLocation;
|
||||
|
||||
// Skip if this element is already selected
|
||||
if (selectedElementIdRef.current === selectorId) {
|
||||
clearHoverOverlays();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all elements with the same ID
|
||||
const elements = findElementsById(selectorId, useSourceLocation);
|
||||
|
||||
// Clear previous hover overlays
|
||||
clearHoverOverlays();
|
||||
|
||||
// Create overlays for all matching elements
|
||||
elements.forEach(el => {
|
||||
const overlay = createOverlay(false);
|
||||
document.body.appendChild(overlay);
|
||||
hoverOverlaysRef.current.push(overlay);
|
||||
positionOverlay(overlay, el);
|
||||
});
|
||||
|
||||
currentHighlightedElementsRef.current = elements;
|
||||
};
|
||||
|
||||
// Handle mouse out event
|
||||
const handleMouseOut = () => {
|
||||
if (isPopoverDraggingRef.current) return;
|
||||
clearHoverOverlays();
|
||||
};
|
||||
|
||||
// Handle element click
|
||||
const handleElementClick = (e) => {
|
||||
if (!isVisualEditModeRef.current) return;
|
||||
|
||||
// Close dropdowns when clicking anywhere in iframe if a dropdown is open
|
||||
if (isDropdownOpenRef.current) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Send message to parent to close all dropdowns
|
||||
window.parent.postMessage({
|
||||
type: 'close-dropdowns'
|
||||
}, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent clicking on SVG path elements
|
||||
if (e.target.tagName.toLowerCase() === 'path') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default behavior immediately when in visual edit mode
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Support both data-source-location and data-visual-selector-id
|
||||
const element = e.target.closest('[data-source-location], [data-visual-selector-id]');
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer data-source-location, fallback to data-visual-selector-id
|
||||
const visualSelectorId = element.dataset.sourceLocation || element.dataset.visualSelectorId;
|
||||
const useSourceLocation = !!element.dataset.sourceLocation;
|
||||
|
||||
// Clear any existing selected overlays
|
||||
selectedOverlaysRef.current.forEach(overlay => {
|
||||
if (overlay && overlay.parentNode) {
|
||||
overlay.remove();
|
||||
}
|
||||
});
|
||||
selectedOverlaysRef.current = [];
|
||||
|
||||
// Find all elements with the same ID
|
||||
const elements = findElementsById(visualSelectorId, useSourceLocation);
|
||||
|
||||
// Create selected overlays for all matching elements
|
||||
elements.forEach(el => {
|
||||
const overlay = createOverlay(true);
|
||||
document.body.appendChild(overlay);
|
||||
selectedOverlaysRef.current.push(overlay);
|
||||
positionOverlay(overlay, el, true);
|
||||
});
|
||||
|
||||
selectedElementIdRef.current = visualSelectorId;
|
||||
|
||||
// Clear hover overlays
|
||||
clearHoverOverlays();
|
||||
|
||||
// Calculate element position for popover positioning
|
||||
const rect = element.getBoundingClientRect();
|
||||
const elementPosition = {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2
|
||||
};
|
||||
|
||||
// Send message to parent window with element info including position
|
||||
const elementData = {
|
||||
type: 'element-selected',
|
||||
tagName: element.tagName,
|
||||
classes: element.className?.baseVal || element.className || '',
|
||||
visualSelectorId: visualSelectorId,
|
||||
content: element.innerText,
|
||||
dataSourceLocation: element.dataset.sourceLocation,
|
||||
isDynamicContent: element.dataset.dynamicContent === 'true',
|
||||
linenumber: element.dataset.linenumber, // Keep for backward compatibility
|
||||
filename: element.dataset.filename, // Keep for backward compatibility
|
||||
position: elementPosition // Add position data for popover
|
||||
};
|
||||
window.parent.postMessage(elementData, '*');
|
||||
};
|
||||
|
||||
// Unselect the current element
|
||||
const unselectElement = () => {
|
||||
// Clear selected overlays
|
||||
selectedOverlaysRef.current.forEach(overlay => {
|
||||
if (overlay && overlay.parentNode) {
|
||||
overlay.remove();
|
||||
}
|
||||
});
|
||||
selectedOverlaysRef.current = [];
|
||||
|
||||
selectedElementIdRef.current = null;
|
||||
};
|
||||
|
||||
// Update element classes by visual selector ID
|
||||
const updateElementClasses = (visualSelectorId, classes, replace = false) => {
|
||||
// Find all elements with the same visual selector ID
|
||||
const elements = findElementsById(visualSelectorId);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update classes for all matching elements
|
||||
elements.forEach(element => {
|
||||
if (replace) {
|
||||
// For reverts, replace classes completely
|
||||
element.className = classes;
|
||||
} else {
|
||||
// For normal updates, merge with existing classes
|
||||
const currentClasses = element.className?.baseVal || element.className || '';
|
||||
element.className = twMerge(currentClasses, classes);
|
||||
}
|
||||
});
|
||||
|
||||
// Use a small delay to allow the browser to recalculate layout before repositioning
|
||||
setTimeout(() => {
|
||||
// Reposition selected overlays
|
||||
if (selectedElementIdRef.current === visualSelectorId) {
|
||||
selectedOverlaysRef.current.forEach((overlay, index) => {
|
||||
if (index < elements.length) {
|
||||
positionOverlay(overlay, elements[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reposition hover overlays if needed
|
||||
if (currentHighlightedElementsRef.current.length > 0) {
|
||||
const hoveredId = currentHighlightedElementsRef.current[0]?.dataset?.visualSelectorId;
|
||||
if (hoveredId === visualSelectorId) {
|
||||
hoverOverlaysRef.current.forEach((overlay, index) => {
|
||||
if (index < currentHighlightedElementsRef.current.length) {
|
||||
positionOverlay(overlay, currentHighlightedElementsRef.current[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 50); // Small delay to ensure the browser has time to recalculate layout
|
||||
};
|
||||
|
||||
// Update element content by visual selector ID
|
||||
const updateElementContent = (visualSelectorId, content) => {
|
||||
// Find all elements with the same visual selector ID
|
||||
const elements = findElementsById(visualSelectorId);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update content for all matching elements
|
||||
elements.forEach((element) => {
|
||||
element.innerText = content;
|
||||
});
|
||||
|
||||
// Use a small delay to allow the browser to recalculate layout before repositioning
|
||||
setTimeout(() => {
|
||||
// Reposition selected overlays
|
||||
if (selectedElementIdRef.current === visualSelectorId) {
|
||||
selectedOverlaysRef.current.forEach((overlay, index) => {
|
||||
if (index < elements.length) {
|
||||
positionOverlay(overlay, elements[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 50); // Small delay to ensure the browser has time to recalculate layout
|
||||
};
|
||||
|
||||
// Toggle visual edit mode
|
||||
const toggleVisualEditMode = (isEnabled) => {
|
||||
setIsVisualEditMode(isEnabled);
|
||||
isVisualEditModeRef.current = isEnabled;
|
||||
|
||||
if (!isEnabled) {
|
||||
// Clear hover overlays
|
||||
clearHoverOverlays();
|
||||
|
||||
// Clear selected overlays
|
||||
selectedOverlaysRef.current.forEach(overlay => {
|
||||
if (overlay && overlay.parentNode) {
|
||||
overlay.remove();
|
||||
}
|
||||
});
|
||||
selectedOverlaysRef.current = [];
|
||||
|
||||
currentHighlightedElementsRef.current = [];
|
||||
selectedElementIdRef.current = null;
|
||||
document.body.style.cursor = 'default';
|
||||
|
||||
// Remove event listeners
|
||||
document.removeEventListener('mouseover', handleMouseOver);
|
||||
document.removeEventListener('mouseout', handleMouseOut);
|
||||
document.removeEventListener('click', handleElementClick, true);
|
||||
} else {
|
||||
// Set cursor and add event listeners
|
||||
document.body.style.cursor = 'crosshair';
|
||||
document.addEventListener('mouseover', handleMouseOver);
|
||||
document.addEventListener('mouseout', handleMouseOut);
|
||||
document.addEventListener('click', handleElementClick, true); // Use capture mode
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for messages from parent window
|
||||
useEffect(() => {
|
||||
// Add IDs to elements that don't have them but have linenumbers
|
||||
const elementsWithLineNumber = document.querySelectorAll('[data-linenumber]:not([data-visual-selector-id])');
|
||||
elementsWithLineNumber.forEach((el, index) => {
|
||||
const id = `visual-id-${el.dataset.filename}-${el.dataset.linenumber}-${index}`;
|
||||
el.dataset.visualSelectorId = id;
|
||||
});
|
||||
|
||||
// Handle scroll events to update popover position
|
||||
const handleScroll = () => {
|
||||
if (selectedElementIdRef.current) {
|
||||
// Find the element using the stored ID
|
||||
const elements = findElementsById(selectedElementIdRef.current);
|
||||
if (elements.length > 0) {
|
||||
const element = elements[0];
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Check if element is in viewport
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const isInViewport = (
|
||||
rect.top < viewportHeight &&
|
||||
rect.bottom > 0 &&
|
||||
rect.left < viewportWidth &&
|
||||
rect.right > 0
|
||||
);
|
||||
|
||||
const elementPosition = {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2
|
||||
};
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'element-position-update',
|
||||
position: elementPosition,
|
||||
isInViewport: isInViewport,
|
||||
visualSelectorId: selectedElementIdRef.current
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMessage = (event) => {
|
||||
// Check origin if desired
|
||||
//if (event.origin !== 'parent-origin') return;
|
||||
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'toggle-visual-edit-mode':
|
||||
toggleVisualEditMode(message.data.enabled);
|
||||
break;
|
||||
|
||||
case 'update-classes':
|
||||
if (message.data && message.data.classes !== undefined) {
|
||||
// Update with the visual selector ID
|
||||
// Pass replace flag if provided (used for reverts)
|
||||
updateElementClasses(
|
||||
message.data.visualSelectorId,
|
||||
message.data.classes,
|
||||
message.data.replace || false
|
||||
);
|
||||
} else {
|
||||
console.warn('[Agent] Invalid update-classes message:', message);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'unselect-element':
|
||||
unselectElement();
|
||||
break;
|
||||
|
||||
case 'refresh-page':
|
||||
window.location.reload();
|
||||
break;
|
||||
|
||||
case 'update-content':
|
||||
if (message.data && message.data.content !== undefined) {
|
||||
updateElementContent(
|
||||
message.data.visualSelectorId,
|
||||
message.data.content
|
||||
);
|
||||
} else {
|
||||
console.warn('[Agent] Invalid update-content message:', message);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'request-element-position':
|
||||
// Send current position of selected element for popover repositioning
|
||||
if (selectedElementIdRef.current) {
|
||||
// Find the element using the stored ID
|
||||
const elements = findElementsById(selectedElementIdRef.current);
|
||||
if (elements.length > 0) {
|
||||
const element = elements[0];
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Check if element is in viewport
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const isInViewport = (
|
||||
rect.top < viewportHeight &&
|
||||
rect.bottom > 0 &&
|
||||
rect.left < viewportWidth &&
|
||||
rect.right > 0
|
||||
);
|
||||
|
||||
const elementPosition = {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2
|
||||
};
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'element-position-update',
|
||||
position: elementPosition,
|
||||
isInViewport: isInViewport,
|
||||
visualSelectorId: selectedElementIdRef.current
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'popover-drag-state':
|
||||
// Handle popover drag state to prevent mouseover conflicts
|
||||
if (message.data && message.data.isDragging !== undefined) {
|
||||
setIsPopoverDragging(message.data.isDragging);
|
||||
isPopoverDraggingRef.current = message.data.isDragging;
|
||||
|
||||
// Clear hover overlays when dragging starts
|
||||
if (message.data.isDragging) {
|
||||
clearHoverOverlays();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dropdown-state':
|
||||
// Handle dropdown open/close state
|
||||
if (message.data && message.data.isOpen !== undefined) {
|
||||
setIsDropdownOpen(message.data.isOpen);
|
||||
isDropdownOpenRef.current = message.data.isOpen;
|
||||
|
||||
// Clear hover overlays when dropdown opens
|
||||
if (message.data.isOpen) {
|
||||
clearHoverOverlays();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
window.addEventListener('scroll', handleScroll, true); // Use capture to catch all scroll events
|
||||
document.addEventListener('scroll', handleScroll, true); // Also listen on document
|
||||
|
||||
// Send ready message to parent
|
||||
window.parent.postMessage({ type: 'visual-edit-agent-ready' }, '*');
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
window.removeEventListener('scroll', handleScroll, true);
|
||||
document.removeEventListener('scroll', handleScroll, true);
|
||||
document.removeEventListener('mouseover', handleMouseOver);
|
||||
document.removeEventListener('mouseout', handleMouseOut);
|
||||
document.removeEventListener('click', handleElementClick, true);
|
||||
|
||||
// Clean up all overlays
|
||||
clearHoverOverlays();
|
||||
|
||||
selectedOverlaysRef.current.forEach(overlay => {
|
||||
if (overlay && overlay.parentNode) {
|
||||
overlay.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Keep the refs in sync with state changes
|
||||
useEffect(() => {
|
||||
isVisualEditModeRef.current = isVisualEditMode;
|
||||
}, [isVisualEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
isPopoverDraggingRef.current = isPopoverDragging;
|
||||
}, [isPopoverDragging]);
|
||||
|
||||
useEffect(() => {
|
||||
isDropdownOpenRef.current = isDropdownOpen;
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
// Handle window resize and scroll to reposition overlays
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
// Reposition selected overlays
|
||||
if (selectedElementIdRef.current) {
|
||||
const elements = findElementsById(selectedElementIdRef.current);
|
||||
selectedOverlaysRef.current.forEach((overlay, index) => {
|
||||
if (index < elements.length) {
|
||||
positionOverlay(overlay, elements[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reposition hover overlays
|
||||
if (currentHighlightedElementsRef.current.length > 0) {
|
||||
hoverOverlaysRef.current.forEach((overlay, index) => {
|
||||
if (index < currentHighlightedElementsRef.current.length) {
|
||||
positionOverlay(overlay, currentHighlightedElementsRef.current[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create a mutation observer to detect changes in the DOM
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
// Check if mutations affect relevant elements
|
||||
const needsUpdate = mutations.some(mutation => {
|
||||
// Check if the target or its children have data-visual-selector-id
|
||||
const hasVisualId = (node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if (node.dataset && node.dataset.visualSelectorId) {
|
||||
return true;
|
||||
}
|
||||
// Check children
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
if (hasVisualId(node.children[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check if this is a style or attribute mutation that might affect layout
|
||||
const isLayoutChange = mutation.type === 'attributes' &&
|
||||
(mutation.attributeName === 'style' ||
|
||||
mutation.attributeName === 'class' ||
|
||||
mutation.attributeName === 'width' ||
|
||||
mutation.attributeName === 'height');
|
||||
|
||||
// Check if target is or contains an element with visual selector ID
|
||||
return isLayoutChange && hasVisualId(mutation.target);
|
||||
});
|
||||
|
||||
if (needsUpdate) {
|
||||
// Use timeout to let browser calculate layout
|
||||
setTimeout(handleResize, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing
|
||||
mutationObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributeFilter: ['style', 'class', 'width', 'height']
|
||||
});
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('scroll', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('scroll', handleResize);
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// No visible UI - all functionality is handled through event listeners and message passing
|
||||
return null;
|
||||
}
|
||||
54
Migdalor-main/src/lib/app-params.js
Normal file
54
Migdalor-main/src/lib/app-params.js
Normal file
@ -0,0 +1,54 @@
|
||||
const isNode = typeof window === 'undefined';
|
||||
const windowObj = isNode ? { localStorage: new Map() } : window;
|
||||
const storage = windowObj.localStorage;
|
||||
|
||||
const toSnakeCase = (str) => {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
}
|
||||
|
||||
const getAppParamValue = (paramName, { defaultValue = undefined, removeFromUrl = false } = {}) => {
|
||||
if (isNode) {
|
||||
return defaultValue;
|
||||
}
|
||||
const storageKey = `base44_${toSnakeCase(paramName)}`;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const searchParam = urlParams.get(paramName);
|
||||
if (removeFromUrl) {
|
||||
urlParams.delete(paramName);
|
||||
const newUrl = `${window.location.pathname}${urlParams.toString() ? `?${urlParams.toString()}` : ""
|
||||
}${window.location.hash}`;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
}
|
||||
if (searchParam) {
|
||||
storage.setItem(storageKey, searchParam);
|
||||
return searchParam;
|
||||
}
|
||||
if (defaultValue) {
|
||||
storage.setItem(storageKey, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
const storedValue = storage.getItem(storageKey);
|
||||
if (storedValue) {
|
||||
return storedValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const getAppParams = () => {
|
||||
if (getAppParamValue("clear_access_token") === 'true') {
|
||||
storage.removeItem('base44_access_token');
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return {
|
||||
appId: getAppParamValue("app_id", { defaultValue: import.meta.env.VITE_BASE44_APP_ID }),
|
||||
serverUrl: getAppParamValue("server_url", { defaultValue: import.meta.env.VITE_BASE44_BACKEND_URL }),
|
||||
token: getAppParamValue("access_token", { removeFromUrl: true }),
|
||||
fromUrl: getAppParamValue("from_url", { defaultValue: window.location.href }),
|
||||
functionsVersion: getAppParamValue("functions_version", { defaultValue: import.meta.env.VITE_BASE44_FUNCTIONS_VERSION }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const appParams = {
|
||||
...getAppParams()
|
||||
}
|
||||
11
Migdalor-main/src/lib/query-client.js
Normal file
11
Migdalor-main/src/lib/query-client.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
|
||||
export const queryClientInstance = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
9
Migdalor-main/src/lib/utils.js
Normal file
9
Migdalor-main/src/lib/utils.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
|
||||
export const isIframe = window.self !== window.top;
|
||||
22
Migdalor-main/src/main.jsx
Normal file
22
Migdalor-main/src/main.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from '@/App.jsx'
|
||||
import '@/index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
// <React.StrictMode>
|
||||
<App />
|
||||
// </React.StrictMode>,
|
||||
)
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||
window.parent?.postMessage({ type: 'sandbox:beforeUpdate' }, '*');
|
||||
});
|
||||
import.meta.hot.on('vite:afterUpdate', () => {
|
||||
window.parent?.postMessage({ type: 'sandbox:afterUpdate' }, '*');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
53
Migdalor-main/src/pages.config.js
Normal file
53
Migdalor-main/src/pages.config.js
Normal file
@ -0,0 +1,53 @@
|
||||
import DailyOverview from './pages/DailyOverview';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import HackalonAssignment from './pages/HackalonAssignment';
|
||||
import HackalonManageProblems from './pages/HackalonManageProblems';
|
||||
import HackalonOverview from './pages/HackalonOverview';
|
||||
import HackalonSchedule from './pages/HackalonSchedule';
|
||||
import HackalonStatus from './pages/HackalonStatus';
|
||||
import HackalonTeamArea from './pages/HackalonTeamArea';
|
||||
import Home from './pages/Home';
|
||||
import KeyAllocation from './pages/KeyAllocation';
|
||||
import ManageCrews from './pages/ManageCrews';
|
||||
import ManageKeys from './pages/ManageKeys';
|
||||
import ManagePermissions from './pages/ManagePermissions';
|
||||
import ManagePositions from './pages/ManagePositions';
|
||||
import ManageSquads from './pages/ManageSquads';
|
||||
import ManageUsers from './pages/ManageUsers';
|
||||
import ManageZones from './pages/ManageZones';
|
||||
import MyProfile from './pages/MyProfile';
|
||||
import MySchedule from './pages/MySchedule';
|
||||
import Onboarding from './pages/Onboarding';
|
||||
import DataExport from './pages/DataExport';
|
||||
import __Layout from './Layout.jsx';
|
||||
|
||||
|
||||
export const PAGES = {
|
||||
"DailyOverview": DailyOverview,
|
||||
"Dashboard": Dashboard,
|
||||
"HackalonAssignment": HackalonAssignment,
|
||||
"HackalonManageProblems": HackalonManageProblems,
|
||||
"HackalonOverview": HackalonOverview,
|
||||
"HackalonSchedule": HackalonSchedule,
|
||||
"HackalonStatus": HackalonStatus,
|
||||
"HackalonTeamArea": HackalonTeamArea,
|
||||
"Home": Home,
|
||||
"KeyAllocation": KeyAllocation,
|
||||
"ManageCrews": ManageCrews,
|
||||
"ManageKeys": ManageKeys,
|
||||
"ManagePermissions": ManagePermissions,
|
||||
"ManagePositions": ManagePositions,
|
||||
"ManageSquads": ManageSquads,
|
||||
"ManageUsers": ManageUsers,
|
||||
"ManageZones": ManageZones,
|
||||
"MyProfile": MyProfile,
|
||||
"MySchedule": MySchedule,
|
||||
"Onboarding": Onboarding,
|
||||
"DataExport": DataExport,
|
||||
}
|
||||
|
||||
export const pagesConfig = {
|
||||
mainPage: "Home",
|
||||
Pages: PAGES,
|
||||
Layout: __Layout,
|
||||
};
|
||||
633
Migdalor-main/src/pages/DailyOverview.jsx
Normal file
633
Migdalor-main/src/pages/DailyOverview.jsx
Normal file
@ -0,0 +1,633 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Calendar, Clock, Loader2, Key, Info } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default function DailyOverview() {
|
||||
const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||
const [filterType, setFilterType] = useState('all'); // 'all', 'room', 'crew', 'platoon'
|
||||
const [selectedRoom, setSelectedRoom] = useState('');
|
||||
const [selectedCrew, setSelectedCrew] = useState('');
|
||||
const [selectedPlatoon, setSelectedPlatoon] = useState('');
|
||||
const [selectedPlatoonForCrew, setSelectedPlatoonForCrew] = useState('');
|
||||
const [selectedUnit, setSelectedUnit] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const { data: lessons = [], isLoading } = useQuery({
|
||||
queryKey: ['daily-lessons', selectedDate],
|
||||
queryFn: () => base44.entities.Lesson.filter({ date: selectedDate }, 'start_time')
|
||||
});
|
||||
|
||||
const { data: squads = [] } = useQuery({
|
||||
queryKey: ['squads'],
|
||||
queryFn: () => base44.entities.Squad.list('order')
|
||||
});
|
||||
|
||||
const { data: crews = [] } = useQuery({
|
||||
queryKey: ['crews'],
|
||||
queryFn: () => base44.entities.Crew.list('order')
|
||||
});
|
||||
|
||||
const { data: keys = [] } = useQuery({
|
||||
queryKey: ['keys'],
|
||||
queryFn: () => base44.entities.ClassroomKey.list()
|
||||
});
|
||||
|
||||
// Generate time slots (07:00 - 23:59)
|
||||
const timeSlots = useMemo(() => {
|
||||
const slots = [];
|
||||
for (let hour = 7; hour <= 23; hour++) {
|
||||
slots.push(`${String(hour).padStart(2, '0')}:00`);
|
||||
}
|
||||
return slots;
|
||||
}, []);
|
||||
|
||||
// Get all unique platoons
|
||||
const platoons = useMemo(() => {
|
||||
const platoonSet = new Set(squads.map(s => s.platoon_name).filter(Boolean));
|
||||
return Array.from(platoonSet).sort();
|
||||
}, [squads]);
|
||||
|
||||
// Get crew names (filtered by platoon if selected for crew filter)
|
||||
const crewNames = useMemo(() => {
|
||||
if (filterType === 'crew' && selectedPlatoonForCrew) {
|
||||
return squads
|
||||
.filter(s => s.platoon_name === selectedPlatoonForCrew)
|
||||
.map(s => s.squad_number)
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.match(/\d+/)?.[0] || 0);
|
||||
const numB = parseInt(b.match(/\d+/)?.[0] || 0);
|
||||
return numA - numB;
|
||||
});
|
||||
}
|
||||
return crews.map(c => c.name).sort((a, b) => a.localeCompare(b, 'he'));
|
||||
}, [crews, filterType, selectedPlatoonForCrew, squads]);
|
||||
|
||||
// Get room numbers
|
||||
const roomNumbers = useMemo(() => {
|
||||
return keys.map(k => k.room_number).sort((a, b) => {
|
||||
const numA = parseInt(a) || 0;
|
||||
const numB = parseInt(b) || 0;
|
||||
return numA - numB;
|
||||
});
|
||||
}, [keys]);
|
||||
|
||||
// Generate colors for crews/squads
|
||||
const crewColors = useMemo(() => {
|
||||
const colors = [
|
||||
'bg-blue-100 border-blue-300 text-blue-800',
|
||||
'bg-green-100 border-green-300 text-green-800',
|
||||
'bg-purple-100 border-purple-300 text-purple-800',
|
||||
'bg-pink-100 border-pink-300 text-pink-800',
|
||||
'bg-orange-100 border-orange-300 text-orange-800',
|
||||
'bg-teal-100 border-teal-300 text-teal-800',
|
||||
'bg-indigo-100 border-indigo-300 text-indigo-800',
|
||||
'bg-rose-100 border-rose-300 text-rose-800',
|
||||
];
|
||||
const colorMap = {};
|
||||
|
||||
// If filtering by platoon, assign colors to squads in that platoon
|
||||
if (filterType === 'platoon' && selectedPlatoon) {
|
||||
const platoonSquads = squads
|
||||
.filter(s => s.platoon_name === selectedPlatoon)
|
||||
.map(s => s.squad_number)
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.match(/\d+/)?.[0] || 0);
|
||||
const numB = parseInt(b.match(/\d+/)?.[0] || 0);
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
platoonSquads.forEach((squad, idx) => {
|
||||
colorMap[squad] = colors[idx % colors.length];
|
||||
});
|
||||
} else {
|
||||
// Otherwise use crew names
|
||||
crewNames.forEach((crew, idx) => {
|
||||
colorMap[crew] = colors[idx % colors.length];
|
||||
});
|
||||
}
|
||||
|
||||
return colorMap;
|
||||
}, [crewNames, filterType, selectedPlatoon, squads]);
|
||||
|
||||
// Get display data based on filter type
|
||||
const displayData = useMemo(() => {
|
||||
if (filterType === 'room' && selectedRoom) {
|
||||
// Show timeline for a specific room
|
||||
return {
|
||||
type: 'room',
|
||||
items: [{ id: selectedRoom, name: `חדר ${selectedRoom}`, platoon: null }]
|
||||
};
|
||||
} else if (filterType === 'crew' && selectedCrew) {
|
||||
// Show timeline for a specific crew
|
||||
return {
|
||||
type: 'crew',
|
||||
items: [{ id: selectedCrew, name: selectedCrew, platoon: null }]
|
||||
};
|
||||
} else if (filterType === 'platoon' && selectedPlatoon) {
|
||||
// Show the platoon itself and all squads in the platoon
|
||||
const items = [];
|
||||
|
||||
// Add the platoon row (for platoon-level lessons)
|
||||
items.push({ id: `platoon_${selectedPlatoon}`, name: selectedPlatoon, platoon: selectedPlatoon, isPlatoon: true });
|
||||
|
||||
// Add all squads in this platoon
|
||||
const platoonSquads = squads
|
||||
.filter(s => s.platoon_name === selectedPlatoon)
|
||||
.map(s => ({ id: s.id, name: s.squad_number, platoon: selectedPlatoon, isPlatoon: false }))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.name.match(/\d+/)?.[0] || 0);
|
||||
const numB = parseInt(b.name.match(/\d+/)?.[0] || 0);
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
items.push(...platoonSquads);
|
||||
|
||||
return {
|
||||
type: 'platoon',
|
||||
items: items
|
||||
};
|
||||
} else {
|
||||
// Show all platoons and crews
|
||||
const items = [];
|
||||
|
||||
// Add all platoons
|
||||
platoons.forEach(platoonName => {
|
||||
items.push({
|
||||
id: `platoon_${platoonName}`,
|
||||
name: platoonName,
|
||||
platoon: platoonName,
|
||||
isPlatoon: true
|
||||
});
|
||||
});
|
||||
|
||||
// Add only crews (not platoons)
|
||||
crews.filter(c => c.name.includes('צוות')).forEach(c => {
|
||||
items.push({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
platoon: squads.find(s => s.squad_number === c.name)?.platoon_name || null,
|
||||
isPlatoon: false
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'all',
|
||||
items: items.sort((a, b) => a.name.localeCompare(b.name, 'he'))
|
||||
};
|
||||
}
|
||||
}, [filterType, selectedRoom, selectedCrew, selectedPlatoon, crews, squads]);
|
||||
|
||||
// Check if a lesson is active during a time slot
|
||||
const isLessonActive = (lesson, timeSlot) => {
|
||||
const slotStart = timeSlot;
|
||||
const slotEnd = `${String(parseInt(timeSlot.split(':')[0]) + 1).padStart(2, '0')}:00`;
|
||||
return lesson.start_time < slotEnd && lesson.end_time > slotStart;
|
||||
};
|
||||
|
||||
// Get lesson for a specific unit and time slot
|
||||
const getLessonForSlot = (unitName, timeSlot, item) => {
|
||||
if (displayData.type === 'room') {
|
||||
// For room view, find any lesson using this room
|
||||
return lessons.find(lesson =>
|
||||
lesson.assigned_key === unitName.replace('חדר ', '') && isLessonActive(lesson, timeSlot)
|
||||
);
|
||||
} else {
|
||||
// For crew/platoon view, find lesson by crew name OR platoon name (if it's a platoon row)
|
||||
return lessons.find(lesson => {
|
||||
if (!isLessonActive(lesson, timeSlot)) return false;
|
||||
|
||||
// If this is a platoon row, show platoon-level lessons
|
||||
if (item?.isPlatoon) {
|
||||
return lesson.crew_name === unitName || lesson.platoon_name === unitName;
|
||||
}
|
||||
|
||||
// Otherwise, show crew lessons
|
||||
return lesson.crew_name === unitName;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get all lessons for a unit
|
||||
const getUnitLessons = (unitName) => {
|
||||
return lessons.filter(lesson => lesson.crew_name === unitName)
|
||||
.sort((a, b) => a.start_time.localeCompare(b.start_time));
|
||||
};
|
||||
|
||||
// Handle cell click
|
||||
const handleCellClick = (unitName, lesson) => {
|
||||
setSelectedUnit({ name: unitName, lesson });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
// Get status color
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'assigned':
|
||||
return 'bg-green-100 border-green-300 text-green-800';
|
||||
case 'pending':
|
||||
return 'bg-yellow-100 border-yellow-300 text-yellow-800';
|
||||
case 'completed':
|
||||
return 'bg-slate-100 border-slate-300 text-slate-600';
|
||||
default:
|
||||
return 'bg-slate-50 border-slate-200 text-slate-500';
|
||||
}
|
||||
};
|
||||
|
||||
// Get cell color based on filter type
|
||||
const getCellColor = (lesson) => {
|
||||
if (!lesson) return '';
|
||||
|
||||
if (displayData.type === 'platoon') {
|
||||
// If this is a platoon-level lesson (crew_name is the platoon itself), use unique platoon color
|
||||
if (lesson.crew_name === selectedPlatoon) {
|
||||
return 'bg-amber-100 border-amber-300 text-amber-800';
|
||||
}
|
||||
// Different color for each squad/crew
|
||||
return crewColors[lesson.crew_name] || getStatusColor(lesson.status);
|
||||
} else {
|
||||
// Default status color
|
||||
return getStatusColor(lesson.status);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-[95vw] mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
תמונת מצב יומית 📊
|
||||
</h1>
|
||||
<p className="text-slate-500">מעקב אחר לוח הזמנים של כל הצוותים והפלוגות</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex flex-col gap-4 mb-6">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">תאריך:</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">סינון לפי:</Label>
|
||||
<Select value={filterType} onValueChange={(value) => {
|
||||
setFilterType(value);
|
||||
setSelectedRoom('');
|
||||
setSelectedCrew('');
|
||||
setSelectedPlatoon('');
|
||||
setSelectedPlatoonForCrew('');
|
||||
}} dir="rtl">
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
<SelectItem value="all">הכל</SelectItem>
|
||||
<SelectItem value="room">חדר ספציפי</SelectItem>
|
||||
<SelectItem value="crew">צוות ספציפי</SelectItem>
|
||||
<SelectItem value="platoon">פלוגה ספציפית</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secondary filters */}
|
||||
{filterType === 'room' && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">בחר חדר:</Label>
|
||||
<Select value={selectedRoom} onValueChange={setSelectedRoom} dir="rtl">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="בחר חדר..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
{roomNumbers.map((room) => (
|
||||
<SelectItem key={room} value={room}>חדר {room}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterType === 'crew' && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">בחר פלוגה:</Label>
|
||||
<Select value={selectedPlatoonForCrew} onValueChange={(value) => {
|
||||
setSelectedPlatoonForCrew(value);
|
||||
setSelectedCrew('');
|
||||
}} dir="rtl">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="בחר פלוגה..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
{platoons.map((platoon) => (
|
||||
<SelectItem key={platoon} value={platoon}>{platoon}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedPlatoonForCrew && (
|
||||
<>
|
||||
<Label className="text-sm font-medium">בחר צוות:</Label>
|
||||
<Select value={selectedCrew} onValueChange={setSelectedCrew} dir="rtl">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="בחר צוות..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
{crewNames.map((crew) => (
|
||||
<SelectItem key={crew} value={crew}>{crew}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterType === 'platoon' && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">בחר פלוגה:</Label>
|
||||
<Select value={selectedPlatoon} onValueChange={setSelectedPlatoon} dir="rtl">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="בחר פלוגה..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
{platoons.map((platoon) => (
|
||||
<SelectItem key={platoon} value={platoon}>{platoon}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
||||
<Card className="p-4">
|
||||
<p className="text-sm text-slate-500">סה״כ שיעורים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{lessons.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-green-50 border-green-200">
|
||||
<p className="text-sm text-green-600">שובצו</p>
|
||||
<p className="text-2xl font-bold text-green-700">
|
||||
{lessons.filter(l => l.status === 'assigned').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-yellow-50 border-yellow-200">
|
||||
<p className="text-sm text-yellow-600">ממתינים</p>
|
||||
<p className="text-2xl font-bold text-yellow-700">
|
||||
{lessons.filter(l => l.status === 'pending').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<p className="text-sm text-blue-600">יחידות פעילות</p>
|
||||
<p className="text-2xl font-bold text-blue-700">
|
||||
{new Set(lessons.map(l => l.crew_name)).size}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<Card className="p-4 mb-6">
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm">
|
||||
<span className="font-semibold text-slate-700">מקרא:</span>
|
||||
{displayData.type === 'platoon' ? (
|
||||
<>
|
||||
<Badge className="bg-amber-100 border-amber-300 text-amber-800 hover:bg-amber-100">
|
||||
{selectedPlatoon} (פלוגה)
|
||||
</Badge>
|
||||
{displayData.items
|
||||
.filter(item => !item.isPlatoon)
|
||||
.map(item => (
|
||||
<Badge
|
||||
key={item.id}
|
||||
className={`${crewColors[item.name]} hover:${crewColors[item.name]}`}
|
||||
>
|
||||
{item.name}
|
||||
</Badge>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Badge className="bg-green-100 text-green-800 hover:bg-green-100">שובץ</Badge>
|
||||
<Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">ממתין</Badge>
|
||||
<Badge className="bg-slate-100 text-slate-600 hover:bg-slate-100">הושלם</Badge>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Timeline Table */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
) : (
|
||||
<Card className="overflow-hidden border-slate-200">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b-2 border-slate-200">
|
||||
<th className="sticky right-0 z-20 bg-slate-50 p-3 text-right font-semibold text-slate-700 border-l-2 border-slate-200 min-w-[150px]">
|
||||
{displayData.type === 'room' ? 'חדר' :
|
||||
displayData.type === 'crew' ? 'צוות' :
|
||||
displayData.type === 'platoon' ? 'צוותים בפלוגה' : 'צוותים'}
|
||||
</th>
|
||||
{timeSlots.map((slot) => (
|
||||
<th key={slot} className="p-2 text-center font-medium text-slate-600 text-sm min-w-[100px] border-l border-slate-200">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{slot}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayData.items.map((item) => (
|
||||
<tr key={item.id} className="border-b border-slate-200 hover:bg-slate-50/50">
|
||||
<td className="sticky right-0 z-10 bg-white p-3 font-medium text-slate-700 border-l-2 border-slate-200">
|
||||
{item.name}
|
||||
</td>
|
||||
{timeSlots.map((slot) => {
|
||||
const lesson = getLessonForSlot(item.name, slot, item);
|
||||
return (
|
||||
<td key={slot} className="p-1 border-l border-slate-200 align-middle">
|
||||
{lesson ? (
|
||||
<div
|
||||
onClick={() => handleCellClick(item.name, lesson)}
|
||||
className={`p-2 rounded border-2 text-center text-xs cursor-pointer hover:shadow-lg transition-all ${getCellColor(lesson)}`}>
|
||||
{displayData.type === 'room' ? (
|
||||
<>
|
||||
<div className="font-bold mb-1">{lesson.crew_name}</div>
|
||||
<div className="text-[10px] opacity-75">
|
||||
{lesson.start_time}-{lesson.end_time}
|
||||
</div>
|
||||
{lesson.room_type_needed && (
|
||||
<div className="mt-1">
|
||||
{lesson.room_type_needed === 'פלוגתי' ? '🏢' : '🏠'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-bold mb-1">
|
||||
{lesson.assigned_key ? `חדר ${lesson.assigned_key}` : 'לא שובץ'}
|
||||
</div>
|
||||
<div className="text-[10px] opacity-75">
|
||||
{lesson.start_time}-{lesson.end_time}
|
||||
</div>
|
||||
{displayData.type === 'platoon' && (
|
||||
<div className="text-[10px] font-semibold mt-1">
|
||||
{lesson.crew_name}
|
||||
</div>
|
||||
)}
|
||||
{displayData.type === 'all' && (
|
||||
<div className="text-[10px] opacity-75 mt-1">
|
||||
{lesson.crew_name.includes('צוות')
|
||||
? `צוות: ${lesson.crew_name}`
|
||||
: `פלוגה: ${lesson.platoon_name}`
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full min-h-[60px]"></div>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
{displayData.items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={timeSlots.length + 1} className="p-8 text-center text-slate-400">
|
||||
{filterType === 'room' && !selectedRoom && 'בחר חדר להצגת לוח הזמנים'}
|
||||
{filterType === 'crew' && !selectedCrew && 'בחר צוות להצגת לוח הזמנים'}
|
||||
{filterType === 'platoon' && !selectedPlatoon && 'בחר פלוגה להצגת לוח הזמנים'}
|
||||
{filterType === 'all' && 'אין צוותים להצגה'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Details Modal */}
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent className="sm:max-w-lg" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex items-center gap-2 flex-row-reverse justify-end">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<Info className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
פרטי שיעור
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedUnit && selectedUnit.lesson && (
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Crew/Platoon Info */}
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="text-sm text-slate-600 mb-1">
|
||||
{displayData.type === 'room' ? 'חדר' : 'צוות'}:
|
||||
</div>
|
||||
<div className="text-xl font-bold text-slate-800">{selectedUnit.name}</div>
|
||||
{selectedUnit.lesson.platoon_name && (
|
||||
<Badge variant="outline" className="mt-2 bg-purple-50">
|
||||
{selectedUnit.lesson.platoon_name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lesson Details */}
|
||||
<Card className={`p-4 border-2 ${getStatusColor(selectedUnit.lesson.status)}`}>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-5 h-5 text-slate-600" />
|
||||
<div>
|
||||
<div className="text-sm text-slate-500">שעות השיעור</div>
|
||||
<div className="text-lg font-bold">
|
||||
{selectedUnit.lesson.end_time} - {selectedUnit.lesson.start_time}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedUnit.lesson.assigned_key ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Key className="w-5 h-5 text-slate-600" />
|
||||
<div>
|
||||
<div className="text-sm text-slate-500">חדר משובץ</div>
|
||||
<div className="text-lg font-bold">חדר {selectedUnit.lesson.assigned_key}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Key className="w-5 h-5 text-slate-400" />
|
||||
<div className="text-slate-500">טרם שובץ חדר</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-3 border-t">
|
||||
<div className="text-sm text-slate-500 mb-2">דרישות</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline">
|
||||
{selectedUnit.lesson.room_type_needed === 'פלוגתי' ? '🏢 חדר פלוגתי' : '🏠 חדר צוותי'}
|
||||
</Badge>
|
||||
{selectedUnit.lesson.needs_computers && (
|
||||
<Badge variant="outline">💻 נדרשים מחשבים</Badge>
|
||||
)}
|
||||
<Badge className={
|
||||
selectedUnit.lesson.status === 'assigned'
|
||||
? 'bg-green-600 hover:bg-green-600'
|
||||
: 'bg-yellow-600 hover:bg-yellow-600'
|
||||
}>
|
||||
{selectedUnit.lesson.status === 'assigned' ? '✓ שובץ' : '⏳ ממתין'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedUnit.lesson.notes && (
|
||||
<div className="pt-3 border-t">
|
||||
<div className="text-sm text-slate-500 mb-1">הערות</div>
|
||||
<div className="text-sm text-slate-700">{selectedUnit.lesson.notes}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
520
Migdalor-main/src/pages/Dashboard.jsx
Normal file
520
Migdalor-main/src/pages/Dashboard.jsx
Normal file
@ -0,0 +1,520 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Plus, Key, Clock, Users, Filter, Calendar } from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import KeyCard from '@/components/keys/KeyCard';
|
||||
import CheckoutModal from '@/components/keys/CheckoutModal';
|
||||
import WaitingQueueCard from '@/components/queue/WaitingQueueCard';
|
||||
import AddToQueueModal from '@/components/queue/AddToQueueModal';
|
||||
import StatsBar from '@/components/stats/StatsBar';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [checkoutKey, setCheckoutKey] = useState(null);
|
||||
const [showQueueModal, setShowQueueModal] = useState(false);
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [timeFilter, setTimeFilter] = useState({ start: '', end: '' });
|
||||
const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||
const [user, setUser] = useState(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [userLoading, setUserLoading] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(user => {
|
||||
console.log('👤 User loaded in Dashboard:', {
|
||||
email: user.email,
|
||||
platoon_name: user.platoon_name,
|
||||
squad_name: user.squad_name
|
||||
});
|
||||
setUser(user);
|
||||
setUserLoading(false);
|
||||
}).catch(() => {
|
||||
setUserLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { data: keys = [], isLoading: keysLoading } = useQuery({
|
||||
queryKey: ['keys'],
|
||||
queryFn: () => base44.entities.ClassroomKey.list()
|
||||
});
|
||||
|
||||
const { data: crews = [] } = useQuery({
|
||||
queryKey: ['crews'],
|
||||
queryFn: () => base44.entities.Crew.list()
|
||||
});
|
||||
|
||||
const { data: squads = [] } = useQuery({
|
||||
queryKey: ['squads'],
|
||||
queryFn: () => base44.entities.Squad.list('order')
|
||||
});
|
||||
|
||||
const { data: allQueue = [] } = useQuery({
|
||||
queryKey: ['queue', selectedDate],
|
||||
queryFn: async () => {
|
||||
return base44.entities.WaitingQueue.filter({ date: selectedDate }, 'priority');
|
||||
}
|
||||
});
|
||||
|
||||
// Filter crews and squads based on user's platoon (admins see all)
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
// Filter queue by platoon and squad (admins see all)
|
||||
const queue = (isAdmin || !user
|
||||
? allQueue
|
||||
: allQueue.filter(item =>
|
||||
item.platoon_name === user.platoon_name ||
|
||||
item.crew_name === user.squad_name
|
||||
));
|
||||
|
||||
const filteredCrews = (isAdmin || !user?.platoon_name
|
||||
? crews
|
||||
: crews.filter((crew) => crew.name === user.platoon_name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'he'));
|
||||
|
||||
const filteredSquads = (isAdmin || !user?.platoon_name
|
||||
? squads
|
||||
: squads.filter((squad) => squad.platoon_name === user.platoon_name))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.squad_number.match(/\d+/)?.[0] || 0);
|
||||
const numB = parseInt(b.squad_number.match(/\d+/)?.[0] || 0);
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
const { data: todayLessons = [] } = useQuery({
|
||||
queryKey: ['today-lessons', selectedDate],
|
||||
queryFn: async () => {
|
||||
return base44.entities.Lesson.filter({ date: selectedDate });
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-return keys after end time
|
||||
React.useEffect(() => {
|
||||
if (!keys.length) return;
|
||||
|
||||
const checkAndReturnKeys = async () => {
|
||||
const now = new Date();
|
||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
const keysToReturn = keys.filter(key =>
|
||||
key.status === 'taken' &&
|
||||
key.checkout_end_time &&
|
||||
currentTime >= key.checkout_end_time
|
||||
);
|
||||
|
||||
for (const key of keysToReturn) {
|
||||
await updateKeyMutation.mutateAsync({
|
||||
id: key.id,
|
||||
data: {
|
||||
status: 'available',
|
||||
current_holder: null,
|
||||
checkout_time: null,
|
||||
checkout_start_time: null,
|
||||
checkout_end_time: null,
|
||||
checked_out_by: null
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(checkAndReturnKeys, 60000); // Check every minute
|
||||
checkAndReturnKeys(); // Check immediately on mount
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [keys]);
|
||||
|
||||
// Get current key holder for a room (considering time filter if active)
|
||||
const getCurrentHolder = (roomNumber) => {
|
||||
// If time filter is active, don't show current holder
|
||||
if (timeFilter.start && timeFilter.end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
const currentLesson = todayLessons.find(lesson =>
|
||||
lesson.assigned_key === roomNumber &&
|
||||
lesson.start_time <= currentTime &&
|
||||
lesson.end_time > currentTime
|
||||
);
|
||||
|
||||
return currentLesson ? currentLesson.crew_name : null;
|
||||
};
|
||||
|
||||
const updateKeyMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.ClassroomKey.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||||
toast.success('מפתח עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const addToQueueMutation = useMutation({
|
||||
mutationFn: (data) => {
|
||||
return base44.entities.WaitingQueue.create({
|
||||
...data,
|
||||
date: selectedDate,
|
||||
priority: queue.length + 1,
|
||||
crew_manager: user?.email
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['queue'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
setShowQueueModal(false);
|
||||
toast.success('נוסף לתור המתנה');
|
||||
}
|
||||
});
|
||||
|
||||
const removeFromQueueMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.WaitingQueue.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['queue'] });
|
||||
toast.success('הוסר מהתור');
|
||||
}
|
||||
});
|
||||
|
||||
const handleCheckout = async (key, holderName, startTime, endTime, platoonName) => {
|
||||
if (!user?.email) {
|
||||
toast.error('שגיאה: משתמש לא מחובר');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for time conflicts with existing lessons
|
||||
const existingLessons = await base44.entities.Lesson.filter({
|
||||
date: selectedDate,
|
||||
assigned_key: key.room_number
|
||||
});
|
||||
|
||||
// Check for overlap
|
||||
const hasConflict = existingLessons.some(lesson =>
|
||||
lesson.start_time < endTime && startTime < lesson.end_time
|
||||
);
|
||||
|
||||
if (hasConflict) {
|
||||
toast.error('החדר תפוס בשעות אלה. בדוק את לוח הזמנים');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a lesson for this checkout
|
||||
await base44.entities.Lesson.create({
|
||||
crew_manager: user.email,
|
||||
crew_name: holderName,
|
||||
platoon_name: platoonName || '',
|
||||
date: selectedDate,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
room_type_needed: key.room_type,
|
||||
needs_computers: key.has_computers || false,
|
||||
assigned_key: key.room_number,
|
||||
status: 'assigned',
|
||||
notes: 'משיכה ידנית מלוח בקרה'
|
||||
});
|
||||
|
||||
// Update key status
|
||||
updateKeyMutation.mutate({
|
||||
id: key.id,
|
||||
data: {
|
||||
status: 'taken',
|
||||
current_holder: holderName,
|
||||
checkout_time: new Date().toISOString(),
|
||||
checkout_start_time: startTime,
|
||||
checkout_end_time: endTime,
|
||||
checked_out_by: user?.email
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh lessons
|
||||
queryClient.invalidateQueries({ queryKey: ['today-lessons'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['my-lessons'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['lessons'] });
|
||||
|
||||
setCheckoutKey(null);
|
||||
};
|
||||
|
||||
const handleReturn = (key) => {
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const isKeyOwner = key.checked_out_by === user?.email;
|
||||
|
||||
if (!isAdmin && !isKeyOwner) {
|
||||
toast.error('רק המשתמש שלקח את המפתח או המנהל יכולים להחזיר אותו');
|
||||
return;
|
||||
}
|
||||
|
||||
updateKeyMutation.mutate({
|
||||
id: key.id,
|
||||
data: {
|
||||
status: 'available',
|
||||
current_holder: null,
|
||||
checkout_time: null,
|
||||
checkout_start_time: null,
|
||||
checkout_end_time: null,
|
||||
checked_out_by: null
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleMoveUp = async (item) => {
|
||||
const currentIndex = queue.findIndex((q) => q.id === item.id);
|
||||
if (currentIndex > 0) {
|
||||
const prevItem = queue[currentIndex - 1];
|
||||
await base44.entities.WaitingQueue.update(item.id, { priority: prevItem.priority });
|
||||
await base44.entities.WaitingQueue.update(prevItem.id, { priority: item.priority });
|
||||
queryClient.invalidateQueries({ queryKey: ['queue'] });
|
||||
}
|
||||
};
|
||||
|
||||
// Check if a key is available during the time filter
|
||||
const isKeyAvailableInTimeRange = (key) => {
|
||||
if (!timeFilter.start || !timeFilter.end) return true;
|
||||
|
||||
// Find lessons for this specific room
|
||||
const roomLessons = todayLessons.filter(lesson =>
|
||||
lesson.assigned_key === key.room_number &&
|
||||
(lesson.status === 'assigned' || lesson.status === 'pending')
|
||||
);
|
||||
|
||||
// Check if any lesson conflicts with this time range
|
||||
const hasConflict = roomLessons.some(lesson => {
|
||||
// Check time overlap: lessons overlap if start1 < end2 AND start2 < end1
|
||||
const overlap = lesson.start_time < timeFilter.end && timeFilter.start < lesson.end_time;
|
||||
return overlap;
|
||||
});
|
||||
|
||||
return !hasConflict;
|
||||
};
|
||||
|
||||
const filteredKeys = keys
|
||||
.filter((k) => filter === 'all' || k.room_type === filter)
|
||||
.filter((k) => isKeyAvailableInTimeRange(k));
|
||||
|
||||
if (userLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-800"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול מפתחות 🔑
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<StatsBar keys={keys} queueCount={queue.length} />
|
||||
|
||||
{/* Main Content */}
|
||||
<Tabs defaultValue="keys" className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row-reverse sm:items-center sm:justify-between gap-4">
|
||||
<TabsList className="bg-white border border-slate-200 p-1">
|
||||
|
||||
<TabsTrigger value="queue" className="data-[state=active]:bg-slate-100">
|
||||
תור ({queue.length})
|
||||
<Clock className="w-4 h-4 ml-2" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="keys" className="data-[state=active]:bg-slate-100">
|
||||
מפתחות
|
||||
<Key className="w-4 h-4 ml-2" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowQueueModal(true)} className="bg-background text-blue-600 px-4 py-2 text-sm font-medium rounded-md inline-flex items-center justify-center gap-2 whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border shadow-sm hover:text-accent-foreground h-9 border-blue-200">
|
||||
|
||||
|
||||
הוסף בקשה מיוחדת
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="keys" className="space-y-6">
|
||||
{/* Filters */}
|
||||
<div className="space-y-4">
|
||||
{/* Room Type Filter */}
|
||||
<div className="flex flex-row-reverse items-center gap-2">
|
||||
<span className="text-sm text-slate-500">:סוג חדר</span>
|
||||
<Filter className="w-4 h-4 text-slate-400" />
|
||||
<div className="flex flex-row-reverse gap-2">
|
||||
{['all', 'צוותי', 'פלוגתי'].map((f) =>
|
||||
<Button
|
||||
key={f}
|
||||
variant={filter === f ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter(f)}
|
||||
className={filter === f ? 'bg-slate-800' : ''}>
|
||||
|
||||
{f === 'all' ? 'הכל' : f === 'צוותי' ? '🏠 צוותי' : '🏢 פלוגתי'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date and Time Range Filter */}
|
||||
<div className="flex flex-col sm:flex-row sm:flex-row-reverse items-stretch sm:items-center gap-4 bg-white p-4 rounded-lg border border-slate-200">
|
||||
<div className="flex flex-row-reverse items-center gap-3">
|
||||
<Label className="text-sm font-medium text-slate-700">:תאריך</Label>
|
||||
<Calendar className="w-4 h-4 text-slate-600" />
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse items-center gap-3">
|
||||
<span className="text-sm font-medium text-slate-700">:סנן לפי זמינות</span>
|
||||
<Clock className="w-5 h-5 text-slate-600" />
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="time"
|
||||
value={timeFilter.end}
|
||||
onChange={(e) => setTimeFilter({ ...timeFilter, end: e.target.value })}
|
||||
className="px-3 py-1.5 border border-slate-300 rounded-md text-sm"
|
||||
/>
|
||||
<span className="text-slate-500">עד</span>
|
||||
<input
|
||||
type="time"
|
||||
value={timeFilter.start}
|
||||
onChange={(e) => setTimeFilter({ ...timeFilter, start: e.target.value })}
|
||||
className="px-3 py-1.5 border border-slate-300 rounded-md text-sm"
|
||||
/>
|
||||
|
||||
{(timeFilter.start || timeFilter.end) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setTimeFilter({ start: '', end: '' })}
|
||||
className="text-slate-500 hover:text-slate-700"
|
||||
>
|
||||
נקה
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{timeFilter.start && timeFilter.end && (
|
||||
<span className="text-xs text-emerald-600 font-medium">
|
||||
מציג {filteredKeys.length} כיתות זמינות
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keys Grid */}
|
||||
{keysLoading ?
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) =>
|
||||
<div key={i} className="h-48 bg-slate-100 rounded-xl animate-pulse" />
|
||||
)}
|
||||
</div> :
|
||||
filteredKeys.length === 0 ?
|
||||
<div className="text-center py-16 bg-white rounded-2xl border border-dashed border-slate-200">
|
||||
<Key className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-600 mb-2">אין מפתחות עדיין</h3>
|
||||
<p className="text-slate-400 mb-4">הוסף את המפתח הראשון שלך כדי להתחיל</p>
|
||||
</div> :
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" dir="rtl">
|
||||
<AnimatePresence>
|
||||
{filteredKeys.map((key) =>
|
||||
<KeyCard
|
||||
key={key.id}
|
||||
keyItem={key}
|
||||
crews={crews}
|
||||
currentUser={user}
|
||||
currentHolder={getCurrentHolder(key.room_number)}
|
||||
isAvailableInTimeRange={isKeyAvailableInTimeRange(key)}
|
||||
timeFilterActive={!!(timeFilter.start && timeFilter.end)}
|
||||
onCheckout={(key) => {
|
||||
setCheckoutKey({
|
||||
...key,
|
||||
prefilledTimes: timeFilter.start && timeFilter.end ? timeFilter : null
|
||||
});
|
||||
}}
|
||||
onReturn={handleReturn} />
|
||||
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="queue" className="space-y-4">
|
||||
{queue.length === 0 ?
|
||||
<div className="text-center py-16 bg-white rounded-2xl border border-dashed border-slate-200">
|
||||
<Clock className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-600 mb-2">אין בקשות מיוחדות</h3>
|
||||
<Button onClick={() => setShowQueueModal(true)}>
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף לתור
|
||||
</Button>
|
||||
</div> :
|
||||
|
||||
<div className="space-y-3">
|
||||
<AnimatePresence>
|
||||
{queue.map((item, index) =>
|
||||
<WaitingQueueCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
position={index + 1}
|
||||
onRemove={() => removeFromQueueMutation.mutate(item.id)}
|
||||
onMoveUp={handleMoveUp}
|
||||
isAdmin={isAdmin} />
|
||||
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{checkoutKey && (
|
||||
<CheckoutModal
|
||||
open={!!checkoutKey}
|
||||
onClose={() => setCheckoutKey(null)}
|
||||
keyItem={checkoutKey}
|
||||
crews={filteredCrews}
|
||||
squads={filteredSquads}
|
||||
currentUser={user}
|
||||
selectedDate={selectedDate}
|
||||
onConfirm={handleCheckout} />
|
||||
)}
|
||||
|
||||
{showQueueModal && (
|
||||
<AddToQueueModal
|
||||
open={showQueueModal}
|
||||
onClose={() => setShowQueueModal(false)}
|
||||
crews={filteredCrews}
|
||||
squads={filteredSquads}
|
||||
currentUser={user}
|
||||
onConfirm={(data) => addToQueueMutation.mutate(data)} />
|
||||
)}
|
||||
|
||||
</div>);
|
||||
|
||||
}
|
||||
549
Migdalor-main/src/pages/DataExport.jsx
Normal file
549
Migdalor-main/src/pages/DataExport.jsx
Normal file
@ -0,0 +1,549 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Database, Download, Loader2, CheckCircle2, Copy } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function DataExport() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDDL, setLoadingDDL] = useState(false);
|
||||
const [loadingRLS, setLoadingRLS] = useState(false);
|
||||
const [exportedSQL, setExportedSQL] = useState('');
|
||||
const [selectedEntities, setSelectedEntities] = useState({});
|
||||
|
||||
const entities = [
|
||||
'HackalonSubmission',
|
||||
'HackalonTeam',
|
||||
'HackalonDepartment',
|
||||
'HackalonScheduleItem',
|
||||
'ClassroomKey',
|
||||
'Crew',
|
||||
'Squad',
|
||||
'Position',
|
||||
'Zone',
|
||||
'PositionPermission',
|
||||
'WaitingQueue',
|
||||
'Lesson'
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
try {
|
||||
const u = await base44.auth.me();
|
||||
setUser(u);
|
||||
|
||||
// Initialize all entities as selected
|
||||
const initial = {};
|
||||
entities.forEach(e => initial[e] = true);
|
||||
setSelectedEntities(initial);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const escapeSQLString = (str) => {
|
||||
if (str === null || str === undefined) return 'NULL';
|
||||
if (typeof str === 'boolean') return str ? 'true' : 'false';
|
||||
if (typeof str === 'number') return str;
|
||||
if (Array.isArray(str)) return `'${JSON.stringify(str).replace(/'/g, "''")}'`;
|
||||
if (typeof str === 'object') return `'${JSON.stringify(str).replace(/'/g, "''")}'`;
|
||||
return `'${String(str).replace(/'/g, "''")}'`;
|
||||
};
|
||||
|
||||
const formatDateForSQL = (date) => {
|
||||
if (!date) return 'NULL';
|
||||
try {
|
||||
return `'${new Date(date).toISOString()}'`;
|
||||
} catch {
|
||||
return 'NULL';
|
||||
}
|
||||
};
|
||||
|
||||
const generateUUID = () => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
const generateSQLForEntity = (entityName, records, schema) => {
|
||||
if (!records || records.length === 0) return '';
|
||||
|
||||
const tableName = entityName.toLowerCase();
|
||||
let sql = `-- ${entityName} (${records.length} records)\n`;
|
||||
|
||||
// Get all possible columns from schema and records
|
||||
const schemaFields = schema?.properties ? Object.keys(schema.properties) : [];
|
||||
const recordFields = new Set();
|
||||
records.forEach(record => {
|
||||
Object.keys(record).forEach(key => recordFields.add(key));
|
||||
});
|
||||
|
||||
const allFields = [...new Set([...schemaFields, ...recordFields])];
|
||||
const fields = ['id', 'created_date', 'updated_date', 'created_by', ...allFields.filter(f => !['id', 'created_date', 'updated_date', 'created_by'].includes(f))];
|
||||
|
||||
records.forEach(record => {
|
||||
const values = fields.map(field => {
|
||||
const value = record[field];
|
||||
|
||||
// Handle special fields
|
||||
if (field === 'id') return escapeSQLString(record.id || generateUUID());
|
||||
if (field === 'created_date' || field === 'updated_date') return formatDateForSQL(value);
|
||||
if (field === 'created_by') return escapeSQLString(value);
|
||||
|
||||
// Handle different data types based on schema
|
||||
const fieldSchema = schema?.properties?.[field];
|
||||
if (fieldSchema) {
|
||||
if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
|
||||
return formatDateForSQL(value);
|
||||
}
|
||||
if (fieldSchema.type === 'boolean') {
|
||||
return value === true ? 'true' : 'false';
|
||||
}
|
||||
if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
|
||||
return value !== null && value !== undefined ? value : 'NULL';
|
||||
}
|
||||
if (fieldSchema.type === 'array' || fieldSchema.type === 'object') {
|
||||
return escapeSQLString(value);
|
||||
}
|
||||
}
|
||||
|
||||
return escapeSQLString(value);
|
||||
});
|
||||
|
||||
sql += `INSERT INTO ${tableName} (${fields.join(', ')}) VALUES (${values.join(', ')});\n`;
|
||||
});
|
||||
|
||||
sql += '\n';
|
||||
return sql;
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setLoading(true);
|
||||
let allSQL = `-- Base44 Data Export to PostgreSQL\n`;
|
||||
allSQL += `-- Generated: ${new Date().toISOString()}\n`;
|
||||
allSQL += `-- App: ${window.location.origin}\n\n`;
|
||||
allSQL += `BEGIN;\n\n`;
|
||||
|
||||
try {
|
||||
for (const entityName of entities) {
|
||||
if (!selectedEntities[entityName]) continue;
|
||||
|
||||
try {
|
||||
// Fetch entity data
|
||||
const records = await base44.entities[entityName].list();
|
||||
|
||||
// Fetch entity schema
|
||||
let schema = null;
|
||||
try {
|
||||
schema = await base44.entities[entityName].schema();
|
||||
} catch (e) {
|
||||
console.warn(`Could not fetch schema for ${entityName}:`, e);
|
||||
}
|
||||
|
||||
// Generate SQL
|
||||
const sql = generateSQLForEntity(entityName, records, schema);
|
||||
allSQL += sql;
|
||||
} catch (error) {
|
||||
console.error(`Error exporting ${entityName}:`, error);
|
||||
allSQL += `-- Error exporting ${entityName}: ${error.message}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
allSQL += `COMMIT;\n`;
|
||||
setExportedSQL(allSQL);
|
||||
toast.success('הנתונים יוצאו בהצלחה');
|
||||
} catch (error) {
|
||||
toast.error('שגיאה בייצוא הנתונים');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([exportedSQL], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `base44_export_${new Date().toISOString().split('T')[0]}.sql`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('הקובץ הורד בהצלחה');
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(exportedSQL);
|
||||
toast.success('הועתק ללוח');
|
||||
};
|
||||
|
||||
const getSQLType = (fieldSchema) => {
|
||||
if (!fieldSchema) return 'TEXT';
|
||||
|
||||
if (fieldSchema.type === 'string') {
|
||||
if (fieldSchema.format === 'date') return 'DATE';
|
||||
if (fieldSchema.format === 'date-time') return 'TIMESTAMP WITH TIME ZONE';
|
||||
if (fieldSchema.enum) return 'TEXT';
|
||||
return 'TEXT';
|
||||
}
|
||||
if (fieldSchema.type === 'number') return 'NUMERIC';
|
||||
if (fieldSchema.type === 'integer') return 'INTEGER';
|
||||
if (fieldSchema.type === 'boolean') return 'BOOLEAN';
|
||||
if (fieldSchema.type === 'array') return 'JSONB';
|
||||
if (fieldSchema.type === 'object') return 'JSONB';
|
||||
|
||||
return 'TEXT';
|
||||
};
|
||||
|
||||
const generateDDL = async () => {
|
||||
setLoadingDDL(true);
|
||||
let ddl = `-- DDL (Data Definition Language) Export\n`;
|
||||
ddl += `-- Generated: ${new Date().toISOString()}\n`;
|
||||
ddl += `-- App: ${window.location.origin}\n\n`;
|
||||
|
||||
try {
|
||||
for (const entityName of entities) {
|
||||
if (!selectedEntities[entityName]) continue;
|
||||
|
||||
try {
|
||||
const schema = await base44.entities[entityName].schema();
|
||||
const tableName = entityName.toLowerCase();
|
||||
|
||||
ddl += `-- ${entityName}\n`;
|
||||
ddl += `CREATE TABLE ${tableName} (\n`;
|
||||
|
||||
// Built-in fields
|
||||
ddl += ` id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n`;
|
||||
ddl += ` created_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n`;
|
||||
ddl += ` updated_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n`;
|
||||
ddl += ` created_by TEXT,\n`;
|
||||
|
||||
// Schema fields
|
||||
if (schema?.properties) {
|
||||
const fields = Object.entries(schema.properties);
|
||||
fields.forEach(([fieldName, fieldSchema], idx) => {
|
||||
const sqlType = getSQLType(fieldSchema);
|
||||
const isRequired = schema.required?.includes(fieldName);
|
||||
const notNull = isRequired ? ' NOT NULL' : '';
|
||||
const defaultVal = fieldSchema.default !== undefined
|
||||
? ` DEFAULT ${typeof fieldSchema.default === 'string' ? `'${fieldSchema.default}'` : fieldSchema.default}`
|
||||
: '';
|
||||
|
||||
ddl += ` ${fieldName} ${sqlType}${notNull}${defaultVal}`;
|
||||
if (idx < fields.length - 1) ddl += ',';
|
||||
if (fieldSchema.description) {
|
||||
ddl += ` -- ${fieldSchema.description}`;
|
||||
}
|
||||
ddl += '\n';
|
||||
});
|
||||
}
|
||||
|
||||
ddl += `);\n\n`;
|
||||
|
||||
// Add indexes
|
||||
ddl += `CREATE INDEX idx_${tableName}_created_date ON ${tableName}(created_date);\n`;
|
||||
ddl += `CREATE INDEX idx_${tableName}_created_by ON ${tableName}(created_by);\n\n`;
|
||||
|
||||
} catch (error) {
|
||||
ddl += `-- Error generating DDL for ${entityName}: ${error.message}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([ddl], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `base44_ddl_${new Date().toISOString().split('T')[0]}.sql`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success('קובץ DDL הורד בהצלחה');
|
||||
} catch (error) {
|
||||
toast.error('שגיאה ביצירת DDL');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingDDL(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateRLS = async () => {
|
||||
setLoadingRLS(true);
|
||||
let rls = `-- RLS (Row Level Security) Policies Export\n`;
|
||||
rls += `-- Generated: ${new Date().toISOString()}\n`;
|
||||
rls += `-- App: ${window.location.origin}\n\n`;
|
||||
|
||||
try {
|
||||
for (const entityName of entities) {
|
||||
if (!selectedEntities[entityName]) continue;
|
||||
|
||||
try {
|
||||
const schema = await base44.entities[entityName].schema();
|
||||
const tableName = entityName.toLowerCase();
|
||||
|
||||
if (!schema?.rls) {
|
||||
rls += `-- ${entityName}: No RLS policies defined\n\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
rls += `-- ${entityName} RLS Policies\n`;
|
||||
rls += `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;\n\n`;
|
||||
|
||||
// Create policies
|
||||
if (schema.rls.read) {
|
||||
rls += `-- Read Policy\n`;
|
||||
rls += `CREATE POLICY "${tableName}_read_policy" ON ${tableName}\n`;
|
||||
rls += ` FOR SELECT\n`;
|
||||
rls += ` USING (\n`;
|
||||
rls += ` -- ${JSON.stringify(schema.rls.read, null, 2).replace(/\n/g, '\n -- ')}\n`;
|
||||
rls += ` true -- Customize based on your RLS rules\n`;
|
||||
rls += ` );\n\n`;
|
||||
}
|
||||
|
||||
if (schema.rls.create) {
|
||||
rls += `-- Create Policy\n`;
|
||||
rls += `CREATE POLICY "${tableName}_create_policy" ON ${tableName}\n`;
|
||||
rls += ` FOR INSERT\n`;
|
||||
rls += ` WITH CHECK (\n`;
|
||||
rls += ` -- ${JSON.stringify(schema.rls.create, null, 2).replace(/\n/g, '\n -- ')}\n`;
|
||||
rls += ` true -- Customize based on your RLS rules\n`;
|
||||
rls += ` );\n\n`;
|
||||
}
|
||||
|
||||
if (schema.rls.update) {
|
||||
rls += `-- Update Policy\n`;
|
||||
rls += `CREATE POLICY "${tableName}_update_policy" ON ${tableName}\n`;
|
||||
rls += ` FOR UPDATE\n`;
|
||||
rls += ` USING (\n`;
|
||||
rls += ` -- ${JSON.stringify(schema.rls.update, null, 2).replace(/\n/g, '\n -- ')}\n`;
|
||||
rls += ` true -- Customize based on your RLS rules\n`;
|
||||
rls += ` );\n\n`;
|
||||
}
|
||||
|
||||
if (schema.rls.delete) {
|
||||
rls += `-- Delete Policy\n`;
|
||||
rls += `CREATE POLICY "${tableName}_delete_policy" ON ${tableName}\n`;
|
||||
rls += ` FOR DELETE\n`;
|
||||
rls += ` USING (\n`;
|
||||
rls += ` -- ${JSON.stringify(schema.rls.delete, null, 2).replace(/\n/g, '\n -- ')}\n`;
|
||||
rls += ` true -- Customize based on your RLS rules\n`;
|
||||
rls += ` );\n\n`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
rls += `-- Error generating RLS for ${entityName}: ${error.message}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([rls], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `base44_rls_${new Date().toISOString().split('T')[0]}.sql`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success('קובץ RLS הורד בהצלחה');
|
||||
} catch (error) {
|
||||
toast.error('שגיאה ביצירת RLS');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingRLS(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.role !== 'admin') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Database className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">רק מנהלים יכולים לייצא נתונים</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2 flex items-center gap-3">
|
||||
<Database className="w-8 h-8 text-indigo-600" />
|
||||
ייצוא נתונים ל-PostgreSQL
|
||||
</h1>
|
||||
<p className="text-slate-500">ייצוא כל הנתונים בפורמט SQL תואם Supabase</p>
|
||||
</div>
|
||||
|
||||
{/* Entity Selection */}
|
||||
<Card className="p-6 mb-6">
|
||||
<h2 className="text-lg font-bold text-slate-800 mb-4">בחר טבלאות לייצוא</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{entities.map(entity => (
|
||||
<label key={entity} className="flex items-center gap-2 p-3 bg-slate-50 rounded-lg cursor-pointer hover:bg-slate-100 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedEntities[entity] || false}
|
||||
onChange={(e) => setSelectedEntities({
|
||||
...selectedEntities,
|
||||
[entity]: e.target.checked
|
||||
})}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span className="text-sm font-medium text-slate-700">{entity}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const all = {};
|
||||
entities.forEach(e => all[e] = true);
|
||||
setSelectedEntities(all);
|
||||
}}
|
||||
>
|
||||
בחר הכל
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const none = {};
|
||||
entities.forEach(e => none[e] = false);
|
||||
setSelectedEntities(none);
|
||||
}}
|
||||
>
|
||||
בטל הכל
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Export Buttons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="font-semibold text-slate-800 mb-2">ייצוא נתונים</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">INSERT statements</p>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={loading || Object.values(selectedEntities).every(v => !v)}
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 ml-2 animate-spin" />
|
||||
מייצא...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Database className="w-4 h-4 ml-2" />
|
||||
ייצא נתונים
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="font-semibold text-slate-800 mb-2">DDL Schema</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">CREATE TABLE statements</p>
|
||||
<Button
|
||||
onClick={generateDDL}
|
||||
disabled={loadingDDL || Object.values(selectedEntities).every(v => !v)}
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{loadingDDL ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 ml-2 animate-spin" />
|
||||
מייצא...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 ml-2" />
|
||||
הורד DDL
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="font-semibold text-slate-800 mb-2">RLS Policies</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">Row Level Security</p>
|
||||
<Button
|
||||
onClick={generateRLS}
|
||||
disabled={loadingRLS || Object.values(selectedEntities).every(v => !v)}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
{loadingRLS ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 ml-2 animate-spin" />
|
||||
מייצא...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 ml-2" />
|
||||
הורד RLS
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{exportedSQL && (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||||
<h2 className="text-lg font-bold text-slate-800">SQL מוכן</h2>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleCopy} variant="outline" size="sm">
|
||||
<Copy className="w-4 h-4 ml-2" />
|
||||
העתק
|
||||
</Button>
|
||||
<Button onClick={handleDownload} size="sm">
|
||||
<Download className="w-4 h-4 ml-2" />
|
||||
הורד קובץ
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<pre className="text-xs font-mono" dir="ltr">{exportedSQL}</pre>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<h3 className="font-semibold text-blue-900 mb-2">הוראות שימוש:</h3>
|
||||
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
|
||||
<li>צור טבלאות ב-Supabase שתואמות את ה-schema של Base44</li>
|
||||
<li>העתק את הקוד SQL או הורד את הקובץ</li>
|
||||
<li>הרץ את הקוד ב-Supabase SQL Editor</li>
|
||||
<li>וודא שכל ה-foreign keys מוגדרים נכון</li>
|
||||
</ol>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
731
Migdalor-main/src/pages/HackalonAssignment.jsx
Normal file
731
Migdalor-main/src/pages/HackalonAssignment.jsx
Normal file
@ -0,0 +1,731 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Users, Building2, Plus, Edit2, Trash2, Loader2, Shield } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function HackalonAssignment() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [showDeptModal, setShowDeptModal] = useState(false);
|
||||
const [showTeamModal, setShowTeamModal] = useState(false);
|
||||
const [showAddMembersModal, setShowAddMembersModal] = useState(false);
|
||||
const [editingDept, setEditingDept] = useState(null);
|
||||
const [editingTeam, setEditingTeam] = useState(null);
|
||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||
const [selectedUserForAssign, setSelectedUserForAssign] = useState(null);
|
||||
const [showUserAssignModal, setShowUserAssignModal] = useState(false);
|
||||
const [deptForm, setDeptForm] = useState({ name: '', icon: 'Users', classroom: '' });
|
||||
const [teamForm, setTeamForm] = useState({ name: '', department: '' });
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [newMemberName, setNewMemberName] = useState('');
|
||||
const [filterView, setFilterView] = useState('all');
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const { data: departments = [] } = useQuery({
|
||||
queryKey: ['hackalon-departments'],
|
||||
queryFn: () => base44.entities.HackalonDepartment.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: teams = [] } = useQuery({
|
||||
queryKey: ['hackalon-teams'],
|
||||
queryFn: () => base44.entities.HackalonTeam.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: users = [] } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => base44.entities.User.list(),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: classroomKeys = [] } = useQuery({
|
||||
queryKey: ['classroom-keys'],
|
||||
queryFn: () => base44.entities.ClassroomKey.list(),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
// Department mutations
|
||||
const createDeptMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.HackalonDepartment.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-departments'] });
|
||||
setShowDeptModal(false);
|
||||
setDeptForm({ name: '', icon: 'Users', classroom: '' });
|
||||
toast.success('המדור נוצר בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateDeptMutation = useMutation({
|
||||
mutationFn: async ({ id, data, oldName }) => {
|
||||
// Update the department
|
||||
await base44.entities.HackalonDepartment.update(id, data);
|
||||
|
||||
// If name changed, update all teams in this department
|
||||
if (oldName && data.name !== oldName) {
|
||||
const deptTeams = teams.filter((t) => t.department_name === oldName);
|
||||
for (const team of deptTeams) {
|
||||
await base44.entities.HackalonTeam.update(team.id, {
|
||||
department_name: data.name
|
||||
});
|
||||
|
||||
// Also update users assigned to this team
|
||||
const teamUsers = users.filter((u) => u.hackalon_department === oldName && u.hackalon_team === team.name);
|
||||
for (const user of teamUsers) {
|
||||
await base44.entities.User.update(user.id, {
|
||||
hackalon_department: data.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-departments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setShowDeptModal(false);
|
||||
setEditingDept(null);
|
||||
setDeptForm({ name: '', icon: 'Users', classroom: '' });
|
||||
toast.success('המדור עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteDeptMutation = useMutation({
|
||||
mutationFn: async (deptId) => {
|
||||
const dept = departments.find((d) => d.id === deptId);
|
||||
|
||||
// Delete all teams in this department first
|
||||
const deptTeams = teams.filter((t) => t.department_name === dept.name);
|
||||
for (const team of deptTeams) {
|
||||
// Remove team assignments from all users
|
||||
const teamUsers = users.filter((u) => u.hackalon_team === team.name);
|
||||
for (const user of teamUsers) {
|
||||
await base44.entities.User.update(user.id, {
|
||||
hackalon_team: null,
|
||||
hackalon_department: null
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the team
|
||||
await base44.entities.HackalonTeam.delete(team.id);
|
||||
}
|
||||
|
||||
// Finally delete the department
|
||||
return base44.entities.HackalonDepartment.delete(deptId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-departments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
toast.success('המדור וכל הצוותים שלו נמחקו בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
// Team mutations
|
||||
const createTeamMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.HackalonTeam.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
setShowTeamModal(false);
|
||||
setTeamForm({ name: '', department: '' });
|
||||
toast.success('הצוות נוצר בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateTeamMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.HackalonTeam.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
setShowTeamModal(false);
|
||||
setEditingTeam(null);
|
||||
setTeamForm({ name: '', department: '' });
|
||||
toast.success('הצוות עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteTeamMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.HackalonTeam.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
toast.success('הצוות נמחק בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
// Add member to team mutation
|
||||
const addMemberMutation = useMutation({
|
||||
mutationFn: async ({ teamId, memberName, userId }) => {
|
||||
const team = teams.find((t) => t.id === teamId);
|
||||
const updatedMembers = [...(team.member_names || []), memberName];
|
||||
|
||||
await base44.entities.HackalonTeam.update(teamId, { member_names: updatedMembers });
|
||||
|
||||
// If user exists, assign them
|
||||
if (userId) {
|
||||
await base44.entities.User.update(userId, {
|
||||
hackalon_department: team.department_name,
|
||||
hackalon_team: team.name
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
toast.success('הצוער נוסף בהצלחה');
|
||||
setSearchQuery('');
|
||||
setNewMemberName('');
|
||||
}
|
||||
});
|
||||
|
||||
// Remove member from team mutation
|
||||
const removeMemberMutation = useMutation({
|
||||
mutationFn: async ({ teamId, memberName }) => {
|
||||
const team = teams.find((t) => t.id === teamId);
|
||||
const updatedMembers = (team.member_names || []).filter((m) => m !== memberName);
|
||||
|
||||
await base44.entities.HackalonTeam.update(teamId, { member_names: updatedMembers });
|
||||
|
||||
// Remove assignment from user if exists
|
||||
const userToUpdate = users.find((u) => (u.onboarding_full_name || u.full_name) === memberName && u.hackalon_team === team.name);
|
||||
if (userToUpdate) {
|
||||
await base44.entities.User.update(userToUpdate.id, {
|
||||
hackalon_department: null,
|
||||
hackalon_team: null
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
toast.success('הצוער הוסר בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSaveDept = () => {
|
||||
if (!deptForm.name.trim()) return;
|
||||
|
||||
// Convert form data to match database fields
|
||||
const dataToSave = {
|
||||
name: deptForm.name,
|
||||
icon: deptForm.icon,
|
||||
classroom_number: deptForm.classroom // המרה לשדה הנכון
|
||||
};
|
||||
|
||||
if (editingDept) {
|
||||
updateDeptMutation.mutate({
|
||||
id: editingDept.id,
|
||||
data: dataToSave,
|
||||
oldName: editingDept.name
|
||||
});
|
||||
} else {
|
||||
createDeptMutation.mutate(dataToSave);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignUserToTeam = (teamId, teamName, deptName) => {
|
||||
if (!selectedUserForAssign) return;
|
||||
|
||||
const team = teams.find((t) => t.id === teamId);
|
||||
const memberName = selectedUserForAssign.onboarding_full_name || selectedUserForAssign.full_name;
|
||||
|
||||
addMemberMutation.mutate({
|
||||
teamId,
|
||||
memberName,
|
||||
userId: selectedUserForAssign.id
|
||||
});
|
||||
|
||||
setShowUserAssignModal(false);
|
||||
setSelectedUserForAssign(null);
|
||||
};
|
||||
|
||||
const handleSaveTeam = () => {
|
||||
if (!teamForm.name.trim() || !teamForm.department) return;
|
||||
|
||||
const data = {
|
||||
name: teamForm.name,
|
||||
department_name: teamForm.department
|
||||
};
|
||||
|
||||
if (editingTeam) {
|
||||
updateTeamMutation.mutate({ id: editingTeam.id, data });
|
||||
} else {
|
||||
createTeamMutation.mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAddMembers = (team) => {
|
||||
setSelectedTeam(team);
|
||||
setShowAddMembersModal(true);
|
||||
setSearchQuery('');
|
||||
setNewMemberName('');
|
||||
};
|
||||
|
||||
const handleAddExistingUser = (u) => {
|
||||
if (!selectedTeam) return;
|
||||
const memberName = u.onboarding_full_name || u.full_name;
|
||||
addMemberMutation.mutate({ teamId: selectedTeam.id, memberName, userId: u.id });
|
||||
};
|
||||
|
||||
const handleAddNewMember = () => {
|
||||
if (!selectedTeam || !newMemberName.trim()) return;
|
||||
addMemberMutation.mutate({ teamId: selectedTeam.id, memberName: newMemberName.trim(), userId: null });
|
||||
};
|
||||
|
||||
const handleRemoveMember = (teamId, memberName) => {
|
||||
removeMemberMutation.mutate({ teamId, memberName });
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">רק מנהלי מערכת יכולים לשבץ צוערים</p>
|
||||
</Card>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
const assignedUsers = users.filter((u) => u.hackalon_team);
|
||||
const unassignedUsers = users.filter((u) => !u.hackalon_team);
|
||||
|
||||
// Get total members including those not yet registered
|
||||
const getTotalMembers = () => {
|
||||
return teams.reduce((sum, team) => sum + (team.member_names?.length || 0), 0);
|
||||
};
|
||||
|
||||
// Filter users for search
|
||||
const filteredUsers = users.filter((u) => {
|
||||
const name = (u.onboarding_full_name || u.full_name || '').toLowerCase();
|
||||
const email = (u.email || '').toLowerCase();
|
||||
const query = searchQuery.toLowerCase();
|
||||
return name.includes(query) || email.includes(query);
|
||||
});
|
||||
|
||||
// Filter view logic
|
||||
const getDisplayedUsers = () => {
|
||||
if (filterView === 'assigned') return assignedUsers;
|
||||
if (filterView === 'unassigned') return unassignedUsers;
|
||||
return users;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
📋 שיבוץ צוערים ל-HackAlon
|
||||
</h1>
|
||||
<p className="text-slate-500">ניהול מדורים, צוותים ושיבוץ צוערים</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<Card
|
||||
className={`p-4 cursor-pointer transition-all ${filterView === 'all' ? 'ring-2 ring-blue-500 bg-blue-50' : 'hover:shadow-md'}`}
|
||||
onClick={() => setFilterView('all')}>
|
||||
|
||||
<p className="text-sm text-slate-500">סה״כ משתמשים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{users.length}</p>
|
||||
</Card>
|
||||
<Card
|
||||
className={`p-4 cursor-pointer transition-all ${filterView === 'assigned' ? 'ring-2 ring-green-500 bg-green-50' : 'bg-green-50 border-green-200 hover:shadow-md'}`}
|
||||
onClick={() => setFilterView('assigned')}>
|
||||
|
||||
<p className="text-sm text-green-600">משובצים</p>
|
||||
<p className="text-2xl font-bold text-green-700">{assignedUsers.length}</p>
|
||||
</Card>
|
||||
<Card
|
||||
className={`p-4 cursor-pointer transition-all ${filterView === 'unassigned' ? 'ring-2 ring-orange-500 bg-orange-50' : 'bg-orange-50 border-orange-200 hover:shadow-md'}`}
|
||||
onClick={() => setFilterView('unassigned')}>
|
||||
|
||||
<p className="text-sm text-orange-600">ממתינים לשיבוץ</p>
|
||||
<p className="text-2xl font-bold text-orange-700">{unassignedUsers.length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Teams by Department */}
|
||||
<div className="space-y-6 mb-6">
|
||||
{departments.map((dept) => {
|
||||
const deptTeams = teams.filter((t) => t.department_name === dept.name);
|
||||
return (
|
||||
<Card key={dept.id} className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Building2 className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-800">{dept.name}</h2>
|
||||
<p className="text-sm text-slate-500">כיתה {dept.classroom_number || 'לא הוגדר'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => {setEditingDept(dept);setDeptForm({ name: dept.name, icon: dept.icon, classroom: dept.classroom_number || '' });setShowDeptModal(true);}}>
|
||||
<Edit2 className="w-4 h-4 ml-2" />
|
||||
ערוך מדור
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => deleteDeptMutation.mutate(dept.id)} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4 ml-2" />
|
||||
מחק
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{deptTeams.map((team) =>
|
||||
<Card key={team.id} className="p-4 hover:shadow-md transition-all cursor-pointer" onClick={() => handleOpenAddMembers(team)}>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-slate-800">{team.name}</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingTeam(team);
|
||||
setTeamForm({ name: team.name, department: team.department_name });
|
||||
setShowTeamModal(true);
|
||||
}}>
|
||||
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm(`האם אתה בטוח שברצונך למחוק את הצוות "${team.name}"?`)) {
|
||||
deleteTeamMutation.mutate(team.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:text-red-700">
|
||||
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{team.member_names?.length || 0} צוערים</span>
|
||||
</div>
|
||||
|
||||
{team.member_names && team.member_names.length > 0 &&
|
||||
<div className="mt-2 space-y-1">
|
||||
{team.member_names.slice(0, 3).map((name, idx) =>
|
||||
<p key={idx} className="text-xs text-slate-500">• {name}</p>
|
||||
)}
|
||||
{team.member_names.length > 3 &&
|
||||
<p className="text-xs text-slate-400">ועוד {team.member_names.length - 3}...</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="p-4 border-dashed border-2 flex items-center justify-center cursor-pointer hover:bg-slate-50" onClick={() => {setShowTeamModal(true);setEditingTeam(null);setTeamForm({ name: '', department: dept.name });}}>
|
||||
<div className="text-center">
|
||||
<Plus className="w-8 h-8 text-slate-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-slate-500">הוסף צוות</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>);
|
||||
|
||||
})}
|
||||
|
||||
{/* Add Department Card */}
|
||||
<Card className="p-6 border-dashed border-2 flex items-center justify-center cursor-pointer hover:bg-slate-50" onClick={() => {setShowDeptModal(true);setEditingDept(null);setDeptForm({ name: '', icon: 'Users', classroom: '' });}}>
|
||||
<div className="text-center">
|
||||
<Plus className="w-8 h-8 text-slate-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-slate-500">הוסף מדור</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Management Sections - Hidden */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6 hidden">
|
||||
{/* Departments */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-slate-800">מדורים</h2>
|
||||
<Button onClick={() => {setShowDeptModal(true);setEditingDept(null);setDeptForm({ name: '', icon: 'Users', classroom: '' });}} size="sm">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף מדור
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{departments.map((dept) =>
|
||||
<div key={dept.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
||||
<span className="font-medium text-slate-800">{dept.name}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={() => {setEditingDept(dept);setDeptForm({ name: dept.name, icon: dept.icon, classroom: dept.classroom_number || '' });setShowDeptModal(true);}}>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => deleteDeptMutation.mutate(dept.id)} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Teams */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-slate-800">צוותים</h2>
|
||||
<Button onClick={() => {setShowTeamModal(true);setEditingTeam(null);setTeamForm({ name: '', department: '' });}} size="sm">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף צוות
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{teams.map((team) =>
|
||||
<div key={team.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-slate-800">{team.name}</p>
|
||||
<p className="text-sm text-slate-500">{team.department_name}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={() => {
|
||||
setEditingTeam(team);
|
||||
setTeamForm({ name: team.name, department: team.department_name });
|
||||
setShowTeamModal(true);
|
||||
}}>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => deleteTeamMutation.mutate(team.id)} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* User List */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-4">רשימת צוערים</h2>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{getDisplayedUsers().map((u) =>
|
||||
<div key={u.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-slate-800">{u.onboarding_full_name || u.full_name}</p>
|
||||
<p className="text-sm text-slate-500">{u.email}</p>
|
||||
{u.hackalon_team &&
|
||||
<p className="text-sm text-green-600 mt-1">✓ {u.hackalon_department} • {u.hackalon_team}</p>
|
||||
}
|
||||
</div>
|
||||
<Button size="sm" onClick={() => {setSelectedUserForAssign(u);setShowUserAssignModal(true);}}>
|
||||
{u.hackalon_team ? 'שנה צוות' : 'שבץ לצוות'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{getDisplayedUsers().length === 0 &&
|
||||
<p className="text-center text-slate-400 py-8">אין משתמשים להצגה</p>
|
||||
}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Department Modal */}
|
||||
// Department Modal
|
||||
<Dialog open={showDeptModal} onOpenChange={setShowDeptModal}>
|
||||
<DialogContent dir="rtl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold leading-none tracking-tight text-right">{editingDept ? 'ערוך מדור' : 'הוסף מדור'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>שם המדור</Label>
|
||||
<Input value={deptForm.name} onChange={(e) => setDeptForm({ ...deptForm, name: e.target.value })} placeholder="הזן שם מדור" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>מספר כיתה</Label>
|
||||
<select value={deptForm.classroom} onChange={(e) => setDeptForm({ ...deptForm, classroom: e.target.value })} className="w-full px-3 py-2 border rounded-md">
|
||||
<option value="">בחר כיתה...</option>
|
||||
{classroomKeys.map((key) => {
|
||||
// בדיקה אם הכיתה תפוסה על ידי מדור אחר
|
||||
const isOccupied = departments.some(
|
||||
(d) => d.classroom_number === key.room_number &&
|
||||
(!editingDept || d.id !== editingDept.id) // לא תופס על ידי המדור הנוכחי
|
||||
);
|
||||
|
||||
return (
|
||||
<option
|
||||
key={key.id}
|
||||
value={key.room_number}
|
||||
disabled={isOccupied}
|
||||
>
|
||||
כיתה {key.room_number} ({key.room_type}) {isOccupied ? '- תפוס' : ''}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveDept} disabled={!deptForm.name.trim()} className="flex-1">שמור</Button>
|
||||
<Button variant="outline" onClick={() => setShowDeptModal(false)} className="flex-1">ביטול</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Team Modal */}
|
||||
<Dialog open={showTeamModal} onOpenChange={setShowTeamModal}>
|
||||
<DialogContent dir="rtl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingTeam ? 'ערוך צוות' : 'הוסף צוות'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>שם הצוות</Label>
|
||||
<Input value={teamForm.name} onChange={(e) => setTeamForm({ ...teamForm, name: e.target.value })} placeholder="הזן שם צוות" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>מדור</Label>
|
||||
<select value={teamForm.department} onChange={(e) => setTeamForm({ ...teamForm, department: e.target.value })} className="w-full px-3 py-2 border rounded-md">
|
||||
<option value="">בחר מדור...</option>
|
||||
{departments.map((d) => <option key={d.id} value={d.name}>{d.name} - כיתה {d.classroom_number}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveTeam} disabled={!teamForm.name.trim() || !teamForm.department} className="flex-1">שמור</Button>
|
||||
<Button variant="outline" onClick={() => setShowTeamModal(false)} className="flex-1">ביטול</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* User Assignment Modal */}
|
||||
<Dialog open={showUserAssignModal} onOpenChange={setShowUserAssignModal}>
|
||||
<DialogContent dir="rtl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>שבץ לצוות - {selectedUserForAssign?.onboarding_full_name || selectedUserForAssign?.full_name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{departments.map((dept) => {
|
||||
const deptTeams = teams.filter((t) => t.department_name === dept.name);
|
||||
return (
|
||||
<div key={dept.id} className="border rounded-lg p-3">
|
||||
<h3 className="font-semibold text-slate-800 mb-2">{dept.name}</h3>
|
||||
<div className="space-y-1">
|
||||
{deptTeams.map((team) =>
|
||||
<Button
|
||||
key={team.id}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => handleAssignUserToTeam(team.id, team.name, dept.name)}>
|
||||
|
||||
{team.name}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
})}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add Members Modal */}
|
||||
<Dialog open={showAddMembersModal} onOpenChange={setShowAddMembersModal}>
|
||||
<DialogContent dir="rtl" className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold leading-none tracking-tight text-right">הוספת צוערים - {selectedTeam?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedTeam &&
|
||||
<div className="space-y-4 flex-1 overflow-hidden flex flex-col">
|
||||
{/* Current Members */}
|
||||
<div className="border-b pb-4">
|
||||
<h3 className="font-semibold text-slate-700 mb-2">צוערים בצוות ({selectedTeam.member_names?.length || 0})</h3>
|
||||
{selectedTeam.member_names && selectedTeam.member_names.length > 0 ?
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTeam.member_names.map((name, idx) =>
|
||||
<div key={idx} className="flex items-center gap-2 px-3 py-1 bg-slate-100 rounded-lg">
|
||||
<span className="text-sm">{name}</span>
|
||||
<button onClick={() => handleRemoveMember(selectedTeam.id, name)} className="text-red-600 hover:text-red-700">
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div> :
|
||||
|
||||
<p className="text-sm text-slate-400">אין צוערים עדיין</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Search Existing Users */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<h3 className="font-semibold text-slate-700 mb-2">הוסף צוער קיים</h3>
|
||||
<Input
|
||||
placeholder="חפש לפי שם או אימייל..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="mb-2" />
|
||||
|
||||
<div className="space-y-2 overflow-y-auto flex-1">
|
||||
{filteredUsers.
|
||||
filter((u) => !selectedTeam.member_names?.includes(u.onboarding_full_name || u.full_name)).
|
||||
map((u) =>
|
||||
<div key={u.id} className="flex items-center justify-between p-2 bg-slate-50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{u.onboarding_full_name || u.full_name}</p>
|
||||
<p className="text-xs text-slate-500">{u.email}</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => handleAddExistingUser(u)}>הוסף</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add New Member */}
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-semibold text-slate-700 mb-2">הוסף צוער שעדיין לא נרשם</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="שם מלא"
|
||||
value={newMemberName}
|
||||
onChange={(e) => setNewMemberName(e.target.value)} />
|
||||
|
||||
<Button onClick={handleAddNewMember} disabled={!newMemberName.trim()}>הוסף</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
301
Migdalor-main/src/pages/HackalonManageProblems.jsx
Normal file
301
Migdalor-main/src/pages/HackalonManageProblems.jsx
Normal file
@ -0,0 +1,301 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Edit2, Lightbulb, Loader2, Shield } from 'lucide-react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function HackalonManageProblems() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||
const [problemForm, setProblemForm] = useState({
|
||||
name: '',
|
||||
intro: '',
|
||||
objective: '',
|
||||
requirements: '',
|
||||
template_url: '',
|
||||
deadline: '',
|
||||
final_deadline: ''
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const { data: departments = [] } = useQuery({
|
||||
queryKey: ['hackalon-departments'],
|
||||
queryFn: () => base44.entities.HackalonDepartment.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: teams = [] } = useQuery({
|
||||
queryKey: ['hackalon-teams'],
|
||||
queryFn: () => base44.entities.HackalonTeam.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const updateProblemMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.HackalonTeam.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-teams'] });
|
||||
setShowModal(false);
|
||||
setSelectedTeam(null);
|
||||
setProblemForm({ name: '', intro: '', objective: '', requirements: '', template_url: '', deadline: '', final_deadline: '' });
|
||||
toast.success('הבעיה עודכנה בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleEditProblem = (team) => {
|
||||
setSelectedTeam(team);
|
||||
setProblemForm({
|
||||
name: team.problem_name || '',
|
||||
intro: team.problem_intro || '',
|
||||
objective: team.problem_objective || '',
|
||||
requirements: team.problem_requirements || '',
|
||||
template_url: team.specification_template_url || '',
|
||||
deadline: team.specification_deadline ? new Date(team.specification_deadline).toISOString().slice(0, 16) : '',
|
||||
final_deadline: team.final_product_deadline ? new Date(team.final_product_deadline).toISOString().slice(0, 16) : ''
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSaveProblem = () => {
|
||||
if (!selectedTeam) return;
|
||||
|
||||
updateProblemMutation.mutate({
|
||||
id: selectedTeam.id,
|
||||
data: {
|
||||
problem_name: problemForm.name,
|
||||
problem_intro: problemForm.intro,
|
||||
problem_objective: problemForm.objective,
|
||||
problem_requirements: problemForm.requirements,
|
||||
specification_template_url: problemForm.template_url || null,
|
||||
specification_deadline: problemForm.deadline ? new Date(problemForm.deadline).toISOString() : null,
|
||||
final_product_deadline: problemForm.final_deadline ? new Date(problemForm.final_deadline).toISOString() : null
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">רק מנהלי מערכת יכולים לנהל בעיות</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
🎯 ניהול בעיות HackAlon
|
||||
</h1>
|
||||
<p className="text-slate-500">הגדרת בעיות לכל צוות</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||
<Card className="p-4">
|
||||
<p className="text-sm text-slate-500">סה״כ צוותים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{teams.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-green-50 border-green-200">
|
||||
<p className="text-sm text-green-600">צוותים עם בעיה מוגדרת</p>
|
||||
<p className="text-2xl font-bold text-green-700">{teams.filter(t => t.problem_name).length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Departments and Teams */}
|
||||
<div className="space-y-6">
|
||||
{departments.map((dept, index) => {
|
||||
const deptTeams = teams.filter(t => t.department_name === dept.name);
|
||||
return (
|
||||
<motion.div
|
||||
key={dept.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="p-6">
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-4">{dept.name}</h3>
|
||||
|
||||
{deptTeams.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{deptTeams.map(team => (
|
||||
<Card key={team.id} className="p-4 bg-slate-50 border-slate-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-slate-800 mb-2">{team.name}</h4>
|
||||
|
||||
{team.problem_name ? (
|
||||
<div>
|
||||
<p className="text-sm text-slate-700">{team.problem_name}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-400 italic">אין בעיה מוגדרת</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button size="sm" onClick={() => handleEditProblem(team)} className="mr-4">
|
||||
<Edit2 className="w-4 h-4 ml-2" />
|
||||
{team.problem_name ? 'ערוך' : 'הגדר בעיה'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-400 text-center py-4">אין צוותים במדור זה</p>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Edit Problem Modal */}
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent dir="rtl" className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold leading-none tracking-tight text-right">הגדרת בעיה - {selectedTeam?.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-6">
|
||||
{/* Template Upload Section */}
|
||||
<div className="border-b pb-4">
|
||||
<Label className="text-base font-semibold mb-2 block">טמפלייט למסמך איפיון</Label>
|
||||
<p className="text-xs text-slate-500 mb-2">העלה קובץ שהצוערים יכולים להוריד ולמלא</p>
|
||||
<input
|
||||
type="file"
|
||||
id="template-upload"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const { file_url } = await base44.integrations.Core.UploadFile({ file });
|
||||
setProblemForm({...problemForm, template_url: file_url});
|
||||
toast.success('הטמפלייט הועלה בהצלחה');
|
||||
} catch (error) {
|
||||
toast.error('שגיאה בהעלאת הטמפלייט');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<label htmlFor="template-upload">
|
||||
<Button type="button" asChild variant="outline">
|
||||
<span>העלה טמפלייט</span>
|
||||
</Button>
|
||||
</label>
|
||||
{problemForm.template_url && (
|
||||
<a href={problemForm.template_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button type="button" variant="outline">צפה בטמפלייט הנוכחי</Button>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deadline Section */}
|
||||
<div className="border-b pb-4">
|
||||
<Label className="text-base font-semibold mb-2 block">דדליין להגשת מסמך איפיון</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={problemForm.deadline || ''}
|
||||
onChange={(e) => setProblemForm({...problemForm, deadline: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Final Product Deadline Section */}
|
||||
<div className="border-b pb-4">
|
||||
<Label className="text-base font-semibold mb-2 block">דדליין להגשת תוצר סופי</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={problemForm.final_deadline || ''}
|
||||
onChange={(e) => setProblemForm({...problemForm, final_deadline: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-base font-semibold mb-2 block">שם הבעיה</Label>
|
||||
<Input
|
||||
value={problemForm.name}
|
||||
onChange={(e) => setProblemForm({...problemForm, name: e.target.value})}
|
||||
placeholder="הזן שם לבעיה"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-6">
|
||||
<div>
|
||||
<Label className="text-base font-semibold mb-2 block">מבוא</Label>
|
||||
<p className="text-xs text-slate-500 mb-2">הצג את ההקשר והרקע לבעיה</p>
|
||||
<Textarea
|
||||
value={problemForm.intro}
|
||||
onChange={(e) => setProblemForm({...problemForm, intro: e.target.value})}
|
||||
placeholder="הזן מבוא"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base font-semibold mb-2 block">מטרת המוצר</Label>
|
||||
<p className="text-xs text-slate-500 mb-2">מה המוצר אמור להשיג ולמי הוא מיועד</p>
|
||||
<Textarea
|
||||
value={problemForm.objective}
|
||||
onChange={(e) => setProblemForm({...problemForm, objective: e.target.value})}
|
||||
placeholder="הזן מטרה"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base font-semibold mb-2 block">דרישות מרכזיות</Label>
|
||||
<p className="text-xs text-slate-500 mb-2">פרט את הדרישות והפיצ׳רים העיקריים</p>
|
||||
<Textarea
|
||||
value={problemForm.requirements}
|
||||
onChange={(e) => setProblemForm({...problemForm, requirements: e.target.value})}
|
||||
placeholder="הזן דרישות"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 sticky bottom-0 bg-white pt-4 border-t">
|
||||
<Button onClick={handleSaveProblem} className="flex-1">שמור</Button>
|
||||
<Button variant="outline" onClick={() => setShowModal(false)} className="flex-1">ביטול</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
296
Migdalor-main/src/pages/HackalonOverview.jsx
Normal file
296
Migdalor-main/src/pages/HackalonOverview.jsx
Normal file
@ -0,0 +1,296 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Users, Building2, Loader2, FileText, Presentation, Download, X, Shield } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function HackalonOverview() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||
const [showTeamModal, setShowTeamModal] = useState(false);
|
||||
const [userPermissions, setUserPermissions] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const u = await base44.auth.me();
|
||||
setUser(u);
|
||||
|
||||
// Load user permissions
|
||||
if (u.positions && u.positions.length > 0) {
|
||||
const allPermissions = await base44.entities.PositionPermission.list();
|
||||
const userPositionPerms = allPermissions.filter(p => u.positions.includes(p.position_name));
|
||||
const mergedPerms = {
|
||||
pages_access: [...new Set(userPositionPerms.flatMap(p => p.pages_access || []))]
|
||||
};
|
||||
setUserPermissions(mergedPerms);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
loadUserData();
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const allowedPositions = ['מפק״ץ', 'מנהל האקתון'];
|
||||
const hasAllowedPosition = user?.positions?.some(pos => allowedPositions.includes(pos));
|
||||
const hasAccess = isAdmin || hasAllowedPosition;
|
||||
|
||||
const { data: departments = [], isLoading: depsLoading } = useQuery({
|
||||
queryKey: ['hackalon-departments'],
|
||||
queryFn: () => base44.entities.HackalonDepartment.list('order'),
|
||||
enabled: hasAccess
|
||||
});
|
||||
|
||||
const { data: teams = [], isLoading: teamsLoading } = useQuery({
|
||||
queryKey: ['hackalon-teams'],
|
||||
queryFn: () => base44.entities.HackalonTeam.list('order'),
|
||||
enabled: hasAccess
|
||||
});
|
||||
|
||||
const { data: submissions = [] } = useQuery({
|
||||
queryKey: ['hackalon-submissions'],
|
||||
queryFn: () => base44.entities.HackalonSubmission.list(),
|
||||
enabled: hasAccess
|
||||
});
|
||||
|
||||
if (!user || depsLoading || teamsLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">אין לך הרשאה לצפות בעמוד זה</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getSubmission = (teamName, type) => {
|
||||
return submissions.find(s => s.team_name === teamName && s.submission_type === type);
|
||||
};
|
||||
|
||||
const getTeamProgress = (teamName) => {
|
||||
const hasSpec = !!getSubmission(teamName, 'specification');
|
||||
const hasFinal = !!getSubmission(teamName, 'final_product');
|
||||
return [hasSpec, hasFinal].filter(Boolean).length;
|
||||
};
|
||||
|
||||
const handleTeamClick = (team) => {
|
||||
setSelectedTeam(team);
|
||||
setShowTeamModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
💡 HackAlon - סקירה כללית
|
||||
</h1>
|
||||
<p className="text-slate-500">מדורים, צוותים ומיקומים</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<p className="text-sm text-blue-600">סה״כ מדורים</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{departments.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-purple-50 border-purple-200">
|
||||
<p className="text-sm text-purple-600">סה״כ צוותים</p>
|
||||
<p className="text-2xl font-bold text-purple-700">{teams.length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Departments and Teams */}
|
||||
<div className="space-y-6">
|
||||
{departments.map((dept, index) => {
|
||||
const deptTeams = teams.filter(t => t.department_name === dept.name);
|
||||
return (
|
||||
<motion.div
|
||||
key={dept.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Building2 className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-800">{dept.name}</h3>
|
||||
<p className="text-sm text-slate-500">{deptTeams.length} צוותים • כיתה {dept.classroom_number || 'לא הוגדר'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{deptTeams.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{deptTeams.map(team => {
|
||||
const progress = getTeamProgress(team.name);
|
||||
return (
|
||||
<Card
|
||||
key={team.id}
|
||||
className="p-4 bg-slate-50 border-slate-200 hover:shadow-md transition-all cursor-pointer"
|
||||
onClick={() => handleTeamClick(team)}
|
||||
>
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<Users className="w-5 h-5 text-purple-600 mt-1" />
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-slate-800">{team.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="flex gap-1 mt-2">
|
||||
{[1, 2].map((step) => (
|
||||
<div
|
||||
key={step}
|
||||
className={`h-2 flex-1 rounded-full ${
|
||||
step <= progress ? 'bg-green-500' : 'bg-slate-200'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1 text-center">{progress}/2 הועלו</p>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-400 text-center py-4">אין צוותים במדור זה</p>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{departments.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<Building2 className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-600 mb-2">אין מדורים עדיין</h3>
|
||||
<p className="text-slate-400">צור מדורים וצוותים בדף השיבוץ</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Team Details Modal */}
|
||||
<Dialog open={showTeamModal} onOpenChange={setShowTeamModal}>
|
||||
<DialogContent dir="rtl" className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<span>{selectedTeam?.name}</span>
|
||||
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedTeam && (
|
||||
<div className="space-y-6">
|
||||
{/* Problem Definition */}
|
||||
{selectedTeam.problem_name && (
|
||||
<div className="border-b pb-4">
|
||||
<h3 className="text-lg font-bold text-slate-800 mb-3">הבעיה</h3>
|
||||
<div className="space-y-3 bg-slate-50 p-4 rounded-lg">
|
||||
<div>
|
||||
<h4 className="font-semibold text-purple-600 mb-1">{selectedTeam.problem_name}</h4>
|
||||
</div>
|
||||
{selectedTeam.problem_intro && (
|
||||
<div>
|
||||
<h5 className="text-sm font-semibold text-slate-600 mb-1">מבוא</h5>
|
||||
<p className="text-sm text-slate-600 whitespace-pre-wrap">{selectedTeam.problem_intro}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedTeam.problem_objective && (
|
||||
<div>
|
||||
<h5 className="text-sm font-semibold text-slate-600 mb-1">מטרת המוצר</h5>
|
||||
<p className="text-sm text-slate-600 whitespace-pre-wrap">{selectedTeam.problem_objective}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedTeam.problem_requirements && (
|
||||
<div>
|
||||
<h5 className="text-sm font-semibold text-slate-600 mb-1">דרישות מרכזיות</h5>
|
||||
<p className="text-sm text-slate-600 whitespace-pre-wrap">{selectedTeam.problem_requirements}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submissions */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-800 mb-3">מסמכים שהועלו</h3>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{/* Specification */}
|
||||
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg border">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-6 h-6 text-green-600" />
|
||||
<div>
|
||||
<p className="font-medium">מסמך איפיון</p>
|
||||
{getSubmission(selectedTeam.name, 'specification') && (
|
||||
<p className="text-xs text-slate-500">
|
||||
הועלה על ידי {getSubmission(selectedTeam.name, 'specification').uploaded_by}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{getSubmission(selectedTeam.name, 'specification') ? (
|
||||
<a href={getSubmission(selectedTeam.name, 'specification').file_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm">
|
||||
<Download className="w-4 h-4 ml-2" />
|
||||
הורד
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-slate-400">לא הועלה</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Final Product */}
|
||||
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg border">
|
||||
<div className="flex items-center gap-3">
|
||||
<Presentation className="w-6 h-6 text-purple-600" />
|
||||
<div>
|
||||
<p className="font-medium">תוצר סופי</p>
|
||||
{getSubmission(selectedTeam.name, 'final_product') && (
|
||||
<p className="text-xs text-slate-500">
|
||||
הועלה על ידי {getSubmission(selectedTeam.name, 'final_product').uploaded_by}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{getSubmission(selectedTeam.name, 'final_product') ? (
|
||||
<a href={getSubmission(selectedTeam.name, 'final_product').file_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm">
|
||||
<Download className="w-4 h-4 ml-2" />
|
||||
הורד
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-slate-400">לא הועלה</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
558
Migdalor-main/src/pages/HackalonSchedule.jsx
Normal file
558
Migdalor-main/src/pages/HackalonSchedule.jsx
Normal file
@ -0,0 +1,558 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Plus, Loader2, Edit2, Trash2, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
import { format, addDays, subDays, parseISO } from 'date-fns';
|
||||
|
||||
export default function HackalonSchedule() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [viewingItem, setViewingItem] = useState(null);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
date: selectedDate,
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
event_type: 'פורום מדורי'
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const { data: scheduleItems = [], isLoading } = useQuery({
|
||||
queryKey: ['hackalon-schedule', selectedDate],
|
||||
queryFn: () => base44.entities.HackalonScheduleItem.filter({ date: selectedDate }, 'start_time')
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.HackalonScheduleItem.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-schedule'] });
|
||||
setShowModal(false);
|
||||
resetForm();
|
||||
toast.success('אירוע נוסף בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.HackalonScheduleItem.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-schedule'] });
|
||||
setShowModal(false);
|
||||
resetForm();
|
||||
toast.success('אירוע עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.HackalonScheduleItem.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-schedule'] });
|
||||
toast.success('אירוע נמחק בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.title || !formData.start_time || !formData.end_time) {
|
||||
toast.error('אנא מלא את כל השדות הנדרשים');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.start_time >= formData.end_time) {
|
||||
toast.error('שעת הסיום חייבת להיות מאוחרת יותר משעת ההתחלה');
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingItem) {
|
||||
updateMutation.mutate({ id: editingItem.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
setEditingItem(item);
|
||||
setFormData({
|
||||
title: item.title,
|
||||
description: item.description || '',
|
||||
date: item.date,
|
||||
start_time: item.start_time,
|
||||
end_time: item.end_time,
|
||||
event_type: item.event_type
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
if (window.confirm('האם אתה בטוח שברצונך למחוק את האירוע?')) {
|
||||
deleteMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingItem(null);
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
date: selectedDate,
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
event_type: 'פורום מדורי'
|
||||
});
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
resetForm();
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const changeDate = (days) => {
|
||||
const newDate = days > 0 ?
|
||||
addDays(parseISO(selectedDate), days) :
|
||||
subDays(parseISO(selectedDate), Math.abs(days));
|
||||
setSelectedDate(format(newDate, 'yyyy-MM-dd'));
|
||||
};
|
||||
|
||||
// Generate time slots for timeline (6:00 to 23:00)
|
||||
const generateTimeSlots = () => {
|
||||
const slots = [];
|
||||
for (let hour = 6; hour <= 23; hour++) {
|
||||
slots.push(`${String(hour).padStart(2, '0')}:00`);
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
const timeSlots = generateTimeSlots();
|
||||
|
||||
// Calculate event position and height
|
||||
const getEventStyle = (startTime, endTime) => {
|
||||
const startHour = parseInt(startTime.split(':')[0]);
|
||||
const startMinute = parseInt(startTime.split(':')[1]);
|
||||
const endHour = parseInt(endTime.split(':')[0]);
|
||||
const endMinute = parseInt(endTime.split(':')[1]);
|
||||
|
||||
const startOffset = (startHour - 6) * 60 + startMinute;
|
||||
const endOffset = (endHour - 6) * 60 + endMinute;
|
||||
const duration = endOffset - startOffset;
|
||||
|
||||
const pixelsPerMinute = 120 / 60; // 120px per hour (increased from 80px)
|
||||
const top = startOffset * pixelsPerMinute;
|
||||
const height = Math.max(duration * pixelsPerMinute - 4, 50); // Subtract 4px for gap, minimum 50px height
|
||||
|
||||
return { top: `${top}px`, height: `${height}px`, duration };
|
||||
};
|
||||
|
||||
// Event type colors
|
||||
const eventTypeColors = {
|
||||
'פורום גדודי': 'bg-purple-100 border-purple-300 text-purple-900',
|
||||
'פורום מדורי': 'bg-blue-100 border-blue-300 text-blue-900',
|
||||
'הרצאת אורח': 'bg-green-100 border-green-300 text-green-900',
|
||||
'מתפללים': 'bg-amber-100 border-amber-300 text-amber-900',
|
||||
'ארוחה': 'bg-orange-100 border-orange-300 text-orange-900'
|
||||
};
|
||||
|
||||
const eventTypeIcons = {
|
||||
'פורום גדודי': '👥',
|
||||
'פורום מדורי': '🏢',
|
||||
'הרצאת אורח': '🎤',
|
||||
'מתפללים': '🙏',
|
||||
'ארוחה': '🍽️'
|
||||
};
|
||||
|
||||
// Detect overlapping events - improved algorithm
|
||||
const getOverlappingEvents = () => {
|
||||
const sorted = [...scheduleItems].sort((a, b) => {
|
||||
const timeCompare = a.start_time.localeCompare(b.start_time);
|
||||
if (timeCompare !== 0) return timeCompare;
|
||||
return a.end_time.localeCompare(b.end_time);
|
||||
});
|
||||
|
||||
const columns = [];
|
||||
|
||||
sorted.forEach((event) => {
|
||||
let placed = false;
|
||||
|
||||
// Try to find a column where this event doesn't overlap
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const col = columns[i];
|
||||
const hasOverlap = col.some(existingEvent => {
|
||||
// Events overlap if one starts before the other ends
|
||||
return !(event.start_time >= existingEvent.end_time ||
|
||||
event.end_time <= existingEvent.start_time);
|
||||
});
|
||||
|
||||
if (!hasOverlap) {
|
||||
col.push(event);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no suitable column found, create a new one
|
||||
if (!placed) {
|
||||
columns.push([event]);
|
||||
}
|
||||
});
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
const eventColumns = getOverlappingEvents();
|
||||
|
||||
if (!user || isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
📅 לוח זמנים HackAlon
|
||||
</h1>
|
||||
<p className="text-slate-500">תכנון ומעקב אחר אירועים</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Date Navigation */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="icon" onClick={() => changeDate(-1)}>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="w-auto" />
|
||||
|
||||
<Button variant="outline" size="icon" onClick={() => changeDate(1)}>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedDate(format(new Date(), 'yyyy-MM-dd'))}>
|
||||
|
||||
היום
|
||||
</Button>
|
||||
</div>
|
||||
{isAdmin &&
|
||||
<Button onClick={openAddModal} className="bg-indigo-600 hover:bg-indigo-700">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף אירוע
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<Card className="p-4 mb-6">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{Object.entries(eventTypeColors).map(([type, color]) =>
|
||||
<div key={type} className="flex items-center gap-2">
|
||||
<div className={`w-4 h-4 rounded ${color}`}></div>
|
||||
<span className="text-sm">{type} {eventTypeIcons[type]}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Timeline View */}
|
||||
<Card className="overflow-auto max-h-[800px]">
|
||||
<div className="relative" style={{ minHeight: '2160px', minWidth: '600px' }}>
|
||||
{/* Time slots */}
|
||||
{timeSlots.map((time, idx) =>
|
||||
<div
|
||||
key={time}
|
||||
className="absolute left-0 right-0 border-t border-slate-200"
|
||||
style={{ top: `${idx * 120}px` }}>
|
||||
|
||||
<div className="absolute -top-3 right-4 bg-white px-2 text-sm font-medium text-slate-600">
|
||||
{time}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Events */}
|
||||
<div className="relative pr-20" style={{ minHeight: '2160px' }}>
|
||||
{eventColumns.map((column, colIdx) =>
|
||||
<div
|
||||
key={colIdx}
|
||||
className="absolute"
|
||||
style={{
|
||||
right: `${100 + colIdx * 290}px`,
|
||||
width: '270px',
|
||||
top: 0,
|
||||
bottom: 0
|
||||
}}>
|
||||
|
||||
{column.map((item) => {
|
||||
const style = getEventStyle(item.start_time, item.end_time);
|
||||
const color = eventTypeColors[item.event_type] || 'bg-slate-100 border-slate-300 text-slate-900';
|
||||
const isShortEvent = style.duration < 30; // Less than 30 minutes
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className={`absolute right-0 left-0 ${color} rounded-lg border-2 p-2 cursor-pointer hover:shadow-lg transition-all overflow-hidden group`}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
if (isAdmin) {
|
||||
handleEdit(item);
|
||||
} else {
|
||||
setViewingItem(item);
|
||||
}
|
||||
}}>
|
||||
|
||||
<div className="flex items-start justify-between gap-2 h-full">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
<span className="text-base flex-shrink-0">{eventTypeIcons[item.event_type]}</span>
|
||||
<p className="font-bold text-sm truncate">{item.title}</p>
|
||||
</div>
|
||||
<p className="text-xs opacity-75 font-medium">
|
||||
{item.start_time} - {item.end_time}
|
||||
</p>
|
||||
{!isShortEvent && item.description &&
|
||||
<p className="text-xs opacity-65 mt-1 line-clamp-2">{item.description}</p>
|
||||
}
|
||||
</div>
|
||||
{isAdmin &&
|
||||
<div className="flex gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-black/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(item);
|
||||
}}>
|
||||
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-red-500/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(item.id);
|
||||
}}>
|
||||
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</motion.div>);
|
||||
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scheduleItems.length === 0 &&
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Calendar className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-400">אין אירועים מתוכננים לתאריך זה</p>
|
||||
{isAdmin &&
|
||||
<Button onClick={openAddModal} variant="outline" className="mt-4">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף אירוע ראשון
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent dir="rtl" className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold leading-none tracking-tight text-right">{editingItem ? 'ערוך אירוע' : 'הוסף אירוע חדש'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>כותרת *</Label>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="שם האירוע" />
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>תיאור</Label>
|
||||
<Textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="פרטים נוספים"
|
||||
rows={3} />
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>תאריך *</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.date}
|
||||
onChange={(e) => setFormData({ ...formData, date: e.target.value })} className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors
|
||||
text-right
|
||||
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-1 focus-visible:ring-ring
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
md:text-sm
|
||||
flex-row-reverse text-right
|
||||
" />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>שעת התחלה *</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.start_time}
|
||||
onChange={(e) => setFormData({ ...formData, start_time: e.target.value })} className="flex h-9 w-full flex-row-reverse rounded-md border border-input bg-transparent
|
||||
px-3 py-1 text-base shadow-sm transition-colors
|
||||
text-right
|
||||
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-1 focus-visible:ring-ring
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
md:text-sm
|
||||
" />
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<Label>שעת סיום *</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.end_time}
|
||||
onChange={(e) => setFormData({ ...formData, end_time: e.target.value })} className="flex h-9 w-full flex-row-reverse rounded-md border border-input bg-transparent
|
||||
px-3 py-1 text-base shadow-sm transition-colors
|
||||
text-right
|
||||
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-1 focus-visible:ring-ring
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
md:text-sm
|
||||
" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>סוג אירוע *</Label>
|
||||
<Select
|
||||
value={formData.event_type}
|
||||
onValueChange={(value) => setFormData({ ...formData, event_type: value })}>
|
||||
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
<SelectItem value="פורום גדודי">פורום גדודי 👥</SelectItem>
|
||||
<SelectItem value="פורום מדורי">פורום מדורי 🏢</SelectItem>
|
||||
<SelectItem value="הרצאת אורח">הרצאת אורח 🎤</SelectItem>
|
||||
<SelectItem value="מתפללים">מתפללים 🙏</SelectItem>
|
||||
<SelectItem value="ארוחה">ארוחה 🍽️</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button onClick={handleSubmit} className="flex-1 bg-indigo-600 hover:bg-indigo-700">
|
||||
{editingItem ? 'עדכן' : 'הוסף'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShowModal(false)} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* View Modal for Non-Admin Users */}
|
||||
<Dialog open={!!viewingItem} onOpenChange={(open) => !open && setViewingItem(null)}>
|
||||
<DialogContent dir="rtl" className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold leading-none tracking-tight text-right flex items-center gap-2">
|
||||
<span className="text-2xl">{viewingItem && eventTypeIcons[viewingItem.event_type]}</span>
|
||||
{viewingItem?.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{viewingItem && (
|
||||
<div className="space-y-4">
|
||||
<div className={`p-4 rounded-lg ${eventTypeColors[viewingItem.event_type]}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span className="font-medium">תאריך:</span>
|
||||
<span>{new Date(viewingItem.date).toLocaleDateString('he-IL')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">🕐</span>
|
||||
<span className="font-medium">שעות:</span>
|
||||
<span className="text-lg font-bold">{viewingItem.start_time} - {viewingItem.end_time}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewingItem.description && (
|
||||
<div>
|
||||
<p className="font-medium mb-2">תיאור:</p>
|
||||
<p className="text-slate-600 bg-slate-50 p-3 rounded-lg">{viewingItem.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="font-medium mb-1">סוג אירוע:</p>
|
||||
<div className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full ${eventTypeColors[viewingItem.event_type]}`}>
|
||||
<span>{eventTypeIcons[viewingItem.event_type]}</span>
|
||||
<span className="font-medium">{viewingItem.event_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => setViewingItem(null)}
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
סגור
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
299
Migdalor-main/src/pages/HackalonStatus.jsx
Normal file
299
Migdalor-main/src/pages/HackalonStatus.jsx
Normal file
@ -0,0 +1,299 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileText, Presentation, Download, CheckCircle2, XCircle, Loader2, Shield } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function HackalonStatus() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [userPermissions, setUserPermissions] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const u = await base44.auth.me();
|
||||
setUser(u);
|
||||
|
||||
// Load user permissions
|
||||
if (u.positions && u.positions.length > 0) {
|
||||
const allPermissions = await base44.entities.PositionPermission.list();
|
||||
const userPositionPerms = allPermissions.filter(p => u.positions.includes(p.position_name));
|
||||
const mergedPerms = {
|
||||
pages_access: [...new Set(userPositionPerms.flatMap(p => p.pages_access || []))]
|
||||
};
|
||||
setUserPermissions(mergedPerms);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
loadUserData();
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const allowedPositions = ['מפק״ץ', 'מנהל האקתון'];
|
||||
const hasAllowedPosition = user?.positions?.some(pos => allowedPositions.includes(pos));
|
||||
const hasAccess = isAdmin || hasAllowedPosition;
|
||||
|
||||
const { data: departments = [] } = useQuery({
|
||||
queryKey: ['hackalon-departments'],
|
||||
queryFn: () => base44.entities.HackalonDepartment.list('order'),
|
||||
enabled: hasAccess
|
||||
});
|
||||
|
||||
const { data: teams = [] } = useQuery({
|
||||
queryKey: ['hackalon-teams'],
|
||||
queryFn: () => base44.entities.HackalonTeam.list('order'),
|
||||
enabled: hasAccess
|
||||
});
|
||||
|
||||
// const { data: submissions = [] } = useQuery({
|
||||
// queryKey: ['hackalon-submissions'],
|
||||
// queryFn: () => base44.entities.HackalonSubmission.list(),
|
||||
// enabled: hasAccess
|
||||
// });
|
||||
// ⭐ השינוי העיקרי - פילטור submissions רק לצוותים קיימים
|
||||
const { data: submissions = [] } = useQuery({
|
||||
queryKey: ['hackalon-submissions', teams.length],
|
||||
queryFn: async () => {
|
||||
const allSubmissions = await base44.entities.HackalonSubmission.list();
|
||||
// Filter only submissions that belong to existing teams
|
||||
const teamNames = teams.map(t => t.name);
|
||||
return allSubmissions.filter(s => teamNames.includes(s.team_name));
|
||||
},
|
||||
enabled: hasAccess && teams.length > 0
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">אין לך הרשאה לצפות בעמוד זה</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getSubmission = (teamName, type) => {
|
||||
return submissions.find(s => s.team_name === teamName && s.submission_type === type);
|
||||
};
|
||||
|
||||
const getStats = () => {
|
||||
const totalTeams = teams.length;
|
||||
const teamsWithSpec = new Set(submissions.filter(s => s.submission_type === 'specification').map(s => s.team_name)).size;
|
||||
const teamsWithFinal = new Set(submissions.filter(s => s.submission_type === 'final_product').map(s => s.team_name)).size;
|
||||
|
||||
const teamsWithoutSpec = totalTeams - teamsWithSpec;
|
||||
const teamsWithoutFinal = totalTeams - teamsWithFinal;
|
||||
|
||||
return { totalTeams, teamsWithSpec, teamsWithFinal, teamsWithoutSpec, teamsWithoutFinal };
|
||||
};
|
||||
|
||||
const stats = getStats();
|
||||
|
||||
const getDeptStats = (deptName) => {
|
||||
const deptTeams = teams.filter(t => t.department_name === deptName);
|
||||
const deptTotal = deptTeams.length;
|
||||
const deptSpec = deptTeams.filter(t => getSubmission(t.name, 'specification')).length;
|
||||
const deptFinal = deptTeams.filter(t => getSubmission(t.name, 'final_product')).length;
|
||||
return { deptTotal, deptSpec, deptFinal };
|
||||
};
|
||||
|
||||
const shouldShowTeam = (team) => {
|
||||
if (filterType === 'all') return true;
|
||||
if (filterType === 'with-spec') return !!getSubmission(team.name, 'specification');
|
||||
if (filterType === 'no-spec') return !getSubmission(team.name, 'specification');
|
||||
if (filterType === 'with-final') return !!getSubmission(team.name, 'final_product');
|
||||
if (filterType === 'no-final') return !getSubmission(team.name, 'final_product');
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
📊 תמונת מצב - HackAlon
|
||||
</h1>
|
||||
<p className="text-slate-500">מעקב אחר העלאות והגשות של הצוותים</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats with filters */}
|
||||
<Card className="p-6 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Specification */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-700 mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
מסמך איפיון
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Card
|
||||
className={`p-3 cursor-pointer transition-all ${filterType === 'with-spec' ? 'ring-2 ring-green-500 bg-green-50' : 'bg-green-50 border-green-200 hover:shadow-md'}`}
|
||||
onClick={() => setFilterType(filterType === 'with-spec' ? 'all' : 'with-spec')}
|
||||
>
|
||||
<p className="text-xs text-green-600">עם מסמך</p>
|
||||
<p className="text-xl font-bold text-green-700">{stats.teamsWithSpec}</p>
|
||||
</Card>
|
||||
<Card
|
||||
className={`p-3 cursor-pointer transition-all ${filterType === 'no-spec' ? 'ring-2 ring-red-500 bg-red-50' : 'bg-red-50 border-red-200 hover:shadow-md'}`}
|
||||
onClick={() => setFilterType(filterType === 'no-spec' ? 'all' : 'no-spec')}
|
||||
>
|
||||
<p className="text-xs text-red-600">ללא מסמך</p>
|
||||
<p className="text-xl font-bold text-red-700">{stats.teamsWithoutSpec}</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Final Product */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-700 mb-3 flex items-center gap-2">
|
||||
<Presentation className="w-4 h-4" />
|
||||
תוצר סופי
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Card
|
||||
className={`p-3 cursor-pointer transition-all ${filterType === 'with-final' ? 'ring-2 ring-purple-500 bg-purple-50' : 'bg-purple-50 border-purple-200 hover:shadow-md'}`}
|
||||
onClick={() => setFilterType(filterType === 'with-final' ? 'all' : 'with-final')}
|
||||
>
|
||||
<p className="text-xs text-purple-600">עם תוצר</p>
|
||||
<p className="text-xl font-bold text-purple-700">{stats.teamsWithFinal}</p>
|
||||
</Card>
|
||||
<Card
|
||||
className={`p-3 cursor-pointer transition-all ${filterType === 'no-final' ? 'ring-2 ring-amber-500 bg-amber-50' : 'bg-amber-50 border-amber-200 hover:shadow-md'}`}
|
||||
onClick={() => setFilterType(filterType === 'no-final' ? 'all' : 'no-final')}
|
||||
>
|
||||
<p className="text-xs text-amber-600">ללא תוצר</p>
|
||||
<p className="text-xl font-bold text-amber-700">{stats.teamsWithoutFinal}</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Departments and Teams Status */}
|
||||
<div className="space-y-6">
|
||||
{departments.map((dept, index) => {
|
||||
const deptTeams = teams.filter(t => t.department_name === dept.name);
|
||||
const visibleTeams = deptTeams.filter(shouldShowTeam);
|
||||
const deptStats = getDeptStats(dept.name);
|
||||
|
||||
if (visibleTeams.length === 0 && filterType !== 'all') return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={dept.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-800">{dept.name}</h3>
|
||||
<p className="text-sm text-slate-500">כיתה {dept.classroom_number || 'לא הוגדר'}</p>
|
||||
</div>
|
||||
|
||||
{/* Department Stats */}
|
||||
<div className="flex gap-3 text-sm">
|
||||
<div className="text-center px-3 py-1 bg-green-50 rounded-lg">
|
||||
<p className="text-xs text-green-600">מסמכי איפיון</p>
|
||||
<p className="font-bold text-green-700">{deptStats.deptSpec}/{deptStats.deptTotal}</p>
|
||||
</div>
|
||||
<div className="text-center px-3 py-1 bg-purple-50 rounded-lg">
|
||||
<p className="text-xs text-purple-600">תוצרים סופיים</p>
|
||||
<p className="font-bold text-purple-700">{deptStats.deptFinal}/{deptStats.deptTotal}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visibleTeams.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{visibleTeams.map(team => {
|
||||
const specSubmission = getSubmission(team.name, 'specification');
|
||||
const finalSubmission = getSubmission(team.name, 'final_product');
|
||||
|
||||
return (
|
||||
<Card key={team.id} className="p-4 bg-slate-50">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-800">{team.name}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{/* Specification */}
|
||||
<div className="flex items-center justify-between p-3 bg-white rounded-lg border">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-green-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">מסמך איפיון</p>
|
||||
{specSubmission && (
|
||||
<p className="text-xs text-slate-500">{specSubmission.uploaded_by}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{specSubmission ? (
|
||||
<a href={specSubmission.file_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm" variant="ghost">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Final Product */}
|
||||
<div className="flex items-center justify-between p-3 bg-white rounded-lg border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Presentation className="w-5 h-5 text-purple-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">תוצר סופי</p>
|
||||
{finalSubmission && (
|
||||
<p className="text-xs text-slate-500">{finalSubmission.uploaded_by}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{finalSubmission ? (
|
||||
<a href={finalSubmission.file_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm" variant="ghost">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-400 text-center py-4">אין צוותים במדור זה</p>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
591
Migdalor-main/src/pages/HackalonTeamArea.jsx
Normal file
591
Migdalor-main/src/pages/HackalonTeamArea.jsx
Normal file
@ -0,0 +1,591 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Upload, FileText, Presentation, Users, MapPin, Lightbulb, Loader2, Link as LinkIcon, Trash2,AppWindow } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function HackalonTeamArea() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [uploading, setUploading] = useState(null);
|
||||
const [showLinkModal, setShowLinkModal] = useState(null);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: teamInfo, isLoading: teamLoading } = useQuery({
|
||||
queryKey: ['hackalon-team-info', user?.hackalon_team],
|
||||
queryFn: async () => {
|
||||
const teams = await base44.entities.HackalonTeam.list();
|
||||
return teams.find((t) => t.name === user.hackalon_team);
|
||||
},
|
||||
enabled: !!user?.hackalon_team
|
||||
});
|
||||
|
||||
// Auto-assign user if name matches team member list
|
||||
useEffect(() => {
|
||||
const autoAssign = async () => {
|
||||
if (!user) return;
|
||||
|
||||
const userName = (user.onboarding_full_name || user.full_name || '').trim().toLowerCase();
|
||||
if (!userName) return;
|
||||
|
||||
try {
|
||||
const allTeams = await base44.entities.HackalonTeam.list();
|
||||
const matchingTeam = allTeams.find(team =>
|
||||
team.member_names?.some(name => name.trim().toLowerCase() === userName)
|
||||
);
|
||||
|
||||
// Check if current team/department still exists
|
||||
const currentTeamExists = user.hackalon_team ?
|
||||
allTeams.some(t => t.name === user.hackalon_team) : false;
|
||||
|
||||
// If assigned team was deleted - remove assignment
|
||||
if (user.hackalon_team && !currentTeamExists) {
|
||||
await base44.entities.User.update(user.id, {
|
||||
hackalon_team: null,
|
||||
hackalon_department: null
|
||||
});
|
||||
|
||||
const updatedUser = await base44.auth.me();
|
||||
setUser(updatedUser);
|
||||
toast.info('הצוות שלך נמחק - הוסרת מהשיבוץ');
|
||||
return;
|
||||
}
|
||||
|
||||
// If name matches a team but user isn't assigned - assign them
|
||||
if (matchingTeam && user.hackalon_team !== matchingTeam.name) {
|
||||
await base44.entities.User.update(user.id, {
|
||||
hackalon_team: matchingTeam.name,
|
||||
hackalon_department: matchingTeam.department_name
|
||||
});
|
||||
|
||||
const updatedUser = await base44.auth.me();
|
||||
setUser(updatedUser);
|
||||
toast.success(`שובצת אוטומטית לצוות ${matchingTeam.name}`);
|
||||
}
|
||||
|
||||
// If name doesn't match current team - remove assignment
|
||||
if (!matchingTeam && user.hackalon_team && currentTeamExists) {
|
||||
await base44.entities.User.update(user.id, {
|
||||
hackalon_team: null,
|
||||
hackalon_department: null
|
||||
});
|
||||
|
||||
const updatedUser = await base44.auth.me();
|
||||
setUser(updatedUser);
|
||||
toast.info('הוסרת מהצוות כי השם שלך השתנה');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auto-assign failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
autoAssign();
|
||||
}, [user?.onboarding_full_name, user?.full_name]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
try {
|
||||
const userData = await base44.auth.me();
|
||||
|
||||
// Check if assigned team still exists
|
||||
if (userData.hackalon_team) {
|
||||
const allTeams = await base44.entities.HackalonTeam.list();
|
||||
const teamExists = allTeams.some(t => t.name === userData.hackalon_team);
|
||||
|
||||
if (!teamExists) {
|
||||
console.log('Team deleted, removing assignment...');
|
||||
// Team was deleted - remove assignment
|
||||
await base44.entities.User.update(userData.id, {
|
||||
hackalon_team: null,
|
||||
hackalon_department: null
|
||||
});
|
||||
|
||||
// Force reload from server
|
||||
const freshUser = await base44.auth.me();
|
||||
setUser(freshUser);
|
||||
toast.info('הצוות שלך נמחק - הוסרת מהשיבוץ');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setUser(userData);
|
||||
} catch (error) {
|
||||
console.error('Load user error:', error);
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
|
||||
// Reload user data every 2 seconds to catch updates from other pages
|
||||
const interval = setInterval(loadUser, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const { data: teamMembers = [], isLoading: membersLoading } = useQuery({
|
||||
queryKey: ['hackalon-team-members', user?.hackalon_team, teamInfo?.member_names],
|
||||
queryFn: async () => {
|
||||
const allUsers = await base44.entities.User.list();
|
||||
// Find by hackalon_team OR by name match in member_names
|
||||
return allUsers.filter((u) => {
|
||||
// Direct team assignment
|
||||
if (u.hackalon_team === user.hackalon_team) return true;
|
||||
|
||||
// Check if user's name is in the team's member_names list
|
||||
if (teamInfo?.member_names) {
|
||||
const userName = (u.onboarding_full_name || u.full_name || '').trim().toLowerCase();
|
||||
return teamInfo.member_names.some((name) =>
|
||||
name.trim().toLowerCase() === userName
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
enabled: !!user?.hackalon_team && !!teamInfo
|
||||
});
|
||||
|
||||
const { data: submissions = [] } = useQuery({
|
||||
queryKey: ['hackalon-submissions', user?.hackalon_team],
|
||||
queryFn: async () => {
|
||||
const allSubmissions = await base44.entities.HackalonSubmission.list();
|
||||
return allSubmissions.filter((s) => s.team_name === user.hackalon_team);
|
||||
},
|
||||
enabled: !!user?.hackalon_team
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: async ({ file, type, existingSubmission }) => {
|
||||
const { file_url } = await base44.integrations.Core.UploadFile({ file });
|
||||
|
||||
// Check if late submission for specification
|
||||
let isLate = false;
|
||||
if (type === 'specification' && teamInfo?.specification_deadline) {
|
||||
const deadline = new Date(teamInfo.specification_deadline);
|
||||
const now = new Date();
|
||||
isLate = now > deadline;
|
||||
}
|
||||
|
||||
if (existingSubmission) {
|
||||
// Update existing
|
||||
return base44.entities.HackalonSubmission.update(existingSubmission.id, {
|
||||
submission_method: 'file',
|
||||
file_url: file_url,
|
||||
file_name: file.name,
|
||||
uploaded_by: user.email,
|
||||
upload_date: new Date().toISOString(),
|
||||
is_late: isLate
|
||||
});
|
||||
} else {
|
||||
// Create new
|
||||
return base44.entities.HackalonSubmission.create({
|
||||
team_name: user.hackalon_team,
|
||||
submission_type: type,
|
||||
submission_method: 'file',
|
||||
file_url: file_url,
|
||||
file_name: file.name,
|
||||
uploaded_by: user.email,
|
||||
upload_date: new Date().toISOString(),
|
||||
is_late: isLate
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-submissions'] });
|
||||
toast.success('הקובץ הועלה בהצלחה');
|
||||
setUploading(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('שגיאה בהעלאת הקובץ');
|
||||
setUploading(null);
|
||||
|
||||
},
|
||||
catch (error) {
|
||||
console.error('Upload error details:', error);
|
||||
throw error; // זה חשוב כדי שה-onError יתפוס את השגיאה
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
console.error('Mutation error:', error);
|
||||
toast.error(`שגיאה בהעלאת הקובץ: ${error.message || 'שגיאה לא ידועה'}`);
|
||||
setUploading(null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const addLinkMutation = useMutation({
|
||||
mutationFn: async ({ url, type, existingSubmission }) => {
|
||||
// Check if late submission for specification
|
||||
let isLate = false;
|
||||
if (type === 'specification' && teamInfo?.specification_deadline) {
|
||||
const deadline = new Date(teamInfo.specification_deadline);
|
||||
const now = new Date();
|
||||
isLate = now > deadline;
|
||||
}
|
||||
|
||||
if (existingSubmission) {
|
||||
return base44.entities.HackalonSubmission.update(existingSubmission.id, {
|
||||
submission_method: 'link',
|
||||
file_url: url,
|
||||
file_name: 'קישור חיצוני',
|
||||
uploaded_by: user.email,
|
||||
upload_date: new Date().toISOString(),
|
||||
is_late: isLate
|
||||
});
|
||||
} else {
|
||||
return base44.entities.HackalonSubmission.create({
|
||||
team_name: user.hackalon_team,
|
||||
submission_type: type,
|
||||
submission_method: 'link',
|
||||
file_url: url,
|
||||
file_name: 'קישור חיצוני',
|
||||
uploaded_by: user.email,
|
||||
upload_date: new Date().toISOString(),
|
||||
is_late: isLate
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-submissions'] });
|
||||
toast.success('הקישור נוסף בהצלחה');
|
||||
setShowLinkModal(null);
|
||||
setLinkUrl('');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('שגיאה בהוספת הקישור');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteSubmissionMutation = useMutation({
|
||||
mutationFn: (submissionId) => base44.entities.HackalonSubmission.delete(submissionId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['hackalon-submissions'] });
|
||||
toast.success('הקובץ נמחק בהצלחה');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('שגיאה במחיקת הקובץ');
|
||||
}
|
||||
});
|
||||
|
||||
const handleFileUpload = async (e, type) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const existingSubmission = getSubmission(type);
|
||||
|
||||
if (existingSubmission) {
|
||||
const confirmed = window.confirm(
|
||||
`כבר קיים קובץ שהועלה על ידי ${existingSubmission.uploaded_by}.\nהאם לדרוס את הקובץ הקיים?`
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setUploading(type);
|
||||
uploadMutation.mutate({ file, type, existingSubmission });
|
||||
};
|
||||
|
||||
const handleAddLink = (type) => {
|
||||
if (!linkUrl.trim()) return;
|
||||
|
||||
const existingSubmission = getSubmission(type);
|
||||
addLinkMutation.mutate({ url: linkUrl, type, existingSubmission });
|
||||
};
|
||||
|
||||
const handleDelete = (submission) => {
|
||||
const confirmed = window.confirm('האם אתה בטוח שברצונך למחוק את הקובץ?');
|
||||
if (confirmed) {
|
||||
deleteSubmissionMutation.mutate(submission.id);
|
||||
}
|
||||
};
|
||||
|
||||
if (!user || teamLoading || membersLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
if (!user.hackalon_team) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Users className="w-16 h-16 text-slate-300 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">לא שובצת לצוות</h2>
|
||||
<p className="text-slate-600">פנה למנהל המערכת לשיבוץ</p>
|
||||
</Card>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
const getSubmission = (type) => submissions.find((s) => s.submission_type === type);
|
||||
const specSubmission = getSubmission('specification');
|
||||
const finalProductSubmission = getSubmission('final_product');
|
||||
|
||||
// Check if specification deadline passed
|
||||
const isSpecDeadlinePassed = teamInfo?.specification_deadline
|
||||
? new Date() > new Date(teamInfo.specification_deadline)
|
||||
: false;
|
||||
|
||||
// Check if final product deadline passed
|
||||
const isFinalDeadlinePassed = teamInfo?.final_product_deadline
|
||||
? new Date() > new Date(teamInfo.final_product_deadline)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2"> אזור הצוות שלי 🚀
|
||||
|
||||
</h1>
|
||||
<p className="text-slate-500">{user.hackalon_team} • {user.hackalon_department}</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
{/* Team Info */}
|
||||
<Card className="p-6 lg:col-span-2">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<Lightbulb className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-800">הבעיה שלנו</h2>
|
||||
{teamInfo?.classroom_number &&
|
||||
<p className="text-sm text-slate-500 flex items-center gap-1">
|
||||
<MapPin className="w-4 h-4" />
|
||||
כיתה {teamInfo.classroom_number}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{teamInfo?.problem_name ?
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-3">{teamInfo.problem_name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{teamInfo.problem_intro &&
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-purple-600 mb-2">מבוא</h4>
|
||||
<p className="text-slate-600 whitespace-pre-wrap">{teamInfo.problem_intro}</p>
|
||||
</div>
|
||||
}
|
||||
{teamInfo.problem_objective &&
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-purple-600 mb-2">מטרת המוצר</h4>
|
||||
<p className="text-slate-600 whitespace-pre-wrap">{teamInfo.problem_objective}</p>
|
||||
</div>
|
||||
}
|
||||
{teamInfo.problem_requirements &&
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-purple-600 mb-2">דרישות מרכזיות</h4>
|
||||
<p className="text-slate-600 whitespace-pre-wrap">{teamInfo.problem_requirements}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
|
||||
<p className="text-slate-400 text-center py-8">המנהל עדיין לא הגדיר את הבעיה</p>
|
||||
}
|
||||
</Card>
|
||||
|
||||
{/* Team Members */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-800">חברי הצוות</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{teamInfo?.member_names && teamInfo.member_names.length > 0 ?
|
||||
teamInfo.member_names.map((name, idx) => {
|
||||
const matchedUser = teamMembers.find((u) =>
|
||||
(u.onboarding_full_name || u.full_name || '').trim().toLowerCase() === name.trim().toLowerCase()
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={idx} className="p-2 bg-slate-50 rounded-lg">
|
||||
<p className="font-medium text-slate-800 text-sm">{name}</p>
|
||||
{matchedUser ?
|
||||
<p className="text-xs text-slate-500">{matchedUser.email}</p> :
|
||||
|
||||
<p className="text-xs text-slate-400"></p>
|
||||
}
|
||||
</div>);
|
||||
|
||||
}) :
|
||||
teamMembers.length > 0 ?
|
||||
teamMembers.map((member) =>
|
||||
<div key={member.id} className="p-2 bg-slate-50 rounded-lg">
|
||||
<p className="font-medium text-slate-800 text-sm">{member.onboarding_full_name || member.full_name}</p>
|
||||
<p className="text-xs text-slate-500">{member.email}</p>
|
||||
</div>
|
||||
) :
|
||||
|
||||
<p className="text-slate-400 text-sm text-center py-4">אין חברי צוות</p>
|
||||
}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Submissions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Specification */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-6 h-6 text-green-600" />
|
||||
<h3 className="text-lg font-bold text-slate-800">מסמך איפיון</h3>
|
||||
</div>
|
||||
{teamInfo?.specification_deadline && (
|
||||
<div className="text-xs text-slate-500">
|
||||
<div>דדליין: {new Date(teamInfo.specification_deadline).toLocaleTimeString('he-IL', { hour: '2-digit', minute: '2-digit' })} {new Date(teamInfo.specification_deadline).toLocaleDateString('he-IL')}</div>
|
||||
{isSpecDeadlinePassed && !specSubmission && (
|
||||
<div className="text-red-600 font-semibold">חלף המועד!</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Download Template */}
|
||||
{teamInfo?.specification_template_url && !specSubmission && (
|
||||
<a
|
||||
href={teamInfo.specification_template_url}
|
||||
download
|
||||
className="block mb-3 p-3 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-blue-700">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="text-sm font-medium"> הורד טמפלייט</span>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
{specSubmission ?
|
||||
<div className="space-y-2">
|
||||
<a href={specSubmission.file_url} target="_blank" rel="noopener noreferrer" className="block p-3 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-green-800 text-sm">{specSubmission.file_name}</p>
|
||||
<p className="text-xs text-green-600 mt-1">הועלה על ידי {specSubmission.uploaded_by}</p>
|
||||
{specSubmission.is_late && (
|
||||
<p className="text-xs text-red-600 font-semibold mt-1">⚠️ הוגש באיחור</p>
|
||||
)}
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={(e) => {e.preventDefault();handleDelete(specSubmission);}} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</a>
|
||||
</div> :
|
||||
|
||||
<div className="space-y-2">
|
||||
<input type="file" id="spec-upload" className="hidden" onChange={(e) => handleFileUpload(e, 'specification')} disabled={uploading === 'specification'} />
|
||||
<label htmlFor="spec-upload">
|
||||
<Button asChild disabled={uploading === 'specification'} className="w-full cursor-pointer">
|
||||
<span>
|
||||
{uploading === 'specification' ? <Loader2 className="w-4 h-4 ml-2 animate-spin" /> : <Upload className="w-4 h-4 ml-2" />}
|
||||
{uploading === 'specification' ? 'מעלה...' : 'העלה קובץ'}
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
<Button variant="outline" onClick={() => {setShowLinkModal('specification');setLinkUrl('');}} className="w-full">
|
||||
<LinkIcon className="w-4 h-4 ml-2" />
|
||||
הוסף קישור
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
|
||||
{/* Final Product */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<AppWindow className="w-6 h-6 text-purple-600" />
|
||||
<h3 className="text-lg font-bold text-slate-800">תוצר סופי</h3>
|
||||
</div>
|
||||
{teamInfo?.final_product_deadline && (
|
||||
<div className="text-xs text-slate-500">
|
||||
<div>דדליין: {new Date(teamInfo.final_product_deadline).toLocaleTimeString('he-IL', { hour: '2-digit', minute: '2-digit' })} {new Date(teamInfo.final_product_deadline).toLocaleDateString('he-IL')}</div>
|
||||
{isFinalDeadlinePassed && !finalProductSubmission && (
|
||||
<div className="text-red-600 font-semibold">חלף המועד!</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{finalProductSubmission ?
|
||||
<div className="space-y-2">
|
||||
<a href={finalProductSubmission.file_url} target="_blank" rel="noopener noreferrer" className="block p-3 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-purple-800 text-sm">{finalProductSubmission.file_name}</p>
|
||||
<p className="text-xs text-purple-600 mt-1">הועלה על ידי {finalProductSubmission.uploaded_by}</p>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={(e) => {e.preventDefault();handleDelete(finalProductSubmission);}} className="text-red-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</a>
|
||||
</div> :
|
||||
|
||||
<div className="space-y-2">
|
||||
<input type="file" id="final-upload" className="hidden" onChange={(e) => handleFileUpload(e, 'final_product')} disabled={uploading === 'final_product'} />
|
||||
<label htmlFor="final-upload">
|
||||
<Button asChild disabled={uploading === 'final_product'} className="w-full cursor-pointer">
|
||||
<span>
|
||||
{uploading === 'final_product' ? <Loader2 className="w-4 h-4 ml-2 animate-spin" /> : <Upload className="w-4 h-4 ml-2" />}
|
||||
{uploading === 'final_product' ? 'מעלה...' : 'העלה קובץ'}
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
<Button variant="outline" onClick={() => {setShowLinkModal('final_product');setLinkUrl('');}} className="w-full">
|
||||
<LinkIcon className="w-4 h-4 ml-2" />
|
||||
הוסף קישור
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Link Modal */}
|
||||
<Dialog open={!!showLinkModal} onOpenChange={() => setShowLinkModal(null)}>
|
||||
<DialogContent dir="ltr">
|
||||
<DialogHeader>
|
||||
<DialogTitle>הוסף קישור</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>URL</Label>
|
||||
<Input
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
placeholder="https://..."
|
||||
type="url" />
|
||||
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => handleAddLink(showLinkModal)} disabled={!linkUrl.trim()} className="flex-1">הוסף</Button>
|
||||
<Button variant="outline" onClick={() => setShowLinkModal(null)} className="flex-1">ביטול</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
263
Migdalor-main/src/pages/Home.jsx
Normal file
263
Migdalor-main/src/pages/Home.jsx
Normal file
@ -0,0 +1,263 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createPageUrl } from '../utils';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Key, Calendar, Users, UserPen, ArrowLeft, Shield, Settings, LogOut, Lightbulb, Sparkles, Database } from 'lucide-react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function Home() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userPermissions, setUserPermissions] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const u = await base44.auth.me();
|
||||
setUser(u);
|
||||
|
||||
// Load user permissions based on positions
|
||||
if (u.positions && u.positions.length > 0) {
|
||||
const allPermissions = await base44.entities.PositionPermission.list();
|
||||
const userPositionPerms = allPermissions.filter((p) =>
|
||||
u.positions.includes(p.position_name)
|
||||
);
|
||||
|
||||
// Merge all permissions
|
||||
const mergedPerms = {
|
||||
has_classroom_management_access: userPositionPerms.some((p) => p.has_classroom_management_access),
|
||||
pages_access: [...new Set(userPositionPerms.flatMap((p) => p.pages_access || []))]
|
||||
};
|
||||
|
||||
setUserPermissions(mergedPerms);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserData();
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
// Get first accessible page for classroom management
|
||||
const getFirstAccessiblePage = () => {
|
||||
if (isAdmin) return 'Dashboard';
|
||||
|
||||
if (!userPermissions?.has_classroom_management_access) return null;
|
||||
|
||||
const pageOrder = ['Dashboard', 'DailyOverview', 'MySchedule', 'ManageKeys', 'KeyAllocation', 'ManageCrews', 'ManageSquads'];
|
||||
const accessiblePage = pageOrder.find((page) => userPermissions.pages_access.includes(page));
|
||||
|
||||
return accessiblePage || null;
|
||||
};
|
||||
|
||||
const classroomPath = getFirstAccessiblePage();
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: 'ניהול כיתות',
|
||||
description: 'ניהול מפתחות, הקצאת חדרים ולוח זמנים',
|
||||
icon: Key,
|
||||
path: classroomPath,
|
||||
color: 'bg-indigo-500',
|
||||
available: !!classroomPath
|
||||
},
|
||||
{
|
||||
title: 'HackAlon',
|
||||
description: 'ניהול האקאלון, מחזור 2',
|
||||
icon: Lightbulb,
|
||||
path: 'HackalonSchedule',
|
||||
color: 'bg-blue-500',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
title: 'המוצר שלכם יהיה ממש כאן!',
|
||||
description: 'וכאן התיאור שלו...',
|
||||
icon: Sparkles,
|
||||
path: null,
|
||||
color: 'bg-purple-500',
|
||||
available: false
|
||||
}];
|
||||
|
||||
|
||||
const adminFeatures = [
|
||||
{
|
||||
title: 'ניהול משתמשים',
|
||||
description: 'צפייה ועריכת משתמשים במערכת',
|
||||
icon: Settings,
|
||||
path: 'ManageUsers',
|
||||
available: isAdmin
|
||||
},
|
||||
{
|
||||
title: 'ייצוא נתונים',
|
||||
description: 'ייצוא נתונים ל-PostgreSQL/Supabase',
|
||||
icon: Database,
|
||||
path: 'DataExport',
|
||||
available: isAdmin
|
||||
}];
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-800"></div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
const hasPositions = user?.positions && user.positions.length > 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
{/* Top Navigation */}
|
||||
<nav className="bg-white border-b border-slate-200 sticky top-0 z-50">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link to={createPageUrl('Home')} className="flex items-center gap-3">
|
||||
<img
|
||||
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/693b00a201212578d09f8396/9732960ed_8.png"
|
||||
|
||||
alt="מגדלור לוגו"
|
||||
className="w-12 h-12 object-contain" />
|
||||
|
||||
<h2 className="text-lg font-semibold text-slate-800">מגדלור</h2>
|
||||
</Link>
|
||||
|
||||
|
||||
{user &&
|
||||
<div className="flex items-center gap-3">
|
||||
{/* My Profile Button */}
|
||||
<Link to={createPageUrl('MyProfile')}>
|
||||
<Button variant="outline" className="text-slate-700 hover:text-slate-900 hover:bg-slate-50">
|
||||
<UserPen className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* Logout Button */}
|
||||
<Button
|
||||
onClick={() => base44.auth.logout()}
|
||||
variant="outline"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200">
|
||||
|
||||
<LogOut className="w-4 h-4 ml-2" />
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-12">
|
||||
|
||||
<img
|
||||
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/693b00a201212578d09f8396/2f970d938_9.png"
|
||||
alt="מגדלור לוגו"
|
||||
className="w-24 h-24 object-contain mx-auto mb-6" />
|
||||
|
||||
<h1 className="text-4xl font-bold text-slate-800 mb-3">
|
||||
שלום {user.onboarding_full_name || user.full_name} 👋
|
||||
|
||||
</h1>
|
||||
{user &&
|
||||
<p className="text-lg text-slate-600">
|
||||
מגדלור, כאן בשבילך 🙂
|
||||
</p>
|
||||
}
|
||||
<p className="text-slate-500 mt-2">״כשהאור תמיד דולק, הדרך ברורה.״</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((feature, index) =>
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}>
|
||||
|
||||
{feature.available ?
|
||||
<Link to={createPageUrl(feature.path)}>
|
||||
<Card className="p-6 hover:shadow-xl transition-all duration-300 cursor-pointer group h-full border-slate-200">
|
||||
<div className={`w-14 h-14 ${feature.color} rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}>
|
||||
<feature.icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-slate-600 mb-4">{feature.description}</p>
|
||||
<div className="flex items-center gap-2 text-indigo-600 font-medium group-hover:gap-3 transition-all"> <span>כניסה</span> <ArrowLeft className="w-4 h-4" /> </div>
|
||||
|
||||
</Card>
|
||||
</Link> :
|
||||
|
||||
<Card className="p-6 h-full border-slate-200 opacity-60 cursor-not-allowed">
|
||||
<div className={`w-14 h-14 ${feature.color} rounded-xl flex items-center justify-center mb-4 opacity-50`}>
|
||||
<feature.icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-slate-600 mb-4">{feature.description}</p>
|
||||
<div className="flex items-center gap-2 text-slate-400 font-medium">
|
||||
<span>בקרוב...</span>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Admin Section */}
|
||||
{isAdmin &&
|
||||
<div className="mt-12">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="text-2xl font-bold text-slate-800 mb-6">
|
||||
|
||||
🛡️ אזור מנהל
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{adminFeatures.map((feature, index) =>
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 + index * 0.1 }}>
|
||||
|
||||
<Link to={createPageUrl(feature.path)}>
|
||||
<Card className="p-6 hover:shadow-xl transition-all duration-300 cursor-pointer group h-full border-2 border-blue-200 hover:border-blue-300 bg-blue-50/30">
|
||||
<div className="w-14 h-14 bg-blue-600 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
||||
<feature.icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-slate-600 mb-4">{feature.description}</p>
|
||||
<div className="flex items-center gap-2 text-blue-600 font-medium group-hover:gap-3 transition-all">
|
||||
<span>כניסה</span>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
840
Migdalor-main/src/pages/KeyAllocation.jsx
Normal file
840
Migdalor-main/src/pages/KeyAllocation.jsx
Normal file
@ -0,0 +1,840 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow } from
|
||||
"@/components/ui/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue } from
|
||||
"@/components/ui/select";
|
||||
import { Wand2, Calendar, Key, RefreshCw, Loader2, AlertTriangle, CheckCircle, Trash2, Shield } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default function KeyAllocation() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||
const [selectedKeys, setSelectedKeys] = useState([]);
|
||||
const [selectedLessons, setSelectedLessons] = useState([]);
|
||||
const [isAllocating, setIsAllocating] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const { data: allKeys = [] } = useQuery({
|
||||
queryKey: ['keys'],
|
||||
queryFn: () => base44.entities.ClassroomKey.list()
|
||||
});
|
||||
|
||||
const { data: lessons = [], isLoading } = useQuery({
|
||||
queryKey: ['all-lessons', selectedDate],
|
||||
queryFn: () => base44.entities.Lesson.filter({ date: selectedDate }, 'start_time')
|
||||
});
|
||||
|
||||
const { data: specialRequests = [] } = useQuery({
|
||||
queryKey: ['special-requests', selectedDate],
|
||||
queryFn: () => base44.entities.WaitingQueue.filter({ date: selectedDate }, 'priority'),
|
||||
enabled: !!selectedDate
|
||||
});
|
||||
|
||||
const updateLessonMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.Lesson.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
}
|
||||
});
|
||||
|
||||
const deleteLessonMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.Lesson.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
toast.success('שיעור נמחק');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteAllLessonsMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
for (const lesson of lessons) {
|
||||
await base44.entities.Lesson.delete(lesson.id);
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
toast.success('כל השיעורים נמחקו');
|
||||
}
|
||||
});
|
||||
|
||||
const manualAssignMutation = useMutation({
|
||||
mutationFn: async ({ lessonId, roomNumber }) => {
|
||||
// Find the lesson
|
||||
const lesson = lessons.find(l => l.id === lessonId);
|
||||
if (!lesson) throw new Error('שיעור לא נמצא');
|
||||
|
||||
// Check for conflicts - if another lesson is using this room at the same time
|
||||
const conflict = lessons.find(l =>
|
||||
l.id !== lessonId &&
|
||||
l.assigned_key === roomNumber &&
|
||||
l.status === 'assigned' &&
|
||||
timeSlotsOverlap(lesson.start_time, lesson.end_time, l.start_time, l.end_time)
|
||||
);
|
||||
|
||||
if (conflict) {
|
||||
throw new Error(`חדר ${roomNumber} תפוס ע״י ${conflict.crew_name} בשעות ${conflict.start_time}-${conflict.end_time}`);
|
||||
}
|
||||
|
||||
// Assign the key
|
||||
return base44.entities.Lesson.update(lessonId, {
|
||||
assigned_key: roomNumber,
|
||||
status: 'assigned'
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
toast.success('חדר הוקצה בהצלחה');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
const toggleKeySelection = (keyId) => {
|
||||
setSelectedKeys((prev) =>
|
||||
prev.includes(keyId) ?
|
||||
prev.filter((id) => id !== keyId) :
|
||||
[...prev, keyId]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedKeys.length === allKeys.length) {
|
||||
setSelectedKeys([]);
|
||||
} else {
|
||||
setSelectedKeys(allKeys.map((k) => k.id));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleLessonSelection = (lessonId) => {
|
||||
setSelectedLessons((prev) =>
|
||||
prev.includes(lessonId) ?
|
||||
prev.filter((id) => id !== lessonId) :
|
||||
[...prev, lessonId]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSelectAllLessons = () => {
|
||||
const allIds = allItemsToDisplay.map(l => l.id);
|
||||
if (selectedLessons.length === allIds.length) {
|
||||
setSelectedLessons([]);
|
||||
} else {
|
||||
setSelectedLessons(allIds);
|
||||
}
|
||||
};
|
||||
|
||||
const allocateKeys = async () => {
|
||||
setIsAllocating(true);
|
||||
|
||||
try {
|
||||
// Get available keys
|
||||
const availableKeys = allKeys.filter((k) => selectedKeys.includes(k.id));
|
||||
|
||||
// Get pending lessons (only selected ones if any selected, otherwise all)
|
||||
const lessonsToProcess = selectedLessons.length > 0
|
||||
? lessons.filter((l) => selectedLessons.includes(l.id) && l.status === 'pending')
|
||||
: lessons.filter((l) => l.status === 'pending');
|
||||
|
||||
const pendingLessons = lessonsToProcess.sort((a, b) => {
|
||||
// Priority 1: Earlier time
|
||||
const timeA = a.start_time;
|
||||
const timeB = b.start_time;
|
||||
if (timeA !== timeB) return timeA.localeCompare(timeB);
|
||||
|
||||
// Priority 2: פלוגתי rooms first
|
||||
if (a.room_type_needed === 'פלוגתי' && b.room_type_needed === 'צוותי') return -1;
|
||||
if (a.room_type_needed === 'צוותי' && b.room_type_needed === 'פלוגתי') return 1;
|
||||
|
||||
// Priority 3: Needs computers (lower priority for those who don't need)
|
||||
if (a.needs_computers && !b.needs_computers) return -1;
|
||||
if (!a.needs_computers && b.needs_computers) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Add special requests to the end (only selected ones if any lessons selected)
|
||||
const requestsToProcess = selectedLessons.length > 0
|
||||
? specialRequests.filter((req) => selectedLessons.includes(`special_${req.id}`))
|
||||
: specialRequests;
|
||||
|
||||
const specialRequestsToAllocate = requestsToProcess.map((req) => ({
|
||||
id: `special_${req.id}`,
|
||||
crew_name: req.crew_name,
|
||||
start_time: req.start_time,
|
||||
end_time: req.end_time,
|
||||
room_type_needed: req.preferred_type === 'any' ? 'צוותי' : req.preferred_type,
|
||||
needs_computers: false,
|
||||
status: 'pending',
|
||||
isSpecialRequest: true,
|
||||
originalRequestId: req.id
|
||||
}));
|
||||
|
||||
const allToAllocate = [...pendingLessons, ...specialRequestsToAllocate];
|
||||
|
||||
const assignments = [];
|
||||
const failureReasons = [];
|
||||
const crewKeyMap = {}; // Track which key each crew/platoon was assigned
|
||||
|
||||
// Pre-populate crewKeyMap with already assigned lessons (including manual assignments)
|
||||
const alreadyAssigned = lessons.filter(l => l.status === 'assigned' && l.assigned_key);
|
||||
for (const lesson of alreadyAssigned) {
|
||||
const key = availableKeys.find(k => k.room_number === lesson.assigned_key);
|
||||
if (key) {
|
||||
// Remember for crew
|
||||
if (lesson.crew_name && !crewKeyMap[lesson.crew_name]) {
|
||||
crewKeyMap[lesson.crew_name] = key.id;
|
||||
}
|
||||
// Remember for platoon
|
||||
if (lesson.platoon_name && !crewKeyMap[lesson.platoon_name]) {
|
||||
crewKeyMap[lesson.platoon_name] = key.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const lesson of allToAllocate) {
|
||||
// Find suitable key
|
||||
let assignedKey = null;
|
||||
const crewName = lesson.crew_name;
|
||||
const platoonName = lesson.platoon_name;
|
||||
|
||||
// Priority 1: Try to use the same key this CREW already has today
|
||||
if (crewName && crewKeyMap[crewName]) {
|
||||
const previousKey = availableKeys.find((k) => k.id === crewKeyMap[crewName]);
|
||||
// Only reuse if: exact match OR upgrade (צוותי request gets פלוגתי room)
|
||||
// Never downgrade (פלוגתי request gets צוותי room)
|
||||
if (previousKey &&
|
||||
!isKeyOccupied(previousKey, lesson, assignments) &&
|
||||
(previousKey.room_type === lesson.room_type_needed ||
|
||||
(lesson.room_type_needed === 'צוותי' && previousKey.room_type === 'פלוגתי')) &&
|
||||
(!lesson.needs_computers || previousKey.has_computers)) {
|
||||
assignedKey = previousKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Try to use the same key this PLATOON already has today
|
||||
if (!assignedKey && platoonName && crewKeyMap[platoonName]) {
|
||||
const previousKey = availableKeys.find((k) => k.id === crewKeyMap[platoonName]);
|
||||
if (previousKey &&
|
||||
!isKeyOccupied(previousKey, lesson, assignments) &&
|
||||
(previousKey.room_type === lesson.room_type_needed ||
|
||||
(lesson.room_type_needed === 'צוותי' && previousKey.room_type === 'פלוגתי')) &&
|
||||
(!lesson.needs_computers || previousKey.has_computers)) {
|
||||
assignedKey = previousKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Try to find a key in the same zone as previously assigned keys
|
||||
if (!assignedKey) {
|
||||
const crewName = lesson.crew_name;
|
||||
const platoonName = lesson.platoon_name;
|
||||
|
||||
// Check if crew or platoon has a previous key with a zone
|
||||
let preferredZone = null;
|
||||
if (crewName && crewKeyMap[crewName]) {
|
||||
const previousKey = availableKeys.find((k) => k.id === crewKeyMap[crewName]);
|
||||
if (previousKey?.zone) preferredZone = previousKey.zone;
|
||||
}
|
||||
if (!preferredZone && platoonName && crewKeyMap[platoonName]) {
|
||||
const previousKey = availableKeys.find((k) => k.id === crewKeyMap[platoonName]);
|
||||
if (previousKey?.zone) preferredZone = previousKey.zone;
|
||||
}
|
||||
|
||||
// If we have a preferred zone, try to find a key in that zone
|
||||
if (preferredZone) {
|
||||
assignedKey = availableKeys.find(
|
||||
(k) => k.zone === preferredZone &&
|
||||
k.room_type === lesson.room_type_needed &&
|
||||
(!lesson.needs_computers || k.has_computers) &&
|
||||
!isKeyOccupied(k, lesson, assignments)
|
||||
);
|
||||
|
||||
// If not found exact match in zone, try upgrade in same zone
|
||||
if (!assignedKey && lesson.room_type_needed === 'צוותי') {
|
||||
assignedKey = availableKeys.find(
|
||||
(k) => k.zone === preferredZone &&
|
||||
k.room_type === 'פלוגתי' &&
|
||||
(!lesson.needs_computers || k.has_computers) &&
|
||||
!isKeyOccupied(k, lesson, assignments)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First try to find exact match with computer requirement
|
||||
if (!assignedKey && lesson.needs_computers) {
|
||||
assignedKey = availableKeys.find(
|
||||
(k) => k.room_type === lesson.room_type_needed &&
|
||||
k.has_computers &&
|
||||
!isKeyOccupied(k, lesson, assignments)
|
||||
);
|
||||
}
|
||||
|
||||
// If not found or doesn't need computers, find by room type
|
||||
if (!assignedKey) {
|
||||
assignedKey = availableKeys.find(
|
||||
(k) => k.room_type === lesson.room_type_needed &&
|
||||
!isKeyOccupied(k, lesson, assignments)
|
||||
);
|
||||
}
|
||||
|
||||
// If still not found, try any available key (upgrade צוותי to פלוגתי)
|
||||
if (!assignedKey && lesson.room_type_needed === 'צוותי') {
|
||||
assignedKey = availableKeys.find(
|
||||
(k) => k.room_type === 'פלוגתי' &&
|
||||
!isKeyOccupied(k, lesson, assignments)
|
||||
);
|
||||
}
|
||||
|
||||
if (assignedKey) {
|
||||
assignments.push({
|
||||
lessonId: lesson.id,
|
||||
keyId: assignedKey.id,
|
||||
roomNumber: assignedKey.room_number,
|
||||
crewName: lesson.crew_name,
|
||||
startTime: lesson.start_time,
|
||||
endTime: lesson.end_time
|
||||
});
|
||||
|
||||
// Remember this key for this crew and platoon (first match wins)
|
||||
const crewName = lesson.crew_name;
|
||||
const platoonName = lesson.platoon_name;
|
||||
|
||||
if (crewName && !crewKeyMap[crewName]) {
|
||||
crewKeyMap[crewName] = assignedKey.id;
|
||||
}
|
||||
if (platoonName && !crewKeyMap[platoonName]) {
|
||||
crewKeyMap[platoonName] = assignedKey.id;
|
||||
}
|
||||
} else {
|
||||
// Track why this lesson couldn't be assigned
|
||||
const matchingTypeKeys = availableKeys.filter((k) => k.room_type === lesson.room_type_needed);
|
||||
const occupiedKeys = matchingTypeKeys.filter((k) => isKeyOccupied(k, lesson, assignments));
|
||||
|
||||
let reason = `${lesson.crew_name} (${lesson.start_time}-${lesson.end_time}): `;
|
||||
if (matchingTypeKeys.length === 0) {
|
||||
reason += `אין מפתחות מסוג ${lesson.room_type_needed}`;
|
||||
} else if (occupiedKeys.length === matchingTypeKeys.length) {
|
||||
reason += `כל המפתחות מסוג ${lesson.room_type_needed} תפוסים בשעות אלה`;
|
||||
} else if (lesson.needs_computers) {
|
||||
reason += `אין מפתחות עם מחשבים זמינים`;
|
||||
} else {
|
||||
reason += `לא נמצא מפתח מתאים`;
|
||||
}
|
||||
|
||||
failureReasons.push(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply assignments
|
||||
for (const assignment of assignments) {
|
||||
// Check if it's a special request
|
||||
if (assignment.lessonId.startsWith('special_')) {
|
||||
// Create a new lesson for the special request
|
||||
const requestId = assignment.lessonId.replace('special_', '');
|
||||
const originalRequest = specialRequests.find(r => r.id === requestId);
|
||||
|
||||
await base44.entities.Lesson.create({
|
||||
crew_manager: originalRequest?.crew_manager || 'special_request',
|
||||
crew_name: assignment.crewName,
|
||||
platoon_name: originalRequest?.platoon_name || '',
|
||||
date: selectedDate,
|
||||
start_time: assignment.startTime,
|
||||
end_time: assignment.endTime,
|
||||
room_type_needed: originalRequest?.preferred_type === 'any' ? 'צוותי' : originalRequest?.preferred_type,
|
||||
needs_computers: false,
|
||||
assigned_key: assignment.roomNumber,
|
||||
status: 'assigned',
|
||||
notes: originalRequest?.notes || 'בקשה מיוחדת'
|
||||
});
|
||||
|
||||
// Delete the special request from queue
|
||||
await base44.entities.WaitingQueue.delete(requestId);
|
||||
toast.success(`בקשה מיוחדת של ${assignment.crewName} שובצה`);
|
||||
} else {
|
||||
// Regular lesson - update status
|
||||
await updateLessonMutation.mutateAsync({
|
||||
id: assignment.lessonId,
|
||||
data: {
|
||||
assigned_key: assignment.roomNumber,
|
||||
status: 'assigned'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh special requests
|
||||
queryClient.invalidateQueries({ queryKey: ['special-requests'] });
|
||||
|
||||
toast.success(`שובצו בהצלחה ${assignments.length} שיעורים ובקשות!`);
|
||||
|
||||
const unassigned = allToAllocate.length - assignments.length;
|
||||
if (unassigned > 0) {
|
||||
console.log('שיעורים שלא שובצו:', failureReasons);
|
||||
toast.warning(`${unassigned} שיעורים לא שובצו. פתח Console לפרטים`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('שגיאה בהקצאת מפתחות');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsAllocating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if a key is already occupied during the lesson time
|
||||
const isKeyOccupied = (key, newLesson, currentAssignments) => {
|
||||
// Check against temporary assignments
|
||||
const overlapping = currentAssignments.filter((a) => a.keyId === key.id);
|
||||
|
||||
for (const assignment of overlapping) {
|
||||
if (timeSlotsOverlap(
|
||||
newLesson.start_time, newLesson.end_time,
|
||||
assignment.startTime, assignment.endTime
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check against already assigned lessons (manual assignments)
|
||||
const alreadyAssigned = lessons.filter(l =>
|
||||
l.status === 'assigned' &&
|
||||
l.assigned_key === key.room_number
|
||||
);
|
||||
|
||||
for (const lesson of alreadyAssigned) {
|
||||
if (timeSlotsOverlap(
|
||||
newLesson.start_time, newLesson.end_time,
|
||||
lesson.start_time, lesson.end_time
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const timeSlotsOverlap = (start1, end1, start2, end2) => {
|
||||
return start1 < end2 && start2 < end1;
|
||||
};
|
||||
|
||||
const resetAllocations = async () => {
|
||||
const assigned = lessons.filter((l) => l.status === 'assigned');
|
||||
for (const lesson of assigned) {
|
||||
await updateLessonMutation.mutateAsync({
|
||||
id: lesson.id,
|
||||
data: { assigned_key: null, status: 'pending' }
|
||||
});
|
||||
}
|
||||
toast.success('ההקצאות אופסו');
|
||||
};
|
||||
|
||||
// Combine lessons and special requests for display
|
||||
const allItemsToDisplay = [
|
||||
...lessons,
|
||||
...specialRequests.map((req) => ({
|
||||
id: `special_${req.id}`,
|
||||
crew_name: req.crew_name,
|
||||
start_time: req.start_time,
|
||||
end_time: req.end_time,
|
||||
room_type_needed: req.preferred_type === 'any' ? 'צוותי' : req.preferred_type,
|
||||
needs_computers: false,
|
||||
status: 'special_request',
|
||||
notes: req.notes,
|
||||
isSpecialRequest: true,
|
||||
originalRequestId: req.id
|
||||
}))
|
||||
].sort((a, b) => a.start_time.localeCompare(b.start_time));
|
||||
|
||||
const pendingCount = lessons.filter((l) => l.status === 'pending').length;
|
||||
const assignedCount = lessons.filter((l) => l.status === 'assigned').length;
|
||||
const specialRequestsCount = specialRequests.length;
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.role !== 'admin') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">רק מנהלי מערכת יכולים לגשת להקצאת מפתחות</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
הקצאת מפתחות 🎯
|
||||
</h1>
|
||||
|
||||
</motion.div>
|
||||
|
||||
{/* Date and Actions */}
|
||||
{/* Date and Actions */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium">תאריך:</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="w-auto" />
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Button
|
||||
onClick={allocateKeys}
|
||||
disabled={selectedKeys.length === 0 || (pendingCount === 0 && specialRequestsCount === 0) || isAllocating}
|
||||
className="bg-emerald-600 hover:bg-emerald-700">
|
||||
{isAllocating ?
|
||||
<Loader2 className="w-4 h-4 ml-2 animate-spin" /> :
|
||||
<Wand2 className="w-4 h-4 ml-2" />
|
||||
}
|
||||
{selectedLessons.length > 0
|
||||
? `שבץ ${selectedLessons.length} נבחרים`
|
||||
: 'שבץ אוטומטית'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={resetAllocations}
|
||||
disabled={assignedCount === 0}>
|
||||
<RefreshCw className="w-4 h-4 ml-2" />
|
||||
אפס הקצאות
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (confirm('האם למחוק את כל השיעורים?')) {
|
||||
deleteAllLessonsMutation.mutate();
|
||||
}
|
||||
}}
|
||||
disabled={lessons.length === 0}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50">
|
||||
<Trash2 className="w-4 h-4 ml-2" />
|
||||
מחק הכל
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-6 gap-4 mb-6">
|
||||
<Card className="p-4">
|
||||
<p className="text-sm text-slate-500">סה״כ שיעורים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{lessons.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-yellow-50 border-yellow-200">
|
||||
<p className="text-sm text-yellow-600">ממתינים</p>
|
||||
<p className="text-2xl font-bold text-yellow-700">{pendingCount}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-green-50 border-green-200">
|
||||
<p className="text-sm text-green-600">שובצו</p>
|
||||
<p className="text-2xl font-bold text-green-700">{assignedCount}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-purple-50 border-purple-200">
|
||||
<p className="text-sm text-purple-600">בקשות מיוחדות</p>
|
||||
<p className="text-2xl font-bold text-purple-700">{specialRequestsCount}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<p className="text-sm text-blue-600">מפתחות זמינים</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{selectedKeys.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-indigo-50 border-indigo-200">
|
||||
<p className="text-sm text-indigo-600">שיעורים נבחרים</p>
|
||||
<p className="text-2xl font-bold text-indigo-700">{selectedLessons.length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Available Keys Selection */}
|
||||
<Card className="p-6 border-slate-200">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-lg flex items-center gap-2">
|
||||
<Key className="w-5 h-5 text-slate-600" />
|
||||
בחר מפתחות זמינים
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={toggleSelectAll} className="bg-background px-1 text-xs font-medium rounded-md inline-flex items-center justify-center gap-2 whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input shadow-sm hover:bg-accent hover:text-accent-foreground h-8">
|
||||
|
||||
|
||||
{selectedKeys.length === allKeys.length ? 'בטל הכל' : 'בחר הכל'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-[500px] overflow-y-auto">
|
||||
{allKeys.map((key) =>
|
||||
<div
|
||||
key={key.id}
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-slate-200 hover:bg-slate-50">
|
||||
|
||||
<Checkbox
|
||||
id={key.id}
|
||||
checked={selectedKeys.includes(key.id)}
|
||||
onCheckedChange={() => toggleKeySelection(key.id)} />
|
||||
|
||||
<label htmlFor={key.id} className="flex-1 cursor-pointer">
|
||||
<div className="text-slate-700 mx-3 font-medium opacity-100">חדר {key.room_number}</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="bg-teal-100 text-foreground mx-2 px-2.5 py-0.5 text-xs font-semibold rounded-md inline-flex items-center border transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
||||
{key.room_type === 'פלוגתי' ? '🏢' : '🏠'} {key.room_type}
|
||||
</Badge>
|
||||
{key.has_computers &&
|
||||
<Badge variant="outline" className="text-xs">💻</Badge>
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Lessons List */}
|
||||
<Card className="lg:col-span-2 overflow-hidden border-slate-200">
|
||||
<div className="p-6 border-b bg-slate-50 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-slate-600" />
|
||||
לוח זמנים שיעורים
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={toggleSelectAllLessons}
|
||||
className="text-xs">
|
||||
{selectedLessons.length === allItemsToDisplay.length ? 'בטל הכל' : 'בחר הכל'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">✓</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">שעה</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">צוות</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">סוג</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">💻</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">סטטוס</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">חדר</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">הקצאה ידנית</TableHead>
|
||||
<TableHead className="h-10 px-2 text-center align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">מחק</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ?
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-slate-400 mx-auto" />
|
||||
</TableCell>
|
||||
</TableRow> :
|
||||
allItemsToDisplay.length === 0 ?
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-8 text-slate-400">
|
||||
אין שיעורים או בקשות מתוכננים לתאריך זה
|
||||
</TableCell>
|
||||
</TableRow> :
|
||||
|
||||
allItemsToDisplay.map((lesson) =>
|
||||
<TableRow key={lesson.id} className="hover:bg-slate-50/50">
|
||||
<TableCell className="p-2 text-center align-middle">
|
||||
<Checkbox
|
||||
checked={selectedLessons.includes(lesson.id)}
|
||||
onCheckedChange={() => toggleLessonSelection(lesson.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="p-2 text-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] font-mono text-sm">
|
||||
{lesson.start_time}-{lesson.end_time}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 text-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] font-medium">
|
||||
{lesson.crew_name}
|
||||
{lesson.isSpecialRequest && (
|
||||
<Badge className="mr-2 bg-purple-100 text-purple-700 text-xs">בקשה מיוחדת</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 flex items-center justify-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\n">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{lesson.room_type_needed === 'פלוגתי' ? '🏢' : '🏠'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="p-2 text-center align-middle[&:has([role=checkbox])]:pr-0 p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
{lesson.needs_computers ? '✅' : '—'}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 flex items-center justify-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\n">
|
||||
{lesson.status === 'assigned' ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
) : lesson.status === 'special_request' ? (
|
||||
<Badge className="bg-purple-100 text-purple-700 text-xs">תור</Badge>
|
||||
) : (
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 text-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
{lesson.assigned_key ?
|
||||
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100">
|
||||
{lesson.assigned_key}
|
||||
</Badge> :
|
||||
|
||||
<span className="text-slate-400">—</span>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 text-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
{lesson.isSpecialRequest ? (
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={async (value) => {
|
||||
// Create lesson from special request
|
||||
const requestId = lesson.originalRequestId;
|
||||
const originalRequest = specialRequests.find(r => r.id === requestId);
|
||||
|
||||
await base44.entities.Lesson.create({
|
||||
crew_manager: originalRequest?.crew_manager || 'special_request',
|
||||
crew_name: lesson.crew_name,
|
||||
platoon_name: originalRequest?.platoon_name || '',
|
||||
date: selectedDate,
|
||||
start_time: lesson.start_time,
|
||||
end_time: lesson.end_time,
|
||||
room_type_needed: originalRequest?.preferred_type === 'any' ? 'צוותי' : originalRequest?.preferred_type,
|
||||
needs_computers: false,
|
||||
assigned_key: value,
|
||||
status: 'assigned',
|
||||
notes: originalRequest?.notes || 'בקשה מיוחדת'
|
||||
});
|
||||
|
||||
// Delete the special request from queue
|
||||
await base44.entities.WaitingQueue.delete(requestId);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['all-lessons'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['special-requests'] });
|
||||
toast.success(`בקשה מיוחדת של ${lesson.crew_name} שובצה ידנית`);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[120px] text-xs">
|
||||
<SelectValue placeholder="שבץ ידנית" />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
{allKeys.map((key) => (
|
||||
<SelectItem key={key.id} value={key.room_number}>
|
||||
{key.room_type === 'פלוגתי' ? '🏢' : '🏠'} חדר {key.room_number}
|
||||
{key.has_computers && ' 💻'}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Select
|
||||
value={lesson.assigned_key || ''}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'unassign') {
|
||||
updateLessonMutation.mutate({
|
||||
id: lesson.id,
|
||||
data: { assigned_key: null, status: 'pending' }
|
||||
});
|
||||
} else {
|
||||
manualAssignMutation.mutate({
|
||||
lessonId: lesson.id,
|
||||
roomNumber: value
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[120px] text-xs">
|
||||
<SelectValue placeholder="בחר חדר" />
|
||||
</SelectTrigger>
|
||||
<SelectContent dir="rtl">
|
||||
{lesson.assigned_key && (
|
||||
<SelectItem value="unassign" className="text-red-600">
|
||||
❌ בטל הקצאה
|
||||
</SelectItem>
|
||||
)}
|
||||
{allKeys.map((key) => (
|
||||
<SelectItem key={key.id} value={key.room_number}>
|
||||
{key.room_type === 'פלוגתי' ? '🏢' : '🏠'} חדר {key.room_number}
|
||||
{key.has_computers && ' 💻'}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 text-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
if (lesson.isSpecialRequest) {
|
||||
base44.entities.WaitingQueue.delete(lesson.originalRequestId).then(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['special-requests'] });
|
||||
toast.success('בקשה מיוחדת נמחקה');
|
||||
});
|
||||
} else {
|
||||
deleteLessonMutation.mutate(lesson.id);
|
||||
}
|
||||
}}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Priority Info */}
|
||||
<Card className="mt-6 p-6 bg-blue-50 border-blue-200">
|
||||
<h4 className="font-semibold text-blue-900 mb-3">סדר עדיפויות הקצאה:</h4>
|
||||
<ol className="space-y-2 text-sm text-blue-800">
|
||||
<li>1. <strong>שימור כיתות לצוות - צוות שקיבל כיתה מסוימת ישאר איתה לאורך היום</strong></li>
|
||||
<li>2. <strong>שימור כיתות לפלוגה - העדפה לאותה כיתה שהפלוגה השתמשה בה</strong></li>
|
||||
<li>3. <strong>שימור אזור - העדפה לכיתות באותו אזור פיזי</strong></li>
|
||||
<li>4. שיעורים מוקדמים יותר מקבלים עדיפות</li>
|
||||
<li>5. חדרים פלוגתיים משובצים ראשונים</li>
|
||||
<li>6. שיעורים שדורשים מחשבים מקבלים עדיפות על פני אלו שלא</li>
|
||||
<li>7. בקשות לחדרים צוותיים עשויות לקבל שדרוג לפלוגתי במידת הצורך</li>
|
||||
<li>8. <strong>בקשות מיוחדות מקבלות עדיפות נמוכה - משובצות אחרונות</strong></li>
|
||||
</ol>
|
||||
</Card>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
}
|
||||
316
Migdalor-main/src/pages/ManageCrews.jsx
Normal file
316
Migdalor-main/src/pages/ManageCrews.jsx
Normal file
@ -0,0 +1,316 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription } from
|
||||
"@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow } from
|
||||
"@/components/ui/table";
|
||||
import { Plus, Users, Trash2, Edit2, Phone, GripVertical } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
export default function ManageCrews() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingCrew, setEditingCrew] = useState(null);
|
||||
const [user, setUser] = useState(null);
|
||||
const [formData, setFormData] = useState({ name: '', contact: '', notes: '' });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const { data: crews = [], isLoading } = useQuery({
|
||||
queryKey: ['crews'],
|
||||
queryFn: async () => {
|
||||
const data = await base44.entities.Crew.list();
|
||||
return data.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.Crew.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['crews'] });
|
||||
setShowModal(false);
|
||||
setFormData({ name: '', contact: '', notes: '' });
|
||||
toast.success('פלוגה נוספה בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.Crew.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['crews'] });
|
||||
setShowModal(false);
|
||||
setEditingCrew(null);
|
||||
setFormData({ name: '', contact: '', notes: '' });
|
||||
toast.success('פלוגה עודכנה בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.Crew.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['crews'] });
|
||||
toast.success('פלוגה נמחקה בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingCrew) {
|
||||
updateMutation.mutate({ id: editingCrew.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (crew) => {
|
||||
setEditingCrew(crew);
|
||||
setFormData({ name: crew.name, contact: crew.contact || '', notes: crew.notes || '' });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowModal(false);
|
||||
setEditingCrew(null);
|
||||
setFormData({ name: '', contact: '', notes: '' });
|
||||
};
|
||||
|
||||
const handleDragEnd = async (result) => {
|
||||
if (!result.destination || !isAdmin) return;
|
||||
|
||||
const items = Array.from(crews);
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
// Update order for all items
|
||||
const updates = items.map((item, index) =>
|
||||
base44.entities.Crew.update(item.id, { order: index })
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
queryClient.invalidateQueries({ queryKey: ['crews'] });
|
||||
toast.success('הסדר עודכן');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול פלוגות 👥
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-8">
|
||||
<Card className="p-4 border-slate-200 inline-block">
|
||||
<p className="text-sm text-slate-500">סה״כ פלוגות</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{crews.length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add Button */}
|
||||
{isAdmin &&
|
||||
<div className="flex justify-end mb-6">
|
||||
<Button onClick={() => setShowModal(true)} className="bg-indigo-600 hover:bg-indigo-700">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף פלוגה חדשה
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* Crews Table */}
|
||||
<Card className="overflow-hidden border-slate-200">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50">
|
||||
{isAdmin && <TableHead className="w-12"></TableHead>}
|
||||
<TableHead className="text-muted-foreground mx-64 my-8 px-2 font-medium text-left h-10 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">פלוגות</TableHead>
|
||||
<TableHead className="h-1 px-2 align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">איש קשר</TableHead>
|
||||
<TableHead className="text-center">הערות</TableHead>
|
||||
{isAdmin && <TableHead className="text-center">פעולות</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="crews">
|
||||
{(provided) => (
|
||||
<TableBody {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{isLoading ?
|
||||
<TableRow>
|
||||
<TableCell colSpan={isAdmin ? 5 : 3} className="text-center py-8 text-slate-400">
|
||||
טוען...
|
||||
</TableCell>
|
||||
</TableRow> :
|
||||
crews.length === 0 ?
|
||||
<TableRow>
|
||||
<TableCell colSpan={isAdmin ? 5 : 3} className="text-center py-8 text-slate-400">
|
||||
עדיין לא נוספו פלוגות
|
||||
</TableCell>
|
||||
</TableRow> :
|
||||
|
||||
crews.map((crew, index) =>
|
||||
<Draggable key={crew.id} draggableId={crew.id} index={index} isDragDisabled={!isAdmin}>
|
||||
{(provided, snapshot) => (
|
||||
<TableRow
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`hover:bg-slate-50/50 [&_td]:text-center ${snapshot.isDragging ? 'bg-slate-100' : ''}`}
|
||||
>
|
||||
{isAdmin && (
|
||||
<TableCell {...provided.dragHandleProps} className="cursor-grab active:cursor-grabbing">
|
||||
<GripVertical className="w-4 h-4 text-slate-400 mx-auto" />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="font-medium text-center">
|
||||
<div className="flex flex-row-reverse items-center justify-between gap-2">
|
||||
<span className="flex-1 text-center">{crew.name}</span>
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0">
|
||||
<Users className="w-4 h-4 text-indigo-600" />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{crew.contact ?
|
||||
<a
|
||||
href={`tel:${crew.contact}`}
|
||||
className="flex flex-row-reverse items-center justify-center gap-2 text-slate-600 hover:text-blue-600 transition-colors cursor-pointer">
|
||||
|
||||
<Phone className="w-4 h-4" />
|
||||
<span dir="ltr">{crew.contact}</span>
|
||||
</a> :
|
||||
|
||||
<span className="text-slate-400">—</span>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-500 max-w-xs truncate">
|
||||
{crew.notes || '—'}
|
||||
</TableCell>
|
||||
{isAdmin &&
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(crew)}
|
||||
className="text-slate-400 hover:text-slate-600">
|
||||
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteMutation.mutate(crew.id)}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50">
|
||||
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
}
|
||||
</TableRow>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
{provided.placeholder}
|
||||
</TableBody>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
|
||||
{editingCrew ? 'ערוך פלוגה' : 'הוסף פלוגה חדשה'}
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<Users className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-right">
|
||||
{editingCrew ? 'עדכן את פרטי הפלוגה' : 'הוסף פלוגה חדשה למעקב'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">שם הפלוגה</Label>
|
||||
<Input
|
||||
placeholder="למשל, פלוגת יפתח..."
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="text-right" />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">טלפון איש קשר</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="+972.."
|
||||
value={formData.contact}
|
||||
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
|
||||
className="text-right" />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">הערות (אופציונלי)</Label>
|
||||
<Textarea
|
||||
placeholder="שם הקה״ד הפלוגתי"
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
className="h-20 text-right" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button variant="outline" onClick={handleClose} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.name}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-700">
|
||||
|
||||
{editingCrew ? 'עדכן פלוגה' : 'הוסף פלוגה'}
|
||||
</Button>
|
||||
|
||||
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>);
|
||||
|
||||
}
|
||||
532
Migdalor-main/src/pages/ManageKeys.jsx
Normal file
532
Migdalor-main/src/pages/ManageKeys.jsx
Normal file
@ -0,0 +1,532 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription } from
|
||||
"@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue } from
|
||||
"@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow } from
|
||||
"@/components/ui/table";
|
||||
import { Plus, Key, Trash2, Edit2, Monitor } from 'lucide-react';
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function ManageKeys() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingKey, setEditingKey] = useState(null);
|
||||
const [misdarEditKey, setMisdarEditKey] = useState(null);
|
||||
const [misdarValue, setMisdarValue] = useState('');
|
||||
const [user, setUser] = useState(null);
|
||||
const [formData, setFormData] = useState({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const { data: keys = [], isLoading } = useQuery({
|
||||
queryKey: ['keys'],
|
||||
queryFn: () => base44.entities.ClassroomKey.list()
|
||||
});
|
||||
|
||||
const { data: zones = [] } = useQuery({
|
||||
queryKey: ['zones'],
|
||||
queryFn: () => base44.entities.Zone.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: todayLessons = [] } = useQuery({
|
||||
queryKey: ['today-lessons'],
|
||||
queryFn: async () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return base44.entities.Lesson.filter({ date: today, status: 'assigned' });
|
||||
}
|
||||
});
|
||||
|
||||
const { data: wednesdayLessons = [] } = useQuery({
|
||||
queryKey: ['wednesday-lessons'],
|
||||
queryFn: async () => {
|
||||
// Find the next Wednesday (or today if it's Wednesday)
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay();
|
||||
const daysUntilWednesday = dayOfWeek === 3 ? 0 : (3 - dayOfWeek + 7) % 7;
|
||||
const nextWednesday = new Date(today);
|
||||
nextWednesday.setDate(today.getDate() + daysUntilWednesday);
|
||||
const wednesdayDate = nextWednesday.toISOString().split('T')[0];
|
||||
|
||||
return base44.entities.Lesson.filter({ date: wednesdayDate, status: 'assigned' }, '-end_time');
|
||||
},
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: allUsers = [] } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => base44.entities.User.list(),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
// Get current key holder for a room
|
||||
const getCurrentHolder = (roomNumber) => {
|
||||
const now = new Date();
|
||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
const currentLesson = todayLessons.find((lesson) =>
|
||||
lesson.assigned_key === roomNumber &&
|
||||
lesson.start_time <= currentTime &&
|
||||
lesson.end_time > currentTime
|
||||
);
|
||||
|
||||
return currentLesson ? currentLesson.crew_name : null;
|
||||
};
|
||||
|
||||
// Get who's responsible for cleaning this room (Misdar)
|
||||
const getMisdarResponsible = (key) => {
|
||||
// Check for manual assignment first
|
||||
if (key.manual_misdar_assignment) {
|
||||
return { crewName: key.manual_misdar_assignment, platoon: null };
|
||||
}
|
||||
|
||||
if (!wednesdayLessons.length) return null;
|
||||
|
||||
// Find all lessons for this room
|
||||
const roomLessons = wednesdayLessons.filter((l) => l.assigned_key === key.room_number);
|
||||
if (roomLessons.length === 0) return null;
|
||||
|
||||
// Check each lesson to see if the key was passed to another crew
|
||||
for (const lesson of roomLessons) {
|
||||
// Check if there's another lesson that took this key after this one
|
||||
const nextLesson = wednesdayLessons.find((l) =>
|
||||
l.assigned_key === key.room_number &&
|
||||
l.crew_manager !== lesson.crew_manager &&
|
||||
l.start_time >= lesson.end_time
|
||||
);
|
||||
|
||||
// If no one took the key after this lesson, this crew is responsible
|
||||
if (!nextLesson) {
|
||||
// Find the user who created this lesson to get their platoon
|
||||
const userWhoCreated = allUsers.find((u) => u.email === lesson.crew_manager);
|
||||
const platoon = userWhoCreated?.platoon_name || null;
|
||||
return { crewName: lesson.crew_name, platoon };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.ClassroomKey.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||||
setShowModal(false);
|
||||
setFormData({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||||
toast.success('מפתח נוסף בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.ClassroomKey.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||||
setShowModal(false);
|
||||
setEditingKey(null);
|
||||
setFormData({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||||
toast.success('מפתח עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.ClassroomKey.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['keys'] });
|
||||
toast.success('מפתח נמחק בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingKey) {
|
||||
updateMutation.mutate({ id: editingKey.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate({ ...formData, status: 'available' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (key) => {
|
||||
setEditingKey(key);
|
||||
setFormData({ room_number: key.room_number, room_type: key.room_type, has_computers: key.has_computers || false, zone: key.zone || '' });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowModal(false);
|
||||
setEditingKey(null);
|
||||
setFormData({ room_number: '', room_type: 'צוותי', has_computers: false, zone: '' });
|
||||
};
|
||||
|
||||
const handleMisdarEdit = (key) => {
|
||||
setMisdarEditKey(key);
|
||||
setMisdarValue(key.manual_misdar_assignment || '');
|
||||
};
|
||||
|
||||
const handleMisdarSave = async () => {
|
||||
if (misdarEditKey) {
|
||||
await updateMutation.mutateAsync({
|
||||
id: misdarEditKey.id,
|
||||
data: { manual_misdar_assignment: misdarValue || null }
|
||||
});
|
||||
setMisdarEditKey(null);
|
||||
setMisdarValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const smallCount = keys.filter((k) => k.room_type === 'צוותי').length;
|
||||
const largeCount = keys.filter((k) => k.room_type === 'פלוגתי').length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8">
|
||||
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול מפתחות 🗝️
|
||||
</h1>
|
||||
<p className="text-slate-500">
|
||||
הוסף, ערוך או הסר מפתחות כיתות
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
|
||||
<Card className="p-4 border-slate-200">
|
||||
<p className="text-sm text-slate-500">סה״כ מפתחות</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{keys.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 border-blue-200 bg-blue-50/50">
|
||||
<p className="text-sm text-blue-600">חדרים צוותיים</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{smallCount}</p>
|
||||
</Card>
|
||||
<Card className="p-4 border-purple-200 bg-purple-50/50">
|
||||
<p className="text-sm text-purple-600">חדרים פלוגתיים</p>
|
||||
<p className="text-2xl font-bold text-purple-700">{largeCount}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{isAdmin &&
|
||||
<div className="flex justify-end mb-6">
|
||||
<Button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="bg-emerald-600 hover:bg-emerald-700">
|
||||
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף מפתח חדש
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* Keys Table */}
|
||||
<Card className="overflow-hidden border-slate-200">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50">
|
||||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">מספר חדר</TableHead>
|
||||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">סוג</TableHead>
|
||||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">אזור</TableHead>
|
||||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">מחשבים</TableHead>
|
||||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">סטטוס / מחזיק</TableHead>
|
||||
{isAdmin &&
|
||||
<TableHead className="h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">מסדר כיתות 🧹</TableHead>
|
||||
}
|
||||
{isAdmin &&
|
||||
<TableHead className="h-10 px-2 align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center">פעולות</TableHead>
|
||||
}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ?
|
||||
<TableRow>
|
||||
<TableCell colSpan={isAdmin ? 7 : 5} className="text-center py-8 text-slate-400">
|
||||
טוען...
|
||||
</TableCell>
|
||||
</TableRow> :
|
||||
keys.length === 0 ?
|
||||
<TableRow>
|
||||
<TableCell colSpan={isAdmin ? 7 : 5} className="text-center py-8 text-slate-400">
|
||||
עדיין לא נוספו מפתחות
|
||||
</TableCell>
|
||||
</TableRow> :
|
||||
|
||||
keys.map((key) =>
|
||||
<TableRow key={key.id} className="hover:bg-slate-50/50">
|
||||
<TableCell className="p-2 text-center flex items-center justify-center align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="w-4 h-4 text-slate-400" />
|
||||
{key.room_number}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
<Badge variant="outline" className={
|
||||
key.room_type === 'פלוגתי' ?
|
||||
'border-purple-300 text-purple-700' :
|
||||
'border-blue-300 text-blue-700'
|
||||
}>
|
||||
{key.room_type === 'פלוגתי' ? '🏢 פלוגתי' : '🏠 צוותי'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
{key.zone ? (
|
||||
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
||||
📍 {key.zone}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-slate-400">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-cente my-3 p-2 text-center t te tex texx text align-middle flex items-center justify-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
{key.has_computers ?
|
||||
<Monitor className="w-4 h-4 text-blue-600" /> :
|
||||
|
||||
<span className="text-slate-300">—</span>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
{(() => {
|
||||
const holder = getCurrentHolder(key.room_number);
|
||||
return holder ?
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100">
|
||||
תפוס
|
||||
</Badge>
|
||||
<span className="text-xs text-slate-600">{holder}</span>
|
||||
</div> :
|
||||
|
||||
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100">
|
||||
זמין
|
||||
</Badge>;
|
||||
|
||||
})()}
|
||||
</TableCell>
|
||||
{isAdmin &&
|
||||
<TableCell className="p-2 align-middle text-center [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{(() => {
|
||||
const responsible = getMisdarResponsible(key);
|
||||
return responsible ?
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-200">
|
||||
🧹 {responsible.crewName}
|
||||
</Badge>
|
||||
{responsible.platoon &&
|
||||
<span className="text-xs text-slate-500 font-medium">
|
||||
{responsible.platoon}
|
||||
</span>
|
||||
}
|
||||
</div> :
|
||||
|
||||
<span className="text-slate-400 text-xs">—</span>;
|
||||
|
||||
})()}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleMisdarEdit(key)}
|
||||
className="h-6 w-6 text-slate-400 hover:text-orange-600">
|
||||
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
}
|
||||
{isAdmin &&
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(key)}
|
||||
className="text-slate-400 hover:text-slate-600">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteMutation.mutate(key.id)}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
}
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Misdar Edit Modal */}
|
||||
<Dialog open={!!misdarEditKey} onOpenChange={() => {setMisdarEditKey(null);setMisdarValue('');}}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
ערוך מסדר כיתות
|
||||
<div className="p-2 bg-orange-100 rounded-lg">
|
||||
<span className="text-lg">🧹</span>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-right">
|
||||
הגדר ידנית איזו פלוגה אחראית על מסדר חדר {misdarEditKey?.room_number}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">שם הפלוגה האחראית</Label>
|
||||
<select
|
||||
value={misdarValue}
|
||||
onChange={(e) => setMisdarValue(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right">
|
||||
|
||||
<option value="">חישוב אוטומטי</option>
|
||||
<option value="פלוגה א - סהר">פלוגה א - סהר</option>
|
||||
<option value="פלוגה ב - יפתח">פלוגה ב - יפתח</option>
|
||||
<option value="פלוגה ג - אייל">פלוגה ג - אייל</option>
|
||||
<option value="פלוגה ד - אסף">פלוגה ד - אסף</option>
|
||||
<option value="פלוגה ה - איתן">פלוגה ה - איתן</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-500 text-right">
|
||||
בחר "חישוב אוטומטי" כדי להשתמש בחישוב לפי לוח השיעורים ביום רביעי
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button variant="outline" onClick={() => {setMisdarEditKey(null);setMisdarValue('');}} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleMisdarSave}
|
||||
className="flex-1 bg-orange-600 hover:bg-orange-700">
|
||||
|
||||
שמור
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
|
||||
{editingKey ? 'ערוך מפתח' : 'הוסף מפתח חדש'}
|
||||
<div className="p-2 bg-emerald-100 rounded-lg">
|
||||
|
||||
<Key className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-right">
|
||||
{editingKey ? 'עדכן את פרטי המפתח' : 'הוסף מפתח חדש למעקב'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-right block">מספר חדר *</Label>
|
||||
<Input
|
||||
placeholder="למשל, 101..."
|
||||
value={formData.room_number}
|
||||
onChange={(e) => setFormData({ ...formData, room_number: e.target.value })}
|
||||
className="text-right" />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">סוג חדר</Label>
|
||||
<Select
|
||||
value={formData.room_type}
|
||||
onValueChange={(value) => setFormData({ ...formData, room_type: value })}>
|
||||
|
||||
<SelectTrigger className="text-right" dir="rtl">
|
||||
<SelectValue className="text-right" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" dir="rtl">
|
||||
<SelectItem value="צוותי">צוותי 🏠</SelectItem>
|
||||
<SelectItem value="פלוגתי">פלוגתי 🏢</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
<Label htmlFor="has_computers" className="cursor-pointer">
|
||||
יש מחשב בכיתה 💻
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="has_computers"
|
||||
checked={formData.has_computers}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData({ ...formData, has_computers: checked })
|
||||
} />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">אזור (אופציונלי)</Label>
|
||||
<select
|
||||
value={formData.zone}
|
||||
onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="">בחר אזור...</option>
|
||||
{zones.map((zone) => (
|
||||
<option key={zone.id} value={zone.name}>{zone.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button variant="outline" onClick={handleClose} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.room_number}
|
||||
className="flex-1 bg-emerald-600 hover:bg-emerald-700">
|
||||
|
||||
{editingKey ? 'עדכן מפתח' : 'הוסף מפתח'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>);
|
||||
|
||||
}
|
||||
411
Migdalor-main/src/pages/ManagePermissions.jsx
Normal file
411
Migdalor-main/src/pages/ManagePermissions.jsx
Normal file
@ -0,0 +1,411 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Shield, Save, Loader2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function ManagePermissions() {
|
||||
const [expandedPosition, setExpandedPosition] = useState(null);
|
||||
const [user, setUser] = useState(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
// Fetch positions
|
||||
const { data: positions = [], isLoading: positionsLoading } = useQuery({
|
||||
queryKey: ['positions'],
|
||||
queryFn: () => base44.entities.Position.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
// Fetch existing permissions
|
||||
const { data: permissions = [], isLoading: permissionsLoading } = useQuery({
|
||||
queryKey: ['permissions'],
|
||||
queryFn: () => base44.entities.PositionPermission.list(),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
// Available pages for access control
|
||||
const availablePages = [
|
||||
{ id: 'Dashboard', name: 'לוח בקרה', area: 'classroom' },
|
||||
{ id: 'DailyOverview', name: 'תמונת מצב', area: 'classroom' },
|
||||
{ id: 'KeyAllocation', name: 'הקצאת מפתחות', area: 'classroom' },
|
||||
{ id: 'ManageKeys', name: 'ניהול מפתחות', area: 'classroom' },
|
||||
{ id: 'MySchedule', name: 'לוח הזמנים שלי', area: 'classroom' },
|
||||
{ id: 'ManageCrews', name: 'ניהול פלוגות', area: 'classroom' },
|
||||
{ id: 'ManageSquads', name: 'ניהול צוותים', area: 'classroom' },
|
||||
{ id: 'HackalonSchedule', name: 'HackAlon - לוח זמנים', area: 'hackalon' },
|
||||
{ id: 'HackalonTeamArea', name: 'HackAlon - אזור הצוות', area: 'hackalon' },
|
||||
{ id: 'HackalonOverview', name: 'HackAlon - סקירה', area: 'hackalon' },
|
||||
{ id: 'HackalonStatus', name: 'HackAlon - תמונת מצב', area: 'hackalon' },
|
||||
{ id: 'HackalonAssignment', name: 'HackAlon - שיבוץ צוערים', area: 'hackalon' },
|
||||
{ id: 'HackalonManageProblems', name: 'HackAlon - ניהול בעיות', area: 'hackalon' },
|
||||
];
|
||||
|
||||
// Pages under classroom management (only these require access toggle)
|
||||
const classroomPages = availablePages.filter(p => p.area === 'classroom');
|
||||
const hackalonPages = availablePages.filter(p => p.area === 'hackalon');
|
||||
|
||||
// Available entities for CRUD permissions
|
||||
const availableEntities = [
|
||||
{ id: 'ClassroomKey', name: 'מפתחות כיתות' },
|
||||
{ id: 'Lesson', name: 'שיעורים' },
|
||||
{ id: 'WaitingQueue', name: 'תור המתנה' },
|
||||
{ id: 'Crew', name: 'פלוגות' },
|
||||
{ id: 'Squad', name: 'צוותים' },
|
||||
];
|
||||
|
||||
const crudOptions = [
|
||||
{ id: 'read', name: 'צפייה', icon: '👁️' },
|
||||
{ id: 'create', name: 'יצירה', icon: '➕' },
|
||||
{ id: 'update', name: 'עריכה', icon: '✏️' },
|
||||
{ id: 'delete', name: 'מחיקה', icon: '🗑️' },
|
||||
];
|
||||
|
||||
// Get permission for a specific position
|
||||
const getPermissionForPosition = (positionId) => {
|
||||
return permissions.find(p => p.position_id === positionId);
|
||||
};
|
||||
|
||||
// Update permissions mutation
|
||||
const updatePermissionMutation = useMutation({
|
||||
mutationFn: async ({ positionId, positionName, pagesAccess, entityPermissions, hasClassroomAccess }) => {
|
||||
const existing = getPermissionForPosition(positionId);
|
||||
|
||||
if (existing) {
|
||||
return base44.entities.PositionPermission.update(existing.id, {
|
||||
pages_access: pagesAccess,
|
||||
entity_permissions: entityPermissions,
|
||||
has_classroom_management_access: hasClassroomAccess
|
||||
});
|
||||
} else {
|
||||
return base44.entities.PositionPermission.create({
|
||||
position_id: positionId,
|
||||
position_name: positionName,
|
||||
pages_access: pagesAccess,
|
||||
entity_permissions: entityPermissions,
|
||||
has_classroom_management_access: hasClassroomAccess || false
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['permissions'] });
|
||||
toast.success('ההרשאות עודכנו בהצלחה');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('שגיאה בעדכון ההרשאות');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle classroom management access toggle
|
||||
const handleClassroomAccessToggle = (position) => {
|
||||
const permission = getPermissionForPosition(position.id);
|
||||
const newAccess = !permission?.has_classroom_management_access;
|
||||
|
||||
updatePermissionMutation.mutate({
|
||||
positionId: position.id,
|
||||
positionName: position.title,
|
||||
pagesAccess: permission?.pages_access || [],
|
||||
entityPermissions: permission?.entity_permissions || {},
|
||||
hasClassroomAccess: newAccess
|
||||
});
|
||||
};
|
||||
|
||||
// Handle page access toggle
|
||||
const handlePageToggle = (position, pageId) => {
|
||||
const permission = getPermissionForPosition(position.id);
|
||||
const currentPages = permission?.pages_access || [];
|
||||
const newPages = currentPages.includes(pageId)
|
||||
? currentPages.filter(p => p !== pageId)
|
||||
: [...currentPages, pageId];
|
||||
|
||||
updatePermissionMutation.mutate({
|
||||
positionId: position.id,
|
||||
positionName: position.title,
|
||||
pagesAccess: newPages,
|
||||
entityPermissions: permission?.entity_permissions || {},
|
||||
hasClassroomAccess: permission?.has_classroom_management_access || false
|
||||
});
|
||||
};
|
||||
|
||||
// Handle entity CRUD toggle
|
||||
const handleEntityCrudToggle = (position, entityId, crudType) => {
|
||||
const permission = getPermissionForPosition(position.id);
|
||||
const currentEntityPerms = permission?.entity_permissions || {};
|
||||
const currentCrud = currentEntityPerms[entityId] || [];
|
||||
|
||||
const newCrud = currentCrud.includes(crudType)
|
||||
? currentCrud.filter(c => c !== crudType)
|
||||
: [...currentCrud, crudType];
|
||||
|
||||
const newEntityPerms = {
|
||||
...currentEntityPerms,
|
||||
[entityId]: newCrud
|
||||
};
|
||||
|
||||
updatePermissionMutation.mutate({
|
||||
positionId: position.id,
|
||||
positionName: position.title,
|
||||
pagesAccess: permission?.pages_access || [],
|
||||
entityPermissions: newEntityPerms,
|
||||
hasClassroomAccess: permission?.has_classroom_management_access || false
|
||||
});
|
||||
};
|
||||
|
||||
if (!user || positionsLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center" dir="rtl">
|
||||
<Card className="p-8 text-center max-w-md">
|
||||
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">אין הרשאת גישה</h2>
|
||||
<p className="text-slate-600">רק מנהלי מערכת יכולים לנהל הרשאות תפקידים</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול הרשאות תפקידים 🔐
|
||||
</h1>
|
||||
<p className="text-slate-500">
|
||||
הגדר לכל תפקיד אילו עמודים וישויות הוא יכול לגשת אליהם
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<Card className="p-4">
|
||||
<p className="text-sm text-slate-500">סה״כ תפקידים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{positions.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<p className="text-sm text-blue-600">תפקידים עם הרשאות</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{permissions.length}</p>
|
||||
</Card>
|
||||
<Card className="p-4 bg-green-50 border-green-200">
|
||||
<p className="text-sm text-green-600">עמודים זמינים</p>
|
||||
<p className="text-2xl font-bold text-green-700">{availablePages.length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Positions List */}
|
||||
<div className="space-y-4">
|
||||
{positions.map((position, index) => {
|
||||
const permission = getPermissionForPosition(position.id);
|
||||
const isExpanded = expandedPosition === position.id;
|
||||
const pagesAccess = permission?.pages_access || [];
|
||||
const entityPermissions = permission?.entity_permissions || {};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={position.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<Card className="overflow-hidden">
|
||||
{/* Position Header */}
|
||||
<div
|
||||
onClick={() => setExpandedPosition(isExpanded ? null : position.id)}
|
||||
className="p-4 cursor-pointer hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Shield className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800">
|
||||
{position.title}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
{pagesAccess.length} עמודים • {Object.keys(entityPermissions).length} ישויות
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-slate-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-slate-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-6 border-t border-slate-200 bg-slate-50/50">
|
||||
{/* Main Classroom Management Access Toggle */}
|
||||
<div className="mb-6 p-4 bg-blue-50 border-2 border-blue-200 rounded-lg">
|
||||
<div
|
||||
onClick={() => handleClassroomAccessToggle(position)}
|
||||
className="flex items-center gap-3 cursor-pointer"
|
||||
>
|
||||
<Checkbox checked={permission?.has_classroom_management_access || false} />
|
||||
<div>
|
||||
<Label className="cursor-pointer text-base font-semibold text-blue-900">
|
||||
גישה לניהול כיתות
|
||||
</Label>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
סמן כדי לאפשר גישה לכל מערכת ניהול הכיתות
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show pages and entities only if classroom access is enabled */}
|
||||
{permission?.has_classroom_management_access && (
|
||||
<>
|
||||
{/* Pages Access */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-md font-semibold text-slate-700 mb-3">
|
||||
גישה לעמודים - ניהול כיתות
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{classroomPages.map((page) => {
|
||||
const hasAccess = pagesAccess.includes(page.id);
|
||||
return (
|
||||
<div
|
||||
key={page.id}
|
||||
onClick={() => handlePageToggle(position, page.id)}
|
||||
className={`p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
||||
hasAccess
|
||||
? 'bg-blue-50 border-blue-300'
|
||||
: 'bg-white border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={hasAccess} />
|
||||
<Label className="cursor-pointer text-sm">
|
||||
{page.name}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* HackAlon Pages Access */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-md font-semibold text-slate-700 mb-3">
|
||||
גישה לעמודים - HackAlon
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{hackalonPages.map((page) => {
|
||||
const hasAccess = pagesAccess.includes(page.id);
|
||||
return (
|
||||
<div
|
||||
key={page.id}
|
||||
onClick={() => handlePageToggle(position, page.id)}
|
||||
className={`p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
||||
hasAccess
|
||||
? 'bg-purple-50 border-purple-300'
|
||||
: 'bg-white border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={hasAccess} />
|
||||
<Label className="cursor-pointer text-sm">
|
||||
{page.name}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entity Permissions */}
|
||||
<div>
|
||||
<h4 className="text-md font-semibold text-slate-700 mb-3">
|
||||
הרשאות CRUD על ישויות
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{availableEntities.map((entity) => {
|
||||
const entityCrud = entityPermissions[entity.id] || [];
|
||||
return (
|
||||
<Card key={entity.id} className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Label className="font-medium text-slate-700">
|
||||
{entity.name}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{crudOptions.map((crud) => {
|
||||
const hasCrud = entityCrud.includes(crud.id);
|
||||
return (
|
||||
<div
|
||||
key={crud.id}
|
||||
onClick={() => handleEntityCrudToggle(position, entity.id, crud.id)}
|
||||
className={`p-2 rounded-lg border-2 cursor-pointer transition-all text-center ${
|
||||
hasCrud
|
||||
? 'bg-green-50 border-green-300'
|
||||
: 'bg-white border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-lg">{crud.icon}</span>
|
||||
<span className="text-xs font-medium">
|
||||
{crud.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{positions.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
<Shield className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-600 mb-2">
|
||||
אין תפקידים עדיין
|
||||
</h3>
|
||||
<p className="text-slate-400">
|
||||
צור תפקידים בעמוד ניהול התפקידים כדי להתחיל להגדיר הרשאות
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
225
Migdalor-main/src/pages/ManagePositions.jsx
Normal file
225
Migdalor-main/src/pages/ManagePositions.jsx
Normal file
@ -0,0 +1,225 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
} from "@/components/ui/dialog";
|
||||
import { Briefcase, X, Plus, Shield, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function ManagePositions() {
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [newPositionTitle, setNewPositionTitle] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(setCurrentUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
const { data: positions = [], isLoading } = useQuery({
|
||||
queryKey: ['positions'],
|
||||
queryFn: () => base44.entities.Position.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: users = [] } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => base44.entities.User.list(),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const createPositionMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.Position.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['positions'] });
|
||||
setShowModal(false);
|
||||
setNewPositionTitle('');
|
||||
toast.success('תפקיד נוסף בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deletePositionMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.Position.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['positions'] });
|
||||
toast.success('תפקיד נמחק');
|
||||
}
|
||||
});
|
||||
|
||||
const handleAddPosition = () => {
|
||||
if (newPositionTitle && newPositionTitle.trim()) {
|
||||
createPositionMutation.mutate({
|
||||
title: newPositionTitle.trim(),
|
||||
order: positions.length
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Count users per position
|
||||
const getUserCountForPosition = (positionTitle) => {
|
||||
return users.filter(user =>
|
||||
user.positions && user.positions.includes(positionTitle)
|
||||
).length;
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Card className="p-8 text-center">
|
||||
<Shield className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold text-slate-700 mb-2">גישה מוגבלת</h2>
|
||||
<p className="text-slate-500">רק מנהלים יכולים לגשת לדף זה</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול תפקידים 💼
|
||||
</h1>
|
||||
<p className="text-slate-500">הוסף, ערוך, ומחק תפקידים במערכת</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-6">
|
||||
<Card className="p-4 border-slate-200">
|
||||
<p className="text-sm text-slate-500">סה״כ תפקידים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{positions.length}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add Button */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף תפקיד חדש
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Positions List */}
|
||||
<Card className="p-6 border-slate-200">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-slate-400" />
|
||||
</div>
|
||||
) : positions.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Briefcase className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-400">אין תפקידים. הוסף את התפקיד הראשון</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{positions.map((position) => {
|
||||
const userCount = getUserCountForPosition(position.title);
|
||||
return (
|
||||
<div
|
||||
key={position.id}
|
||||
className="flex items-center justify-between p-4 bg-white rounded-lg border-2 border-slate-200 hover:border-slate-300 transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Briefcase className="w-5 h-5 text-indigo-600" />
|
||||
<span className="text-lg font-medium text-slate-800">
|
||||
{position.title}
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 bg-slate-100 px-3 py-1 rounded-full">
|
||||
{userCount} {userCount === 1 ? 'משתמש' : 'משתמשים'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`למחוק את התפקיד "${position.title}"?`)) {
|
||||
deletePositionMutation.mutate(position.id);
|
||||
}
|
||||
}}
|
||||
className="hover:bg-red-50 rounded-full p-2 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add Position Modal */}
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
הוסף תפקיד חדש
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<Briefcase className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-right">
|
||||
הזן את שם התפקיד החדש
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">שם התפקיד *</Label>
|
||||
<Input
|
||||
value={newPositionTitle}
|
||||
onChange={(e) => setNewPositionTitle(e.target.value)}
|
||||
placeholder="לדוגמה: קה״ד צוותי"
|
||||
className="text-right"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newPositionTitle.trim()) {
|
||||
handleAddPosition();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
setNewPositionTitle('');
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddPosition}
|
||||
disabled={!newPositionTitle.trim()}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
הוסף תפקיד
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
359
Migdalor-main/src/pages/ManageSquads.jsx
Normal file
359
Migdalor-main/src/pages/ManageSquads.jsx
Normal file
@ -0,0 +1,359 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Plus, Users, Trash2, Edit2, Phone, GripVertical } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
export default function ManageSquads() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingSquad, setEditingSquad] = useState(null);
|
||||
const [user, setUser] = useState(null);
|
||||
const [formData, setFormData] = useState({ squad_number: '', platoon_name: '', contact: '', notes: '' });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(setUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const { data: squads = [], isLoading } = useQuery({
|
||||
queryKey: ['squads'],
|
||||
queryFn: async () => {
|
||||
const data = await base44.entities.Squad.list();
|
||||
return data.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.Squad.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['squads'] });
|
||||
setShowModal(false);
|
||||
setFormData({ squad_number: '', platoon_name: '', contact: '', notes: '' });
|
||||
toast.success('צוות נוסף בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.Squad.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['squads'] });
|
||||
setShowModal(false);
|
||||
setEditingSquad(null);
|
||||
setFormData({ squad_number: '', platoon_name: '', contact: '', notes: '' });
|
||||
toast.success('צוות עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.Squad.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['squads'] });
|
||||
toast.success('צוות נמחק בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingSquad) {
|
||||
updateMutation.mutate({ id: editingSquad.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (squad) => {
|
||||
setEditingSquad(squad);
|
||||
setFormData({
|
||||
squad_number: squad.squad_number,
|
||||
platoon_name: squad.platoon_name,
|
||||
contact: squad.contact || '',
|
||||
notes: squad.notes || ''
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowModal(false);
|
||||
setEditingSquad(null);
|
||||
setFormData({ squad_number: '', platoon_name: '', contact: '', notes: '' });
|
||||
};
|
||||
|
||||
const handleDragEnd = async (result) => {
|
||||
if (!result.destination || !isAdmin) return;
|
||||
|
||||
const items = Array.from(squads);
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
// Update order for all items
|
||||
const updates = items.map((item, index) =>
|
||||
base44.entities.Squad.update(item.id, { order: index })
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
queryClient.invalidateQueries({ queryKey: ['squads'] });
|
||||
toast.success('הסדר עודכן');
|
||||
};
|
||||
|
||||
// Get unique platoon names and assign colors
|
||||
const getPlatoonColor = (platoonName) => {
|
||||
const colors = [
|
||||
{ bg: 'bg-blue-100', text: 'text-blue-700', icon: 'bg-blue-200', iconText: 'text-blue-700' },
|
||||
{ bg: 'bg-purple-100', text: 'text-purple-700', icon: 'bg-purple-200', iconText: 'text-purple-700' },
|
||||
{ bg: 'bg-green-100', text: 'text-green-700', icon: 'bg-green-200', iconText: 'text-green-700' },
|
||||
{ bg: 'bg-orange-100', text: 'text-orange-700', icon: 'bg-orange-200', iconText: 'text-orange-700' },
|
||||
{ bg: 'bg-pink-100', text: 'text-pink-700', icon: 'bg-pink-200', iconText: 'text-pink-700' },
|
||||
];
|
||||
|
||||
const uniquePlatoons = [...new Set(squads.map(s => s.platoon_name))].sort();
|
||||
const index = uniquePlatoons.indexOf(platoonName);
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול צוותים 👥
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-8 flex flex-wrap gap-4">
|
||||
<Card className="p-4 border-slate-200">
|
||||
<p className="text-sm text-slate-500">סה״כ צוותים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{squads.length}</p>
|
||||
</Card>
|
||||
{[...new Set(squads.map(s => s.platoon_name))].sort().map(platoon => {
|
||||
const count = squads.filter(s => s.platoon_name === platoon).length;
|
||||
const colors = getPlatoonColor(platoon);
|
||||
return (
|
||||
<Card key={platoon} className={`p-4 border-2 ${colors.bg}`}>
|
||||
<p className={`text-sm font-medium ${colors.text}`}>{platoon}</p>
|
||||
<p className={`text-2xl font-bold ${colors.text}`}>{count}</p>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Add Button */}
|
||||
{isAdmin && (
|
||||
<div className="flex justify-end mb-6">
|
||||
<Button onClick={() => setShowModal(true)} className="bg-teal-600 hover:bg-teal-700">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף צוות חדש
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Squads Table */}
|
||||
<Card className="overflow-hidden border-slate-200">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50">
|
||||
{isAdmin && <TableHead className="w-12"></TableHead>}
|
||||
<TableHead className="text-center">מספר צוות</TableHead>
|
||||
<TableHead className="text-center">פלוגה</TableHead>
|
||||
<TableHead className="text-center">איש קשר</TableHead>
|
||||
<TableHead className="text-center">הערות</TableHead>
|
||||
{isAdmin && <TableHead className="text-center">פעולות</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="squads">
|
||||
{(provided) => (
|
||||
<TableBody {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={isAdmin ? 6 : 4} className="text-center py-8 text-slate-400">
|
||||
טוען...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : squads.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={isAdmin ? 6 : 4} className="text-center py-8 text-slate-400">
|
||||
עדיין לא נוספו צוותים
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
squads.map((squad, index) => (
|
||||
<Draggable key={squad.id} draggableId={squad.id} index={index} isDragDisabled={!isAdmin}>
|
||||
{(provided, snapshot) => (
|
||||
<TableRow
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`hover:bg-slate-50/50 [&_td]:text-center ${snapshot.isDragging ? 'bg-slate-100' : ''}`}
|
||||
>
|
||||
{isAdmin && (
|
||||
<TableCell {...provided.dragHandleProps} className="cursor-grab active:cursor-grabbing">
|
||||
<GripVertical className="w-4 h-4 text-slate-400 mx-auto" />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="font-medium text-center">
|
||||
<div className="flex flex-row-reverse items-center justify-between gap-2">
|
||||
<span className="flex-1 text-center">{squad.squad_number}</span>
|
||||
<div className={`w-8 h-8 rounded-full ${getPlatoonColor(squad.platoon_name).icon} flex items-center justify-center flex-shrink-0`}>
|
||||
<Users className={`w-4 h-4 ${getPlatoonColor(squad.platoon_name).iconText}`} />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className={`inline-block px-3 py-1 rounded-full font-medium ${getPlatoonColor(squad.platoon_name).bg} ${getPlatoonColor(squad.platoon_name).text}`}>
|
||||
{squad.platoon_name}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{squad.contact ? (
|
||||
<a
|
||||
href={`tel:${squad.contact}`}
|
||||
className="flex flex-row-reverse items-center justify-center gap-2 text-slate-600 hover:text-blue-600 transition-colors cursor-pointer"
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
<span dir="ltr">{squad.contact}</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-slate-400">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-500 max-w-xs truncate">
|
||||
{squad.notes || '—'}
|
||||
</TableCell>
|
||||
{isAdmin && (
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(squad)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteMutation.mutate(squad.id)}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</Draggable>
|
||||
))
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</TableBody>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
{editingSquad ? 'ערוך צוות' : 'הוסף צוות חדש'}
|
||||
<div className="p-2 bg-teal-100 rounded-lg">
|
||||
<Users className="w-5 h-5 text-teal-600" />
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-right">
|
||||
{editingSquad ? 'עדכן את פרטי הצוות' : 'הוסף צוות חדש למעקב'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">מספר צוות</Label>
|
||||
<Input
|
||||
placeholder="למשל, צוות 5..."
|
||||
value={formData.squad_number}
|
||||
onChange={(e) => setFormData({ ...formData, squad_number: e.target.value })}
|
||||
className="text-right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">פלוגה</Label>
|
||||
<Input
|
||||
placeholder="למשל, פלוגת יפתח..."
|
||||
value={formData.platoon_name}
|
||||
onChange={(e) => setFormData({ ...formData, platoon_name: e.target.value })}
|
||||
className="text-right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">טלפון איש קשר (אופציונלי)</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="+972.."
|
||||
value={formData.contact}
|
||||
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
|
||||
className="text-right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">הערות (אופציונלי)</Label>
|
||||
<Textarea
|
||||
placeholder="שם קה״ד צוותי"
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
className="h-20 text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button variant="outline" onClick={handleClose} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.squad_number || !formData.platoon_name}
|
||||
className="flex-1 bg-teal-600 hover:bg-teal-700"
|
||||
>
|
||||
{editingSquad ? 'עדכן צוות' : 'הוסף צוות'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
598
Migdalor-main/src/pages/ManageUsers.jsx
Normal file
598
Migdalor-main/src/pages/ManageUsers.jsx
Normal file
@ -0,0 +1,598 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { Users, Edit2, Mail, Shield, User, Briefcase, Trash2, X, Plus, Filter } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function ManageUsers() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [formData, setFormData] = useState({ squad_name: '', platoon_name: '', positions: [], role: 'user', phone_number: '', full_name: '', onboarding_full_name: '' });
|
||||
const [newPosition, setNewPosition] = useState('');
|
||||
const [filters, setFilters] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
role: '',
|
||||
squad: '',
|
||||
platoon: '',
|
||||
position: '',
|
||||
noPermissions: false
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(setCurrentUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
const { data: users = [], isLoading } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
const allUsers = await base44.entities.User.list();
|
||||
|
||||
// Auto-assign 'צוער' to users without positions
|
||||
const usersWithoutPositions = allUsers.filter(u => !u.positions || u.positions.length === 0);
|
||||
|
||||
for (const user of usersWithoutPositions) {
|
||||
await base44.entities.User.update(user.id, { positions: ['צוער'] });
|
||||
}
|
||||
|
||||
// Fetch updated list if we made changes
|
||||
if (usersWithoutPositions.length > 0) {
|
||||
return await base44.entities.User.list();
|
||||
}
|
||||
|
||||
return allUsers;
|
||||
},
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: positions = [] } = useQuery({
|
||||
queryKey: ['positions'],
|
||||
queryFn: () => base44.entities.Position.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const { data: squads = [] } = useQuery({
|
||||
queryKey: ['squads'],
|
||||
queryFn: () => base44.entities.Squad.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const updateUserMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.User.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setShowModal(false);
|
||||
setEditingUser(null);
|
||||
setFormData({ squad_name: '', platoon_name: '', positions: [], role: 'user', phone_number: '', full_name: '', onboarding_full_name: '' });
|
||||
setNewPosition('');
|
||||
toast.success('פרטי משתמש עודכנו בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.User.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
toast.success('משתמש נמחק בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingUser) {
|
||||
updateUserMutation.mutate({ id: editingUser.id, data: formData });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (user) => {
|
||||
setEditingUser(user);
|
||||
const userPositions = user.positions || (user.position ? [user.position] : []);
|
||||
// Add 'צוער' if user has no positions
|
||||
if (userPositions.length === 0) {
|
||||
userPositions.push('צוער');
|
||||
}
|
||||
setFormData({
|
||||
squad_name: user.squad_name || '',
|
||||
platoon_name: user.platoon_name || '',
|
||||
positions: userPositions,
|
||||
role: user.role || 'user',
|
||||
phone_number: user.phone_number || '',
|
||||
full_name: user.full_name || '',
|
||||
onboarding_full_name: user.onboarding_full_name || user.full_name || ''
|
||||
});
|
||||
setNewPosition('');
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowModal(false);
|
||||
setEditingUser(null);
|
||||
setFormData({ squad_name: '', platoon_name: '', positions: [], role: 'user', phone_number: '' });
|
||||
setNewPosition('');
|
||||
};
|
||||
|
||||
const handleAddPosition = () => {
|
||||
if (newPosition && !formData.positions.includes(newPosition)) {
|
||||
setFormData({ ...formData, positions: [...formData.positions, newPosition] });
|
||||
setNewPosition('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePosition = (positionToRemove) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
positions: formData.positions.filter(p => p !== positionToRemove)
|
||||
});
|
||||
};
|
||||
|
||||
// Predefined platoon names
|
||||
const platoonNames = [
|
||||
'פלוגה א - סהר',
|
||||
'פלוגה ב - יפתח',
|
||||
'פלוגה ג - אייל',
|
||||
'פלוגה ד - אסף',
|
||||
'פלוגה ה - איתן'
|
||||
];
|
||||
|
||||
const positionTitles = positions.map(p => p.title);
|
||||
const normalize = (str = '') =>
|
||||
str
|
||||
.toString()
|
||||
.trim()
|
||||
.normalize('NFKC');
|
||||
// Filter users
|
||||
const filteredUsers = users.filter(user => {
|
||||
const nameValue = user.onboarding_full_name || user.full_name || '';
|
||||
|
||||
const matchName =
|
||||
!filters.name ||
|
||||
normalize(nameValue).includes(normalize(filters.name));
|
||||
|
||||
|
||||
const matchEmail = !filters.email ||
|
||||
(user.email && user.email.toLowerCase().includes(filters.email.toLowerCase()));
|
||||
|
||||
const matchRole = !filters.role || user.role === filters.role;
|
||||
|
||||
const matchSquad = !filters.squad || user.squad_name === filters.squad;
|
||||
|
||||
const matchPlatoon = !filters.platoon || user.platoon_name === filters.platoon;
|
||||
|
||||
const matchPosition = !filters.position ||
|
||||
(user.positions && user.positions.includes(filters.position));
|
||||
|
||||
const matchNoPermissions = !filters.noPermissions ||
|
||||
(!user.positions || user.positions.length === 0);
|
||||
|
||||
return matchName && matchEmail && matchRole && matchSquad && matchPlatoon && matchPosition && matchNoPermissions;
|
||||
});
|
||||
|
||||
// Group users by platoon
|
||||
const usersByPlatoon = users.reduce((acc, user) => {
|
||||
const platoon = user.platoon_name || 'ללא פלוגה';
|
||||
if (!acc[platoon]) acc[platoon] = [];
|
||||
acc[platoon].push(user);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Card className="p-8 text-center">
|
||||
<Shield className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold text-slate-700 mb-2">גישה מוגבלת</h2>
|
||||
<p className="text-slate-500">רק מנהלים יכולים לגשת לדף זה</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול משתמשים 👤
|
||||
</h1>
|
||||
<p className="text-slate-500">עדכן פרטים של משתמשים - פלוגה ותפקיד</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-8 flex flex-wrap gap-4">
|
||||
<Card
|
||||
className={`p-4 border-2 cursor-pointer transition-all ${!filters.platoon && !filters.noPermissions ? 'border-blue-400 bg-blue-50' : 'border-slate-200 hover:border-blue-300'}`}
|
||||
onClick={() => setFilters({ name: '', email: '', role: '', squad: '', platoon: '', position: '', noPermissions: false })}
|
||||
>
|
||||
<p className="text-sm text-slate-500">סה״כ משתמשים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{users.length}</p>
|
||||
</Card>
|
||||
<Card
|
||||
className={`p-4 border-2 cursor-pointer transition-all ${filters.noPermissions ? 'border-amber-400 bg-amber-50' : 'border-slate-200 hover:border-amber-300'}`}
|
||||
onClick={() => setFilters({ ...filters, noPermissions: !filters.noPermissions, platoon: '' })}
|
||||
>
|
||||
<p className="text-sm text-slate-500">ללא הרשאות</p>
|
||||
<p className="text-2xl font-bold text-slate-800">
|
||||
{users.filter(u => !u.positions || u.positions.length === 0).length}
|
||||
</p>
|
||||
</Card>
|
||||
{Object.entries(usersByPlatoon).map(([platoon, platoonUsers]) => (
|
||||
<Card
|
||||
key={platoon}
|
||||
className={`p-4 border-2 cursor-pointer transition-all ${filters.platoon === platoon ? 'border-green-400 bg-green-50' : 'border-slate-200 hover:border-green-300'}`}
|
||||
onClick={() => setFilters({ ...filters, platoon: filters.platoon === platoon ? '' : platoon, noPermissions: false })}
|
||||
>
|
||||
<p className="text-sm text-slate-500">{platoon}</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{platoonUsers.length}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<Card className="overflow-hidden border-slate-200">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50">
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>שם מלא</span>
|
||||
<Input
|
||||
placeholder="סנן..."
|
||||
value={filters.name}
|
||||
onChange={(e) => setFilters({ ...filters, name: e.target.value })}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>אימייל</span>
|
||||
<Input
|
||||
placeholder="סנן..."
|
||||
value={filters.email}
|
||||
onChange={(e) => setFilters({ ...filters, email: e.target.value })}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<span>טלפון</span>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>תפקיד במערכת</span>
|
||||
<select
|
||||
value={filters.role}
|
||||
onChange={(e) => setFilters({ ...filters, role: e.target.value })}
|
||||
className="h-8 text-sm px-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="">הכל</option>
|
||||
<option value="admin">מנהל</option>
|
||||
<option value="user">משתמש</option>
|
||||
</select>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>צוות</span>
|
||||
<select
|
||||
value={filters.squad || ''}
|
||||
onChange={(e) => setFilters({ ...filters, squad: e.target.value })}
|
||||
className="h-8 text-sm px-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="">הכל</option>
|
||||
{squads.map((squad) => (
|
||||
<option key={squad.id} value={squad.squad_number}>{squad.squad_number}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>פלוגה</span>
|
||||
<select
|
||||
value={filters.platoon}
|
||||
onChange={(e) => setFilters({ ...filters, platoon: e.target.value })}
|
||||
className="h-8 text-sm px-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="">הכל</option>
|
||||
{platoonNames.map((name) => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>תפקיד</span>
|
||||
<select
|
||||
value={filters.position}
|
||||
onChange={(e) => setFilters({ ...filters, position: e.target.value })}
|
||||
className="h-8 text-sm px-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="">הכל</option>
|
||||
{positionTitles.map((pos) => (
|
||||
<option key={pos} value={pos}>{pos}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center">פעולות</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-slate-400">
|
||||
טוען...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-slate-400">
|
||||
אין משתמשים מתאימים לסינון
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredUsers.map((user) => (
|
||||
<TableRow key={user.id} className="hover:bg-slate-50/50 [&_td]:text-center">
|
||||
<TableCell className="font-medium text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-1">
|
||||
<span className="font-semibold">{user.onboarding_full_name || user.full_name || 'ללא שם'}</span>
|
||||
{user.onboarding_completed && (
|
||||
<span className="text-xs text-green-600">✓ הושלם</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2 text-slate-600">
|
||||
<Mail className="w-4 h-4" />
|
||||
<span className="text-sm">{user.email}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-slate-600">
|
||||
{user.phone_number || <span className="text-slate-400">—</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.role === 'admin' ? (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700">
|
||||
<Shield className="w-3 h-3" />
|
||||
מנהל
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700">
|
||||
משתמש
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-slate-600">
|
||||
{user.squad_name ? (
|
||||
<span className="font-medium">{user.squad_name}</span>
|
||||
) : (
|
||||
<span className="text-slate-400">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-slate-600">
|
||||
{user.platoon_name ? (
|
||||
<span className="font-medium">{user.platoon_name}</span>
|
||||
) : (
|
||||
<span className="text-slate-400">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-slate-600">
|
||||
{(user.positions && user.positions.length > 0) ? (
|
||||
<div className="flex flex-wrap items-center justify-center gap-1">
|
||||
{user.positions.map((pos, idx) => (
|
||||
<span key={idx} className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-700">
|
||||
<Briefcase className="w-3 h-3" />
|
||||
{pos}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : user.position ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Briefcase className="w-4 h-4 text-slate-400" />
|
||||
<span>{user.position}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-400">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(user)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteUserMutation.mutate(user.id)}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
עדכן פרטי משתמש
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<Users className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-right">
|
||||
{editingUser?.full_name} - {editingUser?.email}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">שם מלא</Label>
|
||||
<Input
|
||||
value={formData.onboarding_full_name}
|
||||
onChange={(e) => setFormData({ ...formData, onboarding_full_name: e.target.value, full_name: e.target.value })}
|
||||
className="text-right"
|
||||
placeholder="הזן שם מלא"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">תפקיד במערכת</Label>
|
||||
<select
|
||||
value={formData.role}
|
||||
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="user">משתמש</option>
|
||||
<option value="admin">מנהל</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">צוות</Label>
|
||||
<select
|
||||
value={formData.squad_name}
|
||||
onChange={(e) => {
|
||||
const selectedSquad = squads.find(s => s.squad_number === e.target.value);
|
||||
setFormData({
|
||||
...formData,
|
||||
squad_name: e.target.value,
|
||||
platoon_name: selectedSquad?.platoon_name || ''
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-md text-right"
|
||||
>
|
||||
<option value="">בחר צוות...</option>
|
||||
{squads.map((squad) => (
|
||||
<option key={squad.id} value={squad.squad_number}>
|
||||
{squad.squad_number} {squad.platoon_name ? `(${squad.platoon_name})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">מספר טלפון</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="05X-XXXXXXX"
|
||||
value={formData.phone_number}
|
||||
onChange={(e) => setFormData({ ...formData, phone_number: e.target.value })}
|
||||
className="text-right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block text-base font-semibold">ניהול תפקידים</Label>
|
||||
|
||||
{/* Display current positions */}
|
||||
<div className="p-3 border-2 border-slate-200 rounded-lg bg-white min-h-[60px]">
|
||||
{formData.positions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.positions.map((pos, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 px-3 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium shadow-sm hover:bg-blue-600 transition-colors">
|
||||
<Briefcase className="w-3.5 h-3.5" />
|
||||
<span>{pos}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemovePosition(pos)}
|
||||
className="hover:bg-blue-700 rounded-full p-1 transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-slate-400 text-sm">
|
||||
אין תפקידים מוקצים
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add new position */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<select
|
||||
value={newPosition}
|
||||
onChange={(e) => setNewPosition(e.target.value)}
|
||||
className="flex-1 px-3 py-2.5 border-2 border-slate-300 rounded-lg text-right text-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 transition-all"
|
||||
>
|
||||
<option value="">בחר תפקיד להוספה...</option>
|
||||
{positionTitles
|
||||
.filter(pos => !formData.positions.includes(pos))
|
||||
.map((pos) => (
|
||||
<option key={pos} value={pos}>{pos}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAddPosition}
|
||||
disabled={!newPosition}
|
||||
className="px-4 bg-green-600 hover:bg-green-700 disabled:bg-slate-300"
|
||||
size="default"
|
||||
>
|
||||
<Plus className="w-4 h-4 ml-1" />
|
||||
הוסף
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button variant="outline" onClick={handleClose} className="flex-1">
|
||||
ביטול
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
שמור שינויים
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
278
Migdalor-main/src/pages/ManageZones.jsx
Normal file
278
Migdalor-main/src/pages/ManageZones.jsx
Normal file
@ -0,0 +1,278 @@
|
||||
import React, { useState } from 'react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { MapPin, Plus, Edit2, Trash2, GripVertical, Shield } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { motion } from 'framer-motion';
|
||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
export default function ManageZones() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingZone, setEditingZone] = useState(null);
|
||||
const [formData, setFormData] = useState({ name: '' });
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
base44.auth.me().then(setCurrentUser).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
const { data: zones = [], isLoading } = useQuery({
|
||||
queryKey: ['zones'],
|
||||
queryFn: () => base44.entities.Zone.list('order'),
|
||||
enabled: isAdmin
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data) => base44.entities.Zone.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['zones'] });
|
||||
setShowModal(false);
|
||||
setFormData({ name: '' });
|
||||
toast.success('אזור נוסף בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.Zone.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['zones'] });
|
||||
setShowModal(false);
|
||||
setEditingZone(null);
|
||||
setFormData({ name: '' });
|
||||
toast.success('אזור עודכן בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id) => base44.entities.Zone.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['zones'] });
|
||||
toast.success('אזור נמחק בהצלחה');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('אנא הזן שם אזור');
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingZone) {
|
||||
updateMutation.mutate({ id: editingZone.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate({ ...formData, order: zones.length });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (zone) => {
|
||||
setEditingZone(zone);
|
||||
setFormData({ name: zone.name });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleDragEnd = async (result) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const items = Array.from(zones);
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
// Update order for all items
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
await base44.entities.Zone.update(items[i].id, { order: i });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['zones'] });
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 flex items-center justify-center">
|
||||
<Card className="p-8 text-center">
|
||||
<Shield className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold text-slate-700 mb-2">גישה מוגבלת</h2>
|
||||
<p className="text-slate-500">רק מנהלים יכולים לגשת לדף זה</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100" dir="rtl">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
ניהול אזורים 📍
|
||||
</h1>
|
||||
<p className="text-slate-500">הגדר אזורים פיזיים למפתחות</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowModal(true)} className="bg-indigo-600 hover:bg-indigo-700">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
הוסף אזור
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<Card className="p-4 mb-6">
|
||||
<p className="text-sm text-slate-500">סה״כ אזורים</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{zones.length}</p>
|
||||
</Card>
|
||||
|
||||
{/* Zones Table */}
|
||||
<Card className="overflow-hidden border-slate-200">
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="zones">
|
||||
{(provided) => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50">
|
||||
<TableHead className="text-center w-12"></TableHead>
|
||||
<TableHead className="text-center">שם האזור</TableHead>
|
||||
<TableHead className="text-center">פעולות</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center py-8 text-slate-400">
|
||||
טוען...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : zones.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center py-8 text-slate-400">
|
||||
אין אזורים עדיין
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
zones.map((zone, index) => (
|
||||
<Draggable key={zone.id} draggableId={zone.id} index={index}>
|
||||
{(provided) => (
|
||||
<TableRow
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className="hover:bg-slate-50/50"
|
||||
>
|
||||
<TableCell className="text-center">
|
||||
<div {...provided.dragHandleProps} className="cursor-move">
|
||||
<GripVertical className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-medium">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-slate-400" />
|
||||
{zone.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(zone)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteMutation.mutate(zone.id)}
|
||||
className="text-red-400 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Draggable>
|
||||
))
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Dialog open={showModal} onOpenChange={(open) => {
|
||||
setShowModal(open);
|
||||
if (!open) {
|
||||
setEditingZone(null);
|
||||
setFormData({ name: '' });
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md" dir="rtl">
|
||||
<DialogHeader className="text-right">
|
||||
<DialogTitle className="flex flex-row-reverse items-center gap-2 justify-end">
|
||||
{editingZone ? 'ערוך אזור' : 'הוסף אזור'}
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<MapPin className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-right block">שם האזור *</Label>
|
||||
<Input
|
||||
placeholder="לדוגמה: קומה 1, בניין A..."
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse gap-3">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!formData.name.trim()}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
{editingZone ? 'עדכן' : 'הוסף'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowModal(false)}
|
||||
className="flex-1"
|
||||
>
|
||||
ביטול
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user