437 lines
14 KiB
JavaScript
437 lines
14 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Button, Input, Card, notification, Space, Typography, Alert, Row, Col } from 'antd';
|
|
import { ReadOutlined, EditOutlined, DeleteOutlined, WifiOutlined } from '@ant-design/icons';
|
|
import mqtt from 'mqtt';
|
|
import dayjs from 'dayjs';
|
|
|
|
const { Title, Text } = Typography;
|
|
const { TextArea } = Input;
|
|
|
|
function App() {
|
|
const [lockNo, setLockNo] = useState('1');
|
|
const [startTime, setStartTime] = useState(dayjs().format('YYYY-MM-DDTHH:mm'));
|
|
const [endTime, setEndTime] = useState(
|
|
dayjs().add(1, 'day').set('hour', 12).set('minute', 0).format('YYYY-MM-DDTHH:mm')
|
|
);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [connectionError, setConnectionError] = useState('');
|
|
const [client, setClient] = useState(null);
|
|
const [receivedMessages, setReceivedMessages] = useState('No messages received yet');
|
|
|
|
// MQTT connection options
|
|
const mqttOptions = {
|
|
host: `${import.meta.env.VITE_MQTT_SERVER}`,
|
|
// host: 'mosquitto-apotek-2',
|
|
port: Number(`${import.meta.env.VITE_MQTT_PORT}`),
|
|
// protocol: "mqtt",
|
|
username: 'morekmorekmorek',
|
|
password: 'morek888',
|
|
// connectTimeout: 5000,
|
|
// reconnectPeriod: 5000,
|
|
};
|
|
// Update end time when start time changes
|
|
useEffect(() => {
|
|
const start = dayjs(startTime);
|
|
const newEndTime = start.add(1, 'day').set('hour', 12).set('minute', 0).set('second', 0);
|
|
setEndTime(newEndTime.format('YYYY-MM-DDTHH:mm'));
|
|
}, [startTime]);
|
|
|
|
// Connect to MQTT broker - and keep connection alive
|
|
useEffect(() => {
|
|
let mqttClient;
|
|
|
|
const connectToMqtt = () => {
|
|
try {
|
|
const connectUrl = `mqtt://${mqttOptions.host}:${mqttOptions.port}`;
|
|
console.log('Attempting to connect to:', connectUrl);
|
|
|
|
mqttClient = mqtt.connect(connectUrl, {
|
|
username: mqttOptions.username,
|
|
password: mqttOptions.password,
|
|
clientId: 'mqttjs_' + Math.random().toString(16).substring(2, 8),
|
|
connectTimeout: mqttOptions.connectTimeout,
|
|
reconnectPeriod: mqttOptions.reconnectPeriod,
|
|
clean: false,
|
|
keepalive: 30,
|
|
});
|
|
|
|
setClient(mqttClient);
|
|
|
|
mqttClient.on('connect', () => {
|
|
console.log('Connected to MQTT broker');
|
|
setIsConnected(true);
|
|
setConnectionError('');
|
|
notification.success({
|
|
message: 'MQTT Connected',
|
|
description: 'Successfully connected to MQTT broker',
|
|
duration: 2,
|
|
});
|
|
|
|
mqttClient.subscribe('hotel1/pc1', (err) => {
|
|
if (err) {
|
|
console.error('Subscription error:', err);
|
|
} else {
|
|
console.log('Subscribed to topic: hotel1/pc1');
|
|
}
|
|
});
|
|
});
|
|
|
|
mqttClient.on('error', (err) => {
|
|
console.error('MQTT connection error:', err);
|
|
setConnectionError(err.message);
|
|
setIsConnected(false);
|
|
});
|
|
|
|
mqttClient.on('close', () => {
|
|
console.log('MQTT connection closed');
|
|
setIsConnected(false);
|
|
});
|
|
|
|
mqttClient.on('reconnect', () => {
|
|
console.log('Attempting to reconnect to MQTT broker');
|
|
setIsConnected(false);
|
|
setConnectionError('Reconnecting...');
|
|
});
|
|
|
|
mqttClient.on('offline', () => {
|
|
console.log('MQTT client is offline');
|
|
setIsConnected(false);
|
|
setConnectionError('Offline - attempting to reconnect');
|
|
});
|
|
|
|
mqttClient.on('message', (topic, message) => {
|
|
console.log('Received message:', topic, message.toString());
|
|
const newMessage = `[${new Date().toLocaleTimeString()}] ${topic}: ${message.toString()}\n`;
|
|
setReceivedMessages(prev => prev === 'No messages received yet' ? newMessage : prev + newMessage);
|
|
});
|
|
} catch (err) {
|
|
console.error('Failed to initialize MQTT client:', err);
|
|
setConnectionError(err.message);
|
|
setTimeout(connectToMqtt, 5000);
|
|
}
|
|
};
|
|
|
|
connectToMqtt();
|
|
|
|
}, []);
|
|
|
|
const handleStartTimeChange = (e) => {
|
|
const newStartTime = e.target.value;
|
|
setStartTime(newStartTime);
|
|
};
|
|
|
|
const handleEndTimeChange = (e) => {
|
|
const newEndTime = e.target.value;
|
|
const start = dayjs(startTime);
|
|
const end = dayjs(newEndTime);
|
|
|
|
if (end.isBefore(start)) {
|
|
notification.error({
|
|
message: 'Invalid End Time',
|
|
description: 'End time cannot be before start time',
|
|
duration: 3,
|
|
});
|
|
return;
|
|
}
|
|
|
|
setEndTime(newEndTime);
|
|
};
|
|
|
|
// Format datetime to YYYY-MM-DD HH:mm:ss
|
|
const formatDateTime = (dateTimeString) => {
|
|
return dayjs(dateTimeString).format('YYYY-MM-DD HH:mm:ss');
|
|
};
|
|
|
|
const handleRead = () => {
|
|
if (client && isConnected) {
|
|
const payload = JSON.stringify({ cmd: "cek_kartu" });
|
|
client.publish('hotel1/pc1', payload);
|
|
notification.info({
|
|
message: 'Read Command Sent',
|
|
description: `Published: ${payload}`,
|
|
duration: 2,
|
|
});
|
|
} else {
|
|
notification.warning({
|
|
message: 'Not Connected',
|
|
description: 'Cannot publish message - not connected to MQTT',
|
|
duration: 2,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleWrite = () => {
|
|
if (client && isConnected) {
|
|
if (!lockNo || !startTime || !endTime) {
|
|
notification.error({
|
|
message: 'Validation Error',
|
|
description: 'Please fill in all fields',
|
|
duration: 2,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const start = dayjs(startTime);
|
|
const end = dayjs(endTime);
|
|
|
|
if (end.isBefore(start)) {
|
|
notification.error({
|
|
message: 'Validation Error',
|
|
description: 'End time must be after start time',
|
|
duration: 2,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Format the datetime values before sending
|
|
const formattedStartTime = formatDateTime(startTime);
|
|
const formattedEndTime = formatDateTime(endTime);
|
|
|
|
const payload = JSON.stringify({
|
|
cmd: "tulis_kartu",
|
|
lock_no: parseInt(lockNo),
|
|
start_time: formattedStartTime,
|
|
end_time: formattedEndTime
|
|
});
|
|
|
|
client.publish('hotel1/pc1', payload);
|
|
notification.info({
|
|
message: 'Write Command Sent',
|
|
description: `Published: ${payload}`,
|
|
duration: 2,
|
|
});
|
|
} else {
|
|
notification.warning({
|
|
message: 'Not Connected',
|
|
description: 'Cannot publish message - not connected to MQTT',
|
|
duration: 2,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
if (client && isConnected) {
|
|
const payload = JSON.stringify({ cmd: "hapus_kartu" });
|
|
client.publish('hotel1/pc1', payload);
|
|
notification.info({
|
|
message: 'Delete Command Sent',
|
|
description: `Published: ${payload}`,
|
|
duration: 2,
|
|
});
|
|
} else {
|
|
notification.warning({
|
|
message: 'Not Connected',
|
|
description: 'Cannot publish message - not connected to MQTT',
|
|
duration: 2,
|
|
});
|
|
}
|
|
};
|
|
|
|
const clearMessages = () => {
|
|
setReceivedMessages('No messages received yet');
|
|
};
|
|
|
|
const disconnectMqtt = () => {
|
|
if (client) {
|
|
client.end(false, () => {
|
|
console.log('Manually disconnected from MQTT');
|
|
setIsConnected(false);
|
|
setConnectionError('Manually disconnected');
|
|
});
|
|
}
|
|
};
|
|
|
|
const reconnectMqtt = () => {
|
|
if (client) {
|
|
client.reconnect();
|
|
}
|
|
};
|
|
|
|
// Calculate min and max values for datetime inputs
|
|
const minEndTime = startTime;
|
|
const maxStartTime = endTime;
|
|
|
|
return (
|
|
<div style={{ padding: '24px', minHeight: '100vh', background: '#f0f2f5' }}>
|
|
<Card
|
|
style={{
|
|
maxWidth: 800,
|
|
margin: '0 auto',
|
|
boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
|
|
borderRadius: '8px'
|
|
}}
|
|
>
|
|
<Title level={2} style={{ textAlign: 'center', color: '#1890ff' }}>
|
|
Lock Management System
|
|
</Title>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px', justifyContent: 'space-between' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
<WifiOutlined
|
|
style={{
|
|
color: isConnected ? '#52c41a' : '#ff4d4f',
|
|
fontSize: '20px',
|
|
marginRight: '8px'
|
|
}}
|
|
/>
|
|
<Text strong>
|
|
Status: {isConnected ? 'Connected' : 'Disconnected'}
|
|
</Text>
|
|
</div>
|
|
<Space>
|
|
<Button size="small" onClick={reconnectMqtt} disabled={isConnected}>
|
|
Reconnect
|
|
</Button>
|
|
<Button size="small" danger onClick={disconnectMqtt} disabled={!isConnected}>
|
|
Disconnect
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
|
|
{connectionError && (
|
|
<Alert
|
|
message="Connection Status"
|
|
description={connectionError}
|
|
type={isConnected ? 'success' : 'warning'}
|
|
showIcon
|
|
style={{ marginBottom: '16px' }}
|
|
/>
|
|
)}
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<Row gutter={16}>
|
|
<Col span={8}>
|
|
<Text strong>Lock Number:</Text>
|
|
<Input
|
|
type="number"
|
|
min="1"
|
|
value={lockNo}
|
|
onChange={(e) => setLockNo(e.target.value)}
|
|
size="large"
|
|
style={{ marginTop: '8px' }}
|
|
placeholder="Enter lock number"
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Text strong>Start Time:</Text>
|
|
<Input
|
|
type="datetime-local"
|
|
value={startTime}
|
|
onChange={handleStartTimeChange}
|
|
size="large"
|
|
style={{ marginTop: '8px' }}
|
|
max={maxStartTime}
|
|
/>
|
|
<Text type="secondary" style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}>
|
|
Will be formatted as: {formatDateTime(startTime)}
|
|
</Text>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Text strong>End Time:</Text>
|
|
<Input
|
|
type="datetime-local"
|
|
value={endTime}
|
|
onChange={handleEndTimeChange}
|
|
size="large"
|
|
style={{ marginTop: '8px' }}
|
|
min={minEndTime}
|
|
/>
|
|
<Text type="secondary" style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}>
|
|
Will be formatted as: {formatDateTime(endTime)}
|
|
</Text>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Space direction="horizontal" style={{ width: '100%', justifyContent: 'center' }}>
|
|
<Button
|
|
type="primary"
|
|
icon={<ReadOutlined />}
|
|
size="large"
|
|
onClick={handleRead}
|
|
style={{ background: '#52c41a', borderColor: '#52c41a' }}
|
|
>
|
|
Read
|
|
</Button>
|
|
|
|
<Button
|
|
type="primary"
|
|
icon={<EditOutlined />}
|
|
size="large"
|
|
onClick={handleWrite}
|
|
>
|
|
Write
|
|
</Button>
|
|
|
|
<Button
|
|
type="primary"
|
|
icon={<DeleteOutlined />}
|
|
size="large"
|
|
onClick={handleDelete}
|
|
danger
|
|
>
|
|
Delete
|
|
</Button>
|
|
</Space>
|
|
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
<Text strong>Received Messages:</Text>
|
|
<Button
|
|
size="small"
|
|
onClick={clearMessages}
|
|
style={{ marginLeft: '8px' }}
|
|
>
|
|
Clear
|
|
</Button>
|
|
</div>
|
|
<TextArea
|
|
rows={10}
|
|
value={receivedMessages}
|
|
readOnly
|
|
style={{
|
|
backgroundColor: '#f9f9f9',
|
|
fontFamily: 'monospace',
|
|
fontSize: '14px'
|
|
}}
|
|
placeholder="Messages from MQTT will appear here..."
|
|
/>
|
|
</div>
|
|
</Space>
|
|
|
|
<div style={{ marginTop: '24px' }}>
|
|
<Title level={5}>Connection Information</Title>
|
|
<Text type="secondary">
|
|
Broker: {mqttOptions.host}:{mqttOptions.port}<br />
|
|
Topic: hotel1/pc1<br />
|
|
Username: {mqttOptions.username}<br />
|
|
Client ID: {client?.options?.clientId || 'Not connected'}
|
|
</Text>
|
|
</div>
|
|
|
|
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f0f9ff', borderRadius: '6px' }}>
|
|
<Title level={5}>Usage Instructions</Title>
|
|
<Text type="secondary">
|
|
1. Enter lock number (integer)<br />
|
|
2. Select start time - end time will auto-set to next day at 12:00<br />
|
|
3. Adjust end time if needed (cannot be before start time)<br />
|
|
4. Click Write to send JSON: {"{"}lock_no: [value], start_time: "YYYY-MM-DD HH:mm:ss", end_time: "YYYY-MM-DD HH:mm:ss"{"}"}<br />
|
|
5. Received messages will appear in the text area below
|
|
</Text>
|
|
</div>
|
|
|
|
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#fff7e6', borderRadius: '6px' }}>
|
|
<Title level={5}>Output Format</Title>
|
|
<Text type="secondary" style={{ fontFamily: 'monospace' }}>
|
|
{`{
|
|
"lock_no": ${lockNo},
|
|
"start_time": "${formatDateTime(startTime)}",
|
|
"end_time": "${formatDateTime(endTime)}"
|
|
}`}
|
|
</Text>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App; |