Files
mamad-app/app/admin/page.tsx

1390 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
//export const dynamic = 'force-dynamic'
//export const revalidate = 0
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 // 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])
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(30) // 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 = async () => {
setIsRefreshing(true)
try {
await Promise.all([refetchGlobal(), refetchTeam(), refetchDepartment(), refetchField()])
} catch (error) {
console.error("Manual refresh failed:", error)
setMessage("שגיאה ברענון הנתונים")
} finally {
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>User: {user?.name} ({user?.role})</div>
<div>Team: {teamName}</div>
<div>Global Users: {globalUsers.length}</div>
<div>Global Connected: {globalConnected ? "Yes" : "No"}</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>
)
}