lavoce #27
@@ -20,7 +20,6 @@ const CustomSparepartCard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
||||||
|
|
||||||
// Construct image source with proper fallback
|
|
||||||
const getImageSrc = () => {
|
const getImageSrc = () => {
|
||||||
if (sparepart.sparepart_foto) {
|
if (sparepart.sparepart_foto) {
|
||||||
if (sparepart.sparepart_foto.startsWith('http')) {
|
if (sparepart.sparepart_foto.startsWith('http')) {
|
||||||
@@ -56,7 +55,6 @@ const CustomSparepartCard = ({
|
|||||||
const getCardActions = () => {
|
const getCardActions = () => {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
// Preview button
|
|
||||||
if (showPreview) {
|
if (showPreview) {
|
||||||
actions.push(
|
actions.push(
|
||||||
<Button
|
<Button
|
||||||
@@ -73,7 +71,6 @@ const CustomSparepartCard = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete button without confirmation
|
|
||||||
if (showDelete && !isReadOnly) {
|
if (showDelete && !isReadOnly) {
|
||||||
actions.push(
|
actions.push(
|
||||||
<Button
|
<Button
|
||||||
@@ -93,7 +90,6 @@ const CustomSparepartCard = ({
|
|||||||
return actions;
|
return actions;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get card styling based on size
|
|
||||||
const getCardStyle = () => {
|
const getCardStyle = () => {
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
@@ -132,11 +128,13 @@ const CustomSparepartCard = ({
|
|||||||
<Card
|
<Card
|
||||||
hoverable={!!onCardClick && !isReadOnly}
|
hoverable={!!onCardClick && !isReadOnly}
|
||||||
style={getCardStyle()}
|
style={getCardStyle()}
|
||||||
bodyStyle={{
|
styles={{
|
||||||
padding: 0,
|
body: {
|
||||||
height: 'calc(100% - 48px)',
|
padding: 0,
|
||||||
display: 'flex',
|
height: 'calc(100% - 48px)',
|
||||||
flexDirection: 'column'
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
actions={getCardActions()}
|
actions={getCardActions()}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
@@ -244,55 +242,65 @@ const CustomSparepartCard = ({
|
|||||||
{sparepart.sparepart_name || sparepart.name || 'Unnamed'}
|
{sparepart.sparepart_name || sparepart.name || 'Unnamed'}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{size !== 'small' && (
|
{/* Stock and Quantity Information */}
|
||||||
<>
|
<div style={{ marginBottom: size === 'small' ? '6px' : '8px' }}>
|
||||||
<Text
|
{/* Stock Status Tag */}
|
||||||
type="secondary"
|
<div style={{ marginBottom: '4px' }}>
|
||||||
|
<Tag
|
||||||
|
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: size === 'small' ? '9px' : '10px',
|
||||||
display: 'block',
|
padding: '0 4px',
|
||||||
marginBottom: '6px',
|
margin: 0,
|
||||||
overflow: 'hidden',
|
height: 'auto'
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Stock: {sparepart.sparepart_stock || sparepart.sparepart_stok || '0'} {sparepart.sparepart_unit || 'pcs'}
|
{sparepart.sparepart_stok || 'Not Available'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quantity */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: size === 'small' ? '10px' : '11px',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#262626',
|
||||||
|
marginRight: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
qty:
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: size === 'small' ? '10px' : '11px',
|
||||||
|
color: sparepart.sparepart_qty > 0 ? '#52c41a' : '#ff4d4f',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sparepart.sparepart_qty || 0}
|
||||||
|
{sparepart.sparepart_unit ? ` ${sparepart.sparepart_unit}` : ''}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '6px' }}>
|
<div style={{ marginBottom: size === 'small' ? '4px' : '6px' }}>
|
||||||
<Text
|
|
||||||
code
|
|
||||||
style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: '3px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sparepart.sparepart_code || 'No code'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{size === 'small' && (
|
|
||||||
<Text
|
<Text
|
||||||
code
|
code
|
||||||
style={{
|
style={{
|
||||||
fontSize: '10px',
|
fontSize: size === 'small' ? '10px' : '11px',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
display: 'block',
|
padding: '2px 6px',
|
||||||
marginBottom: '4px'
|
borderRadius: '3px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sparepart.sparepart_code || 'No code'}
|
{sparepart.sparepart_code || 'No code'}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{(sparepart.sparepart_merk || sparepart.sparepart_model) && (
|
{(sparepart.sparepart_merk || sparepart.sparepart_model) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: size === 'small' ? '10px' : '11px',
|
fontSize: size === 'small' ? '9px' : '10px',
|
||||||
color: '#666',
|
color: '#666',
|
||||||
lineHeight: '1.4',
|
lineHeight: '1.4',
|
||||||
marginBottom: '4px'
|
marginBottom: '4px'
|
||||||
@@ -332,23 +340,24 @@ const CustomSparepartCard = ({
|
|||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
width={700}
|
width={800}
|
||||||
centered
|
centered
|
||||||
bodyStyle={{ padding: '24px' }}
|
styles={{ body: { padding: '24px' } }}
|
||||||
>
|
>
|
||||||
<Row gutter={[24, 24]}>
|
<Row gutter={[24, 24]}>
|
||||||
<Col span={8}>
|
<Col span={10}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f0f0f0',
|
backgroundColor: '#f0f0f0',
|
||||||
width: '200px',
|
width: '220px',
|
||||||
height: '200px',
|
height: '220px',
|
||||||
margin: '0 auto 16px',
|
margin: '0 auto 16px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius: '8px',
|
borderRadius: '12px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
border: '1px solid #E0E0E0',
|
border: '1px solid #E0E0E0',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -360,116 +369,138 @@ const CustomSparepartCard = ({
|
|||||||
objectFit: 'cover'
|
objectFit: 'cover'
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.src = 'https://via.placeholder.com/200x200/d9d9d9/666666?text=No+Image';
|
e.target.src = 'https://via.placeholder.com/220x220/d9d9d9/666666?text=No+Image';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Item Type Tag */}
|
||||||
{sparepart.sparepart_item_type && (
|
{sparepart.sparepart_item_type && (
|
||||||
<Tag color="blue" style={{ marginTop: '12px' }}>
|
<div style={{ marginBottom: '12px' }}>
|
||||||
{sparepart.sparepart_item_type}
|
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
|
||||||
</Tag>
|
{sparepart.sparepart_item_type}
|
||||||
)}
|
</Tag>
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col span={16}>
|
|
||||||
<div>
|
|
||||||
<Title level={3} style={{ marginBottom: '16px' }}>
|
|
||||||
{sparepart.sparepart_name || 'Unnamed'}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: '16px' }}>
|
|
||||||
<Col span={12}>
|
|
||||||
<div style={{ marginBottom: '8px' }}>
|
|
||||||
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
|
|
||||||
Code:
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: '16px', marginLeft: '8px' }}>
|
|
||||||
{sparepart.sparepart_code || 'N/A'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<div style={{ marginBottom: '8px' }}>
|
|
||||||
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
|
|
||||||
Status:
|
|
||||||
</Text>
|
|
||||||
<Tag
|
|
||||||
color={sparepart.is_active ? 'green' : 'red'}
|
|
||||||
style={{ marginLeft: '8px', fontSize: '14px' }}
|
|
||||||
>
|
|
||||||
{sparepart.is_active ? 'Active' : 'Inactive'}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{sparepart.sparepart_description && (
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
|
|
||||||
Description:
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: '16px', marginLeft: '8px' }}>
|
|
||||||
{sparepart.sparepart_description}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
{/* Stock Status and Quantity */}
|
||||||
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
|
<div style={{
|
||||||
Stock:
|
textAlign: 'left',
|
||||||
</Text>
|
background: '#f8f9fa',
|
||||||
<Text style={{ fontSize: '16px', marginLeft: '8px' }}>
|
padding: '12px',
|
||||||
{sparepart.sparepart_stock || sparepart.sparepart_stok || '0'}
|
borderRadius: '8px',
|
||||||
{sparepart.sparepart_unit ? ` ${sparepart.sparepart_unit}` : ' units'}
|
marginTop: '25px'
|
||||||
</Text>
|
}}>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<Text strong style={{ fontSize: '14px', color: '#262626' }}>Stock Status:</Text>
|
||||||
|
<Tag
|
||||||
|
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
|
||||||
|
style={{ marginLeft: '8px', fontSize: '12px' }}
|
||||||
|
>
|
||||||
|
{sparepart.sparepart_stok || 'Not Available'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong style={{ fontSize: '14px', color: '#262626' }}>Quantity:</Text>
|
||||||
|
<Text style={{ fontSize: '14px', marginLeft: '8px', fontWeight: 600 }}>
|
||||||
|
{sparepart.sparepart_qty || 0} {sparepart.sparepart_unit || ''}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={14}>
|
||||||
|
<div>
|
||||||
|
{/* Sparepart Name */}
|
||||||
|
<Title level={3} style={{ marginBottom: '20px', color: '#262626' }}>
|
||||||
|
{sparepart.sparepart_name || 'Unnamed'}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div style={{ marginBottom: '24px' }}>
|
||||||
|
<Row gutter={[16, 12]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<div style={{
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #f0f0f0'
|
||||||
|
}}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Code</Text>
|
||||||
|
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||||
|
{sparepart.sparepart_code || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Brand</Text>
|
||||||
|
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||||
|
{sparepart.sparepart_merk || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Unit</Text>
|
||||||
|
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||||
|
{sparepart.sparepart_unit || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{sparepart.sparepart_model && (
|
||||||
|
<Col span={24}>
|
||||||
|
<div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Model</Text>
|
||||||
|
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||||
|
{sparepart.sparepart_model}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sparepart.sparepart_description && (
|
||||||
|
<Col span={24}>
|
||||||
|
<div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Description</Text>
|
||||||
|
<div style={{ fontSize: '15px', marginTop: '2px', lineHeight: '1.5' }}>
|
||||||
|
{sparepart.sparepart_description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: '16px' }}>
|
{/* Timeline Information */}
|
||||||
{sparepart.sparepart_merk && (
|
{sparepart.created_at && (
|
||||||
<Col span={8}>
|
<div>
|
||||||
<div>
|
<Row gutter={16}>
|
||||||
<Text strong style={{ fontSize: '14px', color: '#262626' }}>
|
<Col span={12}>
|
||||||
Brand:
|
<div>
|
||||||
</Text>
|
<Text type="secondary" style={{ fontSize: '12px' }}>Created</Text>
|
||||||
<Text style={{ fontSize: '14px', marginLeft: '8px' }}>
|
<div style={{ fontSize: '13px', marginTop: '2px' }}>
|
||||||
{sparepart.sparepart_merk}
|
{dayjs(sparepart.created_at).format('DD MMM YYYY, HH:mm')}
|
||||||
</Text>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
<Col span={12}>
|
||||||
{sparepart.sparepart_model && (
|
<div>
|
||||||
<Col span={8}>
|
<Text type="secondary" style={{ fontSize: '12px' }}>Last Updated</Text>
|
||||||
<div>
|
<div style={{ fontSize: '13px', marginTop: '2px' }}>
|
||||||
<Text strong style={{ fontSize: '14px', color: '#262626' }}>
|
{dayjs(sparepart.updated_at).format('DD MMM YYYY, HH:mm')}
|
||||||
Model:
|
</div>
|
||||||
</Text>
|
</div>
|
||||||
<Text style={{ fontSize: '14px', marginLeft: '8px' }}>
|
</Col>
|
||||||
{sparepart.sparepart_model}
|
</Row>
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{sparepart.sparepart_unit && (
|
|
||||||
<Col span={8}>
|
|
||||||
<div>
|
|
||||||
<Text strong style={{ fontSize: '14px', color: '#262626' }}>
|
|
||||||
Unit:
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: '14px', marginLeft: '8px' }}>
|
|
||||||
{sparepart.sparepart_unit}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{sparepart.updated_at && (
|
|
||||||
<div style={{ marginTop: '24px', paddingTop: '16px', borderTop: '1px solid #f0f0f0' }}>
|
|
||||||
<Text type="secondary" style={{ fontSize: '14px' }}>
|
|
||||||
Last updated: {dayjs(sparepart.updated_at).format('DD MMMM YYYY, HH:mm')}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Card, Row, Col, Typography, Tag, Space, Spin, Button, Empty, message } from 'antd';
|
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
|
||||||
import { getAllSparepart } from '../../../../api/sparepart';
|
|
||||||
import { addSparepartToBrand, removeSparepartFromBrand } from '../../../../api/master-brand';
|
|
||||||
import CustomSparepartCard from './CustomSparepartCard';
|
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
|
||||||
|
|
||||||
const SparepartCardSelect = ({
|
|
||||||
selectedSparepartIds = [],
|
|
||||||
onSparepartChange,
|
|
||||||
isLoading: externalLoading = false,
|
|
||||||
isReadOnly = false,
|
|
||||||
brandId = null
|
|
||||||
}) => {
|
|
||||||
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) {
|
|
||||||
// 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 = async (sparepartId) => {
|
|
||||||
if (isReadOnly) return;
|
|
||||||
|
|
||||||
const isCurrentlySelected = selectedSparepartIds.includes(sparepartId);
|
|
||||||
|
|
||||||
// If brandId is provided, save immediately to database
|
|
||||||
if (brandId) {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (isCurrentlySelected) {
|
|
||||||
// Remove from database
|
|
||||||
await removeSparepartFromBrand(brandId, sparepartId);
|
|
||||||
message.success('Sparepart removed from brand successfully');
|
|
||||||
} else {
|
|
||||||
// Add to database
|
|
||||||
await addSparepartToBrand(brandId, sparepartId);
|
|
||||||
message.success('Sparepart added to brand successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
const newSelectedIds = isCurrentlySelected
|
|
||||||
? selectedSparepartIds.filter(id => id !== sparepartId)
|
|
||||||
: [...selectedSparepartIds, sparepartId];
|
|
||||||
|
|
||||||
onSparepartChange(newSelectedIds);
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || 'Failed to update sparepart');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no brandId (add mode), just update local state
|
|
||||||
const newSelectedIds = isCurrentlySelected
|
|
||||||
? 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 xs={24} sm={12} md={12} lg={12} key={sparepart.sparepart_id}>
|
|
||||||
<CustomSparepartCard
|
|
||||||
sparepart={sparepart}
|
|
||||||
isSelected={isSelected(sparepart.sparepart_id)}
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
showPreview={true}
|
|
||||||
showDelete={true}
|
|
||||||
onCardClick={() => handleSparepartToggle(sparepart.sparepart_id)}
|
|
||||||
onDelete={() => {
|
|
||||||
// When delete button is clicked, remove from selection
|
|
||||||
const newSelectedIds = selectedSparepartIds.filter(id => id !== sparepart.sparepart_id);
|
|
||||||
onSparepartChange(newSelectedIds);
|
|
||||||
|
|
||||||
// Also remove from database if brandId exists
|
|
||||||
if (brandId) {
|
|
||||||
removeSparepartFromBrand(brandId, sparepart.sparepart_id)
|
|
||||||
.then(() => message.success('Sparepart removed successfully'))
|
|
||||||
.catch(error => message.error(error.message || 'Failed to remove sparepart'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, Divider, Typography } from 'antd';
|
|
||||||
import SparepartCardSelect from './SparepartCardSelect';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const SparepartForm = ({
|
|
||||||
sparepartForm,
|
|
||||||
selectedSparepartIds,
|
|
||||||
onSparepartChange,
|
|
||||||
isReadOnly = false
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ minHeight: '400px' }}>
|
|
||||||
<Card title="Spareparts" style={{ minHeight: '100%' }}>
|
|
||||||
<SparepartCardSelect
|
|
||||||
selectedSparepartIds={selectedSparepartIds}
|
|
||||||
onSparepartChange={onSparepartChange}
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SparepartForm;
|
|
||||||
@@ -7,7 +7,7 @@ import CustomSparepartCard from './CustomSparepartCard';
|
|||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const SingleSparepartSelect = ({
|
const SparepartSelect = ({
|
||||||
selectedSparepartIds = [],
|
selectedSparepartIds = [],
|
||||||
onSparepartChange,
|
onSparepartChange,
|
||||||
isReadOnly = false
|
isReadOnly = false
|
||||||
@@ -32,45 +32,25 @@ const SingleSparepartSelect = ({
|
|||||||
}
|
}
|
||||||
}, [selectedSparepartIds, spareparts]);
|
}, [selectedSparepartIds, spareparts]);
|
||||||
|
|
||||||
const fetchSpareparts = async () => {
|
const fetchSpareparts = async (searchQuery = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('limit', '1000');
|
params.set('limit', '1000');
|
||||||
|
|
||||||
|
if (searchQuery && searchQuery.trim() !== '') {
|
||||||
|
params.set('criteria', searchQuery.trim());
|
||||||
|
}
|
||||||
|
|
||||||
const response = await getAllSparepart(params);
|
const response = await getAllSparepart(params);
|
||||||
if (response && (response.statusCode === 200 || response.data)) {
|
if (response && (response.statusCode === 200 || response.data)) {
|
||||||
const sparepartData = response.data?.data || response.data || [];
|
const sparepartData = response.data?.data || response.data || [];
|
||||||
setSpareparts(sparepartData);
|
setSpareparts(sparepartData);
|
||||||
} else {
|
} else {
|
||||||
setSpareparts([
|
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',
|
|
||||||
is_active: true,
|
|
||||||
stock_quantity: 50
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSpareparts([
|
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',
|
|
||||||
is_active: true,
|
|
||||||
stock_quantity: 50
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -93,6 +73,17 @@ const SingleSparepartSelect = ({
|
|||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value) => {
|
||||||
|
fetchSpareparts(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDropdownOpenChange = (open) => {
|
||||||
|
setDropdownOpen(open);
|
||||||
|
if (open) {
|
||||||
|
fetchSpareparts();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemoveSparepart = (sparepartId) => {
|
const handleRemoveSparepart = (sparepartId) => {
|
||||||
const newSelectedSpareparts = selectedSpareparts.filter(sp => sp.sparepart_id !== sparepartId);
|
const newSelectedSpareparts = selectedSpareparts.filter(sp => sp.sparepart_id !== sparepartId);
|
||||||
setSelectedSpareparts(newSelectedSpareparts);
|
setSelectedSpareparts(newSelectedSpareparts);
|
||||||
@@ -111,7 +102,7 @@ const SingleSparepartSelect = ({
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
showPreview={true}
|
showPreview={true}
|
||||||
showDelete={isAlreadySelected && !isReadOnly} // Show delete only for already selected items
|
showDelete={isAlreadySelected && !isReadOnly}
|
||||||
onCardClick={!isAlreadySelected && !isReadOnly ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
|
onCardClick={!isAlreadySelected && !isReadOnly ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
|
||||||
onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)}
|
onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)}
|
||||||
style={{
|
style={{
|
||||||
@@ -124,26 +115,34 @@ const SingleSparepartSelect = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
{/* Fixed Search Section */}
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{
|
||||||
<Select
|
marginBottom: 16,
|
||||||
placeholder="Search and select sparepart..."
|
position: 'sticky',
|
||||||
style={{ width: '100%' }}
|
top: 0,
|
||||||
loading={loading}
|
zIndex: 10,
|
||||||
onSelect={handleSparepartSelect}
|
backgroundColor: 'white',
|
||||||
value={null}
|
padding: '8px 0',
|
||||||
showSearch
|
borderBottom: '1px solid #f0f0f0'
|
||||||
filterOption={(input, option) =>
|
}}>
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
<Select
|
||||||
}
|
placeholder="search and select sparepart"
|
||||||
open={dropdownOpen}
|
style={{ width: '100%' }}
|
||||||
onDropdownVisibleChange={setDropdownOpen}
|
loading={loading}
|
||||||
suffixIcon={<PlusOutlined />}
|
onSelect={handleSparepartSelect}
|
||||||
>
|
value={null}
|
||||||
{spareparts
|
showSearch
|
||||||
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
onSearch={handleSearch}
|
||||||
.map((sparepart) => (
|
filterOption={false}
|
||||||
|
open={dropdownOpen}
|
||||||
|
onOpenChange={onDropdownOpenChange}
|
||||||
|
suffixIcon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
{spareparts
|
||||||
|
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((sparepart) => (
|
||||||
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
||||||
<div>
|
<div>
|
||||||
<Text strong>{sparepart.sparepart_name || sparepart.name || 'Unnamed'}</Text>
|
<Text strong>{sparepart.sparepart_name || sparepart.name || 'Unnamed'}</Text>
|
||||||
@@ -153,31 +152,31 @@ const SingleSparepartSelect = ({
|
|||||||
</div>
|
</div>
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{selectedSpareparts.length > 0 ? (
|
|
||||||
<div>
|
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>
|
|
||||||
Selected Spareparts ({selectedSpareparts.length})
|
|
||||||
</Title>
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Empty
|
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
description="No spareparts selected"
|
|
||||||
style={{ margin: '20px 0' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Scrollable Selected Items Section */}
|
||||||
|
<div>
|
||||||
|
{selectedSpareparts.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<Title level={5} style={{ marginBottom: 16 }}>
|
||||||
|
Selected Spareparts ({selectedSpareparts.length})
|
||||||
|
</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description="No spareparts selected"
|
||||||
|
style={{ margin: '20px 0' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SingleSparepartSelect;
|
export default SparepartSelect;
|
||||||
Reference in New Issue
Block a user