lavoce #25

Merged
bragaz_rexita merged 2 commits from lavoce into main 2025-11-28 07:12:41 +00:00
9 changed files with 468 additions and 299 deletions

View File

@@ -20,36 +20,65 @@ html body {
height: 100vh; height: 100vh;
} }
/* Custom Orange Sidebar Menu Styles */ /* Custom green Sidebar Menu Styles */
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected { .custom-green-menu.ant-menu-dark .ant-menu-item-selected {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
color: white !important; color: white !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected::after { .custom-green-menu.ant-menu-dark .ant-menu-item-selected::after {
border-right-color: white !important; border-right-color: white !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item:hover, .custom-green-menu.ant-menu-dark .ant-menu-item:hover,
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title:hover { .custom-green-menu.ant-menu-dark .ant-menu-submenu-title:hover {
background-color: rgba(255, 255, 255, 0.15) !important; background-color: rgba(255, 255, 255, 0.15) !important;
color: white !important; color: white !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title { .custom-green-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
color: white !important; color: white !important;
} }
.custom-orange-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub { .custom-green-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub {
background: rgba(0, 0, 0, 0.2) !important; background: rgba(0, 0, 0, 0.2) !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item, .custom-green-menu.ant-menu-dark .ant-menu-item,
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title { .custom-green-menu.ant-menu-dark .ant-menu-submenu-title {
color: rgba(255, 255, 255, 0.9) !important; color: rgba(255, 255, 255, 0.9) !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item-active, .custom-green-menu.ant-menu-dark .ant-menu-item-active,
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title { .custom-green-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title {
color: white !important; color: white !important;
} }
/*start styling for scrollbar menu */
.custom-menu-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-menu-scrollbar::-webkit-scrollbar-track {
background: transparent;
border-radius: 10px;
margin: 5px 0;
}
.custom-menu-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #1BAA56 0%, rgb(5, 75, 34) 100%);
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.custom-menu-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #2bc56d 0%, rgb(8, 94, 43) 100%);
}
.custom-menu-scrollbar {
scrollbar-width: thin;
scrollbar-color: #1BAA56 transparent;
}
/* Hilangkan panah atas/bawah */
.custom-menu-scrollbar::-webkit-scrollbar-button {
display: none !important;
width: 0 !important;
height: 0 !important;
}
/*end styling for scrollbar menu */

View File

@@ -422,7 +422,7 @@ const LayoutMenu = () => {
border: 'none', border: 'none',
}} }}
theme="dark" theme="dark"
className="custom-orange-menu" className="custom-green-menu"
/> />
); );
}; };

View File

@@ -30,8 +30,24 @@ const LayoutSidebar = () => {
zIndex: 9999 zIndex: 9999
}} }}
> >
<div style={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'hidden'
}}>
{/* Logo section - fixed height */}
<div style={{flexShrink: 0,minHeight: '64px'}}>
<LayoutLogo /> <LayoutLogo />
</div>
{/* Menu section - scrollable */}
<div style={{flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column'}}>
<div className="custom-menu-scrollbar" style={{flex: 1, overflowY: 'auto', overflowX: 'hidden', backgroundColor: 'transparent'}}>
<LayoutMenu /> <LayoutMenu />
</div>
</div>
</div>
</Sider> </Sider>
); );
}; };

View File

