add menu contact
This commit is contained in:
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