+ 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 [confirmLoading, setConfirmLoading] = useState(false);
+ const [fileList, setFileList] = useState([]);
+ const [previewOpen, setPreviewOpen] = useState(false);
+ const [previewImage, setPreviewImage] = useState('');
+ const [previewTitle, setPreviewTitle] = useState('');
const defaultData = {
sparepart_id: '',
@@ -18,7 +44,8 @@ const DetailSparepart = (props) => {
sparepart_item_type: '',
sparepart_unit: '',
sparepart_merk: '',
- sparepart_stok: '',
+ sparepart_stok: '0',
+ sparepart_foto: '',
};
const [formData, setFormData] = useState(defaultData);
@@ -26,60 +53,200 @@ const DetailSparepart = (props) => {
const handleCancel = () => {
props.setSelectedData(null);
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 () => {
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,
- });
+ NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
setConfirmLoading(false);
})
)
return;
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 = {
- 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,
+ sparepart_name: formData.sparepart_name, // Wajib
};
+ // 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
? 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;
+ console.log('API response:', response);
+ if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
- message: `Data Sparepart "${sparepartName}" berhasil ${
+ message: `Data Sparepart berhasil ${
formData.sparepart_id ? 'diubah' : 'ditambahkan'
}.`,
});
-
props.setActionMode('list');
+ setFileList([]);
} else {
NotifAlert({
icon: 'error',
@@ -101,35 +268,47 @@ const DetailSparepart = (props) => {
const handleInputChange = (e) => {
const { name, value } = e.target;
- setFormData({
- ...formData,
- [name]: value,
- });
+ 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,
- });
+ const handleSelectChange = (name, value) => {
+ setFormData({ ...formData, [name]: value });
};
useEffect(() => {
if (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 {
setFormData(defaultData);
+ setFileList([]);
}
}, [props.showModal, props.selectedData, props.actionMode]);
+ const uploadButton = (
+
+ );
+
return (
{
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
- defaultHoverColor: '#23A55A',
},
},
}}
@@ -162,9 +340,7 @@ const DetailSparepart = (props) => {
{
>
{formData && (
)}
);
};
-export default DetailSparepart;
\ No newline at end of file
+export default DetailSparepart;
diff --git a/src/pages/master/sparepart/component/ListSparepart.jsx b/src/pages/master/sparepart/component/ListSparepart.jsx
index dc7d410..d71cc72 100644
--- a/src/pages/master/sparepart/component/ListSparepart.jsx
+++ b/src/pages/master/sparepart/component/ListSparepart.jsx
@@ -13,6 +13,7 @@ import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/
import { useNavigate } from 'react-router-dom';
import { deleteSparepart, getAllSparepart } from '../../../../api/sparepart';
import TableList from '../../../../components/Global/TableList';
+import SparepartCardList from './SparepartCardList'; // Import the new custom card component
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
@@ -265,6 +266,14 @@ const ListSparepart = memo(function ListSparepart(props) {
queryParams={formDataFilter}
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
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
/>
diff --git a/src/pages/master/sparepart/component/SparepartCardList.jsx b/src/pages/master/sparepart/component/SparepartCardList.jsx
new file mode 100644
index 0000000..97313f0
--- /dev/null
+++ b/src/pages/master/sparepart/component/SparepartCardList.jsx
@@ -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 (
+
+ {data.map((item) => {
+ const quantity = updateQuantities[item.sparepart_id] || 0;
+ const isLoading = loadingQuantities[item.sparepart_id] || false;
+
+ return (
+
+
+
+
+
+ {item.sparepart_item_type && (
+
+ {item.sparepart_item_type}
+
+ )}
+
+ {(() => {
+ // 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 (
+
+
{
+ console.error('Image failed to load:', imgSrc);
+ e.target.src = 'https://via.placeholder.com/150';
+ }}
+ onLoad={() => console.log('Image loaded successfully:', imgSrc)}
+ />
+
+ );
+ })()}
+
+
+
+
+
+
+ {showEditModal && (
+ }
+ key="edit"
+ onClick={() => showEditModal(item)}
+ size="small"
+ />
+ )}
+ {showDeleteDialog && (
+ }
+ key="delete"
+ onClick={() => showDeleteDialog(item)}
+ size="small"
+ danger
+ />
+ )}
+
+
+ {item[header]}
+
+
+ Available Stock: {item.sparepart_stok || '0'}
+
+
+
+
+ }
+ onClick={() =>
+ handleQuantityChange(
+ item.sparepart_id,
+ quantity - 1
+ )
+ }
+ disabled={isLoading}
+ style={{ width: 28, height: 28 }}
+ />
+
+ {quantity}
+
+ }
+ onClick={() =>
+ handleQuantityChange(
+ item.sparepart_id,
+ quantity + 1
+ )
+ }
+ disabled={isLoading}
+ style={{ width: 28, height: 28 }}
+ />
+
+ {item.sparepart_unit
+ ? ` / ${item.sparepart_unit}`
+ : ' / pcs'}
+
+
+
+
handleUpdateStock(item)}
+ loading={isLoading}
+ >
+ Update Stock
+
+
+
+
+ Last updated:{' '}
+ {item.updated_at
+ ? dayjs(item.updated_at).format('DD MMM YYYY')
+ : 'N/A'}
+
+
+
+
+
+
+ );
+ })}
+
+ );
+};
+
+export default SparepartCardList;
diff --git a/src/pages/notification/component/ListNotification.jsx b/src/pages/notification/component/ListNotification.jsx
index 89441b8..30e3623 100644
--- a/src/pages/notification/component/ListNotification.jsx
+++ b/src/pages/notification/component/ListNotification.jsx
@@ -146,21 +146,43 @@ const ListNotification = memo(function ListNotification(props) {
const navigate = useNavigate();
// Fetch notifications from API
- const fetchNotifications = async () => {
+ const fetchNotifications = async (page = 1, limit = 10, isRead = null) => {
setLoading(true);
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) {
const transformedData = transformNotificationData(response.data);
setNotifications(transformedData);
- // Update pagination with mock data (since API doesn't provide pagination info)
- const totalItems = transformedData.length;
- setPagination((prev) => ({
- ...prev,
- total_limit: totalItems,
- total_page: Math.ceil(totalItems / prev.current_limit),
- }));
+ // Update pagination with API response or calculate from data
+ if (response.paging) {
+ setPagination({
+ current_page: response.paging.current_page || page,
+ current_limit: response.paging.current_limit || 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) {
console.error('Error fetching notifications:', error);
@@ -178,13 +200,10 @@ const ListNotification = memo(function ListNotification(props) {
current_page: page,
current_limit: pageSize,
}));
- };
- // Get paginated notifications
- const getPaginatedNotifications = () => {
- const startIndex = (pagination.current_page - 1) * pagination.current_limit;
- const endIndex = startIndex + pagination.current_limit;
- return filteredNotifications.slice(startIndex, endIndex);
+ // Fetch notifications with new pagination
+ const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
+ fetchNotifications(page, pageSize, isReadFilter);
};
useEffect(() => {
@@ -194,9 +213,10 @@ const ListNotification = memo(function ListNotification(props) {
return;
}
- // Fetch notifications on component mount
- fetchNotifications();
- }, []);
+ // Fetch notifications on component mount and when tab changes
+ const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
+ fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
+ }, [activeTab]);
const getIconAndColor = (type) => {
switch (type) {
@@ -248,22 +268,17 @@ const ListNotification = memo(function ListNotification(props) {
setSearchTerm('');
};
- const filteredNotifications = notifications
- .filter((n) => {
- const matchesTab =
- activeTab === 'all' ||
- (activeTab === 'unread' && !n.isRead) ||
- (activeTab === 'read' && n.isRead);
- return matchesTab;
- })
- .filter((n) => {
- if (!searchTerm) return true;
- // Search by title and error code name
+ const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
+
+ // Filter notifications based on search term
+ const getFilteredNotifications = () => {
+ if (!searchTerm) return notifications;
+ // Search by title and error code name
+ return notifications.filter((n) => {
const searchableText = `${n.title} ${n.issue}`.toLowerCase();
return searchableText.includes(searchTerm.toLowerCase());
});
-
- const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
+ };
const tabButtonStyle = (isActive) => ({
padding: '12px 16px',
@@ -279,8 +294,7 @@ const ListNotification = memo(function ListNotification(props) {
});
const renderDeviceNotifications = () => {
- const paginatedNotifications = getPaginatedNotifications();
-
+ const filteredNotifications = getFilteredNotifications();
return (
{filteredNotifications.length === 0 ? (
@@ -288,200 +302,215 @@ const ListNotification = memo(function ListNotification(props) {
Tidak ada notifikasi
) : (
- paginatedNotifications.map((notification) => {
- const { IconComponent, color, bgColor } = getIconAndColor(notification.type);
- return (
-