“init”
This commit is contained in:
10
src/App.jsx
10
src/App.jsx
@@ -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
20
src/UnprotectedRoute.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
// console.log(collapsed, type);
|
||||
}}
|
||||
// trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={value => setCollapsed(value)}
|
||||
// onCollapse={(collapsed, type) => {
|
||||
// console.log(collapsed, type);
|
||||
// }}
|
||||
>
|
||||
<LayoutLogo />
|
||||
<LayoutMenu />
|
||||
|
||||
@@ -12,14 +12,17 @@ const MainLayout = ({ children }) => {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
|
||||
console.log("children", children)
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<LayoutSidebar />
|
||||
<Layout
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<LayoutHeader />
|
||||
<Content
|
||||
style={{
|
||||
|
||||
486
src/pages/RoomBook.jsx
Normal file
486
src/pages/RoomBook.jsx
Normal 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;
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
778
src/pages/master/room/RoomManagement.jsx
Normal file
778
src/pages/master/room/RoomManagement.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user