add menu contact
This commit is contained in:
@@ -33,6 +33,7 @@ import IndexReport from './pages/report/report/IndexReport';
|
|||||||
import IndexNotification from './pages/notification/IndexNotification';
|
import IndexNotification from './pages/notification/IndexNotification';
|
||||||
import IndexRole from './pages/role/IndexRole';
|
import IndexRole from './pages/role/IndexRole';
|
||||||
import IndexUser from './pages/user/IndexUser';
|
import IndexUser from './pages/user/IndexUser';
|
||||||
|
import IndexContact from './pages/contact/IndexContact';
|
||||||
|
|
||||||
import SvgTest from './pages/home/SvgTest';
|
import SvgTest from './pages/home/SvgTest';
|
||||||
import SvgOverviewCompressor from './pages/home/SvgOverviewCompressor';
|
import SvgOverviewCompressor from './pages/home/SvgOverviewCompressor';
|
||||||
@@ -112,6 +113,10 @@ const App = () => {
|
|||||||
<Route index element={<IndexUser />} />
|
<Route index element={<IndexUser />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/contact" element={<ProtectedRoute />}>
|
||||||
|
<Route index element={<IndexContact />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="/jadwal-shift" element={<ProtectedRoute />}>
|
<Route path="/jadwal-shift" element={<ProtectedRoute />}>
|
||||||
<Route index element={<IndexJadwalShift />} />
|
<Route index element={<IndexJadwalShift />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
GroupOutlined,
|
GroupOutlined,
|
||||||
SlidersOutlined,
|
SlidersOutlined,
|
||||||
SnippetsOutlined,
|
SnippetsOutlined,
|
||||||
|
ContactsOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -182,6 +183,15 @@ const allItems = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'contact',
|
||||||
|
icon: <ContactsOutlined style={{ fontSize: '19px' }} />,
|
||||||
|
label: (
|
||||||
|
<Link to="/contact" className="fontMenus">
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'notification',
|
key: 'notification',
|
||||||
icon: <BellOutlined style={{ fontSize: '19px' }} />,
|
icon: <BellOutlined style={{ fontSize: '19px' }} />,
|
||||||
@@ -236,6 +246,7 @@ const LayoutMenu = () => {
|
|||||||
if (pathname === '/role') return 'role';
|
if (pathname === '/role') return 'role';
|
||||||
if (pathname === '/notification') return 'notification';
|
if (pathname === '/notification') return 'notification';
|
||||||
if (pathname === '/jadwal-shift') return 'jadwal-shift';
|
if (pathname === '/jadwal-shift') return 'jadwal-shift';
|
||||||
|
if (pathname === '/contact') return 'contact';
|
||||||
|
|
||||||
// Handle master routes
|
// Handle master routes
|
||||||
if (pathname.startsWith('/master/')) {
|
if (pathname.startsWith('/master/')) {
|
||||||
|
|||||||
69
src/pages/contact/IndexContact.jsx
Normal file
69
src/pages/contact/IndexContact.jsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ListContact from './component/ListContact';
|
||||||
|
import DetailContact from './component/DetailContact';
|
||||||
|
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const IndexContact = memo(function IndexContact() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { setBreadcrumbItems } = useBreadcrumb();
|
||||||
|
|
||||||
|
const [actionMode, setActionMode] = useState('list');
|
||||||
|
const [selectedData, setSelectedData] = useState(null);
|
||||||
|
const [readOnly, setReadOnly] = useState(false);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
const setMode = (param) => {
|
||||||
|
setShowModal(param !== 'list');
|
||||||
|
setReadOnly(param === 'preview');
|
||||||
|
setActionMode(param);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContactSaved = (contactData, actionMode) => {
|
||||||
|
setLastSavedContact({ contactData, actionMode });
|
||||||
|
|
||||||
|
// Clear after processing
|
||||||
|
setTimeout(() => setLastSavedContact(null), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [lastSavedContact, setLastSavedContact] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
setBreadcrumbItems([
|
||||||
|
{ title: <Text strong style={{ fontSize: '14px' }}>• Contact</Text> },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, [navigate, setBreadcrumbItems]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ListContact
|
||||||
|
actionMode={actionMode}
|
||||||
|
setActionMode={setMode}
|
||||||
|
selectedData={selectedData}
|
||||||
|
setSelectedData={setSelectedData}
|
||||||
|
readOnly={readOnly}
|
||||||
|
lastSavedContact={lastSavedContact}
|
||||||
|
/>
|
||||||
|
<DetailContact
|
||||||
|
setActionMode={setMode}
|
||||||
|
selectedData={selectedData}
|
||||||
|
setSelectedData={setSelectedData}
|
||||||
|
readOnly={readOnly}
|
||||||
|
showModal={showModal}
|
||||||
|
actionMode={actionMode}
|
||||||
|
onContactSaved={handleContactSaved}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IndexContact;
|
||||||
236
src/pages/contact/component/DetailContact.jsx
Normal file
236
src/pages/contact/component/DetailContact.jsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import React, { memo, useEffect, useState } from 'react';
|
||||||
|
import { Modal, Input, Button, Switch, ConfigProvider, Typography, Divider } from 'antd';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
|
import { validateRun } from '../../../Utils/validate';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const DetailContact = memo(function DetailContact(props) {
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState(defaultData);
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
let name, value;
|
||||||
|
|
||||||
|
if (e && e.target) {
|
||||||
|
name = e.target.name;
|
||||||
|
value = e.target.value;
|
||||||
|
} else if (e && e.type === 'change') {
|
||||||
|
name = e.name || e.target?.name;
|
||||||
|
value = e.value !== undefined ? e.value : e.checked;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi untuk field phone - hanya angka yang diperbolehkan
|
||||||
|
if (name === 'phone') {
|
||||||
|
value = value.replace(/[^0-9+\-\s()]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusToggle = (checked) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
is_active: checked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
|
||||||
|
// Custom validation untuk phone
|
||||||
|
if (formData.phone && !/^[\d\s\+\-\(\)]+$/.test(formData.phone)) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
message: 'Nomor telepon hanya boleh mengandung angka, spasi, +, -, dan ()',
|
||||||
|
});
|
||||||
|
setConfirmLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const validationRules = [
|
||||||
|
{ field: 'name', label: 'Contact Name', required: true },
|
||||||
|
{ field: 'phone', label: 'Phone', required: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
validateRun(formData, validationRules, (errorMessages) => {
|
||||||
|
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
|
||||||
|
setConfirmLoading(false);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contactData = {
|
||||||
|
id: props.selectedData?.id || null,
|
||||||
|
name: formData.name,
|
||||||
|
phone: formData.phone,
|
||||||
|
is_active: formData.is_active,
|
||||||
|
status: formData.is_active ? 'active' : 'inactive',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Saving contact data:', contactData);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: `Data Contact berhasil ${
|
||||||
|
props.actionMode === 'add' ? 'ditambahkan' : 'diperbarui'
|
||||||
|
}.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
props.onContactSaved?.(contactData, props.actionMode);
|
||||||
|
handleCancel();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save failed:', error);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Terjadi kesalahan saat menyimpan data.',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
props.setActionMode('list');
|
||||||
|
props.setSelectedData(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.showModal) {
|
||||||
|
if (props.actionMode === 'edit' && props.selectedData) {
|
||||||
|
setFormData({
|
||||||
|
name: props.selectedData.name,
|
||||||
|
phone: props.selectedData.phone,
|
||||||
|
is_active: props.selectedData.status === 'active',
|
||||||
|
});
|
||||||
|
} else if (props.actionMode === 'add') {
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
is_active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.showModal, props.actionMode, props.selectedData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={`${
|
||||||
|
props.actionMode === 'add'
|
||||||
|
? 'Tambah'
|
||||||
|
: props.actionMode === 'edit'
|
||||||
|
? 'Edit'
|
||||||
|
: 'Detail'
|
||||||
|
} Kontak`}
|
||||||
|
open={props.showModal}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<React.Fragment key="modal-footer">
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!props.readOnly && (
|
||||||
|
<Button loading={confirmLoading} onClick={handleSave}>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ConfigProvider>
|
||||||
|
</React.Fragment>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '8px 0' }}>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Status</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
|
||||||
|
<div style={{ marginRight: '8px' }}>
|
||||||
|
<Switch
|
||||||
|
disabled={props.readOnly}
|
||||||
|
style={{
|
||||||
|
backgroundColor: formData.is_active ? '#23A55A' : '#bfbfbf',
|
||||||
|
}}
|
||||||
|
checked={formData.is_active}
|
||||||
|
onChange={handleStatusToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Name</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Name"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Phone</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Input
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Phone Number"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
maxLength={15}
|
||||||
|
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DetailContact;
|
||||||
448
src/pages/contact/component/ListContact.jsx
Normal file
448
src/pages/contact/component/ListContact.jsx
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
|
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag, Select } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const initialMockOperators = [
|
||||||
|
{ id: 1, name: 'Shof Watun Niswah', phone: '+62 821 9049 8383', status: 'active' },
|
||||||
|
{ id: 2, name: 'Ahmad Susanto', phone: '+62 812 3456 7890', status: 'active' },
|
||||||
|
{ id: 3, name: 'Budi Santoso', phone: '+62 813 2345 6789', status: 'active' },
|
||||||
|
{ id: 4, name: 'Rina Wijaya', phone: '+62 814 3456 7891', status: 'active' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialMockGudang = [
|
||||||
|
{ id: 101, name: 'Eko Prasetyo', phone: '+62 816 5678 9012', status: 'active' },
|
||||||
|
{ id: 102, name: 'Fajar Hidayat', phone: '+62 817 6789 0123', status: 'active' },
|
||||||
|
{ id: 103, name: 'Siti Nurhaliza', phone: '+62 818 7890 1234', status: 'active' },
|
||||||
|
{ id: 104, name: 'Andi Pratama', phone: '+62 819 8901 2345', status: 'inactive' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ContactCard = memo(function ContactCard({ contact, showEditModal, showDeleteModal }) {
|
||||||
|
return (
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<div
|
||||||
|
className="contact-card"
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
height: '100%',
|
||||||
|
padding: '16px',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Status Badge - Top Right */}
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0, zIndex: 1 }}>
|
||||||
|
{contact.status === 'active' ? (
|
||||||
|
<Tag color={'green'} style={{ fontSize: '11px' }}>
|
||||||
|
Active
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag color={'red'} style={{ fontSize: '11px' }}>
|
||||||
|
InActive
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="avatar"
|
||||||
|
style={{
|
||||||
|
width: 55,
|
||||||
|
height: 55,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor:
|
||||||
|
contact.status === 'active' ? '#52c41a' : '#8c8c8c',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserOutlined style={{ color: 'white', fontSize: '25px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '16px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
color: '#262626',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contact.name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PhoneOutlined style={{ marginRight: 6, color: '#1890ff' }} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: contact.status === 'active' ? '#52c41a' : '#ff4d4f',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contact.phone}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit and Delete Buttons - Bottom Right */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '8px' }}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
borderColor: '#faad14',
|
||||||
|
padding: '2px 6px',
|
||||||
|
fontSize: '11px',
|
||||||
|
height: '24px'
|
||||||
|
}}
|
||||||
|
icon={<EditOutlined style={{ color: '#faad14', fontSize: '11px' }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showEditModal(contact);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit info
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
borderColor: 'red',
|
||||||
|
padding: '2px 6px',
|
||||||
|
fontSize: '11px',
|
||||||
|
height: '24px'
|
||||||
|
}}
|
||||||
|
icon={<DeleteOutlined style={{ fontSize: '11px' }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showDeleteModal(contact);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListContact = memo(function ListContact(props) {
|
||||||
|
const [activeTab, setActiveTab] = useState('operator');
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [operators, setOperators] = useState(initialMockOperators);
|
||||||
|
const [gudang, setGudang] = useState(initialMockGudang);
|
||||||
|
const [filteredOperators, setFilteredOperators] = useState(initialMockOperators);
|
||||||
|
const [filteredGudang, setFilteredGudang] = useState(initialMockGudang);
|
||||||
|
const [sortBy, setSortBy] = useState('name');
|
||||||
|
const [filterBy, setFilterBy] = useState('all');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Listen for saved contact data
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.lastSavedContact) {
|
||||||
|
handleContactSaved(
|
||||||
|
props.lastSavedContact.contactData,
|
||||||
|
props.lastSavedContact.actionMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [props.lastSavedContact]);
|
||||||
|
|
||||||
|
// Handle contact data from modal
|
||||||
|
const handleContactSaved = (contactData, actionMode) => {
|
||||||
|
const updateContacts = (contacts) => {
|
||||||
|
if (actionMode === 'add') {
|
||||||
|
const maxId = Math.max(...contacts.map((item) => item.id), 0);
|
||||||
|
return [...contacts, { ...contactData, id: maxId + 1 }];
|
||||||
|
} else if (actionMode === 'edit') {
|
||||||
|
return contacts.map((item) =>
|
||||||
|
item.id === contactData.id ? { ...item, ...contactData } : item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return contacts;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeTab === 'operator') {
|
||||||
|
setOperators(updateContacts(operators));
|
||||||
|
} else {
|
||||||
|
setGudang(updateContacts(gudang));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
navigate('/signin');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filter and sort helper function
|
||||||
|
const filterAndSort = (contacts) => {
|
||||||
|
let filtered = contacts.filter(
|
||||||
|
(contact) =>
|
||||||
|
contact.name.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||||
|
contact.phone.includes(searchValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filterBy !== 'all') {
|
||||||
|
filtered = filtered.filter((contact) => contact.status === filterBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
|
if (sortBy === 'name') return a.name.localeCompare(b.name);
|
||||||
|
if (sortBy === 'phone') return a.phone.localeCompare(b.phone);
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilteredOperators(filterAndSort(operators));
|
||||||
|
setFilteredGudang(filterAndSort(gudang));
|
||||||
|
}, [searchValue, sortBy, filterBy, operators, gudang]);
|
||||||
|
|
||||||
|
const showEditModal = (param) => {
|
||||||
|
props.setSelectedData(param);
|
||||||
|
props.setActionMode('edit');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAddModal = () => {
|
||||||
|
props.setSelectedData(null);
|
||||||
|
props.setActionMode('add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDeleteModal = (contact) => {
|
||||||
|
NotifConfirmDialog({
|
||||||
|
icon: 'question',
|
||||||
|
title: 'Konfirmasi Hapus',
|
||||||
|
message: `Kontak "${contact.name}" akan dihapus?`,
|
||||||
|
onConfirm: () => handleDelete(contact),
|
||||||
|
onCancel: () => props.setSelectedData(null),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (contact) => {
|
||||||
|
if (activeTab === 'operator') {
|
||||||
|
const updatedOperators = operators.filter((op) => op.id !== contact.id);
|
||||||
|
setOperators(updatedOperators);
|
||||||
|
} else {
|
||||||
|
const updatedGudang = gudang.filter((item) => item.id !== contact.id);
|
||||||
|
setGudang(updatedGudang);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: `Kontak "${contact.name}" berhasil dihapus.`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value) => {
|
||||||
|
setSearchValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchClear = () => {
|
||||||
|
setSearchValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Card>
|
||||||
|
<Row>
|
||||||
|
<Col xs={24}>
|
||||||
|
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||||
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
|
<Input.Search
|
||||||
|
placeholder="Search by name or phone..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchValue(value);
|
||||||
|
if (value === '') {
|
||||||
|
handleSearchClear();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
allowClear={{
|
||||||
|
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||||
|
}}
|
||||||
|
enterButton={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#23A55A',
|
||||||
|
borderColor: '#23A55A',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Space wrap size="small">
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => showAddModal()}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Add Contact
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
||||||
|
<TabPane tab="Operator" key="operator" />
|
||||||
|
<TabPane tab="Gudang" key="gudang" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<span style={{ fontSize: '14px', color: '#666' }}>
|
||||||
|
Filter by:
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={filterBy}
|
||||||
|
onChange={setFilterBy}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Select.Option value="all">All</Select.Option>
|
||||||
|
<Select.Option value="active">Active</Select.Option>
|
||||||
|
<Select.Option value="inactive">Inactive</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<span style={{ fontSize: '14px', color: '#666' }}>
|
||||||
|
Sort by:
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={setSortBy}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Select.Option value="name">Name</Select.Option>
|
||||||
|
<Select.Option value="phone">Phone</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'operator' ? (
|
||||||
|
filteredOperators.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<span style={{ color: '#8c8c8c' }}>No operators found</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{filteredOperators.map((operator) => (
|
||||||
|
<ContactCard
|
||||||
|
key={operator.id}
|
||||||
|
contact={operator}
|
||||||
|
showEditModal={showEditModal}
|
||||||
|
showDeleteModal={showDeleteModal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
) : filteredGudang.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<span style={{ color: '#8c8c8c' }}>
|
||||||
|
No warehouse contacts found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{filteredGudang.map((item) => (
|
||||||
|
<ContactCard
|
||||||
|
key={item.id}
|
||||||
|
contact={item}
|
||||||
|
showEditModal={showEditModal}
|
||||||
|
showDeleteModal={showDeleteModal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ListContact;
|
||||||
Reference in New Issue
Block a user