feat(sgw): Add new services and utilities for ship, trip, and photo management

This commit is contained in:
Lê Tuấn Anh
2026-01-23 15:18:02 +07:00
parent e5b388505a
commit 1a06328c77
75 changed files with 9749 additions and 8 deletions

View File

@@ -0,0 +1,38 @@
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import ModalTreeSelectGroup from './ModalTreeSelectGroup';
interface ButtonSelectGroupProps {
visible: boolean;
onVisibleChange: (visible: boolean) => void;
onSubmitGroup: (groupId: string) => Promise<void>;
}
const ButtonSelectGroup: React.FC<ButtonSelectGroupProps> = ({
visible,
onVisibleChange,
onSubmitGroup,
}) => {
const intl = useIntl();
return (
<>
<Button type="primary" onClick={() => onVisibleChange(true)}>
{intl.formatMessage({
id: 'pages.groups.setgroup.text',
defaultMessage: 'Set group',
})}
</Button>
<ModalTreeSelectGroup
visible={visible}
onModalVisible={onVisibleChange}
onSubmit={async (groupId: string) => {
await onSubmitGroup(groupId);
}}
/>
</>
);
};
export default ButtonSelectGroup;

View File

@@ -0,0 +1,103 @@
import { ModalForm, ProFormText } from '@ant-design/pro-form';
import { FormattedMessage, useIntl } from '@umijs/max';
interface EditModalProps {
visible: boolean;
values: {
name?: string;
address?: string;
group_id?: string;
external_id?: string;
type?: string;
};
onVisibleChange: (visible: boolean) => void;
onFinish: (
values: Pick<MasterModel.Thing, 'name'> &
Pick<MasterModel.ThingMetadata, 'address' | 'external_id'>,
) => Promise<boolean>;
}
const EditModal: React.FC<EditModalProps> = ({
visible,
values,
onVisibleChange,
onFinish,
}) => {
const intl = useIntl();
return (
<ModalForm
open={visible}
initialValues={values}
title={intl.formatMessage({
id: 'pages.things.update.title',
defaultMessage: 'Update thing',
})}
width={480}
onVisibleChange={onVisibleChange}
onFinish={onFinish}
modalProps={{
destroyOnHidden: true,
}}
>
<ProFormText
name="name"
label={intl.formatMessage({
id: 'pages.things.name',
defaultMessage: 'Name',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.things.name.required"
defaultMessage="The name is required"
/>
),
},
]}
/>
<ProFormText
name="external_id"
label={intl.formatMessage({
id: 'pages.things.external_id',
defaultMessage: 'ExternalId',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.things.external_id.required"
defaultMessage="The externalId is required"
/>
),
},
]}
/>
<ProFormText
name="address"
label={intl.formatMessage({
id: 'pages.things.address',
defaultMessage: 'Address',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.things.address.required"
defaultMessage="The address is required"
/>
),
},
]}
/>
</ModalForm>
);
};
export default EditModal;

View File

