Merge pull request 'lavoce' (#23) from lavoce into main
Reviewed-on: #23
This commit is contained in:
BIN
public/assets/defaultSparepartImg.jpg
Normal file
BIN
public/assets/defaultSparepartImg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,9 +1,21 @@
|
|||||||
import { SendRequest } from '../components/Global/ApiRequest';
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
export const getAllNotification = async () => {
|
const getAllNotification = async (queryParams) => {
|
||||||
const response = await SendRequest({
|
const response = await SendRequest({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
prefix: 'notification',
|
prefix: `notification?${queryParams.toString()}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNotificationById = async (id) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `notification/${id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getAllNotification, getNotificationById };
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const TableList = memo(function TableList({
|
|||||||
fieldColor,
|
fieldColor,
|
||||||
firstLoad = true,
|
firstLoad = true,
|
||||||
columnDynamic = false,
|
columnDynamic = false,
|
||||||
|
cardComponent, // New prop for custom 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);
|
||||||
|
|
||||||
@@ -103,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;
|
||||||
|
|
||||||
@@ -142,6 +152,9 @@ const TableList = memo(function TableList({
|
|||||||
|
|
||||||
const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile
|
const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile
|
||||||
|
|
||||||
|
// Use the custom card component if provided, otherwise default to CardList
|
||||||
|
const CardViewComponent = cardComponent || CardList;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Segmented
|
<Segmented
|
||||||
@@ -153,7 +166,7 @@ const TableList = memo(function TableList({
|
|||||||
onChange={setViewMode}
|
onChange={setViewMode}
|
||||||
/>
|
/>
|
||||||
{(isMobile && mobile) || viewMode === 'card' ? (
|
{(isMobile && mobile) || viewMode === 'card' ? (
|
||||||
<CardList
|
<CardViewComponent
|
||||||
cardColor={cardColor}
|
cardColor={cardColor}
|
||||||
fieldColor={fieldColor}
|
fieldColor={fieldColor}
|
||||||
data={data}
|
data={data}
|
||||||
@@ -162,6 +175,7 @@ const TableList = memo(function TableList({
|
|||||||
showPreviewModal={showPreviewModal}
|
showPreviewModal={showPreviewModal}
|
||||||
showEditModal={showEditModal}
|
showEditModal={showEditModal}
|
||||||
showDeleteDialog={showDeleteDialog}
|
showDeleteDialog={showDeleteDialog}
|
||||||
|
onStockUpdate={onStockUpdate}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Row gutter={24} style={{ marginTop: '16px' }}>
|
<Row gutter={24} style={{ marginTop: '16px' }}>
|
||||||
@@ -200,3 +214,4 @@ const TableList = memo(function TableList({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default TableList;
|
export default TableList;
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const EditBrandDevice = () => {
|
|||||||
sparepart: ec.sparepart || [],
|
sparepart: ec.sparepart || [],
|
||||||
errorCodeIcon: ec.path_icon
|
errorCodeIcon: ec.path_icon
|
||||||
? {
|
? {
|
||||||
name: 'icon',
|
name: ec.path_icon.split('/').pop(), // Ambil nama file dari path
|
||||||
uploadPath: ec.path_icon,
|
uploadPath: ec.path_icon,
|
||||||
url: (() => {
|
url: (() => {
|
||||||
const pathParts = ec.path_icon.split('/');
|
const pathParts = ec.path_icon.split('/');
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ const ErrorCodeSimpleForm = ({
|
|||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<img
|
<img
|
||||||
src={errorCodeIcon.uploadPath}
|
src={errorCodeIcon.url || errorCodeIcon.uploadPath}
|
||||||
alt="Error Code Icon"
|
alt="Error Code Icon"
|
||||||
style={{
|
style={{
|
||||||
width: 50,
|
width: 50,
|
||||||
|
|||||||
@@ -1,14 +1,40 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, message } from 'antd';
|
import {
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
ConfigProvider,
|
||||||
|
Upload,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Image,
|
||||||
|
} from 'antd';
|
||||||
|
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';
|
||||||
|
import { uploadFile } from '../../../../api/file-uploads';
|
||||||
import { validateRun } from '../../../../Utils/validate';
|
import { validateRun } from '../../../../Utils/validate';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const getBase64 = (file) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
const DetailSparepart = (props) => {
|
const DetailSparepart = (props) => {
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const [fileList, setFileList] = useState([]);
|
||||||
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState('');
|
||||||
|
const [previewTitle, setPreviewTitle] = useState('');
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
sparepart_id: '',
|
sparepart_id: '',
|
||||||
@@ -18,7 +44,8 @@ const DetailSparepart = (props) => {
|
|||||||
sparepart_item_type: '',
|
sparepart_item_type: '',
|
||||||
sparepart_unit: '',
|
sparepart_unit: '',
|
||||||
sparepart_merk: '',
|
sparepart_merk: '',
|
||||||
sparepart_stok: '',
|
sparepart_stok: '0',
|
||||||
|
sparepart_foto: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
@@ -26,60 +53,200 @@ const DetailSparepart = (props) => {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
props.setSelectedData(null);
|
props.setSelectedData(null);
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
|
setFileList([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePreviewCancel = () => setPreviewOpen(false);
|
||||||
|
|
||||||
|
const handlePreview = async (file) => {
|
||||||
|
if (!file.url && !file.preview) {
|
||||||
|
file.preview = await getBase64(file.originFileObj);
|
||||||
|
}
|
||||||
|
setPreviewImage(file.url || file.preview);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = ({ fileList: newFileList }) => setFileList(newFileList);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
// Daftar aturan validasi
|
|
||||||
const validationRules = [
|
const validationRules = [
|
||||||
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
|
{ 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 (
|
if (
|
||||||
validateRun(formData, validationRules, (errorMessages) => {
|
validateRun(formData, validationRules, (errorMessages) => {
|
||||||
NotifOk({
|
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
|
||||||
icon: 'warning',
|
|
||||||
title: 'Peringatan',
|
|
||||||
message: errorMessages,
|
|
||||||
});
|
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let imageUrl = formData.sparepart_foto;
|
||||||
|
const newFile = fileList.length > 0 ? fileList[0] : null;
|
||||||
|
|
||||||
|
if (newFile && newFile.originFileObj) {
|
||||||
|
console.log('Uploading file:', newFile.originFileObj);
|
||||||
|
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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) {
|
||||||
|
// Jika tidak ada file di fileList (termasuk saat user menghapus file), gunakan gambar default
|
||||||
|
imageUrl = '/assets/defaultSparepartImg.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload hanya berisi field yang tidak kosong untuk menghindari error validasi
|
||||||
const payload = {
|
const payload = {
|
||||||
sparepart_name: formData.sparepart_name,
|
sparepart_name: formData.sparepart_name, // Wajib
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tambahkan field-field secara kondisional hanya jika nilainya tidak kosong
|
||||||
|
if (formData.sparepart_description && formData.sparepart_description.trim() !== '') {
|
||||||
|
payload.sparepart_description = formData.sparepart_description;
|
||||||
|
}
|
||||||
|
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
|
||||||
|
payload.sparepart_model = formData.sparepart_model;
|
||||||
|
}
|
||||||
|
if (formData.sparepart_item_type && formData.sparepart_item_type.trim() !== '') {
|
||||||
|
payload.sparepart_item_type = formData.sparepart_item_type;
|
||||||
|
}
|
||||||
|
if (formData.sparepart_unit && formData.sparepart_unit.trim() !== '') {
|
||||||
|
payload.sparepart_unit = formData.sparepart_unit;
|
||||||
|
}
|
||||||
|
if (formData.sparepart_merk && formData.sparepart_merk.trim() !== '') {
|
||||||
|
payload.sparepart_merk = formData.sparepart_merk;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Sertakan sparepart_foto hanya jika nilainya tidak kosong, agar tidak memicu validasi
|
||||||
|
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);
|
||||||
|
|
||||||
// Check if response is successful
|
console.log('API response:', response);
|
||||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
|
||||||
const sparepartName = response.data?.sparepart_name || formData.sparepart_name;
|
|
||||||
|
|
||||||
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `Data Sparepart "${sparepartName}" berhasil ${
|
message: `Data Sparepart berhasil ${
|
||||||
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
||||||
}.`,
|
}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
|
setFileList([]);
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -101,35 +268,47 @@ const DetailSparepart = (props) => {
|
|||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData({
|
setFormData({ ...formData, [name]: value });
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFieldChange = (name, value) => {
|
const handleSelectChange = (name, value) => {
|
||||||
setFormData({
|
setFormData({ ...formData, [name]: value });
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusToggle = (event) => {
|
|
||||||
const isChecked = event;
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
is_active: isChecked ? true : false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
|
if (props.selectedData.sparepart_foto) {
|
||||||
|
// Buat URL lengkap dengan token untuk file yang sudah ada
|
||||||
|
const fileName = props.selectedData.sparepart_foto.split('/').pop();
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const baseURL = import.meta.env.VITE_API_SERVER || '';
|
||||||
|
const fullUrl = `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||||
|
|
||||||
|
setFileList([
|
||||||
|
{
|
||||||
|
uid: '-1',
|
||||||
|
name: fileName,
|
||||||
|
status: 'done',
|
||||||
|
url: fullUrl,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setFileList([]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
|
setFileList([]);
|
||||||
}
|
}
|
||||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|
||||||
|
const uploadButton = (
|
||||||
|
<div>
|
||||||
|
<PlusOutlined />
|
||||||
|
<div style={{ marginTop: 8 }}>Upload</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={`${
|
title={`${
|
||||||
@@ -153,7 +332,6 @@ const DetailSparepart = (props) => {
|
|||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
defaultHoverColor: '#23A55A',
|
||||||
defaultHoverBorderColor: '#23A55A',
|
defaultHoverBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -162,9 +340,7 @@ const DetailSparepart = (props) => {
|
|||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
token: {
|
token: { colorBgContainer: '#209652' },
|
||||||
colorBgContainer: '#209652',
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Button: {
|
Button: {
|
||||||
defaultBg: '#23a55a',
|
defaultBg: '#23a55a',
|
||||||
@@ -187,99 +363,121 @@ const DetailSparepart = (props) => {
|
|||||||
>
|
>
|
||||||
{formData && (
|
{formData && (
|
||||||
<div>
|
<div>
|
||||||
<div hidden>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart ID</Text>
|
<Col span={12}>
|
||||||
<Input
|
<Text strong>Sparepart Name</Text>
|
||||||
name="sparepart_id"
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
value={formData.sparepart_id}
|
<Input
|
||||||
onChange={handleInputChange}
|
name="sparepart_name"
|
||||||
disabled
|
value={formData.sparepart_name}
|
||||||
/>
|
onChange={handleInputChange}
|
||||||
</div>
|
placeholder="Enter Sparepart Name"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Item Type</Text>
|
||||||
|
<Select
|
||||||
|
name="sparepart_item_type"
|
||||||
|
value={formData.sparepart_item_type}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleSelectChange('sparepart_item_type', value)
|
||||||
|
}
|
||||||
|
placeholder="Select Item Type"
|
||||||
|
disabled={props.readOnly}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Select.Option value="Air Dryer">Air Dryer</Select.Option>
|
||||||
|
<Select.Option value="Compressor">Compressor</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Name</Text>
|
<Col span={12}>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
<Text strong>Stock</Text>
|
||||||
<Input
|
<Input
|
||||||
name="sparepart_name"
|
name="sparepart_stok"
|
||||||
value={formData.sparepart_name}
|
value={formData.sparepart_stok}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder="Enter Sparepart Name"
|
placeholder="Initial stock quantity"
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
type="number"
|
||||||
</div>
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Unit</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_unit"
|
||||||
|
value={formData.sparepart_unit}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., pcs, box, roll"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Description</Text>
|
<Col span={24}>
|
||||||
<TextArea
|
<Text strong>Foto</Text>
|
||||||
name="sparepart_description"
|
<Upload
|
||||||
value={formData.sparepart_description}
|
listType="picture-card"
|
||||||
onChange={handleInputChange}
|
fileList={fileList}
|
||||||
placeholder="Enter Sparepart Description (Optional)"
|
onPreview={handlePreview}
|
||||||
readOnly={props.readOnly}
|
onChange={handleChange}
|
||||||
rows={4}
|
beforeUpload={() => false}
|
||||||
/>
|
maxCount={1}
|
||||||
</div>
|
disabled={props.readOnly}
|
||||||
|
>
|
||||||
|
{fileList.length >= 1 ? null : uploadButton}
|
||||||
|
</Upload>
|
||||||
|
<Modal
|
||||||
|
open={previewOpen}
|
||||||
|
title={previewTitle}
|
||||||
|
footer={null}
|
||||||
|
onCancel={handlePreviewCancel}
|
||||||
|
>
|
||||||
|
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Model</Text>
|
<Col span={12}>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
<Text strong>Brand</Text>
|
||||||
<Input
|
<Input
|
||||||
name="sparepart_model"
|
name="sparepart_merk"
|
||||||
value={formData.sparepart_model}
|
value={formData.sparepart_merk}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder="Enter Sparepart Model"
|
placeholder="Enter Brand (Optional)"
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Model</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_model"
|
||||||
|
value={formData.sparepart_model}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Model (Optional)"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Item Type</Text>
|
<Col span={24}>
|
||||||
<Input
|
<Text strong>Description</Text>
|
||||||
name="sparepart_item_type"
|
<TextArea
|
||||||
value={formData.sparepart_item_type}
|
name="sparepart_description"
|
||||||
onChange={handleInputChange}
|
value={formData.sparepart_description}
|
||||||
placeholder="Enter Sparepart Item Type"
|
onChange={handleInputChange}
|
||||||
readOnly={props.readOnly}
|
placeholder="Enter Description (Optional)"
|
||||||
/>
|
readOnly={props.readOnly}
|
||||||
</div>
|
rows={3}
|
||||||
|
/>
|
||||||
<div style={{ marginBottom: 12 }}>
|
</Col>
|
||||||
<Text strong>Sparepart Unit</Text>
|
</Row>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_unit"
|
|
||||||
value={formData.sparepart_unit}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Unit"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Text strong>Sparepart Merk</Text>
|
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_merk"
|
|
||||||
value={formData.sparepart_merk}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Merk"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Text strong>Sparepart Stok</Text>
|
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_stok"
|
|
||||||
value={formData.sparepart_stok}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Stok"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { deleteSparepart, getAllSparepart } from '../../../../api/sparepart';
|
import { deleteSparepart, getAllSparepart } from '../../../../api/sparepart';
|
||||||
import TableList from '../../../../components/Global/TableList';
|
import TableList from '../../../../components/Global/TableList';
|
||||||
|
import SparepartCardList from './SparepartCardList'; // Import the new custom card component
|
||||||
|
|
||||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||||
{
|
{
|
||||||
@@ -265,6 +266,14 @@ const ListSparepart = memo(function ListSparepart(props) {
|
|||||||
queryParams={formDataFilter}
|
queryParams={formDataFilter}
|
||||||
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
||||||
triger={trigerFilter}
|
triger={trigerFilter}
|
||||||
|
cardComponent={SparepartCardList} // Pass the custom component here
|
||||||
|
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>
|
||||||
|
|||||||
335
src/pages/master/sparepart/component/SparepartCardList.jsx
Normal file
335
src/pages/master/sparepart/component/SparepartCardList.jsx
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Card, Button, Row, Col, Typography, Divider, Tag, Space, InputNumber, Input } from 'antd';
|
||||||
|
import { EditOutlined, DeleteOutlined, PlusOutlined, MinusOutlined } from '@ant-design/icons';
|
||||||
|
import { updateSparepart } from '../../../../api/sparepart';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
const SparepartCardList = ({
|
||||||
|
data,
|
||||||
|
header,
|
||||||
|
showPreviewModal,
|
||||||
|
showEditModal,
|
||||||
|
showDeleteDialog,
|
||||||
|
fieldColor,
|
||||||
|
cardColor,
|
||||||
|
onStockUpdate, // Prop to refresh the list
|
||||||
|
}) => {
|
||||||
|
const [updateQuantities, setUpdateQuantities] = useState({});
|
||||||
|
const [loadingQuantities, setLoadingQuantities] = useState({});
|
||||||
|
|
||||||
|
const handleQuantityChange = (id, value) => {
|
||||||
|
const newQuantities = { ...updateQuantities };
|
||||||
|
newQuantities[id] = value;
|
||||||
|
setUpdateQuantities(newQuantities);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateStock = async (item) => {
|
||||||
|
const quantityToAdd = updateQuantities[item.sparepart_id] || 0;
|
||||||
|
if (quantityToAdd === 0) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Info',
|
||||||
|
message: 'Please change the quantity first.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStock = Number(item.sparepart_stok) + quantityToAdd;
|
||||||
|
if (newStock < 0) {
|
||||||
|
NotifAlert({ icon: 'error', title: 'Error', message: 'Stock cannot be negative.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: true }));
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
sparepart_stok: newStock.toString(), // Convert number to string as required by API
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
const response = await updateSparepart(item.sparepart_id, payload);
|
||||||
|
|
||||||
|
// Periksa apakah response valid sebelum mengakses propertinya
|
||||||
|
if (response && response.statusCode === 200) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Stock updated successfully.',
|
||||||
|
});
|
||||||
|
if (onStockUpdate) {
|
||||||
|
onStockUpdate();
|
||||||
|
}
|
||||||
|
handleQuantityChange(item.sparepart_id, 0); // Reset quantity
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Failed',
|
||||||
|
message: response?.message || 'Failed to update stock.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message || 'An error occurred.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
|
||||||
|
{data.map((item) => {
|
||||||
|
const quantity = updateQuantities[item.sparepart_id] || 0;
|
||||||
|
const isLoading = loadingQuantities[item.sparepart_id] || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6} key={item.sparepart_id || item.key}>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: `1px solid ${
|
||||||
|
fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<Col span={8}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
padding: '16px 8px',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.sparepart_item_type && (
|
||||||
|
<Tag
|
||||||
|
color="blue"
|
||||||
|
style={{
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.sparepart_item_type}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: '100%', /* Ini membuat tinggi sama dengan lebar (aspect ratio 1:1) */
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
// Debug: log the image path construction
|
||||||
|
let imgSrc;
|
||||||
|
if (item.sparepart_foto) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Jika filename adalah default file, gunakan dari public assets
|
||||||
|
if (fileName === 'defaultSparepartImg.jpg') {
|
||||||
|
imgSrc = `/assets/defaultSparepartImg.jpg`;
|
||||||
|
} else {
|
||||||
|
// Gunakan API getFileUrl untuk mendapatkan URL yang benar untuk file upload
|
||||||
|
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 (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}>
|
||||||
|
<img
|
||||||
|
src={imgSrc}
|
||||||
|
alt={item[header]}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover', // Mengisi container dan crop sisi berlebih
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '16px',
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showEditModal && (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
color: '#faad14',
|
||||||
|
borderColor: '#faad14',
|
||||||
|
}}
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
key="edit"
|
||||||
|
onClick={() => showEditModal(item)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showDeleteDialog && (
|
||||||
|
<Button
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
key="delete"
|
||||||
|
onClick={() => showDeleteDialog(item)}
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Title
|
||||||
|
level={5}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
marginBottom: '8px',
|
||||||
|
paddingRight: '60px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item[header]}
|
||||||
|
</Title>
|
||||||
|
<Text type="secondary">
|
||||||
|
Available Stock: {item.sparepart_stok || '0'}
|
||||||
|
</Text>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
align="center"
|
||||||
|
style={{
|
||||||
|
marginBottom: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<MinusOutlined />}
|
||||||
|
onClick={() =>
|
||||||
|
handleQuantityChange(
|
||||||
|
item.sparepart_id,
|
||||||
|
quantity - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={isLoading}
|
||||||
|
style={{ width: 28, height: 28 }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ padding: '0 8px', fontSize: '16px' }}
|
||||||
|
>
|
||||||
|
{quantity}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() =>
|
||||||
|
handleQuantityChange(
|
||||||
|
item.sparepart_id,
|
||||||
|
quantity + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={isLoading}
|
||||||
|
style={{ width: 28, height: 28 }}
|
||||||
|
/>
|
||||||
|
<Text type="secondary">
|
||||||
|
{item.sparepart_unit
|
||||||
|
? ` / ${item.sparepart_unit}`
|
||||||
|
: ' / pcs'}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type={quantity === 0 ? 'default' : 'primary'}
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={() => handleUpdateStock(item)}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
Update Stock
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<Text
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SparepartCardList;
|
||||||
@@ -146,21 +146,43 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Fetch notifications from API
|
// Fetch notifications from API
|
||||||
const fetchNotifications = async () => {
|
const fetchNotifications = async (page = 1, limit = 10, isRead = null) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getAllNotification();
|
const queryParams = new URLSearchParams({
|
||||||
|
page: page.toString(),
|
||||||
|
limit: limit.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isRead !== null) {
|
||||||
|
queryParams.append('is_read', isRead.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getAllNotification(queryParams);
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const transformedData = transformNotificationData(response.data);
|
const transformedData = transformNotificationData(response.data);
|
||||||
setNotifications(transformedData);
|
setNotifications(transformedData);
|
||||||
|
|
||||||
// Update pagination with mock data (since API doesn't provide pagination info)
|
// Update pagination with API response or calculate from data
|
||||||
const totalItems = transformedData.length;
|
if (response.paging) {
|
||||||
setPagination((prev) => ({
|
setPagination({
|
||||||
...prev,
|
current_page: response.paging.current_page || page,
|
||||||
total_limit: totalItems,
|
current_limit: response.paging.current_limit || limit,
|
||||||
total_page: Math.ceil(totalItems / prev.current_limit),
|
total_limit: response.paging.total_limit || transformedData.length,
|
||||||
}));
|
total_page:
|
||||||
|
response.paging.total_page || Math.ceil(transformedData.length / limit),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: calculate pagination from data
|
||||||
|
const totalItems = transformedData.length;
|
||||||
|
setPagination((prev) => ({
|
||||||
|
...prev,
|
||||||
|
current_page: page,
|
||||||
|
current_limit: limit,
|
||||||
|
total_limit: totalItems,
|
||||||
|
total_page: Math.ceil(totalItems / limit),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching notifications:', error);
|
console.error('Error fetching notifications:', error);
|
||||||
@@ -178,13 +200,10 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
current_page: page,
|
current_page: page,
|
||||||
current_limit: pageSize,
|
current_limit: pageSize,
|
||||||
}));
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
// Get paginated notifications
|
// Fetch notifications with new pagination
|
||||||
const getPaginatedNotifications = () => {
|
const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
|
||||||
const startIndex = (pagination.current_page - 1) * pagination.current_limit;
|
fetchNotifications(page, pageSize, isReadFilter);
|
||||||
const endIndex = startIndex + pagination.current_limit;
|
|
||||||
return filteredNotifications.slice(startIndex, endIndex);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -194,9 +213,10 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch notifications on component mount
|
// Fetch notifications on component mount and when tab changes
|
||||||
fetchNotifications();
|
const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
|
||||||
}, []);
|
fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
const getIconAndColor = (type) => {
|
const getIconAndColor = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -248,22 +268,17 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredNotifications = notifications
|
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
||||||
.filter((n) => {
|
|
||||||
const matchesTab =
|
// Filter notifications based on search term
|
||||||
activeTab === 'all' ||
|
const getFilteredNotifications = () => {
|
||||||
(activeTab === 'unread' && !n.isRead) ||
|
if (!searchTerm) return notifications;
|
||||||
(activeTab === 'read' && n.isRead);
|
// Search by title and error code name
|
||||||
return matchesTab;
|
return notifications.filter((n) => {
|
||||||
})
|
|
||||||
.filter((n) => {
|
|
||||||
if (!searchTerm) return true;
|
|
||||||
// Search by title and error code name
|
|
||||||
const searchableText = `${n.title} ${n.issue}`.toLowerCase();
|
const searchableText = `${n.title} ${n.issue}`.toLowerCase();
|
||||||
return searchableText.includes(searchTerm.toLowerCase());
|
return searchableText.includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
|
};
|
||||||
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
|
||||||
|
|
||||||
const tabButtonStyle = (isActive) => ({
|
const tabButtonStyle = (isActive) => ({
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
@@ -279,8 +294,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const renderDeviceNotifications = () => {
|
const renderDeviceNotifications = () => {
|
||||||
const paginatedNotifications = getPaginatedNotifications();
|
const filteredNotifications = getFilteredNotifications();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
{filteredNotifications.length === 0 ? (
|
{filteredNotifications.length === 0 ? (
|
||||||
@@ -288,200 +302,215 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
Tidak ada notifikasi
|
Tidak ada notifikasi
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
paginatedNotifications.map((notification) => {
|
filteredNotifications.map((notification) => {
|
||||||
const { IconComponent, color, bgColor } = getIconAndColor(notification.type);
|
const { IconComponent, color, bgColor } = getIconAndColor(
|
||||||
return (
|
notification.type
|
||||||
<Card
|
);
|
||||||
key={notification.id}
|
return (
|
||||||
style={{
|
<Card
|
||||||
backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff',
|
key={notification.id}
|
||||||
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
style={{
|
||||||
cursor: 'pointer',
|
backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff',
|
||||||
}}
|
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
cursor: 'pointer',
|
||||||
>
|
}}
|
||||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
|
onClick={() => handleMarkAsRead(notification.id)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '40px',
|
|
||||||
height: '40px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
color: color,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
gap: '16px',
|
||||||
justifyContent: 'center',
|
alignItems: 'flex-start',
|
||||||
fontSize: '22px',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent style={{ fontSize: '22px' }} />
|
<div
|
||||||
</div>
|
style={{
|
||||||
<div style={{ flex: 1 }}>
|
width: '40px',
|
||||||
<Row align="top">
|
height: '40px',
|
||||||
<Col flex="220px">
|
borderRadius: '50%',
|
||||||
<div
|
backgroundColor: bgColor,
|
||||||
style={{
|
color: color,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
alignItems: 'flex-start',
|
justifyContent: 'center',
|
||||||
}}
|
fontSize: '22px',
|
||||||
>
|
flexShrink: 0,
|
||||||
<div>
|
}}
|
||||||
<Text strong>{notification.title}</Text>
|
>
|
||||||
<div style={{ marginTop: '4px' }}>
|
<IconComponent style={{ fontSize: '22px' }} />
|
||||||
<Text style={{ color }}>
|
</div>
|
||||||
{notification.issue}
|
<div style={{ flex: 1 }}>
|
||||||
</Text>
|
<Row align="top">
|
||||||
|
<Col flex="220px">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text strong>{notification.title}</Text>
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
<Text style={{ color }}>
|
||||||
|
{notification.issue}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!notification.isRead && (
|
||||||
|
<Badge
|
||||||
|
color="red"
|
||||||
|
status="processing"
|
||||||
|
style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
marginTop: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!notification.isRead && (
|
</Col>
|
||||||
<Badge
|
<Col flex="auto">
|
||||||
color="red"
|
<div
|
||||||
status="processing"
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MailOutlined
|
||||||
style={{
|
style={{
|
||||||
marginLeft: '8px',
|
|
||||||
marginTop: '4px',
|
marginTop: '4px',
|
||||||
|
color: '#1890ff',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
<Paragraph
|
||||||
</div>
|
style={{
|
||||||
</Col>
|
color: '#595959',
|
||||||
<Col flex="auto">
|
margin: 0,
|
||||||
<div
|
flex: 1,
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: '8px',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: '12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MailOutlined
|
|
||||||
style={{ marginTop: '4px', color: '#1890ff' }}
|
|
||||||
/>
|
|
||||||
<Paragraph
|
|
||||||
style={{ color: '#595959', margin: 0, flex: 1 }}
|
|
||||||
>
|
|
||||||
{notification.details}
|
|
||||||
</Paragraph>
|
|
||||||
</div>
|
|
||||||
<Space
|
|
||||||
direction="vertical"
|
|
||||||
size={4}
|
|
||||||
style={{ fontSize: '13px', color: '#8c8c8c' }}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<ClockCircleOutlined />
|
|
||||||
<Text type="secondary">
|
|
||||||
{notification.timestamp}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EnvironmentOutlined />
|
|
||||||
<Text type="secondary">
|
|
||||||
{notification.location}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<LinkOutlined />
|
|
||||||
<AntdLink
|
|
||||||
href={notification.link}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{notification.link}
|
|
||||||
</AntdLink>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<SendOutlined />}
|
|
||||||
style={{ paddingLeft: '8px' }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleResend(notification);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Resend
|
{notification.details}
|
||||||
</Button>
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={4}
|
||||||
|
style={{ fontSize: '13px', color: '#8c8c8c' }}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
<Text type="secondary">
|
||||||
|
{notification.timestamp}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EnvironmentOutlined />
|
||||||
|
<Text type="secondary">
|
||||||
|
{notification.location}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<LinkOutlined />
|
||||||
|
<AntdLink
|
||||||
|
href={notification.link}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{notification.link}
|
||||||
|
</AntdLink>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
style={{ paddingLeft: '8px' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleResend(notification);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Col>
|
||||||
</Col>
|
<Col
|
||||||
<Col
|
flex="120px"
|
||||||
flex="120px"
|
style={{ textAlign: 'center' }}
|
||||||
style={{ textAlign: 'center' }}
|
align="bottom"
|
||||||
align="bottom"
|
|
||||||
>
|
|
||||||
<Space
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Space
|
||||||
type="text"
|
|
||||||
icon={
|
|
||||||
<UserOutlined
|
|
||||||
style={{ color: '#1890ff' }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="User History"
|
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #1890ff',
|
display: 'flex',
|
||||||
borderRadius: '4px',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setModalContent('user');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RouterLink
|
|
||||||
to={`/detail-notification/${
|
|
||||||
notification.id.split('-')[1]
|
|
||||||
}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={
|
icon={
|
||||||
<EyeOutlined
|
<UserOutlined
|
||||||
style={{ color: '#1890ff' }}
|
style={{ color: '#1890ff' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
title="Details"
|
title="User History"
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #1890ff',
|
border: '1px solid #1890ff',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
}}
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setModalContent('user');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</RouterLink>
|
<RouterLink
|
||||||
<Button
|
to={`/detail-notification/${
|
||||||
type="text"
|
notification.id.split('-')[1]
|
||||||
icon={
|
}`}
|
||||||
<HistoryOutlined
|
target="_blank"
|
||||||
style={{ color: '#1890ff' }}
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={
|
||||||
|
<EyeOutlined
|
||||||
|
style={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Details"
|
||||||
|
style={{
|
||||||
|
border: '1px solid #1890ff',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
</RouterLink>
|
||||||
title="Log History"
|
<Button
|
||||||
style={{
|
type="text"
|
||||||
border: '1px solid #1890ff',
|
icon={
|
||||||
borderRadius: '4px',
|
<HistoryOutlined
|
||||||
}}
|
style={{ color: '#1890ff' }}
|
||||||
onClick={(e) => {
|
/>
|
||||||
e.stopPropagation();
|
}
|
||||||
setModalContent('log');
|
title="Log History"
|
||||||
}}
|
style={{
|
||||||
/>
|
border: '1px solid #1890ff',
|
||||||
</Space>
|
borderRadius: '4px',
|
||||||
</Col>
|
}}
|
||||||
</Row>
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setModalContent('log');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
);
|
||||||
);
|
})
|
||||||
})
|
)}
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1212,16 +1241,15 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>{renderDeviceNotifications()}</Spin>
|
||||||
{renderDeviceNotifications()}
|
|
||||||
</Spin>
|
|
||||||
|
|
||||||
{/* PAGINATION */}
|
{/* PAGINATION */}
|
||||||
<Row justify="space-between" align="middle" style={{ marginTop: '16px' }}>
|
<Row justify="space-between" align="middle" style={{ marginTop: '16px' }}>
|
||||||
<Col>
|
<Col>
|
||||||
<div>
|
<div>
|
||||||
Menampilkan {pagination.current_limit} data halaman{' '}
|
Menampilkan {pagination.current_limit} data halaman{' '}
|
||||||
{pagination.current_page} dari total {pagination.total_limit} data
|
{pagination.current_page} dari total {pagination.total_limit}{' '}
|
||||||
|
data
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
|
|||||||
Reference in New Issue
Block a user