Updated to using managed types instead of
hard coded ones.
This commit is contained in:
+483
-21
@@ -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)}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { safeQuery } from "@/lib/database"
|
||||
import { hashPassword } from "@/lib/auth"
|
||||
import { type UserRole, DEPARTMENTS, TEAMS, FIELDS } from "@/types/user"
|
||||
import { type UserRole } from "@/types/user"
|
||||
|
||||
const hasManagedType = async (type: "field" | "department" | "team", name: string) => {
|
||||
const rows = (await safeQuery("SELECT 1 FROM managed_types WHERE type = ? AND name = ? LIMIT 1", [
|
||||
type,
|
||||
name,
|
||||
])) as any[]
|
||||
if (rows.length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const legacyRows = (await safeQuery(`SELECT 1 FROM users WHERE ${type} = ? LIMIT 1`, [name])) as any[]
|
||||
return legacyRows.length > 0
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -12,17 +25,41 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate department, team, and field
|
||||
if (!FIELDS.includes(field as any)) {
|
||||
return NextResponse.json({ error: "תחום לא תקין" }, { status: 400 })
|
||||
// Validate department, team, and field against managed types
|
||||
if (!(await hasManagedType("field", field))) {
|
||||
return NextResponse.json({ error: "Invalid field." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!DEPARTMENTS.includes(department as any)) {
|
||||
return NextResponse.json({ error: "מסגרת לא תקינה" }, { status: 400 })
|
||||
if (!(await hasManagedType("department", department))) {
|
||||
return NextResponse.json({ error: "Invalid department." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!TEAMS.includes(team as any)) {
|
||||
return NextResponse.json({ error: "צוות לא תקין" }, { status: 400 })
|
||||
if (!(await hasManagedType("team", team))) {
|
||||
return NextResponse.json({ error: "Invalid team." }, { status: 400 })
|
||||
}
|
||||
|
||||
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
|
||||
field,
|
||||
])) as Array<{ id: number }>
|
||||
const departmentRows = (await safeQuery(
|
||||
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'department' AND name = ?",
|
||||
[department],
|
||||
)) as Array<{ id: number; parentId: number | null }>
|
||||
const teamRows = (await safeQuery(
|
||||
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'team' AND name = ?",
|
||||
[team],
|
||||
)) as Array<{ id: number; parentId: number | null }>
|
||||
|
||||
if (fieldRows.length === 0 || departmentRows.length === 0 || teamRows.length === 0) {
|
||||
return NextResponse.json({ error: "Invalid field, department, or team." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (departmentRows[0].parentId !== fieldRows[0].id) {
|
||||
return NextResponse.json({ error: "Department does not belong to field." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (teamRows[0].parentId !== departmentRows[0].id) {
|
||||
return NextResponse.json({ error: "Team does not belong to department." }, { status: 400 })
|
||||
}
|
||||
|
||||
const validRoles: UserRole[] = ["user", "team_admin", "department_admin", "field_admin", "global_admin"]
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { safeQuery } from "@/lib/database"
|
||||
|
||||
type ManagedTypeKind = "field" | "department" | "team"
|
||||
|
||||
type AdminScope = {
|
||||
role: string
|
||||
field?: string | null
|
||||
department?: string | null
|
||||
team?: string | null
|
||||
}
|
||||
|
||||
const getAdminScope = async (adminId?: string) => {
|
||||
if (!adminId) {
|
||||
return { ok: false, response: NextResponse.json({ error: "Missing admin id." }, { status: 400 }) }
|
||||
}
|
||||
|
||||
const adminRows = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
|
||||
adminId,
|
||||
])) as AdminScope[]
|
||||
if (adminRows.length === 0) {
|
||||
return { ok: false, response: NextResponse.json({ error: "Admin not found." }, { status: 404 }) }
|
||||
}
|
||||
|
||||
return { ok: true as const, admin: adminRows[0] }
|
||||
}
|
||||
|
||||
const canManageType = async (admin: AdminScope, managedType: { type: ManagedTypeKind; name: string; parentId: number | null }) => {
|
||||
if (admin.role === "global_admin") return true
|
||||
if (admin.role === "field_admin") {
|
||||
if (managedType.type === "field") return managedType.name === admin.field
|
||||
if (managedType.type === "department") {
|
||||
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
|
||||
admin.field,
|
||||
])) as Array<{ id: number }>
|
||||
return fieldRows.length > 0 && managedType.parentId === fieldRows[0].id
|
||||
}
|
||||
if (managedType.type === "team") {
|
||||
const departmentRows = (await safeQuery(
|
||||
"SELECT parent_id AS parentId FROM managed_types WHERE id = ? AND type = 'department'",
|
||||
[managedType.parentId],
|
||||
)) as Array<{ parentId: number | null }>
|
||||
if (departmentRows.length === 0) return false
|
||||
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
|
||||
admin.field,
|
||||
])) as Array<{ id: number }>
|
||||
return fieldRows.length > 0 && departmentRows[0].parentId === fieldRows[0].id
|
||||
}
|
||||
}
|
||||
if (admin.role === "department_admin") {
|
||||
if (managedType.type === "department") return managedType.name === admin.department
|
||||
if (managedType.type === "team") {
|
||||
const departmentRows = (await safeQuery(
|
||||
"SELECT id FROM managed_types WHERE type = 'department' AND name = ?",
|
||||
[admin.department],
|
||||
)) as Array<{ id: number }>
|
||||
return departmentRows.length > 0 && managedType.parentId === departmentRows[0].id
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const { adminId } = await request.json()
|
||||
|
||||
const adminCheck = await getAdminScope(adminId)
|
||||
if (!adminCheck.ok) {
|
||||
return adminCheck.response
|
||||
}
|
||||
|
||||
const typeRows = (await safeQuery("SELECT id, type, name, parent_id AS parentId FROM managed_types WHERE id = ?", [
|
||||
params.id,
|
||||
])) as Array<{ id: number; type: ManagedTypeKind; name: string; parentId: number | null }>
|
||||
|
||||
if (typeRows.length === 0) {
|
||||
return NextResponse.json({ error: "Managed type not found." }, { status: 404 })
|
||||
}
|
||||
|
||||
const managedType = typeRows[0]
|
||||
const column = managedType.type
|
||||
|
||||
if (!(await canManageType(adminCheck.admin, managedType))) {
|
||||
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
|
||||
}
|
||||
|
||||
const childRows = (await safeQuery("SELECT COUNT(*) as count FROM managed_types WHERE parent_id = ?", [
|
||||
managedType.id,
|
||||
])) as Array<{ count: number }>
|
||||
if ((childRows[0]?.count || 0) > 0) {
|
||||
return NextResponse.json({ error: "Remove child items first." }, { status: 409 })
|
||||
}
|
||||
|
||||
const usageRows = (await safeQuery(`SELECT COUNT(*) as count FROM users WHERE ${column} = ?`, [
|
||||
managedType.name,
|
||||
])) as Array<{ count: number }>
|
||||
|
||||
if ((usageRows[0]?.count || 0) > 0) {
|
||||
return NextResponse.json({ error: "Value is still assigned to users." }, { status: 409 })
|
||||
}
|
||||
|
||||
await safeQuery("DELETE FROM managed_types WHERE id = ?", [managedType.id])
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error("Managed types delete error:", error)
|
||||
return NextResponse.json({ error: "Failed to delete managed type." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const { adminId, name } = await request.json()
|
||||
|
||||
const adminCheck = await getAdminScope(adminId)
|
||||
if (!adminCheck.ok) {
|
||||
return adminCheck.response
|
||||
}
|
||||
|
||||
const trimmedName = typeof name === "string" ? name.trim() : ""
|
||||
if (!trimmedName) {
|
||||
return NextResponse.json({ error: "Name is required." }, { status: 400 })
|
||||
}
|
||||
|
||||
const typeRows = (await safeQuery("SELECT id, type, name, parent_id AS parentId FROM managed_types WHERE id = ?", [
|
||||
params.id,
|
||||
])) as Array<{ id: number; type: ManagedTypeKind; name: string; parentId: number | null }>
|
||||
|
||||
if (typeRows.length === 0) {
|
||||
return NextResponse.json({ error: "Managed type not found." }, { status: 404 })
|
||||
}
|
||||
|
||||
const managedType = typeRows[0]
|
||||
if (!(await canManageType(adminCheck.admin, managedType))) {
|
||||
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
|
||||
}
|
||||
|
||||
await safeQuery("UPDATE managed_types SET name = ? WHERE id = ?", [trimmedName, managedType.id])
|
||||
|
||||
const column = managedType.type
|
||||
await safeQuery(`UPDATE users SET ${column} = ? WHERE ${column} = ?`, [trimmedName, managedType.name])
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
if (error?.code === "ER_DUP_ENTRY") {
|
||||
return NextResponse.json({ error: "Value already exists." }, { status: 409 })
|
||||
}
|
||||
console.error("Managed types rename error:", error)
|
||||
return NextResponse.json({ error: "Failed to rename managed type." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { safeQuery } from "@/lib/database"
|
||||
|
||||
type ManagedTypeKind = "field" | "department" | "team"
|
||||
|
||||
const managedTypeKinds: ManagedTypeKind[] = ["field", "department", "team"]
|
||||
|
||||
type AdminScope = {
|
||||
role: string
|
||||
field?: string | null
|
||||
department?: string | null
|
||||
team?: string | null
|
||||
}
|
||||
|
||||
const getAdminScope = async (adminId?: string) => {
|
||||
if (!adminId) {
|
||||
return { ok: false, response: NextResponse.json({ error: "Missing admin id." }, { status: 400 }) }
|
||||
}
|
||||
|
||||
const adminRows = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
|
||||
adminId,
|
||||
])) as AdminScope[]
|
||||
|
||||
if (adminRows.length === 0) {
|
||||
return { ok: false, response: NextResponse.json({ error: "Admin not found." }, { status: 404 }) }
|
||||
}
|
||||
|
||||
return { ok: true as const, admin: adminRows[0] }
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const adminId = request.nextUrl.searchParams.get("adminId") || undefined
|
||||
const adminCheck = await getAdminScope(adminId)
|
||||
if (!adminCheck.ok) {
|
||||
return adminCheck.response
|
||||
}
|
||||
|
||||
const admin = adminCheck.admin
|
||||
|
||||
const managedRows = (await safeQuery(
|
||||
"SELECT id, type, name, parent_id AS parentId FROM managed_types ORDER BY type, name",
|
||||
)) as Array<{ id: number; type: ManagedTypeKind; name: string; parentId: number | null }>
|
||||
|
||||
const [fieldsFromUsers, departmentsFromUsers, teamsFromUsers] = await Promise.all([
|
||||
safeQuery("SELECT DISTINCT field AS name FROM users WHERE field IS NOT NULL AND field <> '' ORDER BY field"),
|
||||
safeQuery(
|
||||
"SELECT DISTINCT department AS name FROM users WHERE department IS NOT NULL AND department <> '' ORDER BY department",
|
||||
),
|
||||
safeQuery("SELECT DISTINCT team AS name FROM users WHERE team IS NOT NULL AND team <> '' ORDER BY team"),
|
||||
])
|
||||
|
||||
const byType = {
|
||||
field: managedRows.filter((row) => row.type === "field"),
|
||||
department: managedRows.filter((row) => row.type === "department"),
|
||||
team: managedRows.filter((row) => row.type === "team"),
|
||||
}
|
||||
|
||||
const merge = (
|
||||
managed: Array<{ id: number; name: string; parentId: number | null }>,
|
||||
fromUsers: Array<{ name: string }>,
|
||||
) => {
|
||||
const map = new Map<
|
||||
string,
|
||||
{ id?: number; name: string; managed: boolean; parentId?: number | null }
|
||||
>()
|
||||
|
||||
managed.forEach((row) => {
|
||||
map.set(row.name, { id: row.id, name: row.name, managed: true, parentId: row.parentId })
|
||||
})
|
||||
|
||||
fromUsers.forEach((row) => {
|
||||
if (!map.has(row.name)) {
|
||||
map.set(row.name, { name: row.name, managed: false, parentId: null })
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
let fields = merge(byType.field, fieldsFromUsers as any[])
|
||||
let departments = merge(byType.department, departmentsFromUsers as any[])
|
||||
let teams = merge(byType.team, teamsFromUsers as any[])
|
||||
|
||||
if (admin.role === "field_admin") {
|
||||
fields = fields.filter((field) => field.name === admin.field)
|
||||
const fieldId = fields[0]?.id
|
||||
departments = fieldId ? departments.filter((dept) => dept.parentId === fieldId) : []
|
||||
const departmentIds = new Set(departments.map((dept) => dept.id))
|
||||
teams = teams.filter((team) => team.parentId && departmentIds.has(team.parentId))
|
||||
} else if (admin.role === "department_admin") {
|
||||
fields = fields.filter((field) => field.name === admin.field)
|
||||
departments = departments.filter((dept) => dept.name === admin.department)
|
||||
const departmentId = departments[0]?.id
|
||||
teams = departmentId ? teams.filter((team) => team.parentId === departmentId) : []
|
||||
} else if (admin.role === "team_admin") {
|
||||
fields = fields.filter((field) => field.name === admin.field)
|
||||
departments = departments.filter((dept) => dept.name === admin.department)
|
||||
teams = teams.filter((team) => team.name === admin.team)
|
||||
}
|
||||
|
||||
return NextResponse.json({ fields, departments, teams })
|
||||
} catch (error) {
|
||||
console.error("Managed types fetch error:", error)
|
||||
return NextResponse.json({ error: "Failed to load managed types." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { adminId, type, name, parentId } = await request.json()
|
||||
|
||||
const adminCheck = await getAdminScope(adminId)
|
||||
if (!adminCheck.ok) {
|
||||
return adminCheck.response
|
||||
}
|
||||
|
||||
const admin = adminCheck.admin
|
||||
|
||||
if (!managedTypeKinds.includes(type)) {
|
||||
return NextResponse.json({ error: "Invalid type." }, { status: 400 })
|
||||
}
|
||||
|
||||
const trimmedName = typeof name === "string" ? name.trim() : ""
|
||||
if (!trimmedName) {
|
||||
return NextResponse.json({ error: "Name is required." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (type === "field") {
|
||||
if (admin.role !== "global_admin" && admin.role !== "field_admin") {
|
||||
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
|
||||
}
|
||||
if (admin.role === "field_admin" && admin.field && admin.field !== trimmedName) {
|
||||
return NextResponse.json({ error: "Field admins can only manage their field." }, { status: 403 })
|
||||
}
|
||||
await safeQuery("INSERT INTO managed_types (type, name, parent_id) VALUES (?, ?, NULL)", [type, trimmedName])
|
||||
} else if (type === "department") {
|
||||
if (!parentId) {
|
||||
return NextResponse.json({ error: "Parent field is required." }, { status: 400 })
|
||||
}
|
||||
if (admin.role === "department_admin" || admin.role === "team_admin") {
|
||||
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
|
||||
}
|
||||
if (admin.role === "field_admin") {
|
||||
const fieldRows = (await safeQuery("SELECT id, name FROM managed_types WHERE id = ? AND type = 'field'", [
|
||||
parentId,
|
||||
])) as Array<{ id: number; name: string }>
|
||||
if (fieldRows.length === 0 || fieldRows[0].name !== admin.field) {
|
||||
return NextResponse.json({ error: "Field admins can only manage their field." }, { status: 403 })
|
||||
}
|
||||
}
|
||||
await safeQuery("INSERT INTO managed_types (type, name, parent_id) VALUES (?, ?, ?)", [
|
||||
type,
|
||||
trimmedName,
|
||||
parentId,
|
||||
])
|
||||
} else {
|
||||
if (!parentId) {
|
||||
return NextResponse.json({ error: "Parent department is required." }, { status: 400 })
|
||||
}
|
||||
if (admin.role === "team_admin") {
|
||||
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
|
||||
}
|
||||
if (admin.role === "department_admin") {
|
||||
const departmentRows = (await safeQuery(
|
||||
"SELECT id, name FROM managed_types WHERE id = ? AND type = 'department'",
|
||||
[parentId],
|
||||
)) as Array<{ id: number; name: string }>
|
||||
if (departmentRows.length === 0 || departmentRows[0].name !== admin.department) {
|
||||
return NextResponse.json({ error: "Department admins can only manage their department." }, { status: 403 })
|
||||
}
|
||||
}
|
||||
if (admin.role === "field_admin") {
|
||||
const departmentRows = (await safeQuery(
|
||||
"SELECT id, parent_id AS parentId FROM managed_types WHERE id = ? AND type = 'department'",
|
||||
[parentId],
|
||||
)) as Array<{ id: number; parentId: number | null }>
|
||||
if (departmentRows.length === 0) {
|
||||
return NextResponse.json({ error: "Department not found." }, { status: 404 })
|
||||
}
|
||||
const fieldRows = (await safeQuery("SELECT id, name FROM managed_types WHERE id = ?", [
|
||||
departmentRows[0].parentId,
|
||||
])) as Array<{ id: number; name: string }>
|
||||
if (fieldRows.length === 0 || fieldRows[0].name !== admin.field) {
|
||||
return NextResponse.json({ error: "Field admins can only manage their field." }, { status: 403 })
|
||||
}
|
||||
}
|
||||
await safeQuery("INSERT INTO managed_types (type, name, parent_id) VALUES (?, ?, ?)", [
|
||||
type,
|
||||
trimmedName,
|
||||
parentId,
|
||||
])
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
if (error?.code === "ER_DUP_ENTRY") {
|
||||
return NextResponse.json({ error: "Value already exists." }, { status: 409 })
|
||||
}
|
||||
console.error("Managed types create error:", error)
|
||||
return NextResponse.json({ error: "Failed to create managed type." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { safeQuery } from "@/lib/database"
|
||||
|
||||
const hasManagedType = async (type: "field" | "department" | "team", name: string) => {
|
||||
const rows = (await safeQuery("SELECT 1 FROM managed_types WHERE type = ? AND name = ? LIMIT 1", [
|
||||
type,
|
||||
name,
|
||||
])) as any[]
|
||||
if (rows.length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const legacyRows = (await safeQuery(`SELECT 1 FROM users WHERE ${type} = ? LIMIT 1`, [name])) as any[]
|
||||
return legacyRows.length > 0
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { adminId, targetUserId, field, department, team } = await request.json()
|
||||
|
||||
if (!adminId || !targetUserId || !field || !department || !team) {
|
||||
return NextResponse.json({ error: "Missing required fields." }, { status: 400 })
|
||||
}
|
||||
|
||||
const adminRows = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
|
||||
adminId,
|
||||
])) as any[]
|
||||
if (adminRows.length === 0) {
|
||||
return NextResponse.json({ error: "Admin not found." }, { status: 404 })
|
||||
}
|
||||
|
||||
const admin = adminRows[0]
|
||||
|
||||
const userRows = (await safeQuery("SELECT national_id, field, department, team FROM users WHERE national_id = ?", [
|
||||
targetUserId,
|
||||
])) as any[]
|
||||
if (userRows.length === 0) {
|
||||
return NextResponse.json({ error: "User not found." }, { status: 404 })
|
||||
}
|
||||
const targetUser = userRows[0]
|
||||
|
||||
const [fieldOk, departmentOk, teamOk] = await Promise.all([
|
||||
hasManagedType("field", field),
|
||||
hasManagedType("department", department),
|
||||
hasManagedType("team", team),
|
||||
])
|
||||
|
||||
if (!fieldOk || !departmentOk || !teamOk) {
|
||||
return NextResponse.json({ error: "Invalid field, department, or team." }, { status: 400 })
|
||||
}
|
||||
|
||||
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
|
||||
field,
|
||||
])) as Array<{ id: number }>
|
||||
const departmentRows = (await safeQuery(
|
||||
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'department' AND name = ?",
|
||||
[department],
|
||||
)) as Array<{ id: number; parentId: number | null }>
|
||||
const teamRows = (await safeQuery(
|
||||
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'team' AND name = ?",
|
||||
[team],
|
||||
)) as Array<{ id: number; parentId: number | null }>
|
||||
|
||||
if (fieldRows.length === 0 || departmentRows.length === 0 || teamRows.length === 0) {
|
||||
return NextResponse.json({ error: "Invalid field, department, or team." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (departmentRows[0].parentId !== fieldRows[0].id) {
|
||||
return NextResponse.json({ error: "Department does not belong to field." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (teamRows[0].parentId !== departmentRows[0].id) {
|
||||
return NextResponse.json({ error: "Team does not belong to department." }, { status: 400 })
|
||||
}
|
||||
|
||||
if (admin.role === "field_admin") {
|
||||
if (targetUser.field !== admin.field) {
|
||||
return NextResponse.json({ error: "Target user is outside your field." }, { status: 403 })
|
||||
}
|
||||
if (field !== admin.field) {
|
||||
return NextResponse.json({ error: "Field admins can only assign within their field." }, { status: 403 })
|
||||
}
|
||||
} else if (admin.role === "department_admin") {
|
||||
if (targetUser.department !== admin.department) {
|
||||
return NextResponse.json({ error: "Target user is outside your department." }, { status: 403 })
|
||||
}
|
||||
if (department !== admin.department) {
|
||||
return NextResponse.json({ error: "Department admins can only assign within their department." }, { status: 403 })
|
||||
}
|
||||
if (admin.field && field !== admin.field) {
|
||||
return NextResponse.json({ error: "Department admins can only assign within their field." }, { status: 403 })
|
||||
}
|
||||
} else if (admin.role === "team_admin") {
|
||||
if (targetUser.team !== admin.team) {
|
||||
return NextResponse.json({ error: "Target user is outside your team." }, { status: 403 })
|
||||
}
|
||||
if (team !== admin.team) {
|
||||
return NextResponse.json({ error: "Team admins can only assign within their team." }, { status: 403 })
|
||||
}
|
||||
if (admin.department && department !== admin.department) {
|
||||
return NextResponse.json({ error: "Team admins can only assign within their department." }, { status: 403 })
|
||||
}
|
||||
if (admin.field && field !== admin.field) {
|
||||
return NextResponse.json({ error: "Team admins can only assign within their field." }, { status: 403 })
|
||||
}
|
||||
} else if (admin.role !== "global_admin") {
|
||||
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
|
||||
}
|
||||
|
||||
await safeQuery("UPDATE users SET field = ?, department = ?, team = ? WHERE national_id = ?", [
|
||||
field,
|
||||
department,
|
||||
team,
|
||||
targetUserId,
|
||||
])
|
||||
|
||||
return NextResponse.json({ success: true, message: "User assignment updated." })
|
||||
} catch (error) {
|
||||
console.error("Update user scope error:", error)
|
||||
return NextResponse.json({ error: "Failed to update user." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { safeQuery } from "@/lib/database"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { nationalId } = await request.json()
|
||||
|
||||
if (!nationalId) {
|
||||
return NextResponse.json({ error: "Missing national id." }, { status: 400 })
|
||||
}
|
||||
|
||||
const users = (await safeQuery(
|
||||
"SELECT national_id, name, is_admin, role, field, department, team, in_shelter, last_updated, must_change_password FROM users WHERE national_id = ?",
|
||||
[nationalId],
|
||||
)) as any[]
|
||||
|
||||
if (users.length === 0) {
|
||||
return NextResponse.json({ error: "User not found." }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user: users[0] })
|
||||
} catch (error) {
|
||||
console.error("User fetch error:", error)
|
||||
return NextResponse.json({ error: "Failed to load user." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,26 @@ export default function DashboardPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const refreshUser = async (currentUser: User) => {
|
||||
try {
|
||||
const response = await fetch("/api/user", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ nationalId: currentUser.national_id }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (response.ok && data.user) {
|
||||
setUser(data.user)
|
||||
setSelectedStatus(data.user.in_shelter)
|
||||
setLastUpdated(data.user.last_updated)
|
||||
localStorage.setItem("user", JSON.stringify(data.user))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error refreshing user:", err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const userData = localStorage.getItem("user")
|
||||
if (!userData) {
|
||||
@@ -25,6 +45,17 @@ export default function DashboardPage() {
|
||||
setUser(parsedUser)
|
||||
setSelectedStatus(parsedUser.in_shelter)
|
||||
setLastUpdated(parsedUser.last_updated)
|
||||
|
||||
refreshUser(parsedUser)
|
||||
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === "visible" && parsedUser) {
|
||||
refreshUser(parsedUser)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("visibilitychange", handleVisibility)
|
||||
return () => window.removeEventListener("visibilitychange", handleVisibility)
|
||||
}, [router])
|
||||
|
||||
const handleStatusUpdate = async (status: string) => {
|
||||
|
||||
Reference in New Issue
Block a user