882 lines
35 KiB
JavaScript
882 lines
35 KiB
JavaScript
import { useState, useEffect } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import api from "../api/client";
|
|
|
|
const TIER_OPTIONS = ["basic", "small", "mini", "premium", "vip", "custom"];
|
|
const LOCALE_OPTIONS = ["orthodox", "catholic", "all"];
|
|
const RING_ALERT_OPTIONS = ["disabled", "single", "multi"];
|
|
|
|
const defaultAttributes = {
|
|
hasAssistant: false,
|
|
hasClock: false,
|
|
hasBells: false,
|
|
totalBells: 0,
|
|
bellOutputs: [],
|
|
hammerTimings: [],
|
|
bellGuardOn: false,
|
|
bellGuardSafetyOn: false,
|
|
warningsOn: false,
|
|
towerClockTime: "",
|
|
clockSettings: {
|
|
clockOutputs: [],
|
|
clockTimings: [],
|
|
ringAlertsMasterOn: false,
|
|
ringAlerts: "disabled",
|
|
ringIntervals: 0,
|
|
hourAlertsBell: 0,
|
|
halfhourAlertsBell: 0,
|
|
quarterAlertsBell: 0,
|
|
isDaySilenceOn: false,
|
|
isNightSilenceOn: false,
|
|
daySilenceFrom: "",
|
|
daySilenceTo: "",
|
|
nightSilenceFrom: "",
|
|
nightSilenceTo: "",
|
|
backlightTurnOnTime: "",
|
|
backlightTurnOffTime: "",
|
|
isBacklightAutomationOn: false,
|
|
backlightOutput: 0,
|
|
},
|
|
deviceLocale: "all",
|
|
networkSettings: {
|
|
hostname: "",
|
|
useStaticIP: false,
|
|
ipAddress: [],
|
|
gateway: [],
|
|
subnet: [],
|
|
dns1: [],
|
|
dns2: [],
|
|
},
|
|
serialLogLevel: 0,
|
|
sdLogLevel: 0,
|
|
};
|
|
|
|
const defaultSubscription = {
|
|
subscrTier: "basic",
|
|
subscrStart: "",
|
|
subscrDuration: 0,
|
|
maxUsers: 0,
|
|
maxOutputs: 0,
|
|
};
|
|
|
|
const defaultStats = {
|
|
totalPlaybacks: 0,
|
|
totalHammerStrikes: 0,
|
|
perBellStrikes: [],
|
|
totalWarningsGiven: 0,
|
|
warrantyActive: false,
|
|
warrantyStart: "",
|
|
warrantyPeriod: 0,
|
|
maintainedOn: "",
|
|
maintainancePeriod: 0,
|
|
};
|
|
|
|
const parseIntList = (str) => {
|
|
if (!str.trim()) return [];
|
|
return str
|
|
.split(",")
|
|
.map((s) => parseInt(s.trim(), 10))
|
|
.filter((n) => !isNaN(n));
|
|
};
|
|
|
|
export default function DeviceForm() {
|
|
const { id } = useParams();
|
|
const isEdit = Boolean(id);
|
|
const navigate = useNavigate();
|
|
|
|
const [deviceName, setDeviceName] = useState("");
|
|
const [devicePhoto, setDevicePhoto] = useState("");
|
|
const [deviceLocation, setDeviceLocation] = useState("");
|
|
const [isOnline, setIsOnline] = useState(false);
|
|
const [eventsOn, setEventsOn] = useState(false);
|
|
const [locationCoordinates, setLocationCoordinates] = useState("");
|
|
const [websocketUrl, setWebsocketUrl] = useState("");
|
|
const [churchAssistantURL, setChurchAssistantURL] = useState("");
|
|
|
|
const [attributes, setAttributes] = useState({ ...defaultAttributes, clockSettings: { ...defaultAttributes.clockSettings }, networkSettings: { ...defaultAttributes.networkSettings } });
|
|
const [subscription, setSubscription] = useState({ ...defaultSubscription });
|
|
const [stats, setStats] = useState({ ...defaultStats });
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (isEdit) loadDevice();
|
|
}, [id]);
|
|
|
|
const loadDevice = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const device = await api.get(`/devices/${id}`);
|
|
setDeviceName(device.device_name || "");
|
|
setDevicePhoto(device.device_photo || "");
|
|
setDeviceLocation(device.device_location || "");
|
|
setIsOnline(device.is_Online || false);
|
|
setEventsOn(device.events_on || false);
|
|
setLocationCoordinates(device.device_location_coordinates || "");
|
|
setWebsocketUrl(device.websocket_url || "");
|
|
setChurchAssistantURL(device.churchAssistantURL || "");
|
|
|
|
setAttributes({
|
|
...defaultAttributes,
|
|
...device.device_attributes,
|
|
clockSettings: {
|
|
...defaultAttributes.clockSettings,
|
|
...(device.device_attributes?.clockSettings || {}),
|
|
},
|
|
networkSettings: {
|
|
...defaultAttributes.networkSettings,
|
|
...(device.device_attributes?.networkSettings || {}),
|
|
},
|
|
});
|
|
setSubscription({ ...defaultSubscription, ...(device.device_subscription || {}) });
|
|
setStats({ ...defaultStats, ...(device.device_stats || {}) });
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const updateAttr = (field, value) =>
|
|
setAttributes((prev) => ({ ...prev, [field]: value }));
|
|
|
|
const updateClock = (field, value) =>
|
|
setAttributes((prev) => ({
|
|
...prev,
|
|
clockSettings: { ...prev.clockSettings, [field]: value },
|
|
}));
|
|
|
|
const updateNetwork = (field, value) =>
|
|
setAttributes((prev) => ({
|
|
...prev,
|
|
networkSettings: { ...prev.networkSettings, [field]: value },
|
|
}));
|
|
|
|
const updateSub = (field, value) =>
|
|
setSubscription((prev) => ({ ...prev, [field]: value }));
|
|
|
|
const updateStats = (field, value) =>
|
|
setStats((prev) => ({ ...prev, [field]: value }));
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
setError("");
|
|
|
|
try {
|
|
const body = {
|
|
device_name: deviceName,
|
|
device_photo: devicePhoto,
|
|
device_location: deviceLocation,
|
|
is_Online: isOnline,
|
|
device_attributes: attributes,
|
|
device_subscription: subscription,
|
|
device_stats: stats,
|
|
events_on: eventsOn,
|
|
device_location_coordinates: locationCoordinates,
|
|
device_melodies_all: [],
|
|
device_melodies_favorites: [],
|
|
user_list: [],
|
|
websocket_url: websocketUrl,
|
|
churchAssistantURL,
|
|
};
|
|
|
|
let deviceId = id;
|
|
if (isEdit) {
|
|
await api.put(`/devices/${id}`, body);
|
|
} else {
|
|
const created = await api.post("/devices", body);
|
|
deviceId = created.id;
|
|
}
|
|
|
|
navigate(`/devices/${deviceId}`);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-8 text-gray-500">Loading...</div>;
|
|
}
|
|
|
|
const inputClass =
|
|
"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm";
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
|
{isEdit ? "Edit Device" : "Add Device"}
|
|
</h1>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 text-sm rounded-md p-3 mb-4">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
|
{/* ===== Left Column ===== */}
|
|
<div className="space-y-6">
|
|
{/* --- Basic Info --- */}
|
|
<section className="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
|
Basic Information
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Device Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={deviceName}
|
|
onChange={(e) => setDeviceName(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Location
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={deviceLocation}
|
|
onChange={(e) => setDeviceLocation(e.target.value)}
|
|
placeholder="e.g. St. Mary's Church, Vienna"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Location Coordinates
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={locationCoordinates}
|
|
onChange={(e) => setLocationCoordinates(e.target.value)}
|
|
placeholder="e.g. 48.2082,16.3738"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Device Photo URL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={devicePhoto}
|
|
onChange={(e) => setDevicePhoto(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-4 pt-2">
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={eventsOn}
|
|
onChange={(e) => setEventsOn(e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700">Events On</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* --- Device Attributes --- */}
|
|
<section className="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
|
Device Attributes
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex flex-wrap gap-4 md:col-span-2">
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.hasAssistant}
|
|
onChange={(e) => updateAttr("hasAssistant", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Has Assistant</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.hasClock}
|
|
onChange={(e) => updateAttr("hasClock", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Has Clock</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.hasBells}
|
|
onChange={(e) => updateAttr("hasBells", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Has Bells</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.bellGuardOn}
|
|
onChange={(e) => updateAttr("bellGuardOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Bell Guard</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.bellGuardSafetyOn}
|
|
onChange={(e) => updateAttr("bellGuardSafetyOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Bell Guard Safety</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.warningsOn}
|
|
onChange={(e) => updateAttr("warningsOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Warnings On</span>
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Total Bells
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.totalBells}
|
|
onChange={(e) => updateAttr("totalBells", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Device Locale
|
|
</label>
|
|
<select
|
|
value={attributes.deviceLocale}
|
|
onChange={(e) => updateAttr("deviceLocale", e.target.value)}
|
|
className={inputClass}
|
|
>
|
|
{LOCALE_OPTIONS.map((o) => (
|
|
<option key={o} value={o}>
|
|
{o.charAt(0).toUpperCase() + o.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Bell Outputs (comma-separated)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={attributes.bellOutputs.join(", ")}
|
|
onChange={(e) => updateAttr("bellOutputs", parseIntList(e.target.value))}
|
|
placeholder="e.g. 1, 2, 3"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Hammer Timings (comma-separated)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={attributes.hammerTimings.join(", ")}
|
|
onChange={(e) => updateAttr("hammerTimings", parseIntList(e.target.value))}
|
|
placeholder="e.g. 100, 150, 200"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Serial Log Level
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.serialLogLevel}
|
|
onChange={(e) => updateAttr("serialLogLevel", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
SD Log Level
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.sdLogLevel}
|
|
onChange={(e) => updateAttr("sdLogLevel", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* --- Network Settings --- */}
|
|
<section className="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
|
Network Settings
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Hostname
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={attributes.networkSettings.hostname}
|
|
onChange={(e) => updateNetwork("hostname", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 pt-6">
|
|
<input
|
|
type="checkbox"
|
|
id="useStaticIP"
|
|
checked={attributes.networkSettings.useStaticIP}
|
|
onChange={(e) => updateNetwork("useStaticIP", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<label htmlFor="useStaticIP" className="text-sm font-medium text-gray-700">
|
|
Use Static IP
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
WebSocket URL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={websocketUrl}
|
|
onChange={(e) => setWebsocketUrl(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Church Assistant URL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={churchAssistantURL}
|
|
onChange={(e) => setChurchAssistantURL(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{/* ===== Right Column ===== */}
|
|
<div className="space-y-6">
|
|
{/* --- Subscription --- */}
|
|
<section className="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
|
Subscription
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tier
|
|
</label>
|
|
<select
|
|
value={subscription.subscrTier}
|
|
onChange={(e) => updateSub("subscrTier", e.target.value)}
|
|
className={inputClass}
|
|
>
|
|
{TIER_OPTIONS.map((t) => (
|
|
<option key={t} value={t}>
|
|
{t.charAt(0).toUpperCase() + t.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Start Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={subscription.subscrStart}
|
|
onChange={(e) => updateSub("subscrStart", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Duration (months)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={subscription.subscrDuration}
|
|
onChange={(e) => updateSub("subscrDuration", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Max Users
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={subscription.maxUsers}
|
|
onChange={(e) => updateSub("maxUsers", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Max Outputs
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={subscription.maxOutputs}
|
|
onChange={(e) => updateSub("maxOutputs", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* --- Clock Settings --- */}
|
|
<section className="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
|
Clock Settings
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Ring Alerts
|
|
</label>
|
|
<select
|
|
value={attributes.clockSettings.ringAlerts}
|
|
onChange={(e) => updateClock("ringAlerts", e.target.value)}
|
|
className={inputClass}
|
|
>
|
|
{RING_ALERT_OPTIONS.map((o) => (
|
|
<option key={o} value={o}>
|
|
{o.charAt(0).toUpperCase() + o.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Ring Intervals
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.clockSettings.ringIntervals}
|
|
onChange={(e) => updateClock("ringIntervals", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 pt-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.clockSettings.ringAlertsMasterOn}
|
|
onChange={(e) => updateClock("ringAlertsMasterOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Ring Alerts Master On</span>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Clock Outputs (comma-separated)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={attributes.clockSettings.clockOutputs.join(", ")}
|
|
onChange={(e) => updateClock("clockOutputs", parseIntList(e.target.value))}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Clock Timings (comma-separated)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={attributes.clockSettings.clockTimings.join(", ")}
|
|
onChange={(e) => updateClock("clockTimings", parseIntList(e.target.value))}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Hour Alerts Bell
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.clockSettings.hourAlertsBell}
|
|
onChange={(e) => updateClock("hourAlertsBell", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Half-hour Alerts Bell
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.clockSettings.halfhourAlertsBell}
|
|
onChange={(e) => updateClock("halfhourAlertsBell", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Quarter Alerts Bell
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.clockSettings.quarterAlertsBell}
|
|
onChange={(e) => updateClock("quarterAlertsBell", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Backlight Output
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={attributes.clockSettings.backlightOutput}
|
|
onChange={(e) => updateClock("backlightOutput", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 pt-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.clockSettings.isBacklightAutomationOn}
|
|
onChange={(e) => updateClock("isBacklightAutomationOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Backlight Automation</span>
|
|
</div>
|
|
|
|
{/* Silence settings */}
|
|
<div className="md:col-span-2 border-t border-gray-100 pt-4 mt-2">
|
|
<h3 className="text-sm font-semibold text-gray-700 mb-3">Silence Periods</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.clockSettings.isDaySilenceOn}
|
|
onChange={(e) => updateClock("isDaySilenceOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Day Silence</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={attributes.clockSettings.isNightSilenceOn}
|
|
onChange={(e) => updateClock("isNightSilenceOn", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm text-gray-700">Night Silence</span>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Day Silence From</label>
|
|
<input
|
|
type="time"
|
|
value={attributes.clockSettings.daySilenceFrom}
|
|
onChange={(e) => updateClock("daySilenceFrom", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Day Silence To</label>
|
|
<input
|
|
type="time"
|
|
value={attributes.clockSettings.daySilenceTo}
|
|
onChange={(e) => updateClock("daySilenceTo", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Night Silence From</label>
|
|
<input
|
|
type="time"
|
|
value={attributes.clockSettings.nightSilenceFrom}
|
|
onChange={(e) => updateClock("nightSilenceFrom", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">Night Silence To</label>
|
|
<input
|
|
type="time"
|
|
value={attributes.clockSettings.nightSilenceTo}
|
|
onChange={(e) => updateClock("nightSilenceTo", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* --- Statistics --- */}
|
|
<section className="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
|
Statistics & Warranty
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Total Playbacks
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={stats.totalPlaybacks}
|
|
onChange={(e) => updateStats("totalPlaybacks", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Total Hammer Strikes
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={stats.totalHammerStrikes}
|
|
onChange={(e) => updateStats("totalHammerStrikes", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Total Warnings Given
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={stats.totalWarningsGiven}
|
|
onChange={(e) => updateStats("totalWarningsGiven", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Per-Bell Strikes (comma-separated)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={stats.perBellStrikes.join(", ")}
|
|
onChange={(e) => updateStats("perBellStrikes", parseIntList(e.target.value))}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 pt-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={stats.warrantyActive}
|
|
onChange={(e) => updateStats("warrantyActive", e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700">Warranty Active</span>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Warranty Start
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={stats.warrantyStart}
|
|
onChange={(e) => updateStats("warrantyStart", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Warranty Period (months)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={stats.warrantyPeriod}
|
|
onChange={(e) => updateStats("warrantyPeriod", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Last Maintained On
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={stats.maintainedOn}
|
|
onChange={(e) => updateStats("maintainedOn", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Maintenance Period (months)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={stats.maintainancePeriod}
|
|
onChange={(e) => updateStats("maintainancePeriod", parseInt(e.target.value, 10) || 0)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
{/* --- Actions --- */}
|
|
<div className="flex gap-3 mt-6">
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="px-6 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{saving ? "Saving..." : isEdit ? "Update Device" : "Create Device"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate(isEdit ? `/devices/${id}` : "/devices")}
|
|
className="px-6 py-2 bg-gray-100 text-gray-700 text-sm rounded-md hover:bg-gray-200 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|