From b05e3fe5d9b512c12d59b735b0d3a2a1e552cb66 Mon Sep 17 00:00:00 2001 From: vinix Date: Mon, 24 Nov 2025 20:40:38 +0700 Subject: [PATCH] feat: implement sparepart management with CRUD operations and UI components --- src/api/sparepart.jsx | 50 +++ src/pages/master/sparepart/IndexSparepart.jsx | 422 +++--------------- .../sparepart/component/DetailSparepart.jsx | 289 ++++++++++++ .../sparepart/component/ListSparepart.jsx | 276 ++++++++++++ 4 files changed, 674 insertions(+), 363 deletions(-) create mode 100644 src/api/sparepart.jsx create mode 100644 src/pages/master/sparepart/component/DetailSparepart.jsx create mode 100644 src/pages/master/sparepart/component/ListSparepart.jsx diff --git a/src/api/sparepart.jsx b/src/api/sparepart.jsx new file mode 100644 index 0000000..0435d13 --- /dev/null +++ b/src/api/sparepart.jsx @@ -0,0 +1,50 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllSparepart = async (queryParams) => { + const response = await SendRequest({ + method: 'get', + prefix: `sparepart?${queryParams.toString()}`, + }); + + return response.data; +}; + +const getSparepartById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `sparepart/${id}`, + }); + + return response.data; +}; + +const createSparepart = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `sparepart`, + params: queryParams, + }); + + return response.data; +}; + +const updateSparepart = async (id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `sparepart/${id}`, + params: queryParams, + }); + + return response.data; +}; + +const deleteSparepart = async (id) => { + const response = await SendRequest({ + method: 'delete', + prefix: `sparepart/${id}`, + }); + + return response.data; +}; + +export { getAllSparepart, getSparepartById, createSparepart, updateSparepart, deleteSparepart }; \ No newline at end of file diff --git a/src/pages/master/sparepart/IndexSparepart.jsx b/src/pages/master/sparepart/IndexSparepart.jsx index c6e55fe..334a661 100644 --- a/src/pages/master/sparepart/IndexSparepart.jsx +++ b/src/pages/master/sparepart/IndexSparepart.jsx @@ -1,379 +1,75 @@ -import React, { memo, useEffect, useState } from 'react'; +import React, { memo, useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag, Spin, Table, Modal } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined } from '@ant-design/icons'; -import { NotifAlert, NotifConfirmDialog, NotifOk } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { Typography } from 'antd'; +import ListSparepart from './component/ListSparepart'; +import DetailSparepart from './component/DetailSparepart'; -const { Text, Title } = Typography; - -// Mock data untuk sparepart -const mockSpareparts = [ - { - sparepart_id: 1, - sparepart_name: 'Compressor Oil', - sparepart_code: 'SP-COMP-001', - sparepart_description: 'Oil for industrial compressor maintenance', - brand_name: 'Sullair', - model: '750H', - type: 'consumable', - is_active: true, - created_date: '2024-01-15', - updated_date: '2024-11-20' - }, - { - sparepart_id: 2, - sparepart_name: 'Air Filter Element', - sparepart_code: 'SP-AF-002', - sparepart_description: 'High efficiency air filter for compressor', - brand_name: 'Ingersoll Rand', - model: 'X7', - type: 'filter', - is_active: true, - created_date: '2024-02-20', - updated_date: '2024-10-15' - }, - { - sparepart_id: 3, - sparepart_name: 'Oil Filter', - sparepart_code: 'SP-OF-003', - sparepart_description: 'Oil filtration system component', - brand_name: 'Atlas Copco', - model: 'GA 55', - type: 'filter', - is_active: false, - created_date: '2024-03-10', - updated_date: '2024-09-05' - }, - { - sparepart_id: 4, - sparepart_name: 'Cooling Fan Motor', - sparepart_code: 'SP-CFM-004', - sparepart_description: 'Motor for cooling system in compressor', - brand_name: 'Kobelco', - model: 'SRE-110', - type: 'electrical', - is_active: true, - created_date: '2024-04-05', - updated_date: '2024-11-10' - }, - { - sparepart_id: 5, - sparepart_name: 'Coupling Elastomer', - sparepart_code: 'SP-CE-005', - sparepart_description: 'Elastomer coupling for drive system', - brand_name: 'Kaesar', - model: 'Comprex', - type: 'mechanical', - is_active: true, - created_date: '2024-05-12', - updated_date: '2024-11-18' - } -]; +const { Text } = Typography; const IndexSparepart = memo(function IndexSparepart() { - const navigate = useNavigate(); - const { setBreadcrumbItems } = useBreadcrumb(); - const [spareparts, setSpareparts] = useState([]); - const [filteredSpareparts, setFilteredSpareparts] = useState([]); - const [loading, setLoading] = useState(true); - const [searchValue, setSearchValue] = useState(''); + const navigate = useNavigate(); + const { setBreadcrumbItems } = useBreadcrumb(); - useEffect(() => { - const token = localStorage.getItem('token'); - if (token) { - // Simulasi loading data - setTimeout(() => { - setSpareparts(mockSpareparts); - setFilteredSpareparts(mockSpareparts); - setLoading(false); - }, 1000); + const [actionMode, setActionMode] = useState('list'); + const [selectedData, setSelectedData] = useState(null); + const [readOnly, setReadOnly] = useState(false); + const [showModal, setShowmodal] = useState(false); - setBreadcrumbItems([ - { title: • Master }, - { title: Sparepart } - ]); - } else { - navigate('/signin'); - } - }, [navigate, setBreadcrumbItems]); + const setMode = (param) => { + setShowmodal(true); + switch (param) { + case 'add': + setReadOnly(false); + break; - // Fungsi untuk pencarian - const handleSearch = (value) => { - if (!value) { - setFilteredSpareparts(spareparts); - } else { - const filtered = spareparts.filter(item => - item.sparepart_name.toLowerCase().includes(value.toLowerCase()) || - item.sparepart_code.toLowerCase().includes(value.toLowerCase()) || - item.brand_name.toLowerCase().includes(value.toLowerCase()) || - item.model.toLowerCase().includes(value.toLowerCase()) - ); - setFilteredSpareparts(filtered); - } - }; + case 'edit': + setReadOnly(false); + break; - const handleSearchChange = (e) => { - const value = e.target.value; - setSearchValue(value); - handleSearch(value); - }; + case 'preview': + setReadOnly(true); + break; - const handleSearchClear = () => { - setSearchValue(''); - setFilteredSpareparts(spareparts); - }; + default: + setShowmodal(false); + break; + } + setActionMode(param); + }; - const handleAddSparepart = () => { - // Navigasi ke halaman tambah sparepart (akan dibuat nanti) - NotifAlert({ - icon: 'info', - title: 'Info', - message: 'Fitur tambah sparepart akan segera dibuat', - }); - }; + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setBreadcrumbItems([ + { title: • Master }, + { title: Sparepart } + ]); + } else { + navigate('/signin'); + } + }, []); - const handleEditSparepart = (record) => { - // Navigasi ke halaman edit sparepart (akan dibuat nanti) - NotifAlert({ - icon: 'info', - title: 'Info', - message: `Edit sparepart: ${record.sparepart_name}`, - }); - }; - - const handleViewSparepart = (record) => { - // Tampilkan detail sparepart (akan dibuat nanti) - NotifOk({ - icon: 'success', - title: 'Info', - message: `View sparepart: ${record.sparepart_name}`, - }); - }; - - const handleDeleteSparepart = (record) => { - NotifConfirmDialog({ - icon: 'question', - title: 'Konfirmasi', - message: 'Apakah anda yakin hapus sparepart "' + record.sparepart_name + '" ?', - onConfirm: () => { - const updatedSpareparts = spareparts.filter(item => item.sparepart_id !== record.sparepart_id); - setSpareparts(updatedSpareparts); - setFilteredSpareparts(updatedSpareparts); - NotifOk({ - icon: 'success', - title: 'Berhasil', - message: `Sparepart ${record.sparepart_name} deleted successfully.`, - }); - }, - onCancel: () => {}, - }); - }; - - // Kolom-kolom tabel - const columns = [ - { - title: 'No', - key: 'no', - width: '5%', - align: 'center', - render: (_, __, index) => index + 1, - }, - { - title: 'Sparepart Code', - dataIndex: 'sparepart_code', - key: 'sparepart_code', - width: '15%', - }, - { - title: 'Sparepart Name', - dataIndex: 'sparepart_name', - key: 'sparepart_name', - width: '20%', - }, - { - title: 'Description', - dataIndex: 'sparepart_description', - key: 'sparepart_description', - width: '20%', - render: (text) => text || '-', - }, - { - title: 'Brand', - dataIndex: 'brand_name', - key: 'brand_name', - width: '12%', - }, - { - title: 'Model', - dataIndex: 'model', - key: 'model', - width: '10%', - }, - { - title: 'Type', - dataIndex: 'type', - key: 'type', - width: '10%', - render: (text) => ( - - {text?.toUpperCase()} - - ), - }, - { - title: 'Status', - dataIndex: 'is_active', - key: 'is_active', - width: '8%', - align: 'center', - render: (_, { is_active }) => ( - <> - {is_active === true ? ( - - Active - - ) : ( - - Inactive - - )} - - ), - }, - { - title: 'Action', - key: 'action', - align: 'center', - width: '15%', - render: (_, record) => ( - - - } - size="large" - /> - - - - - - - - - - - - {loading ? ( -
- -
- ) : ( - `${range[0]}-${range[1]} of ${total} items`, - }} - scroll={{ x: 1000 }} - size="middle" - /> - )} - - - - - ); + return ( + + + + + ); }); export default IndexSparepart; \ No newline at end of file diff --git a/src/pages/master/sparepart/component/DetailSparepart.jsx b/src/pages/master/sparepart/component/DetailSparepart.jsx new file mode 100644 index 0000000..7dd4e38 --- /dev/null +++ b/src/pages/master/sparepart/component/DetailSparepart.jsx @@ -0,0 +1,289 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, message } from 'antd'; +import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; +import { createSparepart, updateSparepart } from '../../../../api/sparepart'; +import { validateRun } from '../../../../Utils/validate'; + +const { Text } = Typography; +const { TextArea } = Input; + +const DetailSparepart = (props) => { + const [confirmLoading, setConfirmLoading] = useState(false); + + const defaultData = { + sparepart_id: '', + sparepart_name: '', + sparepart_description: '', + sparepart_model: '', + sparepart_item_type: '', + sparepart_unit: '', + sparepart_merk: '', + sparepart_stok: '', + }; + + const [formData, setFormData] = useState(defaultData); + + const handleCancel = () => { + props.setSelectedData(null); + props.setActionMode('list'); + }; + + const handleSave = async () => { + setConfirmLoading(true); + + // Daftar aturan validasi + const validationRules = [ + { field: 'sparepart_name', label: 'Sparepart Name', required: true }, + { field: 'sparepart_model', label: 'Sparepart Model', required: true }, + { field: 'sparepart_unit', label: 'Sparepart Unit', required: true }, + { field: 'sparepart_merk', label: 'Sparepart Merk', required: true }, + { field: 'sparepart_stok', label: 'Sparepart Stok', required: true }, + ]; + + if ( + validateRun(formData, validationRules, (errorMessages) => { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: errorMessages, + }); + setConfirmLoading(false); + }) + ) + return; + + try { + const payload = { + sparepart_name: formData.sparepart_name, + sparepart_description: formData.sparepart_description, + sparepart_model: formData.sparepart_model, + sparepart_item_type: formData.sparepart_item_type, + sparepart_unit: formData.sparepart_unit, + sparepart_merk: formData.sparepart_merk, + sparepart_stok: formData.sparepart_stok, + }; + + const response = formData.sparepart_id + ? await updateSparepart(formData.sparepart_id, payload) + : await createSparepart(payload); + + // Check if response is successful + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + const sparepartName = response.data?.sparepart_name || formData.sparepart_name; + + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Data Sparepart "${sparepartName}" berhasil ${ + formData.sparepart_id ? 'diubah' : 'ditambahkan' + }.`, + }); + + props.setActionMode('list'); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Terjadi kesalahan saat menyimpan data.', + }); + } + } catch (error) { + console.error('Save Sparepart Error:', error); + NotifAlert({ + icon: 'error', + title: 'Error', + message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', + }); + } + + setConfirmLoading(false); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleFieldChange = (name, value) => { + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleStatusToggle = (event) => { + const isChecked = event; + setFormData({ + ...formData, + is_active: isChecked ? true : false, + }); + }; + + useEffect(() => { + if (props.selectedData) { + setFormData(props.selectedData); + } else { + setFormData(defaultData); + } + }, [props.showModal, props.selectedData, props.actionMode]); + + return ( + + + + + + {!props.readOnly && ( + + )} + + , + ]} + > + {formData && ( +
+ + +
+ Sparepart Name + * + +
+ +
+ Sparepart Description +