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 IndexRole from './pages/role/IndexRole';
|
||||
import IndexUser from './pages/user/IndexUser';
|
||||
import IndexContact from './pages/contact/IndexContact';
|
||||
|
||||
import SvgTest from './pages/home/SvgTest';
|
||||
import SvgOverviewCompressor from './pages/home/SvgOverviewCompressor';
|
||||
@@ -112,6 +113,10 @@ const App = () => {
|
||||
<Route index element={<IndexUser />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/contact" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexContact />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/jadwal-shift" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexJadwalShift />} />
|
||||
</Route>
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
GroupOutlined,
|
||||
SlidersOutlined,
|
||||
SnippetsOutlined,
|
||||
ContactsOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
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',
|
||||
icon: <BellOutlined style={{ fontSize: '19px' }} />,
|
||||
@@ -236,6 +246,7 @@ const LayoutMenu = () => {
|
||||
if (pathname === '/role') return 'role';
|
||||
if (pathname === '/notification') return 'notification';
|
||||
if (pathname === '/jadwal-shift') return 'jadwal-shift';
|
||||
if (pathname === '/contact') return 'contact';
|
||||
|
||||
// Handle master routes
|
||||
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