Updated to using managed types instead of

hard coded ones.
This commit is contained in:
2026-01-16 17:48:46 +02:00
parent 14e6737a1d
commit 4defe33bd3
280 changed files with 48455 additions and 62 deletions

View File

@@ -9,18 +9,7 @@ import { useRouter } from "next/navigation"
// ... (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"
import { type User, type UserRole, ROLE_NAMES } from "@/types/user"
// ... (rest of your component code)
@@ -65,6 +54,7 @@ import {
MessageSquare,
Lock,
LockOpen,
Pencil,
ArrowLeft,
Home,
} from "lucide-react"
@@ -80,6 +70,7 @@ import { useDepartmentRealTimeUpdates } from "@/hooks/useDepartmentRealTimeUpdat
import { FieldUserCategoryModal } from "@/components/field-user-category-modal"
import { useFieldRealTimeUpdates } from "@/hooks/useFieldRealTimeUpdates"
import { ReportOnBehalfModal } from "@/components/report-on-behalf-modal"
import { UserScopeModal } from "@/components/user-scope-modal"
interface Stats {
no_report: number
@@ -102,6 +93,13 @@ interface UserData {
lock_status?: boolean
}
interface ManagedTypeOption {
id?: number
name: string
managed: boolean
parentId?: number | null
}
export default function AdminPage() {
const [user, setUser] = useState<User | null>(null)
const [activeTab, setActiveTab] = useState("team")
@@ -142,6 +140,25 @@ export default function AdminPage() {
team: "",
role: "",
})
const [managedTypes, setManagedTypes] = useState<{
fields: ManagedTypeOption[]
departments: ManagedTypeOption[]
teams: ManagedTypeOption[]
}>({
fields: [],
departments: [],
teams: [],
})
const [managedTypesLoading, setManagedTypesLoading] = useState(false)
const [managedTypeTab, setManagedTypeTab] = useState<"field" | "department" | "team">("field")
const [newFieldName, setNewFieldName] = useState("")
const [newDepartmentName, setNewDepartmentName] = useState("")
const [newTeamName, setNewTeamName] = useState("")
const [newDepartmentParentId, setNewDepartmentParentId] = useState<string>("")
const [newTeamParentId, setNewTeamParentId] = useState<string>("")
const [scopeModalOpen, setScopeModalOpen] = useState(false)
const [scopeUser, setScopeUser] = useState<UserData | null>(null)
const [scopeSaving, setScopeSaving] = useState(false)
const [message, setMessage] = useState("")
const [loadingUsers, setLoadingUsers] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
@@ -316,6 +333,43 @@ export default function AdminPage() {
setUser(parsedUser)
}, [router])
const fetchManagedTypes = async () => {
setManagedTypesLoading(true)
try {
const response = await fetch(`/api/admin/managed-types?adminId=${encodeURIComponent(user?.national_id || "")}`)
const data = await response.json()
if (response.ok) {
setManagedTypes({
fields: data.fields || [],
departments: data.departments || [],
teams: data.teams || [],
})
} else {
setMessage(data.error || "Failed to load managed types.")
}
} catch (error) {
console.error("Managed types fetch error:", error)
setMessage("Failed to load managed types.")
} finally {
setManagedTypesLoading(false)
}
}
useEffect(() => {
if (user?.national_id) {
fetchManagedTypes()
}
}, [user?.national_id])
useEffect(() => {
if (!newDepartmentParentId && managedTypes.fields.length === 1 && managedTypes.fields[0].id) {
setNewDepartmentParentId(String(managedTypes.fields[0].id))
}
if (!newTeamParentId && managedTypes.departments.length === 1 && managedTypes.departments[0].id) {
setNewTeamParentId(String(managedTypes.departments[0].id))
}
}, [managedTypes, newDepartmentParentId, newTeamParentId])
useEffect(() => {
if (globalResetCooldown > 0) {
const timer = setTimeout(() => setGlobalResetCooldown(globalResetCooldown - 1), 1000)
@@ -474,6 +528,108 @@ export default function AdminPage() {
}
}
const handleAddManagedType = async (type: "field" | "department" | "team") => {
const name =
type === "field" ? newFieldName : type === "department" ? newDepartmentName : newTeamName
if (!name.trim()) {
return
}
const parentId =
type === "department"
? newDepartmentParentId
: type === "team"
? newTeamParentId
: ""
if ((type === "department" || type === "team") && !parentId) {
setMessage("Select a parent before adding.")
return
}
try {
const response = await fetch("/api/admin/managed-types", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
adminId: user?.national_id,
type,
name: name.trim(),
parentId: parentId ? Number(parentId) : undefined,
}),
})
const data = await response.json()
if (response.ok) {
if (type === "field") setNewFieldName("")
if (type === "department") setNewDepartmentName("")
if (type === "team") setNewTeamName("")
if (type === "department") setNewDepartmentParentId("")
if (type === "team") setNewTeamParentId("")
await fetchManagedTypes()
} else {
setMessage(data.error || "שגיאה בהוספת Type מנוהל")
}
} catch (error) {
console.error("Managed type add error:", error)
setMessage("שגיאה בהוספת Type מנוהל")
}
}
const handleDeleteManagedType = async (id?: number) => {
if (!id || !user?.national_id) return
if (!confirm("למחוק ערך זה? יש לשייך לפני המחיקה את כלל המשתמשים תחת ערך זה מחדש.")) {
return
}
try {
const response = await fetch(`/api/admin/managed-types/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ adminId: user.national_id }),
})
const data = await response.json()
if (response.ok) {
await fetchManagedTypes()
} else {
setMessage(data.error || "Failed to delete managed type.")
}
} catch (error) {
console.error("Managed type delete error:", error)
setMessage("Failed to delete managed type.")
}
}
const handleRenameManagedType = async (id?: number, currentName?: string) => {
if (!id || !user?.national_id || !currentName) return
const nextName = prompt("Rename value:", currentName)
if (!nextName || nextName.trim() === currentName) return
try {
const response = await fetch(`/api/admin/managed-types/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ adminId: user.national_id, name: nextName.trim() }),
})
const data = await response.json()
if (response.ok) {
await fetchManagedTypes()
await Promise.all([refetchGlobal(), refetchTeam(), refetchDepartment(), refetchField()])
} else {
setMessage(data.error || "Failed to rename managed type.")
}
} catch (error) {
console.error("Managed type rename error:", error)
setMessage("Failed to rename managed type.")
}
}
const handleAddUser = async (e: React.FormEvent) => {
e.preventDefault()
@@ -597,6 +753,45 @@ export default function AdminPage() {
}
}
const handleUpdateUserScope = async (payload: {
userId: string
field: string
department: string
team: string
}) => {
if (!user?.national_id) return
setScopeSaving(true)
try {
const response = await fetch("/api/admin/update-user-scope", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
adminId: user.national_id,
targetUserId: payload.userId,
field: payload.field,
department: payload.department,
team: payload.team,
}),
})
const data = await response.json()
if (response.ok) {
setMessage(data.message || "User updated.")
setScopeModalOpen(false)
setScopeUser(null)
await Promise.all([refetchGlobal(), refetchTeam(), refetchDepartment(), refetchField()])
} else {
setMessage(data.error || "Failed to update user.")
}
} catch (error) {
console.error("User scope update error:", error)
setMessage("Failed to update user.")
} finally {
setScopeSaving(false)
}
}
const getStatusText = (status?: string) => {
switch (status) {
case "yes":
@@ -833,6 +1028,20 @@ export default function AdminPage() {
{!isReadOnly && (
<TableCell>
<div className="flex gap-2">
{user?.role !== "user" && (
<Button
variant="outline"
size="sm"
onClick={() => {
setScopeUser(userData)
setScopeModalOpen(true)
}}
className="text-blue-600 hover:text-blue-700"
title="Edit assignment"
>
<Pencil className="h-4 w-4" />
</Button>
)}
<Button
variant="outline"
size="sm"
@@ -889,6 +1098,21 @@ export default function AdminPage() {
)
}
const fieldNameById = new Map(managedTypes.fields.map((field) => [field.id, field.name]))
const departmentNameById = new Map(managedTypes.departments.map((dept) => [dept.id, dept.name]))
const allowedManagedTabs =
user?.role === "global_admin" || user?.role === "field_admin"
? ["field", "department", "team"]
: user?.role === "department_admin"
? ["department", "team"]
: []
useEffect(() => {
if (allowedManagedTabs.length > 0 && !allowedManagedTabs.includes(managedTypeTab)) {
setManagedTypeTab(allowedManagedTabs[0] as "field" | "department" | "team")
}
}, [allowedManagedTabs, managedTypeTab])
if (!user) return null
return (
@@ -1232,9 +1456,9 @@ export default function AdminPage() {
<SelectValue placeholder="בחר תחום" />
</SelectTrigger>
<SelectContent dir="rtl">
{FIELDS.map((field) => (
<SelectItem key={field} value={field}>
{field}
{managedTypes.fields.map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.name}
</SelectItem>
))}
</SelectContent>
@@ -1250,9 +1474,9 @@ export default function AdminPage() {
<SelectValue placeholder="בחר מסגרת" />
</SelectTrigger>
<SelectContent dir="rtl">
{DEPARTMENTS.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
{managedTypes.departments.map((dept) => (
<SelectItem key={dept.name} value={dept.name}>
{dept.name}
</SelectItem>
))}
</SelectContent>
@@ -1265,9 +1489,9 @@ export default function AdminPage() {
<SelectValue placeholder="בחר צוות" />
</SelectTrigger>
<SelectContent dir="rtl">
{TEAMS.map((team) => (
<SelectItem key={team} value={team}>
{team}
{managedTypes.teams.map((team) => (
<SelectItem key={team.name} value={team.name}>
{team.name}
</SelectItem>
))}
</SelectContent>
@@ -1307,6 +1531,233 @@ export default function AdminPage() {
</CardContent>
</Card>
{allowedManagedTabs.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
ניהול תחומים, מסגרות וצוותים
</CardTitle>
</CardHeader>
<CardContent>
<Tabs
value={managedTypeTab}
onValueChange={(value) => setManagedTypeTab(value as "field" | "department" | "team")}
className="w-full"
dir="rtl"
>
<TabsList className="grid w-full grid-cols-3">
{allowedManagedTabs.includes("field") && <TabsTrigger value="field">תחומים</TabsTrigger>}
{allowedManagedTabs.includes("department") && (
<TabsTrigger value="department">מסגרות</TabsTrigger>
)}
{allowedManagedTabs.includes("team") && <TabsTrigger value="team">צוותים</TabsTrigger>}
</TabsList>
{allowedManagedTabs.includes("field") && (
<TabsContent value="field" className="space-y-4">
<div className="flex gap-2">
<Input
value={newFieldName}
onChange={(e) => setNewFieldName(e.target.value)}
placeholder="תחום חדש"
disabled={managedTypesLoading}
/>
<Button
onClick={() => handleAddManagedType("field")}
disabled={managedTypesLoading || !newFieldName.trim()}
>
הוספה
</Button>
</div>
<div className="text-xs text-gray-500">
יש לשייך משתמשים לתחום אחר לפני מחיקה!
</div>
<div className="space-y-2">
{managedTypes.fields.length === 0 ? (
<div className="text-sm text-gray-500">No fields yet.</div>
) : (
managedTypes.fields.map((item) => (
<div key={item.name} className="flex items-center justify-between rounded border p-2">
<div className="flex flex-col">
<span>{item.name}</span>
</div>
{item.managed ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleRenameManagedType(item.id, item.name)}
>
<Pencil className="h-4 w-4 text-blue-600" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteManagedType(item.id)}
>
<Trash2 className="h-4 w-4 text-red-600" />
</Button>
</div>
) : (
<span className="text-xs text-gray-400">In use</span>
)}
</div>
))
)}
</div>
</TabsContent>
)}
{allowedManagedTabs.includes("department") && (
<TabsContent value="department" className="space-y-4">
<div className="flex gap-2">
<Select value={newDepartmentParentId} onValueChange={setNewDepartmentParentId}>
<SelectTrigger dir="rtl">
<SelectValue placeholder="בחרו תחום" />
</SelectTrigger>
<SelectContent dir="rtl">
{managedTypes.fields.filter((field) => field.id).map((field) => (
<SelectItem key={field.name} value={String(field.id)}>
{field.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={newDepartmentName}
onChange={(e) => setNewDepartmentName(e.target.value)}
placeholder="מסגרת חדשה"
disabled={managedTypesLoading}
/>
<Button
onClick={() => handleAddManagedType("department")}
disabled={managedTypesLoading || !newDepartmentName.trim()}
>
הוספה
</Button>
</div>
<div className="text-xs text-gray-500">
יש לשייך משתמשים למסגרת אחרת לפני מחיקה!
</div>
<div className="space-y-2">
{managedTypes.departments.length === 0 ? (
<div className="text-sm text-gray-500">No departments yet.</div>
) : (
managedTypes.departments.map((item) => (
<div key={item.name} className="flex items-center justify-between rounded border p-2">
<div className="flex flex-col">
<span>{item.name}</span>
{item.parentId && (
<span className="text-xs text-gray-500">
תחום: {fieldNameById.get(item.parentId) || "לא ידוע"}
</span>
)}
</div>
{item.managed ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleRenameManagedType(item.id, item.name)}
>
<Pencil className="h-4 w-4 text-blue-600" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteManagedType(item.id)}
>
<Trash2 className="h-4 w-4 text-red-600" />
</Button>
</div>
) : (
<span className="text-xs text-gray-400">In use</span>
)}
</div>
))
)}
</div>
</TabsContent>
)}
{allowedManagedTabs.includes("team") && (
<TabsContent value="team" className="space-y-4">
<div className="flex gap-2">
<Select value={newTeamParentId} onValueChange={setNewTeamParentId}>
<SelectTrigger dir="rtl">
<SelectValue placeholder="בחרו מסגרת" />
</SelectTrigger>
<SelectContent dir="rtl">
{managedTypes.departments.filter((dept) => dept.id).map((dept) => (
<SelectItem key={dept.name} value={String(dept.id)}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
placeholder="צוות חדש"
disabled={managedTypesLoading}
/>
<Button
onClick={() => handleAddManagedType("team")}
disabled={managedTypesLoading || !newTeamName.trim()}
>
הוספה
</Button>
</div>
<div className="text-xs text-gray-500">
יש לשייך משתמשים לצוות אחר לפני מחיקה!
</div>
<div className="space-y-2">
{managedTypes.teams.length === 0 ? (
<div className="text-sm text-gray-500">No teams yet.</div>
) : (
managedTypes.teams.map((item) => (
<div key={item.name} className="flex items-center justify-between rounded border p-2">
<div className="flex flex-col">
<span>{item.name}</span>
{item.parentId && (
<span className="text-xs text-gray-500">
מסגרת: {departmentNameById.get(item.parentId) || "לא ידועה"}
</span>
)}
</div>
{item.managed ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleRenameManagedType(item.id, item.name)}
>
<Pencil className="h-4 w-4 text-blue-600" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteManagedType(item.id)}
>
<Trash2 className="h-4 w-4 text-red-600" />
</Button>
</div>
) : (
<span className="text-xs text-gray-400">In use</span>
)}
</div>
))
)}
</div>
</TabsContent>
)}
</Tabs>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -1364,6 +1815,17 @@ export default function AdminPage() {
fieldName={fieldName}
/>
<UserScopeModal
isOpen={scopeModalOpen}
onClose={() => setScopeModalOpen(false)}
user={scopeUser}
fields={managedTypes.fields}
departments={managedTypes.departments}
teams={managedTypes.teams}
onSave={handleUpdateUserScope}
isSaving={scopeSaving}
/>
<ReportOnBehalfModal
isOpen={reportModalOpen}
onClose={() => setReportModalOpen(false)}