1390 lines
52 KiB
TypeScript
1390 lines
52 KiB
TypeScript
"use client"
|
||
|
||
import type React from "react"
|
||
import { useState, useEffect } from "react"
|
||
import { useRouter } from "next/navigation"
|
||
// app/admin/page.tsx
|
||
|
||
// ... (other imports)
|
||
|
||
// Updated import to get all necessary types and constants from types/user.ts
|
||
import {
|
||
type User,
|
||
type UserRole,
|
||
type Field,
|
||
type Department,
|
||
type Team,
|
||
ROLE_NAMES,
|
||
SHELTER_STATUS_NAMES, // You might need this if displaying status names
|
||
DEPARTMENTS, // If you use this array anywhere for dropdowns/validation
|
||
TEAMS, // If you use this array anywhere for dropdowns/validation
|
||
FIELDS, // If you use this array anywhere for dropdowns/validation
|
||
} from "@/types/user"
|
||
|
||
// ... (rest of your component code)
|
||
|
||
import {
|
||
AlertDialog,
|
||
AlertDialogTrigger,
|
||
AlertDialogContent,
|
||
AlertDialogHeader,
|
||
AlertDialogFooter,
|
||
AlertDialogTitle,
|
||
AlertDialogDescription,
|
||
AlertDialogAction,
|
||
AlertDialogCancel,
|
||
} from "@/components/ui/alert-dialog"
|
||
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Checkbox } from "@/components/ui/checkbox"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import {
|
||
ArrowRight,
|
||
RotateCcw,
|
||
Users,
|
||
UserPlus,
|
||
Clock,
|
||
Trash2,
|
||
Eye,
|
||
KeyRound,
|
||
RefreshCw,
|
||
WifiOff,
|
||
Zap,
|
||
BarChart3,
|
||
PieChart,
|
||
UsersIcon,
|
||
Globe,
|
||
Building2,
|
||
UserCog,
|
||
MessageSquare,
|
||
Lock,
|
||
LockOpen,
|
||
ArrowLeft,
|
||
Home,
|
||
} from "lucide-react"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import { UserCategoryModal } from "@/components/user-category-modal"
|
||
import { TeamUserCategoryModal } from "@/components/team-user-category-modal"
|
||
import { StatsPieChart } from "@/components/stats-pie-chart"
|
||
import { SimplePieChart } from "@/components/simple-pie-chart"
|
||
import { useRealTimeUpdates } from "@/hooks/useRealTimeUpdates"
|
||
import { useTeamRealTimeUpdates } from "@/hooks/useTeamRealTimeUpdates"
|
||
import { DepartmentUserCategoryModal } from "@/components/department-user-category-modal"
|
||
import { useDepartmentRealTimeUpdates } from "@/hooks/useDepartmentRealTimeUpdates"
|
||
import { FieldUserCategoryModal } from "@/components/field-user-category-modal"
|
||
import { useFieldRealTimeUpdates } from "@/hooks/useFieldRealTimeUpdates"
|
||
import { ReportOnBehalfModal } from "@/components/report-on-behalf-modal"
|
||
|
||
interface Stats {
|
||
no_report: number
|
||
in_shelter: number
|
||
not_in_shelter: number
|
||
no_alarm: number
|
||
safe_after_exit: number
|
||
}
|
||
|
||
interface UserData {
|
||
national_id: string
|
||
name: string
|
||
in_shelter?: string
|
||
last_updated?: string
|
||
is_admin: boolean
|
||
must_change_password?: boolean
|
||
field?: string
|
||
department?: string
|
||
team?: string
|
||
lock_status?: boolean
|
||
}
|
||
|
||
export default function AdminPage() {
|
||
const [user, setUser] = useState<User | null>(null)
|
||
const [activeTab, setActiveTab] = useState("team")
|
||
|
||
// Global stats and data
|
||
const [globalStats, setGlobalStats] = useState<Stats | null>(null)
|
||
const [globalUsers, setGlobalUsers] = useState<UserData[]>([])
|
||
const [globalLastReset, setGlobalLastReset] = useState<string | null>(null)
|
||
const [globalResetCooldown, setGlobalResetCooldown] = useState(0)
|
||
|
||
// Team stats and data
|
||
const [teamStats, setTeamStats] = useState<Stats | null>(null)
|
||
const [teamUsers, setTeamUsers] = useState<UserData[]>([])
|
||
const [teamName, setTeamName] = useState<string>("")
|
||
const [teamResetCooldown, setTeamResetCooldown] = useState(0)
|
||
|
||
// Department stats and data
|
||
const [departmentStats, setDepartmentStats] = useState<Stats | null>(null)
|
||
const [departmentUsers, setDepartmentUsers] = useState<UserData[]>([])
|
||
const [departmentName, setDepartmentName] = useState<string>("")
|
||
const [departmentResetCooldown, setDepartmentResetCooldown] = useState(0)
|
||
const [departmentChangedRows, setDepartmentChangedRows] = useState<Set<string>>(new Set())
|
||
const [departmentModalOpen, setDepartmentModalOpen] = useState(false)
|
||
|
||
// Field stats and data
|
||
const [fieldStats, setFieldStats] = useState<Stats | null>(null)
|
||
const [fieldUsers, setFieldUsers] = useState<UserData[]>([])
|
||
const [fieldName, setFieldName] = useState<string>("")
|
||
const [fieldResetCooldown, setFieldResetCooldown] = useState(0)
|
||
const [fieldChangedRows, setFieldChangedRows] = useState<Set<string>>(new Set())
|
||
const [fieldModalOpen, setFieldModalOpen] = useState(false)
|
||
|
||
const [newUser, setNewUser] = useState({
|
||
name: "",
|
||
isAdmin: false,
|
||
field: "",
|
||
department: "",
|
||
team: "",
|
||
role: "",
|
||
})
|
||
const [message, setMessage] = useState("")
|
||
const [loadingUsers, setLoadingUsers] = useState(false)
|
||
const [modalOpen, setModalOpen] = useState(false)
|
||
const [teamModalOpen, setTeamModalOpen] = useState(false)
|
||
const [selectedCategory, setSelectedCategory] = useState("")
|
||
const [selectedCategoryName, setSelectedCategoryName] = useState("")
|
||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||
const [changedRows, setChangedRows] = useState<Set<string>>(new Set())
|
||
const [teamChangedRows, setTeamChangedRows] = useState<Set<string>>(new Set())
|
||
const [viewMode, setViewMode] = useState<"list" | "pie">("list")
|
||
const [debugMode, setDebugMode] = useState(false)
|
||
const [useSimpleChart, setUseSimpleChart] = useState(false)
|
||
const router = useRouter()
|
||
|
||
const [reportModalOpen, setReportModalOpen] = useState(false)
|
||
const [selectedUserForReport, setSelectedUserForReport] = useState<UserData | null>(null)
|
||
|
||
// Global real-time updates
|
||
const { isConnected: globalConnected, refetch: refetchGlobal } = useRealTimeUpdates((data) => {
|
||
if (data.stats) {
|
||
setGlobalStats(data.stats)
|
||
}
|
||
|
||
if (data.users) {
|
||
const newChangedRows = new Set<string>()
|
||
data.users.forEach((newUser: UserData) => {
|
||
const existingUser = globalUsers.find((u) => u.national_id === newUser.national_id)
|
||
if (
|
||
existingUser &&
|
||
(existingUser.in_shelter !== newUser.in_shelter || existingUser.last_updated !== newUser.last_updated)
|
||
) {
|
||
newChangedRows.add(newUser.national_id)
|
||
}
|
||
})
|
||
|
||
setGlobalUsers(data.users)
|
||
setChangedRows(newChangedRows)
|
||
|
||
if (newChangedRows.size > 0) {
|
||
setTimeout(() => setChangedRows(new Set()), 3000)
|
||
}
|
||
}
|
||
|
||
if (data.lastReset?.lastReset) {
|
||
setGlobalLastReset(data.lastReset.lastReset)
|
||
if (data.lastReset.timestamp) {
|
||
const resetTime = new Date(data.lastReset.timestamp).getTime()
|
||
const now = new Date().getTime()
|
||
const cooldownMs = 2 * 60 * 1000 // 2 minutes
|
||
const remaining = Math.max(0, cooldownMs - (now - resetTime))
|
||
setGlobalResetCooldown(Math.ceil(remaining / 1000))
|
||
}
|
||
}
|
||
})
|
||
|
||
// Team real-time updates
|
||
const { isConnected: teamConnected, refetch: refetchTeam } = useTeamRealTimeUpdates(
|
||
user?.national_id || "",
|
||
(data) => {
|
||
if (data.stats) {
|
||
setTeamStats(data.stats)
|
||
}
|
||
|
||
if (data.users) {
|
||
const newChangedRows = new Set<string>()
|
||
data.users.forEach((newUser: UserData) => {
|
||
const existingUser = teamUsers.find((u) => u.national_id === newUser.national_id)
|
||
if (
|
||
existingUser &&
|
||
(existingUser.in_shelter !== newUser.in_shelter || existingUser.last_updated !== newUser.last_updated)
|
||
) {
|
||
newChangedRows.add(newUser.national_id)
|
||
}
|
||
})
|
||
|
||
setTeamUsers(data.users)
|
||
setTeamChangedRows(newChangedRows)
|
||
|
||
if (newChangedRows.size > 0) {
|
||
setTimeout(() => setTeamChangedRows(new Set()), 3000)
|
||
}
|
||
}
|
||
|
||
if (data.team) {
|
||
setTeamName(data.team)
|
||
}
|
||
},
|
||
)
|
||
|
||
// Department real-time updates
|
||
const { isConnected: departmentConnected, refetch: refetchDepartment } = useDepartmentRealTimeUpdates(
|
||
user?.national_id || "",
|
||
(data) => {
|
||
if (data.stats) {
|
||
setDepartmentStats(data.stats)
|
||
}
|
||
|
||
if (data.users) {
|
||
const newChangedRows = new Set<string>()
|
||
data.users.forEach((newUser: UserData) => {
|
||
const existingUser = departmentUsers.find((u) => u.national_id === newUser.national_id)
|
||
if (
|
||
existingUser &&
|
||
(existingUser.in_shelter !== newUser.in_shelter || existingUser.last_updated !== newUser.last_updated)
|
||
) {
|
||
newChangedRows.add(newUser.national_id)
|
||
}
|
||
})
|
||
|
||
setDepartmentUsers(data.users)
|
||
setDepartmentChangedRows(newChangedRows)
|
||
|
||
if (newChangedRows.size > 0) {
|
||
setTimeout(() => setDepartmentChangedRows(new Set()), 3000)
|
||
}
|
||
}
|
||
|
||
if (data.department) {
|
||
setDepartmentName(data.department)
|
||
}
|
||
},
|
||
)
|
||
|
||
// Field real-time updates
|
||
const { isConnected: fieldConnected, refetch: refetchField } = useFieldRealTimeUpdates(
|
||
user?.national_id || "",
|
||
(data) => {
|
||
if (data.stats) {
|
||
setFieldStats(data.stats)
|
||
}
|
||
|
||
if (data.users) {
|
||
const newChangedRows = new Set<string>()
|
||
data.users.forEach((newUser: UserData) => {
|
||
const existingUser = fieldUsers.find((u) => u.national_id === newUser.national_id)
|
||
if (
|
||
existingUser &&
|
||
(existingUser.in_shelter !== newUser.in_shelter || existingUser.last_updated !== newUser.last_updated)
|
||
) {
|
||
newChangedRows.add(newUser.national_id)
|
||
}
|
||
})
|
||
|
||
setFieldUsers(data.users)
|
||
setFieldChangedRows(newChangedRows)
|
||
|
||
if (newChangedRows.size > 0) {
|
||
setTimeout(() => setFieldChangedRows(new Set()), 3000)
|
||
}
|
||
}
|
||
|
||
if (data.field) {
|
||
setFieldName(data.field)
|
||
}
|
||
},
|
||
)
|
||
|
||
useEffect(() => {
|
||
const userData = localStorage.getItem("user")
|
||
if (!userData) {
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
const parsedUser = JSON.parse(userData)
|
||
if (!["global_admin", "field_admin", "department_admin", "team_admin"].includes(parsedUser.role)) {
|
||
router.push("/dashboard")
|
||
return
|
||
}
|
||
|
||
|
||
setUser(parsedUser)
|
||
}, [router])
|
||
|
||
// Add this right after the existing useEffect that sets the user
|
||
useEffect(() => {
|
||
// Force initial fetch when user changes
|
||
if (user?.national_id) {
|
||
refetchGlobal()
|
||
}
|
||
}, [user?.national_id, refetchGlobal])
|
||
useEffect(() => {
|
||
if (globalResetCooldown > 0) {
|
||
const timer = setTimeout(() => setGlobalResetCooldown(globalResetCooldown - 1), 1000)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [globalResetCooldown])
|
||
|
||
useEffect(() => {
|
||
if (teamResetCooldown > 0) {
|
||
const timer = setTimeout(() => setTeamResetCooldown(teamResetCooldown - 1), 1000)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [teamResetCooldown])
|
||
|
||
useEffect(() => {
|
||
if (departmentResetCooldown > 0) {
|
||
const timer = setTimeout(() => setDepartmentResetCooldown(departmentResetCooldown - 1), 1000)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [departmentResetCooldown])
|
||
|
||
useEffect(() => {
|
||
if (fieldResetCooldown > 0) {
|
||
const timer = setTimeout(() => setFieldResetCooldown(fieldResetCooldown - 1), 1000)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [fieldResetCooldown])
|
||
|
||
const handleGlobalResetAll = async () => {
|
||
if (globalResetCooldown > 0) return
|
||
|
||
try {
|
||
const response = await fetch("/api/admin/reset-all", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ adminId: user?.national_id }),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setMessage(data.message || "כל הסטטוסים אופסו בהצלחה")
|
||
setGlobalResetCooldown(120) // 2 minutes
|
||
setGlobalLastReset(`${user?.name} - ${new Date().toLocaleString("he-IL")}`)
|
||
refetchGlobal()
|
||
refetchTeam() // Also refresh team data
|
||
refetchDepartment()
|
||
refetchField()
|
||
} else {
|
||
// Handle cooldown error specifically
|
||
if (response.status === 429 && data.remainingSeconds) {
|
||
setGlobalResetCooldown(data.remainingSeconds)
|
||
setMessage(`יש להמתין ${data.remainingSeconds} שניות לפני איפוס נוסף`)
|
||
} else {
|
||
setMessage(data.error || "שגיאה באיפוס הסטטוסים")
|
||
}
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה באיפוס הסטטוסים")
|
||
}
|
||
}
|
||
|
||
const handleTeamReset = async () => {
|
||
if (teamResetCooldown > 0) return
|
||
|
||
try {
|
||
const response = await fetch("/api/admin/team-reset", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ adminId: user?.national_id }),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setMessage(data.message || `כל הסטטוסים של צוות ${data.team} אופסו בהצלחה`)
|
||
setTeamResetCooldown(60) // 1 minute
|
||
refetchTeam()
|
||
refetchGlobal() // Also refresh global data
|
||
refetchDepartment()
|
||
refetchField()
|
||
} else {
|
||
if (response.status === 429 && data.remainingSeconds) {
|
||
setTeamResetCooldown(data.remainingSeconds)
|
||
setMessage(`יש להמתין ${data.remainingSeconds} שניות לפני איפוס צוות נוסף`)
|
||
} else {
|
||
setMessage(data.error || "שגיאה באיפוס הצוות")
|
||
}
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה באיפוס הצוות")
|
||
}
|
||
}
|
||
|
||
const handleDepartmentReset = async () => {
|
||
if (departmentResetCooldown > 0) return
|
||
|
||
try {
|
||
const response = await fetch("/api/admin/department-reset", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ adminId: user?.national_id }),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setMessage(data.message || `כל הסטטוסים של מסגרת ${data.department} אופסו בהצלחה`)
|
||
setDepartmentResetCooldown(90) // 1.5 minutes
|
||
refetchDepartment()
|
||
refetchGlobal() // Also refresh global data
|
||
refetchTeam()
|
||
refetchField()
|
||
} else {
|
||
if (response.status === 429 && data.remainingSeconds) {
|
||
setDepartmentResetCooldown(data.remainingSeconds)
|
||
setMessage(`יש להמתין ${data.remainingSeconds} שניות לפני איפוס מסגרת נוסף`)
|
||
} else {
|
||
setMessage(data.error || "שגיאה באיפוס המסגרת")
|
||
}
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה באיפוס המסגרת")
|
||
}
|
||
}
|
||
|
||
const handleFieldReset = async () => {
|
||
if (fieldResetCooldown > 0) return
|
||
|
||
try {
|
||
const response = await fetch("/api/admin/field-reset", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ adminId: user?.national_id }),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setMessage(data.message || `כל הסטטוסים של תחום ${data.field} אופסו בהצלחה`)
|
||
setFieldResetCooldown(120) // 2 minutes
|
||
refetchField()
|
||
refetchGlobal() // Also refresh global data
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
} else {
|
||
if (response.status === 429 && data.remainingSeconds) {
|
||
setFieldResetCooldown(data.remainingSeconds)
|
||
setMessage(`יש להמתין ${data.remainingSeconds} שניות לפני איפוס תחום נוסף`)
|
||
} else {
|
||
setMessage(data.error || "שגיאה באיפוס התחום")
|
||
}
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה באיפוס התחום")
|
||
}
|
||
}
|
||
|
||
const handleAddUser = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
|
||
if (!newUser.field || !newUser.department || !newUser.team || !newUser.role) {
|
||
setMessage("יש לבחור תפקיד, תחום, מסגרת וצוות")
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await fetch("/api/admin/add-user", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
...newUser,
|
||
adminId: user?.national_id,
|
||
}),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setMessage(`${data.message}. הסיסמה הזמנית: password123`)
|
||
setNewUser({ name: "", isAdmin: false, field: "", department: "", team: "", role: "" })
|
||
refetchGlobal()
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
refetchField()
|
||
} else {
|
||
setMessage(data.error || "שגיאה בהוספת משתמש")
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה בחיבור לשרת")
|
||
}
|
||
}
|
||
|
||
const handleDeleteUser = async (nationalId: string) => {
|
||
try {
|
||
const response = await fetch(`/api/admin/users/${nationalId}`, {
|
||
method: "DELETE",
|
||
})
|
||
|
||
if (response.ok) {
|
||
setMessage("משתמש נמחק בהצלחה")
|
||
refetchGlobal()
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
refetchField()
|
||
} else {
|
||
const data = await response.json()
|
||
setMessage(data.error || "שגיאה במחיקת משתמש")
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה בחיבור לשרת")
|
||
}
|
||
}
|
||
|
||
const handleResetPassword = async (nationalId: string, userName: string) => {
|
||
try {
|
||
const response = await fetch("/api/admin/reset-password", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
adminId: user?.national_id,
|
||
targetUserId: nationalId,
|
||
}),
|
||
})
|
||
|
||
if (response.ok) {
|
||
setMessage(`סיסמה אופסה בהצלחה עבור ${userName}. הסיסמה החדשה: password123`)
|
||
refetchGlobal()
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
refetchField()
|
||
} else {
|
||
const data = await response.json()
|
||
setMessage(data.error || "שגיאה באיפוס סיסמה")
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה בחיבור לשרת")
|
||
}
|
||
}
|
||
|
||
const handleToggleUserLock = async (nationalId: string, currentLockStatus: boolean, userName: string) => {
|
||
try {
|
||
const response = await fetch("/api/admin/toggle-user-lock", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
adminId: user?.national_id,
|
||
targetUserId: nationalId,
|
||
lockStatus: !currentLockStatus,
|
||
}),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (response.ok) {
|
||
setMessage(data.message)
|
||
|
||
// Immediately update the local state to reflect the change
|
||
const updateUserLockStatus = (users: UserData[]) =>
|
||
users.map((u) => (u.national_id === nationalId ? { ...u, lock_status: !currentLockStatus } : u))
|
||
|
||
setGlobalUsers((prev) => updateUserLockStatus(prev))
|
||
setTeamUsers((prev) => updateUserLockStatus(prev))
|
||
setDepartmentUsers((prev) => updateUserLockStatus(prev))
|
||
setFieldUsers((prev) => updateUserLockStatus(prev))
|
||
|
||
// Also refresh from server to ensure consistency
|
||
setTimeout(() => {
|
||
refetchGlobal()
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
refetchField()
|
||
}, 100)
|
||
} else {
|
||
setMessage(data.error || "שגיאה בשינוי סטטוס נעילה")
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה בחיבור לשרת")
|
||
}
|
||
}
|
||
|
||
const getStatusText = (status?: string) => {
|
||
switch (status) {
|
||
case "yes":
|
||
return { text: "במקלט/חדר מוגן", color: "text-green-600" }
|
||
case "no":
|
||
return { text: "לא במקלט", color: "text-orange-600" }
|
||
case "no_alarm":
|
||
return { text: "אין אזעקה", color: "text-blue-600" }
|
||
case "safe_after_exit":
|
||
return { text: "אני בטוח.ה (סוף אירוע)", color: "text-emerald-600" }
|
||
default:
|
||
return { text: "דיווח חסר", color: "text-gray-500" }
|
||
}
|
||
}
|
||
|
||
const handleGlobalCategoryClick = (category: string, categoryName: string) => {
|
||
setSelectedCategory(category)
|
||
setSelectedCategoryName(categoryName)
|
||
setModalOpen(true)
|
||
}
|
||
|
||
const handleTeamCategoryClick = (category: string, categoryName: string) => {
|
||
setSelectedCategory(category)
|
||
setSelectedCategoryName(categoryName)
|
||
setTeamModalOpen(true)
|
||
}
|
||
|
||
const handleDepartmentCategoryClick = (category: string, categoryName: string) => {
|
||
setSelectedCategory(category)
|
||
setSelectedCategoryName(categoryName)
|
||
setDepartmentModalOpen(true)
|
||
}
|
||
|
||
const handleFieldCategoryClick = (category: string, categoryName: string) => {
|
||
setSelectedCategory(category)
|
||
setSelectedCategoryName(categoryName)
|
||
setFieldModalOpen(true)
|
||
}
|
||
|
||
const handleManualRefresh = () => {
|
||
setIsRefreshing(true)
|
||
refetchGlobal()
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
refetchField()
|
||
setTimeout(() => setIsRefreshing(false), 500)
|
||
}
|
||
|
||
const handleReportOnBehalf = async (userId: string, status: string) => {
|
||
try {
|
||
const response = await fetch("/api/admin/report-on-behalf", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
adminId: user?.national_id,
|
||
targetUserId: userId,
|
||
status,
|
||
}),
|
||
})
|
||
|
||
const data = await response.json()
|
||
if (response.ok) {
|
||
setMessage(data.message)
|
||
refetchGlobal()
|
||
refetchTeam()
|
||
refetchDepartment()
|
||
refetchField()
|
||
} else {
|
||
setMessage(data.error || "שגיאה בדיווח")
|
||
}
|
||
} catch (err) {
|
||
setMessage("שגיאה בחיבור לשרת")
|
||
}
|
||
}
|
||
|
||
const formatCooldownTime = (seconds: number) => {
|
||
if (seconds <= 0) return ""
|
||
|
||
const minutes = Math.floor(seconds / 60)
|
||
const remainingSeconds = seconds % 60
|
||
|
||
if (minutes > 0) {
|
||
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
|
||
}
|
||
return `${remainingSeconds} שניות`
|
||
}
|
||
|
||
const renderStatsSection = (
|
||
stats: Stats | null,
|
||
onCategoryClick: (category: string, categoryName: string) => void,
|
||
isTeam = false,
|
||
customName?: string,
|
||
) => {
|
||
const displayName = customName || (isTeam ? `צוות ${teamName}` : "כלליות")
|
||
|
||
return (
|
||
<Card dir="rtl">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-5 w-5" />
|
||
{isTeam
|
||
? `צוות ${teamName}`
|
||
: customName
|
||
? `מסגרת ${customName}`
|
||
: "כללי"}{" "}
|
||
{isRefreshing && <RefreshCw className="h-4 w-4 animate-spin text-blue-500" />}
|
||
{(isTeam
|
||
? teamConnected
|
||
: departmentConnected
|
||
? departmentConnected
|
||
: fieldConnected
|
||
? fieldConnected
|
||
: globalConnected) && <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>}
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant={viewMode === "list" ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setViewMode("list")}
|
||
>
|
||
<BarChart3 className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant={viewMode === "pie" ? "default" : "outline"} size="sm" onClick={() => setViewMode("pie")}>
|
||
<PieChart className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="text-center" dir="rtl">
|
||
{stats ? (
|
||
<>
|
||
{viewMode === "list" ? (
|
||
<div className="space-y-2 text-center" dir="rtl">
|
||
<div
|
||
className="flex justify-between p-2 rounded hover:bg-gray-100 cursor-pointer transition-colors"
|
||
onClick={() => onCategoryClick("no_report", "לא דיווחו")}
|
||
>
|
||
<span>לא דיווחו:</span>
|
||
<span className="font-semibold text-red-600">{stats.no_report}</span>
|
||
</div>
|
||
<div
|
||
className="flex justify-between p-2 rounded hover:bg-gray-100 cursor-pointer transition-colors"
|
||
onClick={() => onCategoryClick("in_shelter", "במקלט/חדר מוגן")}
|
||
>
|
||
<span>במקלט:</span>
|
||
<span className="font-semibold text-green-600">{stats.in_shelter}</span>
|
||
</div>
|
||
<div
|
||
className="flex justify-between p-2 rounded hover:bg-gray-100 cursor-pointer transition-colors"
|
||
onClick={() => onCategoryClick("not_in_shelter", "לא במקלט - אין מקלט בקרבת מקום")}
|
||
>
|
||
<span>לא במקלט:</span>
|
||
<span className="font-semibold text-orange-600">{stats.not_in_shelter}</span>
|
||
</div>
|
||
<div
|
||
className="flex justify-between p-2 rounded hover:bg-gray-100 cursor-pointer transition-colors"
|
||
onClick={() => onCategoryClick("no_alarm", "אין אזעקה באזור")}
|
||
>
|
||
<span>אין אזעקה:</span>
|
||
<span className="font-semibold text-blue-600">{stats.no_alarm}</span>
|
||
</div>
|
||
<div
|
||
className="flex justify-between p-2 rounded hover:bg-gray-100 cursor-point transition-colors"
|
||
onClick={() => onCategoryClick("safe_after_exit", "אני בטוח.ה (סוף אירוע)")}
|
||
>
|
||
<span>אני בטוח.ה (סוף אירוע)</span>
|
||
<span className="font-semibold text-emerald-600">{stats.safe_after_exit}</span>
|
||
</div>
|
||
</div>
|
||
) : useSimpleChart ? (
|
||
<SimplePieChart stats={stats} onCategoryClick={onCategoryClick} />
|
||
) : (
|
||
<StatsPieChart stats={stats} onCategoryClick={onCategoryClick} />
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="text-center py-8 text-gray-500">טוען סטטיסטיקות...</div>
|
||
)}
|
||
<div className="mt-4 text-xs text-gray-500 text-center">
|
||
{(
|
||
isTeam
|
||
? teamConnected
|
||
: departmentConnected
|
||
? departmentConnected
|
||
: fieldConnected
|
||
? fieldConnected
|
||
: globalConnected
|
||
) ? (
|
||
<span className="text-green-600 font-medium"></span>
|
||
) : (
|
||
"מנסה להתחבר לעדכונים..."
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
const renderUsersTable = (users: UserData[], changedRows: Set<string>, isReadOnly = false) => {
|
||
return (
|
||
<div className="overflow-x-auto" dir="rtl">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="text-right">שם</TableHead>
|
||
<TableHead className="text-right">דיווח</TableHead>
|
||
{!isReadOnly && <TableHead className="text-right">פעולות</TableHead>}
|
||
<TableHead className="text-right">תחום</TableHead>
|
||
<TableHead className="text-right">מסגרת</TableHead>
|
||
<TableHead className="text-right">צוות</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{users.map((userData) => {
|
||
const status = getStatusText(userData.in_shelter)
|
||
const isChanged = changedRows.has(userData.national_id)
|
||
const isLocked = userData.lock_status || false
|
||
return (
|
||
<TableRow
|
||
key={userData.national_id}
|
||
className={isChanged ? "bg-green-50 border-green-200 animate-pulse" : ""}
|
||
>
|
||
<TableCell className="font-medium">
|
||
{userData.name}
|
||
{isChanged && <span className="ml-2 text-green-600">🔄</span>}
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className={status.color}>{status.text}</span>
|
||
</TableCell>
|
||
{!isReadOnly && (
|
||
<TableCell>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedUserForReport(userData)
|
||
setReportModalOpen(true)
|
||
}}
|
||
className="text-green-600 hover:text-green-700"
|
||
>
|
||
<MessageSquare className="h-4 w-4" />
|
||
</Button>
|
||
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleToggleUserLock(userData.national_id, isLocked, userData.name)}
|
||
className={
|
||
isLocked
|
||
? "text-yellow-600 hover:text-yellow-700 bg-yellow-50 border-yellow-200"
|
||
: "text-gray-600 hover:text-gray-700 bg-gray-50 border-gray-200"
|
||
}
|
||
title={isLocked ? "לחץ לביטול נעילה" : "לחץ לנעילה"}
|
||
>
|
||
{isLocked ? <Lock className="h-4 w-4" /> : <LockOpen className="h-4 w-4" />}
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
)}
|
||
<TableCell>
|
||
<span className="text-sm bg-green-100 text-green-800 px-2 py-1 rounded">
|
||
{userData.field || "לא הוגדר"}
|
||
</span>
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||
{userData.department || "לא הוגדר"}
|
||
</span>
|
||
</TableCell>
|
||
<TableCell>
|
||
<span className="text-sm bg-purple-100 text-purple-800 px-2 py-1 rounded">
|
||
{userData.team || "לא הוגדר"}
|
||
</span>
|
||
</TableCell>
|
||
|
||
|
||
|
||
</TableRow>
|
||
)
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
{users.length === 0 && <div className="text-center py-8 text-gray-500">אין משתמשים</div>}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!user) return null
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 p-4" dir="rtl">
|
||
<div className="max-w-6xl mx-auto space-y-6" dir="rtl">
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex justify-between items-center">
|
||
<CardTitle className="text-xl">ניהול</CardTitle>
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex items-center gap-2 text-sm text-gray-600 text-center">
|
||
{globalConnected || teamConnected || departmentConnected || fieldConnected ? (
|
||
<>
|
||
<Zap className="h-4 w-4 text-green-500" />
|
||
<span className="text-green-600">מקוון</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<WifiOff className="h-4 w-4 text-red-500" />
|
||
<span className="text-red-600">מתחבר...</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleManualRefresh}
|
||
disabled={isRefreshing}
|
||
className="flex items-center gap-1"
|
||
>
|
||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||
</Button>
|
||
{user?.role !== "user" && (
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => router.push("/role-admin")}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<UserCog className="h-4 w-4" />
|
||
</Button>
|
||
)}
|
||
<Button variant="outline" onClick={() => router.push("/dashboard")} className="flex items-center gap-2">
|
||
<Home className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
</Card>
|
||
|
||
{message && (
|
||
<Alert>
|
||
<AlertDescription>{message}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{debugMode && (
|
||
<Alert>
|
||
<AlertDescription>
|
||
<strong>Debug Info:</strong>
|
||
<div className="text-xs mt-2 bg-gray-100 p-2 rounded space-y-1">
|
||
<div>Team: {teamName}</div>
|
||
<div>Global Reset Cooldown: {globalResetCooldown} seconds</div>
|
||
<div>Team Reset Cooldown: {teamResetCooldown} seconds</div>
|
||
<div>Department Reset Cooldown: {departmentResetCooldown} seconds</div>
|
||
<div>Field Reset Cooldown: {fieldResetCooldown} seconds</div>
|
||
</div>
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" dir="rtl">
|
||
<TabsList className="grid w-full grid-cols-4">
|
||
<TabsTrigger value="team" className="flex items-center gap-2">
|
||
<UsersIcon className="h-4 w-4" />
|
||
צוות
|
||
</TabsTrigger>
|
||
<TabsTrigger value="department" className="flex items-center gap-2">
|
||
<Building2 className="h-4 w-4" />
|
||
מסגרת
|
||
</TabsTrigger>
|
||
<TabsTrigger value="field" className="flex items-center gap-2">
|
||
<Globe className="h-4 w-4" />
|
||
תחום
|
||
</TabsTrigger>
|
||
<TabsTrigger value="global" className="flex items-center gap-2">
|
||
<Globe className="h-4 w-4" />
|
||
כללי
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="team" className="space-y-6">
|
||
<div className="grid md:grid-cols-2 gap-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<RotateCcw className="h-5 w-5" />
|
||
איפוס סטטוסי הצוות
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<Button
|
||
onClick={handleTeamReset}
|
||
disabled={teamResetCooldown > 0}
|
||
className="w-full"
|
||
variant={teamResetCooldown > 0 ? "secondary" : "destructive"}
|
||
>
|
||
{teamResetCooldown > 0 ? (
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
המתן {formatCooldownTime(teamResetCooldown)}
|
||
</div>
|
||
) : (
|
||
`אפס את כל הסטטוסים של צוות ${teamName}`
|
||
)}
|
||
</Button>
|
||
<div className="text-xs text-gray-500 bg-yellow-50 p-2 rounded">
|
||
<strong>הערה:</strong> איפוס יאפס רק את המשתמשים מהצוות שלך ({teamName}) שאינם נעולים
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{renderStatsSection(teamStats, handleTeamCategoryClick, true)}
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Eye className="h-5 w-5" />
|
||
ניהול משתמשי צוות {teamName}
|
||
{teamChangedRows.size > 0 && (
|
||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
|
||
{teamChangedRows.size} עדכונים חדשים
|
||
</span>
|
||
)}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loadingUsers ? (
|
||
<div className="text-center py-4">טוען משתמשים...</div>
|
||
) : (
|
||
renderUsersTable(teamUsers, teamChangedRows)
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="department" className="space-y-6">
|
||
<div className="grid md:grid-cols-2 gap-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<RotateCcw className="h-5 w-5" />
|
||
{user?.role === "team_admin" ? "צפייה במסגרת" : "איפוס סטטוסי המסגרת"}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{user?.role === "team_admin" ? (
|
||
<div className="text-center p-4 bg-blue-50 rounded">
|
||
<p className="text-blue-800 font-semibold">צפייה בלבד</p>
|
||
<p className="text-sm text-blue-600">כמנהל צוות, אתה יכול לראות את המסגרת שלך אך לא לאפס אותה</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<Button
|
||
onClick={handleDepartmentReset}
|
||
disabled={departmentResetCooldown > 0}
|
||
className="w-full"
|
||
variant={departmentResetCooldown > 0 ? "secondary" : "destructive"}
|
||
>
|
||
{departmentResetCooldown > 0 ? (
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
המתן {formatCooldownTime(departmentResetCooldown)}
|
||
</div>
|
||
) : (
|
||
`אפס את כל הסטטוסים של מסגרת ${departmentName}`
|
||
)}
|
||
</Button>
|
||
<div className="text-xs text-gray-500 bg-yellow-50 p-2 rounded">
|
||
<strong>הערה:</strong> איפוס יאפס את כל המשתמשים מהמסגרת שלך ({departmentName}) שאינם נעולים
|
||
</div>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{renderStatsSection(departmentStats, handleDepartmentCategoryClick, false, departmentName)}
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Eye className="h-5 w-5" />
|
||
{user?.role === "team_admin" ? "צפייה במשתמשי מסגרת" : "ניהול משתמשי מסגרת"} {departmentName}
|
||
{departmentChangedRows.size > 0 && (
|
||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
|
||
{departmentChangedRows.size} עדכונים חדשים
|
||
</span>
|
||
)}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loadingUsers ? (
|
||
<div className="text-center py-4">טוען משתמשים...</div>
|
||
) : (
|
||
renderUsersTable(departmentUsers, departmentChangedRows, user?.role === "team_admin")
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="field" className="space-y-6">
|
||
<div className="grid md:grid-cols-2 gap-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<RotateCcw className="h-5 w-5" />
|
||
איפוס סטטוסי התחום
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{(user?.role === "department_admin" || user?.role === "team_admin") ? (
|
||
<div className="text-center p-4 bg-blue-50 rounded">
|
||
<p className="text-blue-800 font-semibold">צפייה בלבד</p>
|
||
<p className="text-sm text-blue-600">כ{ROLE_NAMES[user.role]}, אתה יכול לראות את התחום שלך אך לא לאפס אותו</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<Button
|
||
onClick={handleFieldReset}
|
||
disabled={fieldResetCooldown > 0}
|
||
className="w-full"
|
||
variant={fieldResetCooldown > 0 ? "secondary" : "destructive"}
|
||
>
|
||
{fieldResetCooldown > 0 ? (
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
המתן {formatCooldownTime(fieldResetCooldown)}
|
||
</div>
|
||
) : (
|
||
`אפס את כל הסטטוסים של תחום ${fieldName}`
|
||
)}
|
||
</Button>
|
||
<div className="text-xs text-gray-500 bg-yellow-50 p-2 rounded">
|
||
<strong>הערה:</strong> איפוס יאפס את כל המשתמשים מהתחום שלך ({fieldName}) שאינם נעולים
|
||
</div>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{renderStatsSection(fieldStats, handleFieldCategoryClick, false, fieldName)}
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Eye className="h-5 w-5" />
|
||
{(user?.role === "team_admin" || user?.role === "department_admin") ? "צפייה במשתמשי תחום" : "ניהול משתמשי תחום"} {fieldName}
|
||
{fieldChangedRows.size > 0 && (
|
||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
|
||
{fieldChangedRows.size} עדכונים חדשים
|
||
</span>
|
||
)}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loadingUsers ? (
|
||
<div className="text-center py-4">טוען משתמשים...</div>
|
||
) : (
|
||
renderUsersTable(fieldUsers, fieldChangedRows, (user?.role === "team_admin" || user?.role === "department_admin"))
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="global" className="space-y-6">
|
||
<div className="grid md:grid-cols-2 gap-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<RotateCcw className="h-5 w-5" />
|
||
איפוס סטטוסים כללי
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<Button
|
||
onClick={handleGlobalResetAll}
|
||
disabled={globalResetCooldown > 0}
|
||
className="w-full"
|
||
variant={globalResetCooldown > 0 ? "secondary" : "destructive"}
|
||
>
|
||
{globalResetCooldown > 0 ? (
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
המתן {formatCooldownTime(globalResetCooldown)}
|
||
</div>
|
||
) : (
|
||
"אפס את כל הסטטוסים"
|
||
)}
|
||
</Button>
|
||
{globalLastReset && <p className="text-sm text-gray-600">איפוס אחרון: {globalLastReset}</p>}
|
||
<div className="text-xs text-gray-500 bg-yellow-50 p-2 rounded">
|
||
<strong>הערה:</strong> איפוס יאפס את כל המשתמשים במערכת (כולל מנהלים) שאינם נעולים
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{renderStatsSection(globalStats, handleGlobalCategoryClick, false)}
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<UserPlus className="h-5 w-5" />
|
||
הוספת משתמש חדש
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleAddUser} className="space-y-4">
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="name">שם</Label>
|
||
<Input
|
||
id="name"
|
||
value={newUser.name}
|
||
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
||
placeholder="שם"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="field">תחום</Label>
|
||
<Select value={newUser.field} onValueChange={(value) => setNewUser({ ...newUser, field: value })}>
|
||
<SelectTrigger dir="rtl">
|
||
<SelectValue placeholder="בחר תחום" />
|
||
</SelectTrigger>
|
||
<SelectContent dir="rtl">
|
||
{FIELDS.map((field) => (
|
||
<SelectItem key={field} value={field}>
|
||
{field}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="department">מסגרת</Label>
|
||
<Select
|
||
value={newUser.department}
|
||
onValueChange={(value) => setNewUser({ ...newUser, department: value })}
|
||
>
|
||
<SelectTrigger dir="rtl">
|
||
<SelectValue placeholder="בחר מסגרת" />
|
||
</SelectTrigger>
|
||
<SelectContent dir="rtl">
|
||
{DEPARTMENTS.map((dept) => (
|
||
<SelectItem key={dept} value={dept}>
|
||
{dept}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="team">צוות</Label>
|
||
<Select value={newUser.team} onValueChange={(value) => setNewUser({ ...newUser, team: value })}>
|
||
<SelectTrigger dir="rtl">
|
||
<SelectValue placeholder="בחר צוות" />
|
||
</SelectTrigger>
|
||
<SelectContent dir="rtl">
|
||
{TEAMS.map((team) => (
|
||
<SelectItem key={team} value={team}>
|
||
{team}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="role">הרשאה</Label>
|
||
<Select
|
||
value={newUser.role}
|
||
onValueChange={(value) =>
|
||
setNewUser({
|
||
...newUser,
|
||
role: value,
|
||
isAdmin: value !== "user", // Automatically set isAdmin
|
||
})
|
||
}
|
||
>
|
||
<SelectTrigger dir="rtl">
|
||
<SelectValue placeholder="בחר הרשאה" />
|
||
</SelectTrigger>
|
||
<SelectContent dir="rtl">
|
||
<SelectItem value="user">משתמש רגיל</SelectItem>
|
||
<SelectItem value="team_admin">מנהל צוות</SelectItem>
|
||
<SelectItem value="department_admin">מנהל מסגרת</SelectItem>
|
||
<SelectItem value="field_admin">מנהל תחום</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
<div className="text-sm text-gray-600 bg-blue-50 p-3 rounded">
|
||
<strong>הערה:</strong> המשתמש יקבל את הסיסמה הזמנית "password123" ויידרש לשנותה בכניסה הראשונה
|
||
</div>
|
||
<Button type="submit" className="w-full">
|
||
הוסף משתמש
|
||
</Button>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Eye className="h-5 w-5" />
|
||
ניהול כל המשתמשים
|
||
{changedRows.size > 0 && (
|
||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
|
||
{changedRows.size} עדכונים חדשים
|
||
</span>
|
||
)}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loadingUsers ? (
|
||
<div className="text-center py-4">טוען משתמשים...</div>
|
||
) : (
|
||
renderUsersTable(globalUsers, changedRows)
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
<UserCategoryModal
|
||
isOpen={modalOpen}
|
||
onClose={() => setModalOpen(false)}
|
||
category={selectedCategory}
|
||
categoryName={selectedCategoryName}
|
||
/>
|
||
|
||
<TeamUserCategoryModal
|
||
isOpen={teamModalOpen}
|
||
onClose={() => setTeamModalOpen(false)}
|
||
category={selectedCategory}
|
||
categoryName={selectedCategoryName}
|
||
adminId={user?.national_id || ""}
|
||
teamName={teamName}
|
||
/>
|
||
|
||
<DepartmentUserCategoryModal
|
||
isOpen={departmentModalOpen}
|
||
onClose={() => setDepartmentModalOpen(false)}
|
||
category={selectedCategory}
|
||
categoryName={selectedCategoryName}
|
||
adminId={user?.national_id || ""}
|
||
departmentName={departmentName}
|
||
/>
|
||
|
||
<FieldUserCategoryModal
|
||
isOpen={fieldModalOpen}
|
||
onClose={() => setFieldModalOpen(false)}
|
||
category={selectedCategory}
|
||
categoryName={selectedCategoryName}
|
||
adminId={user?.national_id || ""}
|
||
fieldName={fieldName}
|
||
/>
|
||
|
||
<ReportOnBehalfModal
|
||
isOpen={reportModalOpen}
|
||
onClose={() => setReportModalOpen(false)}
|
||
user={selectedUserForReport}
|
||
onReport={handleReportOnBehalf}
|
||
/>
|
||
|
||
{/* Hostname Footer */}
|
||
<Card className="mt-8">
|
||
<CardContent className="py-3">
|
||
<div className="text-center text-xs text-gray-500">
|
||
סביבה: {process.env.NEXT_PUBLIC_HOSTNAME || process.env.HOSTNAME || "לא זוהה"}
|
||
<br/>
|
||
גרסה: {process.env.APPVERSION || "לא הוצהר ב-Dockerfile!"}
|
||
<br/>
|
||
2025 COPYRIGHT TR-WEB
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|