Initial commit

This commit is contained in:
2025-06-22 00:01:22 +03:00
parent fd70166cf6
commit 033b80bfad
153 changed files with 26874 additions and 1 deletions

1380
app/admin/page.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
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"
export async function POST(request: NextRequest) {
try {
const { name, isAdmin, field, department, team, role } = await request.json()
// Input validation
if (!name || !field || !department || !team) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Validate department, team, and field
if (!FIELDS.includes(field as any)) {
return NextResponse.json({ error: "תחום לא תקין" }, { status: 400 })
}
if (!DEPARTMENTS.includes(department as any)) {
return NextResponse.json({ error: "מסגרת לא תקינה" }, { status: 400 })
}
if (!TEAMS.includes(team as any)) {
return NextResponse.json({ error: "צוות לא תקין" }, { status: 400 })
}
const validRoles: UserRole[] = ["user", "team_admin", "department_admin", "field_admin", "global_admin"]
// Set role based on isAdmin flag or explicit role
const userRole: UserRole = (role as UserRole) || (isAdmin ? "global_admin" : "user")
if (!validRoles.includes(userRole)) {
return NextResponse.json({ error: "תפקיד לא תקין" }, { status: 400 })
}
// Generate unique Login ID
const { generateUniqueIsraeliID } = await import("@/lib/auth")
const nationalId = await generateUniqueIsraeliID()
// Hash default password "password123"
const hashedPassword = await hashPassword("password123")
await safeQuery(
"INSERT INTO users (national_id, password, name, is_admin, role, must_change_password, field, department, team) VALUES (?, ?, ?, ?, ?, TRUE, ?, ?, ?)",
[nationalId, hashedPassword, name, isAdmin, userRole, field, department, team],
)
return NextResponse.json({
success: true,
nationalId: nationalId,
message: `משתמש ${name} נוסף בהצלחה עם מזהה: ${nationalId}`,
})
} catch (error) {
console.error("Add user error:", error)
return NextResponse.json({ error: "שגיאה בהוספת משתמש" }, { status: 500 })
}
}

View File

@@ -0,0 +1,76 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { type UserRole, ROLE_HIERARCHY, ROLE_NAMES } from "@/types/user"
export async function POST(request: NextRequest) {
try {
const { adminId, targetUserId, newRole } = await request.json()
if (!adminId || !targetUserId || !newRole) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Get admin data
const adminData = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const admin = adminData[0]
const adminLevel = ROLE_HIERARCHY[admin.role as UserRole] || 0
// Get target user data
const targetData = (await safeQuery("SELECT role, field, department, team, name FROM users WHERE national_id = ?", [
targetUserId,
])) as any[]
if (targetData.length === 0) {
return NextResponse.json({ error: "משתמש לא נמצא" }, { status: 404 })
}
const target = targetData[0]
const newRoleLevel = ROLE_HIERARCHY[newRole as UserRole] || 0
// Check if admin has permission to change this role
if (adminLevel <= newRoleLevel) {
return NextResponse.json({ error: "אין הרשאה לתת תפקיד זה" }, { status: 403 })
}
// Check if admin can manage this user based on hierarchy
let canManage = false
if (admin.role === "global_admin") {
canManage = true
} else if (admin.role === "field_admin" && admin.field === target.field) {
canManage = true
} else if (admin.role === "department_admin" && admin.department === target.department) {
canManage = true
} else if (admin.role === "team_admin" && admin.team === target.team) {
canManage = true
}
if (!canManage) {
return NextResponse.json({ error: "אין הרשאה לנהל משתמש זה" }, { status: 403 })
}
// Update user role
await safeQuery("UPDATE users SET role = ? WHERE national_id = ?", [newRole, targetUserId])
// Log the action
await safeQuery(
'INSERT INTO admin_actions (admin_id, action_type, target_user_id, target_role) VALUES (?, "role_change", ?, ?)',
[adminId, targetUserId, newRole],
)
return NextResponse.json({
success: true,
message: `תפקיד ${target.name} שונה ל${ROLE_NAMES[newRole as UserRole]}`,
})
} catch (error) {
console.error("Change role error:", error)
return NextResponse.json({ error: "שגיאה בשינוי תפקיד" }, { status: 500 })
}
}

View File

@@ -0,0 +1,54 @@
import { NextResponse } from "next/server"
import { healthCheck, getPoolStats } from "@/lib/database"
export async function GET() {
try {
const health = await healthCheck()
const poolStats = getPoolStats()
return NextResponse.json({
...health,
poolStats,
recommendations: generateRecommendations(poolStats),
})
} catch (error) {
console.error("Database health check error:", error)
return NextResponse.json(
{
status: "error",
error: error instanceof Error ? error.message : "Unknown error",
timestamp: new Date().toISOString(),
},
{ status: 500 },
)
}
}
function generateRecommendations(stats: any) {
const recommendations = []
if (!stats) {
recommendations.push("Unable to get pool statistics")
return recommendations
}
const utilizationRate = (stats.totalConnections - stats.freeConnections) / stats.connectionLimit
if (utilizationRate > 0.8) {
recommendations.push("High connection utilization - consider increasing connection limit")
}
if (stats.acquiringConnections > 5) {
recommendations.push("Many connections waiting - possible connection leak or high load")
}
if (stats.freeConnections === 0) {
recommendations.push("No free connections available - increase pool size or check for connection leaks")
}
if (recommendations.length === 0) {
recommendations.push("Database pool is healthy")
}
return recommendations
}

View File

@@ -0,0 +1,79 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field and department
const adminData = (await safeQuery(
"SELECT field, department FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment } = adminData[0]
if (!adminField || !adminDepartment) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום ומסגרת" }, { status: 400 })
}
// Check cooldown for department resets
const lastReset = (await safeQuery(
'SELECT timestamp FROM admin_actions WHERE action_type = "reset_department" AND admin_id = ? ORDER BY timestamp DESC LIMIT 1',
[adminId],
)) as any[]
if (lastReset.length > 0) {
const lastResetTime = new Date(lastReset[0].timestamp).getTime()
const now = new Date().getTime()
const cooldownMs = 90 * 10 // 1.5 minutes for department resets
if (now - lastResetTime < cooldownMs) {
return NextResponse.json({ error: "יש להמתין 90 שניות בין איפוסי מסגרת" }, { status: 429 })
}
}
// Reset department users' statuses with field and department context, but exclude locked users
await safeQuery(
"UPDATE users SET in_shelter = NULL, last_updated = NULL WHERE field = ? AND department = ? AND lock_status = FALSE",
[adminField, adminDepartment],
)
// Get count of locked users that were skipped
const lockedUsers = (await safeQuery(
"SELECT COUNT(*) as count FROM users WHERE field = ? AND department = ? AND lock_status = TRUE",
[adminField, adminDepartment],
)) as any[]
const lockedCount = lockedUsers[0]?.count || 0
// Log the action
await safeQuery(
'INSERT INTO admin_actions (admin_id, action_type, target_user_id) VALUES (?, "reset_department", NULL)',
[adminId],
)
let message = `כל הסטטוסים של מסגרת ${adminDepartment} בתחום ${adminField} אופסו בהצלחה`
if (lockedCount > 0) {
message += ` (${lockedCount} משתמשים נעולים לא אופסו)`
}
return NextResponse.json({
success: true,
field: adminField,
department: adminDepartment,
message,
lockedCount,
})
} catch (error) {
console.error("Department reset error:", error)
return NextResponse.json({ error: "שגיאה באיפוס המסגרת" }, { status: 500 })
}
}

View File

