From 08f8c4708f252e04345282e53c8ee13580ace478 Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Thu, 13 Nov 2025 13:58:52 +0700 Subject: [PATCH] add menu contact --- src/App.jsx | 5 + src/layout/LayoutMenu.jsx | 11 + src/pages/contact/IndexContact.jsx | 69 +++ src/pages/contact/component/DetailContact.jsx | 236 +++++++++ src/pages/contact/component/ListContact.jsx | 448 ++++++++++++++++++ 5 files changed, 769 insertions(+) create mode 100644 src/pages/contact/IndexContact.jsx create mode 100644 src/pages/contact/component/DetailContact.jsx create mode 100644 src/pages/contact/component/ListContact.jsx diff --git a/src/App.jsx b/src/App.jsx index ddaac6d..9081ffe 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 = () => { } /> + }> + } /> + + }> } /> diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx index c0f12f0..71ff29c 100644 --- a/src/layout/LayoutMenu.jsx +++ b/src/layout/LayoutMenu.jsx @@ -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: , + label: ( + + Contact + + ), + }, { key: 'notification', icon: , @@ -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/')) { diff --git a/src/pages/contact/IndexContact.jsx b/src/pages/contact/IndexContact.jsx new file mode 100644 index 0000000..d8df467 --- /dev/null +++ b/src/pages/contact/IndexContact.jsx @@ -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: • Contact }, + ]); + } else { + navigate('/signin'); + } + }, [navigate, setBreadcrumbItems]); + + return ( + + + + + ); +}); + +export default IndexContact; diff --git a/src/pages/contact/component/DetailContact.jsx b/src/pages/contact/component/DetailContact.jsx new file mode 100644 index 0000000..ec0ada2 --- /dev/null +++ b/src/pages/contact/component/DetailContact.jsx @@ -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 ( + + + + + + {!props.readOnly && ( + + )} + + , + ]} + > +
+
+
+ Status +
+
+
+ +
+
+ {formData.is_active ? 'Active' : 'Inactive'} +
+
+
+ + +
+ Name + * + +
+
+ Phone + * + +
+
+
+ ); +}); + +export default DetailContact; diff --git a/src/pages/contact/component/ListContact.jsx b/src/pages/contact/component/ListContact.jsx new file mode 100644 index 0000000..5cb789c --- /dev/null +++ b/src/pages/contact/component/ListContact.jsx @@ -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 ( + +
{ + 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)'; + }} + > +
+ {/* Status Badge - Top Right */} +
+ {contact.status === 'active' ? ( + + Active + + ) : ( + + InActive + + )} +
+ + {/* Main Content */} +
+
+ +
+
+
+ {contact.name} +
+
+ + + {contact.phone} + +
+
+
+ + {/* Edit and Delete Buttons - Bottom Right */} +
+ + + + +
+
+
+ + ); +}); + +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 ( + + + + + + + { + const value = e.target.value; + setSearchValue(value); + if (value === '') { + handleSearchClear(); + } + }} + onSearch={handleSearch} + allowClear={{ + clearIcon: , + }} + enterButton={ + + } + size="large" + /> + + + + + + + + + + + +
+ + + + + +
+
+ + Filter by: + + +
+ +
+ + Sort by: + + +
+
+
+ + {activeTab === 'operator' ? ( + filteredOperators.length === 0 ? ( +
+ No operators found +
+ ) : ( + + {filteredOperators.map((operator) => ( + + ))} + + ) + ) : filteredGudang.length === 0 ? ( +
+ + No warehouse contacts found + +
+ ) : ( + + {filteredGudang.map((item) => ( + + ))} + + )} + +
+
+
+ ); +}); + +export default ListContact;