Compare commits

...

20 Commits

Author SHA1 Message Date
3b5d41c1a2 Merge pull request 'fix(notif): fixing for notification Monthly' (#57) from lavoce into main
Reviewed-on: #57
2026-07-08 05:40:17 +00:00
e02aa92c64 fix(notif): fixing for notification Monthly 2026-07-08 12:36:29 +07:00
ab59715c1f Merge pull request 'fix(showTime): show time in datePicker' (#56) from lavoce into main
Reviewed-on: #56
2026-06-30 04:58:29 +00:00
276a75f13f fix(showTime): show time in datePicker 2026-06-30 11:57:49 +07:00
dc4c975df2 Merge pull request 'lavoce' (#55) from lavoce into main
Reviewed-on: #55
2026-06-25 06:41:12 +00:00
51e2fa46bf fix(date): auto detect start_date & end_date 2026-06-25 13:39:48 +07:00
c22000437b fix(monthly): adjustments in the frontend. 2026-06-24 16:20:16 +07:00
60927e045e Merge pull request 'feat(month): add monthy selection' (#54) from lavoce into main
Reviewed-on: #54
2026-06-22 07:38:47 +00:00
1f5678f435 feat(month): add monthy selection 2026-06-22 14:27:23 +07:00
7063865a7b Merge pull request 'fix(email): fix payload' (#53) from lavoce into main
Reviewed-on: #53
2026-06-05 09:39:11 +00:00
d7747b6f83 fix(email): fix payload 2026-06-05 16:38:43 +07:00
0fa282c4db Merge pull request 'fix(email): maxLength 64' (#52) from lavoce into main
Reviewed-on: #52
2026-06-05 08:57:04 +00:00
cabf0e4ce1 fix(email): maxLength 64 2026-06-05 15:56:01 +07:00
c79a5fc361 Merge pull request 'add: space' (#51) from lavoce into main
Reviewed-on: #51
2026-06-05 08:16:59 +00:00
00c2a583f4 add: space 2026-06-05 15:13:29 +07:00
3ae2d84378 Merge pull request 'lavoce' (#50) from lavoce into main
Reviewed-on: #50
2026-06-05 08:09:27 +00:00
bde507b85d fix(email): something miss 2026-06-05 12:59:13 +07:00
f5b81a6250 feat(email): add new feature email in contact 2026-06-05 12:57:05 +07:00
f3faf41308 Merge pull request 'Add detailed rendering for non-reminder notifications in NotificationDetailTab, including error information and device details. Introduced new properties for reminders in notification data transformation.' (#49) from lavoce into main
Reviewed-on: #49
2026-04-30 11:16:06 +00:00
2fcf79f1aa Add detailed rendering for non-reminder notifications in NotificationDetailTab, including error information and device details. Introduced new properties for reminders in notification data transformation. 2026-04-30 18:01:40 +07:00
6 changed files with 432 additions and 102 deletions

View File

@@ -91,6 +91,25 @@ const searchData = async (queryParams) => {
return response.data; return response.data;
}; };
// Reminder Monthly
const reminderMonthly = async (id, queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `notifikasi-wa/reminder-monthly/${id}`,
params: queryParams,
});
return response.data;
};
const reminderCustom= async (id, queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `notifikasi-wa/reminder-custom/${id}`,
params: queryParams,
});
return response.data;
};
export { export {
getAllNotification, getAllNotification,
getNotificationById, getNotificationById,
@@ -102,4 +121,6 @@ export {
resendChatByUser, resendChatByUser,
resendChatAllUser, resendChatAllUser,
searchData, searchData,
reminderMonthly,
reminderCustom,
}; };

View File

@@ -20,6 +20,19 @@ const NotifOk = ({ icon, title, message }) => {
}); });
}; };
const NotifSmall = ({ icon, title, message }) => {
Swal.fire({
toast: true,
position: 'top-end',
icon: icon,
title: title,
text: message,
showConfirmButton: false,
timer: 2000,
timerProgressBar: true,
});
};
const NotifConfirmDialog = ({ const NotifConfirmDialog = ({
icon, icon,
title, title,
@@ -67,4 +80,4 @@ const QuestionConfirmSubmit = ({ icon, title, message, onConfirm, onCancel }) =>
}); });
}; };
export { NotifAlert, NotifOk, NotifConfirmDialog, QuestionConfirmSubmit }; export { NotifAlert, NotifOk, NotifSmall, NotifConfirmDialog, QuestionConfirmSubmit };