@@ -56,6 +56,7 @@ const AddBrandDevice = () => {
const [formData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
const [errorCodes, setErrorCodes] = useState([]); const [errorCodes, setErrorCodes] = useState([]);
const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const { const {
solutionFields, solutionFields,
@@ -73,19 +74,33 @@ const AddBrandDevice = () => {
setSolutionsForExistingRecord, setSolutionsForExistingRecord,
} = useSolutionLogic(solutionForm); } = useSolutionLogic(solutionForm);
const { // For spareparts, we'll use the local state directly since it's just an array of IDs
sparepartFields, const handleSparepartChange = (values) => {
sparepartTypes, setSelectedSparepartIds(values || []);
sparepartStatuses, };
sparepartsToDelete,
handleAddSparepartField, const resetSparepartFields = () => {
handleRemoveSparepartField, setSelectedSparepartIds([]);
handleSparepartTypeChange, };
handleSparepartStatusChange,
resetSparepartFields, const getSparepartData = () => {
getSparepartData, return selectedSparepartIds;
setSparepartsForExistingRecord, };
} = useSparepartLogic(sparepartForm);
const setSparepartsForExistingRecord = (sparepartData) => {
if (!sparepartData) {
setSelectedSparepartIds([]);
return;
}
if (Array.isArray(sparepartData)) {
setSelectedSparepartIds(sparepartData);
} else if (typeof sparepartData === 'object' && sparepartData.spareparts) {
setSelectedSparepartIds(sparepartData.spareparts || []);
} else {
setSelectedSparepartIds(sparepartData.map(sp => sp.sparepart_id || sp.brand_sparepart_id || sp.id).filter(id => id));
}
};
useEffect(() => { useEffect(() => {
setBreadcrumbItems([ setBreadcrumbItems([
@@ -155,22 +170,16 @@ const AddBrandDevice = () => {
path_solution: sol.path_solution || '', path_solution: sol.path_solution || '',
is_active: sol.is_active !== false, is_active: sol.is_active !== false,
})), })),
...(ec.sparepart && ec.sparepart.length > 0 && {
sparepart: ec.sparepart.map((sp) => ({
sparepart_name: sp.sparepart_name || sp.name || sp.label || '',
brand_sparepart_description: sp.brand_sparepart_description || sp.description || sp.sparepart_description || '',
is_active: sp.is_active !== false,
path_foto: sp.path_foto || '',
})),
}),
})); }));
const sparepartData = getSparepartData();
const finalFormData = { const finalFormData = {
brand_name: formData.brand_name, brand_name: formData.brand_name,
brand_type: formData.brand_type || '', brand_type: formData.brand_type || '',
brand_model: formData.brand_model || '', brand_model: formData.brand_model || '',
brand_manufacture: formData.brand_manufacture, brand_manufacture: formData.brand_manufacture,
is_active: formData.is_active, is_active: formData.is_active,
spareparts: sparepartData,
error_code: transformedErrorCodes, error_code: transformedErrorCodes,
}; };
@@ -222,7 +231,7 @@ const AddBrandDevice = () => {
} }
if (record.sparepart && record.sparepart.length > 0) { if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart, sparepartForm); setSparepartsForExistingRecord(record.sparepart);
} }
}; };
@@ -254,6 +263,10 @@ const AddBrandDevice = () => {
} else { } else {
resetSolutionFields(); resetSolutionFields();
} }
if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart);
}
}; };
const handleAddErrorCode = async () => { const handleAddErrorCode = async () => {
@@ -282,7 +295,6 @@ const AddBrandDevice = () => {
return; return;
} }
const sparepartData = getSparepartData();
const newErrorCode = { const newErrorCode = {
key: Date.now(), key: Date.now(),
error_code: formValues.error_code, error_code: formValues.error_code,
@@ -293,7 +305,6 @@ const AddBrandDevice = () => {
status: formValues.status !== false, status: formValues.status !== false,
errorCodeIcon: errorCodeIcon, errorCodeIcon: errorCodeIcon,
solution: solutions, solution: solutions,
...(sparepartData && sparepartData.length > 0 && { sparepart: sparepartData }), // Only add sparepart if there are spareparts
}; };
if (editingErrorCodeKey) { if (editingErrorCodeKey) {
@@ -516,22 +527,16 @@ const AddBrandDevice = () => {
</Form> </Form>
</Card> </Card>
</Col> </Col>
{/* Sparepart Form Column */}
<Col span={8}> <Col span={8}>
<Card size="small" title="Spareparts"> <Card size="small" title="Spareparts">
<Form <Form
form={sparepartForm} form={sparepartForm}
layout="vertical" layout="vertical"
initialValues={{
sparepart_status_0: true,
}}
> >
<SparepartForm <SparepartForm
sparepartForm={sparepartForm} sparepartForm={sparepartForm}
sparepartFields={sparepartFields} selectedSparepartIds={selectedSparepartIds}
onAddSparepartField={handleAddSparepartField} onSparepartChange={handleSparepartChange}
onRemoveSparepartField={handleRemoveSparepartField}
isReadOnly={isErrorCodeFormReadOnly} isReadOnly={isErrorCodeFormReadOnly}
/> />
</Form> </Form>

View File

@@ -61,6 +61,7 @@ const EditBrandDevice = () => {
const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [solutionForm] = Form.useForm(); const [solutionForm] = Form.useForm();
const [sparepartForm] = Form.useForm(); const [sparepartForm] = Form.useForm();
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic( const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic(
errorCodeForm, errorCodeForm,
@@ -80,19 +81,33 @@ const EditBrandDevice = () => {
setSolutionsForExistingRecord, setSolutionsForExistingRecord,
} = useSolutionLogic(solutionForm); } = useSolutionLogic(solutionForm);
const { // For spareparts, we'll use the local state directly since it's just an array of IDs
sparepartFields, const handleSparepartChange = (values) => {
sparepartTypes, setSelectedSparepartIds(values || []);
sparepartStatuses, };
sparepartsToDelete,
handleAddSparepartField, const resetSparepartFields = () => {
handleRemoveSparepartField, setSelectedSparepartIds([]);
handleSparepartTypeChange, };
handleSparepartStatusChange,
resetSparepartFields, const getSparepartData = () => {
getSparepartData, return selectedSparepartIds;
setSparepartsForExistingRecord, };
} = useSparepartLogic(sparepartForm);
const setSparepartsForExistingRecord = (sparepartData) => {
if (!sparepartData) {
setSelectedSparepartIds([]);
return;
}
if (Array.isArray(sparepartData)) {
setSelectedSparepartIds(sparepartData);
} else if (typeof sparepartData === 'object' && sparepartData.spareparts) {
setSelectedSparepartIds(sparepartData.spareparts || []);
} else {
setSelectedSparepartIds(sparepartData.map(sp => sp.sparepart_id || sp.brand_sparepart_id || sp.id).filter(id => id));
}
};
useEffect(() => { useEffect(() => {
const fetchBrandData = async () => { const fetchBrandData = async () => {
@@ -176,6 +191,14 @@ const EditBrandDevice = () => {
setFormData(newFormData); setFormData(newFormData);
brandForm.setFieldsValue(newFormData); brandForm.setFieldsValue(newFormData);
setErrorCodes(existingErrorCodes); setErrorCodes(existingErrorCodes);
// Set the selected sparepart IDs if available in the response
if (response.data.spareparts) {
// Extract the IDs from the spareparts objects
const sparepartIds = response.data.spareparts.map(sp => sp.sparepart_id);
setSelectedSparepartIds(sparepartIds);
setSparepartsForExistingRecord(sparepartIds);
}
} else { } else {
NotifAlert({ NotifAlert({
icon: 'error', icon: 'error',
@@ -244,12 +267,6 @@ const EditBrandDevice = () => {
path_solution: sol.path_solution || '', path_solution: sol.path_solution || '',
is_active: sol.is_active !== false, is_active: sol.is_active !== false,
})), })),
sparepart: (ec.sparepart || []).map((sp) => ({
sparepart_name: sp.sparepart_name || sp.name || sp.label || '',
brand_sparepart_description: sp.brand_sparepart_description || sp.description || sp.brand_sparepart_description || '',
is_active: sp.is_active !== false,
path_foto: sp.path_foto || '',
})),
}; };
} }
@@ -268,19 +285,17 @@ const EditBrandDevice = () => {
path_solution: sol.path_solution || '', path_solution: sol.path_solution || '',
is_active: sol.is_active !== false, is_active: sol.is_active !== false,
})), })),
...(ec.sparepart && ec.sparepart.length > 0 && {
sparepart: ec.sparepart.map((sp) => ({
sparepart_name: sp.sparepart_name || sp.name || sp.label || '',
brand_sparepart_description: sp.brand_sparepart_description || sp.description || sp.brand_sparepart_description || '',
is_active: sp.is_active !== false,
path_foto: sp.path_foto || '',
})),
}),
}; };
}), }),
}; };
const response = await updateBrand(id, finalFormData); const sparepartData = getSparepartData();
const updatedFinalFormData = {
...finalFormData,
spareparts: sparepartData,
};
const response = await updateBrand(id, updatedFinalFormData);
if (response && (response.statusCode === 200 || response.statusCode === 201)) { if (response && (response.statusCode === 200 || response.statusCode === 201)) {
localStorage.removeItem(`brand_device_edit_${id}_temp_data`); localStorage.removeItem(`brand_device_edit_${id}_temp_data`);
@@ -329,7 +344,7 @@ const EditBrandDevice = () => {
// Load spareparts to sparepart form // Load spareparts to sparepart form
if (record.sparepart && record.sparepart.length > 0) { if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart, sparepartForm); setSparepartsForExistingRecord(record.sparepart);
} else { } else {
resetSparepartFields(); resetSparepartFields();
} }
@@ -354,7 +369,7 @@ const EditBrandDevice = () => {
// Load spareparts to sparepart form // Load spareparts to sparepart form
if (record.sparepart && record.sparepart.length > 0) { if (record.sparepart && record.sparepart.length > 0) {
setSparepartsForExistingRecord(record.sparepart, sparepartForm); setSparepartsForExistingRecord(record.sparepart);
} }
const formElement = document.querySelector('.ant-form'); const formElement = document.querySelector('.ant-form');
@@ -624,15 +639,11 @@ const EditBrandDevice = () => {
<Form <Form
form={sparepartForm} form={sparepartForm}
layout="vertical" layout="vertical"
initialValues={{
sparepart_status_0: true,
}}
> >
<SparepartForm <SparepartForm
sparepartForm={sparepartForm} sparepartForm={sparepartForm}
sparepartFields={sparepartFields} selectedSparepartIds={selectedSparepartIds}
onAddSparepartField={handleAddSparepartField} onSparepartChange={handleSparepartChange}
onRemoveSparepartField={handleRemoveSparepartField}
isReadOnly={isErrorCodeFormReadOnly} isReadOnly={isErrorCodeFormReadOnly}
/> />
</Form> </Form>

View File

@@ -0,0 +1,310 @@
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Image, Typography, Tag, Space, Spin, Button, Empty } from 'antd';
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons';
import { getAllSparepart } from '../../../../api/sparepart';
const { Text, Title } = Typography;
const SparepartCardSelect = ({
selectedSparepartIds = [],
onSparepartChange,
isLoading: externalLoading = false,
isReadOnly = false
}) => {
const [spareparts, setSpareparts] = useState([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
loadSpareparts();
}, []);
const loadSpareparts = async () => {
setLoading(true);
try {
const params = new URLSearchParams();
params.set('limit', '1000'); // Get all spareparts
const response = await getAllSparepart(params);
if (response && (response.statusCode === 200 || response.data)) {
const sparepartData = response.data?.data || response.data || [];
setSpareparts(sparepartData);
} else {
// For demo purposes, use mock data if API fails
setSpareparts([
{
sparepart_id: 1,
sparepart_name: 'Compressor Oil Filter',
sparepart_description: 'Oil filter for compressor',
sparepart_foto: null,
sparepart_code: 'SP-001',
sparepart_merk: 'Brand A',
sparepart_model: 'Model X'
},
{
sparepart_id: 2,
sparepart_name: 'Air Intake Filter',
sparepart_description: 'Air intake filter',
sparepart_foto: null,
sparepart_code: 'SP-002',
sparepart_merk: 'Brand B',
sparepart_model: 'Model Y'
},
{
sparepart_id: 3,
sparepart_name: 'Cooling Fan Motor',
sparepart_description: 'Motor for cooling fan',
sparepart_foto: null,
sparepart_code: 'SP-003',
sparepart_merk: 'Brand C',
sparepart_model: 'Model Z'
},
]);
}
} catch (error) {
console.error('Error loading spareparts:', error);
// Default mock data
setSpareparts([
{
sparepart_id: 1,
sparepart_name: 'Compressor Oil Filter',
sparepart_description: 'Oil filter for compressor',
sparepart_foto: null,
sparepart_code: 'SP-001',
sparepart_merk: 'Brand A',
sparepart_model: 'Model X'
},
{
sparepart_id: 2,
sparepart_name: 'Air Intake Filter',
sparepart_description: 'Air intake filter',
sparepart_foto: null,
sparepart_code: 'SP-002',
sparepart_merk: 'Brand B',
sparepart_model: 'Model Y'
},
{
sparepart_id: 3,
sparepart_name: 'Cooling Fan Motor',
sparepart_description: 'Motor for cooling fan',
sparepart_foto: null,
sparepart_code: 'SP-003',
sparepart_merk: 'Brand C',
sparepart_model: 'Model Z'
},
]);
} finally {
setLoading(false);
}
};
const filteredSpareparts = spareparts.filter(sp =>
sp.sparepart_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
sp.sparepart_code.toLowerCase().includes(searchTerm.toLowerCase()) ||
sp.sparepart_merk?.toLowerCase().includes(searchTerm.toLowerCase()) ||
sp.sparepart_model?.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleSparepartToggle = (sparepartId) => {
if (isReadOnly) return;
const newSelectedIds = selectedSparepartIds.includes(sparepartId)
? selectedSparepartIds.filter(id => id !== sparepartId)
: [...selectedSparepartIds, sparepartId];
onSparepartChange(newSelectedIds);
};
const isSelected = (sparepartId) => selectedSparepartIds.includes(sparepartId);
const combinedLoading = loading || externalLoading;
return (
<div>
<div style={{ marginBottom: 16 }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Title level={5} style={{ margin: 0 }}>
Select Spareparts
</Title>
<div style={{ position: 'relative', width: '200px' }}>
<input
type="text"
placeholder="Search spareparts..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
padding: '8px 30px 8px 12px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
width: '100%'
}}
/>
<SearchOutlined
style={{
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
color: '#bfbfbf'
}}
/>
</div>
</Space>
</div>
{combinedLoading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Spin size="large" />
</div>
) : filteredSpareparts.length === 0 ? (
<Empty
description="No spareparts found"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
) : (
<Row gutter={[16, 16]}>
{filteredSpareparts.map(sparepart => (
<Col span={8} key={sparepart.sparepart_id}>
<Card
size="small"
hoverable
style={{
border: isSelected(sparepart.sparepart_id)
? '2px solid #23A55A'
: '1px solid #d9d9d9',
backgroundColor: isSelected(sparepart.sparepart_id)
? '#f6ffed'
: 'white',
cursor: isReadOnly ? 'default' : 'pointer',
position: 'relative'
}}
onClick={() => handleSparepartToggle(sparepart.sparepart_id)}
>
<div style={{ position: 'absolute', top: 8, right: 8 }}>
{isSelected(sparepart.sparepart_id) ? (
<CheckCircleOutlined
style={{
fontSize: '18px',
color: '#23A55A',
backgroundColor: 'white',
borderRadius: '50%'
}}
/>
) : (
<CloseCircleOutlined
style={{
fontSize: '18px',
color: '#d9d9d9',
backgroundColor: 'white',
borderRadius: '50%'
}}
/>
)}
</div>
<div style={{ textAlign: 'center', marginBottom: 12 }}>
<div style={{
width: '100%',
height: 120,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
borderRadius: 8,
overflow: 'hidden'
}}>
{sparepart.sparepart_foto ? (
<Image
src={sparepart.sparepart_foto}
alt={sparepart.sparepart_name}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain'
}}
preview={false}
fallback="/assets/defaultSparepartImg.jpg"
/>
) : (
<div style={{
color: '#bfbfbf',
fontSize: 12
}}>
No Image
</div>
)}
</div>
</div>
<div>
<Text
strong
style={{
display: 'block',
fontSize: '14px',
marginBottom: 4,
color: isSelected(sparepart.sparepart_id) ? '#23A55A' : 'inherit'
}}
>
{sparepart.sparepart_name}
</Text>
<Text
type="secondary"
style={{
fontSize: '12px',
display: 'block',
marginBottom: 4
}}
>
{sparepart.sparepart_description || 'No description'}
</Text>
<Space size="small" style={{ marginBottom: 4 }}>
<Tag color="blue" style={{ margin: 0 }}>
{sparepart.sparepart_code}
</Tag>
<Tag color="geekblue" style={{ margin: 0 }}>
{sparepart.sparepart_merk || 'N/A'}
</Tag>
</Space>
{sparepart.sparepart_model && (
<div style={{
fontSize: '12px',
color: '#666'
}}>
Model: {sparepart.sparepart_model}
</div>
)}
</div>
</Card>
</Col>
))}
</Row>
)}
{selectedSparepartIds.length > 0 && (
<div style={{ marginTop: 16 }}>
<Text strong>Selected Spareparts: </Text>
<Space wrap>
{selectedSparepartIds.map(id => {
const sparepart = spareparts.find(sp => sp.sparepart_id === id);
return sparepart ? (
<Tag key={id} color="green">
{sparepart.sparepart_name} (ID: {id})
</Tag>
) : (
<Tag key={id} color="green">
Sparepart ID: {id}
</Tag>
);
})}
</Space>
</div>
)}
</div>
);
};
export default SparepartCardSelect;

