Add contact management functionality with CRUD operations and UI enhancements
This commit is contained in:
56
src/api/contact.jsx
Normal file
56
src/api/contact.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { SendRequest } from '../components/Global/ApiRequest';
|
||||||
|
|
||||||
|
const getAllContact = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `contact?${queryParams.toString()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContactById = async (id) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'get',
|
||||||
|
prefix: `contact/${id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createContact = async (queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'post',
|
||||||
|
prefix: `contact`,
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContact = async (id, queryParams) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'put',
|
||||||
|
prefix: `contact/${id}`,
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteContact = async (id) => {
|
||||||
|
const response = await SendRequest({
|
||||||
|
method: 'delete',
|
||||||
|
prefix: `contact/${id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAllContact,
|
||||||
|
getContactById,
|
||||||
|
createContact,
|
||||||
|
updateContact,
|
||||||
|
deleteContact,
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ const IndexContact = memo(function IndexContact() {
|
|||||||
const [selectedData, setSelectedData] = useState(null);
|
const [selectedData, setSelectedData] = useState(null);
|
||||||
const [readOnly, setReadOnly] = useState(false);
|
const [readOnly, setReadOnly] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [contactType, setContactType] = useState('operator');
|
||||||
|
|
||||||
const setMode = (param) => {
|
const setMode = (param) => {
|
||||||
setShowModal(param !== 'list');
|
setShowModal(param !== 'list');
|
||||||
@@ -52,6 +53,7 @@ const IndexContact = memo(function IndexContact() {
|
|||||||
setSelectedData={setSelectedData}
|
setSelectedData={setSelectedData}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
lastSavedContact={lastSavedContact}
|
lastSavedContact={lastSavedContact}
|
||||||
|
setContactType={setContactType}
|
||||||
/>
|
/>
|
||||||
<DetailContact
|
<DetailContact
|
||||||
setActionMode={setMode}
|
setActionMode={setMode}
|
||||||
@@ -61,6 +63,7 @@ const IndexContact = memo(function IndexContact() {
|
|||||||
showModal={showModal}
|
showModal={showModal}
|
||||||
actionMode={actionMode}
|
actionMode={actionMode}
|
||||||
onContactSaved={handleContactSaved}
|
onContactSaved={handleContactSaved}
|
||||||
|
contactType={contactType}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { memo, useEffect, useState } from 'react';
|
import React, { memo, useEffect, useState } from 'react';
|
||||||
import { Modal, Input, Button, Switch, ConfigProvider, Typography, Divider } from 'antd';
|
import { Modal, Input, Button, Switch, ConfigProvider, Typography, Divider, Select } from 'antd';
|
||||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||||
import { validateRun } from '../../../Utils/validate';
|
import { validateRun } from '../../../Utils/validate';
|
||||||
|
import { createContact, updateContact } from '../../../api/contact';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
|
contact_type: 'operator',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
@@ -26,6 +28,10 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
} else if (e && e.type === 'change') {
|
} else if (e && e.type === 'change') {
|
||||||
name = e.name || e.target?.name;
|
name = e.name || e.target?.name;
|
||||||
value = e.value !== undefined ? e.value : e.checked;
|
value = e.value !== undefined ? e.value : e.checked;
|
||||||
|
} else if (typeof e === 'string' || typeof e === 'number') {
|
||||||
|
// Handle Select onChange
|
||||||
|
value = e;
|
||||||
|
name = 'contact_type';
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,12 +59,13 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
// Custom validation untuk phone
|
// Custom validation untuk phone - Indonesian phone format
|
||||||
if (formData.phone && !/^[\d\s\+\-\(\)]+$/.test(formData.phone)) {
|
const phoneRegex = /^(?:\+62|0)8\d{7,10}$/;
|
||||||
|
if (formData.phone && !phoneRegex.test(formData.phone.replace(/[\s\-\(\)]/g, ''))) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Peringatan',
|
title: 'Peringatan',
|
||||||
message: 'Nomor telepon hanya boleh mengandung angka, spasi, +, -, dan ()',
|
message: 'Nomor telepon harus format Indonesia (+628XXXXXXXXX atau 08XXXXXXXXX)',
|
||||||
});
|
});
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -68,6 +75,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
const validationRules = [
|
const validationRules = [
|
||||||
{ field: 'name', label: 'Contact Name', required: true },
|
{ field: 'name', label: 'Contact Name', required: true },
|
||||||
{ field: 'phone', label: 'Phone', required: true },
|
{ field: 'phone', label: 'Phone', required: true },
|
||||||
|
{ field: 'contact_type', label: 'Contact Type', required: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -80,15 +88,18 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const contactData = {
|
const contactData = {
|
||||||
id: props.selectedData?.id || null,
|
contact_name: formData.name,
|
||||||
name: formData.name,
|
contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number
|
||||||
phone: formData.phone,
|
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
status: formData.is_active ? 'active' : 'inactive',
|
contact_type: formData.contact_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Saving contact data:', contactData);
|
let response;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
if (props.actionMode === 'edit') {
|
||||||
|
response = await updateContact(props.selectedData.contact_id || props.selectedData.id, contactData);
|
||||||
|
} else {
|
||||||
|
response = await createContact(contactData);
|
||||||
|
}
|
||||||
|
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -98,14 +109,14 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
}.`,
|
}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
props.onContactSaved?.(contactData, props.actionMode);
|
props.onContactSaved?.(response.data, props.actionMode);
|
||||||
handleCancel();
|
handleCancel();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Save failed:', error);
|
console.error('Save failed:', error);
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Terjadi kesalahan saat menyimpan data.',
|
message: error.response?.data?.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
@@ -121,19 +132,21 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
if (props.showModal) {
|
if (props.showModal) {
|
||||||
if (props.actionMode === 'edit' && props.selectedData) {
|
if (props.actionMode === 'edit' && props.selectedData) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: props.selectedData.name,
|
name: props.selectedData.contact_name || props.selectedData.name,
|
||||||
phone: props.selectedData.phone,
|
phone: props.selectedData.contact_phone || props.selectedData.phone,
|
||||||
is_active: props.selectedData.status === 'active',
|
is_active: props.selectedData.is_active || props.selectedData.status === 'active',
|
||||||
|
contact_type: props.selectedData.contact_type || props.contactType || 'operator',
|
||||||
});
|
});
|
||||||
} else if (props.actionMode === 'add') {
|
} else if (props.actionMode === 'add') {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
|
contact_type: props.contactType === 'all' ? 'operator' : props.contactType || 'operator',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.showModal, props.actionMode, props.selectedData]);
|
}, [props.showModal, props.actionMode, props.selectedData, props.contactType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -228,6 +241,20 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
|
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong>Contact Type</Text>
|
||||||
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
|
<Select
|
||||||
|
value={formData.contact_type}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Select Contact Type"
|
||||||
|
disabled={props.readOnly}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Select.Option value="operator">Operator</Select.Option>
|
||||||
|
<Select.Option value="gudang">Gudang</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
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 {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -10,24 +10,10 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
|
import { getAllContact, deleteContact } from '../../../api/contact';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
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 }) {
|
const ContactCard = memo(function ContactCard({ contact, showEditModal, showDeleteModal }) {
|
||||||
return (
|
return (
|
||||||
<Col xs={24} sm={12} md={8} lg={6}>
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
@@ -110,7 +96,7 @@ const ContactCard = memo(function ContactCard({ contact, showEditModal, showDele
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{contact.name}
|
{contact.contact_name || contact.name}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -125,7 +111,7 @@ const ContactCard = memo(function ContactCard({ contact, showEditModal, showDele
|
|||||||
color: contact.status === 'active' ? '#52c41a' : '#ff4d4f',
|
color: contact.status === 'active' ? '#52c41a' : '#ff4d4f',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{contact.phone}
|
{contact.contact_phone || contact.phone}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,77 +164,81 @@ const ContactCard = memo(function ContactCard({ contact, showEditModal, showDele
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ListContact = memo(function ListContact(props) {
|
const ListContact = memo(function ListContact(props) {
|
||||||
const [activeTab, setActiveTab] = useState('operator');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [filteredContacts, setFilteredContacts] = useState([]);
|
||||||
const [operators, setOperators] = useState(initialMockOperators);
|
const [loading, setLoading] = useState(false);
|
||||||
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();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Listen for saved contact data
|
// Default filter object matching plantSection pattern
|
||||||
useEffect(() => {
|
const defaultFilter = { criteria: '' };
|
||||||
if (props.lastSavedContact) {
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
handleContactSaved(
|
|
||||||
props.lastSavedContact.contactData,
|
|
||||||
props.lastSavedContact.actionMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [props.lastSavedContact]);
|
|
||||||
|
|
||||||
// Handle contact data from modal
|
// Fetch contacts from API
|
||||||
const handleContactSaved = (contactData, actionMode) => {
|
const fetchContacts = async () => {
|
||||||
const updateContacts = (contacts) => {
|
setLoading(true);
|
||||||
if (actionMode === 'add') {
|
try {
|
||||||
const maxId = Math.max(...contacts.map((item) => item.id), 0);
|
// Build search parameters matching database pattern
|
||||||
return [...contacts, { ...contactData, id: maxId + 1 }];
|
const searchParams = { ...formDataFilter };
|
||||||
} else if (actionMode === 'edit') {
|
|
||||||
return contacts.map((item) =>
|
// Add specific filters if not "all"
|
||||||
item.id === contactData.id ? { ...item, ...contactData } : item
|
if (activeTab !== 'all') {
|
||||||
);
|
if (activeTab === 'operator') {
|
||||||
|
searchParams.code = 'operator';
|
||||||
|
} else if (activeTab === 'gudang') {
|
||||||
|
searchParams.code = 'gudang';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return contacts;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (activeTab === 'operator') {
|
// Backend doesn't support is_active filter or order parameter
|
||||||
setOperators(updateContacts(operators));
|
// Contact hanya supports: criteria, name, code, limit, page
|
||||||
} else {
|
|
||||||
setGudang(updateContacts(gudang));
|
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(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
navigate('/signin');
|
navigate('/signin');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
fetchContacts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Filter and sort helper function
|
// Refetch when filters change
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
setFilteredOperators(filterAndSort(operators));
|
fetchContacts();
|
||||||
setFilteredGudang(filterAndSort(gudang));
|
}, [formDataFilter, activeTab]);
|
||||||
}, [searchValue, sortBy, filterBy, operators, gudang]);
|
|
||||||
|
// 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) => {
|
const showEditModal = (param) => {
|
||||||
props.setSelectedData(param);
|
props.setSelectedData(param);
|
||||||
@@ -258,42 +248,41 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDeleteModal = (contact) => {
|
const showDeleteModal = (contact) => {
|
||||||
NotifConfirmDialog({
|
NotifConfirmDialog({
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
title: 'Konfirmasi Hapus',
|
title: 'Konfirmasi Hapus',
|
||||||
message: `Kontak "${contact.name}" akan dihapus?`,
|
message: `Kontak "${contact.contact_name || contact.name}" akan dihapus?`,
|
||||||
onConfirm: () => handleDelete(contact),
|
onConfirm: () => handleDelete(contact),
|
||||||
onCancel: () => props.setSelectedData(null),
|
onCancel: () => props.setSelectedData(null),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (contact) => {
|
const handleDelete = async (contact) => {
|
||||||
if (activeTab === 'operator') {
|
try {
|
||||||
const updatedOperators = operators.filter((op) => op.id !== contact.id);
|
await deleteContact(contact.contact_id || contact.id);
|
||||||
setOperators(updatedOperators);
|
NotifAlert({
|
||||||
} else {
|
icon: 'success',
|
||||||
const updatedGudang = gudang.filter((item) => item.id !== contact.id);
|
title: 'Berhasil',
|
||||||
setGudang(updatedGudang);
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -302,18 +291,18 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||||
<Col xs={24} sm={24} md={12} lg={12}>
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Search by name or phone..."
|
placeholder="Search by name or type..."
|
||||||
value={searchValue}
|
value={formDataFilter.criteria}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setSearchValue(value);
|
setFormDataFilter({ criteria: value });
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
handleSearchClear();
|
setFormDataFilter(defaultFilter);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSearch={handleSearch}
|
onSearch={(value) => setFormDataFilter({ criteria: value })}
|
||||||
allowClear={{
|
allowClear={{
|
||||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
clearIcon: <span onClick={() => setFormDataFilter(defaultFilter)}>✕</span>,
|
||||||
}}
|
}}
|
||||||
enterButton={
|
enterButton={
|
||||||
<Button
|
<Button
|
||||||
@@ -365,73 +354,31 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
||||||
|
<TabPane tab="All" key="all" />
|
||||||
<TabPane tab="Operator" key="operator" />
|
<TabPane tab="Operator" key="operator" />
|
||||||
<TabPane tab="Gudang" key="gudang" />
|
<TabPane tab="Gudang" key="gudang" />
|
||||||
</Tabs>
|
</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>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'operator' ? (
|
{getFilteredContacts().length === 0 ? (
|
||||||
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' }}>
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
<span style={{ color: '#8c8c8c' }}>
|
<span style={{ color: '#8c8c8c' }}>
|
||||||
No warehouse contacts found
|
{loading ? 'Loading contacts...' : 'No contacts found'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{filteredGudang.map((item) => (
|
{getFilteredContacts().map((contact) => (
|
||||||
<ContactCard
|
<ContactCard
|
||||||
key={item.id}
|
key={contact.contact_id || contact.id}
|
||||||
contact={item}
|
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}
|
showEditModal={showEditModal}
|
||||||
showDeleteModal={showDeleteModal}
|
showDeleteModal={showDeleteModal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user