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
+483 -21
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)}
+45 -8
View File
@@ -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"]
+151
View File
@@ -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 })
}
}
+203
View File
@@ -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 })
}
}
+122
View File
@@ -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 })
}
}
+26
View File
@@ -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 })
}
}
+31
View File
@@ -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) => {