feat(sgw): Add new services and utilities for ship, trip, and photo management
This commit is contained in:
38
src/pages/Slave/SGW/Ship/components/ButtonSelectGroup.tsx
Normal file
38
src/pages/Slave/SGW/Ship/components/ButtonSelectGroup.tsx
Normal 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;
|
||||
103
src/pages/Slave/SGW/Ship/components/EditModal.tsx
Normal file
103
src/pages/Slave/SGW/Ship/components/EditModal.tsx
Normal 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;
|
||||
243
src/pages/Slave/SGW/Ship/components/FormAdd.tsx
Normal file
243
src/pages/Slave/SGW/Ship/components/FormAdd.tsx
Normal 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;
|
||||
250
src/pages/Slave/SGW/Ship/components/FormShareVms.tsx
Normal file
250
src/pages/Slave/SGW/Ship/components/FormShareVms.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
72
src/pages/Slave/SGW/Ship/components/ModalTreeSelectGroup.tsx
Normal file
72
src/pages/Slave/SGW/Ship/components/ModalTreeSelectGroup.tsx
Normal 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;
|
||||
490
src/pages/Slave/SGW/Ship/index.tsx
Normal file
490
src/pages/Slave/SGW/Ship/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user