@@ -0,0 +1,243 @@
import { PlusOutlined } from '@ant-design/icons';
import {
ModalForm,
ProFormDateTimePicker,
ProFormDigit,
ProFormSelect,
ProFormText,
} from '@ant-design/pro-form';
import { FormattedMessage, useIntl, useModel } from '@umijs/max';
import { Button, Col, FormInstance, Row, Table } from 'antd';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { useEffect, useRef, useState } from 'react';
import FormShareVms from './FormShareVms';
dayjs.extend(utc);
interface ShipFormValues extends SgwModel.ShipCreateParams {
targets?: SgwModel.SgwThing[];
}
interface FormAddProps {
visible: boolean;
onVisibleChange: (visible: boolean) => void;
onSubmit: (values: ShipFormValues) => Promise<boolean>;
}
const FormAdd: React.FC<FormAddProps> = ({
visible,
onVisibleChange,
onSubmit,
}) => {
const intl = useIntl();
const formRef = useRef<FormInstance<ShipFormValues>>();
const [shareModalVisible, handleShareModalVisible] = useState(false);
const [selectedDevices, setSelectedDevices] = useState<SgwModel.SgwThing[]>(
[],
);
const { shipTypes, getShipTypes } = useModel('slave.sgw.useShipTypes');
const { homeports } = useModel('slave.sgw.useHomePorts');
const { groups, getGroups } = useModel('master.useGroups');
useEffect(() => {
if (!shipTypes) {
getShipTypes();
}
}, [shipTypes]);
useEffect(() => {
if (!groups) {
getGroups();
}
}, [groups]);
console.log('groups', homeports, groups);
// Lọc homeports theo province_code của groups
const groupProvinceCodes = Array.isArray(groups)
? groups.map((g: MasterModel.GroupNode) => g.metadata?.code).filter(Boolean)
: [];
const filteredHomeports = Array.isArray(homeports)
? homeports.filter((p: SgwModel.Port) =>
groupProvinceCodes.includes(p.province_code),
)
: [];
return (
<ModalForm
formRef={formRef}
initialValues={{
reg_number: '',
ship_type: '',
name: '', // Changed from shipname to name
targets: [],
imo_number: '',
mmsi_number: '',
ship_length: '',
ship_power: '',
ship_group_id: '',
fishing_license_number: '',
fishing_license_expiry_date: null,
home_port: '',
}}
title={intl.formatMessage({
id: 'pages.ships.create.title',
defaultMessage: 'Tạo tàu mới',
})}
width="580px"
open={visible}
onVisibleChange={onVisibleChange}
onFinish={async (formValues: ShipFormValues) => {
console.log('FormAdd onFinish - formValues:', formValues);
console.log('FormAdd onFinish - selectedDevices:', selectedDevices);
// Gửi selectedDevices vào targets
const rest = formValues;
const thing_id = selectedDevices?.[0]?.id;
const result = await onSubmit({
...rest,
thing_id,
targets: selectedDevices,
});
console.log('FormAdd onFinish - result:', result);
return result;
}}
>
<Row gutter={16}>
<Col span={12}>
<ProFormText
name="reg_number"
label="Số đăng ký"
rules={[{ required: true, message: 'Nhập số đăng ký' }]}
/>
</Col>
<Col span={12}>
<ProFormText
name="name"
label="Tên tàu"
rules={[{ required: true, message: 'Nhập tên tàu' }]}
/>
</Col>
<Col span={12}>
<ProFormSelect
name="ship_type"
label="Loại tàu"
options={
Array.isArray(shipTypes)
? shipTypes.map((t) => ({ label: t.name, value: t.id }))
: []
}
rules={[{ required: true, message: 'Chọn loại tàu' }]}
showSearch
/>
</Col>
<Col span={12}>
<ProFormSelect
name="home_port"
label="Cảng nhà"
options={filteredHomeports.map((p) => ({
label: p.name,
value: p.id,
}))}
rules={[{ required: true, message: 'Chọn cảng nhà' }]}
showSearch
/>
</Col>
<Col span={12}>
<ProFormText
name="fishing_license_number"
label="Số giấy phép"
rules={[{ required: true, message: 'Nhập số giấy phép' }]}
/>
</Col>
<Col span={12}>
<ProFormDateTimePicker
name="fishing_license_expiry_date"
label={intl.formatMessage({
id: 'pages.things.fishing_license_expiry_date',
defaultMessage: 'fishing_license_expiry_date',
})}
rules={[{ required: false }]}
transform={(value: string) => {
if (!value) return {};
return {
fishing_license_expiry_date: dayjs(value)
.endOf('day')
.utc()
.format(), // ISO 8601 format: YYYY-MM-DDTHH:mm:ssZ
};
}}
fieldProps={{
format: 'YYYY-MM-DD',
}}
/>
</Col>
<Col span={12}>
<ProFormDigit
name="ship_length"
label="Chiều dài (m)"
min={0}
rules={[{ required: true, message: 'Nhập chiều dài tàu' }]}
/>
</Col>
<Col span={12}>
<ProFormDigit
name="ship_power"
label="Công suất (CV)"
min={0}
rules={[{ required: true, message: 'Nhập công suất tàu' }]}
/>
</Col>
{/* <Col span={12}>
<GroupShipSelect />
</Col> */}
<Col span={24}>
<Button
type="primary"
key="primary"
onClick={() => {
handleShareModalVisible(true);
}}
>
<PlusOutlined />{' '}
<FormattedMessage
id="pages.things.share.text"
defaultMessage="Share"
/>
</Button>
{/* Hiển thị bảng thiết bị đã chọn */}
{selectedDevices.length > 0 && (
<Table
size="small"
pagination={false}
dataSource={selectedDevices.map((item, idx) => ({
key: idx,
device: item.name || item,
}))}
columns={[
{ title: 'Thiết bị', dataIndex: 'device', key: 'device' },
]}
scroll={{ y: 240 }}
style={{ marginTop: 12 }}
/>
)}
<FormShareVms
visible={shareModalVisible}
onVisibleChange={handleShareModalVisible}
groups={groups || []}
onSubmit={async (values: { things: SgwModel.SgwThing[] }) => {
setSelectedDevices(values.things || []);
handleShareModalVisible(false);
}}
/>
</Col>
</Row>
</ModalForm>
);
};
export default FormAdd;

