Phase 6 Complete by Claude Code

This commit is contained in:
2026-02-17 23:57:23 +02:00
parent c0605c77db
commit d6e522deb8
12 changed files with 1360 additions and 3 deletions

View File

@@ -0,0 +1,266 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import api from "../api/client";
const CATEGORIES = ["general", "maintenance", "installation", "issue", "other"];
export default function NoteForm() {
const { id } = useParams();
const isEdit = Boolean(id);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [category, setCategory] = useState("general");
const [deviceId, setDeviceId] = useState(searchParams.get("device_id") || "");
const [userId, setUserId] = useState(searchParams.get("user_id") || "");
const [devices, setDevices] = useState([]);
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
loadOptions();
if (isEdit) loadNote();
}, [id]);
const loadOptions = async () => {
try {
const [devData, usrData] = await Promise.all([
api.get("/devices"),
api.get("/users"),
]);
setDevices(devData.devices || []);
setUsers(usrData.users || []);
} catch {
// Non-critical — dropdowns will just be empty
}
};
const loadNote = async () => {
setLoading(true);
try {
const note = await api.get(`/equipment/notes/${id}`);
setTitle(note.title || "");
setContent(note.content || "");
setCategory(note.category || "general");
setDeviceId(note.device_id || "");
setUserId(note.user_id || "");
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
setError("");
try {
const body = {
title,
content,
category,
device_id: deviceId || null,
user_id: userId || null,
};
let noteId = id;
if (isEdit) {
await api.put(`/equipment/notes/${id}`, body);
} else {
const created = await api.post("/equipment/notes", body);
noteId = created.id;
}
navigate(`/equipment/notes/${noteId}`);
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
};
if (loading) {
return <div className="text-center py-8" style={{ color: "var(--text-muted)" }}>Loading...</div>;
}
const inputClass = "w-full px-3 py-2 rounded-md text-sm border";
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold" style={{ color: "var(--text-heading)" }}>
{isEdit ? "Edit Note" : "Add Note"}
</h1>
<div className="flex gap-3">
<button
type="button"
onClick={() => navigate(isEdit ? `/equipment/notes/${id}` : "/equipment/notes")}
className="px-4 py-2 text-sm rounded-md hover:opacity-80 transition-colors"
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
>
Cancel
</button>
<button
type="submit"
form="note-form"
disabled={saving}
className="px-4 py-2 text-sm rounded-md hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
>
{saving ? "Saving..." : isEdit ? "Update Note" : "Create Note"}
</button>
</div>
</div>
{error && (
<div
className="text-sm rounded-md p-3 mb-4 border"
style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}
>
{error}
</div>
)}
<form id="note-form" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Left Column — Note Content */}
<div className="space-y-6">
<section
className="rounded-lg border p-6"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<h2 className="text-lg font-semibold mb-4" style={{ color: "var(--text-heading)" }}>
Note Details
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
Title *
</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Brief description of the note"
className={inputClass}
style={{
backgroundColor: "var(--bg-primary)",
color: "var(--text-primary)",
borderColor: "var(--border-primary)",
}}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
Content *
</label>
<textarea
required
value={content}
onChange={(e) => setContent(e.target.value)}
rows={8}
placeholder="Detailed note content..."
className={inputClass}
style={{
backgroundColor: "var(--bg-primary)",
color: "var(--text-primary)",
borderColor: "var(--border-primary)",
resize: "vertical",
}}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
Category
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className={inputClass}
style={{
backgroundColor: "var(--bg-primary)",
color: "var(--text-primary)",
borderColor: "var(--border-primary)",
}}
>
{CATEGORIES.map((c) => (
<option key={c} value={c}>
{c.charAt(0).toUpperCase() + c.slice(1)}
</option>
))}
</select>
</div>
</div>
</section>
</div>
{/* Right Column — Associations */}
<div className="space-y-6">
<section
className="rounded-lg border p-6"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<h2 className="text-lg font-semibold mb-4" style={{ color: "var(--text-heading)" }}>
Link To
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
Device (optional)
</label>
<select
value={deviceId}
onChange={(e) => setDeviceId(e.target.value)}
className={inputClass}
style={{
backgroundColor: "var(--bg-primary)",
color: "var(--text-primary)",
borderColor: "var(--border-primary)",
}}
>
<option value="">No device linked</option>
{devices.map((d) => (
<option key={d.id} value={d.id}>
{d.device_name || "Unnamed"} ({d.device_id || d.id})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
User (optional)
</label>
<select
value={userId}
onChange={(e) => setUserId(e.target.value)}
className={inputClass}
style={{
backgroundColor: "var(--bg-primary)",
color: "var(--text-primary)",
borderColor: "var(--border-primary)",
}}
>
<option value="">No user linked</option>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.display_name || "Unnamed"} ({u.email || u.id})
</option>
))}
</select>
</div>
</div>
</section>
</div>
</div>
</form>
</div>
);
}