@@ -0,0 +1,52 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: Request) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field and department
const adminData = (await executeQuery(
"SELECT field, department FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment } = adminData[0]
if (!adminField || !adminDepartment) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום ומסגרת" }, { status: 400 })
}
// Get department stats with field and department context
const results = (await executeQuery(
`
SELECT
SUM(CASE WHEN in_shelter IS NULL THEN 1 ELSE 0 END) as no_report,
SUM(CASE WHEN in_shelter = 'yes' THEN 1 ELSE 0 END) as in_shelter,
SUM(CASE WHEN in_shelter = 'no' THEN 1 ELSE 0 END) as not_in_shelter,
SUM(CASE WHEN in_shelter = 'no_alarm' THEN 1 ELSE 0 END) as no_alarm,
SUM(CASE WHEN in_shelter = 'safe_after_exit' THEN 1 ELSE 0 END) as safe_after_exit
FROM users
WHERE field = ? AND department = ?
`,
[adminField, adminDepartment],
)) as any[]
return NextResponse.json({
...results[0],
field: adminField,
department: adminDepartment,
})
} catch (error) {
console.error("Department stats error:", error)
return NextResponse.json({ error: "שגיאה בטעינת סטטיסטיקות המסגרת" }, { status: 500 })
}
}

View File

@@ -0,0 +1,62 @@
import { type NextRequest, NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId, category } = await request.json()
if (!adminId || !category) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Get admin's field and department
const adminData = (await executeQuery(
"SELECT field, department FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment } = adminData[0]
if (!adminField || !adminDepartment) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום ומסגרת" }, { status: 400 })
}
let query = ""
const params: any[] = [adminField, adminDepartment]
switch (category) {
case "no_report":
query =
"SELECT national_id, name, team FROM users WHERE field = ? AND department = ? AND in_shelter IS NULL ORDER BY team, name"
break
case "in_shelter":
query =
"SELECT national_id, name, team FROM users WHERE field = ? AND department = ? AND in_shelter = 'yes' ORDER BY team, name"
break
case "not_in_shelter":
query =
"SELECT national_id, name, team FROM users WHERE field = ? AND department = ? AND in_shelter = 'no' ORDER BY team, name"
break
case "no_alarm":
query =
"SELECT national_id, name, team FROM users WHERE field = ? AND department = ? AND in_shelter = 'no_alarm' ORDER BY team, name"
break
case "safe_after_exit":
query =
"SELECT national_id, name , team FROM users WHERE field = ? AND department = ? AND in_shelter = 'safe_after_exit' ORDER BY name"
break
default:
return NextResponse.json({ error: "קטגוריה לא תקינה" }, { status: 400 })
}
const users = (await executeQuery(query, params)) as any[]
return NextResponse.json(users)
} catch (error) {
console.error("Get department users by category error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשי המסגרת" }, { status: 500 })
}
}

View File

@@ -0,0 +1,58 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: Request) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field and department
const adminData = (await executeQuery(
"SELECT field, department FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment } = adminData[0]
if (!adminField || !adminDepartment) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום ומסגרת" }, { status: 400 })
}
// Get department users with field and department context
const users = (await executeQuery(
`
SELECT
national_id,
name,
in_shelter,
last_updated,
is_admin,
must_change_password,
field,
department,
team,
lock_status
FROM users
WHERE field = ? AND department = ?
ORDER BY team, name
`,
[adminField, adminDepartment],
)) as any[]
return NextResponse.json({
users,
field: adminField,
department: adminDepartment,
})
} catch (error) {
console.error("Department users error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשי המסגרת" }, { status: 500 })
}
}

View File

