lavoce #45

Merged
bragaz_rexita merged 3 commits from lavoce into main 2026-04-27 06:31:40 +00:00
4 changed files with 259 additions and 19 deletions

View 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 };

View File

@@ -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

View File

@@ -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>

View File

@@ -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
/> />