Compare commits

..

23 Commits

Author SHA1 Message Date
zain94rif
c0fe3aaca1 fix(icon): change size & color icon svg 2026-01-13 17:04:50 +07:00
zain94rif
e8da716e8f feat(svg): add new icon svg open mail 2026-01-13 16:31:21 +07:00
zain94rif
0ffcf4c3c0 fix(color): change design & color for icon notification 2026-01-12 15:35:41 +07:00
zain94rif
c2163cec5e Merge branch 'lavoce' of https://gitea.idetama.id/yogiedigital/cod-fe into lavoce 2026-01-08 17:25:23 +07:00
zain94rif
d5866ceae4 fix(api): search use api 2026-01-08 17:25:17 +07:00
6fdb259246 fixing validate solution optional in brand error code 2026-01-08 14:32:27 +07:00
0aad43c751 add label error code in list notification 2026-01-08 14:24:32 +07:00
d988d47e30 fixing layout mobile detail notification 2026-01-08 14:17:38 +07:00
zain94rif
e08eaaa43e fix(text): add 'error code' & move solution name 2026-01-08 13:55:01 +07:00
zain94rif
f6ca54f5b4 Merge branch 'lavoce' of https://gitea.idetama.id/yogiedigital/cod-fe into lavoce 2026-01-08 13:07:27 +07:00
zain94rif
a9b8053bd8 fix: change message 'Setiap error code harus memiliki minimal 1 solution!' disabled 2026-01-08 13:07:21 +07:00
600c101c68 fixing typo detail notification user 2026-01-08 12:17:13 +07:00
zain94rif
14a6884f43 Merge branch 'lavoce' of https://gitea.idetama.id/yogiedigital/cod-fe into lavoce 2026-01-07 17:07:46 +07:00
zain94rif
8e151ffe0b fix(comp): modified the card in notification detail 2026-01-07 17:07:42 +07:00
8f64843613 fixing redirect wa session token 2026-01-07 16:10:23 +07:00
zain94rif
fe8f6d1002 fix: move update is read's api after fetchLogHistory 2026-01-07 14:40:36 +07:00
zain94rif
5281e288a9 feat(var): add update at from create at 2026-01-07 11:00:35 +07:00
zain94rif
4ed05cc640 fix: resize add log card 2026-01-07 10:30:03 +07:00
zain94rif
14e97fead2 feat(api): add update is_read for detail 2026-01-06 16:12:58 +07:00
zain94rif
0935d7c9f5 fix(var): use notification_error_id from item, not from users 2026-01-06 09:53:15 +07:00
zain94rif
3266641f81 fix(api): fixing put to post 2026-01-05 14:48:46 +07:00
zain94rif
739c55c0bc fix(api): fixing endpoint notification 2026-01-05 14:26:12 +07:00
zain94rif
5b4485d20d feat(api): add API for resend chat user 2026-01-05 14:08:02 +07:00
6 changed files with 821 additions and 664 deletions

View File

@@ -46,6 +46,15 @@ const getNotificationLogByNotificationId = async (notificationId) => {
return response.data;
};
// update is_read status
const updateIsRead = async (notificationId) => {
const response = await SendRequest({
method: 'put',
prefix: `notification/${notificationId}`,
});
return response.data;
};
// Resend notification to specific user
const resendNotificationToUser = async (notificationId, userId) => {
const response = await SendRequest({
@@ -55,11 +64,42 @@ const resendNotificationToUser = async (notificationId, userId) => {
return response.data;
};
// Resend Chat by User
const resendChatByUser = async (notificationId, userPhone) => {
const response = await SendRequest({
method: 'post',
prefix: `notification-user/resend/${notificationId}/${userPhone}`,
});
return response.data;
};
// Resend Chat All User
const resendChatAllUser = async (notificationId) => {
const response = await SendRequest({
method: 'post',
prefix: `notification/resend/${notificationId}`,
});
return response.data;
};
// Searching
const searchData = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `notification?criteria=${queryParams}`,
});
return response.data;
};
export {
getAllNotification,
getNotificationById,
getNotificationDetail,
createNotificationLog,
getNotificationLogByNotificationId,
updateIsRead,
resendNotificationToUser,
resendChatByUser,
resendChatAllUser,
searchData,
};

View File

