541 lines
18 KiB
JavaScript

import React, { useState, useEffect, useCallback } from 'react'
import { useLocation } from 'react-router-dom'
import axios from 'axios'
import {
Descriptions,
Spin,
Space,
Button,
message,
Typography,
Flex,
Form,
Input,
Collapse,
Dropdown,
Popover,
Card,
Checkbox
} from 'antd'
import {
LoadingOutlined,
ExportOutlined,
CaretLeftOutlined
} from '@ant-design/icons'
import IdText from '../../common/IdText'
import CountrySelect from '../../common/CountrySelect'
import CountryDisplay from '../../common/CountryDisplay'
import TimeDisplay from '../../common/TimeDisplay'
import ReloadIcon from '../../../Icons/ReloadIcon'
import EditIcon from '../../../Icons/EditIcon.jsx'
import XMarkIcon from '../../../Icons/XMarkIcon.jsx'
import CheckIcon from '../../../Icons/CheckIcon.jsx'
import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import config from '../../../../config.js'
const { Title, Link, Text } = Typography
const VendorInfo = () => {
const [vendorData, setVendorData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const location = useLocation()
const [messageApi, contextHolder] = message.useMessage()
const vendorId = new URLSearchParams(location.search).get('vendorId')
const [isEditing, setIsEditing] = useState(false)
const [form] = Form.useForm()
const [fetchLoading, setFetchLoading] = useState(true)
const [collapseState, updateCollapseState] = useCollapseState('VendorInfo', {
info: true,
notes: true,
auditLogs: true
})
useEffect(() => {
if (vendorId) {
fetchVendorDetails()
}
}, [vendorId, fetchVendorDetails])
useEffect(() => {
if (vendorData) {
form.setFieldsValue({
name: vendorData.name || '',
website: vendorData.website || '',
contact: vendorData.contact || '',
country: vendorData.country || '',
phone: vendorData.phone || '',
email: vendorData.email || ''
})
}
}, [vendorData, form])
const fetchVendorDetails = useCallback(async () => {
try {
setFetchLoading(true)
const response = await axios.get(
`${config.backendUrl}/vendors/${vendorId}`,
{
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
setVendorData(response.data)
setError(null)
} catch (err) {
setError('Failed to fetch vendor details')
messageApi.error('Failed to fetch vendor details')
} finally {
setFetchLoading(false)
}
}, [messageApi, vendorId])
const startEditing = () => {
updateCollapseState('info', true)
setIsEditing(true)
}
const cancelEditing = () => {
// Reset form values to original data
if (vendorData) {
form.setFieldsValue({
name: vendorData.name || '',
website: vendorData.website || '',
contact: vendorData.contact || '',
country: vendorData.country || '',
phone: vendorData.phone || '',
email: vendorData.email || ''
})
}
setIsEditing(false)
}
const updateInfo = async () => {
try {
const values = await form.validateFields()
setLoading(true)
await axios.put(`${config.backendUrl}/vendors/${vendorId}`, values, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
setVendorData({ ...vendorData, ...values })
setIsEditing(false)
messageApi.success('Vendor information updated successfully')
} catch (err) {
if (err.errorFields) {
return
}
messageApi.error('Failed to update vendor information')
} finally {
fetchVendorDetails()
setLoading(false)
}
}
const actionItems = {
items: [
{
label: 'Reload Vendor',
key: 'reload',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reload') {
fetchVendorDetails()
}
}
}
const getViewDropdownItems = () => {
const sections = [
{ key: 'info', label: 'Vendor Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{sections.map((section) => (
<Checkbox
checked={collapseState[section.key]}
key={section.key}
onChange={(e) => {
updateCollapseState(section.key, e.target.checked)
}}
>
{section.label}
</Checkbox>
))}
</Flex>
</Flex>
)
}
if (error) {
return (
<Space
direction='vertical'
style={{ width: '100%', textAlign: 'center' }}
>
<p>{error || 'Vendor not found'}</p>
<Button icon={<ReloadIcon />} onClick={fetchVendorDetails}>
Retry
</Button>
</Space>
)
}
return (
<>
{contextHolder}
<Flex
gap='large'
vertical='true'
style={{ height: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
</Space>
<Space>
{isEditing ? (
<>
<Button
icon={<CheckIcon />}
type='primary'
onClick={updateInfo}
loading={loading}
disabled={loading}
/>
<Button
icon={<XMarkIcon />}
onClick={cancelEditing}
disabled={loading}
/>
</>
) : (
<Button icon={<EditIcon />} onClick={startEditing} />
)}
</Space>
</Flex>
{error ? (
<Space
direction='vertical'
style={{ width: '100%', textAlign: 'center' }}
>
<p>{error || 'Vendor not found'}</p>
<Button icon={<ReloadIcon />} onClick={fetchVendorDetails}>
Retry
</Button>
</Space>
) : (
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.info ? ['1'] : []}
onChange={(keys) =>
updateCollapseState('info', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined
rotate={isActive ? -90 : 0}
style={{ paddingTop: '9px' }}
/>
)}
className='no-h-padding-collapse no-t-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<InfoCircleIcon />
<Title level={5} style={{ margin: 0 }}>
Vendor Information
</Title>
</Flex>
}
key='1'
>
<Form form={form} layout='vertical'>
<Spin
indicator={<LoadingOutlined />}
spinning={fetchLoading}
>
<Descriptions
bordered
column={{
xs: 1,
sm: 1,
md: 1,
lg: 2,
xl: 2,
xxl: 2
}}
>
<Descriptions.Item label='ID'>
{vendorData?._id ? (
<IdText id={vendorData._id} type='vendor' />
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Created At'>
{vendorData?.createdAt ? (
<TimeDisplay
dateTime={vendorData.createdAt}
showSince={true}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Name'>
{isEditing ? (
<Form.Item
name='name'
rules={[
{
required: true,
message: 'Please enter a vendor name'
},
{
max: 100,
message: 'Name cannot exceed 100 characters'
}
]}
style={{ margin: 0 }}
>
<Input />
</Form.Item>
) : vendorData?.name ? (
<Text>{vendorData.name}</Text>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Updated At'>
{vendorData?.updatedAt ? (
<TimeDisplay
dateTime={vendorData.updatedAt}
showSince={true}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Website'>
{isEditing ? (
<Form.Item
name='website'
rules={[
{
type: 'url',
message: 'Please enter a valid URL'
},
{
max: 200,
message:
'Website URL cannot exceed 200 characters'
}
]}
style={{ margin: 0 }}
>
<Input />
</Form.Item>
) : vendorData?.website ? (
<Link
href={vendorData.website}
target='_blank'
rel='noopener noreferrer'
>
{new URL(vendorData.website).hostname + ' '}
<ExportOutlined />
</Link>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Country'>
{isEditing ? (
<Form.Item name='country' style={{ margin: 0 }}>
<CountrySelect
countryCode={vendorData?.country}
/>
</Form.Item>
) : vendorData?.country ? (
<CountryDisplay countryCode={vendorData.country} />
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Contact'>
{isEditing ? (
<Form.Item
name='contact'
rules={[
{
max: 200,
message:
'Contact info cannot exceed 200 characters'
}
]}
style={{ margin: 0 }}
>
<Input />
</Form.Item>
) : vendorData?.contact ? (
<Text>{vendorData.contact}</Text>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Phone'>
{isEditing ? (
<Form.Item
name='phone'
rules={[
{
type: 'phone',
message: 'Please enter a valid phone number'
}
]}
style={{ margin: 0 }}
>
<Input />
</Form.Item>
) : vendorData?.phone ? (
<Text>{vendorData.phone}</Text>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Email'>
{isEditing ? (
<Form.Item
name='email'
rules={[
{
type: 'email',
message: 'Please enter a valid email'
}
]}
style={{ margin: 0 }}
>
<Input />
</Form.Item>
) : vendorData?.email ? (
<Link href={`mailto:${vendorData.email}`}>
{vendorData.email + ' '}
<ExportOutlined />
</Link>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
</Descriptions>
</Spin>
</Form>
</Collapse.Panel>
</Collapse>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.notes ? ['notes'] : []}
onChange={(keys) =>
updateCollapseState('notes', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<NoteIcon />
<Title level={5} style={{ margin: 0 }}>
Notes
</Title>
</Flex>
}
key='notes'
>
<Card>
<DashboardNotes _id={vendorId} />
</Card>
</Collapse.Panel>
</Collapse>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.auditLogs ? ['auditLogs'] : []}
onChange={(keys) =>
updateCollapseState('auditLogs', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<AuditLogIcon />
<Title level={5} style={{ margin: 0 }}>
Audit Logs
</Title>
</Flex>
}
key='auditLogs'
>
<AuditLogTable
items={vendorData?.auditLogs || []}
loading={fetchLoading}
showTargetColumn={false}
/>
</Collapse.Panel>
</Collapse>
</Flex>
</div>
)}
</Flex>
</>
)
}
export default VendorInfo