feat: add onGetData callback to TableList and enhance DetailSparepart with improved image handling

This commit is contained in:
2025-11-27 13:30:45 +07:00
parent 572042ab53
commit ed4570e8dd
4 changed files with 334 additions and 70 deletions

View File

@@ -22,6 +22,7 @@ const TableList = memo(function TableList({
columnDynamic = false, columnDynamic = false,
cardComponent, // New prop for custom card component cardComponent, // New prop for custom card component
onStockUpdate, // Prop to pass to card component onStockUpdate, // Prop to pass to card component
onGetData, // Callback to execute when data is received
}) { }) {
const [gridLoading, setGridLoading] = useState(false); const [gridLoading, setGridLoading] = useState(false);
@@ -105,7 +106,14 @@ const TableList = memo(function TableList({
setColumnsDynamic([...defaultColumns, ...numericColumns]); setColumnsDynamic([...defaultColumns, ...numericColumns]);
} }
setData(resData?.data ?? []); const fetchedData = resData?.data ?? [];
// Panggil callback jika disediakan
if (onGetData && typeof onGetData === 'function') {
onGetData(fetchedData);
}
setData(fetchedData);
const pagingData = resData?.paging; const pagingData = resData?.paging;

View File

@@ -1,5 +1,17 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Modal, Input, Select, Divider, Typography, Button, ConfigProvider, Upload, Row, Col, Image } from 'antd'; import {
Modal,
Input,
Select,
Divider,
Typography,
Button,
ConfigProvider,
Upload,
Row,
Col,
Image,
} from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createSparepart, updateSparepart } from '../../../../api/sparepart'; import { createSparepart, updateSparepart } from '../../../../api/sparepart';
@@ -33,7 +45,7 @@ const DetailSparepart = (props) => {
sparepart_unit: '', sparepart_unit: '',
sparepart_merk: '', sparepart_merk: '',
sparepart_stok: '0', sparepart_stok: '0',
image_url: '', sparepart_foto: '',
}; };
const [formData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
@@ -60,7 +72,9 @@ const DetailSparepart = (props) => {
const handleSave = async () => { const handleSave = async () => {
setConfirmLoading(true); setConfirmLoading(true);
const validationRules = [{ field: 'sparepart_name', label: 'Sparepart Name', required: true }]; const validationRules = [
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
];
if ( if (
validateRun(formData, validationRules, (errorMessages) => { validateRun(formData, validationRules, (errorMessages) => {
@@ -71,40 +85,153 @@ const DetailSparepart = (props) => {
return; return;
try { try {
let imageUrl = formData.image_url; let imageUrl = formData.sparepart_foto;
const newFile = fileList.length > 0 ? fileList[0] : null; const newFile = fileList.length > 0 ? fileList[0] : null;
if (newFile && newFile.originFileObj) { if (newFile && newFile.originFileObj) {
console.log('Uploading file:', newFile.originFileObj);
const uploadResponse = await uploadFile(newFile.originFileObj, 'images'); const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
if (uploadResponse && uploadResponse.file_url) {
imageUrl = uploadResponse.file_url; // Log untuk debugging
console.log('Upload response:', uploadResponse);
// Cek berbagai kemungkinan struktur respons dari API
let uploadedUrl = null;
// Cek berbagai kemungkinan struktur respons dari API
// Cek langsung properti file_url atau url
if (uploadResponse && typeof uploadResponse === 'object') {
// Cek jika uploadResponse langsung memiliki file_url
if (uploadResponse.file_url) {
uploadedUrl = uploadResponse.file_url;
}
// Cek jika uploadResponse memiliki data yang berisi file_url
else if (uploadResponse.data && uploadResponse.data.file_url) {
uploadedUrl = uploadResponse.data.file_url;
}
// Cek jika uploadResponse memiliki data yang berisi url
else if (uploadResponse.data && uploadResponse.data.url) {
uploadedUrl = uploadResponse.data.url;
}
// Cek jika uploadResponse langsung memiliki url
else if (uploadResponse.url) {
uploadedUrl = uploadResponse.url;
}
// Cek jika uploadResponse.data adalah string URL
else if (uploadResponse.data && typeof uploadResponse.data === 'string') {
uploadedUrl = uploadResponse.data;
}
// Cek jika uploadResponse.data adalah objek yang berisi file URL dalam format berbeda
else if (uploadResponse.data && typeof uploadResponse.data === 'object') {
// Cek kemungkinan nama field lain
if (uploadResponse.data.file) {
uploadedUrl = uploadResponse.data.file;
} else if (uploadResponse.data.filename) {
// Jika hanya nama file dikembalikan, bangun URL
const baseUrl = import.meta.env.VITE_API_SERVER || '';
uploadedUrl = `${baseUrl}/uploads/images/${uploadResponse.data.filename}`;
} else if (uploadResponse.data.path) {
uploadedUrl = uploadResponse.data.path;
} else if (uploadResponse.data.location) {
uploadedUrl = uploadResponse.data.location;
}
// Tambahkan kemungkinan lain berdasarkan struktur respons umum
else if (uploadResponse.data.filePath) {
uploadedUrl = uploadResponse.data.filePath;
} else if (uploadResponse.data.file_path) {
uploadedUrl = uploadResponse.data.file_path;
} else if (uploadResponse.data.publicUrl) {
uploadedUrl = uploadResponse.data.publicUrl;
} else if (uploadResponse.data.public_url) {
uploadedUrl = uploadResponse.data.public_url;
}
// Berdasarkan log yang ditampilkan, API mengembalikan path_document atau path_solution
else if (uploadResponse.data.path_document) {
uploadedUrl = uploadResponse.data.path_document;
} else if (uploadResponse.data.path_solution) {
uploadedUrl = uploadResponse.data.path_solution;
} else if (uploadResponse.data.file_upload_name) {
// Jika hanya nama file dikembalikan, bangun URL
const baseUrl = import.meta.env.VITE_API_SERVER || '';
uploadedUrl = `${baseUrl}/uploads/images/${uploadResponse.data.file_upload_name}`;
}
}
}
// Jika respons adalah string, mungkin itu adalah URL
else if (uploadResponse && typeof uploadResponse === 'string') {
uploadedUrl = uploadResponse;
}
if (uploadedUrl) {
console.log('Successfully extracted image URL:', uploadedUrl);
imageUrl = uploadedUrl;
} else { } else {
throw new Error('Image upload failed or did not return a URL.'); console.error('Upload response structure:', uploadResponse);
console.error('Available properties:', Object.keys(uploadResponse || {}));
console.error('Response type:', typeof uploadResponse);
console.error(
'Is response an object?',
uploadResponse && typeof uploadResponse === 'object'
);
if (uploadResponse && typeof uploadResponse === 'object') {
console.error('Response keys:', Object.keys(uploadResponse));
console.error(
'Response data keys:',
uploadResponse.data
? Object.keys(uploadResponse.data)
: 'No data property'
);
}
// Tampilkan notifikasi bahwa upload gagal tapi lanjutkan penyimpanan
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Upload gambar gagal. Data akan disimpan tanpa gambar.',
});
// Gunakan URL gambar yang sebelumnya jika ada, atau kosongkan
imageUrl = formData.sparepart_foto || '';
} }
} else if (fileList.length === 0 && formData.sparepart_id) { } else if (fileList.length === 0 && formData.sparepart_id) {
imageUrl = ''; imageUrl = '';
} }
// Kirim semua field yang dianggap required oleh API, bahkan jika kosong
const payload = { const payload = {
sparepart_name: formData.sparepart_name, sparepart_name: formData.sparepart_name, // Wajib
sparepart_item_type: formData.sparepart_item_type, sparepart_description: formData.sparepart_description || '', // Diperlukan oleh API
sparepart_stok: formData.sparepart_stok || '0', sparepart_model: formData.sparepart_model || '', // Diperlukan oleh API
image_url: imageUrl, sparepart_item_type: formData.sparepart_item_type || '', // Diperlukan oleh API
sparepart_merk: formData.sparepart_merk, sparepart_unit: formData.sparepart_unit || '', // Diperlukan oleh API
sparepart_model: formData.sparepart_model, sparepart_merk: formData.sparepart_merk || '', // Diperlukan oleh API
sparepart_description: formData.sparepart_description,
sparepart_unit: formData.sparepart_unit,
}; };
// Tambahkan field-field yang tidak dianggap required jika ada nilainya
if (formData.sparepart_stok && formData.sparepart_stok.trim() !== '') {
payload.sparepart_stok = formData.sparepart_stok.toString();
} else {
payload.sparepart_stok = '0'; // Set default value jika tidak diisi
}
if (imageUrl && imageUrl.trim() !== '') {
payload.sparepart_foto = imageUrl;
}
console.log('Sending payload:', payload);
const response = formData.sparepart_id const response = formData.sparepart_id
? await updateSparepart(formData.sparepart_id, payload) ? await updateSparepart(formData.sparepart_id, payload)
: await createSparepart(payload); : await createSparepart(payload);
console.log('API response:', response);
if (response && (response.statusCode === 200 || response.statusCode === 201)) { if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({ NotifOk({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
message: `Data Sparepart berhasil ${formData.sparepart_id ? 'diubah' : 'ditambahkan'}.`, message: `Data Sparepart berhasil ${
formData.sparepart_id ? 'diubah' : 'ditambahkan'
}.`,
}); });
props.setActionMode('list'); props.setActionMode('list');
setFileList([]); setFileList([]);
@@ -139,13 +266,13 @@ const DetailSparepart = (props) => {
useEffect(() => { useEffect(() => {
if (props.selectedData) { if (props.selectedData) {
setFormData(props.selectedData); setFormData(props.selectedData);
if (props.selectedData.image_url) { if (props.selectedData.sparepart_foto) {
setFileList([ setFileList([
{ {
uid: '-1', uid: '-1',
name: props.selectedData.image_url.split('/').pop(), name: props.selectedData.sparepart_foto.split('/').pop(),
status: 'done', status: 'done',
url: props.selectedData.image_url, url: props.selectedData.sparepart_foto,
}, },
]); ]);
} else { } else {
@@ -167,7 +294,11 @@ const DetailSparepart = (props) => {
return ( return (
<Modal <Modal
title={`${ title={`${
props.actionMode === 'add' ? 'Tambah' : props.actionMode === 'preview' ? 'Preview' : 'Edit' props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
? 'Preview'
: 'Edit'
} Sparepart`} } Sparepart`}
open={props.showModal} open={props.showModal}
onCancel={handleCancel} onCancel={handleCancel}
@@ -231,7 +362,9 @@ const DetailSparepart = (props) => {
<Select <Select
name="sparepart_item_type" name="sparepart_item_type"
value={formData.sparepart_item_type} value={formData.sparepart_item_type}
onChange={(value) => handleSelectChange('sparepart_item_type', value)} onChange={(value) =>
handleSelectChange('sparepart_item_type', value)
}
placeholder="Select Item Type" placeholder="Select Item Type"
disabled={props.readOnly} disabled={props.readOnly}
style={{ width: '100%' }} style={{ width: '100%' }}
@@ -280,7 +413,12 @@ const DetailSparepart = (props) => {
> >
{fileList.length >= 1 ? null : uploadButton} {fileList.length >= 1 ? null : uploadButton}
</Upload> </Upload>
<Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handlePreviewCancel}> <Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={handlePreviewCancel}
>
<img alt="preview" style={{ width: '100%' }} src={previewImage} /> <img alt="preview" style={{ width: '100%' }} src={previewImage} />
</Modal> </Modal>
</Col> </Col>
@@ -328,4 +466,4 @@ const DetailSparepart = (props) => {
); );
}; };
export default DetailSparepart; export default DetailSparepart;

View File

@@ -268,6 +268,12 @@ const ListSparepart = memo(function ListSparepart(props) {
triger={trigerFilter} triger={trigerFilter}
cardComponent={SparepartCardList} // Pass the custom component here cardComponent={SparepartCardList} // Pass the custom component here
onStockUpdate={doFilter} onStockUpdate={doFilter}
onGetData={(data) => {
if(data && data.length > 0) {
console.log('Sample sparepart data from API:', data[0]);
console.log('Available fields:', Object.keys(data[0] || {}));
}
}} // Log untuk debugging field-field yang tersedia
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -29,7 +29,11 @@ const SparepartCardList = ({
const handleUpdateStock = async (item) => { const handleUpdateStock = async (item) => {
const quantityToAdd = updateQuantities[item.sparepart_id] || 0; const quantityToAdd = updateQuantities[item.sparepart_id] || 0;
if (quantityToAdd === 0) { if (quantityToAdd === 0) {
NotifAlert({ icon: 'info', title: 'Info', message: 'Please change the quantity first.' }); NotifAlert({
icon: 'info',
title: 'Info',
message: 'Please change the quantity first.',
});
return; return;
} }
@@ -39,32 +43,55 @@ const SparepartCardList = ({
return; return;
} }
setLoadingQuantities(prev => ({ ...prev, [item.sparepart_id]: true })); setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: true }));
const payload = { const payload = {
...item, sparepart_stok: newStock.toString(), // Convert number to string as required by API
sparepart_stok: newStock,
}; };
// Remove unnecessary keys if the API doesn't like them
delete payload.key;
delete payload.id;
// Hanya tambahkan field jika nilainya tidak kosong untuk menghindari validasi error
if (item.sparepart_unit && item.sparepart_unit.trim() !== '') {
payload.sparepart_unit = item.sparepart_unit;
}
if (item.sparepart_merk && item.sparepart_merk.trim() !== '') {
payload.sparepart_merk = item.sparepart_merk;
}
if (item.sparepart_model && item.sparepart_model.trim() !== '') {
payload.sparepart_model = item.sparepart_model;
}
if (item.sparepart_description && item.sparepart_description.trim() !== '') {
payload.sparepart_description = item.sparepart_description;
}
try { try {
const response = await updateSparepart(item.sparepart_id, payload); const response = await updateSparepart(item.sparepart_id, payload);
if (response.statusCode === 200) {
NotifOk({ icon: 'success', title: 'Success', message: 'Stock updated successfully.' }); // Periksa apakah response valid sebelum mengakses propertinya
if (response && response.statusCode === 200) {
NotifOk({
icon: 'success',
title: 'Success',
message: 'Stock updated successfully.',
});
if (onStockUpdate) { if (onStockUpdate) {
onStockUpdate(); onStockUpdate();
} }
handleQuantityChange(item.sparepart_id, 0); // Reset quantity handleQuantityChange(item.sparepart_id, 0); // Reset quantity
} else { } else {
NotifAlert({ icon: 'error', title: 'Failed', message: response.message || 'Failed to update stock.' }); NotifAlert({
icon: 'error',
title: 'Failed',
message: response?.message || 'Failed to update stock.',
});
} }
} catch (error) { } catch (error) {
NotifAlert({ icon: 'error', title: 'Error', message: error.message || 'An error occurred.' }); NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'An error occurred.',
});
} finally { } finally {
setLoadingQuantities(prev => ({ ...prev, [item.sparepart_id]: false })); setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: false }));
} }
}; };
@@ -80,7 +107,9 @@ const SparepartCardList = ({
style={{ style={{
borderRadius: '8px', borderRadius: '8px',
overflow: 'hidden', overflow: 'hidden',
border: `1px solid ${fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'}`, border: `1px solid ${
fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'
}`,
}} }}
bodyStyle={{ padding: 0 }} bodyStyle={{ padding: 0 }}
> >
@@ -106,42 +135,79 @@ const SparepartCardList = ({
{item.sparepart_item_type} {item.sparepart_item_type}
</Tag> </Tag>
)} )}
<div style={{ <div
backgroundColor: '#f0f0f0', style={{
width: '100%', backgroundColor: '#f0f0f0',
padding: '8px', width: '100%',
borderRadius: '4px', padding: '8px',
display: 'flex', borderRadius: '4px',
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
aspectRatio: '1 / 1', justifyContent: 'center',
}}> aspectRatio: '1 / 1',
<img }}
src={item.image_url || "https://via.placeholder.com/150"} >
alt={item[header]} {(() => {
style={{ // Debug: log the image path construction
maxWidth: '100%', let imgSrc;
maxHeight: '100%', if (item.sparepart_foto) {
objectFit: 'contain', if (item.sparepart_foto.startsWith('http')) {
}} imgSrc = item.sparepart_foto;
/> } else {
// Gunakan format file URL seperti di brandDevice
const fileName = item.sparepart_foto.split('/').pop();
// Gunakan API getFileUrl untuk mendapatkan URL yang benar
const token = localStorage.getItem('token');
const baseURL = import.meta.env.VITE_API_SERVER || '';
imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
}
console.log('Image path being constructed:', imgSrc);
} else {
imgSrc = 'https://via.placeholder.com/150';
}
return (
<img
src={imgSrc}
alt={item[header]}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
onError={(e) => {
console.error('Image failed to load:', imgSrc);
e.target.src = 'https://via.placeholder.com/150';
}}
onLoad={() => console.log('Image loaded successfully:', imgSrc)}
/>
);
})()}
</div> </div>
</div> </div>
</Col> </Col>
<Col span={16}> <Col span={16}>
<div style={{ padding: '16px', position: 'relative', height: '100%' }}> <div
style={{
padding: '16px',
position: 'relative',
height: '100%',
}}
>
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: 8, top: 8,
right: 8, right: 8,
display: 'flex', display: 'flex',
gap: '8px' gap: '8px',
}} }}
> >
{showEditModal && ( {showEditModal && (
<Button <Button
style={{ color: '#faad14', borderColor: '#faad14' }} style={{
color: '#faad14',
borderColor: '#faad14',
}}
icon={<EditOutlined />} icon={<EditOutlined />}
key="edit" key="edit"
onClick={() => showEditModal(item)} onClick={() => showEditModal(item)}
@@ -158,7 +224,17 @@ const SparepartCardList = ({
/> />
)} )}
</div> </div>
<Title level={5} style={{ margin: 0, marginBottom: '8px', paddingRight: '60px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Title
level={5}
style={{
margin: 0,
marginBottom: '8px',
paddingRight: '60px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{item[header]} {item[header]}
</Title> </Title>
<Text type="secondary"> <Text type="secondary">
@@ -166,23 +242,49 @@ const SparepartCardList = ({
</Text> </Text>
<Divider style={{ margin: '8px 0' }} /> <Divider style={{ margin: '8px 0' }} />
<Space align="center" style={{ marginBottom: '8px', display: 'flex', justifyContent: 'center' }}> <Space
align="center"
style={{
marginBottom: '8px',
display: 'flex',
justifyContent: 'center',
}}
>
<Button <Button
icon={<MinusOutlined />} icon={<MinusOutlined />}
onClick={() => handleQuantityChange(item.sparepart_id, quantity - 1)} onClick={() =>
handleQuantityChange(
item.sparepart_id,
quantity - 1
)
}
disabled={isLoading} disabled={isLoading}
style={{ width: 28, height: 28 }} style={{ width: 28, height: 28 }}
/> />
<Text strong style={{ padding: '0 8px', fontSize: '16px' }}>{quantity}</Text> <Text
strong
style={{ padding: '0 8px', fontSize: '16px' }}
>
{quantity}
</Text>
<Button <Button
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => handleQuantityChange(item.sparepart_id, quantity + 1)} onClick={() =>
handleQuantityChange(
item.sparepart_id,
quantity + 1
)
}
disabled={isLoading} disabled={isLoading}
style={{ width: 28, height: 28 }} style={{ width: 28, height: 28 }}
/> />
<Text type="secondary">{item.sparepart_unit ? ` / ${item.sparepart_unit}` : ' / pcs'}</Text> <Text type="secondary">
{item.sparepart_unit
? ` / ${item.sparepart_unit}`
: ' / pcs'}
</Text>
</Space> </Space>
<Button <Button
type={quantity === 0 ? 'default' : 'primary'} type={quantity === 0 ? 'default' : 'primary'}
size="small" size="small"
@@ -192,10 +294,20 @@ const SparepartCardList = ({
> >
Update Stock Update Stock
</Button> </Button>
<br /> <br />
<Text type="secondary" style={{ fontSize: '12px', marginTop: '8px', display: 'inline-block' }}> <Text
Last updated: {item.updated_at ? dayjs(item.updated_at).format('DD MMM YYYY') : 'N/A'} type="secondary"
style={{
fontSize: '12px',
marginTop: '8px',
display: 'inline-block',
}}
>
Last updated:{' '}
{item.updated_at
? dayjs(item.updated_at).format('DD MMM YYYY')
: 'N/A'}
</Text> </Text>
</div> </div>
</Col> </Col>
@@ -208,4 +320,4 @@ const SparepartCardList = ({
); );
}; };
export default SparepartCardList; export default SparepartCardList;