“init”

This commit is contained in:
2025-09-25 09:56:27 +07:00
parent 507951392b
commit 3c89a3bc70
11 changed files with 1388 additions and 84 deletions

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import SignIn from './pages/auth/SignIn';
import { ProtectedRoute } from './ProtectedRoute';
import { UnprotectedRoute } from './UnprotectedRoute';
import NotFound from './pages/blank/NotFound';
import { getSessionData } from './components/Global/Formatter';
@@ -11,6 +12,10 @@ import Blank from './pages/blank/Blank';
// master
import IndexDevice from './pages/master/device/IndexDevice';
import Room from './pages/master/room/RoomManagement';
import RoomBook from './pages/RoomBook';
// import MainLayout from './layout/MainLayout';
// Setting
@@ -38,6 +43,11 @@ const App = () => {
<Route path="/" element={<Navigate to="/dashboard/home-vendor" />} />
)}
<Route path="/new" element={<UnprotectedRoute />}>
<Route path="room" element={<Room />} />
<Route path="frontdesk" element={<RoomBook />} />
</Route>
<Route path="/signin" element={<SignIn />} />
<Route path="/dashboard" element={<ProtectedRoute />}>
<Route path="home" element={<Home />} />

20
src/UnprotectedRoute.jsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import MainLayout from './layout/MainLayout';
// import { getSessionData } from './components/Global/Formatter';
export const UnprotectedRoute = () => {
// const session = getSessionData();
// console.log(session);
// const isAuthenticated = session?.auth ?? false;
// if (!isAuthenticated) {
// return <Navigate to="/signin" replace />;
// }
return (
<MainLayout>
<Outlet />
</MainLayout>
);
};

View File

@@ -1,5 +1,5 @@
import { Image } from 'antd';
import logoPiu from '../assets/freepik/LOGOPIU.png';
// import logoPiu from '../assets/freepik/LOGOPIU.png';
import React from 'react';
const LayoutLogo = () => {
@@ -50,7 +50,7 @@ const LayoutLogo = () => {
zIndex: 2,
}}
>
<Image
{/* <Image
src={logoPiu}
alt="logo"
width={140}
@@ -59,7 +59,7 @@ const LayoutLogo = () => {
style={{
filter: 'drop-shadow(0 0 3px rgba(0, 0, 0, 0.2))',
}}
/>
/> */}
</div>
</div>
</div>

View File

