Initial commit
This commit is contained in:
1380
app/admin/page.tsx
Normal file
1380
app/admin/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
58
app/api/admin/add-user/route.ts
Normal file
58
app/api/admin/add-user/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
76
app/api/admin/change-role/route.ts
Normal file
76
app/api/admin/change-role/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
54
app/api/admin/db-health/route.ts
Normal file
54
app/api/admin/db-health/route.ts
Normal 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
|
||||
}
|
||||
79
app/api/admin/department-reset/route.ts
Normal file
79
app/api/admin/department-reset/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
52
app/api/admin/department-stats/route.ts
Normal file
52
app/api/admin/department-stats/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
62
app/api/admin/department-users-by-category/route.ts
Normal file
62
app/api/admin/department-users-by-category/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
58
app/api/admin/department-users/route.ts
Normal file
58
app/api/admin/department-users/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
61
app/api/admin/events/route.ts
Normal file
61
app/api/admin/events/route.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
70
app/api/admin/field-reset/route.ts
Normal file
70
app/api/admin/field-reset/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
50
app/api/admin/field-stats/route.ts
Normal file
50
app/api/admin/field-stats/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
60
app/api/admin/field-users-by-category/route.ts
Normal file
60
app/api/admin/field-users-by-category/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
56
app/api/admin/field-users/route.ts
Normal file
56
app/api/admin/field-users/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
28
app/api/admin/last-reset/route.ts
Normal file
28
app/api/admin/last-reset/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
122
app/api/admin/manageable-users/route.ts
Normal file
122
app/api/admin/manageable-users/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
86
app/api/admin/report-on-behalf/route.ts
Normal file
86
app/api/admin/report-on-behalf/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
72
app/api/admin/reset-all/route.ts
Normal file
72
app/api/admin/reset-all/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
56
app/api/admin/reset-password/route.ts
Normal file
56
app/api/admin/reset-password/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
21
app/api/admin/stats/route.ts
Normal file
21
app/api/admin/stats/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
66
app/api/admin/team-reset/route.ts
Normal file
66
app/api/admin/team-reset/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
53
app/api/admin/team-stats/route.ts
Normal file
53
app/api/admin/team-stats/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
62
app/api/admin/team-users-by-category/route.ts
Normal file
62
app/api/admin/team-users-by-category/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
58
app/api/admin/team-users/route.ts
Normal file
58
app/api/admin/team-users/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
74
app/api/admin/toggle-user-lock/route.ts
Normal file
74
app/api/admin/toggle-user-lock/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
45
app/api/admin/users-by-category/route.ts
Normal file
45
app/api/admin/users-by-category/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
27
app/api/admin/users/[id]/route.ts
Normal file
27
app/api/admin/users/[id]/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
27
app/api/admin/users/route.ts
Normal file
27
app/api/admin/users/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
45
app/api/auth/change-password/route.ts
Normal file
45
app/api/auth/change-password/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
53
app/api/auth/login/route.ts
Normal file
53
app/api/auth/login/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
28
app/api/status/update/route.ts
Normal file
28
app/api/status/update/route.ts
Normal 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
45
app/api/test-db/route.ts
Normal 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
6
app/api/websocket/route.ts
Normal file
6
app/api/websocket/route.ts
Normal 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
BIN
app/apple-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
137
app/change-password/page.tsx
Normal file
137
app/change-password/page.tsx
Normal 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
176
app/dashboard/page.tsx
Normal 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
94
app/globals.css
Normal 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
BIN
app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
28
app/layout.tsx
Normal file
28
app/layout.tsx
Normal 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
100
app/login/page.tsx
Normal 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
23
app/manifest.webmanifest
Normal 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
25
app/page.tsx
Normal 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
409
app/role-admin/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user