View File

@@ -0,0 +1,250 @@
import TreeGroup from '@/components/shared/TreeGroup';
import { PADDING_BLOCK, PADDING_IN_LINE } from '@/constants';
import { apiSearchThings } from '@/services/master/ThingController';
import ProCard from '@ant-design/pro-card';
import type { ProFormInstance } from '@ant-design/pro-form';
import { ModalForm } from '@ant-design/pro-form';
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { useIntl } from '@umijs/max';
import { Input } from 'antd';
import { useEffect, useRef, useState } from 'react';
type Group = {
id: string;
metadata?: Record<string, unknown> & {
code?: string;
province_code?: string;
};
};
export default ({
visible,
onVisibleChange,
onSubmit,
groups,
}: {
visible: boolean;
onVisibleChange: (visible: boolean) => void;
onSubmit: (
values: { things: SgwModel.SgwThing[] } & Record<string, unknown>,
) => Promise<void>;
groups: Group[];
}) => {
const intl = useIntl();
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance<Record<string, unknown>>>();
const [selectedRowsState, setSelectedRows] = useState<SgwModel.SgwThing[]>(
[],
);
const [searchValue, setSearchValue] = useState<string>('');
const [groupCheckedKeys, setGroupCheckedKeys] = useState<string[]>([]);
const [responsive] = useState(false);
// Lấy danh sách group IDs từ groups truyền vào
const accountManagedGroups = Array.isArray(groups)
? groups.map((g: Group) => g.id)
: [];
useEffect(() => {
return () => {
setSearchValue('');
setSelectedRows([]);
};
}, []);
const columns: ProColumns<SgwModel.SgwThing>[] = [
{
title: intl.formatMessage({
id: 'pages.things.name',
defaultMessage: 'Name',
}),
dataIndex: 'name',
render: (dom: React.ReactNode, record: SgwModel.SgwThing) => {
const text = record?.name;
const isDisabled = !!record?.metadata?.ship_id;
return (
<span style={{ color: isDisabled ? '#aaa' : 'inherit' }}>{text}</span>
);
},
},
];
return (
<ModalForm
formRef={formRef}
initialValues={{ things: [] }}
title={intl.formatMessage({
id: 'pages.users.share.title',
defaultMessage: 'Share thing',
})}
width="720px"
visible={visible}
onVisibleChange={onVisibleChange}
onFinish={async (values) => {
// Validate things selected
if (!selectedRowsState || selectedRowsState.length === 0) {
formRef.current?.setFields([
{
name: 'things',
errors: [
intl.formatMessage({
id: 'pages.users.things.required',
defaultMessage: 'Please select things',
}),
],
},
]);
return false;
}
// Add selected things to values
const submitValues = {
...values,
things: selectedRowsState,
};
await onSubmit(submitValues);
}}
>
{/* Danh sách thiết bị */}
<div>
<label style={{ marginBottom: 8, display: 'block', fontWeight: 500 }}>
{intl.formatMessage({
id: 'pages.users.things.list',
defaultMessage: 'List things',
})}
<span style={{ color: '#ff4d4f', marginLeft: 4 }}>*</span>
</label>
<ProCard split="vertical" bordered>
<ProCard
colSpan="240px"
bodyStyle={
responsive
? {
paddingInline: PADDING_IN_LINE,
paddingBlock: PADDING_BLOCK,
}
: undefined
}
>
<TreeGroup
multiple={true}
groupIds={groupCheckedKeys}
allowedGroupIds={accountManagedGroups}
onSelected={(value) => {
setGroupCheckedKeys(
value ? (Array.isArray(value) ? value : [value]) : [],
);
if (actionRef.current) {
actionRef.current.reload();
}
}}
/>
</ProCard>
<ProCard
bodyStyle={
responsive
? {
paddingInline: PADDING_IN_LINE,
paddingBlock: PADDING_BLOCK,
}
: undefined
}
>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
marginBottom: 8,
}}
>
<Input.Search
allowClear
placeholder={intl.formatMessage({
id: 'pages.things.name',
defaultMessage: 'Search by name',
})}
style={{ width: 240 }}
onSearch={(value) => {
setSearchValue(value);
actionRef.current?.reload();
}}
onChange={(e) => {
if (!e.target.value) {
setSearchValue('');
actionRef.current?.reload();
}
}}
/>
</div>
<ProTable
actionRef={actionRef}
columns={columns}
tableLayout="auto"
rowKey="id"
dateFormatter="string"
pagination={{
showQuickJumper: false,
showSizeChanger: false,
pageSize: 5,
}}
request={async (
params: { current?: number; pageSize?: number } = {},
) => {
const { current = 1, pageSize } = params;
const size = pageSize || 5;
const offset = current === 1 ? 0 : (current - 1) * size;
// Build search body
const searchBody: MasterModel.SearchThingPaginationBody = {
offset: offset,
limit: size,
order: 'name',
dir: 'asc',
};
// Add group filter if selected
if (groupCheckedKeys.length > 0 && groupCheckedKeys[0]) {
searchBody.metadata = {
group_id: groupCheckedKeys[0], // Use first selected group
};
}
// Add name filter if search value exists
if (searchValue) {
searchBody.name = searchValue;
}
// Call API
const response = await apiSearchThings(searchBody, 'sgw');
return {
data: response.things || [],
success: true,
total: response.total || 0,
};
}}
search={false}
rowSelection={{
type: 'radio',
selectedRowKeys: selectedRowsState
.map((row) => row.id)
.filter(Boolean) as React.Key[],
onChange: (_: unknown, selectedRows: SgwModel.SgwThing[]) => {
const first = selectedRows?.[0] ? [selectedRows[0]] : [];
setSelectedRows(first as SgwModel.SgwThing[]);
formRef.current?.setFieldsValue({ things: first });
},
getCheckboxProps: (record: SgwModel.SgwThing) => ({
// Disable if already linked to a ship (ship_id present)
disabled: !!record?.metadata?.ship_id,
}),
}}
/>
</ProCard>
</ProCard>
</div>
</ModalForm>
);
};