@@ -5,16 +5,23 @@ import LayoutMenu from './LayoutMenu';
const { Sider } = Layout;
const LayoutSidebar = () => {
const [collapsed, setCollapsed] = React.useState(false);
return (
<Sider width={300}
<Sider
width={300}
// width={0}
breakpoint="lg"
collapsedWidth="0"
onBreakpoint={(broken) => {
// console.log(broken);
}}
onCollapse={(collapsed, type) => {
// trigger={null}
collapsible
collapsed={collapsed}
onCollapse={value => setCollapsed(value)}
// onCollapse={(collapsed, type) => {
// console.log(collapsed, type);
}}
// }}
>
<LayoutLogo />
<LayoutMenu />

View File

@@ -12,6 +12,8 @@ const MainLayout = ({ children }) => {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
console.log("children", children)
return (
<Layout style={{ height: '100vh' }}>
<LayoutSidebar />
@@ -19,7 +21,8 @@ const MainLayout = ({ children }) => {
style={{
overflow: 'auto',
}}>
}}
>
<LayoutHeader />
<Content
style={{

486
src/pages/RoomBook.jsx Normal file
View File

@@ -0,0 +1,486 @@
import React, { memo, useState, useEffect } from 'react';
import {
Row,
Col,
Card,
Button,
Modal,
Form,
Input,
InputNumber,
Select,
Tag,
Space,
Divider,
message,
Typography,
DatePicker,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
EyeOutlined,
ReloadOutlined,
CreditCardOutlined, // 👈 Card icon
BookOutlined, // 👈 Booking icon
} from '@ant-design/icons';
import dayjs from "dayjs";
const { Title, Text } = Typography;
const { Option } = Select;
// Mock data for room types
const roomTypes = [
{ id: 1, name: 'Standard' },
{ id: 2, name: 'Deluxe' },
{ id: 3, name: 'Suite' },
{ id: 4, name: 'Executive' },
{ id: 5, name: 'Presidential' }
];
// Initial room data (your existing initialRooms stays the same)
const initialRooms = [
{
no_kamar: '101',
id_tp_kamar: 1,
tarif: 250000,
isdetail: '1',
lvl: 1,
diskripsi: 'Standard Room with 1 bed',
total_bed: '1',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '0',
id_parent: null,
no_bed: '1',
tp_kamar: 'Standard',
jns_kartu: 'MIF',
tax: 25000,
service: 30000,
base_tarif: 195000,
tarif_traveloka: 220000,
tarif_tiket_com: 225000,
tarif_pegi_pegi: 230000,
tarif_booking_com: 240000,
tarif_phone: 250000,
status: 'ready' // ready, occupied, maintenance, cleaning
},
{
no_kamar: '102',
id_tp_kamar: 1,
tarif: 250000,
isdetail: '1',
lvl: 1,
diskripsi: 'Standard Room with 2 beds',
total_bed: '2',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '1',
id_parent: null,
no_bed: '2',
tp_kamar: 'Standard',
jns_kartu: 'RFID',
tax: 25000,
service: 30000,
base_tarif: 195000,
tarif_traveloka: 220000,
tarif_tiket_com: 225000,
tarif_pegi_pegi: 230000,
tarif_booking_com: 240000,
tarif_phone: 250000,
status: 'occupied'
},
{
no_kamar: '201',
id_tp_kamar: 2,
tarif: 400000,
isdetail: '1',
lvl: 2,
diskripsi: 'Deluxe Room with king size bed',
total_bed: '1',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '0',
id_parent: null,
no_bed: '1',
tp_kamar: 'Deluxe',
jns_kartu: 'MIF',
tax: 40000,
service: 50000,
base_tarif: 310000,
tarif_traveloka: 350000,
tarif_tiket_com: 360000,
tarif_pegi_pegi: 370000,
tarif_booking_com: 380000,
tarif_phone: 400000,
status: 'maintenance'
},
{
no_kamar: '202',
id_tp_kamar: 2,
tarif: 450000,
isdetail: '1',
lvl: 2,
diskripsi: 'Deluxe Room with ocean view',
total_bed: '2',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '0',
id_parent: null,
no_bed: '2',
tp_kamar: 'Deluxe',
jns_kartu: 'NFC',
tax: 45000,
service: 55000,
base_tarif: 350000,
tarif_traveloka: 400000,
tarif_tiket_com: 410000,
tarif_pegi_pegi: 420000,
tarif_booking_com: 430000,
tarif_phone: 450000,
status: 'cleaning'
},
{
no_kamar: '301',
id_tp_kamar: 3,
tarif: 750000,
isdetail: '1',
lvl: 3,
diskripsi: 'Suite with living area',
total_bed: '2',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '1',
id_parent: null,
no_bed: '2',
tp_kamar: 'Suite',
jns_kartu: 'MIF',
tax: 75000,
service: 90000,
base_tarif: 585000,
tarif_traveloka: 650000,
tarif_tiket_com: 670000,
tarif_pegi_pegi: 690000,
tarif_booking_com: 710000,
tarif_phone: 750000,
status: 'ready'
}
];
const RoomBook = memo(function RoomBook(props) {
const [formData, setFormData] = useState({
roomNumber: "000",
name: "",
noId: "",
jenisId: "",
bookingDari: "",
bookingCode: "",
dateStart: "",
dateEnd: "",
status: "ready",
roomPrice: "500000",
});
const [rooms, setRooms] = useState(initialRooms);
const [filteredRooms, setFilteredRooms] = useState(initialRooms);
const [filterStatus, setFilterStatus] = useState('all');
const [isBookingModalVisible, setIsBookingModalVisible] = useState(false);
const [bookingRoom, setBookingRoom] = useState(null);
const [bookingForm] = Form.useForm();
// Handle filter
useEffect(() => {
if (filterStatus === 'all') {
setFilteredRooms(rooms);
} else {
setFilteredRooms(rooms.filter(room => room.status === filterStatus));
}
}, [filterStatus, rooms]);
const handleFilterChange = (status) => {
setFilterStatus(status);
};
// Booking handler
const handleBookingRoom = (room) => {
setBookingRoom(room);
bookingForm.setFieldsValue({
no_kamar: room.no_kamar,
room_price: room.tarif,
});
setIsBookingModalVisible(true);
};
const handleBookingSubmit = (values) => {
console.log("Booking Data:", values);
message.success(`Booking for room ${values.no_kamar} saved!`);
setIsBookingModalVisible(false);
bookingForm.resetFields();
setBookingRoom(null);
};
const handleBookingCancel = () => {
setIsBookingModalVisible(false);
bookingForm.resetFields();
setBookingRoom(null);
};
// Get status color
const getStatusColor = (status) => {
switch (status) {
case 'ready': return 'green';
case 'occupied': return 'red';
case 'maintenance': return 'orange';
case 'cleaning': return 'purple';
default: return 'blue';
}
};
const getStatusText = (status) => {
switch (status) {
case 'ready': return 'Ready';
case 'occupied': return 'Occupied';
case 'maintenance': return 'Maintenance';
case 'cleaning': return 'Cleaning';
default: return 'Unknown';
}
};
return (
<Card>
<Title level={2}>Hotel Room Management</Title>
{/* Filter */}
<Card style={{ marginBottom: '24px' }}>
<Space wrap>
<Text strong>Filter by Status:</Text>
{['all', 'ready', 'occupied', 'maintenance', 'cleaning'].map(status => (
<Button
key={status}
type={filterStatus === status ? 'primary' : 'default'}
onClick={() => handleFilterChange(status)}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</Button>
))}
<Button icon={<ReloadOutlined />} onClick={() => handleFilterChange('all')}>
Reset Filter
</Button>
</Space>
</Card>
{/* Room grid */}
<Row gutter={[16, 16]}>
{filteredRooms.map(room => (
<Col key={room.no_kamar} xs={12} sm={8} md={6} lg={4}>
<Card
size="small"
style={{
border: `2px solid ${getStatusColor(room.status)}`,
cursor: 'pointer'
}}
actions={[
<EyeOutlined key="view" />,
<EditOutlined key="edit" />,
<BookOutlined key="book" onClick={() => handleBookingRoom(room)} />, // 👈 Booking button
<CreditCardOutlined key="card" onClick={() => message.info('Card tapped!')} /> // 👈 Card button
]}
>
<div style={{ textAlign: 'center' }}>
<Title level={3} style={{ margin: 0 }}>{room.no_kamar}</Title>
<Tag color={getStatusColor(room.status)}>
{getStatusText(room.status)}
</Tag>
<div>
<Text type="secondary">{room.tp_kamar}</Text>
</div>
<div>
<Text>Rp {room.tarif.toLocaleString('id-ID')}</Text>
</div>
</div>
</Card>
</Col>
))}
</Row>
{/* Booking Modal */}
<Modal
title={`Booking Room - ${bookingRoom?.no_kamar}`}
open={isBookingModalVisible}
onCancel={handleBookingCancel}
footer={null}
width={600}
>
<Form form={bookingForm} layout="vertical" onFinish={handleBookingSubmit}>
<Form.Item label="Room Number" name="no_kamar">
<Input
value={formData.roomNumber}
disabled
/>
</Form.Item>
<Form.Item
label="Guest Name"
name="name"
rules={[{ required: true, message: 'Please enter guest name!' }]}
>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Jenis ID"
name="jenis_id"
rules={[{ required: true, message: 'Please enter ID type!' }]}
>
<Select
value={formData.jenisId}
onChange={(e) => setFormData({ ...formData, jenisId: e })}
>
<Option value="">Pilih Jenis ID</Option>
<Option value="ktp">KTP</Option>
<Option value="passport">Passport</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="No ID"
name="no_id"
rules={[{ required: true, message: 'Please enter ID number!' }]}
>
<Input
value={formData.noId}
onChange={(e) => setFormData({ ...formData, noId: e.target.value })}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Booking Dari"
name="booking_dari"
rules={[{ required: true, message: 'Please select booking source!' }]}
>
<Select
value={formData.bookingDari}
onChange={(e) => setFormData({ ...formData, bookingDari: e })}
>
<Option value="traveloka">Traveloka</Option>
<Option value="agoda">Agoda</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Booking Code"
name="booking_code"
rules={[{ required: true, message: 'Please enter booking code!' }]}
>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Date Start"
name="date_start"
rules={[{ required: true, message: 'Please enter date start!' }]}
>
<DatePicker
showTime
value={formData.dateStart ? dayjs(formData.dateStart) : null}
onChange={(value) =>
setFormData({ ...formData, dateStart: value ? value.toISOString() : "" })
}
className="w-1/2"
placeholder="Date Start"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Date End"
name="date_end"
rules={[{ required: true, message: 'Please enter date end!' }]}
>
<DatePicker
showTime
value={formData.dateEnd ? dayjs(formData.dateEnd) : null}
onChange={(value) =>
setFormData({ ...formData, dateEnd: value ? value.toISOString() : "" })
}
className="w-1/2"
placeholder="Date End"
disabledDate={(current) =>
formData.dateStart ? current && current < dayjs(formData.dateStart) : false
}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item
label="Status"
name="status"
rules={[{ required: true, message: 'Please select status!' }]}
>
<Select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e })}
>
<Option value="ready">Ready</Option>
<Option value="occupied">Occupied</Option>
<Option value="maintenance">Maintenance</Option>
<Option value="cleaning">Cleaning</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
label="Room Price"
name="room_price"
rules={[{ required: true, message: 'Please enter room price!' }]}
>
<InputNumber
readOnly
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
Submit Booking
</Button>
</Form.Item>
</Form>
</Modal>
</Card>
);
});
export default RoomBook;

View File

@@ -1,8 +1,8 @@
import { Flex, Input, Form, Button, Card, Space, Image } from 'antd';
import React from 'react';
import handleSignIn from '../../Utils/Auth/SignIn';
import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
import logo from 'assets/freepik/LOGOPIU.png';
// import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
// import logo from 'assets/freepik/LOGOPIU.png';
import { useNavigate } from 'react-router-dom';
import { NotifAlert } from '../../components/Global/ToastNotif';
import { decryptData } from '../../components/Global/Formatter';
@@ -101,7 +101,7 @@ const SignIn = () => {
height: '100vh',
// marginTop: '10vh',
// backgroundImage: `url('https://via.placeholder.com/300')`,
backgroundImage: `url(${sypiu_ggcp})`,
// backgroundImage: `url(${sypiu_ggcp})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
@@ -112,14 +112,14 @@ const SignIn = () => {
}}
>
<Flex align="center" justify="center">
<Image
{/* <Image
// src="/src/assets/freepik/LOGOPIU.png"
src={logo}
height={150}
width={220}
preview={false}
alt="signin"
/>
/> */}
</Flex>
<br />
<Form onFinish={handleOnSubmit} layout="vertical" style={{ width: '250px' }}>

View File

@@ -1,6 +1,6 @@
import { Button, Typography } from 'antd';
import { Link } from 'react-router-dom';
import ImgRobot from '../../assets/freepik/404.png';
// import ImgRobot from '../../assets/freepik/404.png';
const { Title, Paragraph, Text } = Typography;
@@ -42,7 +42,7 @@ const NotFound = () => {
page.
</Paragraph>
<img
{/* <img
src={ImgRobot}
alt="404 Not Found"
style={{
@@ -50,7 +50,7 @@ const NotFound = () => {
width: '480px',
marginBottom: '4vh',
}}
/>
/> */}
<Link to="/">
<Button

View File

@@ -1,7 +1,7 @@
import React, {useEffect, useState } from 'react';
import { Modal, Button, ConfigProvider } from 'antd';
import { jsPDF } from 'jspdf';
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
// import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
import { kopReportPdf } from '../../../../components/Global/KopReport';
const GeneratePdf = (props) => {
@@ -10,7 +10,7 @@ const GeneratePdf = (props) => {
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
generatePdf();
// generatePdf();
} else {
navigate('/signin');
}
@@ -21,66 +21,66 @@ const GeneratePdf = (props) => {
props.setActionMode('list');
};
const generatePdf = async () => {
const {images, title} = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
// const generatePdf = async () => {
// const {images, title} = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
const doc = new jsPDF({
orientation: "portrait",
unit: "mm",
format: "a4"
});
// const doc = new jsPDF({
// orientation: "portrait",
// unit: "mm",
// format: "a4"
// });
const width = 45;
const height = 23;
const marginTop = 6;
const marginLeft = 10;
doc.addImage(images, 'PNG', marginLeft, marginTop, width, height);
// const width = 45;
// const height = 23;
// const marginTop = 6;
// const marginLeft = 10;
// doc.addImage(images, 'PNG', marginLeft, marginTop, width, height);
doc.setFont('helvetica', 'bold');
doc.setFontSize(25);
doc.setTextColor(35, 165, 90);
doc.setTextColor('#00b0f0');
doc.text(title, 100, 25);
doc.setTextColor('#000000');
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
// doc.setFont('helvetica', 'bold');
// doc.setFontSize(25);
// doc.setTextColor(35, 165, 90);
// doc.setTextColor('#00b0f0');
// doc.text(title, 100, 25);
// doc.setTextColor('#000000');
// doc.setFontSize(11);
// doc.setFont('helvetica', 'normal');
doc.setLineWidth(0.2);
doc.line(10, 32, 200, 32);
doc.setLineWidth(0.6);
doc.line(10, 32.8, 200, 32.8);
// doc.setLineWidth(0.2);
// doc.line(10, 32, 200, 32);
// doc.setLineWidth(0.6);
// doc.line(10, 32.8, 200, 32.8);
doc.text("Tanggal Pengajuan", 10, 42);
doc.text(":", 59, 42);
// doc.text("Tanggal Pengajuan", 10, 42);
// doc.text(":", 59, 42);
doc.text("Deskripsi Pekerjaan", 10, 48);
doc.text(":", 59, 48);
// doc.text("Deskripsi Pekerjaan", 10, 48);
// doc.text(":", 59, 48);
doc.text("No. Permit", 10, 54);
doc.text(":", 59, 54);
doc.text("Spesifik Lokasi", 120, 54);
doc.text(":", 160, 54);
// doc.text("No. Permit", 10, 54);
// doc.text(":", 59, 54);
// doc.text("Spesifik Lokasi", 120, 54);
// doc.text(":", 160, 54);
doc.text("No. Order", 10, 60);
doc.text(":", 59, 60);
doc.text("Jum. Personil Terlihat", 120, 60);
doc.text(":", 160, 60);
// doc.text("No. Order", 10, 60);
// doc.text(":", 59, 60);
// doc.text("Jum. Personil Terlihat", 120, 60);
// doc.text(":", 160, 60);
doc.text("Peralatan yang digunakan", 10, 66);
doc.text(":", 59, 66);
// doc.text("Peralatan yang digunakan", 10, 66);
// doc.text(":", 59, 66);
doc.text("Jenis APD yang digunakan", 10, 72);
doc.text(":", 59, 72);
// doc.text("Jenis APD yang digunakan", 10, 72);
// doc.text(":", 59, 72);
const blob = doc.output('blob');
const url = URL.createObjectURL(blob);
// const blob = doc.output('blob');
// const url = URL.createObjectURL(blob);
setPdfUrl(url);
// setPdfUrl(url);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
};
// setTimeout(() => {
// URL.revokeObjectURL(url);
// }, 1000);
// };
return (
<Modal

View File

@@ -22,7 +22,7 @@ import TableList from '../../../../components/Global/TableList';
import { getFilterData } from '../../../../components/Global/DataFilter';
import ExcelJS from 'exceljs';
import { saveAs } from 'file-saver';
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
// import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
const columns = (items, handleClickMenu) => [
{
@@ -174,25 +174,25 @@ const ListDevice = memo(function ListDevice(props) {
const sheet = workbook.addWorksheet('Data APD');
let rowCursor = 1;
// Kop Logo PIE
if (logoPiEnergi) {
const response = await fetch(logoPiEnergi);
const blob = await response.blob();
const buffer = await blob.arrayBuffer();
// if (logoPiEnergi) {
// const response = await fetch(logoPiEnergi);
// const blob = await response.blob();
// const buffer = await blob.arrayBuffer();
const imageId = workbook.addImage({
buffer,
extension: 'png',
});
// const imageId = workbook.addImage({
// buffer,
// extension: 'png',
// });
// Tempatkan gambar di pojok atas
sheet.addImage(imageId, {
tl: { col: 0.2, row: 0.8 },
ext: { width: 163, height: 80 },
});
// // Tempatkan gambar di pojok atas
// sheet.addImage(imageId, {
// tl: { col: 0.2, row: 0.8 },
// ext: { width: 163, height: 80 },
// });
sheet.getRow(5).height = 15; // biar ada jarak ke tabel
rowCursor = 3;
}
// sheet.getRow(5).height = 15; // biar ada jarak ke tabel
// rowCursor = 3;
// }
// Tambah Judul
const titleCell = sheet.getCell(`C${rowCursor}`);

View File

@@ -0,0 +1,778 @@
import React, { useState } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
InputNumber,
Select,
Tag,
Space,
Divider,
message,
Typography,
Card,
Row,
Col,
Popconfirm
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined
} from '@ant-design/icons';
const { Title, Text } = Typography;
const { Option } = Select;
const { TextArea } = Input;
// Mock data for room types
const roomTypes = [
{ id: 1, name: 'Standard' },
{ id: 2, name: 'Deluxe' },
{ id: 3, name: 'Suite' },
{ id: 4, name: 'Executive' },
{ id: 5, name: 'Presidential' }
];
// Mock data for card types
const cardTypes = [
{ id: 'MIF', name: 'MIFARE' },
{ id: 'RFID', name: 'RFID' },
{ id: 'NFC', name: 'NFC' }
];
// Initial room data
const initialRooms = [
{
no_kamar: '101',
id_tp_kamar: 1,
tarif: 250000,
isdetail: '1',
lvl: 1,
diskripsi: 'Standard Room with 1 bed',
total_bed: '1',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '0',
id_parent: null,
no_bed: '1',
tp_kamar: 'Standard',
jns_kartu: 'MIF',
tax: 25000,
service: 30000,
base_tarif: 195000,
tarif_traveloka: 220000,
tarif_tiket_com: 225000,
tarif_pegi_pegi: 230000,
tarif_booking_com: 240000,
tarif_phone: 250000,
status: 'ready'
},
{
no_kamar: '102',
id_tp_kamar: 1,
tarif: 250000,
isdetail: '1',
lvl: 1,
diskripsi: 'Standard Room with 2 beds',
total_bed: '2',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '1',
id_parent: null,
no_bed: '2',
tp_kamar: 'Standard',
jns_kartu: 'RFID',
tax: 25000,
service: 30000,
base_tarif: 195000,
tarif_traveloka: 220000,
tarif_tiket_com: 225000,
tarif_pegi_pegi: 230000,
tarif_booking_com: 240000,
tarif_phone: 250000,
status: 'occupied'
},
{
no_kamar: '201',
id_tp_kamar: 2,
tarif: 400000,
isdetail: '1',
lvl: 2,
diskripsi: 'Deluxe Room with king size bed',
total_bed: '1',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '0',
id_parent: null,
no_bed: '1',
tp_kamar: 'Deluxe',
jns_kartu: 'MIF',
tax: 40000,
service: 50000,
base_tarif: 310000,
tarif_traveloka: 350000,
tarif_tiket_com: 360000,
tarif_pegi_pegi: 370000,
tarif_booking_com: 380000,
tarif_phone: 400000,
status: 'maintenance'
},
{
no_kamar: '202',
id_tp_kamar: 2,
tarif: 450000,
isdetail: '1',
lvl: 2,
diskripsi: 'Deluxe Room with ocean view',
total_bed: '2',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '0',
id_parent: null,
no_bed: '2',
tp_kamar: 'Deluxe',
jns_kartu: 'NFC',
tax: 45000,
service: 55000,
base_tarif: 350000,
tarif_traveloka: 400000,
tarif_tiket_com: 410000,
tarif_pegi_pegi: 420000,
tarif_booking_com: 430000,
tarif_phone: 450000,
status: 'cleaning'
},
{
no_kamar: '301',
id_tp_kamar: 3,
tarif: 750000,
isdetail: '1',
lvl: 3,
diskripsi: 'Suite with living area',
total_bed: '2',
dt_ins: '2023-05-10 10:00:00',
dt_upd: '2023-05-10 10:00:00',
usr_ins: 'admin',
usr_upd: 'admin',
is_isi: '1',
id_parent: null,
no_bed: '2',
tp_kamar: 'Suite',
jns_kartu: 'MIF',
tax: 75000,
service: 90000,
base_tarif: 585000,
tarif_traveloka: 650000,
tarif_tiket_com: 670000,
tarif_pegi_pegi: 690000,
tarif_booking_com: 710000,
tarif_phone: 750000,
status: 'ready'
}
];
const RoomManagement = () => {
const [rooms, setRooms] = useState(initialRooms);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingRoom, setEditingRoom] = useState(null);
const [viewingRoom, setViewingRoom] = useState(null);
const [isViewModalVisible, setIsViewModalVisible] = useState(false);
const [form] = Form.useForm();
// Status options
const statusOptions = [
{ value: 'ready', label: 'Ready', color: 'green' },
{ value: 'occupied', label: 'Occupied', color: 'red' },
{ value: 'maintenance', label: 'Maintenance', color: 'orange' },
{ value: 'cleaning', label: 'Cleaning', color: 'purple' }
];
// Handle form submission
const handleFormSubmit = (values) => {
if (editingRoom) {
// Update existing room
setRooms(rooms.map(room =>
room.no_kamar === editingRoom.no_kamar
? { ...room, ...values, dt_upd: new Date().toISOString(), usr_upd: 'admin' }
: room
));
message.success('Room updated successfully');
} else {
// Add new room - check if room number already exists
if (rooms.some(room => room.no_kamar === values.no_kamar)) {
message.error('Room number already exists');
return;
}
const newRoom = {
...values,
dt_ins: new Date().toISOString(),
dt_upd: new Date().toISOString(),
usr_ins: 'admin',
usr_upd: 'admin'
};
setRooms([...rooms, newRoom]);
message.success('Room added successfully');
}
setIsModalVisible(false);
setEditingRoom(null);
form.resetFields();
};
// Handle delete room
const handleDeleteRoom = (roomNumber) => {
setRooms(rooms.filter(room => room.no_kamar !== roomNumber));
message.success('Room deleted successfully');
};
// Handle modal cancel
const handleModalCancel = () => {
setIsModalVisible(false);
setEditingRoom(null);
form.resetFields();
};
// Handle view modal cancel
const handleViewModalCancel = () => {
setIsViewModalVisible(false);
setViewingRoom(null);
};
// Get status tag color
const getStatusColor = (status) => {
const statusObj = statusOptions.find(opt => opt.value === status);
return statusObj ? statusObj.color : 'default';
};
// Get status text
const getStatusText = (status) => {
const statusObj = statusOptions.find(opt => opt.value === status);
return statusObj ? statusObj.label : 'Unknown';
};
// Table columns
const columns = [
{
title: 'Room Number',
dataIndex: 'no_kamar',
key: 'no_kamar',
sorter: (a, b) => a.no_kamar.localeCompare(b.no_kamar),
},
{
title: 'Type',
dataIndex: 'tp_kamar',
key: 'tp_kamar',
filters: roomTypes.map(type => ({ text: type.name, value: type.name })),
onFilter: (value, record) => record.tp_kamar === value,
},
{
title: 'Description',
dataIndex: 'diskripsi',
key: 'diskripsi',
ellipsis: true,
},
{
title: 'Beds',
dataIndex: 'total_bed',
key: 'total_bed',
sorter: (a, b) => a.total_bed - b.total_bed,
},
{
title: 'Level',
dataIndex: 'lvl',
key: 'lvl',
sorter: (a, b) => a.lvl - b.lvl,
},
{
title: 'Tariff',
dataIndex: 'tarif',
key: 'tarif',
render: (tarif) => `Rp ${tarif.toLocaleString('id-ID')}`,
sorter: (a, b) => a.tarif - b.tarif,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status) => (
<Tag color={getStatusColor(status)}>
{getStatusText(status)}
</Tag>
),
filters: statusOptions.map(opt => ({ text: opt.label, value: opt.value })),
onFilter: (value, record) => record.status === value,
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space size="0">
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => {
setViewingRoom(record);
setIsViewModalVisible(true);
}}
/>
{/* View
</Button> */}
<Button
type="link"
icon={<EditOutlined />}
onClick={() => {
setEditingRoom(record);
form.setFieldsValue(record);
setIsModalVisible(true);
}}
/>
{/* Edit
</Button> */}
<Popconfirm
title="Delete this room?"
description="Are you sure you want to delete this room?"
onConfirm={() => handleDeleteRoom(record.no_kamar)}
okText="Yes"
cancelText="No"
>
<Button type="link" danger icon={<DeleteOutlined />} />
{/* Delete
</Button> */}
</Popconfirm>
</Space>
),
},
];
return (
<div style={{ padding: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<Title level={2}>Room Management</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingRoom(null);
form.resetFields();
setIsModalVisible(true);
}}
>
Add New Room
</Button>
</div>
<Table
columns={columns}
dataSource={rooms}
rowKey="no_kamar"
scroll={{ x: 1000 }}
/>
{/* Add/Edit Room Modal */}
<Modal
title={editingRoom ? 'Edit Room' : 'Add New Room'}
open={isModalVisible}
onCancel={handleModalCancel}
footer={null}
width={700}
>
<Form
form={form}
layout="vertical"
onFinish={handleFormSubmit}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Room Number"
name="no_kamar"
rules={[{ required: true, message: 'Please input room number!' }]}
>
<Input disabled={!!editingRoom} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Room Type"
name="id_tp_kamar"
rules={[{ required: true, message: 'Please select room type!' }]}
>
<Select>
{roomTypes.map(type => (
<Option key={type.id} value={type.id}>{type.name}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Base Tarif"
name="base_tarif"
rules={[{ required: true, message: 'Please input base tariff!' }]}
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Final Tarif"
name="tarif"
rules={[{ required: true, message: 'Please input final tariff!' }]}
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Tax"
name="tax"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Service Charge"
name="service"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
</Row>
<Form.Item
label="Description"
name="diskripsi"
>
<TextArea />
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item
label="Total Bed"
name="total_bed"
>
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="Bed Number"
name="no_bed"
>
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="Level"
name="lvl"
>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Card Type"
name="jns_kartu"
>
<Select>
{cardTypes.map(type => (
<Option key={type.id} value={type.id}>{type.name}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Status"
name="status"
rules={[{ required: true, message: 'Please select status!' }]}
>
<Select>
<Option value="ready">Ready</Option>
<Option value="occupied">Occupied</Option>
<Option value="maintenance">Maintenance</Option>
<Option value="cleaning">Cleaning</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Is Detail"
name="isdetail"
>
<Select>
<Option value="1">Yes</Option>
<Option value="0">No</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Is Occupied"
name="is_isi"
>
<Select>
<Option value="1">Yes</Option>
<Option value="0">No</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Divider>Online Travel Agency Rates</Divider>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Traveloka Rate"
name="tarif_traveloka"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Tiket.com Rate"
name="tarif_tiket_com"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Pegipegi Rate"
name="tarif_pegi_pegi"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Booking.com Rate"
name="tarif_booking_com"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
</Col>
</Row>
<Form.Item
label="Phone Rate"
name="tarif_phone"
>
<InputNumber
style={{ width: '100%' }}
formatter={value => `Rp ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={value => value.replace(/Rp\s?|(,*)/g, '')}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" style={{ marginRight: '8px' }}>
{editingRoom ? 'Update' : 'Add'} Room
</Button>
<Button onClick={handleModalCancel}>
Cancel
</Button>
</Form.Item>
</Form>
</Modal>
{/* View Room Details Modal */}
<Modal
title={`Room Details - ${viewingRoom?.no_kamar}`}
open={isViewModalVisible}
onCancel={handleViewModalCancel}
footer={[
<Button key="close" onClick={handleViewModalCancel}>
Close
</Button>
]}
width={700}
>
{viewingRoom && (
<div>
<Row gutter={16}>
<Col span={12}>
<Text strong>Room Number: </Text>
<Text>{viewingRoom.no_kamar}</Text>
</Col>
<Col span={12}>
<Text strong>Room Type: </Text>
<Text>{viewingRoom.tp_kamar}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Text strong>Status: </Text>
<Tag color={getStatusColor(viewingRoom.status)}>
{getStatusText(viewingRoom.status)}
</Tag>
</Col>
<Col span={12}>
<Text strong>Level: </Text>
<Text>{viewingRoom.lvl}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Text strong>Beds: </Text>
<Text>{viewingRoom.total_bed} ({viewingRoom.no_bed})</Text>
</Col>
<Col span={12}>
<Text strong>Card Type: </Text>
<Text>{viewingRoom.jns_kartu}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Text strong>Is Detail: </Text>
<Text>{viewingRoom.isdetail === '1' ? 'Yes' : 'No'}</Text>
</Col>
<Col span={12}>
<Text strong>Is Occupied: </Text>
<Text>{viewingRoom.is_isi === '1' ? 'Yes' : 'No'}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Text strong>Description: </Text>
<Text>{viewingRoom.diskripsi}</Text>
</Col>
</Row>
<Divider>Rates Information</Divider>
<Row gutter={16}>
<Col span={12}>
<Text strong>Base Tariff: </Text>
<Text>Rp {viewingRoom.base_tarif?.toLocaleString('id-ID')}</Text>
</Col>
<Col span={12}>
<Text strong>Final Tariff: </Text>
<Text>Rp {viewingRoom.tarif?.toLocaleString('id-ID')}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Text strong>Tax: </Text>
<Text>Rp {viewingRoom.tax?.toLocaleString('id-ID')}</Text>
</Col>
<Col span={12}>
<Text strong>Service: </Text>
<Text>Rp {viewingRoom.service?.toLocaleString('id-ID')}</Text>
</Col>
</Row>
<Divider>Online Travel Agency Rates</Divider>
<Row gutter={16}>
<Col span={12}>
<Text strong>Traveloka: </Text>
<Text>Rp {viewingRoom.tarif_traveloka?.toLocaleString('id-ID')}</Text>
</Col>
<Col span={12}>
<Text strong>Tiket.com: </Text>
<Text>Rp {viewingRoom.tarif_tiket_com?.toLocaleString('id-ID')}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Text strong>Pegipegi: </Text>
<Text>Rp {viewingRoom.tarif_pegi_pegi?.toLocaleString('id-ID')}</Text>
</Col>
<Col span={12}>
<Text strong>Booking.com: </Text>
<Text>Rp {viewingRoom.tarif_booking_com?.toLocaleString('id-ID')}</Text>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Text strong>Phone Rate: </Text>
<Text>Rp {viewingRoom.tarif_phone?.toLocaleString('id-ID')}</Text>
</Col>
</Row>
<Divider>Audit Information</Divider>
<Row gutter={16}>
<Col span={12}>
<Text strong>Created: </Text>
<Text>{viewingRoom.dt_ins} by {viewingRoom.usr_ins}</Text>
</Col>
<Col span={12}>
<Text strong>Last Updated: </Text>
<Text>{viewingRoom.dt_upd} by {viewingRoom.usr_upd}</Text>
</Col>
</Row>
</div>
)}
</Modal>
</div>
);
};
export default RoomManagement;