View File

@@ -1,152 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Form, Select, Button, Switch, Typography, Space, Input, message } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { getAllSparepart } from '../../../../api/sparepart';
const { Text } = Typography;
const SparepartField = ({
fieldKey,
fieldName,
index,
sparepartType,
sparepartStatus,
isReadOnly = false,
canRemove = true,
onRemove,
spareparts = [],
onSparepartChange
}) => {
const [currentStatus, setCurrentStatus] = useState(sparepartStatus ?? true);
const [sparepartList, setSparepartList] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setCurrentStatus(sparepartStatus ?? true);
loadSpareparts();
}, [sparepartStatus]);
const loadSpareparts = async () => {
setLoading(true);
try {
// Get all spareparts from the API
const params = new URLSearchParams();
params.set('limit', '100'); // Get all spareparts
const response = await getAllSparepart(params);
// Response structure should have { data: [...], statusCode: 200 }
if (response && (response.statusCode === 200 || response.data)) {
// If response has data array directly
const sparepartData = response.data?.data || response.data || [];
setSparepartList(sparepartData);
if (onSparepartChange) {
onSparepartChange(sparepartData);
}
} else {
// For demo purposes, use mock data if API fails
setSparepartList([
{ brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' },
{ brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' },
{ brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' },
]);
if (onSparepartChange) {
onSparepartChange([
{ brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' },
{ brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' },
{ brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' },
]);
}
}
} catch (error) {
console.error('Error loading spareparts:', error);
// Default mock data
const mockSpareparts = [
{ brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' },
{ brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' },
{ brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' },
];
setSparepartList(mockSpareparts);
if (onSparepartChange) {
onSparepartChange(mockSpareparts);
}
} finally {
setLoading(false);
}
};
const sparepartOptions = sparepartList.map(sparepart => ({
label: sparepart.sparepart_name || sparepart.sparepart_name || `Sparepart ${sparepart.sparepart_id || sparepart.brand_sparepart_id}`,
value: sparepart.sparepart_id || sparepart.brand_sparepart_id,
description: sparepart.sparepart_description
}));
return (
<div style={{
border: '1px solid #d9d9d9',
borderRadius: 8,
padding: 16,
marginBottom: 16,
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Text strong>Sparepart #{index + 1}</Text>
<Space>
<Form.Item
name={[fieldName, 'sparepart_id']}
rules={[{ required: false, message: 'Sparepart wajib dipilih!' }]} /* Making it optional since sparepart is optional */
style={{ margin: 0, width: 200 }}
>
<Select
placeholder="Pilih sparepart"
loading={loading}
disabled={isReadOnly}
options={sparepartOptions}
showSearch
optionFilterProp="label"
/>
</Form.Item>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Form.Item name={[fieldName, 'status']} valuePropName="checked" noStyle>
<Switch
disabled={isReadOnly}
onChange={(checked) => {
setCurrentStatus(checked);
}}
style={{
backgroundColor: currentStatus ? '#23A55A' : '#bfbfbf'
}}
/>
</Form.Item>
<Text style={{ fontSize: 12, color: '#666' }}>
{currentStatus ? 'Active' : 'Inactive'}
</Text>
</div>
{canRemove && !isReadOnly && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={onRemove}
/>
)}
</Space>
</div>
{/* Sparepart Description */}
<Form.Item
name={[fieldName, 'description']}
label="Deskripsi"
>
<Input.TextArea
placeholder="Deskripsi sparepart"
rows={2}
disabled={isReadOnly}
/>
</Form.Item>
</div>
);
};
export default SparepartField;

View File

@@ -1,74 +1,24 @@
import React, { useState } from 'react'; import React from 'react';
import { Form, Card, Typography, Divider, Button } from 'antd'; import { Card, Divider, Typography } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import SparepartCardSelect from './SparepartCardSelect';
import SparepartField from './SparepartField';
const { Text } = Typography; const { Text } = Typography;
const SparepartForm = ({ const SparepartForm = ({
sparepartForm, sparepartForm,
sparepartFields, selectedSparepartIds,
onAddSparepartField, onSparepartChange,
onRemoveSparepartField, isReadOnly = false
isReadOnly = false,
spareparts = [],
onSparepartChange
}) => { }) => {
const [sparepartList, setSparepartList] = useState([]);
const handleSparepartChange = (list) => {
setSparepartList(list);
if (onSparepartChange) {
onSparepartChange(list);
}
};
return ( return (
<div> <div>
<Form <Card size="small" title="Spareparts">
form={sparepartForm} <SparepartCardSelect
layout="vertical" selectedSparepartIds={selectedSparepartIds}
initialValues={{ onSparepartChange={onSparepartChange}
sparepart_status_0: true,
}}
>
<Divider orientation="left">Sparepart Items</Divider>
{sparepartFields.map((field, index) => (
<SparepartField
key={field.key}
fieldKey={field.key}
fieldName={field.name}
index={index}
sparepartStatus={field.status}
onRemove={() => onRemoveSparepartField(field.key)}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
canRemove={sparepartFields.length > 1}
spareparts={sparepartList}
onSparepartChange={handleSparepartChange}
/> />
))} </Card>
{!isReadOnly && (
<>
<Form.Item>
<Button
type="dashed"
onClick={onAddSparepartField}
icon={<PlusOutlined />}
style={{ width: '100%' }}
>
+ Add Sparepart
</Button>
</Form.Item>
<div style={{ marginTop: 16 }}>
<Text type="secondary">
* Sparepart is optional and can be added for each error code if needed.
</Text>
</div>
</>
)}
</Form>
</div> </div>
); );
}; };