Merge pull request 'lavoce' (#29) from lavoce into main
Reviewed-on: #29
This commit is contained in:
@@ -53,6 +53,7 @@ import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
|
|||||||
|
|
||||||
// Image Viewer
|
// Image Viewer
|
||||||
import ImageViewer from './Utils/ImageViewer';
|
import ImageViewer from './Utils/ImageViewer';
|
||||||
|
import RedirectWa from './pages/blank/RedirectWa';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
@@ -72,6 +73,8 @@ const App = () => {
|
|||||||
element={<IndexVerificationSparepart />}
|
element={<IndexVerificationSparepart />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route path="/redirect" element={<RedirectWa />} />
|
||||||
|
|
||||||
{/* Protected Routes */}
|
{/* Protected Routes */}
|
||||||
<Route path="/dashboard" element={<ProtectedRoute />}>
|
<Route path="/dashboard" element={<ProtectedRoute />}>
|
||||||
<Route path="home" element={<Home />} />
|
<Route path="home" element={<Home />} />
|
||||||
|
|||||||
@@ -70,7 +70,18 @@ async function ApiRequest({ method = 'GET', params = {}, prefix = '/', token = t
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawToken = localStorage.getItem('token');
|
const tokenRedirect = sessionStorage.getItem('token_redirect');
|
||||||
|
|
||||||
|
let rawToken = '';
|
||||||
|
|
||||||
|
if (tokenRedirect !== null) {
|
||||||
|
rawToken = tokenRedirect;
|
||||||
|
// console.log(`sessionStorage: ${tokenRedirect}`);
|
||||||
|
} else {
|
||||||
|
rawToken = localStorage.getItem('token');
|
||||||
|
// console.log(`localStorage: ${rawToken}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (token && rawToken) {
|
if (token && rawToken) {
|
||||||
const cleanToken = rawToken.replace(/"/g, '');
|
const cleanToken = rawToken.replace(/"/g, '');
|
||||||
request.headers['Authorization'] = `Bearer ${cleanToken}`;
|
request.headers['Authorization'] = `Bearer ${cleanToken}`;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const allItems = [
|
|||||||
{
|
{
|
||||||
key: 'master-sparepart',
|
key: 'master-sparepart',
|
||||||
icon: <ToolOutlined style={{ fontSize: '19px' }} />,
|
icon: <ToolOutlined style={{ fontSize: '19px' }} />,
|
||||||
label: <Link to="/master/sparepart">sparepart</Link>,
|
label: <Link to="/master/sparepart">Sparepart</Link>,
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'master-shift',
|
// key: 'master-shift',
|
||||||
|
|||||||
49
src/pages/blank/RedirectWa.jsx
Normal file
49
src/pages/blank/RedirectWa.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { verifyRedirect } from '../../api/auth';
|
||||||
|
import { encryptData } from '../../components/Global/Formatter';
|
||||||
|
import NotFound from './NotFound';
|
||||||
|
import Waiting from './Waiting';
|
||||||
|
import NotificationDetailTab from '../notificationDetail/IndexNotificationDetail';
|
||||||
|
|
||||||
|
export default function RedirectWa() {
|
||||||
|
const [idData, setIdData] = useState(0);
|
||||||
|
const [ready, setReady] = useState(0);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// URLSearchParams untuk ambil query
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const token = queryParams.get('token');
|
||||||
|
|
||||||
|
const handleInitForm = async (encodedToken) => {
|
||||||
|
const originalToken = decodeURIComponent(encodedToken);
|
||||||
|
// console.log(originalToken);
|
||||||
|
|
||||||
|
const response = await verifyRedirect({
|
||||||
|
tokenRedirect: originalToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('tes', response);
|
||||||
|
|
||||||
|
const tokenResult = JSON.stringify(response.data?.accessToken);
|
||||||
|
|
||||||
|
sessionStorage.setItem('token_redirect', tokenResult);
|
||||||
|
response.data.auth = true;
|
||||||
|
sessionStorage.setItem('session', encryptData(response?.data));
|
||||||
|
|
||||||
|
setIdData(response.data.data.idData);
|
||||||
|
|
||||||
|
setReady(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleInitForm(token);
|
||||||
|
}, [idData]);
|
||||||
|
|
||||||
|
if (ready == 0) return <Waiting />;
|
||||||
|
|
||||||
|
if (idData === 0) return <NotFound />;
|
||||||
|
|
||||||
|
return <NotificationDetailTab id={idData} />;
|
||||||
|
}
|
||||||
@@ -352,7 +352,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
<Title level={3}>Jadwal Shift</Title>
|
<Title level={3}>Jadwal Shift</Title>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Row>
|
{/* <Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<Row justify="end" align="middle" gutter={[8, 8]}>
|
<Row justify="end" align="middle" gutter={[8, 8]}>
|
||||||
<Col xs={24} sm={24} md={12} lg={12}>
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
@@ -383,7 +383,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row> */}
|
||||||
|
|
||||||
<div style={{ marginTop: '24px' }}>
|
<div style={{ marginTop: '24px' }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ const ListErrorCode = ({
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
height: '32px',
|
height: '32px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '300px'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const SparepartSelect = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('limit', '1000');
|
params.set('limit', '10');
|
||||||
|
|
||||||
if (searchQuery && searchQuery.trim() !== '') {
|
if (searchQuery && searchQuery.trim() !== '') {
|
||||||
params.set('criteria', searchQuery.trim());
|
params.set('criteria', searchQuery.trim());
|
||||||
@@ -137,7 +137,7 @@ const SparepartSelect = ({
|
|||||||
>
|
>
|
||||||
{spareparts
|
{spareparts
|
||||||
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
||||||
.slice(0, 5)
|
.slice(0, 10)
|
||||||
.map((sparepart) => (
|
.map((sparepart) => (
|
||||||
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
|
||||||
|
|
||||||
export const useErrorCodeLogic = (errorCodeForm, fileList) => {
|
|
||||||
const [solutionFields, setSolutionFields] = useState([0]);
|
|
||||||
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
|
||||||
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
|
||||||
const [firstSolutionValid, setFirstSolutionValid] = useState(false);
|
|
||||||
const [solutionsToDelete, setSolutionsToDelete] = useState(new Set());
|
|
||||||
|
|
||||||
const checkPreviousSolutionValid = (currentSolutionIndex) => {
|
|
||||||
for (let i = 0; i < currentSolutionIndex; i++) {
|
|
||||||
const fieldId = solutionFields[i];
|
|
||||||
const solutionType = solutionTypes[fieldId];
|
|
||||||
|
|
||||||
const solutionName = errorCodeForm.getFieldValue(`solution_name_${fieldId}`);
|
|
||||||
if (!solutionName || solutionName.trim() === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
|
||||||
const textSolution = errorCodeForm.getFieldValue(`text_solution_${fieldId}`);
|
|
||||||
if (!textSolution || textSolution.trim() === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (solutionType === 'file') {
|
|
||||||
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
|
|
||||||
if (filesForSolution.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkFirstSolutionValid = () => {
|
|
||||||
if (solutionFields.length === 0) {
|
|
||||||
setFirstSolutionValid(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const isValid = checkPreviousSolutionValid(1);
|
|
||||||
setFirstSolutionValid(isValid);
|
|
||||||
return isValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddSolutionField = () => {
|
|
||||||
const currentSolutionCount = solutionFields.length;
|
|
||||||
const nextSolutionNumber = currentSolutionCount + 1;
|
|
||||||
|
|
||||||
if (!checkPreviousSolutionValid(currentSolutionCount)) {
|
|
||||||
let incompleteSolutionIndex = -1;
|
|
||||||
for (let i = 0; i < currentSolutionCount; i++) {
|
|
||||||
const fieldId = solutionFields[i];
|
|
||||||
const solutionType = solutionTypes[fieldId];
|
|
||||||
const solutionName = errorCodeForm.getFieldValue(`solution_name_${fieldId}`);
|
|
||||||
let hasContent = false;
|
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
|
||||||
const textSolution = errorCodeForm.getFieldValue(`text_solution_${fieldId}`);
|
|
||||||
hasContent = textSolution && textSolution.trim();
|
|
||||||
} else if (solutionType === 'file') {
|
|
||||||
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
|
|
||||||
hasContent = filesForSolution.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solutionName?.trim() || !hasContent) {
|
|
||||||
incompleteSolutionIndex = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Perhatian',
|
|
||||||
message: `Harap lengkapi Solution ${incompleteSolutionIndex} terlebih dahulu sebelum menambah Solution ${nextSolutionNumber}!`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newId = `new-${Date.now()}`;
|
|
||||||
setSolutionFields(prev => [...prev, newId]);
|
|
||||||
setSolutionTypes(prev => ({ ...prev, [newId]: 'text' }));
|
|
||||||
setSolutionStatuses(prev => ({ ...prev, [newId]: true }));
|
|
||||||
errorCodeForm.setFieldValue(`solution_status_${newId}`, true);
|
|
||||||
errorCodeForm.setFieldValue(`solution_type_${newId}`, 'text');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveSolutionField = (id) => {
|
|
||||||
const isNewSolution = !id.toString().startsWith('existing-');
|
|
||||||
|
|
||||||
if (isNewSolution) {
|
|
||||||
if (solutionFields.length > 1) {
|
|
||||||
setSolutionFields(solutionFields.filter(fieldId => fieldId !== id));
|
|
||||||
setSolutionTypes(prev => {
|
|
||||||
const newTypes = { ...prev };
|
|
||||||
delete newTypes[id];
|
|
||||||
return newTypes;
|
|
||||||
});
|
|
||||||
setSolutionStatuses(prev => {
|
|
||||||
const newStatuses = { ...prev };
|
|
||||||
delete newStatuses[id];
|
|
||||||
return newStatuses;
|
|
||||||
});
|
|
||||||
setSolutionsToDelete(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(id);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Perhatian',
|
|
||||||
message: 'Setiap error code harus memiliki minimal 1 solution!'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const solutionName = errorCodeForm.getFieldValue(`solution_name_${id}`);
|
|
||||||
const solutionType = solutionTypes[id];
|
|
||||||
let isEmpty = true;
|
|
||||||
|
|
||||||
const existingSolution = window.currentSolutionData?.[id];
|
|
||||||
const hasExistingData = existingSolution && (
|
|
||||||
(existingSolution.solution_name && existingSolution.solution_name.trim()) ||
|
|
||||||
(existingSolution.text_solution && existingSolution.text_solution.trim()) ||
|
|
||||||
(existingSolution.path_solution && existingSolution.path_solution.trim())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
|
||||||
const textSolution = errorCodeForm.getFieldValue(`text_solution_${id}`);
|
|
||||||
isEmpty = !solutionName?.trim() && !textSolution?.trim() && !hasExistingData;
|
|
||||||
} else if (solutionType === 'file') {
|
|
||||||
const filesForSolution = fileList.filter(file => file.solutionId === id);
|
|
||||||
isEmpty = !solutionName?.trim() && filesForSolution.length === 0 && !hasExistingData;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty) {
|
|
||||||
if (solutionFields.length > 1) {
|
|
||||||
setSolutionFields(solutionFields.filter(fieldId => fieldId !== id));
|
|
||||||
setSolutionTypes(prev => {
|
|
||||||
const newTypes = { ...prev };
|
|
||||||
delete newTypes[id];
|
|
||||||
return newTypes;
|
|
||||||
});
|
|
||||||
setSolutionStatuses(prev => {
|
|
||||||
const newStatuses = { ...prev };
|
|
||||||
delete newStatuses[id];
|
|
||||||
return newStatuses;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.currentSolutionData) {
|
|
||||||
delete window.currentSolutionData[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
setSolutionsToDelete(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(id);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Perhatian',
|
|
||||||
message: 'Setiap error code harus memiliki minimal 1 solution!'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (solutionFields.length > 1) {
|
|
||||||
setSolutionsToDelete(prev => new Set(prev).add(id));
|
|
||||||
|
|
||||||
const solutionElement = document.querySelector(`[data-solution-id="${id}"]`);
|
|
||||||
if (solutionElement) {
|
|
||||||
solutionElement.style.opacity = '0.5';
|
|
||||||
solutionElement.style.border = '2px dashed #ff4d4f';
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifOk({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: 'Solution ditandai untuk dihapus. Klik "Update Error Code" untuk menyimpan perubahan.'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Perhatian',
|
|
||||||
message: 'Setiap error code harus memiliki minimal 1 solution!'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionTypeChange = (fieldId, type) => {
|
|
||||||
setSolutionTypes(prev => ({ ...prev, [fieldId]: type }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionStatusChange = (fieldId, status) => {
|
|
||||||
setSolutionStatuses(prev => ({
|
|
||||||
...prev,
|
|
||||||
[fieldId]: status
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSolutionsForExistingRecord = (solutions, errorCodeForm) => {
|
|
||||||
const newSolutionFields = [];
|
|
||||||
const newSolutionTypes = {};
|
|
||||||
const newSolutionStatuses = {};
|
|
||||||
const newSolutionData = {};
|
|
||||||
|
|
||||||
solutions.forEach((solution, index) => {
|
|
||||||
const fieldId = `existing-${index}`;
|
|
||||||
newSolutionFields.push(fieldId);
|
|
||||||
newSolutionTypes[fieldId] = solution.type_solution || 'text';
|
|
||||||
newSolutionStatuses[fieldId] = solution.is_active !== false;
|
|
||||||
newSolutionData[fieldId] = {
|
|
||||||
...solution
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
errorCodeForm.setFieldsValue({
|
|
||||||
[`solution_name_${fieldId}`]: solution.solution_name,
|
|
||||||
[`text_solution_${fieldId}`]: solution.text_solution || '',
|
|
||||||
[`solution_status_${fieldId}`]: solution.is_active !== false,
|
|
||||||
[`solution_type_${fieldId}`]: solution.type_solution === 'image' || solution.type_solution === 'pdf' ? 'file' : solution.type_solution
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
setSolutionFields(newSolutionFields);
|
|
||||||
setSolutionTypes(newSolutionTypes);
|
|
||||||
setSolutionStatuses(newSolutionStatuses);
|
|
||||||
window.currentSolutionData = newSolutionData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSolutionFields = () => {
|
|
||||||
setSolutionFields([0]);
|
|
||||||
setSolutionTypes({ 0: 'text' });
|
|
||||||
setSolutionStatuses({ 0: true });
|
|
||||||
setFirstSolutionValid(false);
|
|
||||||
setSolutionsToDelete(new Set());
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
checkFirstSolutionValid();
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [solutionFields, solutionTypes, fileList, errorCodeForm]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
solutionFields,
|
|
||||||
solutionTypes,
|
|
||||||
solutionStatuses,
|
|
||||||
firstSolutionValid,
|
|
||||||
solutionsToDelete,
|
|
||||||
handleAddSolutionField,
|
|
||||||
handleRemoveSolutionField,
|
|
||||||
handleSolutionTypeChange,
|
|
||||||
handleSolutionStatusChange,
|
|
||||||
resetSolutionFields,
|
|
||||||
checkFirstSolutionValid,
|
|
||||||
setSolutionsForExistingRecord
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export const useSolutionLogic = (solutionForm) => {
|
|
||||||
const [solutionFields, setSolutionFields] = useState([0]);
|
|
||||||
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
|
||||||
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
|
||||||
const [solutionsToDelete, setSolutionsToDelete] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (solutionForm) {
|
|
||||||
solutionForm.setFieldsValue({
|
|
||||||
solution_items: {
|
|
||||||
0: {
|
|
||||||
name: 'Solution 1',
|
|
||||||
status: true,
|
|
||||||
type: 'text',
|
|
||||||
text: 'Solution description',
|
|
||||||
file: null,
|
|
||||||
fileUpload: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, [solutionForm]);
|
|
||||||
|
|
||||||
const handleAddSolutionField = () => {
|
|
||||||
const newKey = Date.now();
|
|
||||||
|
|
||||||
setSolutionFields(prev => [...prev, newKey]);
|
|
||||||
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
|
|
||||||
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentFormValues = solutionForm.getFieldsValue(true);
|
|
||||||
const existingNames = [];
|
|
||||||
|
|
||||||
Object.keys(currentFormValues).forEach(key => {
|
|
||||||
if (key.startsWith('solution_items,') || key.startsWith('solution_items.')) {
|
|
||||||
const solutionData = currentFormValues[key];
|
|
||||||
if (solutionData && solutionData.name) {
|
|
||||||
existingNames.push(solutionData.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentFormValues.solution_items) {
|
|
||||||
Object.values(currentFormValues.solution_items).forEach(solution => {
|
|
||||||
if (solution && solution.name) {
|
|
||||||
existingNames.push(solution.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let solutionNumber = solutionFields.length + 1;
|
|
||||||
let defaultName = `Solution ${solutionNumber}`;
|
|
||||||
|
|
||||||
while (existingNames.includes(defaultName)) {
|
|
||||||
solutionNumber++;
|
|
||||||
defaultName = `Solution ${solutionNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'name'], defaultName);
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'text'], 'Solution description');
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'status'], true);
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'file'], null);
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'fileUpload'], null);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveSolutionField = (key) => {
|
|
||||||
if (solutionFields.length <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSolutionFields(prev => prev.filter(field => field !== key));
|
|
||||||
|
|
||||||
const newTypes = { ...solutionTypes };
|
|
||||||
const newStatuses = { ...solutionStatuses };
|
|
||||||
delete newTypes[key];
|
|
||||||
delete newStatuses[key];
|
|
||||||
|
|
||||||
setSolutionTypes(newTypes);
|
|
||||||
setSolutionStatuses(newStatuses);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
solutionForm.setFieldValue(['solution_items', key], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'name'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'type'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'text'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'status'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'file'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'fileUpload'], undefined);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionTypeChange = (key, value) => {
|
|
||||||
setSolutionTypes(prev => ({ ...prev, [key]: value }));
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const fieldName = ['solution_items', key];
|
|
||||||
const currentSolutionData = solutionForm.getFieldsValue([fieldName]) || {};
|
|
||||||
const solutionData = currentSolutionData[`solution_items,${key}`] || currentSolutionData[`solution_items.${key}`] || {};
|
|
||||||
|
|
||||||
if (value === 'text') {
|
|
||||||
const updatedSolutionData = {
|
|
||||||
...solutionData,
|
|
||||||
fileUpload: null,
|
|
||||||
file: null,
|
|
||||||
path_solution: null,
|
|
||||||
fileName: null,
|
|
||||||
text: solutionData.text || 'Solution description'
|
|
||||||
};
|
|
||||||
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileUpload'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'file'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'path_solution'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileName'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'text'], updatedSolutionData.text);
|
|
||||||
} else if (value === 'file') {
|
|
||||||
const updatedSolutionData = {
|
|
||||||
...solutionData,
|
|
||||||
text: '',
|
|
||||||
fileUpload: null,
|
|
||||||
file: null,
|
|
||||||
path_solution: null,
|
|
||||||
fileName: null
|
|
||||||
};
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'text'], '');
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileUpload'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'file'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'path_solution'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileName'], null);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionStatusChange = (key, value) => {
|
|
||||||
setSolutionStatuses(prev => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSolutionFields = () => {
|
|
||||||
setSolutionFields([0]);
|
|
||||||
setSolutionTypes({ 0: 'text' });
|
|
||||||
setSolutionStatuses({ 0: true });
|
|
||||||
|
|
||||||
if (!solutionForm || !solutionForm.resetFields) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
solutionForm.resetFields();
|
|
||||||
setTimeout(() => {
|
|
||||||
solutionForm.setFieldsValue({
|
|
||||||
solution_items: {
|
|
||||||
0: {
|
|
||||||
name: 'Solution 1',
|
|
||||||
status: true,
|
|
||||||
type: 'text',
|
|
||||||
text: 'Solution description',
|
|
||||||
file: null,
|
|
||||||
fileUpload: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'name'], 'Solution 1');
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'type'], 'text');
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'text'], 'Solution description');
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'status'], true);
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'file'], null);
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'fileUpload'], null);
|
|
||||||
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkFirstSolutionValid = () => {
|
|
||||||
if (!solutionForm || !solutionForm.getFieldsValue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const values = solutionForm.getFieldsValue();
|
|
||||||
|
|
||||||
const firstField = solutionFields[0];
|
|
||||||
if (!firstField) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const solutionKey = firstField.key || firstField;
|
|
||||||
const commaPath = `solution_items,${solutionKey}`;
|
|
||||||
const dotPath = `solution_items.${solutionKey}`;
|
|
||||||
const firstSolution = values[commaPath] || values[dotPath];
|
|
||||||
|
|
||||||
if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solutionTypes[solutionKey] === 'text' && (!firstSolution.text || firstSolution.text.trim() === '')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSolutionData = () => {
|
|
||||||
try {
|
|
||||||
const values = solutionForm.getFieldsValue(true);
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
solutionFields.forEach(key => {
|
|
||||||
let solution = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
solution = solutionForm.getFieldValue(['solution_items', key]);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution && values.solution_items && values.solution_items[key]) {
|
|
||||||
solution = values.solution_items[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const commaKey = `solution_items,${key}`;
|
|
||||||
solution = values[commaKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const dotKey = `solution_items.${key}`;
|
|
||||||
solution = values[dotKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const allKeys = Object.keys(values);
|
|
||||||
const foundKey = allKeys.find(k =>
|
|
||||||
k.includes(key.toString()) &&
|
|
||||||
k.includes('solution_items')
|
|
||||||
);
|
|
||||||
if (foundKey) {
|
|
||||||
solution = values[foundKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const rawValues = solutionForm.getFieldsValue();
|
|
||||||
|
|
||||||
if (rawValues.solution_items && rawValues.solution_items[key]) {
|
|
||||||
solution = rawValues.solution_items[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const hasName = solution.name && solution.name.trim() !== '';
|
|
||||||
|
|
||||||
if (!hasName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const solutionType = solutionTypes[key] || solution.type || 'text';
|
|
||||||
let isValidType = true;
|
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
|
||||||
isValidType = solution.text && solution.text.trim() !== '';
|
|
||||||
if (!isValidType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (solutionType === 'file') {
|
|
||||||
const hasPathSolution = solution.path_solution && solution.path_solution.trim() !== '';
|
|
||||||
const hasFileUpload = (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0);
|
|
||||||
const hasFile = (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0);
|
|
||||||
|
|
||||||
isValidType = hasPathSolution || hasFileUpload || hasFile;
|
|
||||||
if (!isValidType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pathSolution = '';
|
|
||||||
let fileObject = null;
|
|
||||||
const typeSolution = solutionTypes[key] || solution.type || 'text';
|
|
||||||
|
|
||||||
if (typeSolution === 'file') {
|
|
||||||
if (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0) {
|
|
||||||
pathSolution = solution.fileUpload.path_solution || solution.fileUpload.uploadPath || '';
|
|
||||||
fileObject = solution.fileUpload;
|
|
||||||
} else if (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0) {
|
|
||||||
pathSolution = solution.file.path_solution || solution.file.uploadPath || '';
|
|
||||||
fileObject = solution.file;
|
|
||||||
} else if (solution.file && typeof solution.file === 'string' && solution.file.trim() !== '') {
|
|
||||||
pathSolution = solution.file;
|
|
||||||
} else if (solution.path_solution && solution.path_solution.trim() !== '') {
|
|
||||||
pathSolution = solution.path_solution;
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalTypeSolution = typeSolution;
|
|
||||||
if (typeSolution === 'file') {
|
|
||||||
if (fileObject && fileObject.type_solution) {
|
|
||||||
finalTypeSolution = fileObject.type_solution;
|
|
||||||
} else {
|
|
||||||
finalTypeSolution = 'image';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalSolution = {
|
|
||||||
solution_name: solution.name,
|
|
||||||
type_solution: finalTypeSolution,
|
|
||||||
is_active: solution.status !== false && solution.status !== undefined ? solution.status : (solutionStatuses[key] !== false),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeSolution === 'text') {
|
|
||||||
finalSolution.text_solution = solution.text || '';
|
|
||||||
finalSolution.path_solution = '';
|
|
||||||
} else {
|
|
||||||
finalSolution.text_solution = '';
|
|
||||||
finalSolution.path_solution = pathSolution;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(finalSolution);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSolutionsForExistingRecord = (solutions, form) => {
|
|
||||||
if (!solutions || solutions.length === 0) return;
|
|
||||||
|
|
||||||
const newFields = solutions.map((solution, index) => solution.id || index);
|
|
||||||
|
|
||||||
setSolutionFields(newFields);
|
|
||||||
|
|
||||||
const solutionsValues = {};
|
|
||||||
const newTypes = {};
|
|
||||||
const newStatuses = {};
|
|
||||||
|
|
||||||
solutions.forEach((solution, index) => {
|
|
||||||
const key = solution.brand_code_solution_id || solution.id || index;
|
|
||||||
|
|
||||||
let fileObject = null;
|
|
||||||
if (solution.path_solution && solution.path_solution.trim() !== '') {
|
|
||||||
const fileName = solution.file_upload_name || solution.path_solution.split('/').pop() || `file_${index}`;
|
|
||||||
|
|
||||||
fileObject = {
|
|
||||||
uploadPath: solution.path_solution,
|
|
||||||
path_solution: solution.path_solution,
|
|
||||||
name: fileName,
|
|
||||||
type_solution: solution.type_solution || 'image',
|
|
||||||
isExisting: true,
|
|
||||||
size: 0,
|
|
||||||
type: solution.type_solution === 'pdf' ? 'application/pdf' : 'image/jpeg',
|
|
||||||
fileExtension: solution.type_solution === 'pdf' ? 'pdf' : (fileName.split('.').pop().toLowerCase() || 'jpg')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFileType = solution.type_solution && solution.type_solution !== 'text' && fileObject;
|
|
||||||
|
|
||||||
solutionsValues[key] = {
|
|
||||||
name: solution.solution_name || '',
|
|
||||||
type: isFileType ? 'file' : 'text',
|
|
||||||
text: solution.text_solution || '',
|
|
||||||
file: fileObject,
|
|
||||||
fileUpload: fileObject,
|
|
||||||
status: solution.is_active !== false,
|
|
||||||
path_solution: solution.path_solution || ''
|
|
||||||
};
|
|
||||||
newTypes[key] = isFileType ? 'file' : 'text';
|
|
||||||
newStatuses[key] = solution.is_active !== false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const nestedFormValues = {
|
|
||||||
solution_items: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
|
||||||
const solution = solutionsValues[key];
|
|
||||||
nestedFormValues.solution_items[key] = {
|
|
||||||
name: solution.name,
|
|
||||||
type: solution.type,
|
|
||||||
text: solution.text,
|
|
||||||
file: solution.file,
|
|
||||||
fileUpload: solution.fileUpload,
|
|
||||||
status: solution.status,
|
|
||||||
path_solution: solution.path_solution
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
form.setFieldsValue(nestedFormValues);
|
|
||||||
|
|
||||||
const fallbackFormValues = {};
|
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
|
||||||
const solution = solutionsValues[key];
|
|
||||||
fallbackFormValues[`solution_items,${key}`] = {
|
|
||||||
name: solution.name,
|
|
||||||
type: solution.type,
|
|
||||||
text: solution.text,
|
|
||||||
file: solution.file,
|
|
||||||
fileUpload: solution.fileUpload,
|
|
||||||
status: solution.status,
|
|
||||||
path_solution: solution.path_solution
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
form.setFieldsValue(fallbackFormValues);
|
|
||||||
|
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
|
||||||
const solution = solutionsValues[key];
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'name'], solution.name);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'type'], solution.type);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'text'], solution.text);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'file'], solution.file);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'fileUpload'], solution.fileUpload);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'status'], solution.status);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'path_solution'], solution.path_solution);
|
|
||||||
|
|
||||||
form.setFieldValue(['solution_items', key, 'name'], solution.name);
|
|
||||||
form.setFieldValue(['solution_items', key, 'type'], solution.type);
|
|
||||||
form.setFieldValue(['solution_items', key, 'text'], solution.text);
|
|
||||||
form.setFieldValue(['solution_items', key, 'file'], solution.file);
|
|
||||||
form.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload);
|
|
||||||
form.setFieldValue(['solution_items', key, 'status'], solution.status);
|
|
||||||
form.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution);
|
|
||||||
});
|
|
||||||
|
|
||||||
setSolutionTypes(newTypes);
|
|
||||||
setSolutionStatuses(newStatuses);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
solutionFields,
|
|
||||||
solutionTypes,
|
|
||||||
solutionStatuses,
|
|
||||||
solutionsToDelete,
|
|
||||||
firstSolutionValid: checkFirstSolutionValid(),
|
|
||||||
handleAddSolutionField,
|
|
||||||
handleRemoveSolutionField,
|
|
||||||
handleSolutionTypeChange,
|
|
||||||
handleSolutionStatusChange,
|
|
||||||
resetSolutionFields,
|
|
||||||
checkFirstSolutionValid,
|
|
||||||
getSolutionData,
|
|
||||||
setSolutionsForExistingRecord,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import { getAllNotification } from '../../../api/notification';
|
import { getAllNotification, getNotificationLogByNotificationId } from '../../../api/notification';
|
||||||
|
|
||||||
const { Text, Paragraph, Link: AntdLink } = Typography;
|
const { Text, Paragraph, Link: AntdLink } = Typography;
|
||||||
|
|
||||||
@@ -47,17 +47,18 @@ const transformNotificationData = (apiData) => {
|
|||||||
return apiData.map((item, index) => ({
|
return apiData.map((item, index) => ({
|
||||||
id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index
|
id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index
|
||||||
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
|
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
|
||||||
title: item.error_code?.error_code_name || item.device_name || 'Unknown Error',
|
title: item.error_code_name || 'Unknown Error',
|
||||||
issue: item.error_code || item.error_code_name || 'Unknown Error',
|
issue: item.error_code || item.error_code_name || 'Unknown Error',
|
||||||
description: `${item.error_code} - ${item.error_code_name || ''}`,
|
description: `${item.error_code} - ${item.error_code_name || ''}`,
|
||||||
timestamp:
|
timestamp: item.created_at
|
||||||
item.created_at ? new Date(item.created_at).toLocaleString('id-ID', {
|
? new Date(item.created_at).toLocaleString('id-ID', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
}) + ' WIB' : 'N/A',
|
}) + ' WIB'
|
||||||
|
: 'N/A',
|
||||||
location: item.plant_sub_section_name || item.device_location || 'Location not specified',
|
location: item.plant_sub_section_name || item.device_location || 'Location not specified',
|
||||||
details: item.message_error_issue || 'No details available',
|
details: item.message_error_issue || 'No details available',
|
||||||
link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part
|
link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part
|
||||||
@@ -68,7 +69,10 @@ const transformNotificationData = (apiData) => {
|
|||||||
errorCode: item.error_code,
|
errorCode: item.error_code,
|
||||||
solutionName: item.error_code?.solution?.[0]?.solution_name || 'N/A',
|
solutionName: item.error_code?.solution?.[0]?.solution_name || 'N/A',
|
||||||
typeSolution: item.error_code?.solution?.[0]?.type_solution || 'N/A',
|
typeSolution: item.error_code?.solution?.[0]?.type_solution || 'N/A',
|
||||||
pathSolution: item.error_code?.solution?.[0]?.path_document || item.error_code?.solution?.[0]?.path_solution || 'N/A',
|
pathSolution:
|
||||||
|
item.error_code?.solution?.[0]?.path_document ||
|
||||||
|
item.error_code?.solution?.[0]?.path_solution ||
|
||||||
|
'N/A',
|
||||||
error_code: item.error_code,
|
error_code: item.error_code,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -98,37 +102,6 @@ const userHistoryData = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Dummy data untuk log history
|
|
||||||
const logHistoryData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
timestamp: '04-11-2025 11:55 WIB',
|
|
||||||
addedBy: {
|
|
||||||
name: 'Budi Santoso',
|
|
||||||
phone: '081122334455',
|
|
||||||
},
|
|
||||||
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
timestamp: '04-11-2025 11:45 WIB',
|
|
||||||
addedBy: {
|
|
||||||
name: 'John Doe',
|
|
||||||
phone: '081234567890',
|
|
||||||
},
|
|
||||||
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
timestamp: '04-11-2025 11:40 WIB',
|
|
||||||
addedBy: {
|
|
||||||
name: 'Jane Smith',
|
|
||||||
phone: '087654321098',
|
|
||||||
},
|
|
||||||
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ListNotification = memo(function ListNotification(props) {
|
const ListNotification = memo(function ListNotification(props) {
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
@@ -138,6 +111,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null
|
const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null
|
||||||
const [isAddingLog, setIsAddingLog] = useState(false);
|
const [isAddingLog, setIsAddingLog] = useState(false);
|
||||||
const [selectedNotification, setSelectedNotification] = useState(null);
|
const [selectedNotification, setSelectedNotification] = useState(null);
|
||||||
|
const [logHistoryData, setLogHistoryData] = useState([]);
|
||||||
|
const [logLoading, setLogLoading] = useState(false);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
current_limit: 10,
|
current_limit: 10,
|
||||||
@@ -281,6 +256,40 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch log history from API
|
||||||
|
const fetchLogHistory = async (notificationId) => {
|
||||||
|
try {
|
||||||
|
setLogLoading(true);
|
||||||
|
const response = await getNotificationLogByNotificationId(notificationId);
|
||||||
|
if (response && response.data) {
|
||||||
|
// Transform API data to component format
|
||||||
|
const transformedLogs = response.data.map((log) => ({
|
||||||
|
id: log.notification_error_log_id,
|
||||||
|
timestamp: log.created_at
|
||||||
|
? new Date(log.created_at).toLocaleString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}) + ' WIB'
|
||||||
|
: 'N/A',
|
||||||
|
addedBy: {
|
||||||
|
name: log.contact_name || 'Unknown',
|
||||||
|
phone: log.contact_phone || 'N/A',
|
||||||
|
},
|
||||||
|
description: log.notification_error_log_description || '',
|
||||||
|
}));
|
||||||
|
setLogHistoryData(transformedLogs);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching log history:', err);
|
||||||
|
setLogHistoryData([]); // Set empty array on error
|
||||||
|
} finally {
|
||||||
|
setLogLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tabButtonStyle = (isActive) => ({
|
const tabButtonStyle = (isActive) => ({
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -315,7 +324,6 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -500,6 +508,15 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Set the selected notification for the log history
|
||||||
|
const notificationId =
|
||||||
|
notification.id.split('-')[1];
|
||||||
|
setSelectedNotification(notification);
|
||||||
|
|
||||||
|
// Fetch log history for the selected notification
|
||||||
|
fetchLogHistory(notificationId);
|
||||||
|
|
||||||
setModalContent('log');
|
setModalContent('log');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -554,6 +571,15 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
|
|
||||||
const renderLogHistory = () => (
|
const renderLogHistory = () => (
|
||||||
<>
|
<>
|
||||||
|
{logLoading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : logHistoryData.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
||||||
|
Tidak ada log history
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div style={{ padding: '0 16px', position: 'relative' }}>
|
<div style={{ padding: '0 16px', position: 'relative' }}>
|
||||||
{/* Garis vertikal yang menyambung */}
|
{/* Garis vertikal yang menyambung */}
|
||||||
<div
|
<div
|
||||||
@@ -605,7 +631,10 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Space>
|
<Space>
|
||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined />
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
>
|
||||||
Added at {log.timestamp}
|
Added at {log.timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -643,6 +672,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -813,7 +843,16 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
bodyStyle={{ padding: '12px' }}
|
bodyStyle={{ padding: '12px' }}
|
||||||
onClick={() => setModalContent('log')}
|
onClick={() => {
|
||||||
|
// Set the selected notification for the log history if not already set
|
||||||
|
if (selectedNotification) {
|
||||||
|
const notificationId =
|
||||||
|
selectedNotification.id.split('-')[1];
|
||||||
|
// Fetch log history for the selected notification
|
||||||
|
fetchLogHistory(notificationId);
|
||||||
|
}
|
||||||
|
setModalContent('log');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<HistoryOutlined
|
<HistoryOutlined
|
||||||
@@ -838,7 +877,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<Text type="secondary" style={{ fontSize: '10px' }}>
|
<Text type="secondary" style={{ fontSize: '10px' }}>
|
||||||
PDF
|
PDF
|
||||||
</Text>
|
</Text>
|
||||||
} >
|
}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -1129,7 +1169,22 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
{logHistoryData.map((log) => (
|
{logLoading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '12px' }}>
|
||||||
|
<Spin size="small" />
|
||||||
|
</div>
|
||||||
|
) : logHistoryData.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '12px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tidak ada log history
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
logHistoryData.map((log) => (
|
||||||
<Card
|
<Card
|
||||||
key={log.id}
|
key={log.id}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1146,7 +1201,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
{log.timestamp}
|
{log.timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Layout, Card, Row, Col, Typography, Space, Button, Spin, Result, Input, message } from 'antd';
|
import {
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Typography,
|
||||||
|
Space,
|
||||||
|
Button,
|
||||||
|
Spin,
|
||||||
|
Result,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Avatar,
|
||||||
|
Tag,
|
||||||
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
CloseCircleFilled,
|
CloseCircleFilled,
|
||||||
@@ -15,10 +29,16 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
SendOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getNotificationDetail, createNotificationLog, getNotificationLogByNotificationId } from '../../api/notification';
|
import {
|
||||||
import UserHistoryModal from '../notification/component/UserHistoryModal';
|
getNotificationDetail,
|
||||||
import LogHistoryCard from '../notification/component/LogHistoryCard';
|
createNotificationLog,
|
||||||
|
getNotificationLogByNotificationId,
|
||||||
|
} from '../../api/notification';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
const { Text, Paragraph, Link } = Typography;
|
const { Text, Paragraph, Link } = Typography;
|
||||||
@@ -77,6 +97,58 @@ const transformNotificationData = (apiData) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dummy data baru untuk user history
|
||||||
|
const getDummyUsers = (notification) => {
|
||||||
|
if (!notification) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'John Doe',
|
||||||
|
phone: '081234567890',
|
||||||
|
status: 'delivered',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Jane Smith',
|
||||||
|
phone: '082345678901',
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Bob Johnson',
|
||||||
|
phone: '083456789012',
|
||||||
|
status: 'failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Alice Brown',
|
||||||
|
phone: '084567890123',
|
||||||
|
status: 'delivered',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusTag = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'delivered':
|
||||||
|
return (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||||
|
Delivered
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'sent':
|
||||||
|
return (
|
||||||
|
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||||
|
Sent
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'failed':
|
||||||
|
return <Tag color="error">Failed</Tag>;
|
||||||
|
default:
|
||||||
|
return <Tag color="default">{status}</Tag>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getIconAndColor = (type) => {
|
const getIconAndColor = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'critical':
|
case 'critical':
|
||||||
@@ -90,13 +162,13 @@ const getIconAndColor = (type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotificationDetailTab = () => {
|
const NotificationDetailTab = (props) => {
|
||||||
const { notificationId } = useParams(); // Mungkin perlu disesuaikan jika route berbeda
|
const params = useParams(); // Mungkin perlu disesuaikan jika route berbeda
|
||||||
|
const notificationId = props.id ?? params.notificationId;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [notification, setNotification] = useState(null);
|
const [notification, setNotification] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [modalContent, setModalContent] = useState(null); // 'user', atau null
|
|
||||||
const [isAddingLog, setIsAddingLog] = useState(false);
|
const [isAddingLog, setIsAddingLog] = useState(false);
|
||||||
|
|
||||||
// Log history states
|
// Log history states
|
||||||
@@ -251,6 +323,7 @@ const NotificationDetailTab = () => {
|
|||||||
marginBottom: '24px',
|
marginBottom: '24px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!props.id && (
|
||||||
<Row justify="space-between" align="middle">
|
<Row justify="space-between" align="middle">
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
@@ -262,15 +335,9 @@ const NotificationDetailTab = () => {
|
|||||||
Back to notification list
|
Back to notification list
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
|
||||||
<Button
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
onClick={() => setModalContent('user')}
|
|
||||||
>
|
|
||||||
User History
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f6ffed',
|
backgroundColor: '#f6ffed',
|
||||||
@@ -387,13 +454,79 @@ const NotificationDetailTab = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{/* Kolom Kanan: Log History */}
|
{/* Kolom Kanan: User History */}
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<LogHistoryCard
|
<Card title="User History" size="small" style={{ height: '100%' }}>
|
||||||
notificationData={notification}
|
<div
|
||||||
logData={logHistoryData}
|
style={{
|
||||||
loading={logLoading}
|
maxHeight: '400px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={2}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{getDummyUsers(notification).map((user) => (
|
||||||
|
<Card
|
||||||
|
key={user.id}
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%', margin: 0 }}
|
||||||
|
>
|
||||||
|
<Row align="middle" justify="space-between">
|
||||||
|
<Col>
|
||||||
|
<Space align="center">
|
||||||
|
<Avatar
|
||||||
|
size="large"
|
||||||
|
icon={<UserOutlined />}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<Text strong>{user.name}</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PhoneOutlined
|
||||||
|
style={{
|
||||||
|
color: '#8c8c8c',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text type="secondary">
|
||||||
|
{user.phone}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Space align="center" size="large">
|
||||||
|
{getStatusTag(user.status)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log(
|
||||||
|
`Resend to ${user.name}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@@ -428,11 +561,8 @@ const NotificationDetailTab = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={8} style={{ cursor: 'pointer' }}>
|
<Col xs={24} md={8}>
|
||||||
<Card
|
<Card bodyStyle={{ padding: '12px', textAlign: 'center' }}>
|
||||||
hoverable
|
|
||||||
bodyStyle={{ padding: '12px', textAlign: 'center' }}
|
|
||||||
>
|
|
||||||
<Space>
|
<Space>
|
||||||
<HistoryOutlined
|
<HistoryOutlined
|
||||||
style={{ fontSize: '16px', color: '#1890ff' }}
|
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||||
@@ -722,7 +852,9 @@ const NotificationDetailTab = () => {
|
|||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Tuliskan update penanganan di sini..."
|
placeholder="Tuliskan update penanganan di sini..."
|
||||||
value={newLogDescription}
|
value={newLogDescription}
|
||||||
onChange={(e) => setNewLogDescription(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setNewLogDescription(e.target.value)
|
||||||
|
}
|
||||||
disabled={submitLoading}
|
disabled={submitLoading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -731,8 +863,18 @@ const NotificationDetailTab = () => {
|
|||||||
type={isAddingLog ? 'primary' : 'dashed'}
|
type={isAddingLog ? 'primary' : 'dashed'}
|
||||||
size="small"
|
size="small"
|
||||||
block
|
block
|
||||||
icon={submitLoading ? <LoadingOutlined /> : (!isAddingLog && <PlusOutlined />)}
|
icon={
|
||||||
onClick={isAddingLog ? handleSubmitLog : () => setIsAddingLog(true)}
|
submitLoading ? (
|
||||||
|
<LoadingOutlined />
|
||||||
|
) : (
|
||||||
|
!isAddingLog && <PlusOutlined />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
isAddingLog
|
||||||
|
? handleSubmitLog
|
||||||
|
: () => setIsAddingLog(true)
|
||||||
|
}
|
||||||
loading={submitLoading}
|
loading={submitLoading}
|
||||||
disabled={submitLoading}
|
disabled={submitLoading}
|
||||||
>
|
>
|
||||||
@@ -780,12 +922,6 @@ const NotificationDetailTab = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<UserHistoryModal
|
|
||||||
visible={modalContent === 'user'}
|
|
||||||
onCancel={() => setModalContent(null)}
|
|
||||||
notificationData={notification}
|
|
||||||
/>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import autoTable from 'jspdf-autotable';
|
import autoTable from 'jspdf-autotable';
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -54,9 +56,9 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async (page = 1, pageSize = 10, showModal = false) => {
|
const fetchData = async (page = 1, pageSize = 10, showModal = false) => {
|
||||||
if (!plantSubSection) {
|
// if (!plantSubSection) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (showModal) {
|
if (showModal) {
|
||||||
setIsLoadingModal(true);
|
setIsLoadingModal(true);
|
||||||
@@ -195,8 +197,34 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
fetchData(pagination.current, pagination.pageSize, false);
|
fetchData(pagination.current, pagination.pageSize, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = async () => {
|
||||||
fetchData(1, pagination.pageSize, true);
|
setIsLoadingModal(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
plant_sub_section_id: plantSubSection,
|
||||||
|
from: formattedDateStart,
|
||||||
|
to: formattedDateEnd,
|
||||||
|
interval: periode,
|
||||||
|
page: 1,
|
||||||
|
limit: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pivotResponse = await getAllHistoryValueReportPivot(params);
|
||||||
|
|
||||||
|
// Jika response sukses, proses data
|
||||||
|
if (pivotResponse && pivotResponse.data) {
|
||||||
|
await fetchData(1, pagination.pageSize, false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
// Error akan ditangkap oleh api-request.js dan muncul Swal otomatis
|
||||||
|
} finally {
|
||||||
|
setIsLoadingModal(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@@ -247,6 +275,168 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
{ value: 120, label: '2 Hour', disabled: false },
|
{ value: 120, label: '2 Hour', disabled: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const exportToExcel = async () => {
|
||||||
|
if (pivotData.length === 0) {
|
||||||
|
alert('No data to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagMapping = {};
|
||||||
|
valueReportData.forEach(item => {
|
||||||
|
if (item.tag_name && item.tag_number) {
|
||||||
|
tagMapping[item.tag_name] = item.tag_number;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSection = plantSubSectionList.find(
|
||||||
|
item => item.plant_sub_section_id === plantSubSection
|
||||||
|
);
|
||||||
|
const sectionName = selectedSection ? selectedSection.plant_sub_section_name : 'Unknown';
|
||||||
|
|
||||||
|
// Buat struktur pivot yang sama seperti di tabel
|
||||||
|
const timeMap = new Map();
|
||||||
|
const tagSet = new Set();
|
||||||
|
|
||||||
|
pivotData.forEach((row) => {
|
||||||
|
const tagName = row.id;
|
||||||
|
tagSet.add(tagName);
|
||||||
|
|
||||||
|
const dataPoints = row.data || [];
|
||||||
|
dataPoints.forEach((item) => {
|
||||||
|
if (item && typeof item === 'object' && 'x' in item && 'y' in item) {
|
||||||
|
const datetime = item.x;
|
||||||
|
if (!timeMap.has(datetime)) {
|
||||||
|
timeMap.set(datetime, {});
|
||||||
|
}
|
||||||
|
timeMap.get(datetime)[tagName] = item.y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedTimes = Array.from(timeMap.keys()).sort();
|
||||||
|
const sortedTags = Array.from(tagSet).sort();
|
||||||
|
|
||||||
|
const pivotTableData = sortedTimes.map((datetime) => {
|
||||||
|
const rowData = {
|
||||||
|
datetime: datetime,
|
||||||
|
};
|
||||||
|
|
||||||
|
sortedTags.forEach((tagName) => {
|
||||||
|
rowData[tagName] = timeMap.get(datetime)[tagName];
|
||||||
|
});
|
||||||
|
|
||||||
|
return rowData;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Excel Pivot data:', pivotTableData.slice(0, 5));
|
||||||
|
console.log('Total rows for Excel:', pivotTableData.length);
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const ws = workbook.addWorksheet('Pivot Report');
|
||||||
|
|
||||||
|
// Buat header info (3 baris pertama)
|
||||||
|
ws.addRow(['PT. PUPUK INDONESIA UTILITAS']);
|
||||||
|
ws.addRow(['GRESIK GAS COGENERATION PLANT']);
|
||||||
|
ws.addRow([`${sectionName}`]);
|
||||||
|
ws.addRow([]); // Baris kosong sebagai pemisah
|
||||||
|
|
||||||
|
// Buat header kolom dengan tag number
|
||||||
|
const headerRow = [
|
||||||
|
'Datetime',
|
||||||
|
...sortedTags.map(tag => tagMapping[tag] || tag)
|
||||||
|
];
|
||||||
|
ws.addRow(headerRow);
|
||||||
|
|
||||||
|
// Buat data rows - PERBAIKAN: Simpan sebagai number murni
|
||||||
|
pivotTableData.forEach((rowData) => {
|
||||||
|
const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')];
|
||||||
|
sortedTags.forEach((tagName) => {
|
||||||
|
const value = rowData[tagName];
|
||||||
|
// Simpan sebagai number, bukan string
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
row.push(Number(value));
|
||||||
|
} else {
|
||||||
|
row.push('-');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws.addRow(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set column widths
|
||||||
|
ws.getColumn(1).width = 18; // Datetime column
|
||||||
|
for (let i = 2; i <= sortedTags.length + 1; i++) {
|
||||||
|
ws.getColumn(i).width = 12; // Tag columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge cells untuk header info
|
||||||
|
const totalCols = sortedTags.length + 1;
|
||||||
|
ws.mergeCells(1, 1, 1, totalCols); // Baris 1
|
||||||
|
ws.mergeCells(2, 1, 2, totalCols); // Baris 2
|
||||||
|
ws.mergeCells(3, 1, 3, totalCols); // Baris 3
|
||||||
|
|
||||||
|
// Style untuk header info (3 baris pertama - bold dan center)
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
const cell = ws.getCell(i, 1);
|
||||||
|
cell.font = { bold: true, size: 12 };
|
||||||
|
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style untuk header kolom (bold, background color, center, border)
|
||||||
|
const headerRowIndex = 5; // Baris header
|
||||||
|
for (let col = 1; col <= totalCols; col++) {
|
||||||
|
const cell = ws.getCell(headerRowIndex, col);
|
||||||
|
cell.font = { bold: true, size: 11 };
|
||||||
|
cell.fill = {
|
||||||
|
type: 'pattern',
|
||||||
|
pattern: 'solid',
|
||||||
|
fgColor: { argb: 'FFDCDCDC' }
|
||||||
|
};
|
||||||
|
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
||||||
|
cell.border = {
|
||||||
|
top: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
left: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
right: { style: 'thin', color: { argb: 'FF000000' } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style untuk data cells (border dan alignment) - PERBAIKAN: Format number dengan 2 desimal
|
||||||
|
for (let row = headerRowIndex + 1; row <= ws.rowCount; row++) {
|
||||||
|
for (let col = 1; col <= totalCols; col++) {
|
||||||
|
const cell = ws.getCell(row, col);
|
||||||
|
|
||||||
|
cell.alignment = {
|
||||||
|
horizontal: 'center',
|
||||||
|
vertical: 'middle',
|
||||||
|
wrapText: true
|
||||||
|
};
|
||||||
|
cell.border = {
|
||||||
|
top: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
left: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
right: { style: 'thin', color: { argb: 'FF000000' } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format number dengan 2 desimal untuk kolom value (kolom 2 dst)
|
||||||
|
if (col > 1) {
|
||||||
|
const cellValue = cell.value;
|
||||||
|
// Hanya set format number jika cell berisi angka
|
||||||
|
if (typeof cellValue === 'number') {
|
||||||
|
cell.numFmt = '0.00';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate file name
|
||||||
|
const fileName = `Report_Pivot_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}.xlsx`;
|
||||||
|
|
||||||
|
// Save file
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||||
|
saveAs(blob, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
const exportToPDF = async () => {
|
const exportToPDF = async () => {
|
||||||
if (pivotData.length === 0) {
|
if (pivotData.length === 0) {
|
||||||
alert('No data to export');
|
alert('No data to export');
|
||||||
@@ -393,7 +583,7 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
doc.setFontSize(9);
|
doc.setFontSize(9);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.text(`Plant Section : ${sectionName}`, marginLeft + col1Width + col2Width / 2, 41, { align: 'center' });
|
doc.text(`${sectionName}`, marginLeft + col1Width + col2Width / 2, 38, { align: 'center' });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hitung total kolom tag chunks
|
// Hitung total kolom tag chunks
|
||||||
@@ -534,7 +724,7 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
head: [headerRow],
|
head: [headerRow],
|
||||||
body: pdfRows,
|
body: pdfRows,
|
||||||
startY: isFirstPage ? 50 : 15,
|
startY: isFirstPage ? 43 : 15,
|
||||||
theme: 'grid',
|
theme: 'grid',
|
||||||
rowPageBreak: 'avoid',
|
rowPageBreak: 'avoid',
|
||||||
styles: {
|
styles: {
|
||||||
@@ -542,7 +732,7 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
cellPadding: 1.5,
|
cellPadding: 1.5,
|
||||||
minCellHeight: 8,
|
minCellHeight: 8,
|
||||||
lineColor: [0, 0, 0],
|
lineColor: [0, 0, 0],
|
||||||
lineWidth: 0.1,
|
lineWidth: 0.5,
|
||||||
halign: 'center',
|
halign: 'center',
|
||||||
valign: 'middle',
|
valign: 'middle',
|
||||||
overflow: 'linebreak',
|
overflow: 'linebreak',
|
||||||
@@ -554,7 +744,7 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
halign: 'center',
|
halign: 'center',
|
||||||
valign: 'middle',
|
valign: 'middle',
|
||||||
lineColor: [0, 0, 0],
|
lineColor: [0, 0, 0],
|
||||||
lineWidth: 0.3,
|
lineWidth: 0.5,
|
||||||
},
|
},
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
0: {
|
0: {
|
||||||
@@ -694,11 +884,23 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
onClick={exportToPDF}
|
onClick={exportToPDF}
|
||||||
disabled={false}
|
disabled={pivotData.length === 0}
|
||||||
|
style={{ backgroundColor: '#1890ff', borderColor: '#1890ff' }}
|
||||||
>
|
>
|
||||||
Export PDF
|
Export PDF
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={exportToExcel}
|
||||||
|
disabled={pivotData.length === 0}
|
||||||
|
style={{ backgroundColor: '#28a745', borderColor: '#28a745' }}
|
||||||
|
>
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
|
|||||||
@@ -130,9 +130,26 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fungsi untuk menentukan apakah rentang tanggal lebih dari 1 hari
|
||||||
|
const isMultipleDays = () => {
|
||||||
|
return !startDate.isSame(endDate, 'day');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format sumbu X yang otomatis menyesuaikan
|
||||||
const formatXAxis = (tickItem) => {
|
const formatXAxis = (tickItem) => {
|
||||||
const date = new Date(tickItem);
|
const date = new Date(tickItem);
|
||||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
// Jika rentang lebih dari 1 hari, tampilkan tanggal + waktu
|
||||||
|
if (isMultipleDays()) {
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
return `${day}/${month} ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika hanya 1 hari, tampilkan waktu saja
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload, label }) => {
|
const CustomTooltip = ({ active, payload, label }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user