lavoce #45
52
src/components/Global/AppInput.jsx
Normal file
52
src/components/Global/AppInput.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Input } from "antd";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
|
||||||
|
const InputNumber = ({
|
||||||
|
name,
|
||||||
|
value = 0,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [inputValue, setInputValue] = useState(
|
||||||
|
value !== null && value !== undefined ? value.toString() : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newValue = value !== null && value !== undefined ? value.toString() : '';
|
||||||
|
setInputValue(newValue);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const rawValue = e.target.value;
|
||||||
|
if (/^\d*$/.test(rawValue)) {
|
||||||
|
setInputValue(rawValue);
|
||||||
|
const numValue = rawValue === '' ? 0 : parseInt(rawValue, 10);
|
||||||
|
|
||||||
|
// Kirim event object sintetis yang konsisten dengan handler biasa
|
||||||
|
onChange?.({
|
||||||
|
target: {
|
||||||
|
name: name,
|
||||||
|
value: numValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (e) => {
|
||||||
|
// Pilih semua teks saat fokus (opsional)
|
||||||
|
e.target.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
name={name} // Teruskan name ke input
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
style={{ textAlign: 'left' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { InputNumber };
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd';
|
import {
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
Switch,
|
||||||
|
Button,
|
||||||
|
ConfigProvider,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
} from 'antd';
|
||||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
import { createDevice, updateDevice } from '../../../../api/master-device';
|
import { createDevice, updateDevice } from '../../../../api/master-device';
|
||||||
import { getAllBrands } from '../../../../api/master-brand';
|
import { getAllBrands } from '../../../../api/master-brand';
|
||||||
import { validateRun } from '../../../../Utils/validate';
|
import { validateRun } from '../../../../Utils/validate';
|
||||||
|
import { toApiDateFormatter } from '../../../../components/Global/Formatter';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
@@ -23,6 +35,7 @@ const DetailDevice = (props) => {
|
|||||||
device_location: '',
|
device_location: '',
|
||||||
device_description: '',
|
device_description: '',
|
||||||
ip_address: '',
|
ip_address: '',
|
||||||
|
reminder_at: null,
|
||||||
listen_channel: '',
|
listen_channel: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,8 +80,12 @@ const DetailDevice = (props) => {
|
|||||||
ip_address: formData.ip_address,
|
ip_address: formData.ip_address,
|
||||||
brand_id: formData.brand_id,
|
brand_id: formData.brand_id,
|
||||||
listen_channel: formData.listen_channel,
|
listen_channel: formData.listen_channel,
|
||||||
|
listen_channel_reminder: formData.listen_channel_reminder,
|
||||||
|
reminder_at: formData.reminder_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
|
||||||
const response = formData.device_id
|
const response = formData.device_id
|
||||||
? await updateDevice(formData.device_id, payload)
|
? await updateDevice(formData.device_id, payload)
|
||||||
: await createDevice(payload);
|
: await createDevice(payload);
|
||||||
@@ -128,6 +145,13 @@ const DetailDevice = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (field, value) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Fungsi untuk mengambil daftar brand
|
// Fungsi untuk mengambil daftar brand
|
||||||
const fetchBrands = async () => {
|
const fetchBrands = async () => {
|
||||||
setLoadingBrands(true);
|
setLoadingBrands(true);
|
||||||
@@ -218,9 +242,10 @@ const DetailDevice = (props) => {
|
|||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
<Divider />
|
||||||
{formData && (
|
{formData && (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<div>
|
<div>
|
||||||
<Text strong>Device Status</Text>
|
<Text strong>Device Status</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,7 +272,7 @@ const DetailDevice = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
{/* <Divider style={{ margin: '12px 0' }} /> */}
|
||||||
<div hidden>
|
<div hidden>
|
||||||
<Text strong>Device ID</Text>
|
<Text strong>Device ID</Text>
|
||||||
<Input
|
<Input
|
||||||
@@ -330,8 +355,12 @@ const DetailDevice = (props) => {
|
|||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}></div>
|
||||||
<Text strong>Listen Channel</Text>
|
<div style={{ marginBottom: 12 }}></div>
|
||||||
|
<div style={{ display: 'flex', gap: 16 }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{' '}
|
||||||
|
<Text strong>Channel Error</Text>
|
||||||
<Input
|
<Input
|
||||||
name="listen_channel"
|
name="listen_channel"
|
||||||
value={formData.listen_channel}
|
value={formData.listen_channel}
|
||||||
@@ -340,6 +369,50 @@ const DetailDevice = (props) => {
|
|||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text strong>Channel Reminder</Text>
|
||||||
|
<Input
|
||||||
|
name="listen_channel_reminder"
|
||||||
|
value={formData.listen_channel_reminder}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Listen Channel Reminder"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Reminder At</Text>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
<DatePicker
|
||||||
|
value={
|
||||||
|
formData.reminder_at
|
||||||
|
? dayjs(formData.reminder_at) // ✅ langsung parse ISO
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={(date) =>
|
||||||
|
handleDateChange(
|
||||||
|
'reminder_at',
|
||||||
|
date ? date.toISOString() : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
format="DD-MM-YYYY HH:mm"
|
||||||
|
showTime={{ format: 'HH:mm' }}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
onClick={() =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
reminder_at: null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text strong>Device Description</Text>
|
<Text strong>Device Description</Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Image,
|
Image,
|
||||||
|
Checkbox,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
@@ -37,9 +38,46 @@ const DetailSparepart = (props) => {
|
|||||||
const [previewTitle, setPreviewTitle] = useState('');
|
const [previewTitle, setPreviewTitle] = useState('');
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
const optionsYearly = [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
value: 1,
|
||||||
|
text: '1 Year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
value: 2,
|
||||||
|
text: '2 Year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
value: 3,
|
||||||
|
text: '3 Year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 4,
|
||||||
|
value: 4,
|
||||||
|
text: '4 Year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 5,
|
||||||
|
value: 5,
|
||||||
|
text: '5 Year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 6,
|
||||||
|
value: 6,
|
||||||
|
text: '6 Year',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const [checkedList, setCheckedList] = useState([]);
|
||||||
|
const [checkAll, setCheckAll] = useState(false);
|
||||||
|
const [indeterminate, setIndeterminate] = useState(false);
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
sparepart_id: '',
|
sparepart_id: '',
|
||||||
sparepart_name: '',
|
sparepart_name: '',
|
||||||
|
sparepart_number: '',
|
||||||
sparepart_description: '',
|
sparepart_description: '',
|
||||||
sparepart_model: '',
|
sparepart_model: '',
|
||||||
sparepart_item_type: null,
|
sparepart_item_type: null,
|
||||||
@@ -236,6 +274,9 @@ const DetailSparepart = (props) => {
|
|||||||
payload.sparepart_foto = imageUrl;
|
payload.sparepart_foto = imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload.sparepart_number = formData.sparepart_number;
|
||||||
|
payload.sparepart_yearly = checkedList;
|
||||||
|
|
||||||
// console.log('Sending payload:', payload);
|
// console.log('Sending payload:', payload);
|
||||||
|
|
||||||
const response = formData.sparepart_id
|
const response = formData.sparepart_id
|
||||||
@@ -282,6 +323,21 @@ const DetailSparepart = (props) => {
|
|||||||
setFormData({ ...formData, [name]: value });
|
setFormData({ ...formData, [name]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeCheckbox = (list) => {
|
||||||
|
setCheckedList(list);
|
||||||
|
|
||||||
|
setIndeterminate(!!list.length && list.length < optionsYearly.length);
|
||||||
|
setCheckAll(list.length === optionsYearly.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCheckAllChange = (e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
|
||||||
|
setCheckedList(checked ? optionsYearly.map((item) => item.value) : []);
|
||||||
|
setIndeterminate(false);
|
||||||
|
setCheckAll(checked);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
@@ -318,9 +374,13 @@ const DetailSparepart = (props) => {
|
|||||||
} else {
|
} else {
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
}
|
}
|
||||||
|
setCheckedList(JSON.parse(props.selectedData.sparepart_yearly));
|
||||||
} else {
|
} else {
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
|
setIndeterminate(false);
|
||||||
|
setCheckedList([]);
|
||||||
|
setCheckAll(false);
|
||||||
}
|
}
|
||||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|
||||||
@@ -333,6 +393,7 @@ const DetailSparepart = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
width="40rem"
|
||||||
title={`${
|
title={`${
|
||||||
props.actionMode === 'add'
|
props.actionMode === 'add'
|
||||||
? 'Tambah'
|
? 'Tambah'
|
||||||
@@ -463,7 +524,7 @@ const DetailSparepart = (props) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '180px', // Fixed width for square
|
width: '180px', // Fixed width for square
|
||||||
height: '180px',
|
height: '240px',
|
||||||
border: '1px dashed #d9d9d9',
|
border: '1px dashed #d9d9d9',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -485,6 +546,17 @@ const DetailSparepart = (props) => {
|
|||||||
{/* Kolom untuk field lainnya */}
|
{/* Kolom untuk field lainnya */}
|
||||||
<Col span={14}>
|
<Col span={14}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Text strong>Part Number</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_number"
|
||||||
|
value={formData.sparepart_number}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Sparepart Number"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Text strong>Sparepart Name</Text>
|
<Text strong>Sparepart Name</Text>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
@@ -560,7 +632,44 @@ const DetailSparepart = (props) => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#23A55A',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row style={{ marginTop: 16 }}>
|
||||||
|
<Col>
|
||||||
|
<Checkbox
|
||||||
|
indeterminate={indeterminate}
|
||||||
|
onChange={onCheckAllChange}
|
||||||
|
checked={checkAll}
|
||||||
|
>
|
||||||
|
{checkAll ? 'Unselect All Years' : 'Select All Years'}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row style={{ marginTop: 10 }}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Checkbox.Group
|
||||||
|
value={checkedList}
|
||||||
|
onChange={onChangeCheckbox}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{optionsYearly.map((item) => (
|
||||||
|
<Checkbox key={item.key} value={item.value}>
|
||||||
|
{item.text}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</ConfigProvider>
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Text strong>Description</Text>
|
<Text strong>Description</Text>
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
key: 'sparepart_name',
|
key: 'sparepart_name',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Sparepart Number',
|
||||||
|
dataIndex: 'sparepart_number',
|
||||||
|
key: 'sparepart_number',
|
||||||
|
width: '20%',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Description',
|
title: 'Description',
|
||||||
dataIndex: 'sparepart_description',
|
dataIndex: 'sparepart_description',
|
||||||
@@ -277,8 +283,8 @@ const ListSparepart = memo(function ListSparepart(props) {
|
|||||||
onStockUpdate={doFilter}
|
onStockUpdate={doFilter}
|
||||||
onGetData={(data) => {
|
onGetData={(data) => {
|
||||||
if(data && data.length > 0) {
|
if(data && data.length > 0) {
|
||||||
console.log('Sample sparepart data from API:', data[0]);
|
// console.log('Sample sparepart data from API:', data[0]);
|
||||||
console.log('Available fields:', Object.keys(data[0] || {}));
|
// console.log('Available fields:', Object.keys(data[0] || {}));
|
||||||
}
|
}
|
||||||
}} // Log untuk debugging field-field yang tersedia
|
}} // Log untuk debugging field-field yang tersedia
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user