View File

@@ -13,6 +13,7 @@ const DetailContact = memo(function DetailContact(props) {
id: '', id: '',
name: '', name: '',
phone: '', phone: '',
email: '',
is_active: true, is_active: true,
}; };
@@ -34,6 +35,22 @@ const DetailContact = memo(function DetailContact(props) {
[name]: value, [name]: value,
})); }));
} }
if (name === 'email') {
const email = value.trim();
setFormData((prev) => ({
...prev,
[name]: email,
}));
setErrors((prev) => ({
...prev,
email:
email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
? 'Format email tidak valid'
: '',
}));
}
}; };
@@ -51,6 +68,7 @@ const DetailContact = memo(function DetailContact(props) {
const validationRules = [ const validationRules = [
{ field: 'name', label: 'Contact Name', required: true }, { field: 'name', label: 'Contact Name', required: true },
{ field: 'phone', label: 'Phone', required: true }, { field: 'phone', label: 'Phone', required: true },
{ field: 'email', label: 'Email', required: false },
]; ];
if ( if (
@@ -84,10 +102,23 @@ const DetailContact = memo(function DetailContact(props) {
return; return;
} }
// Validasi Email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (formData.email && !emailRegex.test(formData.email.trim())) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Format email tidak valid.',
});
setConfirmLoading(false);
return;
}
try { try {
const contactData = { const contactData = {
contact_name: formData.name, contact_name: formData.name,
contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number
email: formData.email?.trim(),
is_active: formData.is_active, is_active: formData.is_active,
}; };
@@ -134,6 +165,7 @@ const DetailContact = memo(function DetailContact(props) {
setFormData({ setFormData({
name: props.selectedData.contact_name || props.selectedData.name, name: props.selectedData.contact_name || props.selectedData.name,
phone: props.selectedData.contact_phone || props.selectedData.phone, phone: props.selectedData.contact_phone || props.selectedData.phone,
email: props.selectedData.contact_email || props.selectedData.email,
is_active: is_active:
props.selectedData.is_active || props.selectedData.status === 'active', props.selectedData.is_active || props.selectedData.status === 'active',
}); });
@@ -141,6 +173,7 @@ const DetailContact = memo(function DetailContact(props) {
setFormData({ setFormData({
name: '', name: '',
phone: '', phone: '',
email: '',
is_active: true, is_active: true,
}); });
} }
@@ -249,6 +282,19 @@ const DetailContact = memo(function DetailContact(props) {
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }} style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
/> />
</div> </div>
<div style={{ marginBottom: 12 }}>
<Text strong>Email</Text>
{/* <Text style={{ color: 'red' }}> *</Text> */}
<Input
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="Enter Email Number"
readOnly={props.readOnly}
maxLength={64}
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
/>
</div>
{/* Contact Type */} {/* Contact Type */}
{/* <div style={{ marginBottom: 12 }}> {/* <div style={{ marginBottom: 12 }}>
<Text strong>Contact Type</Text> <Text strong>Contact Type</Text>

View File

@@ -10,8 +10,9 @@ import {
Select, Select,
DatePicker, DatePicker,
} from 'antd'; } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk, NotifSmall } from '../../../../components/Global/ToastNotif';
import { createDevice, updateDevice } from '../../../../api/master-device'; import { createDevice, updateDevice } from '../../../../api/master-device';
import { reminderMonthly, reminderCustom } from '../../../../api/notification';
import { getAllBrands } from '../../../../api/master-brand'; import { getAllBrands } from '../../../../api/master-brand';
import { validateRun } from '../../../../Utils/validate'; import { validateRun } from '../../../../Utils/validate';
import { toApiDateFormatter } from '../../../../components/Global/Formatter'; import { toApiDateFormatter } from '../../../../components/Global/Formatter';
@@ -35,8 +36,12 @@ const DetailDevice = (props) => {
device_location: '', device_location: '',
device_description: '', device_description: '',
ip_address: '', ip_address: '',
reminder_at: null,
listen_channel: null, listen_channel: null,
reminder_at: null,
reminder_at_monthly: null,
start_date: null,
end_date: null,
period: null,
}; };
const [formData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
@@ -82,6 +87,9 @@ const DetailDevice = (props) => {
listen_channel: formData.listen_channel, listen_channel: formData.listen_channel,
listen_channel_reminder: formData.listen_channel_reminder, listen_channel_reminder: formData.listen_channel_reminder,
reminder_at: formData.reminder_at, reminder_at: formData.reminder_at,
reminder_at_monthly: formData.period !== 'custom' ? formData.reminder_at_monthly : null,
start_date: formData.start_date,
end_date: formData.end_date,
}; };
console.log(payload); console.log(payload);
@@ -90,6 +98,13 @@ const DetailDevice = (props) => {
? await updateDevice(formData.device_id, payload) ? await updateDevice(formData.device_id, payload)
: await createDevice(payload); : await createDevice(payload);
const response2 = formData.period !== 'custom'
? await updateDevice(formData.device_id, { reminder_at_monthly: formData.reminder_at_monthly,})
: await updateDevice(formData.device_id, {
start_date: formData.start_date,
end_date: formData.end_date,
});
// Check if response is successful // Check if response is successful
if (response && (response.statusCode === 200 || response.statusCode === 201)) { if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const deviceName = response.data?.device_name || formData.device_name; const deviceName = response.data?.device_name || formData.device_name;
@@ -110,6 +125,25 @@ const DetailDevice = (props) => {
message: response?.message || 'Terjadi kesalahan saat menyimpan data.', message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
}); });
} }
// Check if response Reminder At(Monthly) or Period are successful
const tipe = formData.period !== 'custom' ? 'Reminder At(Monthly)' : 'Period';
if (response2 && (response2.statusCode === 200 || response2.statusCode === 201)) {
NotifSmall({
icon: 'success',
title: 'Berhasil',
message: `${tipe} berhasil disimpan`,
});
} else {
NotifSmall({
icon: 'error',
title: 'Gagal',
message:
response?.message ||
`Terjadi kesalahan saat menyimpan ${tipe}`,
});
}
} catch (error) { } catch (error) {
console.error('Save Device Error:', error); console.error('Save Device Error:', error);
NotifAlert({ NotifAlert({
@@ -172,6 +206,56 @@ const DetailDevice = (props) => {
} }
}; };
useEffect(() => {
if (formData.start_date && formData.end_date) {
const start = dayjs(formData.start_date);
const end = dayjs(formData.end_date);
let periodValue = 'custom';
if (end.isSame(start.add(1, 'month').subtract(1, 'day'), 'day')) {
periodValue = '1';
} else if (end.isSame(start.add(3, 'month').subtract(1, 'day'), 'day')) {
periodValue = '3';
} else if (end.isSame(start.add(6, 'month').subtract(1, 'day'), 'day')) {
periodValue = '6';
}
setFormData((prev) => ({
...prev,
period: periodValue,
}));
}
}, [formData.start_date, formData.end_date]);
const handleMonthly = (field, value) => {
setFormData((prev) => {
const data = {
...prev,
[field]: value,
};
if (value !== 'custom') {
const months = Number(value);
const startDate = dayjs().startOf('day');
const endDate = startDate
.add(months, 'month')
.subtract(1, 'day')
.endOf('day');
data.start_date = startDate.toISOString();
data.end_date = endDate.toISOString();
data.reminder_at_monthly = null;
} else {
data.start_date = null;
data.end_date = null;
}
return data;
});
};
useEffect(() => { useEffect(() => {
if (props.showModal && (props.actionMode === 'add' || props.actionMode === 'edit')) { if (props.showModal && (props.actionMode === 'add' || props.actionMode === 'edit')) {
fetchBrands(); fetchBrands();
@@ -381,7 +465,7 @@ const DetailDevice = (props) => {
</div> </div>
</div> </div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Reminder At</Text> <Text strong>Reminder At(Yearly)</Text>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<DatePicker <DatePicker
value={ value={
@@ -413,6 +497,92 @@ const DetailDevice = (props) => {
</Button> </Button>
</div> </div>
</div> </div>
<div style={{ marginBottom: 12 }}>
<Text strong>Period</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
value={formData.period}
onChange={(value) => handleMonthly('period', value)}
placeholder="Select Period"
style={{ width: '100%' }}
>
<Select.Option value="1">1 Monthly</Select.Option>
<Select.Option value="3">3 Monthly</Select.Option>
<Select.Option value="6">6 Monthly</Select.Option>
<Select.Option value="custom">Custom</Select.Option>
</Select>
</div>
{formData.period === 'custom' && (
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<DatePicker
placeholder="Start Date"
value={formData.start_date ? dayjs(formData.start_date) : null}
onChange={(date) =>
handleDateChange('start_date', date ? date.toISOString() : null)
}
disabledDate={(current) =>
formData.end_date &&
current &&
current.isAfter(dayjs(formData.end_date), 'day')
}
format="DD-MM-YYYY HH:mm"
showTime={{ format: 'HH:mm' }}
style={{ flex: 1 }}
/>
<DatePicker
placeholder="End Date"
value={formData.end_date ? dayjs(formData.end_date) : null}
onChange={(date) =>
handleDateChange('end_date', date ? date.toISOString() : null)
}
disabledDate={(current) =>
formData.start_date &&
current &&
current.isBefore(dayjs(formData.start_date), 'day')
}
format="DD-MM-YYYY HH:mm"
showTime={{ format: 'HH:mm' }}
style={{ flex: 1 }}
/>
</div>
)}
<div style={{ marginBottom: 12 }}>
<Text strong>Reminder At(Monthly)</Text>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<DatePicker
disabled={formData.period === 'custom'}
value={
formData.reminder_at_monthly
? dayjs(formData.reminder_at_monthly) // ✅ langsung parse ISO
: null
}
onChange={(date) =>
handleDateChange(
'reminder_at_monthly',
date ? date.toISOString() : null
)
}
format="DD-MM-YYYY HH:mm"
showTime={{ format: 'HH:mm' }}
style={{ flex: 1 }}
/>
<Button
disabled={formData.period === 'custom'}
danger
onClick={() =>
setFormData((prev) => ({
...prev,
reminder_at_monthly: null,
}))
}
>
Clear
</Button>
</div>
</div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Device Description</Text> <Text strong>Device Description</Text>
<TextArea <TextArea

View File

@@ -38,6 +38,24 @@ const DetailSparepart = (props) => {
const [previewTitle, setPreviewTitle] = useState(''); const [previewTitle, setPreviewTitle] = useState('');
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const optionsMonthly = [
{
key: 1,
value: 1,
text: '1 Month',
},
{
key: 3,
value: 3,
text: '3 Month',
},
{
key: 6,
value: 6,
text: '6 Month',
},
];
const optionsYearly = [ const optionsYearly = [
{ {
key: 1, key: 1,
@@ -70,6 +88,10 @@ const DetailSparepart = (props) => {
text: '6 Year', text: '6 Year',
}, },
]; ];
const [checkedListM, setCheckedListM] = useState([]);
const [checkAllM, setCheckAllM] = useState(false);
const [indeterminateM, setIndeterminateM] = useState(false);
const [checkedList, setCheckedList] = useState([]); const [checkedList, setCheckedList] = useState([]);
const [checkAll, setCheckAll] = useState(false); const [checkAll, setCheckAll] = useState(false);
const [indeterminate, setIndeterminate] = useState(false); const [indeterminate, setIndeterminate] = useState(false);
@@ -276,6 +298,7 @@ const DetailSparepart = (props) => {
payload.sparepart_number = formData.sparepart_number; payload.sparepart_number = formData.sparepart_number;
payload.sparepart_yearly = checkedList; payload.sparepart_yearly = checkedList;
payload.sparepart_monthly = checkedListM;
// console.log('Sending payload:', payload); // console.log('Sending payload:', payload);
@@ -323,6 +346,21 @@ const DetailSparepart = (props) => {
setFormData({ ...formData, [name]: value }); setFormData({ ...formData, [name]: value });
}; };
const onChangeCheckboxM = (list) => {
setCheckedListM(list);
setIndeterminateM(!!list.length && list.length < optionsMonthly.length);
setCheckAllM(list.length === optionsMonthly.length);
};
const onCheckAllChangeM = (e) => {
const checked = e.target.checked;
setCheckedListM(checked ? optionsMonthly.map((item) => item.value) : []);
setIndeterminateM(false);
setCheckAllM(checked);
};
const onChangeCheckbox = (list) => { const onChangeCheckbox = (list) => {
setCheckedList(list); setCheckedList(list);
@@ -374,10 +412,16 @@ const DetailSparepart = (props) => {
} else { } else {
setFileList([]); setFileList([]);
} }
setCheckedListM(JSON.parse(props.selectedData.sparepart_monthly));
setCheckedList(JSON.parse(props.selectedData.sparepart_yearly)); setCheckedList(JSON.parse(props.selectedData.sparepart_yearly));
} else { } else {
setFormData(defaultData); setFormData(defaultData);
setFileList([]); setFileList([]);
setIndeterminateM(false);
setCheckedListM([]);
setCheckAllM(false);
setIndeterminate(false); setIndeterminate(false);
setCheckedList([]); setCheckedList([]);
setCheckAll(false); setCheckAll(false);
@@ -639,6 +683,8 @@ const DetailSparepart = (props) => {
}, },
}} }}
> >
{/* Yearly */}
<Row style={{ marginTop: 16 }}> <Row style={{ marginTop: 16 }}>
<Col> <Col>
<Checkbox <Checkbox
@@ -669,6 +715,38 @@ const DetailSparepart = (props) => {
</Checkbox.Group> </Checkbox.Group>
</Col> </Col>
</Row> </Row>
{/* Monthly */}
<Row style={{ marginTop: 16 }}>
<Col>
<Checkbox
indeterminate={indeterminateM}
onChange={onCheckAllChangeM}
checked={checkAllM}
>
{checkAllM ? 'Unselect All Months' : 'Select All Months'}
</Checkbox>
</Col>
</Row>
<Row style={{ marginTop: 10 }}>
<Col span={24}>
<Checkbox.Group
value={checkedListM}
onChange={onChangeCheckboxM}
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
}}
>
{optionsMonthly.map((item) => (
<Checkbox key={item.key} value={item.value}>
{item.text}
</Checkbox>
))}
</Checkbox.Group>
</Col>
</Row>
</ConfigProvider> </ConfigProvider>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}> <Col span={24}>

View File

@@ -102,6 +102,8 @@ const transformNotificationData = (apiData) => {
brand_name: apiData.brand_name, brand_name: apiData.brand_name,
}, },
users: apiData.users || [], users: apiData.users || [],
is_reminder: apiData.is_reminder || false,
spareparts_reminder: apiData.spareparts_reminder || [],
}; };
}; };
@@ -362,6 +364,104 @@ const NotificationDetailTab = (props) => {
<Row gutter={[8, 8]}> <Row gutter={[8, 8]}>
{/* Kolom Kiri: Data Kompresor */} {/* Kolom Kiri: Data Kompresor */}
{notification.is_reminder == false && (
<Col xs={24} lg={8}>
<Card
size="small"
style={{ height: '100%', borderColor: '#d4380d' }}
bodyStyle={{ padding: '16px' }}
>
<Space
direction="vertical"
size="large"
style={{ width: '100%' }}
>
<Row gutter={16} align="middle">
<Col>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: '#d4380d',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#ffffff',
fontSize: '18px',
}}
>
<CloseOutlined />
</div>
</Col>
<Col>
<Text>{notification.title}</Text>
<div style={{ marginTop: '2px' }}>
<Text strong style={{ fontSize: '16px' }}>
Error Code {notification.issue}
</Text>
</div>
</Col>
</Row>
<div>
<Text strong>Plant Subsection</Text>
<div>{notification.location}</div>
<Text
strong
style={{ display: 'block', marginTop: '8px' }}
>
Date & Time
</Text>
<div>{notification.timestamp}</div>
</div>
</Space>
</Card>
</Col>
)}
{/* Kolom Tengah: Informasi Teknis */}
<Col xs={24} lg={8}>
<Card
title="Device Information"
size="small"
style={{ height: '100%' }}
>
<Space
direction="vertical"
size="middle"
style={{ width: '100%' }}
>
<div>
<Text strong>Error Channel</Text>
<div>{notification.error_chanel || 'N/A'}</div>
</div>
<div>
<Text strong>Device Code</Text>
<div>
{notification.device_info?.device_code || 'N/A'}
</div>
</div>
<div>
<Text strong>Device Name</Text>
<div>
{notification.device_info?.device_name || 'N/A'}
</div>
</div>
<div>
<Text strong>Device Location</Text>
<div>
{notification.device_info?.device_location || 'N/A'}
</div>
</div>
<div>
<Text strong>Brand</Text>
<div>
{notification.device_info?.brand_name || 'N/A'}
</div>
</div>
</Space>
</Card>
</Col>
{notification.is_reminder == true && ( {notification.is_reminder == true && (
<Col xs={24} md={8}> <Col xs={24} md={8}>
<div> <div>
@@ -515,104 +615,6 @@ const NotificationDetailTab = (props) => {
</Col> </Col>
)} )}
{notification.is_reminder == false && (
<Col xs={24} lg={8}>
<Card
size="small"
style={{ height: '100%', borderColor: '#d4380d' }}
bodyStyle={{ padding: '16px' }}
>
<Space
direction="vertical"
size="large"
style={{ width: '100%' }}
>
<Row gutter={16} align="middle">
<Col>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: '#d4380d',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#ffffff',
fontSize: '18px',
}}
>
<CloseOutlined />
</div>
</Col>
<Col>
<Text>{notification.title}</Text>
<div style={{ marginTop: '2px' }}>
<Text strong style={{ fontSize: '16px' }}>
Error Code {notification.issue}
</Text>
</div>
</Col>
</Row>
<div>
<Text strong>Plant Subsection</Text>
<div>{notification.location}</div>
<Text
strong
style={{ display: 'block', marginTop: '8px' }}
>
Date & Time
</Text>
<div>{notification.timestamp}</div>
</div>
</Space>
</Card>
</Col>
)}
{/* Kolom Tengah: Informasi Teknis */}
<Col xs={24} lg={8}>
<Card
title="Device Information"
size="small"
style={{ height: '100%' }}
>
<Space
direction="vertical"
size="middle"
style={{ width: '100%' }}
>
<div>
<Text strong>Error Channel</Text>
<div>{notification.error_chanel || 'N/A'}</div>
</div>
<div>
<Text strong>Device Code</Text>
<div>
{notification.device_info?.device_code || 'N/A'}
</div>
</div>
<div>
<Text strong>Device Name</Text>
<div>
{notification.device_info?.device_name || 'N/A'}
</div>
</div>
<div>
<Text strong>Device Location</Text>
<div>
{notification.device_info?.device_location || 'N/A'}
</div>
</div>
<div>
<Text strong>Brand</Text>
<div>
{notification.device_info?.brand_name || 'N/A'}
</div>
</div>
</Space>
</Card>
</Col>
{/* Kolom Kanan: User History */} {/* Kolom Kanan: User History */}
<Col xs={24} lg={8}> <Col xs={24} lg={8}>
<Card title="User History" size="small" style={{ height: '100%' }}> <Card title="User History" size="small" style={{ height: '100%' }}>