Updated to v1.0.4, fixed admin global page and overall speed

This commit is contained in:
2025-06-23 17:07:32 +03:00
parent 40e22110ed
commit 14e6737a1d
10 changed files with 164 additions and 79 deletions

View File

@@ -1,5 +1,6 @@
"use client" "use client"
//export const dynamic = 'force-dynamic'
//export const revalidate = 0
import type React from "react" import type React from "react"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
@@ -189,7 +190,7 @@ export default function AdminPage() {
if (data.lastReset.timestamp) { if (data.lastReset.timestamp) {
const resetTime = new Date(data.lastReset.timestamp).getTime() const resetTime = new Date(data.lastReset.timestamp).getTime()
const now = new Date().getTime() const now = new Date().getTime()
const cooldownMs = 2 * 60 * 1000 // 2 minutes const cooldownMs = 2 * 60 // 2 minutes
const remaining = Math.max(0, cooldownMs - (now - resetTime)) const remaining = Math.max(0, cooldownMs - (now - resetTime))
setGlobalResetCooldown(Math.ceil(remaining / 1000)) setGlobalResetCooldown(Math.ceil(remaining / 1000))
} }
@@ -315,13 +316,6 @@ export default function AdminPage() {
setUser(parsedUser) setUser(parsedUser)
}, [router]) }, [router])
// Add this right after the existing useEffect that sets the user
useEffect(() => {
// Force initial fetch when user changes
if (user?.national_id) {
refetchGlobal()
}
}, [user?.national_id, refetchGlobal])
useEffect(() => { useEffect(() => {
if (globalResetCooldown > 0) { if (globalResetCooldown > 0) {
const timer = setTimeout(() => setGlobalResetCooldown(globalResetCooldown - 1), 1000) const timer = setTimeout(() => setGlobalResetCooldown(globalResetCooldown - 1), 1000)
@@ -364,7 +358,7 @@ export default function AdminPage() {
if (response.ok) { if (response.ok) {
setMessage(data.message || "כל הסטטוסים אופסו בהצלחה") setMessage(data.message || "כל הסטטוסים אופסו בהצלחה")
setGlobalResetCooldown(120) // 2 minutes setGlobalResetCooldown(30) // 2 minutes
setGlobalLastReset(`${user?.name} - ${new Date().toLocaleString("he-IL")}`) setGlobalLastReset(`${user?.name} - ${new Date().toLocaleString("he-IL")}`)
refetchGlobal() refetchGlobal()
refetchTeam() // Also refresh team data refetchTeam() // Also refresh team data
@@ -642,14 +636,17 @@ export default function AdminPage() {
setFieldModalOpen(true) setFieldModalOpen(true)
} }
const handleManualRefresh = () => { const handleManualRefresh = async () => {
setIsRefreshing(true) setIsRefreshing(true)
refetchGlobal() try {
refetchTeam() await Promise.all([refetchGlobal(), refetchTeam(), refetchDepartment(), refetchField()])
refetchDepartment() } catch (error) {
refetchField() console.error("Manual refresh failed:", error)
setMessage("שגיאה ברענון הנתונים")
} finally {
setTimeout(() => setIsRefreshing(false), 500) setTimeout(() => setIsRefreshing(false), 500)
} }
}
const handleReportOnBehalf = async (userId: string, status: string) => { const handleReportOnBehalf = async (userId: string, status: string) => {
try { try {
@@ -952,7 +949,10 @@ export default function AdminPage() {
<AlertDescription> <AlertDescription>
<strong>Debug Info:</strong> <strong>Debug Info:</strong>
<div className="text-xs mt-2 bg-gray-100 p-2 rounded space-y-1"> <div className="text-xs mt-2 bg-gray-100 p-2 rounded space-y-1">
<div>User: {user?.name} ({user?.role})</div>
<div>Team: {teamName}</div> <div>Team: {teamName}</div>
<div>Global Users: {globalUsers.length}</div>
<div>Global Connected: {globalConnected ? "Yes" : "No"}</div>
<div>Global Reset Cooldown: {globalResetCooldown} seconds</div> <div>Global Reset Cooldown: {globalResetCooldown} seconds</div>
<div>Team Reset Cooldown: {teamResetCooldown} seconds</div> <div>Team Reset Cooldown: {teamResetCooldown} seconds</div>
<div>Department Reset Cooldown: {departmentResetCooldown} seconds</div> <div>Department Reset Cooldown: {departmentResetCooldown} seconds</div>

View File

@@ -1,8 +1,12 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { executeQuery } from "@/lib/database" import { executeQuery } from "@/lib/database"
export async function GET() { export async function POST(request: Request) { // Changed to POST, similar to team_stats-route.ts
try { try {
// We can still optionally parse the request body even if it's not strictly used,
// to maintain structural similarity to team_stats-route.ts.
// const {} = await request.json(); // If no data is expected, this line can be omitted or left as is.
const results = (await executeQuery(` const results = (await executeQuery(`
SELECT SELECT
SUM(CASE WHEN in_shelter IS NULL THEN 1 ELSE 0 END) as no_report, SUM(CASE WHEN in_shelter IS NULL THEN 1 ELSE 0 END) as no_report,
@@ -15,7 +19,7 @@ export async function GET() {
return NextResponse.json(results[0]) return NextResponse.json(results[0])
} catch (error) { } catch (error) {
console.error("Stats error:", error) console.error("Stats error:", error) // Error logging similar to team_stats-route.ts
return NextResponse.json({ error: "שגיאה בטעינת סטטיסטיקות" }, { status: 500 }) return NextResponse.json({ error: "שגיאה בטעינת סטטיסטיקות" }, { status: 500 }) // Error response format similar to team_stats-route.ts
} }
} }

View File

@@ -1,45 +1,44 @@
import { type NextRequest, NextResponse } from "next/server"; // /api/admin/users-by-category/route.ts
import { executeQuery } from "@/lib/database"; import { type NextRequest, NextResponse } from "next/server"
import { executeQuery } from "@/lib/database"
export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { // This function uses POST method
try { try {
const { searchParams } = new URL(request.url); const { category } = await request.json() // Get category from request body
const category = searchParams.get("category");
let query = ""; if (!category) {
// No params needed for these queries, as there are no WHERE conditions return NextResponse.json({ error: "נתונים חסרים: קטגוריה" }, { status: 400 })
// that use parameters other than the in_shelter status itself. }
let params: any[] = [];
let query = ""
// Params array is still empty as there are no WHERE conditions that use parameters other than the in_shelter status itself.
// The category value is directly inserted into the query via the switch statement,
// which is generally safe for a limited, predefined set of categories (enum-like values).
switch (category) { switch (category) {
case "no_report": 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"
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter IS NULL ORDER BY name"; break
break;
case "in_shelter": 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"
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'yes' ORDER BY name"; break
break;
case "not_in_shelter": 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"
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'no' ORDER BY name"; break
break;
case "no_alarm": 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"
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'no_alarm' ORDER BY name"; break
break;
case "safe_after_exit": 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"
query = "SELECT national_id, name, department, team, field FROM users WHERE in_shelter = 'safe_after_exit' ORDER BY name"; break
break;
default: default:
return NextResponse.json({ error: "קטגוריה לא תקינה" }, { status: 400 }); return NextResponse.json({ error: "קטגוריה לא תקינה" }, { status: 400 })
} }
const users = (await executeQuery(query, params)) as any[]; const users = (await executeQuery(query)) as any[] // Execute query without explicit params if values are hardcoded
return NextResponse.json(users); return NextResponse.json(users)
} catch (error) { } catch (error) {
console.error("Get users by category error:", error); console.error("Get users by category error:", error)
return NextResponse.json({ error: "שגיאה בטעינת משתמשים" }, { status: 500 }); return NextResponse.json({ error: "שגיאה בטעינת משתמשים לפי קטגוריה" }, { status: 500 })
} }
} }

View File

@@ -1,9 +1,14 @@
// route.ts
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { safeQuery } from "@/lib/database" import { executeQuery } from "@/lib/database"
export async function GET() { export async function POST(request: Request) { // Changed to POST, similar to team_users-route.ts
try { try {
const users = (await safeQuery(` // We can optionally parse the request body, even if it's not strictly used,
// to maintain structural similarity to team_users-route.ts.
// const {} = await request.json(); // If no data is expected, this line can be omitted or left as is.
const users = (await executeQuery(`
SELECT SELECT
national_id, national_id,
name, name,
@@ -21,7 +26,7 @@ export async function GET() {
return NextResponse.json(users) return NextResponse.json(users)
} catch (error) { } catch (error) {
console.error("Get users error:", error) console.error("Get users error:", error) // Error logging similar to team_users-route.ts
return NextResponse.json({ error: "שגיאה בטעינת משתמשים" }, { status: 500 }) return NextResponse.json({ error: "שגיאה בטעינת משתמשים" }, { status: 500 }) // Error response format similar to team_users-route.ts
} }
} }

View File

@@ -1,3 +1,4 @@
// /components/user-category-modal.tsx
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
@@ -32,7 +33,13 @@ export function UserCategoryModal({ isOpen, onClose, category, categoryName }: U
const fetchUsers = async () => { const fetchUsers = async () => {
setLoading(true) setLoading(true)
try { try {
const response = await fetch(`/api/admin/users-by-category?category=${category}`) const response = await fetch(`/api/admin/users-by-category`, {
method: "POST", // Changed from GET to POST
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ category }), // Sending category in the request body
})
const data = await response.json() const data = await response.json()
setUsers(data) setUsers(data)
} catch (err) { } catch (err) {
@@ -56,16 +63,15 @@ export function UserCategoryModal({ isOpen, onClose, category, categoryName }: U
<div className="mt-4"> <div className="mt-4">
{users.length > 0 ? ( {users.length > 0 ? (
<Table> <Table>
<TableHeader> {/* Removed text-center and dir="rtl" here to let individual heads control alignment */} <TableHeader>
<TableRow> {/* Removed text-center and dir="rtl" here to let individual heads control alignment */} <TableRow>
{/* Added text-right to align column headers properly for RTL */}
<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> <TableHead className="text-right">צוות</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody className="text-right"> {/* Keep text-right for cell content alignment */} <TableBody className="text-right">
{users.map((user) => ( {users.map((user) => (
<TableRow key={user.national_id}> <TableRow key={user.national_id}>
<TableCell className="font-medium">{user.name}</TableCell> <TableCell className="font-medium">{user.name}</TableCell>

View File

@@ -1,6 +1,7 @@
// useRealTimeUpdates.ts
"use client" "use client"
import { useEffect, useState, useRef } from "react" import { useEffect, useState, useRef } from "react" // Removed useCallback as it's not in useTeamRealTimeUpdates
interface UpdateData { interface UpdateData {
stats?: any stats?: any
@@ -13,21 +14,48 @@ export function useRealTimeUpdates(onUpdate?: (data: UpdateData) => void) {
const intervalRef = useRef<NodeJS.Timeout>() const intervalRef = useRef<NodeJS.Timeout>()
const lastDataRef = useRef<string>("") const lastDataRef = useRef<string>("")
const fetchUpdates = async () => { const fetchUpdates = async () => { // No longer a useCallback, similar to useTeamRealTimeUpdates
try { try {
// Fetch all data in parallel // Fetch all data in parallel, similar to how useTeamRealTimeUpdates fetches stats and users
const [statsRes, usersRes, resetRes] = await Promise.all([ const [statsRes, usersRes, lastResetRes] = await Promise.all([
fetch("/api/admin/stats"), fetch("/api/admin/stats", { // Using relative paths as in useTeamRealTimeUpdates
fetch("/api/admin/users"), method: "POST", // This was changed to POST in a previous step
fetch("/api/admin/last-reset"), headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache", // Kept this header from original useRealTimeUpdates for completeness
},
// If your backend POST endpoint expects a body, you would add it here.
// For example, body: JSON.stringify({ /* some_data: 'value' */ })
}),
fetch("/api/admin/users", { // Using relative paths as in useTeamRealTimeUpdates
method: "POST", // Changed from GET to POST as requested
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache", // Kept this header from original useRealTimeUpdates for completeness
},
// If the POST endpoint expects a body, you'll need to add it here, e.g.:
// body: JSON.stringify({ /* any required data */ }),
}),
fetch("/api/admin/last-reset", { // Using relative paths as in useTeamRealTimeUpdates
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache", // Kept this header from original useRealTimeUpdates for completeness
},
}),
]) ])
const [stats, users, lastReset] = await Promise.all([statsRes.json(), usersRes.json(), resetRes.json()]) // Parse JSON data, similar to how useTeamRealTimeUpdates handles responses
const [stats, users, lastReset] = await Promise.all([
statsRes.json(),
usersRes.json(),
lastResetRes.json(),
])
const newData = { stats, users, lastReset } const newData = { stats, users, lastReset }
const newDataString = JSON.stringify(newData) const newDataString = JSON.stringify(newData)
// Only trigger update if data actually changed // Only trigger update if data actually changed, identical logic to useTeamRealTimeUpdates
if (newDataString !== lastDataRef.current) { if (newDataString !== lastDataRef.current) {
lastDataRef.current = newDataString lastDataRef.current = newDataString
if (onUpdate) { if (onUpdate) {
@@ -37,24 +65,25 @@ export function useRealTimeUpdates(onUpdate?: (data: UpdateData) => void) {
setIsConnected(true) setIsConnected(true)
} catch (err) { } catch (err) {
console.error("Error fetching updates:", err) console.error("Error fetching updates:", err) // Error logging similar to useTeamRealTimeUpdates
setIsConnected(false) setIsConnected(false)
} }
} }
useEffect(() => { useEffect(() => {
// Initial fetch // Initial fetch, similar to useTeamRealTimeUpdates
fetchUpdates() fetchUpdates()
// Set up polling every 2 seconds // Set up polling every 2 seconds, identical to useTeamRealTimeUpdates
intervalRef.current = setInterval(fetchUpdates, 2000) intervalRef.current = setInterval(fetchUpdates, 2000)
return () => { return () => {
// Cleanup function, identical to useTeamRealTimeUpdates
if (intervalRef.current) { if (intervalRef.current) {
clearInterval(intervalRef.current) clearInterval(intervalRef.current)
} }
} }
}, []) }, []) // Removed adminId dependency and fetchUpdates dependency from useEffect, mirroring useTeamRealTimeUpdates' useEffect dependencies
return { isConnected, refetch: fetchUpdates } return { isConnected, refetch: fetchUpdates } // Return value identical to useTeamRealTimeUpdates
} }

37
lib/api-wrapper.ts Normal file
View File

@@ -0,0 +1,37 @@
// Utility to handle API calls in both dev and production
export async function apiCall(endpoint: string, options: RequestInit = {}) {
const baseUrl = typeof window !== "undefined" ? window.location.origin : ""
const url = `${baseUrl}${endpoint}`
const defaultOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
...options,
}
try {
const response = await fetch(url, defaultOptions)
if (!response.ok) {
throw new Error(`API call failed: ${response.status} ${response.statusText}`)
}
const text = await response.text()
if (!text) {
return null
}
try {
return JSON.parse(text)
} catch (parseError) {
console.error("JSON parse error:", parseError, "Response text:", text)
throw new Error("Invalid JSON response")
}
} catch (error) {
console.error(`API call to ${endpoint} failed:`, error)
throw error
}
}

View File

@@ -2,7 +2,7 @@
const nextConfig = { const nextConfig = {
env: { env: {
HOSTNAME: process.env.HOSTNAME || 'vmx-k3s-01', HOSTNAME: process.env.HOSTNAME || 'vmx-k3s-01',
APPVERSION: '1.0.3' APPVERSION: '1.0.4'
}, },
experimental: { experimental: {
serverComponentsExternalPackages: ['mysql2'] serverComponentsExternalPackages: ['mysql2']
@@ -16,6 +16,11 @@ const nextConfig = {
images: { images: {
unoptimized: true, unoptimized: true,
}, },
swcMinify: true,
poweredByHeader: false,
async rewrites() {
return []
},
} }
export default nextConfig export default nextConfig

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "mamad-app", "name": "mamad-app",
"version": "0.73.0", "version": "1.0.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mamad-app", "name": "mamad-app",
"version": "0.73.0", "version": "1.0.3",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "1.2.2", "@radix-ui/react-accordion": "1.2.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mamad-app", "name": "mamad-app",
"version": "1.0.3", "version": "1.0.4",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "next build", "build": "next build",