@@ -0,0 +1,61 @@
export const dynamic = 'force-dynamic';
import type { NextRequest } from "next/server"
// Store active connections
const connections = new Set<ReadableStreamDefaultController>()
export async function GET(request: NextRequest) {
const stream = new ReadableStream({
start(controller) {
connections.add(controller)
// Send initial connection message
controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}\n\n`)
// Keep connection alive with periodic pings
const pingInterval = setInterval(() => {
try {
controller.enqueue(`data: ${JSON.stringify({ type: "ping", timestamp: new Date().toISOString() })}\n\n`)
} catch (err) {
clearInterval(pingInterval)
connections.delete(controller)
}
}, 30000)
// Clean up on close
request.signal.addEventListener("abort", () => {
clearInterval(pingInterval)
connections.delete(controller)
try {
controller.close()
} catch (err) {
// Connection already closed
}
})
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control",
},
})
}
// Function to broadcast updates to all connected clients
export function broadcastUpdate(data: any) {
const message = `data: ${JSON.stringify({ type: "update", data, timestamp: new Date().toISOString() })}\n\n`
connections.forEach((controller) => {
try {
controller.enqueue(message)
} catch (err) {
connections.delete(controller)
}
})
}

View File

@@ -0,0 +1,70 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field
const adminData = (await safeQuery("SELECT field FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const adminField = adminData[0].field
if (!adminField) {
return NextResponse.json({ error: "למנהל לא הוגדר תחום" }, { status: 400 })
}
// Check cooldown for field resets
const lastReset = (await safeQuery(
'SELECT timestamp FROM admin_actions WHERE action_type = "reset_field" AND admin_id = ? ORDER BY timestamp DESC LIMIT 1',
[adminId],
)) as any[]
if (lastReset.length > 0) {
const lastResetTime = new Date(lastReset[0].timestamp).getTime()
const now = new Date().getTime()
const cooldownMs = 2 * 60 * 10 // 2 minutes for field resets
if (now - lastResetTime < cooldownMs) {
return NextResponse.json({ error: "יש להמתין 2 דקות בין איפוסי תחום" }, { status: 429 })
}
}
// Reset field users' statuses, but exclude locked users
await safeQuery("UPDATE users SET in_shelter = NULL, last_updated = NULL WHERE field = ? AND lock_status = FALSE", [
adminField,
])
// Get count of locked users that were skipped
const lockedUsers = (await safeQuery("SELECT COUNT(*) as count FROM users WHERE field = ? AND lock_status = TRUE", [
adminField,
])) as any[]
const lockedCount = lockedUsers[0]?.count || 0
// Log the action
await safeQuery(
'INSERT INTO admin_actions (admin_id, action_type, target_user_id) VALUES (?, "reset_field", NULL)',
[adminId],
)
let message = `כל הסטטוסים של תחום ${adminField} אופסו בהצלחה`
if (lockedCount > 0) {
message += ` (${lockedCount} משתמשים נעולים לא אופסו)`
}
return NextResponse.json({ success: true, field: adminField, message, lockedCount })
} catch (error) {
console.error("Field reset error:", error)
return NextResponse.json({ error: "שגיאה באיפוס התחום" }, { status: 500 })
}
}

View File

@@ -0,0 +1,50 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: Request) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field
const adminData = (await executeQuery("SELECT field FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const adminField = adminData[0].field
if (!adminField) {
return NextResponse.json({ error: "למנהל לא הוגדר תחום" }, { status: 400 })
}
// Get field stats
const results = (await executeQuery(
`
SELECT
SUM(CASE WHEN in_shelter IS NULL THEN 1 ELSE 0 END) as no_report,
SUM(CASE WHEN in_shelter = 'yes' THEN 1 ELSE 0 END) as in_shelter,
SUM(CASE WHEN in_shelter = 'no' THEN 1 ELSE 0 END) as not_in_shelter,
SUM(CASE WHEN in_shelter = 'no_alarm' THEN 1 ELSE 0 END) as no_alarm,
SUM(CASE WHEN in_shelter = 'safe_after_exit' THEN 1 ELSE 0 END) as safe_after_exit
FROM users
WHERE field = ?
`,
[adminField],
)) as any[]
return NextResponse.json({
...results[0],
field: adminField,
})
} catch (error) {
console.error("Field stats error:", error)
return NextResponse.json({ error: "שגיאה בטעינת סטטיסטיקות התחום" }, { status: 500 })
}
}

View File

@@ -0,0 +1,60 @@
import { type NextRequest, NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId, category } = await request.json()
if (!adminId || !category) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Get admin's field
const adminData = (await executeQuery("SELECT field FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const adminField = adminData[0].field
if (!adminField) {
return NextResponse.json({ error: "למנהל לא הוגדר תחום" }, { status: 400 })
}
let query = ""
const params: any[] = [adminField]
switch (category) {
case "no_report":
query =
"SELECT national_id, name, department, team FROM users WHERE field = ? AND in_shelter IS NULL ORDER BY department, team, name"
break
case "in_shelter":
query =
"SELECT national_id, name, department, team FROM users WHERE field = ? AND in_shelter = 'yes' ORDER BY department, team, name"
break
case "not_in_shelter":
query =
"SELECT national_id, name, department, team FROM users WHERE field = ? AND in_shelter = 'no' ORDER BY department, team, name"
break
case "no_alarm":
query =
"SELECT national_id, name, department, team FROM users WHERE field = ? AND in_shelter = 'no_alarm' ORDER BY department, team, name"
break
case "safe_after_exit":
query = "SELECT national_id, name, department, team FROM users WHERE in_shelter = 'safe_after_exit' ORDER BY name"
break
default:
return NextResponse.json({ error: "קטגוריה לא תקינה" }, { status: 400 })
}
const users = (await executeQuery(query, params)) as any[]
return NextResponse.json(users)
} catch (error) {
console.error("Get field users by category error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשי התחום" }, { status: 500 })
}
}

View File

@@ -0,0 +1,56 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: Request) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field
const adminData = (await executeQuery("SELECT field FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const adminField = adminData[0].field
if (!adminField) {
return NextResponse.json({ error: "למנהל לא הוגדר תחום" }, { status: 400 })
}
// Get field users
const users = (await executeQuery(
`
SELECT
national_id,
name,
in_shelter,
last_updated,
is_admin,
must_change_password,
field,
department,
team,
lock_status
FROM users
WHERE field = ?
ORDER BY department, team, name
`,
[adminField],
)) as any[]
return NextResponse.json({
users,
field: adminField,
})
} catch (error) {
console.error("Field users error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשי התחום" }, { status: 500 })
}
}

View File

@@ -0,0 +1,28 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function GET() {
try {
const results = (await executeQuery(`
SELECT aa.timestamp, u.name
FROM admin_actions aa
JOIN users u ON aa.admin_id = u.national_id
WHERE aa.action_type = 'reset_all'
ORDER BY aa.timestamp DESC
LIMIT 1
`)) as any[]
if (results.length > 0) {
const result = results[0]
return NextResponse.json({
lastReset: `${result.name} - ${new Date(result.timestamp).toLocaleString("he-IL")}`,
timestamp: result.timestamp,
})
}
return NextResponse.json({ lastReset: null })
} catch (error) {
console.error("Last reset error:", error)
return NextResponse.json({ error: "שגיאה בטעינת נתונים" }, { status: 500 })
}
}

View File

@@ -0,0 +1,122 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin data
const adminData = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const admin = adminData[0]
let query = ""
let params: any[] = []
// Build query based on admin role
if (admin.role === "global_admin") {
query = `
SELECT
national_id,
name,
role,
field,
department,
team,
in_shelter,
last_updated,
lock_status,
is_admin,
must_change_password
FROM users
ORDER BY field, department, team, name
`
} else if (admin.role === "field_admin") {
query = `
SELECT
national_id,
name,
role,
field,
department,
team,
in_shelter,
last_updated,
lock_status,
is_admin,
must_change_password
FROM users
WHERE field = ?
ORDER BY department, team, name
`
params = [admin.field]
} else if (admin.role === "department_admin") {
query = `
SELECT
national_id,
name,
role,
field,
department,
team,
in_shelter,
last_updated,
lock_status,
is_admin,
must_change_password
FROM users
WHERE department = ?
ORDER BY team, name
`
params = [admin.department]
} else if (admin.role === "team_admin") {
// Team admins can only manage their own team members
query = `
SELECT
national_id,
name,
role,
field,
department,
team,
in_shelter,
last_updated,
lock_status,
is_admin,
must_change_password
FROM users
WHERE team = ? AND role = 'user'
ORDER BY name
`
params = [admin.team]
} else {
return NextResponse.json({ error: "אין הרשאות ניהול" }, { status: 403 })
}
const users = (await safeQuery(query, params)) as any[]
console.log("Manageable users query result:", users) // Debug log
return NextResponse.json({
users,
adminRole: admin.role,
scope: {
field: admin.field,
department: admin.department,
team: admin.team,
},
})
} catch (error) {
console.error("Get manageable users error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשים" }, { status: 500 })
}
}

View File

@@ -0,0 +1,86 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId, targetUserId, status } = await request.json()
if (!adminId || !targetUserId || !status) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
if (!["yes", "no", "no_alarm", "safe_after_exit"].includes(status)) {
return NextResponse.json({ error: "סטטוס לא תקין" }, { status: 400 })
}
// Get admin data
const adminData = (await safeQuery("SELECT role, field, department, team, name FROM users WHERE national_id = ?", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const admin = adminData[0]
// Get target user data
const targetData = (await safeQuery("SELECT field, department, team, name FROM users WHERE national_id = ?", [
targetUserId,
])) as any[]
if (targetData.length === 0) {
return NextResponse.json({ error: "משתמש לא נמצא" }, { status: 404 })
}
const target = targetData[0]
// Check if admin can manage this user based on hierarchy
let canManage = false
if (admin.role === "global_admin") {
canManage = true
} else if (admin.role === "field_admin" && admin.field === target.field) {
canManage = true
} else if (admin.role === "department_admin" && admin.department === target.department) {
canManage = true
} else if (admin.role === "team_admin" && admin.team === target.team) {
canManage = true
}
if (!canManage) {
return NextResponse.json({ error: "אין הרשאה לדווח עבור משתמש זה" }, { status: 403 })
}
// Update user status
await safeQuery("UPDATE users SET in_shelter = ?, last_updated = NOW() WHERE national_id = ?", [
status,
targetUserId,
])
// Log the action
await safeQuery(
'INSERT INTO admin_actions (admin_id, action_type, target_user_id) VALUES (?, "report_on_behalf", ?)',
[adminId, targetUserId],
)
const statusText =
status === "yes"
? "במקלט/חדר מוגן"
: status === "no"
? "לא במקלט"
: status === "no_alarm"
? "אין אזעקה"
: status === "safe_after_exit"
? "בטוח אחרי יציאה מהמקלט"
: "לא ידוע"
return NextResponse.json({
success: true,
message: `דווח עבור ${target.name}: ${statusText}`,
})
} catch (error) {
console.error("Report on behalf error:", error)
return NextResponse.json({ error: "שגיאה בדיווח" }, { status: 500 })
}
}

View File

@@ -0,0 +1,72 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId } = await request.json()
// Check cooldown
const lastReset = (await safeQuery(
'SELECT timestamp FROM admin_actions WHERE action_type = "reset_all" ORDER BY timestamp DESC LIMIT 1',
)) as any[]
if (lastReset.length > 0) {
const lastResetTime = new Date(lastReset[0].timestamp).getTime()
const now = new Date().getTime()
const cooldownMs = 2 * 60 * 1000 // 2 minutes in milliseconds
const timeSinceReset = now - lastResetTime
console.log("Reset cooldown check:", {
lastResetTime: new Date(lastResetTime).toISOString(),
now: new Date(now).toISOString(),
timeSinceReset: timeSinceReset,
cooldownMs: cooldownMs,
remainingMs: cooldownMs - timeSinceReset,
})
if (timeSinceReset < cooldownMs) {
const remainingSeconds = Math.ceil((cooldownMs - timeSinceReset) / 1000)
return NextResponse.json(
{
error: `יש להמתין ${remainingSeconds} שניות בין איפוסים`,
remainingSeconds,
cooldownMs,
},
{ status: 429 },
)
}
}
// Reset ALL users' statuses including admins, but exclude locked users
const result = await safeQuery("UPDATE users SET in_shelter = NULL, last_updated = NULL WHERE lock_status = FALSE")
// Get count of locked users that were skipped
const lockedUsers = (await safeQuery("SELECT COUNT(*) as count FROM users WHERE lock_status = TRUE")) as any[]
const lockedCount = lockedUsers[0]?.count || 0
// Log the action
await safeQuery('INSERT INTO admin_actions (admin_id, action_type) VALUES (?, "reset_all")', [adminId])
let message = "כל הסטטוסים אופסו בהצלחה"
if (lockedCount > 0) {
message += ` (${lockedCount} משתמשים נעולים לא אופסו)`
}
console.log("Reset completed:", {
affectedRows: (result as any).affectedRows,
lockedCount,
adminId,
})
return NextResponse.json({
success: true,
message,
lockedCount,
affectedRows: (result as any).affectedRows,
})
} catch (error) {
console.error("Reset error:", error)
return NextResponse.json({ error: "שגיאה באיפוס" }, { status: 500 })
}
}

View File

@@ -0,0 +1,56 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { hashPassword } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const { adminId, targetUserId } = await request.json()
// Input validation
if (!adminId || !targetUserId) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Verify admin permissions
const admins = await safeQuery(
"SELECT role FROM users WHERE national_id = ?",
[adminId]
) as { role: string | null }[]
if (
admins.length === 0 ||
!admins[0].role ||
admins[0].role === "user"
) {
return NextResponse.json({ error: "אין הרשאות מנהל" }, { status: 403 })
}
// Check if target user exists
const targetUsers = (await safeQuery("SELECT national_id FROM users WHERE national_id = ?", [
targetUserId,
])) as any[]
if (targetUsers.length === 0) {
return NextResponse.json({ error: "משתמש לא נמצא" }, { status: 404 })
}
// Reset password to "password123"
const hashedPassword = await hashPassword("password123")
await safeQuery(
"UPDATE users SET password = ?, must_change_password = TRUE, password_changed_at = NULL WHERE national_id = ?",
[hashedPassword, targetUserId],
)
// Log the action
await safeQuery(
'INSERT INTO admin_actions (admin_id, action_type, target_user_id) VALUES (?, "reset_password", ?)',
[adminId, targetUserId],
)
return NextResponse.json({ success: true })
} catch (error) {
console.error("Reset password error:", error)
return NextResponse.json({ error: "שגיאה באיפוס סיסמה" }, { status: 500 })
}
}

View File

@@ -0,0 +1,21 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function GET() {
try {
const results = (await executeQuery(`
SELECT
SUM(CASE WHEN in_shelter IS NULL THEN 1 ELSE 0 END) as no_report,
SUM(CASE WHEN in_shelter = 'yes' THEN 1 ELSE 0 END) as in_shelter,
SUM(CASE WHEN in_shelter = 'no' THEN 1 ELSE 0 END) as not_in_shelter,
SUM(CASE WHEN in_shelter = 'no_alarm' THEN 1 ELSE 0 END) as no_alarm,
SUM(CASE WHEN in_shelter = 'safe_after_exit' THEN 1 ELSE 0 END) as safe_after_exit
FROM users
`)) as any[]
return NextResponse.json(results[0])
} catch (error) {
console.error("Stats error:", error)
return NextResponse.json({ error: "שגיאה בטעינת סטטיסטיקות" }, { status: 500 })
}
}

View File

@@ -0,0 +1,66 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { revalidatePath } from 'next/cache' // Crucial for Next.js App Router caching
export async function POST(request: NextRequest) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field, department, and team
const adminData = (await safeQuery(
"SELECT field, department, team FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment, team: adminTeam } = adminData[0]
if (!adminField || !adminDepartment || !adminTeam) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום, מסגרת וצוות" }, { status: 400 })
}
await safeQuery(
"UPDATE users SET in_shelter = NULL, last_updated = NULL WHERE field = ? AND department = ? AND team = ? AND lock_status = FALSE",
[adminField, adminDepartment, adminTeam],
)
// Get count of locked users that were skipped
const lockedUsers = (await safeQuery(
"SELECT COUNT(*) as count FROM users WHERE field = ? AND department = ? AND team = ? AND lock_status = TRUE",
[adminField, adminDepartment, adminTeam],
)) as any[]
const lockedCount = lockedUsers[0]?.count || 0
// Log the action
await safeQuery(
'INSERT INTO admin_actions (admin_id, action_type, target_user_id) VALUES (?, "reset_team", NULL)',
[adminId],
)
revalidatePath('/admin')
let message = `כל הסטטוסים של צוות ${adminTeam} במסגרת ${adminDepartment} בתחום ${adminField} אופסו בהצלחה`
if (lockedCount > 0) {
message += ` (${lockedCount} משתמשים נעולים לא אופסו)`
}
return NextResponse.json({
success: true,
field: adminField,
department: adminDepartment,
team: adminTeam,
message,
lockedCount,
}, { status: 200 })
} catch (error) {
console.error("Team reset error:", error)
return NextResponse.json({ error: "שגיאה באיפוס הצוות" }, { status: 500 })
}
}

View File

@@ -0,0 +1,53 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: Request) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field, department, and team
const adminData = (await executeQuery(
"SELECT field, department, team FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment, team: adminTeam } = adminData[0]
if (!adminField || !adminDepartment || !adminTeam) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום, מסגרת וצוות" }, { status: 400 })
}
// Get team stats with full context (field + department + team)
const results = (await executeQuery(
`
SELECT
SUM(CASE WHEN in_shelter IS NULL THEN 1 ELSE 0 END) as no_report,
SUM(CASE WHEN in_shelter = 'yes' THEN 1 ELSE 0 END) as in_shelter,
SUM(CASE WHEN in_shelter = 'no' THEN 1 ELSE 0 END) as not_in_shelter,
SUM(CASE WHEN in_shelter = 'no_alarm' THEN 1 ELSE 0 END) as no_alarm,
SUM(CASE WHEN in_shelter = 'safe_after_exit' THEN 1 ELSE 0 END) as safe_after_exit
FROM users
WHERE field = ? AND department = ? AND team = ?
`,
[adminField, adminDepartment, adminTeam],
)) as any[]
return NextResponse.json({
...results[0],
field: adminField,
department: adminDepartment,
team: adminTeam,
})
} catch (error) {
console.error("Team stats error:", error)
return NextResponse.json({ error: "שגיאה בטעינת סטטיסטיקות הצוות" }, { status: 500 })
}
}

View File

@@ -0,0 +1,62 @@
import { type NextRequest, NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId, category } = await request.json()
if (!adminId || !category) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Get admin's field, department, and team
const adminData = (await executeQuery(
"SELECT field, department, team FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'",
[adminId],
)) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment, team: adminTeam } = adminData[0]
if (!adminField || !adminDepartment || !adminTeam) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום, מסגרת וצוות" }, { status: 400 })
}
let query = ""
const params: any[] = [adminField, adminDepartment, adminTeam]
switch (category) {
case "no_report":
query =
"SELECT national_id, name FROM users WHERE field = ? AND department = ? AND team = ? AND in_shelter IS NULL ORDER BY name"
break
case "in_shelter":
query =
"SELECT national_id, name FROM users WHERE field = ? AND department = ? AND team = ? AND in_shelter = 'yes' ORDER BY name"
break
case "not_in_shelter":
query =
"SELECT national_id, name FROM users WHERE field = ? AND department = ? AND team = ? AND in_shelter = 'no' ORDER BY name"
break
case "no_alarm":
query =
"SELECT national_id, name FROM users WHERE field = ? AND department = ? AND team = ? AND in_shelter = 'no_alarm' ORDER BY name"
break
case "safe_after_exit":
query =
"SELECT national_id, name FROM users WHERE field = ? AND department = ? AND team = ? AND in_shelter = 'safe_after_exit' ORDER BY name"
break
default:
return NextResponse.json({ error: "קטגוריה לא תקינה" }, { status: 400 })
}
const users = (await executeQuery(query, params)) as any[]
return NextResponse.json(users)
} catch (error) {
console.error("Get team users by category error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשי הצוות" }, { status: 500 })
}
}

View File

@@ -0,0 +1,58 @@
import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function POST(request: Request) {
try {
const { adminId } = await request.json()
if (!adminId) {
return NextResponse.json({ error: "מזהה מנהל חסר" }, { status: 400 })
}
// Get admin's field, department, and team
const adminData = (await executeQuery("SELECT field, department, team FROM users WHERE national_id = ? AND role IS NOT NULL AND role != 'user'", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const { field: adminField, department: adminDepartment, team: adminTeam } = adminData[0]
if (!adminField || !adminDepartment || !adminTeam) {
return NextResponse.json({ error: "למנהל לא הוגדרו תחום, מסגרת וצוות" }, { status: 400 })
}
// Get team users with full context (field + department + team)
const users = (await executeQuery(
`
SELECT
national_id,
name,
in_shelter,
last_updated,
is_admin,
must_change_password,
field,
department,
team,
lock_status
FROM users
WHERE field = ? AND department = ? AND team = ?
ORDER BY name
`,
[adminField, adminDepartment, adminTeam],
)) as any[]
return NextResponse.json({
users,
field: adminField,
department: adminDepartment,
team: adminTeam,
})
} catch (error) {
console.error("Team users error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשי הצוות" }, { status: 500 })
}
}

View File

@@ -0,0 +1,74 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { adminId, targetUserId, lockStatus } = await request.json()
if (!adminId || !targetUserId || typeof lockStatus !== "boolean") {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Get admin data
const adminData = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
adminId,
])) as any[]
if (adminData.length === 0) {
return NextResponse.json({ error: "מנהל לא נמצא" }, { status: 404 })
}
const admin = adminData[0]
// Get target user data
const targetData = (await safeQuery(
"SELECT field, department, team, name, lock_status FROM users WHERE national_id = ?",
[targetUserId],
)) as any[]
if (targetData.length === 0) {
return NextResponse.json({ error: "משתמש לא נמצא" }, { status: 404 })
}
const target = targetData[0]
// Check if admin can manage this user based on hierarchy
let canManage = false
if (admin.role === "global_admin") {
canManage = true
} else if (admin.role === "field_admin" && admin.field === target.field) {
canManage = true
} else if (admin.role === "department_admin" && admin.department === target.department) {
canManage = true
} else if (admin.role === "team_admin" && admin.team === target.team) {
canManage = true
}
if (!canManage) {
return NextResponse.json({ error: "אין הרשאה לנעול/לבטל נעילה של משתמש זה" }, { status: 403 })
}
// Update user lock status
await safeQuery("UPDATE users SET lock_status = ? WHERE national_id = ?", [lockStatus, targetUserId])
// Log the action
const actionType = lockStatus ? "lock_user" : "unlock_user"
await safeQuery("INSERT INTO admin_actions (admin_id, action_type, target_user_id) VALUES (?, ?, ?)", [
adminId,
actionType,
targetUserId,
])
const statusText = lockStatus ? "נעול" : "לא נעול"
return NextResponse.json({
success: true,
message: `סטטוס נעילה של ${target.name} שונה ל: ${statusText}`,
lockStatus,
})
} catch (error) {
console.error("Toggle user lock error:", error)
return NextResponse.json({ error: "שגיאה בשינוי סטטוס נעילה" }, { status: 500 })
}
}

View File

@@ -0,0 +1,45 @@
import { type NextRequest, NextResponse } from "next/server";
import { executeQuery } from "@/lib/database";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
let query = "";
// No params needed for these queries, as there are no WHERE conditions
// that use parameters other than the in_shelter status itself.
let params: any[] = [];
switch (category) {
case "no_report":
// Added 'department', 'team', 'field' to SELECT clause
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter IS NULL ORDER BY name";
break;
case "in_shelter":
// Added 'department', 'team', 'field' to SELECT clause
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'yes' ORDER BY name";
break;
case "not_in_shelter":
// Added 'department', 'team', 'field' to SELECT clause
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'no' ORDER BY name";
break;
case "no_alarm":
// Added 'department', 'team', 'field' to SELECT clause
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'no_alarm' ORDER BY name";
break;
case "safe_after_exit":
// Added 'department', 'team', 'field' to SELECT clause
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'safe_after_exit' ORDER BY name";
break;
default:
return NextResponse.json({ error: "קטגוריה לא תקינה" }, { status: 400 });
}
const users = (await executeQuery(query, params)) as any[];
return NextResponse.json(users);
} catch (error) {
console.error("Get users by category error:", error);
return NextResponse.json({ error: "שגיאה בטעינת משתמשים" }, { status: 500 });
}
}

View File

@@ -0,0 +1,27 @@
import { type NextRequest, NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
try {
const nationalId = params.id
// Check if user exists
const users = (await executeQuery("SELECT national_id FROM users WHERE national_id = ?", [nationalId])) as any[]
if (users.length === 0) {
return NextResponse.json({ error: "משתמש לא נמצא" }, { status: 404 })
}
// Delete user
await executeQuery(
"DELETE FROM admin_actions WHERE admin_id = ? OR target_user_id = ?",
[nationalId, nationalId]
);
await executeQuery("DELETE FROM users WHERE national_id = ?", [nationalId])
return NextResponse.json({ success: true })
} catch (error) {
console.error("Delete user error:", error)
return NextResponse.json({ error: "שגיאה במחיקת משתמש" }, { status: 500 })
}
}

View File

@@ -0,0 +1,27 @@
import { NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function GET() {
try {
const users = (await safeQuery(`
SELECT
national_id,
name,
in_shelter,
last_updated,
is_admin,
must_change_password,
field,
department,
team,
lock_status
FROM users
ORDER BY field, department, team, name
`)) as any[]
return NextResponse.json(users)
} catch (error) {
console.error("Get users error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשים" }, { status: 500 })
}
}

View File

@@ -0,0 +1,45 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { verifyPassword, hashPassword } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const { nationalId, currentPassword, newPassword } = await request.json()
// Input validation
if (!nationalId || !currentPassword || !newPassword) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
if (newPassword.length < 6) {
return NextResponse.json({ error: "הסיסמה החדשה חייבת להכיל לפחות 6 תווים" }, { status: 400 })
}
// Get current user data
const users = (await safeQuery("SELECT password FROM users WHERE national_id = ?", [nationalId])) as any[]
if (users.length === 0) {
return NextResponse.json({ error: "משתמש לא נמצא" }, { status: 404 })
}
const user = users[0]
const isValidCurrentPassword = await verifyPassword(currentPassword, user.password)
if (!isValidCurrentPassword) {
return NextResponse.json({ error: "הסיסמה הנוכחית שגויה" }, { status: 401 })
}
// Hash new password and update
const hashedNewPassword = await hashPassword(newPassword)
await safeQuery(
"UPDATE users SET password = ?, must_change_password = FALSE, password_changed_at = NOW() WHERE national_id = ?",
[hashedNewPassword, nationalId],
)
return NextResponse.json({ success: true })
} catch (error) {
console.error("Change password error:", error)
return NextResponse.json({ error: "שגיאה בשינוי סיסמה" }, { status: 500 })
}
}

View File

@@ -0,0 +1,53 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { validateIsraeliID, verifyPassword } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const { nationalId, password } = await request.json()
// Input validation
if (!nationalId || !password) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
if (!validateIsraeliID(nationalId)) {
return NextResponse.json({ error: "מספר תעודת זהות לא תקין" }, { status: 400 })
}
// Use parameterized query to prevent SQL injection
const users = (await safeQuery(
"SELECT national_id, password, 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: "משתמש לא נמצא" }, { status: 401 })
}
const user = users[0]
const isValidPassword = await verifyPassword(password, user.password)
if (!isValidPassword) {
return NextResponse.json({ error: "סיסמה שגויה" }, { status: 401 })
}
return NextResponse.json({
user: {
national_id: user.national_id,
name: user.name,
is_admin: user.is_admin,
role: user.role,
field: user.field,
department: user.department,
team: user.team,
in_shelter: user.in_shelter,
last_updated: user.last_updated,
must_change_password: user.must_change_password,
},
})
} catch (error) {
console.error("Login error:", error)
return NextResponse.json({ error: "שגיאה בשרת" }, { status: 500 })
}
}

View File

@@ -0,0 +1,28 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { broadcastUpdate } from "@/lib/websocket"
export async function POST(request: NextRequest) {
try {
const { nationalId, status } = await request.json()
if (!["yes", "no", "no_alarm", "safe_after_exit"].includes(status)) {
return NextResponse.json({ error: "סטטוס לא תקין" }, { status: 400 })
}
await safeQuery("UPDATE users SET in_shelter = ?, last_updated = NOW() WHERE national_id = ?", [status, nationalId])
// Broadcast the update to all connected admins
broadcastUpdate({
type: "status_change",
user_id: nationalId,
status: status,
timestamp: new Date().toISOString(),
})
return NextResponse.json({ success: true })
} catch (error) {
console.error("Status update error:", error)
return NextResponse.json({ error: "שגיאה בעדכון סטטוס" }, { status: 500 })
}
}

45
app/api/test-db/route.ts Normal file
View File

@@ -0,0 +1,45 @@
import { NextResponse } from "next/server"
import { testConnection, executeQuery, getPoolStats, healthCheck } from "@/lib/database"
export async function GET() {
try {
// Test basic connection
const connectionTest = await testConnection()
if (!connectionTest) {
return NextResponse.json(
{
error: "Database connection failed",
details: "Could not connect to MySQL database",
},
{ status: 500 },
)
}
// Test query execution with automatic connection management
const result = await executeQuery("SELECT COUNT(*) as user_count FROM users")
// Get pool statistics
const poolStats = getPoolStats()
// Get comprehensive health check
const health = await healthCheck()
return NextResponse.json({
success: true,
message: "Database connection successful",
userCount: (result as any)[0].user_count,
poolStats,
health,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error("Database test error:", error)
return NextResponse.json(
{
error: "Database test failed",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
)
}
}

View File

@@ -0,0 +1,6 @@
import type { NextRequest } from "next/server"
export async function GET(request: NextRequest) {
// This will be handled by the custom server
return new Response("WebSocket endpoint", { status: 200 })
}

BIN
app/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

View File

@@ -0,0 +1,137 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
export default function ChangePasswordPage() {
const [currentPassword, setCurrentPassword] = useState("")
const [newPassword, setNewPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const [user, setUser] = useState<any>(null)
const router = useRouter()
useEffect(() => {
const userData = localStorage.getItem("user")
if (!userData) {
router.push("/login")
return
}
setUser(JSON.parse(userData))
}, [router])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError("")
if (newPassword !== confirmPassword) {
setError("הסיסמאות החדשות אינן תואמות")
setLoading(false)
return
}
if (newPassword.length < 6) {
setError("הסיסמה החדשה חייבת להכיל לפחות 6 תווים")
setLoading(false)
return
}
try {
const response = await fetch("/api/auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nationalId: user.national_id,
currentPassword,
newPassword,
}),
})
const data = await response.json()
if (response.ok) {
// Update user data
const updatedUser = { ...user, must_change_password: false }
localStorage.setItem("user", JSON.stringify(updatedUser))
router.push("/dashboard")
} else {
setError(data.error || "שגיאה בשינוי סיסמה")
}
} catch (err) {
setError("שגיאה בחיבור לשרת")
} finally {
setLoading(false)
}
}
if (!user) return null
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50" dir="rtl">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">שינוי סיסמה</CardTitle>
<CardDescription>
{user.must_change_password ? "הסיסמה אופסה או לא שונתה, ויש לשנותה על מנת להמשיך." : "שינוי סיסמה"}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword">סיסמה נוכחית</Label>
<Input
id="currentPassword"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="הזן סיסמה נוכחית"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="newPassword">סיסמה חדשה</Label>
<Input
id="newPassword"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="הזן סיסמה חדשה (לפחות 6 תווים)"
required
minLength={6}
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">אישור סיסמה חדשה</Label>
<Input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="הזן שוב את הסיסמה החדשה"
required
minLength={6}
/>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "משנה סיסמה..." : "שנה סיסמה"}
</Button>
</form>
</CardContent>
</Card>
</div>
)
}

176
app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,176 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Shield, ShieldAlert, ShieldX, Settings, LogOut, Users, ShieldCheck, Home, Sun, Smile } from "lucide-react"
import { type User, ROLE_NAMES } from "@/types/user"
export default function DashboardPage() {
const [user, setUser] = useState<User | null>(null)
const [selectedStatus, setSelectedStatus] = useState<string | null>(null)
const [lastUpdated, setLastUpdated] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const router = useRouter()
useEffect(() => {
const userData = localStorage.getItem("user")
if (!userData) {
router.push("/login")
return
}
const parsedUser = JSON.parse(userData)
setUser(parsedUser)
setSelectedStatus(parsedUser.in_shelter)
setLastUpdated(parsedUser.last_updated)
}, [router])
const handleStatusUpdate = async (status: string) => {
if (!user) return
setLoading(true)
try {
const response = await fetch("/api/status/update", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nationalId: user.national_id,
status,
}),
})
if (response.ok) {
setSelectedStatus(status)
setLastUpdated(new Date().toLocaleString("he-IL"))
}
} catch (err) {
console.error("Error updating status:", err)
} finally {
setLoading(false)
}
}
const handleLogout = () => {
localStorage.removeItem("user")
router.push("/login")
}
const canAccessAdmin = () => {
return !!user?.role && user.role !== "user"
}
if (!user) return null
const getStatusText = (status: string) => {
switch (status) {
case "yes":
return "במקלט/חדר מוגן"
case "no":
return "לא במקלט - אין מקלט בקרבת מקום"
case "no_alarm":
return "אין אזעקה באזור שלי"
case "safe_after_exit":
return "אני בטוח (אחרי יציאה מהמקלט)"
default:
return ""
}
}
return (
<div className="min-h-screen bg-gray-50 p-4" dir="rtl">
<div className="max-w-md mx-auto space-y-6">
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">שלום {user.name}</CardTitle>
<div className="text-sm text-gray-600 mb-2">
{ROLE_NAMES[user.role] || user.role}
{user.field && ` - תחום ${user.field}`}
{user.department && ` - מסגרת ${user.department}`}
{user.team && ` - צוות ${user.team}`}
</div>
<div className="flex justify-between items-center">
<Button variant="outline" size="sm" onClick={handleLogout} className="flex items-center gap-2">
<LogOut className="h-4 w-4" />
התנתקות
</Button>
{canAccessAdmin() && (
<Button
variant="outline"
size="sm"
onClick={() => router.push("/admin")}
className="flex items-center gap-2"
>
{user.role === "user" ? <Settings className="h-4 w-4" /> : <Users className="h-4 w-4" />}
{user.role === "user" ? "ניהול" : "ניהול"}
</Button>
)}
</div>
</CardHeader>
</Card>
<div className="space-y-4">
<Button
variant={selectedStatus === "safe_after_exit" ? "default" : "outline"}
className={`w-full h-16 text-lg ${selectedStatus === "safe_after_exit" ? "bg-green-600 hover:bg-green-700" : ""}`}
onClick={() => handleStatusUpdate("safe_after_exit")}
disabled={loading}
>
<Shield className="ml-2 h-6 w-6" />
אני בטוח.ה (אחרי יציאה מהמקלט)
</Button>
<Button
variant={selectedStatus === "yes" ? "default" : "outline"}
className={`w-full h-20 text-xl ${selectedStatus === "yes" ? "bg-green-600 hover:bg-green-700" : ""}`}
onClick={() => handleStatusUpdate("yes")}
disabled={loading}
>
<ShieldCheck className="ml-2 h-8 w-8" />
במקלט/מרחב מוגן
</Button>
<Button
variant={selectedStatus === "no" ? "default" : "outline"}
className={`w-full h-16 text-lg ${selectedStatus === "no" ? "bg-green-600 hover:bg-green-700" : ""}`}
onClick={() => handleStatusUpdate("no")}
disabled={loading}
>
<ShieldX className="ml-2 h-6 w-6" />
לא במקלט - אין מקלט בקרבת מקום
</Button>
<Button
variant={selectedStatus === "no_alarm" ? "default" : "outline"}
className={`w-full h-16 text-lg ${selectedStatus === "no_alarm" ? "bg-green-600 hover:bg-green-700" : ""}`}
onClick={() => handleStatusUpdate("no_alarm")}
disabled={loading}
>
<Smile className="ml-2 h-6 w-6" />
אין אזעקה באזור שלי
</Button>
</div>
{selectedStatus && lastUpdated && (
<Card>
<CardContent className="pt-6 text-center">
<p className="text-sm text-gray-600">סטטוס נוכחי:</p>
<p className="font-semibold text-green-600">{getStatusText(selectedStatus)}</p>
<p className="text-xs text-gray-500 mt-2">עודכן: {lastUpdated}</p>
</CardContent>
</Card>
)}
{/* Hostname Footer */}
<Card className="mt-8">
<CardContent className="py-3">
<div className="text-center text-xs text-gray-500">
סביבה: {process.env.NEXT_PUBLIC_HOSTNAME || process.env.HOSTNAME || "לא זוהה"}
<br />
2025 COPYRIGHT TR-WEB
</div>
</CardContent>
</Card>
</div>
</div>
)
}

94
app/globals.css Normal file
View File

@@ -0,0 +1,94 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

BIN
app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

28
app/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: 'ממ"ד-אפ',
manifest: '/manifest.webmanifest',
description: 'דיווחים ומידע על מקלטים באזור שלך',
icons: {
icon: '/icon.png',
apple: '/apple-icon.png',
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="he" dir="rtl">
<body className={inter.className}>{children}</body>
</html>
)
}

100
app/login/page.tsx Normal file
View File

@@ -0,0 +1,100 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
export default function LoginPage() {
const [nationalId, setNationalId] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError("")
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nationalId, password }),
})
const data = await response.json()
if (response.ok) {
localStorage.setItem("user", JSON.stringify(data.user))
// Check if user must change password
if (data.user.must_change_password) {
router.push("/change-password")
} else {
router.push("/dashboard")
}
} else {
setError(data.error || "שגיאה בהתחברות")
}
} catch (err) {
setError("שגיאה בחיבור לשרת")
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50" dir="rtl">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">התחברות לממ"ד-אפ</CardTitle>
<CardDescription>יש להזדהות על מנת להמשיך.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="nationalId">מזהה משתמש</Label>
<Input
id="nationalId"
type="text"
value={nationalId}
onChange={(e) => setNationalId(e.target.value)}
placeholder="9 ספרות"
maxLength={9}
required
className="text-right"
dir="rtl"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">סיסמה</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="הזינו סיסמה"
required
/>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "מתחבר..." : "התחברו"}
</Button>
</form>
</CardContent>
</Card>
</div>
)
}

23
app/manifest.webmanifest Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "ממ\"ד-אפ",
"short_name": "ממ\"ד",
"description": "דיווחים בשעת חירום",
"start_url": "/",
"display": "standalone",
"background_color": "#f9fafb",
"theme_color": "#f9fafb",
"icons": [
{
"src": "/icon.png",
"sizes": "1024x1024",
"type": "image/png"
},
{
"src": "/apple-icon.png",
"sizes": "1024x1024",
"type": "image/png",
"purpose": "any"
}
],
"lang": "he"
}

25
app/page.tsx Normal file
View File

@@ -0,0 +1,25 @@
"use client"
import { useEffect } from "react"
import { useRouter } from "next/navigation"
export default function HomePage() {
const router = useRouter()
useEffect(() => {
const user = localStorage.getItem("user")
if (user) {
router.push("/dashboard")
} else {
router.push("/login")
}
}, [router])
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">טוען...</h1>
</div>
</div>
)
}

409
app/role-admin/page.tsx Normal file
View File

@@ -0,0 +1,409 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { ArrowRight, Users, UserCog, MessageSquare, KeyRound, Trash2, Home } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { RoleManagementModal } from "@/components/role-management-modal"
import { ReportOnBehalfModal } from "@/components/report-on-behalf-modal"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { type User, type UserRole, ROLE_NAMES } from "@/types/user"
interface AdminUser {
national_id: string
name: string
role: UserRole
field?: string
department?: string
team?: string
}
export default function RoleAdminPage() {
const [admin, setAdmin] = useState<AdminUser | null>(null)
const [users, setUsers] = useState<User[]>([])
const [filteredUsers, setFilteredUsers] = useState<User[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [message, setMessage] = useState("")
const [loading, setLoading] = useState(false)
const [roleModalOpen, setRoleModalOpen] = useState(false)
const [reportModalOpen, setReportModalOpen] = useState(false)
const [selectedUser, setSelectedUser] = useState<User | null>(null)
const router = useRouter()
useEffect(() => {
const userData = localStorage.getItem("user")
if (!userData) {
router.push("/login")
return
}
const parsedUser = JSON.parse(userData)
if (parsedUser.role === "user") {
router.push("/dashboard")
return
}
setAdmin(parsedUser)
fetchManageableUsers(parsedUser.national_id)
}, [router])
useEffect(() => {
if (searchTerm) {
setFilteredUsers(users.filter((user) => user.name.includes(searchTerm)))
} else {
setFilteredUsers(users)
}
}, [searchTerm, users])
const fetchManageableUsers = async (adminId: string) => {
setLoading(true)
try {
const response = await fetch("/api/admin/manageable-users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ adminId }),
})
const data = await response.json()
if (response.ok) {
setUsers(data.users)
} else {
setMessage(data.error || "שגיאה בטעינת משתמשים")
}
} catch (err) {
setMessage("שגיאה בחיבור לשרת")
} finally {
setLoading(false)
}
}
const handleRoleChange = async (userId: string, newRole: string) => {
try {
const response = await fetch("/api/admin/change-role", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
adminId: admin?.national_id,
targetUserId: userId,
newRole,
}),
})
const data = await response.json()
if (response.ok) {
setMessage(data.message)
fetchManageableUsers(admin?.national_id || "")
} else {
setMessage(data.error || "שגיאה בשינוי תפקיד")
}
} catch (err) {
setMessage("שגיאה בחיבור לשרת")
}
}
const handleReportOnBehalf = async (userId: string, status: string) => {
try {
const response = await fetch("/api/admin/report-on-behalf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
adminId: admin?.national_id,
targetUserId: userId,
status,
}),
})
const data = await response.json()
if (response.ok) {
setMessage(data.message)
fetchManageableUsers(admin?.national_id || "")
} else {
setMessage(data.error || "שגיאה בדיווח")
}
} catch (err) {
setMessage("שגיאה בחיבור לשרת")
}
}
const handleDeleteUser = async (nationalId: string) => {
try {
const response = await fetch(`/api/admin/users/${nationalId}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
adminId: admin?.national_id,
}),
})
if (response.ok) {
setMessage("משתמש נמחק בהצלחה")
fetchManageableUsers(admin?.national_id || "")
} else {
const data = await response.json()
setMessage(data.error || "שגיאה במחיקת משתמש")
}
} catch (err) {
setMessage("שגיאה בחיבור לשרת")
}
}
const handleResetPassword = async (nationalId: string, userName: string) => {
try {
const response = await fetch("/api/admin/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
adminId: admin?.national_id,
targetUserId: nationalId,
}),
})
if (response.ok) {
setMessage(`סיסמה אופסה בהצלחה עבור ${userName}. הסיסמה החדשה: password123`)
fetchManageableUsers(admin?.national_id || "")
} else {
const data = await response.json()
setMessage(data.error || "שגיאה באיפוס סיסמה")
}
} catch (err) {
setMessage("שגיאה בחיבור לשרת")
}
}
const getStatusText = (status?: string) => {
switch (status) {
case "yes":
return { text: "במקלט/חדר מוגן", color: "text-green-600" }
case "no":
return { text: "לא במקלט", color: "text-orange-600" }
case "no_alarm":
return { text: "אין אזעקה", color: "text-blue-600" }
case "safe_after_exit":
return { text: "אני בטוח (אחרי יציאה מהמקלט)", color: "text-purple-600" }
default:
return { text: "לא דיווח", color: "text-gray-500" }
}
}
const getScopeText = () => {
if (!admin) return ""
switch (admin.role) {
case "global_admin":
return "כל המערכת"
case "field_admin":
return `תחום ${admin.field}`
case "department_admin":
return `מסגרת ${admin.department}`
case "team_admin":
return `צוות ${admin.team}`
default:
return ""
}
}
if (!admin) return null
return (
<div className="min-h-screen bg-gray-50 p-4" dir="rtl">
<div className="max-w-6xl mx-auto space-y-6">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle className="text-xl">ניהול תפקידים ודיווחים</CardTitle>
<p className="text-sm text-gray-600 mt-1">
{ROLE_NAMES[admin.role]} - {getScopeText()}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => router.push("/admin")} className="flex items-center gap-2">
<Users className="h-4 w-4" />
</Button>
<Button variant="outline" onClick={() => router.push("/dashboard")} className="flex items-center gap-2">
<Home className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
{message && (
<Alert>
<AlertDescription>{message}</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
ניהול משתמשים ({filteredUsers.length})
</CardTitle>
<div className="flex gap-4">
<Input
placeholder="חיפוש לפי שם..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-md"
/>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">טוען משתמשים...</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-right">שם</TableHead>
<TableHead className="text-right">מזהה</TableHead>
<TableHead className="text-right">תפקיד</TableHead>
<TableHead className="text-right">תחום</TableHead>
<TableHead className="text-right">מסגרת</TableHead>
<TableHead className="text-right">צוות</TableHead>
<TableHead className="text-right">פעולות</TableHead>
</TableRow>
</TableHeader>
<TableBody className="text-right" dir="rtl">
{filteredUsers.map((user) => {
const status = getStatusText(user.in_shelter)
return (
<TableRow key={user.national_id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell className="font-mono text-right" dir="ltr">
{user.national_id}
</TableCell>
<TableCell>
<span className="text-sm bg-indigo-100 text-indigo-800 px-2 py-1 rounded">
{ROLE_NAMES[user.role] || user.role}
</span>
</TableCell>
<TableCell>
<span className="text-sm bg-green-100 text-green-800 px-2 py-1 rounded">
{user.field || "לא הוגדר"}
</span>
</TableCell>
<TableCell>
<span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
{user.department || "לא הוגדר"}
</span>
</TableCell>
<TableCell>
<span className="text-sm bg-purple-100 text-purple-800 px-2 py-1 rounded">
{user.team || "לא הוגדר"}
</span>
</TableCell>
<TableCell className="text-right" dir="rtl">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedUser(user)
setRoleModalOpen(true)
}}
className="text-blue-600 hover:text-blue-700"
>
<UserCog className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" className="text-red-600 hover:text-red-700">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent dir="rtl">
<AlertDialogHeader className="text-right" dir="rtl">
<AlertDialogTitle className="text-right" dir="rtl">האם אתה בטוח?</AlertDialogTitle>
<AlertDialogDescription className="text-right" dir="rtl">
פעולה זו בלתי הפיכה. האם אתה בטוח שברצונך למחוק את {user.name}?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter dir="rtl" className="flex-row-reverse sm:justify-start">
<AlertDialogCancel>ביטול</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (user.national_id === admin?.national_id) {
setMessage("לא ניתן למחוק את עצמך")
return
}
handleDeleteUser(user.national_id)
}}
>
מחיקה
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" className="text-orange-600 hover:text-orange-700">
<KeyRound className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent dir="rtl">
<AlertDialogHeader>
<AlertDialogTitle>איפוס סיסמה</AlertDialogTitle>
<AlertDialogDescription>
האם אתה בטוח שברצונך לאפס את הסיסמה של {user.name}? הסיסמה החדשה תהיה: password123
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>ביטול</AlertDialogCancel>
<AlertDialogAction onClick={() => handleResetPassword(user.national_id, user.name)}>
איפוס סיסמה
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
{filteredUsers.length === 0 && (
<div className="text-center py-8 text-gray-500">
{searchTerm ? "לא נמצאו משתמשים התואמים לחיפוש" : "אין משתמשים"}
</div>
)}
</div>
)}
</CardContent>
</Card>
<RoleManagementModal
isOpen={roleModalOpen}
onClose={() => setRoleModalOpen(false)}
user={selectedUser}
adminRole={admin.role}
onRoleChange={handleRoleChange}
/>
<ReportOnBehalfModal
isOpen={reportModalOpen}
onClose={() => setReportModalOpen(false)}
user={selectedUser}
onReport={handleReportOnBehalf}
/>
</div>
</div>
)
}