View File

@@ -0,0 +1,72 @@
import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup';
import { ModalForm, ProFormItem } from '@ant-design/pro-form';
import { useIntl } from '@umijs/max';
import { Alert, Form } from 'antd';
interface ModalTreeSelectGroupProps {
visible: boolean;
onModalVisible: (visible: boolean) => void;
onSubmit: (groupId: string) => Promise<boolean | void>;
}
interface FormValues {
group_id?: string;
}
const ModalTreeSelectGroup: React.FC<ModalTreeSelectGroupProps> = ({
visible,
onSubmit,
onModalVisible,
}) => {
const intl = useIntl();
const [form] = Form.useForm<FormValues>();
return (
<ModalForm<FormValues>
form={form}
title={intl.formatMessage({
id: 'pages.groups.setgroup.title',
defaultMessage: 'Set group',
})}
width="480px"
open={visible} // ⚠️ dùng open, không dùng visible (antd mới)
onOpenChange={onModalVisible} // ⚠️ thay cho onVisibleChange
onFinish={async (values) => {
const groupId = values.group_id;
if (!groupId) {
return false; // không cho submit nếu chưa chọn
}
await onSubmit(groupId);
return true;
}}
>
<Alert
type="warning"
message={intl.formatMessage({
id: 'pages.groups.select.alert',
defaultMessage: 'The thing is only added to the leaf group',
})}
/>
<br />
<ProFormItem
name="group_id"
rules={[{ required: true, message: 'Please select a group' }]}
>
<TreeSelectedGroup
groupIds={form.getFieldValue('group_id')}
onSelected={(value) => {
form.setFieldsValue({
group_id: value as string,
});
}}
/>
</ProFormItem>
</ModalForm>
);
};
export default ModalTreeSelectGroup;

