Add contact management functionality with CRUD operations and UI enhancements

This commit is contained in:
2025-11-15 13:36:38 +07:00
parent 7dd38aa50c
commit 8cf5878d46
4 changed files with 204 additions and 171 deletions

View File

@@ -1,5 +1,5 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag, Select } from 'antd';
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag } from 'antd';
import {
PlusOutlined,
EditOutlined,
@@ -10,24 +10,10 @@ import {
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { getAllContact, deleteContact } from '../../../api/contact';
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}>
@@ -110,7 +96,7 @@ const ContactCard = memo(function ContactCard({ contact, showEditModal, showDele
whiteSpace: 'nowrap',
}}
>
{contact.name}
{contact.contact_name || contact.name}
</div>
<div
style={{
@@ -125,7 +111,7 @@ const ContactCard = memo(function ContactCard({ contact, showEditModal, showDele
color: contact.status === 'active' ? '#52c41a' : '#ff4d4f',
}}
>
{contact.phone}
{contact.contact_phone || contact.phone}
</span>
</div>
</div>
@@ -178,77 +164,81 @@ const ContactCard = memo(function ContactCard({ contact, showEditModal, showDele
});
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 [activeTab, setActiveTab] = useState('all');
const [filteredContacts, setFilteredContacts] = useState([]);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
// Listen for saved contact data
useEffect(() => {
if (props.lastSavedContact) {
handleContactSaved(
props.lastSavedContact.contactData,
props.lastSavedContact.actionMode
);
}
}, [props.lastSavedContact]);
// Default filter object matching plantSection pattern
const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
// 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
);
// Fetch contacts from API
const fetchContacts = async () => {
setLoading(true);
try {
// Build search parameters matching database pattern
const searchParams = { ...formDataFilter };
// Add specific filters if not "all"
if (activeTab !== 'all') {
if (activeTab === 'operator') {
searchParams.code = 'operator';
} else if (activeTab === 'gudang') {
searchParams.code = 'gudang';
}
}
return contacts;
};
if (activeTab === 'operator') {
setOperators(updateContacts(operators));
} else {
setGudang(updateContacts(gudang));
// Backend doesn't support is_active filter or order parameter
// Contact hanya supports: criteria, name, code, limit, page
const queryParams = new URLSearchParams();
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
queryParams.append(key, value);
}
});
const response = await getAllContact(queryParams);
setFilteredContacts(response.data || []);
} catch (error) {
console.error('Error fetching contacts:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal memuat data kontak',
});
} finally {
setLoading(false);
}
};
// Fetch contacts on component mount
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
return;
}
fetchContacts();
}, []);
// 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;
});
};
// Refetch when filters change
useEffect(() => {
setFilteredOperators(filterAndSort(operators));
setFilteredGudang(filterAndSort(gudang));
}, [searchValue, sortBy, filterBy, operators, gudang]);
fetchContacts();
}, [formDataFilter, activeTab]);
// Listen for saved contact data
useEffect(() => {
if (props.lastSavedContact) {
fetchContacts(); // Refetch all contacts when data is saved
}
}, [props.lastSavedContact]);
// Get contacts (already filtered by backend)
const getFilteredContacts = () => {
return filteredContacts;
};
const showEditModal = (param) => {
props.setSelectedData(param);
@@ -258,42 +248,41 @@ const ListContact = memo(function ListContact(props) {
const showAddModal = () => {
props.setSelectedData(null);
props.setActionMode('add');
// Pass the current active tab to determine contact type
props.setContactType?.(activeTab);
};
const showDeleteModal = (contact) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi Hapus',
message: `Kontak "${contact.name}" akan dihapus?`,
message: `Kontak "${contact.contact_name || 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);
const handleDelete = async (contact) => {
try {
await deleteContact(contact.contact_id || contact.id);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Kontak "${contact.contact_name || contact.name}" berhasil dihapus.`,
});
// Refetch contacts after deletion
fetchContacts();
} catch (error) {
console.error('Error deleting contact:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal menghapus kontak',
});
}
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Kontak "${contact.name}" berhasil dihapus.`,
});
};
const handleSearch = (value) => {
setSearchValue(value);
};
const handleSearchClear = () => {
setSearchValue('');
};
return (
<React.Fragment>
<Card>
@@ -302,18 +291,18 @@ const ListContact = memo(function ListContact(props) {
<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}
placeholder="Search by name or type..."
value={formDataFilter.criteria}
onChange={(e) => {
const value = e.target.value;
setSearchValue(value);
setFormDataFilter({ criteria: value });
if (value === '') {
handleSearchClear();
setFormDataFilter(defaultFilter);
}
}}
onSearch={handleSearch}
onSearch={(value) => setFormDataFilter({ criteria: value })}
allowClear={{
clearIcon: <span onClick={handleSearchClear}></span>,
clearIcon: <span onClick={() => setFormDataFilter(defaultFilter)}></span>,
}}
enterButton={
<Button
@@ -365,73 +354,31 @@ const ListContact = memo(function ListContact(props) {
}}
>
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
<TabPane tab="All" key="all" />
<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 ? (
{getFilteredContacts().length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<span style={{ color: '#8c8c8c' }}>
No warehouse contacts found
{loading ? 'Loading contacts...' : 'No contacts found'}
</span>
</div>
) : (
<Row gutter={[16, 16]}>
{filteredGudang.map((item) => (
{getFilteredContacts().map((contact) => (
<ContactCard
key={item.id}
contact={item}
key={contact.contact_id || contact.id}
contact={{
...contact,
id: contact.contact_id || contact.id,
name: contact.contact_name || contact.name,
phone: contact.contact_phone || contact.phone,
status: contact.is_active ? 'active' : 'inactive'
}}
showEditModal={showEditModal}
showDeleteModal={showDeleteModal}
/>