lavoce #30
@@ -46,10 +46,20 @@ const getNotificationLogByNotificationId = async (notificationId) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resend notification to specific user
|
||||||
|
const resendNotificationToUser = async (notificationId, userId) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: `notification/${notificationId}/resend/${userId}`,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAllNotification,
|
getAllNotification,
|
||||||
getNotificationById,
|
getNotificationById,
|
||||||
getNotificationDetail,
|
getNotificationDetail,
|
||||||
createNotificationLog,
|
createNotificationLog,
|
||||||
getNotificationLogByNotificationId
|
getNotificationLogByNotificationId,
|
||||||
|
resendNotificationToUser,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,18 +30,18 @@ instance.interceptors.response.use(
|
|||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Refresh token dipanggil...');
|
// console.log('🔄 Refresh token dipanggil...');
|
||||||
const refreshRes = await refreshApi.post('/auth/refresh-token');
|
const refreshRes = await refreshApi.post('/auth/refresh-token');
|
||||||
|
|
||||||
const newAccessToken = refreshRes.data.data.accessToken;
|
const newAccessToken = refreshRes.data.data.accessToken;
|
||||||
localStorage.setItem('token', newAccessToken);
|
localStorage.setItem('token', newAccessToken);
|
||||||
console.log('✅ Token refreshed successfully');
|
// console.log('✅ Token refreshed successfully');
|
||||||
|
|
||||||
// update token di header
|
// update token di header
|
||||||
instance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
instance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
||||||
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
||||||
|
|
||||||
console.log('🔁 Retrying original request...');
|
// console.log('🔁 Retrying original request...');
|
||||||
return instance(originalRequest);
|
return instance(originalRequest);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -85,20 +85,20 @@ async function ApiRequest({ method = 'GET', params = {}, prefix = '/', token = t
|
|||||||
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}`;
|
||||||
console.log('🔐 Sending request with token:', cleanToken.substring(0, 20) + '...');
|
// console.log('🔐 Sending request with token:', cleanToken.substring(0, 20) + '...');
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ No token found in localStorage');
|
console.warn('⚠️ No token found in localStorage');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📤 API Request:', { method, url: prefix, hasToken: !!rawToken });
|
// console.log('📤 API Request:', { method, url: prefix, hasToken: !!rawToken });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await instance(request);
|
const response = await instance(request);
|
||||||
console.log('✅ API Response:', {
|
// console.log('✅ API Response:', {
|
||||||
url: prefix,
|
// url: prefix,
|
||||||
status: response.status,
|
// status: response.status,
|
||||||
statusCode: response.data?.statusCode,
|
// statusCode: response.data?.statusCode,
|
||||||
});
|
// });
|
||||||
return { ...response, error: false };
|
return { ...response, error: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = error?.response?.status || 500;
|
const status = error?.response?.status || 500;
|
||||||
@@ -143,17 +143,10 @@ async function cekError(status, message = '') {
|
|||||||
const SendRequest = async (queryParams) => {
|
const SendRequest = async (queryParams) => {
|
||||||
try {
|
try {
|
||||||
const response = await ApiRequest(queryParams);
|
const response = await ApiRequest(queryParams);
|
||||||
console.log('📦 SendRequest response:', {
|
|
||||||
hasError: response.error,
|
|
||||||
status: response.status,
|
|
||||||
statusCode: response.data?.statusCode,
|
|
||||||
data: response.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If ApiRequest returned error flag, return error structure
|
// If ApiRequest returned error flag, return error structure
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
const errorMsg = response.data?.message || response.statusText || 'Request failed';
|
const errorMsg = response.data?.message || response.statusText || 'Request failed';
|
||||||
console.error('❌ SendRequest error response:', errorMsg);
|
|
||||||
|
|
||||||
// Return consistent error structure instead of empty array
|
// Return consistent error structure instead of empty array
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -267,9 +267,6 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend doesn't support is_active filter or order parameter
|
|
||||||
// Contact hanya supports: criteria, name, code, limit, page
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
Object.entries(searchParams).forEach(([key, value]) => {
|
Object.entries(searchParams).forEach(([key, value]) => {
|
||||||
if (value !== '' && value !== null && value !== undefined) {
|
if (value !== '' && value !== null && value !== undefined) {
|
||||||
@@ -309,11 +306,10 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
// Listen for saved contact data
|
// Listen for saved contact data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.lastSavedContact) {
|
if (props.lastSavedContact) {
|
||||||
fetchContacts(); // Refetch all contacts when data is saved
|
fetchContacts();
|
||||||
}
|
}
|
||||||
}, [props.lastSavedContact]);
|
}, [props.lastSavedContact]);
|
||||||
|
|
||||||
// Get contacts (already filtered by backend)
|
|
||||||
const getFilteredContacts = () => {
|
const getFilteredContacts = () => {
|
||||||
return filteredContacts;
|
return filteredContacts;
|
||||||
};
|
};
|
||||||
@@ -326,7 +322,7 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
const showAddModal = () => {
|
const showAddModal = () => {
|
||||||
props.setSelectedData(null);
|
props.setSelectedData(null);
|
||||||
props.setActionMode('add');
|
props.setActionMode('add');
|
||||||
// Pass the current active tab to determine contact type
|
|
||||||
props.setContactType?.(activeTab);
|
props.setContactType?.(activeTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const DetailPlantSubSection = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📝 Input change: ${name} = ${value}`);
|
// console.log(`📝 Input change: ${name} = ${value}`);
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
@@ -74,16 +74,20 @@ const DetailPlantSubSection = (props) => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('💾 Current formData before save:', formData);
|
// console.log('💾 Current formData before save:', formData);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
plant_sub_section_name: formData.plant_sub_section_name,
|
plant_sub_section_name: formData.plant_sub_section_name,
|
||||||
plant_sub_section_description: (formData.plant_sub_section_description && formData.plant_sub_section_description.trim() !== '') ? formData.plant_sub_section_description : ' ',
|
plant_sub_section_description:
|
||||||
|
formData.plant_sub_section_description &&
|
||||||
|
formData.plant_sub_section_description.trim() !== ''
|
||||||
|
? formData.plant_sub_section_description
|
||||||
|
: ' ',
|
||||||
table_name_value: formData.table_name_value, // Fix field name
|
table_name_value: formData.table_name_value, // Fix field name
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📤 Payload to be sent:', payload);
|
// console.log('📤 Payload to be sent:', payload);
|
||||||
|
|
||||||
const response =
|
const response =
|
||||||
props.actionMode === 'edit'
|
props.actionMode === 'edit'
|
||||||
@@ -126,17 +130,17 @@ const DetailPlantSubSection = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔄 Modal state changed:', {
|
// console.log('🔄 Modal state changed:', {
|
||||||
showModal: props.showModal,
|
// showModal: props.showModal,
|
||||||
actionMode: props.actionMode,
|
// actionMode: props.actionMode,
|
||||||
selectedData: props.selectedData,
|
// selectedData: props.selectedData,
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
console.log('📋 Setting form data from selectedData:', props.selectedData);
|
// console.log('📋 Setting form data from selectedData:', props.selectedData);
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
} else {
|
} else {
|
||||||
console.log('📋 Resetting to default data');
|
// console.log('📋 Resetting to default data');
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
}
|
}
|
||||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ const DetailShift = (props) => {
|
|||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Payload yang dikirim:', payload);
|
// console.log('Payload yang dikirim:', payload);
|
||||||
console.log('Type start_time:', typeof payload.start_time, payload.start_time);
|
// console.log('Type start_time:', typeof payload.start_time, payload.start_time);
|
||||||
console.log('Type end_time:', typeof payload.end_time, payload.end_time);
|
// console.log('Type end_time:', typeof payload.end_time, payload.end_time);
|
||||||
|
|
||||||
const response =
|
const response =
|
||||||
props.actionMode === 'edit'
|
props.actionMode === 'edit'
|
||||||
|
|||||||
@@ -95,11 +95,11 @@ const DetailSparepart = (props) => {
|
|||||||
const newFile = fileList.length > 0 ? fileList[0] : null;
|
const newFile = fileList.length > 0 ? fileList[0] : null;
|
||||||
|
|
||||||
if (newFile && newFile.originFileObj) {
|
if (newFile && newFile.originFileObj) {
|
||||||
console.log('Uploading file:', newFile.originFileObj);
|
// console.log('Uploading file:', newFile.originFileObj);
|
||||||
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
||||||
|
|
||||||
// Log untuk debugging
|
// Log untuk debugging
|
||||||
console.log('Upload response:', uploadResponse);
|
// console.log('Upload response:', uploadResponse);
|
||||||
|
|
||||||
// Cek berbagai kemungkinan struktur respons dari API
|
// Cek berbagai kemungkinan struktur respons dari API
|
||||||
let uploadedUrl = null;
|
let uploadedUrl = null;
|
||||||
@@ -169,7 +169,7 @@ const DetailSparepart = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedUrl) {
|
if (uploadedUrl) {
|
||||||
console.log('Successfully extracted image URL:', uploadedUrl);
|
// console.log('Successfully extracted image URL:', uploadedUrl);
|
||||||
imageUrl = uploadedUrl;
|
imageUrl = uploadedUrl;
|
||||||
} else {
|
} else {
|
||||||
console.error('Upload response structure:', uploadResponse);
|
console.error('Upload response structure:', uploadResponse);
|
||||||
@@ -209,7 +209,10 @@ const DetailSparepart = (props) => {
|
|||||||
sparepart_name: formData.sparepart_name, // Wajib
|
sparepart_name: formData.sparepart_name, // Wajib
|
||||||
};
|
};
|
||||||
|
|
||||||
payload.sparepart_description = (formData.sparepart_description && formData.sparepart_description.trim() !== '') ? formData.sparepart_description : ' ';
|
payload.sparepart_description =
|
||||||
|
formData.sparepart_description && formData.sparepart_description.trim() !== ''
|
||||||
|
? formData.sparepart_description
|
||||||
|
: ' ';
|
||||||
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
|
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
|
||||||
payload.sparepart_model = formData.sparepart_model;
|
payload.sparepart_model = formData.sparepart_model;
|
||||||
}
|
}
|
||||||
@@ -233,13 +236,13 @@ const DetailSparepart = (props) => {
|
|||||||
payload.sparepart_foto = imageUrl;
|
payload.sparepart_foto = imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Sending payload:', payload);
|
// console.log('Sending payload:', payload);
|
||||||
|
|
||||||
const response = formData.sparepart_id
|
const response = formData.sparepart_id
|
||||||
? await updateSparepart(formData.sparepart_id, payload)
|
? await updateSparepart(formData.sparepart_id, payload)
|
||||||
: await createSparepart(payload);
|
: await createSparepart(payload);
|
||||||
|
|
||||||
console.log('API response:', response);
|
// console.log('API response:', response);
|
||||||
|
|
||||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const ListUnit = memo(function ListUnit(props) {
|
|||||||
const handleDelete = async (param) => {
|
const handleDelete = async (param) => {
|
||||||
try {
|
try {
|
||||||
const response = await deleteUnit(param.unit_id);
|
const response = await deleteUnit(param.unit_id);
|
||||||
console.log('deleteUnit response:', response);
|
// console.log('deleteUnit response:', response);
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ 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, getNotificationLogByNotificationId } from '../../../api/notification';
|
import {
|
||||||
|
getAllNotification,
|
||||||
|
getNotificationLogByNotificationId,
|
||||||
|
getNotificationDetail,
|
||||||
|
} from '../../../api/notification';
|
||||||
|
|
||||||
const { Text, Paragraph, Link: AntdLink } = Typography;
|
const { Text, Paragraph, Link: AntdLink } = Typography;
|
||||||
|
|
||||||
@@ -77,31 +81,6 @@ const transformNotificationData = (apiData) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dummy data untuk user history
|
|
||||||
const userHistoryData = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'John Doe',
|
|
||||||
phone: '081234567890',
|
|
||||||
status: 'Delivered',
|
|
||||||
timestamp: '04-11-2025 11:40 WIB',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Jane Smith',
|
|
||||||
phone: '087654321098',
|
|
||||||
status: 'Delivered',
|
|
||||||
timestamp: '04-11-2025 11:41 WIB',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Peter Jones',
|
|
||||||
phone: '082345678901',
|
|
||||||
status: 'Delivered',
|
|
||||||
timestamp: '04-11-2025 11:42 WIB',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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');
|
||||||
@@ -113,6 +92,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
const [selectedNotification, setSelectedNotification] = useState(null);
|
const [selectedNotification, setSelectedNotification] = useState(null);
|
||||||
const [logHistoryData, setLogHistoryData] = useState([]);
|
const [logHistoryData, setLogHistoryData] = useState([]);
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
const [logLoading, setLogLoading] = useState(false);
|
||||||
|
const [userHistoryData, setUserHistoryData] = useState([]);
|
||||||
|
const [userLoading, setUserLoading] = useState(false);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
current_limit: 10,
|
current_limit: 10,
|
||||||
@@ -290,6 +271,42 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch user history from API
|
||||||
|
const fetchUserHistory = async (notificationId) => {
|
||||||
|
try {
|
||||||
|
setUserLoading(true);
|
||||||
|
|
||||||
|
const response = await getNotificationDetail(notificationId);
|
||||||
|
|
||||||
|
if (response && response.data && response.data.users) {
|
||||||
|
// Transform API data to component format
|
||||||
|
const transformedUsers = response.data.users.map((user) => ({
|
||||||
|
id: user.notification_error_user_id.toString(),
|
||||||
|
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'
|
||||||
|
: 'N/A',
|
||||||
|
}));
|
||||||
|
setUserHistoryData(transformedUsers);
|
||||||
|
} else {
|
||||||
|
setUserHistoryData([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user history:', err);
|
||||||
|
setUserHistoryData([]); // Set empty array on error
|
||||||
|
} finally {
|
||||||
|
setUserLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tabButtonStyle = (isActive) => ({
|
const tabButtonStyle = (isActive) => ({
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -467,8 +484,18 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
border: '1px solid #1890ff',
|
border: '1px solid #1890ff',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setSelectedNotification(notification);
|
||||||
|
|
||||||
|
// Extract notification ID from the notification object
|
||||||
|
const notificationId =
|
||||||
|
notification.id.split('-')[1];
|
||||||
|
|
||||||
|
// Fetch user history for the selected notification
|
||||||
|
await fetchUserHistory(notificationId);
|
||||||
|
|
||||||
setModalContent('user');
|
setModalContent('user');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -535,37 +562,69 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
|
|
||||||
const renderUserHistory = () => (
|
const renderUserHistory = () => (
|
||||||
<>
|
<>
|
||||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
{userLoading ? (
|
||||||
{userHistoryData.map((user) => (
|
<div style={{ textAlign: 'center', padding: '24px' }}>
|
||||||
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
<Spin size="large" />
|
||||||
<Row align="middle" justify="space-between">
|
</div>
|
||||||
<Col>
|
) : (
|
||||||
<Space align="center">
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
<Text strong>{user.name}</Text>
|
{userHistoryData.map((user) => (
|
||||||
<Text>|</Text>
|
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
||||||
<Text>
|
<Row align="middle" justify="space-between">
|
||||||
<MobileOutlined /> {user.phone}
|
<Col>
|
||||||
</Text>
|
<Space align="center">
|
||||||
<Text>|</Text>
|
<Text strong>{user.name}</Text>
|
||||||
<Badge status="success" text={user.status} />
|
<Text>|</Text>
|
||||||
</Space>
|
<Text>
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
<MobileOutlined /> {user.phone}
|
||||||
<Space align="center">
|
</Text>
|
||||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
<Text>|</Text>
|
||||||
<Text type="secondary">
|
<Badge
|
||||||
Success Delivered at {user.timestamp}
|
status={
|
||||||
</Text>
|
user.status === 'Delivered' ? 'success' : 'default'
|
||||||
</Space>
|
}
|
||||||
</Col>
|
text={user.status}
|
||||||
<Col>
|
/>
|
||||||
<Button type="primary" ghost icon={<SendOutlined />}>
|
</Space>
|
||||||
Resend
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
</Button>
|
<Space align="center">
|
||||||
</Col>
|
{user.status === 'Delivered' ? (
|
||||||
</Row>
|
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||||
</Card>
|
) : (
|
||||||
))}
|
<ClockCircleOutlined style={{ color: '#faad14' }} />
|
||||||
</Space>
|
)}
|
||||||
|
<Text type="secondary">
|
||||||
|
{user.status === 'Delivered'
|
||||||
|
? 'Success Delivered at'
|
||||||
|
: 'Status '}{' '}
|
||||||
|
{user.timestamp}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
message.info(
|
||||||
|
'Resend feature is not available yet. This feature is still under development.'
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{userHistoryData.length === 0 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
||||||
|
No user history available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -580,97 +639,108 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
Tidak ada log history
|
Tidak ada log history
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '0 16px', position: 'relative' }}>
|
<div
|
||||||
{/* Garis vertikal yang menyambung */}
|
style={{
|
||||||
<div
|
height: '400px',
|
||||||
style={{
|
overflowY: 'auto',
|
||||||
position: 'absolute',
|
padding: '0 16px',
|
||||||
top: '7px',
|
position: 'relative',
|
||||||
left: '23px',
|
border: '1px solid #f0f0f0',
|
||||||
bottom: '7px',
|
borderRadius: '4px'
|
||||||
width: '2px',
|
}}
|
||||||
backgroundColor: '#91d5ff',
|
>
|
||||||
zIndex: 0,
|
<div style={{ position: 'relative' }}>
|
||||||
}}
|
{/* Garis vertikal yang menyambung */}
|
||||||
></div>
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '7px',
|
||||||
|
left: '23px',
|
||||||
|
bottom: '7px',
|
||||||
|
width: '2px',
|
||||||
|
backgroundColor: '#91d5ff',
|
||||||
|
zIndex: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
{logHistoryData.map((log, index) => (
|
{logHistoryData.map((log, index) => (
|
||||||
<Row
|
<Row
|
||||||
key={log.id}
|
key={log.id}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
style={{ marginBottom: '16px', position: 'relative', zIndex: 1 }}
|
style={{ marginBottom: '16px', position: 'relative', zIndex: 1 }}
|
||||||
>
|
|
||||||
{/* Kolom Kiri: Branch/Timeline */}
|
|
||||||
<Col
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: '16px',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
{/* Kolom Kiri: Branch/Timeline */}
|
||||||
|
<Col
|
||||||
style={{
|
style={{
|
||||||
width: '14px',
|
position: 'relative',
|
||||||
height: '14px',
|
display: 'flex',
|
||||||
backgroundColor: '#fff',
|
flexDirection: 'column',
|
||||||
border: '3px solid #1890ff',
|
alignItems: 'center',
|
||||||
borderRadius: '50%',
|
marginRight: '16px',
|
||||||
zIndex: 1,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
></div>
|
>
|
||||||
</Col>
|
<div
|
||||||
|
style={{
|
||||||
|
width: '14px',
|
||||||
|
height: '14px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '3px solid #1890ff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
zIndex: 1,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
{/* Kolom Kanan: Card */}
|
{/* Kolom Kanan: Card */}
|
||||||
<Col flex="auto">
|
<Col flex="auto">
|
||||||
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
||||||
<Row gutter={[16, 8]} align="middle">
|
<Row gutter={[16, 8]} align="middle">
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Space>
|
<Space>
|
||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined />
|
||||||
<Text
|
<Text
|
||||||
type="secondary"
|
type="secondary"
|
||||||
style={{ fontSize: '12px' }}
|
style={{ fontSize: '12px' }}
|
||||||
>
|
>
|
||||||
Added at {log.timestamp}
|
Added at {log.timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<div>
|
||||||
|
<Text strong>Added by: {log.addedBy.name}</Text>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
border: '1px solid #52c41a',
|
||||||
|
color: '#52c41a',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MobileOutlined /> {log.addedBy.phone}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
<div>
|
</Col>
|
||||||
<Text strong>Added by: {log.addedBy.name}</Text>
|
<Col xs={24} md={12}>
|
||||||
<span
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
marginLeft: '8px',
|
color: '#595959',
|
||||||
border: '1px solid #52c41a',
|
margin: 0,
|
||||||
color: '#52c41a',
|
fontSize: '13px',
|
||||||
padding: '2px 6px',
|
}}
|
||||||
borderRadius: '4px',
|
>
|
||||||
fontSize: '12px',
|
{log.description}
|
||||||
}}
|
</Paragraph>
|
||||||
>
|
</Col>
|
||||||
<MobileOutlined /> {log.addedBy.phone}
|
</Row>
|
||||||
</span>
|
</Card>
|
||||||
</div>
|
</Col>
|
||||||
</Space>
|
</Row>
|
||||||
</Col>
|
))}
|
||||||
<Col xs={24} md={12}>
|
</div>
|
||||||
<Paragraph
|
|
||||||
style={{
|
|
||||||
color: '#595959',
|
|
||||||
margin: 0,
|
|
||||||
fontSize: '13px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{log.description}
|
|
||||||
</Paragraph>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
getNotificationDetail,
|
getNotificationDetail,
|
||||||
createNotificationLog,
|
createNotificationLog,
|
||||||
getNotificationLogByNotificationId,
|
getNotificationLogByNotificationId,
|
||||||
|
resendNotificationToUser,
|
||||||
} from '../../api/notification';
|
} from '../../api/notification';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
@@ -94,38 +95,21 @@ const transformNotificationData = (apiData) => {
|
|||||||
device_location: apiData.device_location,
|
device_location: apiData.device_location,
|
||||||
brand_name: apiData.brand_name,
|
brand_name: apiData.brand_name,
|
||||||
},
|
},
|
||||||
|
users: apiData.users || [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dummy data baru untuk user history
|
// Function to get actual users from notification data
|
||||||
const getDummyUsers = (notification) => {
|
const getUsersFromNotification = (notification) => {
|
||||||
if (!notification) return [];
|
if (!notification || !notification.users) return [];
|
||||||
return [
|
|
||||||
{
|
return notification.users.map((user) => ({
|
||||||
id: '1',
|
id: user.notification_error_user_id.toString(),
|
||||||
name: 'John Doe',
|
name: user.contact_name,
|
||||||
phone: '081234567890',
|
phone: user.contact_phone,
|
||||||
status: 'delivered',
|
status: user.is_send ? 'sent' : 'pending',
|
||||||
},
|
loading: user.loading || false,
|
||||||
{
|
}));
|
||||||
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) => {
|
const getStatusTag = (status) => {
|
||||||
@@ -469,7 +453,7 @@ const NotificationDetailTab = (props) => {
|
|||||||
size={2}
|
size={2}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
{getDummyUsers(notification).map((user) => (
|
{getUsersFromNotification(notification).map((user) => (
|
||||||
<Card
|
<Card
|
||||||
key={user.id}
|
key={user.id}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -510,11 +494,64 @@ const NotificationDetailTab = (props) => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => {
|
loading={user.loading}
|
||||||
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log(
|
const userId = parseInt(user.id);
|
||||||
`Resend to ${user.name}`
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Resend
|
Resend
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const ChangePasswordModal = (props) => {
|
|||||||
try {
|
try {
|
||||||
const response = await changePassword(props.selectedUser.user_id, formData.newPassword);
|
const response = await changePassword(props.selectedUser.user_id, formData.newPassword);
|
||||||
|
|
||||||
console.log('Change Password Response:', response);
|
// console.log('Change Password Response:', response);
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
if (response && response.statusCode === 200) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
|
|||||||
@@ -220,35 +220,27 @@ const DetailUser = (props) => {
|
|||||||
|
|
||||||
// For update mode: only send email if it has changed
|
// For update mode: only send email if it has changed
|
||||||
if (FormData.user_id) {
|
if (FormData.user_id) {
|
||||||
// Only include email if it has changed from original
|
|
||||||
if (FormData.user_email !== originalEmail) {
|
if (FormData.user_email !== originalEmail) {
|
||||||
payload.user_email = FormData.user_email;
|
payload.user_email = FormData.user_email;
|
||||||
}
|
}
|
||||||
// Add is_active for update mode
|
|
||||||
payload.is_active = FormData.is_active;
|
payload.is_active = FormData.is_active;
|
||||||
} else {
|
} else {
|
||||||
// For create mode: always send email
|
|
||||||
payload.user_email = FormData.user_email;
|
payload.user_email = FormData.user_email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add role_id if it exists (backend requires number >= 1, no null)
|
|
||||||
if (FormData.role_id) {
|
if (FormData.role_id) {
|
||||||
payload.role_id = FormData.role_id;
|
payload.role_id = FormData.role_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add password and name for new user (create mode)
|
// Add password and name for new user (create mode)
|
||||||
if (!FormData.user_id) {
|
if (!FormData.user_id) {
|
||||||
payload.user_name = FormData.user_name; // Username only for create
|
payload.user_name = FormData.user_name;
|
||||||
payload.user_password = FormData.password; // Backend expects 'user_password'
|
payload.user_password = FormData.password;
|
||||||
// Don't send confirmPassword, is_sa for create
|
|
||||||
}
|
}
|
||||||
// For update mode:
|
|
||||||
// - Don't send 'user_name' (username is immutable)
|
|
||||||
// - is_active is now sent for update mode
|
|
||||||
// - Only send email if it has changed
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Payload being sent:', payload);
|
// console.log('Payload being sent:', payload);
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (!FormData.user_id) {
|
if (!FormData.user_id) {
|
||||||
@@ -257,11 +249,10 @@ const DetailUser = (props) => {
|
|||||||
response = await updateUser(FormData.user_id, payload);
|
response = await updateUser(FormData.user_id, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Save User Response:', response);
|
// console.log('Save User Response:', response);
|
||||||
|
|
||||||
// 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)) {
|
||||||
// If in edit mode and newPassword is provided, change password
|
|
||||||
if (FormData.user_id && FormData.newPassword) {
|
if (FormData.user_id && FormData.newPassword) {
|
||||||
try {
|
try {
|
||||||
const passwordResponse = await changePassword(
|
const passwordResponse = await changePassword(
|
||||||
@@ -385,9 +376,9 @@ const DetailUser = (props) => {
|
|||||||
search: '',
|
search: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Fetching roles with params:', queryParams.toString());
|
// console.log('Fetching roles with params:', queryParams.toString());
|
||||||
const response = await getAllRole(queryParams);
|
const response = await getAllRole(queryParams);
|
||||||
console.log('Fetched roles response:', response);
|
// console.log('Fetched roles response:', response);
|
||||||
|
|
||||||
// Handle different response structures
|
// Handle different response structures
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
@@ -408,7 +399,7 @@ const DetailUser = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRoleList(roles);
|
setRoleList(roles);
|
||||||
console.log('Setting role list:', roles);
|
// console.log('Setting role list:', roles);
|
||||||
} else {
|
} else {
|
||||||
// Add mock data as fallback
|
// Add mock data as fallback
|
||||||
console.warn('No response data, using mock data');
|
console.warn('No response data, using mock data');
|
||||||
@@ -418,7 +409,7 @@ const DetailUser = (props) => {
|
|||||||
{ role_id: 3, role_name: 'User', role_level: 3 },
|
{ role_id: 3, role_name: 'User', role_level: 3 },
|
||||||
];
|
];
|
||||||
setRoleList(mockRoles);
|
setRoleList(mockRoles);
|
||||||
console.log('Setting mock role list:', mockRoles);
|
// console.log('Setting mock role list:', mockRoles);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching roles:', error);
|
console.error('Error fetching roles:', error);
|
||||||
@@ -429,7 +420,7 @@ const DetailUser = (props) => {
|
|||||||
{ role_id: 3, role_name: 'User', role_level: 3 },
|
{ role_id: 3, role_name: 'User', role_level: 3 },
|
||||||
];
|
];
|
||||||
setRoleList(mockRoles);
|
setRoleList(mockRoles);
|
||||||
console.log('Setting mock role list due to error:', mockRoles);
|
// console.log('Setting mock role list due to error:', mockRoles);
|
||||||
|
|
||||||
// Only show error notification if we don't have fallback data
|
// Only show error notification if we don't have fallback data
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
@@ -1146,9 +1137,7 @@ const DetailUser = (props) => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{errors.role_id && (
|
{errors.role_id && (
|
||||||
<Text style={{ color: 'red', fontSize: '12px' }}>
|
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.role_id}</Text>
|
||||||
{errors.role_id}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user