View File

@@ -0,0 +1,490 @@
import { DeleteButton, EditButton } from '@/components/shared/Button';
import { DEFAULT_PAGE_SIZE } from '@/constants';
import {
apiAddShip,
apiDeleteShip,
apiQueryShips,
apiUpdateShip,
} from '@/services/slave/sgw/ShipController';
import { PlusOutlined } from '@ant-design/icons';
import ProCard from '@ant-design/pro-card';
import ProDescriptions from '@ant-design/pro-descriptions';
import { FooterToolbar } from '@ant-design/pro-layout';
import ProTable, { ActionType, ProColumns } from '@ant-design/pro-table';
import { FormattedMessage, useIntl, useModel } from '@umijs/max';
import { Button, Drawer, message, Typography } from 'antd';
import { useEffect, useRef, useState } from 'react';
import EditModal from './components/EditModal';
import FormAdd from './components/FormAdd';
const { Paragraph } = Typography;
type ShipFormValues = Partial<SgwModel.ShipDetail>;
type ShipFormAdd = Partial<SgwModel.ShipCreateParams>;
const ManagerShips: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const { currentUserProfile } = initialState || {};
console.log('Current User Profile:', currentUserProfile);
const [responsive] = useState<boolean>(false);
const [updateModalVisible, handleUpdateModalVisible] =
useState<boolean>(false);
const [createModalVisible, handleCreateModalVisible] =
useState<boolean>(false);
const [showDetail, setShowDetail] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<SgwModel.ShipDetail | null>(
null,
);
const actionRef = useRef<ActionType | null>(null);
const [messageApi, contextHolder] = message.useMessage();
const [selectedRowsState, setSelectedRowsState] = useState<
SgwModel.ShipDetail[]
>([]);
const { shipTypes, getShipTypes } = useModel('slave.sgw.useShipTypes');
const { homeports, getHomeportsByProvinceCode } = useModel(
'slave.sgw.useHomePorts',
);
useEffect(() => {
if (!shipTypes) {
getShipTypes();
}
}, [shipTypes]);
useEffect(() => {
getHomeportsByProvinceCode();
}, [getHomeportsByProvinceCode]);
useEffect(() => {
return () => {
setSelectedRowsState([]);
setCurrentRow(null);
actionRef.current = null;
};
}, []);
const handleAdd = async (fields: ShipFormAdd) => {
const key = 'add_ship';
const {
reg_number,
name, // Changed from shipname to name
thing_id,
ship_type,
ship_length,
ship_power,
ship_group_id,
home_port,
fishing_license_number,
fishing_license_expiry_date,
} = fields;
console.log('Fields received from form:', fields);
if (!thing_id || thing_id.length === 0) {
message.error('Vui lòng chọn một thiết bị để liên kết.');
return false;
}
const newShip = {
name: name, // Use name directly
reg_number: reg_number,
thing_id: thing_id,
ship_type: ship_type,
home_port: home_port,
fishing_license_number: fishing_license_number,
fishing_license_expiry_date: fishing_license_expiry_date,
ship_length: Number(ship_length),
ship_power: Number(ship_power),
ship_group_id: ship_group_id,
};
try {
messageApi.open({
type: 'loading',
content: intl.formatMessage({
id: 'pages.things.creating',
defaultMessage: 'creating...',
}),
duration: 0,
key,
});
const id = await apiAddShip({ ...newShip });
if (id) {
messageApi.open({
type: 'success',
content: intl.formatMessage({
id: 'pages.things.add.success',
defaultMessage: 'Added successfully and will refresh soon',
}),
key,
});
return true;
}
return false;
} catch (error) {
messageApi.open({
type: 'error',
content: intl.formatMessage({
id: 'pages.things.add.failed',
defaultMessage: 'Adding failed, please try again!',
}),
key,
});
return false;
}
};
const handleRemove = async (
selectedRows: SgwModel.ShipDetail[],
): Promise<boolean> => {
const key = 'remove_ship';
if (!selectedRows) return true;
try {
messageApi.open({
type: 'loading',
content: intl.formatMessage({
id: 'pages.ships.deleting',
defaultMessage: 'Deleting...',
}),
duration: 0,
key,
});
const allDelete = selectedRows.map(async (row) => {
if (row.id) await apiDeleteShip(row.id);
});
await Promise.all(allDelete);
messageApi.open({
type: 'success',
content: intl.formatMessage({
id: 'pages.ships.delete.success',
defaultMessage: 'Deleted successfully and will refresh soon',
}),
key,
});
return true;
} catch (error) {
messageApi.open({
type: 'error',
content: intl.formatMessage({
id: 'pages.ships.delete.failed',
defaultMessage: 'Delete failed, please try again!',
}),
key,
});
return false;
}
};
const handleUpdate = async (values: ShipFormValues): Promise<boolean> => {
const key = 'update_ship';
try {
messageApi.open({
type: 'loading',
content: intl.formatMessage({
id: 'pages.ships.updating',
defaultMessage: 'Updating...',
}),
duration: 0,
key,
});
// Fix: loại bỏ ship_group_id nếu là null
const patch = { ...values };
if (patch.ship_group_id === null) delete patch.ship_group_id;
if (!currentRow?.id) throw new Error('Missing ship id');
await apiUpdateShip(currentRow.id, patch as SgwModel.ShipUpdateParams);
messageApi.open({
type: 'success',
content: intl.formatMessage({
id: 'pages.ships.update.success',
defaultMessage: 'Updated successfully and will refresh soon',
}),
key,
});
return true;
} catch (error) {
messageApi.open({
type: 'error',
content: intl.formatMessage({
id: 'pages.ships.update.failed',
defaultMessage: 'Update failed, please try again!',
}),
key,
});
return false;
}
};
const columns: ProColumns<SgwModel.ShipDetail, 'text'>[] = [
{
key: 'reg_number',
title: (
<FormattedMessage
id="pages.ships.reg_number"
defaultMessage="Registration Number"
/>
),
dataIndex: 'reg_number',
render: (_, record) => (
<a
style={{ color: '#1890ff', cursor: 'pointer' }}
onClick={() => {
setCurrentRow(record);
setShowDetail(true);
}}
>
{record?.reg_number}
</a>
),
},
{
key: 'name',
title: (
<FormattedMessage id="pages.ships.name" defaultMessage="Ship Name" />
),
dataIndex: 'name',
render: (dom, record) => (
<Paragraph
style={{
marginBottom: 0,
verticalAlign: 'middle',
display: 'inline-block',
}}
copyable
>
{record?.name}
</Paragraph>
),
},
{
key: 'ship_type',
title: <FormattedMessage id="pages.ships.type" defaultMessage="Type" />,
dataIndex: 'ship_type',
render: (dom, record) => {
const typeObj = Array.isArray(shipTypes)
? shipTypes.find((t) => t.id === record?.ship_type)
: undefined;
return typeObj?.name || record?.ship_type || '-';
},
},
{
key: 'home_port',
title: (
<FormattedMessage
id="pages.ships.home_port"
defaultMessage="Home Port"
/>
),
dataIndex: 'home_port',
hideInSearch: true,
render: (dom, record) => {
const portObj = Array.isArray(homeports)
? homeports.find((p) => p.id === record?.home_port)
: undefined;
return portObj?.name || record?.home_port || '-';
},
},
{
title: (
<FormattedMessage id="pages.ships.option" defaultMessage="Operating" />
),
hideInSearch: true,
render: (dom, record) => (
<EditButton
text={intl.formatMessage({
id: 'pages.ships.edit.text',
defaultMessage: 'Edit',
})}
onClick={() => {
setCurrentRow(record);
handleUpdateModalVisible(true);
}}
/>
),
},
];
// Định nghĩa tạm detailColumns cho ProDescriptions
const detailColumns = [
{ title: 'Tên tàu', dataIndex: 'name' },
{ title: 'Chiều dài tàu', dataIndex: 'ship_length' },
{ title: 'Công suất tàu', dataIndex: 'ship_power' },
{ title: 'Đội tàu', dataIndex: 'group_ship' },
{ title: 'Ảnh tàu', dataIndex: 'reg_number' },
];
return (
<>
{contextHolder}
<ProCard
split={responsive ? 'horizontal' : 'vertical'}
style={{ minHeight: 560 }}
>
<ProCard colSpan={{ xs: 24, sm: 24, md: 24, lg: 24, xl: 24 }}>
<ProTable
actionRef={actionRef}
columns={columns}
tableLayout="auto"
rowKey="id"
search={{ layout: 'vertical', defaultCollapsed: false }}
dateFormatter="string"
rowSelection={{
selectedRowKeys: selectedRowsState
.map((row) => row.id!)
.filter(Boolean),
onChange: (
_: React.Key[],
selectedRows: SgwModel.ShipDetail[],
) => {
setSelectedRowsState(selectedRows);
},
}}
pagination={{ pageSize: DEFAULT_PAGE_SIZE * 2 }}
request={async (params = {}) => {
const {
current = 1,
pageSize,
name,
registration_number,
} = params as {
current?: number;
pageSize?: number;
name?: string;
registration_number?: string;
};
const size = pageSize || DEFAULT_PAGE_SIZE * 2;
const offset = current === 1 ? 0 : (current - 1) * size;
const query: SgwModel.ShipQueryParams = {
offset: offset,
limit: size,
order: 'name',
dir: 'asc',
name,
registration_number,
};
const resp = await apiQueryShips(query);
return {
data: resp.ships || [],
success: true,
total: resp.total || 0,
};
}}
toolBarRender={() => {
return [
<Button
type="primary"
key="primary"
onClick={() => handleCreateModalVisible(true)}
>
<PlusOutlined />{' '}
<FormattedMessage
id="pages.ship.create.text"
defaultMessage="New"
/>
</Button>,
];
}}
/>
</ProCard>
</ProCard>
<FormAdd
visible={createModalVisible}
onVisibleChange={handleCreateModalVisible}
onSubmit={async (values: ShipFormAdd) => {
console.log('index.tsx onSubmit called with values:', values);
const success = await handleAdd(values);
console.log('index.tsx onSubmit - success:', success);
if (success) {
handleCreateModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
return success;
}}
/>
{currentRow && (
<EditModal
visible={updateModalVisible}
values={currentRow}
onVisibleChange={(visible) => {
handleUpdateModalVisible(visible);
if (!visible) setCurrentRow(null);
}}
onFinish={async (values: ShipFormValues) => {
const success = await handleUpdate(values);
if (success) {
handleUpdateModalVisible(false);
setCurrentRow(null);
actionRef.current?.reload();
}
return success;
}}
/>
)}
{selectedRowsState?.length > 0 && (
<FooterToolbar
extra={
<div>
<FormattedMessage
id="pages.ships.chosen"
defaultMessage="Chosen"
/>{' '}
<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
<FormattedMessage id="pages.ships.item" defaultMessage="item" />
</div>
}
>
<DeleteButton
title={intl.formatMessage({
id: 'pages.ships.deletion.title',
defaultMessage: 'Are you sure to delete these selected ships?',
})}
text={intl.formatMessage({
id: 'pages.ships.deletion.text',
defaultMessage: 'Batch deletion',
})}
onOk={async () => {
const success = await handleRemove(selectedRowsState);
if (success) {
setSelectedRowsState([]);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
/>
</FooterToolbar>
)}
<Drawer
width={600}
visible={showDetail}
title={
<FormattedMessage
id="pages.ships.detail"
defaultMessage="Ship Detail"
/>
}
onClose={() => {
setShowDetail(false);
setCurrentRow(null);
}}
>
<ProDescriptions<SgwModel.ShipDetail>
column={1}
bordered
request={async () => ({
data: currentRow ? [currentRow] : [],
success: true,
})}
params={{ id: currentRow?.id }}
columns={detailColumns}
/>
</Drawer>
</>
);
};
export default ManagerShips;