fix setup form

This commit is contained in:
2026-01-23 15:27:42 +07:00
parent 505b0caeeb
commit 27a8f3ed0b
9 changed files with 273 additions and 159 deletions

View File

@@ -2,27 +2,30 @@ import { useState } from "react";
import {
getDevices,
saveDevices,
isDuplicateDevice,
getMerchant,
} from "../../../utils/storage";
import { useNavigate } from "@tanstack/react-router";
const initialState: DeviceData = {
towerNumber: "",
floorName: "",
unitNumber: "",
roomName: "",
deviceName: "",
deviceType: "",
code: "",
payload: "",
active: true,
status: false,
};
export default function AddDeviceFeature() {
const [form, setForm] = useState<DeviceData>(initialState);
const [form, setForm] = useState<DeviceData | null >(null);
const navigate = useNavigate();
const merchant = getMerchant();
const isMerchantValid =
merchant?.merchantName &&
merchant?.tower &&
merchant?.floor &&
merchant?.unit;
const isFormValid =
form?.roomName?.trim() &&
form?.deviceName?.trim() &&
form?.deviceType?.trim() &&
form?.deviceLabel?.trim();
function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) {
@@ -32,116 +35,104 @@ export default function AddDeviceFeature() {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!isMerchantValid || !isFormValid) return;
const devices = getDevices();
const deviceToSave: DeviceData = {
...form,
deviceLabel: form.deviceLabel || `${form.roomName} ${form.deviceName}`,
towerNumber: merchant.tower,
floorName: merchant.floor,
unitNumber: merchant.unit,
deviceLabel: form?.deviceLabel,
};
if (isDuplicateDevice(devices, deviceToSave)) {
return;
}
saveDevices([...devices, deviceToSave]);
setForm(initialState);
navigate({ to: '/' });
setForm(null);
navigate({ to: "/" });
}
return (
<form onSubmit={handleSubmit} className="p-4">
<h2 className="text-lg font-semibold mb-4">Add Device</h2>
{!isMerchantValid && (
<p className="text-sm text-red-500 mb-4">
Lengkapi data merchant (tower, floor, unit) terlebih dahulu
</p>
)}
<div className="flex flex-col gap-4">
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Tower
</label>
<input
name="towerNumber"
value={form.towerNumber}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
/>
</div>
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Floor
</label>
<input
name="floorName"
value={form.floorName}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
/>
</div>
{/* ROOM */}
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Room
</label>
<input
name="roomName"
value={form.roomName}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
/>
</div>
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Unit
</label>
<input
name="unitNumber"
value={form.unitNumber}
placeholder="Room Name"
value={form?.roomName}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
/>
</div>
{/* DEVICE NAME */}
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Device Name
</label>
<input
name="deviceName"
value={form.deviceName}
placeholder="Device Name"
value={form?.deviceName}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
/>
</div>
{/* DEVICE TYPE */}
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Device Type
</label>
<input
name="deviceType"
value={form.deviceType}
placeholder="Device Type"
value={form?.deviceType}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
required
/>
</div>
{/* DEVICE LABEL */}
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Device Label
</label>
<input
name="deviceLabel"
value={form.deviceLabel}
value={form?.deviceLabel}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
placeholder={`${form?.roomName || "Room"} ${form?.deviceName || "Device"}`}
required
/>
</div>
</div>
<button
type="submit"
className="w-full bg-orange-500 text-white py-2 rounded-lg mt-8"
disabled={!isMerchantValid || !isFormValid}
className="
w-full py-2 rounded-lg mt-8 text-white cursor-pointer
bg-orange-500
disabled:bg-neutral-400
disabled:cursor-not-allowed
"
>
Save Device
</button>

View File

@@ -1,32 +1,36 @@
import { useQueries } from "@tanstack/react-query";
import { getDeviceStatus } from "../../../repositories/device";
import { DEVICES_DATA } from "../../../utils/data";
import { getDevices, getMerchant } from "../../../utils/storage";
export function useDevices() {
const merchant = getMerchant();
const dataStorage = getDevices();
const datas = [...DEVICES_DATA, ...dataStorage];
const datas = [...dataStorage];
const queries = useQueries({
queries: datas.map((device) => ({
queryKey: [
"device-status",
device.code,
merchant?.merchantName,
device.towerNumber,
device.floorName,
device.unitNumber,
device.deviceName,
device.roomName,
device.deviceType,
"S",
],
queryFn: async () => {
const merchant = getMerchant();
const params = {
merchantName: merchant,
merchantName: merchant?.merchantName,
towerNumber: device.towerNumber,
floorName: device.floorName,
unitNumber: device.unitNumber,
deviceName: device.deviceName,
roomName: device.roomName,
deviceType: device.deviceType,
commandType: "S",
towerNumber: device.towerNumber,
};
const data = await getDeviceStatus(params);
@@ -34,28 +38,28 @@ export function useDevices() {
...device,
code: data.data.code,
deviceName: device.deviceName,
payload: data.data.payload,
status:
data.data.payload?.toLowerCase().includes("on") ||
data.data.payload?.toLowerCase().includes("lock") ||
data.data.payload?.toLowerCase().includes("Open")
data.data.payload?.toLowerCase().includes("open")
? true
: false,
} as unknown as Device;
},
staleTime: 10_000,
retry: 1,
})),
});
const devices = queries
.map((q, index) => ({
...q.data,
isLoading: q.isLoading || q.isFetching,
isError: q.isError,
refetch: q.refetch, // 👈 per device
key: datas[index].code,
}))
.filter((d) => !d?.isError);
.map((q, index) => ({
...q.data,
isLoading: q.isLoading || q.isFetching,
isError: q.isError,
refetch: q.refetch, // 👈 per device
key: datas[index].code,
}))
.filter((d) => !d?.isError);
console.log(devices);
return {
data: devices as unknown as DeviceData[],

View File

@@ -1,9 +1,19 @@
import { Lightbulb, Minus, Plus, SpeakerIcon, Tv, Wind } from "lucide-react";
import {
Blinds,
ChefHat,
DoorClosed,
Fan,
Lightbulb,
Minus,
Plus,
Wind,
WindArrowDown,
} from "lucide-react";
import { Switch } from "../../../components/ui/switch";
import { useNavigate } from "@tanstack/react-router";
import Card from "../../../components/ui/card";
import { useDevices } from "../hooks/queries";
import { getMerchant } from "../../../utils/storage";
import { getMerchant } from "../../../utils/storage";
import { useDeviceCommand } from "../hooks/mutations";
export default function Devices() {
@@ -15,17 +25,16 @@ export default function Devices() {
const { isPending, mutate } = useDeviceCommand();
const onSubmit = (payload: DeviceData, action = "") => {
mutate(
{
towerNumber: payload.towerNumber,
merchantName: merchant?.merchantName,
commandType: "C",
deviceName: payload.deviceName,
deviceType: payload.deviceType,
floorName: payload.floorName,
merchantName: merchant,
payload: { action },
roomName: payload.roomName,
towerNumber: payload.towerNumber,
unitNumber: payload.unitNumber,
},
{
@@ -35,20 +44,20 @@ export default function Devices() {
onError: () => {
payload?.refetch?.();
},
}
},
);
};
const icons = [
{ icon: <Lightbulb size={24} />, label: "light" },
{ icon: <Tv size={24} />, label: "tv" },
{ icon: <SpeakerIcon size={24} />, label: "power" },
{ icon: <Wind size={24} />, label: "ac" },
{ icon: <Tv size={24} />, label: "device" },
];
// console.log(data);
const icons = {
L: { icon: <Lightbulb size={24} />, label: "Lampu" },
BL: { icon: <Blinds size={24} />, label: "Blind" },
AC: { icon: <Wind size={24} />, label: "AC" },
DL: { icon: <DoorClosed size={24} />, label: "Doorlock" },
ERV: { icon: <Fan size={24} />, label: "ERV" },
PM25: { icon: <WindArrowDown size={24} />, label: "Sensor udara" },
KC: { icon: <ChefHat size={24} />, label: "Kitchen Appliances" },
FT1: { icon: <WindArrowDown size={24} />, label: "IAQ" },
};
return (
<>
@@ -56,7 +65,7 @@ export default function Devices() {
<h1 className="text-base text-neutral-900 font-bold mb-4">
Your Devices
</h1>
<div className="flex items-center gap-3 flex-wrap justify-around">
<div className="flex items-center gap-3 flex-wrap justify-start">
<button
onClick={() =>
navigate({
@@ -67,31 +76,58 @@ export default function Devices() {
>
<Plus size={24} />
</button>
{icons.map((item, idx) => (
{/* {icons.map((item, idx) => (
<button
key={idx}
className="shrink-0 w-12 h-12 rounded-full flex items-center justify-center transition-all bg-white text-gray-700 hover:bg-gray-200"
>
{item.icon}
</button>
))}
))} */}
</div>
</div>
<div className="py-4 px-6">
<div className="grid grid-cols-2 auto-rows-fr gap-4 grid-flow-dense">
{isLoading && (
<>
{[...Array(4)].map((item) => (
<Card className={`animate-pulse `} key={item}>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="h-12 w-12 rounded-full bg-gray-200" />
<div className="h-6 w-10 rounded-full bg-gray-200" />
</div>
{/* Title */}
<div className="h-4 w-32 bg-gray-200 rounded mb-2" />
{/* Device */}
<div className="h-3 w-40 bg-gray-200 rounded mb-2" />
{/* Status */}
<div className="h-3 w-24 bg-gray-200 rounded mb-2" />
{/* Location */}
<div className="h-3 w-full bg-gray-200 rounded mb-4" />
</Card>
))}
</>
)}
{!isLoading &&
data?.map((item, index) => (
<Card
key={index}
className={`${
item.deviceName === "AC" && "row-span-2 col-span-1"
}`}
} ${item.status && "bg-orange-100 border-orange-500"}`}
>
<div className="flex items-start justify-between mb-3">
<div className="h-12 w-12 bg-neutral-100 rounded-full flex justify-center items-center">
icon
<div
className={`h-12 w-12 rounded-full flex justify-center items-center ${item.status ? "bg-orange-500 text-white" : "bg-gray-200 text-gray-700"}`}
>
{icons[item.deviceName as keyof typeof icons]?.icon}
</div>
{!["FT1"].includes(item.deviceName) && (
{!["FT1"].includes(item.deviceName ?? "") && (
<Switch
disabled={isPending}
defaultChecked={item.status}
@@ -109,11 +145,20 @@ export default function Devices() {
)}
</div>
<h1 className="font-semibold text-gray-800 text-sm mb-1">
<h1 className="font-semibold text-gray-800 text-sm">
{item?.deviceLabel}
</h1>
<p className="text-xs font-medium text-gray-700 mb-2">
Device:{" "}
{icons[item.deviceName as keyof typeof icons]?.label ??
item.deviceName}
</p>
<p className="text-xs font-medium text-gray-700 mb-2">
Status: {item?.payload?.toString()}
</p>
<p className="text-xs text-gray-500">
{item?.towerNumber}/{item?.floorName}/{item?.roomName}
{item?.towerNumber}/{item?.floorName}/{item?.unitNumber}/
{item?.roomName}
</p>
{item?.deviceName === "AC" && (

View File

@@ -4,32 +4,56 @@ import {
saveMerchant,
getMerchant,
} from "../../utils/storage";
import { useNavigate } from "@tanstack/react-router";
export default function SettingsFeature() {
const [form, setForm] = useState<string>('');
const navigate = useNavigate();
const [form, setForm] = useState<MerchantForm>({
merchantName: "",
tower: "",
floor: "",
unit: "",
});
const [loading, setLoading] = useState<boolean>(false);
const merchant = getMerchant();
function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
e: React.ChangeEvent<HTMLInputElement>
) {
const { value } = e.target;
setForm(value);
const { name, value } = e.target;
setForm((prev) => ({
...prev,
[name]: value,
}));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!isFormValid) return;
setLoading(true);
saveMerchant(form)
saveMerchant(form);
setTimeout(() => {
setLoading(false);
navigate({ to: "/" });
}, 200);
}
useEffect(() => {
setForm(merchant || 'SAVY');
},[merchant]);
const merchant = getMerchant();
if (merchant) {
setForm(merchant);
}
}, []);
const isFormValid =
form.merchantName?.trim() &&
form.tower?.trim() &&
form.floor?.trim() &&
form.unit?.trim();
return (
<form onSubmit={handleSubmit} className="p-4">
@@ -40,21 +64,62 @@ export default function SettingsFeature() {
</label>
<input
name="merchantName"
value={form}
value={form.merchantName}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
required
/>
</div>
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Tower
</label>
<input
name="tower"
value={form.tower}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Floor
</label>
<input
name="floor"
value={form.floor}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
/>
</div>
<div>
<label className="mb-1 text-neutral-900 block font-semibold">
Unit
</label>
<input
name="unit"
value={form.unit}
onChange={handleChange}
className="p-2 border border-neutral-200 w-full rounded-md"
/>
</div>
</div>
</div>
<button
type="submit"
className="w-full bg-orange-500 text-white py-2 rounded-lg mt-8 disabled:bg-neutral-500"
disabled={loading}
disabled={!isFormValid || loading}
className="
w-full py-2 rounded-lg mt-8 text-white
bg-orange-500
disabled:bg-neutral-400
disabled:cursor-not-allowed
"
>
Save Device
{loading ? "Saving..." : "Save Device"}
</button>
</form>
);