import { useState, useEffect, useRef } from "react";
import { useParams, useNavigate } from "react-router-dom";
import api from "../api/client";
import { useAuth } from "../auth/AuthContext";
import ConfirmDialog from "../components/ConfirmDialog";
import NotesPanel from "../equipment/NotesPanel";
function Field({ label, children }) {
return (
{label}
{children || "-"}
);
}
export default function UserDetail() {
const { id } = useParams();
const navigate = useNavigate();
const { hasPermission } = useAuth();
const canEdit = hasPermission("app_users", "edit");
const [user, setUser] = useState(null);
const [devices, setDevices] = useState([]);
const [allDevices, setAllDevices] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [showDelete, setShowDelete] = useState(false);
const [blocking, setBlocking] = useState(false);
const [assigningDevice, setAssigningDevice] = useState(false);
const [selectedDeviceId, setSelectedDeviceId] = useState("");
const [showAssignPanel, setShowAssignPanel] = useState(false);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
const photoInputRef = useRef(null);
useEffect(() => {
loadData();
}, [id]);
const loadData = async () => {
setLoading(true);
try {
const [u, d] = await Promise.all([
api.get(`/users/${id}`),
api.get(`/users/${id}/devices`),
]);
setUser(u);
setDevices(d);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const loadAllDevices = async () => {
try {
const data = await api.get("/devices");
setAllDevices(data.devices || []);
} catch (err) {
setError(err.message);
}
};
const handleDelete = async () => {
try {
await api.delete(`/users/${id}`);
navigate("/users");
} catch (err) {
setError(err.message);
setShowDelete(false);
}
};
const handleBlockToggle = async () => {
setBlocking(true);
setError("");
try {
const endpoint = user.status === "blocked"
? `/users/${id}/unblock`
: `/users/${id}/block`;
const updated = await api.post(endpoint);
setUser(updated);
} catch (err) {
setError(err.message);
} finally {
setBlocking(false);
}
};
const handleAssignDevice = async () => {
if (!selectedDeviceId) return;
setAssigningDevice(true);
setError("");
try {
await api.post(`/users/${id}/devices/${selectedDeviceId}`);
setSelectedDeviceId("");
setShowAssignPanel(false);
const d = await api.get(`/users/${id}/devices`);
setDevices(d);
} catch (err) {
setError(err.message);
} finally {
setAssigningDevice(false);
}
};
const handleUnassignDevice = async (deviceId) => {
setError("");
try {
await api.delete(`/users/${id}/devices/${deviceId}`);
const d = await api.get(`/users/${id}/devices`);
setDevices(d);
} catch (err) {
setError(err.message);
}
};
const openAssignPanel = () => {
setShowAssignPanel(true);
loadAllDevices();
};
const handlePhotoUpload = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingPhoto(true);
setError("");
try {
const result = await api.upload(`/users/${id}/photo`, file);
setUser((prev) => ({ ...prev, photo_url: result.photo_url }));
} catch (err) {
setError(err.message);
} finally {
setUploadingPhoto(false);
if (photoInputRef.current) photoInputRef.current.value = "";
}
};
if (loading) {
return (
Loading...
);
}
if (error && !user) {
return (
{error}
);
}
if (!user) return null;
const isBlocked = user.status === "blocked";
const assignedDeviceIds = new Set(devices.map((d) => d.id));
const availableDevices = allDevices.filter((d) => !assignedDeviceIds.has(d.id));
return (
navigate("/users")}
className="text-sm hover:underline mb-2 inline-block"
style={{ color: "var(--accent)" }}
>
← Back to App Users
{user.display_name || "Unnamed User"}
{user.status || "unknown"}
{canEdit && (
{blocking ? "..." : isBlocked ? "Unblock" : "Block"}
navigate(`/users/${id}/edit`)}
className="px-4 py-2 text-sm rounded-md transition-colors"
style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}
>
Edit
setShowDelete(true)}
className="px-4 py-2 text-sm rounded-md transition-colors"
style={{ backgroundColor: "var(--danger)", color: "var(--text-white)" }}
>
Delete
)}
{error && (
{error}
)}
{/* Left column */}
{/* Account Info */}
Account Information
{/* Profile Photo */}
{user.photo_url ? (
) : (
{(user.display_name || user.email || "?").charAt(0).toUpperCase()}
)}
{canEdit && (
<>
photoInputRef.current?.click()}
disabled={uploadingPhoto}
className="text-xs hover:opacity-80 cursor-pointer transition-colors"
style={{ color: "var(--text-link)" }}
>
{uploadingPhoto ? "Uploading..." : "Change Photo"}
>
)}
{/* Fields */}
{user.userTitle}
{user.phone_number}
{user.uid || user.id}
{user.email}
{user.status || "unknown"}
{/* Profile */}
{/* Timestamps */}
Activity
{user.created_time}
{user.createdAt}
{user.lastActive}
{/* Right column */}
{/* Security */}
Security
{user.settingsPIN ? "****" : "-"}
{user.quickSettingsPIN ? "****" : "-"}
{/* Friends */}
Friends
{user.friendsList?.length ?? 0}
{user.friendsInvited?.length ?? 0}
{/* Assigned Devices */}
Assigned Devices ({devices.length})
{canEdit && (
Assign Device
)}
{showAssignPanel && (
Select Device
setSelectedDeviceId(e.target.value)}
className="w-full px-3 py-2 rounded-md text-sm border"
style={{
backgroundColor: "var(--bg-card)",
color: "var(--text-primary)",
borderColor: "var(--border-primary)",
}}
>
Choose a device...
{availableDevices.map((d) => (
{d.device_name || "Unnamed"} ({d.device_id || d.id})
))}
{assigningDevice ? "..." : "Assign"}
{ setShowAssignPanel(false); setSelectedDeviceId(""); }}
className="px-3 py-2 text-sm rounded-md hover:opacity-80 transition-colors"
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
>
Cancel
)}
{devices.length === 0 ? (
No devices assigned.
) : (
{devices.map((device) => (
navigate(`/devices/${device.id}`)}
className="text-sm font-medium hover:underline"
style={{ color: "var(--text-link)" }}
>
{device.device_name || "Unnamed Device"}
{device.device_id || device.id}
{device.device_location && (
{device.device_location}
)}
{canEdit && (
handleUnassignDevice(device.id)}
className="text-xs hover:opacity-80 cursor-pointer"
style={{ color: "var(--danger)" }}
>
Remove
)}
))}
)}
{/* Issues and Notes */}
setShowDelete(false)}
/>
);
}