@@ -26,7 +26,7 @@ export default function RedirectWa() {
console.log('tes', response);
const tokenResult = JSON.stringify(response.data?.accessToken);
const tokenResult = JSON.stringify(response.data?.data?.accessToken);
sessionStorage.setItem('token_redirect', tokenResult);
response.data.auth = true;

View File

@@ -13,13 +13,18 @@ import {
Space,
ConfigProvider,
} from 'antd';
import {
EditOutlined,
DeleteOutlined
} from '@ant-design/icons';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById, createBrand, createErrorCode, getErrorCodesByBrandId, updateErrorCode, deleteErrorCode, deleteBrand } from '../../../api/master-brand';
import {
getBrandById,
createBrand,
createErrorCode,
getErrorCodesByBrandId,
updateErrorCode,
deleteErrorCode,
deleteBrand,
} from '../../../api/master-brand';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import SolutionForm from './component/SolutionForm';
@@ -42,7 +47,9 @@ const AddBrandDevice = () => {
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [loading, setLoading] = useState(false);
const tab = searchParams.get('tab');
const [currentStep, setCurrentStep] = useState(tab === 'error-codes' ? 1 : (location.state?.phase || 0));
const [currentStep, setCurrentStep] = useState(
tab === 'error-codes' ? 1 : location.state?.phase || 0
);
const [editingErrorCodeKey, setEditingErrorCodeKey] = useState(null);
const [isErrorCodeFormReadOnly, setIsErrorCodeFormReadOnly] = useState(false);
const [searchText, setSearchText] = useState('');
@@ -68,7 +75,7 @@ const AddBrandDevice = () => {
const values = solutionForm.getFieldsValue(true);
const solutions = [];
solutionFields.forEach(fieldKey => {
solutionFields.forEach((fieldKey) => {
let solution = null;
if (values.solution_items && values.solution_items[fieldKey]) {
@@ -85,9 +92,16 @@ const AddBrandDevice = () => {
if (solutionType === 'text') {
isValid = solution.text && solution.text.trim() !== '';
} 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);
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;
isValid = hasPathSolution || hasFileUpload || hasFile;
}
@@ -118,9 +132,9 @@ const AddBrandDevice = () => {
text: '',
status: true,
file: null,
fileUpload: null
}
}
fileUpload: null,
},
},
});
}, 100);
}
@@ -128,7 +142,6 @@ const AddBrandDevice = () => {
};
const setSolutionsForExistingRecord = (solutions, targetForm) => {
if (!targetForm || !solutions || solutions.length === 0) {
return;
}
@@ -153,11 +166,14 @@ const AddBrandDevice = () => {
fileObject = {
uploadPath: solution.path_solution || solution.path_document,
path_solution: solution.path_solution || solution.path_document,
name: solution.file_upload_name || (solution.path_solution || solution.path_document).split('/').pop() || 'File',
name:
solution.file_upload_name ||
(solution.path_solution || solution.path_document).split('/').pop() ||
'File',
type_solution: solution.type_solution,
isExisting: true,
size: 0,
url: solution.path_solution || solution.path_document
url: solution.path_solution || solution.path_document,
};
}
@@ -170,7 +186,7 @@ const AddBrandDevice = () => {
file: fileObject,
fileUpload: fileObject,
path_solution: solution.path_solution || solution.path_document || null,
fileName: solution.file_upload_name || null
fileName: solution.file_upload_name || null,
};
});
@@ -180,28 +196,35 @@ const AddBrandDevice = () => {
setSolutionStatuses(newSolutionStatuses);
targetForm.resetFields();
setTimeout(() => {
targetForm.setFieldsValue({
solution_items: solutionItems
solution_items: solutionItems,
});
setTimeout(() => {
Object.keys(solutionItems).forEach(key => {
Object.keys(solutionItems).forEach((key) => {
const solution = solutionItems[key];
targetForm.setFieldValue(['solution_items', key, 'name'], solution.name);
targetForm.setFieldValue(['solution_items', key, 'type'], solution.type);
targetForm.setFieldValue(['solution_items', key, 'text'], solution.text);
targetForm.setFieldValue(['solution_items', key, 'file'], solution.file);
targetForm.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload);
targetForm.setFieldValue(
['solution_items', key, 'fileUpload'],
solution.fileUpload
);
targetForm.setFieldValue(['solution_items', key, 'status'], solution.status);
targetForm.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution);
targetForm.setFieldValue(['solution_items', key, 'fileName'], solution.fileName);
targetForm.setFieldValue(
['solution_items', key, 'path_solution'],
solution.path_solution
);
targetForm.setFieldValue(
['solution_items', key, 'fileName'],
solution.fileName
);
});
const finalValues = targetForm.getFieldsValue();
}, 100);
}, 100);
@@ -209,14 +232,14 @@ const AddBrandDevice = () => {
const handleAddSolutionField = () => {
const newKey = Math.max(...solutionFields, 0) + 1;
setSolutionFields(prev => [...prev, newKey]);
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
setSolutionFields((prev) => [...prev, newKey]);
setSolutionTypes((prev) => ({ ...prev, [newKey]: 'text' }));
setSolutionStatuses((prev) => ({ ...prev, [newKey]: true }));
};
const handleRemoveSolutionField = (fieldKey) => {
if (solutionFields.length > 1) {
setSolutionFields(prev => prev.filter(key => key !== fieldKey));
setSolutionFields((prev) => prev.filter((key) => key !== fieldKey));
const newTypes = { ...solutionTypes };
const newStatuses = { ...solutionStatuses };
delete newTypes[fieldKey];
@@ -233,7 +256,7 @@ const AddBrandDevice = () => {
};
const handleSolutionTypeChange = (fieldKey, type) => {
setSolutionTypes(prev => ({ ...prev, [fieldKey]: type }));
setSolutionTypes((prev) => ({ ...prev, [fieldKey]: type }));
if (type === 'file') {
solutionForm.setFieldValue(['solution_items', fieldKey, 'text'], '');
@@ -246,7 +269,7 @@ const AddBrandDevice = () => {
};
const handleSolutionStatusChange = (fieldKey, status) => {
setSolutionStatuses(prev => ({ ...prev, [fieldKey]: status }));
setSolutionStatuses((prev) => ({ ...prev, [fieldKey]: status }));
};
const handleNextStep = async () => {
@@ -259,7 +282,7 @@ const AddBrandDevice = () => {
brand_type: brandValues.brand_type || '',
brand_manufacture: brandValues.brand_manufacture || '',
brand_model: brandValues.brand_model || '',
is_active: brandValues.is_active !== undefined ? brandValues.is_active : true
is_active: brandValues.is_active !== undefined ? brandValues.is_active : true,
};
const response = await createBrand(brandApiData);
@@ -268,7 +291,7 @@ const AddBrandDevice = () => {
const newBrandInfo = {
...brandValues,
brand_id: response.data.brand_id,
brand_code: response.data.brand_code
brand_code: response.data.brand_code,
};
setBrandInfo(newBrandInfo);
setTemporaryBrandId(response.data.brand_id);
@@ -307,8 +330,7 @@ const AddBrandDevice = () => {
if (isTemporaryBrand && temporaryBrandId) {
try {
await deleteBrand(temporaryBrandId);
} catch (error) {
}
} catch (error) {}
}
navigate('/master/brand-device');
};
@@ -360,8 +382,6 @@ const AddBrandDevice = () => {
setTrigerFilter((prev) => !prev);
};
const resetErrorCodeForm = () => {
errorCodeForm.resetFields();
errorCodeForm.setFieldsValue({
@@ -391,16 +411,16 @@ const AddBrandDevice = () => {
return;
}
if (!solutionData || solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!',
});
return;
}
// if (!solutionData || solutionData.length === 0) {
// NotifAlert({
// icon: 'warning',
// title: 'Perhatian',
// message: 'Setiap error code harus memiliki minimal 1 solution!',
// });
// return;
// }
const formattedSolutions = solutionData.map(solution => {
const formattedSolutions = solutionData.map((solution) => {
const solutionType = solution.type || 'text';
let typeSolution = solutionType === 'text' ? 'text' : 'image';
@@ -422,7 +442,11 @@ const AddBrandDevice = () => {
} else {
formattedSolution.text_solution = '';
formattedSolution.path_solution = solution.path_solution || solution.file?.uploadPath || solution.fileUpload?.uploadPath || '';
formattedSolution.path_solution =
solution.path_solution ||
solution.file?.uploadPath ||
solution.fileUpload?.uploadPath ||
'';
}
if (formattedSolution.brand_code_solution_id) {
@@ -440,7 +464,7 @@ const AddBrandDevice = () => {
path_icon: errorCodeIcon?.uploadPath || '',
is_active: errorCodeValues.status === undefined ? true : errorCodeValues.status,
solution: formattedSolutions,
spareparts: selectedSparepartIds || []
spareparts: selectedSparepartIds || [],
};
let response;
@@ -456,11 +480,13 @@ const AddBrandDevice = () => {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: editingErrorCodeKey ? 'Error code berhasil diupdate!' : 'Error code berhasil ditambahkan!',
message: editingErrorCodeKey
? 'Error code berhasil diupdate!'
: 'Error code berhasil ditambahkan!',
});
resetErrorCodeForm();
setTrigerFilter(prev => !prev);
setTrigerFilter((prev) => !prev);
} else {
NotifAlert({
icon: 'error',
@@ -479,12 +505,10 @@ const AddBrandDevice = () => {
}
};
const handleErrorCodeIconRemove = () => {
setErrorCodeIcon(null);
};
const handleFinish = async () => {
setConfirmLoading(true);
try {
@@ -506,10 +530,10 @@ const AddBrandDevice = () => {
const response = await getErrorCodesByBrandId(brandInfo.brand_id, queryParams);
if (response && response.statusCode === 200 && response.data) {
const freshErrorCodes = response.data.map(ec => ({
const freshErrorCodes = response.data.map((ec) => ({
...ec,
tempId: `existing_${ec.error_code_id}`,
status: 'existing'
status: 'existing',
}));
setApiErrorCodes(freshErrorCodes);
@@ -517,7 +541,8 @@ const AddBrandDevice = () => {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap tambahkan minimal 1 error code sebelum menyelesaikan.',
message:
'Harap tambahkan minimal 1 error code sebelum menyelesaikan.',
});
return;
}
@@ -526,7 +551,8 @@ const AddBrandDevice = () => {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Harap tambahkan minimal 1 error code sebelum menyelesaikan.',
message:
'Harap tambahkan minimal 1 error code sebelum menyelesaikan.',
});
return;
}
@@ -591,11 +617,7 @@ const AddBrandDevice = () => {
<Spin size="large" />
</div>
)}
<BrandForm
form={brandForm}
isEdit={false}
brandInfo={brandInfo}
/>
<BrandForm form={brandForm} isEdit={false} brandInfo={brandInfo} />
</div>
);
}
@@ -635,31 +657,39 @@ const AddBrandDevice = () => {
</Col>
<Col xs={24} md={16} lg={16}>
<div style={{
paddingLeft: '12px'
}}>
<div
style={{
paddingLeft: '12px',
}}
>
<Card
title={
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%'
}}>
<span style={{
fontSize: '16px',
fontWeight: '600',
color: '#262626',
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{
width: '4px',
height: '20px',
backgroundColor: '#23A55A',
borderRadius: '2px'
}}></span>
justifyContent: 'space-between',
width: '100%',
}}
>
<span
style={{
fontSize: '16px',
fontWeight: '600',
color: '#262626',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span
style={{
width: '4px',
height: '20px',
backgroundColor: '#23A55A',
borderRadius: '2px',
}}
></span>
Error Code Form
</span>
<Button
@@ -675,43 +705,51 @@ const AddBrandDevice = () => {
padding: '0 24px',
fontWeight: '500',
boxShadow: '0 2px 4px rgba(35, 165, 90, 0.2)',
transition: 'all 0.3s ease'
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
e.target.style.boxShadow = '0 4px 8px rgba(35, 165, 90, 0.3)';
e.target.style.boxShadow =
'0 4px 8px rgba(35, 165, 90, 0.3)';
}}
onMouseLeave={(e) => {
e.target.style.boxShadow = '0 2px 4px rgba(35, 165, 90, 0.2)';
e.target.style.boxShadow =
'0 2px 4px rgba(35, 165, 90, 0.2)';
}}
>
{editingErrorCodeKey ? 'Update Error Code' : 'Save Error Code'}
{editingErrorCodeKey
? 'Update Error Code'
: 'Save Error Code'}
</Button>
</div>
}
style={{
width: '100%',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
borderRadius: '12px'
borderRadius: '12px',
}}
styles={{
body: { padding: '16px 24px 12px 24px' },
header: {
padding: '16px 24px',
borderBottom: '1px solid #f0f0f0',
backgroundColor: '#fafafa'
}
backgroundColor: '#fafafa',
},
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
marginBottom: '0',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}
>
<div
style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
marginBottom: '0',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}
>
<ErrorCodeForm
errorCodeForm={errorCodeForm}
isErrorCodeFormReadOnly={isErrorCodeFormReadOnly}
@@ -724,29 +762,42 @@ const AddBrandDevice = () => {
<Row gutter={[20, 0]} style={{ marginTop: '0' }}>
<Col xs={24} md={12} lg={12}>
<div style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5'
}}>
<div style={{
width: '3px',
height: '16px',
backgroundColor: '#1890ff',
borderRadius: '2px'
}}></div>
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
<div
style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5',
}}
>
<div
style={{
width: '3px',
height: '16px',
backgroundColor: '#1890ff',
borderRadius: '2px',
}}
></div>
<h4
style={{
margin: 0,
color: '#262626',
fontSize: '14px',
fontWeight: '600',
}}
>
Solution
</h4>
</div>
@@ -756,14 +807,23 @@ const AddBrandDevice = () => {
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
onAddSolutionField={handleAddSolutionField}
onRemoveSolutionField={handleRemoveSolutionField}
onRemoveSolutionField={
handleRemoveSolutionField
}
onSolutionTypeChange={handleSolutionTypeChange}
onSolutionStatusChange={handleSolutionStatusChange}
onSolutionFileUpload={(fileData) => {
}}
onSolutionStatusChange={
handleSolutionStatusChange
}
onSolutionFileUpload={(fileData) => {}}
onFileView={(fileData) => {
if (fileData && (fileData.url || fileData.uploadPath)) {
window.open(fileData.url || fileData.uploadPath, '_blank');
if (
fileData &&
(fileData.url || fileData.uploadPath)
) {
window.open(
fileData.url || fileData.uploadPath,
'_blank'
);
}
}}
isReadOnly={false}
@@ -772,40 +832,55 @@ const AddBrandDevice = () => {
</div>
</Col>
<Col xs={24} md={12} lg={12}>
<div style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5'
}}>
<div style={{
width: '3px',
height: '16px',
backgroundColor: '#faad14',
borderRadius: '2px'
}}></div>
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
<div
style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5',
}}
>
<div
style={{
width: '3px',
height: '16px',
backgroundColor: '#faad14',
borderRadius: '2px',
}}
></div>
<h4
style={{
margin: 0,
color: '#262626',
fontSize: '14px',
fontWeight: '600',
}}
>
Sparepart Selection
</h4>
</div>
<div style={{
maxHeight: '45vh',
overflow: 'auto',
border: '1px solid #e8e8e8',
borderRadius: '8px',
padding: '12px',
backgroundColor: '#fafafa'
}}>
<div
style={{
maxHeight: '45vh',
overflow: 'auto',
border: '1px solid #e8e8e8',
borderRadius: '8px',
padding: '12px',
backgroundColor: '#fafafa',
}}
>
<SparepartSelect
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={setSelectedSparepartIds}
@@ -816,15 +891,16 @@ const AddBrandDevice = () => {
</Col>
</Row>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px 0 0 0',
borderTop: '1px solid #f0f0f0',
marginTop: '12px'
}}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px 0 0 0',
borderTop: '1px solid #f0f0f0',
marginTop: '12px',
}}
>
{editingErrorCodeKey && (
<Button
size="large"
@@ -837,7 +913,7 @@ const AddBrandDevice = () => {
height: '40px',
padding: '0 24px',
fontWeight: '500',
transition: 'all 0.3s ease'
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
e.target.style.borderColor = '#ff4d4f';
@@ -871,7 +947,7 @@ const AddBrandDevice = () => {
setBreadcrumbItems([
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span>
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span>,
},
{
title: (
@@ -895,12 +971,11 @@ const AddBrandDevice = () => {
if (location.state?.fromFileViewer && location.state.phase !== undefined) {
setCurrentStep(location.state.phase);
}
}, [setBreadcrumbItems, navigate, searchParams, location.state]);
useEffect(() => {
if (brandInfo.brand_id && currentStep === 1) {
setTrigerFilter(prev => !prev);
setTrigerFilter((prev) => !prev);
}
}, [brandInfo.brand_id, currentStep]);
@@ -913,8 +988,7 @@ const AddBrandDevice = () => {
const errorCodes = response.data || [];
setApiErrorCodes(errorCodes);
}
} catch (error) {
}
} catch (error) {}
}
};
fetchErrorCodes();
@@ -925,8 +999,7 @@ const AddBrandDevice = () => {
if (isTemporaryBrand && temporaryBrandId && currentStep === 0) {
try {
await deleteBrand(temporaryBrandId);
} catch (error) {
}
} catch (error) {}
}
};
@@ -937,7 +1010,6 @@ const AddBrandDevice = () => {
};
}, [isTemporaryBrand, temporaryBrandId, currentStep]);
return (
<ConfigProvider
theme={{
@@ -988,14 +1060,9 @@ const AddBrandDevice = () => {
<Divider />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
<Button onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleCancel}>Cancel</Button>
{currentStep === 1 && (
<Button
onClick={handlePrevStep}
style={{ marginLeft: 8 }}
>
<Button onClick={handlePrevStep} style={{ marginLeft: 8 }}>
Kembali ke Brand Info
</Button>
)}

View File

@@ -479,14 +479,14 @@ const EditBrandDevice = () => {
return;
}
if (!solutionData || solutionData.length === 0) {
NotifAlert({
icon: 'warning',
title: 'Perhatian',
message: 'Setiap error code harus memiliki minimal 1 solution!',
});
return;
}
// if (!solutionData || solutionData.length === 0) {
// NotifAlert({
// icon: 'warning',
// title: 'Perhatian',
// message: 'Setiap error code harus memiliki minimal 1 solution!',
// });
// return;
// }
const formattedSolutions = solutionData.map(solution => {
const solutionType = solution.type || 'text';

View File

@@ -42,16 +42,30 @@ import {
getAllNotification,
getNotificationLogByNotificationId,
getNotificationDetail,
resendChatByUser,
resendChatAllUser,
searchData,
} from '../../../api/notification';
const { Text, Paragraph, Link: AntdLink } = Typography;
const OpenMail = ({ size = 22, color = 'black' }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 640"
width={size}
height={size}
fill={color}
>
<path d="M576 480C576 515.3 547.5 544 512.1 544L128 544C92.6 544 64 515.3 64 480L64 228C64.1 212.5 71.8 198 84.5 189.2L270 61.3C300.1 40.6 339.8 40.6 369.9 61.3L555.5 189.2C568.3 198 575.9 212.5 576 228L576 480zM128 496L512.1 496C520.9 496 528 488.9 528 480L528 288.3L373.2 405.7C341.8 429.6 298.3 429.6 266.8 405.7L112 288.3L112 480C112 488.9 119.2 496 128 496zM527.6 228.4L342.7 100.8C329 91.4 311 91.4 297.3 100.8L112.4 228.4L295.8 367.5C310.1 378.3 329.9 378.3 344.2 367.5L527.6 228.4z" />
</svg>
);
// Transform API response to component format
const transformNotificationData = (apiData) => {
return apiData.map((item, index) => ({
id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
title: item.error_code_name || 'Unknown Error',
color: item.error_code_color || 'Black',
issue: item.error_code || item.error_code_name || 'Unknown Error',
description: `${item.error_code} - ${item.error_code_name || ''}`,
timestamp: item.created_at
@@ -65,6 +79,7 @@ const transformNotificationData = (apiData) => {
: 'N/A',
location: item.plant_sub_section_name || item.device_location || 'Location not specified',
details: item.device_name || '-',
errId: item.notification_error_id || 0,
link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part
subsection: item.plant_sub_section_name || 'N/A',
isRead: item.is_read,
@@ -178,13 +193,13 @@ const ListNotification = memo(function ListNotification(props) {
const getIconAndColor = (type) => {
switch (type) {
case 'critical':
return { IconComponent: CloseCircleFilled, color: '#ff4d4f', bgColor: '#fff1f0' };
return { IconComponent: MailOutlined, color: '#faad14', bgColor: '#fff1f0' };
case 'warning':
return { IconComponent: WarningFilled, color: '#faad14', bgColor: '#fffbe6' };
return { IconComponent: MailOutlined, color: '#1890ff', bgColor: '#fffbe6' };
case 'resolved':
return { IconComponent: CheckCircleFilled, color: '#52c41a', bgColor: '#f6ffed' };
return { IconComponent: MailOutlined, color: '#52c41a', bgColor: '#f6ffed' };
default:
return { IconComponent: InfoCircleFilled, color: '#1890ff', bgColor: '#e6f7ff' };
return { IconComponent: MailOutlined, color: '#1890ff', bgColor: '#e6f7ff' };
}
};
@@ -195,9 +210,9 @@ const ListNotification = memo(function ListNotification(props) {
content: `Are you sure you want to resend the notification for "${notification.title}"?`,
okText: 'Resend',
cancelText: 'Cancel',
onOk() {
async onOk() {
console.log('Resending notification:', notification.id);
await resendChatAllUser(notification.errId);
message.success(
`Notification for "${notification.title}" has been resent successfully.`
);
@@ -216,13 +231,49 @@ const ListNotification = memo(function ListNotification(props) {
);
};
const fetchSearch = async (data) => {
setLoading(true);
try {
const response = await searchData(data);
if (response && response.data) {
const transformedData = transformNotificationData(response.data);
setNotifications(transformedData);
// 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);
} finally {
setLoading(false);
}
};
const handleSearch = () => {
setSearchTerm(searchValue);
fetchSearch(searchValue);
};
const handleSearchClear = () => {
setSearchValue('');
setSearchTerm('');
fetchSearch('');
};
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
@@ -285,14 +336,16 @@ const ListNotification = memo(function ListNotification(props) {
name: user.contact_name,
phone: user.contact_phone,
status: user.is_send ? 'Delivered' : 'Pending',
timestamp: user.created_at
? new Date(user.created_at).toLocaleString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}) + ' WIB'
timestamp: user.updated_at
? new Date(user.updated_at)
.toLocaleString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
.replace('.', ':') + ' WIB'
: 'N/A',
}));
setUserHistoryData(transformedUsers);
@@ -363,7 +416,11 @@ const ListNotification = memo(function ListNotification(props) {
flexShrink: 0,
}}
>
<IconComponent style={{ fontSize: '22px' }} />
{notification.type === 'resolved' ? (
<OpenMail size={28.5} color={color} />
) : (
<IconComponent style={{ fontSize: '22px' }} />
)}
</div>
<div style={{ flex: 1 }}>
<Row align="top">
@@ -378,8 +435,12 @@ const ListNotification = memo(function ListNotification(props) {
<div>
<Text strong>{notification.title}</Text>
<div style={{ marginTop: '4px' }}>
<Text style={{ color }}>
{notification.issue}
<Text
style={{
color: notification.color,
}}
>
Error Code {notification.issue}
</Text>
</div>
</div>
@@ -605,10 +666,8 @@ const ListNotification = memo(function ListNotification(props) {
type="primary"
ghost
icon={<SendOutlined />}
onClick={() => {
message.info(
'Resend feature is not available yet. This feature is still under development.'
);
onClick={async () => {
await resendChatByUser(user.id, user.phone);
}}
>
Resend
@@ -694,8 +753,8 @@ const ListNotification = memo(function ListNotification(props) {
{/* Kolom Kanan: Card */}
<Col flex="auto">
<Card size="small" style={{ borderColor: '#91d5ff' }}>
<Row gutter={[16, 8]} align="middle">
<Col xs={24} md={12}>
<Row gutter={[16, 8]} align="top">
<Col xs={24} md={10}>
<Space direction="vertical" size={4}>
<Space>
<ClockCircleOutlined />
@@ -706,13 +765,14 @@ const ListNotification = memo(function ListNotification(props) {
Added at {log.timestamp}
</Text>
</Space>
<div>
<Text strong>{log.addedBy.name}</Text>
</div>
<div>
<Text strong>
Added by: {log.addedBy.name}
</Text>
<span
style={{
marginLeft: '8px',
border: '1px solid #52c41a',
color: '#52c41a',
padding: '2px 6px',
@@ -725,7 +785,8 @@ const ListNotification = memo(function ListNotification(props) {
</div>
</Space>
</Col>
<Col xs={24} md={12}>
<Col xs={24} md={14}>
<Text strong>Description:</Text>
<Paragraph
style={{
color: '#595959',

View File

@@ -14,6 +14,8 @@ import {
message,
Avatar,
Tag,
Badge,
Divider,
} from 'antd';
import {
ArrowLeftOutlined,
@@ -33,12 +35,16 @@ import {
CheckCircleOutlined,
SyncOutlined,
SendOutlined,
MobileOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import {
getNotificationDetail,
createNotificationLog,
getNotificationLogByNotificationId,
updateIsRead,
resendNotificationToUser,
resendChatByUser,
} from '../../api/notification';
const { Content } = Layout;
@@ -107,8 +113,19 @@ const getUsersFromNotification = (notification) => {
id: user.notification_error_user_id.toString(),
name: user.contact_name,
phone: user.contact_phone,
status: user.is_send ? 'sent' : 'pending',
status: user.is_send ? 'Delivered' : 'Pending',
loading: user.loading || false,
timestamp: user.updated_at
? new Date(user.updated_at)
.toLocaleString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
.replace('.', ':') + ' WIB'
: 'N/A',
}));
};
@@ -241,6 +258,9 @@ const NotificationDetailTab = (props) => {
// Fetch log history
fetchLogHistory(notificationId);
// Fetch using the actual API
const resUpdate = await updateIsRead(notificationId);
} else {
throw new Error('Notification not found');
}
@@ -374,7 +394,7 @@ const NotificationDetailTab = (props) => {
<Text>{notification.title}</Text>
<div style={{ marginTop: '2px' }}>
<Text strong style={{ fontSize: '16px' }}>
{notification.issue}
Error Code {notification.issue}
</Text>
</div>
</Col>
@@ -462,101 +482,56 @@ const NotificationDetailTab = (props) => {
<Row align="middle" justify="space-between">
<Col>
<Space align="center">
<Avatar
size="large"
icon={<UserOutlined />}
<Text strong>{user.name}</Text>
<Text>|</Text>
<Text>
<MobileOutlined /> {user.phone}
</Text>
<Text>|</Text>
<Badge
status={
user.status === 'Delivered'
? 'success'
: 'default'
}
text={user.status}
/>
<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>
<Divider style={{ margin: '8px 0' }} />
<Space align="center">
{user.status === 'Delivered' ? (
<CheckCircleFilled
style={{ color: '#52c41a' }}
/>
) : (
<ClockCircleOutlined
style={{ color: '#faad14' }}
/>
)}
<Text type="secondary">
{user.status === 'Delivered'
? 'Success Delivered at'
: 'Status '}{' '}
{user.timestamp}
</Text>
</Space>
</Col>
<Col>
<Space align="center" size="large">
{getStatusTag(user.status)}
<Col>
<Button
type="primary"
ghost
icon={<SendOutlined />}
size="small"
loading={user.loading}
onClick={async (e) => {
e.stopPropagation();
const userId = parseInt(user.id);
try {
// Update user status to show loading
const updatedUsers = notification.users.map(u =>
u.notification_error_user_id === userId
? { ...u, loading: true }
: u
);
setNotification({
...notification,
users: updatedUsers
});
// Call the resend API
const response = await resendNotificationToUser(
notification.notification_error_id,
userId
);
if (response && response.statusCode === 200) {
message.success(`Notification resent to ${user.name}`);
// Update user status
const updatedUsersAfterSuccess = notification.users.map(u =>
u.notification_error_user_id === userId
? {
...u,
is_send: true,
status: 'sent',
loading: false
}
: { ...u, loading: false }
);
setNotification({
...notification,
users: updatedUsersAfterSuccess
});
} else {
throw new Error(response?.message || 'Failed to resend notification');
}
} catch (error) {
console.error('Error resending notification:', error);
message.error(error.message || 'Failed to resend notification');
// Reset loading state
const resetUsers = notification.users.map(u =>
u.notification_error_user_id === userId
? { ...u, loading: false }
: u
);
setNotification({
...notification,
users: resetUsers
});
}
onClick={async () => {
await resendChatByUser(
user.id,
user.phone
);
}}
>
Resend
</Button>
</Space>
</Col>
</Col>
</Row>
</Card>
@@ -567,393 +542,407 @@ const NotificationDetailTab = (props) => {
</Col>
</Row>
<Row gutter={[8, 8]} style={{ marginBottom: 'px' }}>
<Row gutter={[8, 8]}>
<Col xs={24} md={8}>
<Card
hoverable
bodyStyle={{ padding: '12px', textAlign: 'center' }}
>
<Space>
<BookOutlined
style={{ fontSize: '16px', color: '#1890ff' }}
/>
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
Handling Guideline
</Text>
</Space>
</Card>
</Col>
<Col xs={24} md={8}>
<Card
hoverable
bodyStyle={{ padding: '12px', textAlign: 'center' }}
>
<Space>
<ToolOutlined
style={{ fontSize: '16px', color: '#1890ff' }}
/>
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
Spare Part
</Text>
</Space>
</Card>
</Col>
<Col xs={24} md={8}>
<Card bodyStyle={{ padding: '12px', textAlign: 'center' }}>
<Space>
<HistoryOutlined
style={{ fontSize: '16px', color: '#1890ff' }}
/>
<Text strong style={{ fontSize: '16px', color: '#262626' }}>
Log Activity
</Text>
</Space>
</Card>
</Col>
</Row>
<Row gutter={[8, 8]} style={{ marginTop: '-12px' }}>
<Col xs={24} md={8}>
<Card
size="small"
title="Guideline Documents"
style={{ height: '100%' }}
>
<Space
direction="vertical"
size="small"
style={{ width: '100%' }}
<div>
<Card
hoverable
bodyStyle={{ padding: '12px'}}
>
{notification.error_code?.solution &&
notification.error_code.solution.length > 0 ? (
<>
{notification.error_code.solution
.filter((sol) => sol.is_active) // Hanya tampilkan solusi yang aktif
.map((sol, index) => (
<div
key={
sol.brand_code_solution_id || index
}
>
{sol.path_document ? (
<Card
size="small"
bodyStyle={{
padding: '8px 12px',
marginBottom: '4px',
}}
hoverable
extra={
<Text
type="secondary"
style={{
fontSize: '10px',
}}
>
PDF
</Text>
}
>
<div
style={{
display: 'flex',
justifyContent:
'space-between',
alignItems: 'center',
<Space>
<BookOutlined
style={{ fontSize: '16px', color: '#1890ff' }}
/>
<Text
strong
style={{ fontSize: '16px', color: '#262626' }}
>
Handling Guideline
</Text>
</Space>
<Space
direction="vertical"
size="small"
style={{ width: '100%' }}
>
{notification.error_code?.solution &&
notification.error_code.solution.length > 0 ? (
<>
{notification.error_code.solution
.filter((sol) => sol.is_active) // Hanya tampilkan solusi yang aktif
.map((sol, index) => (
<div
key={
sol.brand_code_solution_id ||
index
}
>
{sol.path_document ? (
<Card
size="small"
bodyStyle={{
padding: '8px 12px',
marginBottom: '4px',
}}
>
<div>
hoverable
extra={
<Text
type="secondary"
style={{
fontSize:
'12px',
color: '#262626',
'10px',
}}
>
<FilePdfOutlined
style={{
marginRight:
'8px',
}}
/>{' '}
{sol.file_upload_name ||
'Solution Document.pdf'}
PDF
</Text>
<Link
href={sol.path_document.replace(
'/detail-notification/pdf/',
'/notification-detail/pdf/'
)}
target="_blank"
style={{
fontSize:
'12px',
display:
'block',
}}
>
lihat disini
</Link>
</div>
</div>
</Card>
) : null}
{sol.type_solution === 'text' &&
sol.text_solution ? (
<Card
size="small"
bodyStyle={{
padding: '8px 12px',
marginBottom: '4px',
}}
extra={
<Text
type="secondary"
style={{
fontSize: '10px',
}}
>
{sol.type_solution.toUpperCase()}
</Text>
}
>
<div>
<Text strong>
{sol.solution_name}:
</Text>
}
>
<div
style={{
marginTop: '4px',
display: 'flex',
justifyContent:
'space-between',
alignItems:
'center',
}}
>
{sol.text_solution}
<div>
<Text
style={{
fontSize:
'12px',
color: '#262626',
}}
>
<FilePdfOutlined
style={{
marginRight:
'8px',
}}
/>{' '}
{sol.file_upload_name ||
'Solution Document.pdf'}
</Text>
<Link
href={sol.path_document.replace(
'/detail-notification/pdf/',
'/notification-detail/pdf/'
)}
target="_blank"
style={{
fontSize:
'12px',
display:
'block',
}}
>
lihat disini
</Link>
</div>
</div>
</div>
</Card>
) : null}
</div>
))}
</>
) : (
<div
style={{
textAlign: 'center',
padding: '20px',
color: '#8c8c8c',
}}
>
Tidak ada dokumen solusi tersedia
</div>
)}
</Space>
</Card>
</Card>
) : null}
{sol.type_solution === 'text' &&
sol.text_solution ? (
<Card
size="small"
title={
<Text strong>
{sol.solution_name}:
</Text>
}
bodyStyle={{
padding: '8px 12px',
marginBottom: '4px',
}}
extra={
<Text
type="secondary"
style={{
fontSize:
'10px',
}}
>
{sol.type_solution.toUpperCase()}
</Text>
}
>
<div>
<div
style={{
marginTop:
'4px',
}}
>
{sol.text_solution}
</div>
</div>
</Card>
) : null}
</div>
))}
</>
) : (
<div
style={{
textAlign: 'center',
padding: '20px',
color: '#8c8c8c',
}}
>
Tidak ada dokumen solusi tersedia
</div>
)}
</Space>
</Card>
</div>
</Col>
<Col xs={24} md={8}>
<Card
size="small"
title="Required Spare Parts"
style={{ height: '100%' }}
>
<Space
direction="vertical"
size="small"
style={{ width: '100%' }}
<div>
<Card
hoverable
bodyStyle={{ padding: '12px'}}
>
{notification.spareparts &&
notification.spareparts.length > 0 ? (
notification.spareparts.map((sparepart, index) => (
<Card
size="small"
key={index}
bodyStyle={{ padding: '12px' }}
hoverable
>
<Row gutter={16} align="top">
<Col
span={7}
style={{ textAlign: 'center' }}
>
<div
style={{
width: '100%',
height: '60px',
backgroundColor: '#f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '4px',
marginBottom: '8px',
}}
<Space>
<ToolOutlined
style={{ fontSize: '16px', color: '#1890ff' }}
/>
<Text
strong
style={{ fontSize: '16px', color: '#262626' }}
>
Spare Part
</Text>
</Space>
<Space
direction="vertical"
size="small"
style={{ width: '100%' }}
>
{notification.spareparts &&
notification.spareparts.length > 0 ? (
notification.spareparts.map((sparepart, index) => (
<Card
size="small"
key={index}
bodyStyle={{ padding: '12px' }}
hoverable
>
<Row gutter={16} align="top">
<Col
span={7}
style={{ textAlign: 'center' }}
>
<ToolOutlined
style={{
fontSize: '24px',
color: '#bfbfbf',
}}
/>
</div>
<Text
style={{
fontSize: '12px',
color:
sparepart.sparepart_stok ===
'Available' ||
sparepart.sparepart_stok ===
'available'
? '#52c41a'
: '#ff4d4f',
fontWeight: 500,
}}
>
{sparepart.sparepart_stok}
</Text>
</Col>
<Col span={17}>
<Space
direction="vertical"
size={4}
style={{ width: '100%' }}
>
<Text strong>
{sparepart.sparepart_name}
</Text>
<Paragraph
style={{
fontSize: '12px',
margin: 0,
color: '#595959',
}}
>
{sparepart.sparepart_description ||
'Deskripsi tidak tersedia'}
</Paragraph>
<div
style={{
border: '1px solid #d9d9d9',
width: '100%',
height: '60px',
backgroundColor: '#f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '4px',
padding: '4px 8px',
fontSize: '11px',
color: '#8c8c8c',
marginTop: '8px',
marginBottom: '8px',
}}
>
Kode: {sparepart.sparepart_code}{' '}
| Qty: {sparepart.sparepart_qty}{' '}
| Unit:{' '}
{sparepart.sparepart_unit}
<ToolOutlined
style={{
fontSize: '24px',
color: '#bfbfbf',
}}
/>
</div>
</Space>
</Col>
</Row>
</Card>
))
) : (
<div
style={{
textAlign: 'center',
padding: '20px',
color: '#8c8c8c',
}}
>
Tidak ada spare parts terkait
</div>
)}
</Space>
</Card>
</Col>
<Col span={8}>
<Card size="small" style={{ height: '100%' }}>
<Space
direction="vertical"
size="small"
style={{ width: '100%' }}
>
<Card
size="small"
bodyStyle={{
padding: '8px 12px',
backgroundColor: isAddingLog ? '#fafafa' : '#fff',
}}
>
<Space
direction="vertical"
style={{ width: '100%' }}
size="small"
>
{isAddingLog && (
<>
<Text strong style={{ fontSize: '12px' }}>
Add New Log / Update Progress
</Text>
<Input.TextArea
rows={2}
placeholder="Tuliskan update penanganan di sini..."
value={newLogDescription}
onChange={(e) =>
setNewLogDescription(e.target.value)
}
disabled={submitLoading}
/>
</>
)}
<Button
type={isAddingLog ? 'primary' : 'dashed'}
size="small"
block
icon={
submitLoading ? (
<LoadingOutlined />
) : (
!isAddingLog && <PlusOutlined />
)
}
onClick={
isAddingLog
? handleSubmitLog
: () => setIsAddingLog(true)
}
loading={submitLoading}
disabled={submitLoading}
<Text
style={{
fontSize: '12px',
color:
sparepart.sparepart_stok ===
'Available' ||
sparepart.sparepart_stok ===
'available'
? '#52c41a'
: '#ff4d4f',
fontWeight: 500,
}}
>
{sparepart.sparepart_stok}
</Text>
</Col>
<Col span={17}>
<Space
direction="vertical"
size={4}
style={{ width: '100%' }}
>
<Text strong>
{sparepart.sparepart_name}
</Text>
<Paragraph
style={{
fontSize: '12px',
margin: 0,
color: '#595959',
}}
>
{sparepart.sparepart_description ||
'Deskripsi tidak tersedia'}
</Paragraph>
<div
style={{
border: '1px solid #d9d9d9',
borderRadius: '4px',
padding: '4px 8px',
fontSize: '11px',
color: '#8c8c8c',
marginTop: '8px',
}}
>
Kode:{' '}
{sparepart.sparepart_code} |
Qty:{' '}
{sparepart.sparepart_qty} |
Unit:{' '}
{sparepart.sparepart_unit}
</div>
</Space>
</Col>
</Row>
</Card>
))
) : (
<div
style={{
textAlign: 'center',
padding: '20px',
color: '#8c8c8c',
}}
>
{isAddingLog ? 'Submit Log' : 'Add Log'}
</Button>
{isAddingLog && (
<Button
size="small"
block
onClick={() => {
setIsAddingLog(false);
setNewLogDescription('');
}}
disabled={submitLoading}
>
Cancel
</Button>
)}
</Space>
</Card>
{logHistoryData.map((log) => (
Tidak ada spare parts terkait
</div>
)}
</Space>
</Card>
</div>
</Col>
<Col xs={24} md={8}>
<div>
<Card bodyStyle={{ padding: '12px'}}>
<Space>
<HistoryOutlined
style={{ fontSize: '16px', color: '#1890ff' }}
/>
<Text
strong
style={{ fontSize: '16px', color: '#262626' }}
>
Log Activity
</Text>
</Space>
<Space
direction="vertical"
size="small"
style={{ width: '100%' }}
>
<Card
key={log.id}
size="small"
bodyStyle={{
padding: '8px 12px',
backgroundColor: isAddingLog
? '#fafafa'
: '#fff',
}}
>
<Paragraph
style={{ fontSize: '12px', margin: 0 }}
ellipsis={{ rows: 2 }}
<Space
direction="vertical"
style={{ width: '100%' }}
size="small"
>
<Text strong>{log.addedBy.name}:</Text>{' '}
{log.description}
</Paragraph>
<Text type="secondary" style={{ fontSize: '11px' }}>
{log.timestamp}
</Text>
{isAddingLog && (
<>
<Text
strong
style={{ fontSize: '12px' }}
>
Add New Log / Update Progress
</Text>
<Input.TextArea
rows={2}
placeholder="Tuliskan update penanganan di sini..."
value={newLogDescription}
onChange={(e) =>
setNewLogDescription(
e.target.value
)
}
disabled={submitLoading}
/>
</>
)}
<Button
type={isAddingLog ? 'primary' : 'dashed'}
size="small"
block
icon={
submitLoading ? (
<LoadingOutlined />
) : (
!isAddingLog && <PlusOutlined />
)
}
onClick={
isAddingLog
? handleSubmitLog
: () => setIsAddingLog(true)
}
loading={submitLoading}
disabled={submitLoading}
>
{isAddingLog ? 'Submit Log' : 'Add Log'}
</Button>
{isAddingLog && (
<Button
size="small"
block
onClick={() => {
setIsAddingLog(false);
setNewLogDescription('');
}}
disabled={submitLoading}
>
Cancel
</Button>
)}
</Space>
</Card>
))}
</Space>
</Card>
{logHistoryData.map((log) => (
<Card
key={log.id}
size="small"
bodyStyle={{
padding: '8px 12px',
}}
>
<Paragraph
style={{ fontSize: '12px', margin: 0 }}
// ellipsis={{ rows: 2 }}
>
<Text strong>{log.addedBy.name}:</Text>{' '}
{log.description}
</Paragraph>
<Text
type="secondary"
style={{ fontSize: '11px' }}
>
{log.timestamp}
</Text>
</Card>
))}
</Space>
</Card>
</div>
</Col>
</Row>
</Space>