Major overhaul to the Notes/Issues. Minor tweaks to the UI. Added Profile photos
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import api from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
@@ -37,6 +37,8 @@ export default function UserDetail() {
|
||||
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();
|
||||
@@ -126,6 +128,22 @@ export default function UserDetail() {
|
||||
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 (
|
||||
<div className="text-center py-8" style={{ color: "var(--text-muted)" }}>
|
||||
@@ -247,33 +265,77 @@ export default function UserDetail() {
|
||||
>
|
||||
Account Information
|
||||
</h2>
|
||||
<dl className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<Field label="Document ID">
|
||||
<span className="font-mono text-xs" style={{ color: "var(--text-muted)" }}>
|
||||
{user.id}
|
||||
</span>
|
||||
</Field>
|
||||
<Field label="UID">
|
||||
<span className="font-mono text-xs">{user.uid}</span>
|
||||
</Field>
|
||||
<Field label="Status">
|
||||
<span
|
||||
className="px-2 py-0.5 text-xs rounded-full"
|
||||
style={
|
||||
isBlocked
|
||||
? { backgroundColor: "var(--danger-bg)", color: "var(--danger-text)" }
|
||||
: user.status === "active"
|
||||
? { backgroundColor: "var(--success-bg)", color: "var(--success-text)" }
|
||||
: { backgroundColor: "var(--bg-card-hover)", color: "var(--text-muted)" }
|
||||
}
|
||||
<div style={{ display: "flex", gap: "1.5rem" }}>
|
||||
{/* Profile Photo */}
|
||||
<div className="shrink-0" style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "0.5rem" }}>
|
||||
<div
|
||||
className="relative rounded-full overflow-hidden"
|
||||
style={{ width: 80, height: 80, backgroundColor: "var(--bg-card-hover)" }}
|
||||
>
|
||||
{user.status || "unknown"}
|
||||
</span>
|
||||
</Field>
|
||||
<Field label="Email">{user.email}</Field>
|
||||
<Field label="Phone">{user.phone_number}</Field>
|
||||
<Field label="Title">{user.userTitle}</Field>
|
||||
</dl>
|
||||
{user.photo_url ? (
|
||||
<img
|
||||
src={user.photo_url}
|
||||
alt={user.display_name || "User"}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center text-2xl font-bold"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
{(user.display_name || user.email || "?").charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{canEdit && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => photoInputRef.current?.click()}
|
||||
disabled={uploadingPhoto}
|
||||
className="text-xs hover:opacity-80 cursor-pointer transition-colors"
|
||||
style={{ color: "var(--text-link)" }}
|
||||
>
|
||||
{uploadingPhoto ? "Uploading..." : "Change Photo"}
|
||||
</button>
|
||||
<input
|
||||
ref={photoInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoUpload}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Fields */}
|
||||
<dl className="grid grid-cols-2 md:grid-cols-3 gap-4 flex-1">
|
||||
<Field label="Document ID">
|
||||
<span className="font-mono text-xs" style={{ color: "var(--text-muted)" }}>
|
||||
{user.id}
|
||||
</span>
|
||||
</Field>
|
||||
<Field label="UID">
|
||||
<span className="font-mono text-xs">{user.uid}</span>
|
||||
</Field>
|
||||
<Field label="Status">
|
||||
<span
|
||||
className="px-2 py-0.5 text-xs rounded-full"
|
||||
style={
|
||||
isBlocked
|
||||
? { backgroundColor: "var(--danger-bg)", color: "var(--danger-text)" }
|
||||
: user.status === "active"
|
||||
? { backgroundColor: "var(--success-bg)", color: "var(--success-text)" }
|
||||
: { backgroundColor: "var(--bg-card-hover)", color: "var(--text-muted)" }
|
||||
}
|
||||
>
|
||||
{user.status || "unknown"}
|
||||
</span>
|
||||
</Field>
|
||||
<Field label="Email">{user.email}</Field>
|
||||
<Field label="Phone">{user.phone_number}</Field>
|
||||
<Field label="Title">{user.userTitle}</Field>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Profile */}
|
||||
@@ -289,7 +351,6 @@ export default function UserDetail() {
|
||||
</h2>
|
||||
<dl className="grid grid-cols-1 gap-4">
|
||||
<Field label="Bio">{user.bio}</Field>
|
||||
<Field label="Photo URL">{user.photo_url}</Field>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -475,7 +536,7 @@ export default function UserDetail() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Equipment Notes */}
|
||||
{/* Issues and Notes */}
|
||||
<NotesPanel userId={id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user