555 lines
19 KiB
TypeScript
555 lines
19 KiB
TypeScript
import { apiQueryShips } from '@/services/slave/sgw/ShipController';
|
||
import {
|
||
apiCreateTrip,
|
||
apiQueryLastTrips,
|
||
} from '@/services/slave/sgw/TripController';
|
||
import { PlusOutlined } from '@ant-design/icons';
|
||
import type {
|
||
ProFormInstance,
|
||
SubmitterProps,
|
||
} from '@ant-design/pro-components';
|
||
import {
|
||
ModalForm,
|
||
ProForm,
|
||
ProFormDatePicker,
|
||
ProFormList,
|
||
ProFormSelect,
|
||
ProFormText,
|
||
StepsForm,
|
||
} from '@ant-design/pro-components';
|
||
import { FormattedMessage, useModel } from '@umijs/max';
|
||
import { Button, message, Table, Typography } from 'antd';
|
||
import dayjs from 'dayjs';
|
||
import utc from 'dayjs/plugin/utc';
|
||
import { useEffect, useRef, useState } from 'react';
|
||
|
||
dayjs.extend(utc);
|
||
|
||
interface CreateTripProps {
|
||
onSuccess?: (ok: boolean) => void;
|
||
ship_id?: string;
|
||
thing_id?: string;
|
||
}
|
||
|
||
interface TripFormValues {
|
||
name: string;
|
||
departure_time: any; // dayjs object or string
|
||
departure_port_id: number;
|
||
arrival_time?: any; // dayjs object or string
|
||
arrival_port_id?: number;
|
||
fishing_ground_codes?: number[];
|
||
fishing_gear?: SgwModel.FishingGear[];
|
||
trip_cost?: SgwModel.TripCost[];
|
||
ship_id?: string;
|
||
}
|
||
|
||
const CreateTrip: React.FC<CreateTripProps> = ({ onSuccess, thing_id }) => {
|
||
const [visible, setVisible] = useState(false);
|
||
const [selectedThing, setSelectedThing] = useState<string | undefined>();
|
||
const [ships, setShips] = useState<SgwModel.ShipDetail[]>([]);
|
||
const [selectedShip, setSelectedShip] = useState<SgwModel.ShipDetail | null>(
|
||
null,
|
||
);
|
||
const [loading, setLoading] = useState(false);
|
||
const [lastTrip, setLastTrip] = useState<SgwModel.Trip | null>(null);
|
||
const [initialFormValues, setInitialFormValues] = useState<
|
||
Partial<TripFormValues>
|
||
>({});
|
||
const formRef = useRef<ProFormInstance<TripFormValues>>(null);
|
||
const { homeports, getHomeportsByProvinceCode } = useModel(
|
||
'slave.sgw.useHomePorts',
|
||
);
|
||
|
||
useEffect(() => {
|
||
getHomeportsByProvinceCode();
|
||
}, [getHomeportsByProvinceCode]);
|
||
|
||
useEffect(() => {
|
||
setSelectedThing(thing_id);
|
||
}, [thing_id]);
|
||
|
||
// Load danh sách tàu
|
||
const loadShips = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const resp = await apiQueryShips({ offset: 0, limit: 100 });
|
||
setShips(resp.ships || []);
|
||
} catch (error) {
|
||
message.error('Không thể tải danh sách tàu');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (visible) {
|
||
loadShips();
|
||
}
|
||
}, [visible]);
|
||
|
||
// Auto-fill form when initialFormValues changes
|
||
useEffect(() => {
|
||
if (formRef.current && Object.keys(initialFormValues).length > 0) {
|
||
console.log('<27> InitialFormValues:', initialFormValues);
|
||
// Fill form after a short delay to ensure all steps are ready
|
||
setTimeout(() => {
|
||
if (formRef.current) {
|
||
console.log('<27> Filling form with values...');
|
||
formRef.current.setFieldsValue(initialFormValues);
|
||
}
|
||
}, 100);
|
||
}
|
||
}, [initialFormValues]);
|
||
|
||
// Load last trip when ship is selected
|
||
const loadLastTrip = async (thingId: string) => {
|
||
try {
|
||
setLoading(true);
|
||
const trip = await apiQueryLastTrips(thingId);
|
||
setLastTrip(trip);
|
||
// Prepare form values with last trip data
|
||
if (trip) {
|
||
console.log('📦 Raw trip data:', trip);
|
||
const formValues = {
|
||
name: trip.name,
|
||
departure_time: dayjs(trip.departure_time),
|
||
arrival_time: dayjs(trip.arrival_time),
|
||
departure_port_id: trip.departure_port_id,
|
||
arrival_port_id: trip.arrival_port_id,
|
||
fishing_ground_codes: trip.fishing_ground_codes || [],
|
||
fishing_gear: trip.fishing_gears || [],
|
||
trip_cost: trip.trip_cost || [],
|
||
};
|
||
console.log('📋 Prepared form values:', formValues);
|
||
setInitialFormValues(formValues);
|
||
|
||
message.success(
|
||
`Đã tải ${trip.fishing_gears?.length || 0} ngư cụ và ${
|
||
trip.trip_cost?.length || 0
|
||
} chi phí từ chuyến trước`,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading last trip:', error);
|
||
setLastTrip(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async (values: TripFormValues) => {
|
||
try {
|
||
if (!selectedShip) {
|
||
message.error('Vui lòng chọn tàu!');
|
||
return false;
|
||
}
|
||
if (!values.departure_port_id) {
|
||
message.error('Vui lòng chọn cảng khởi hành!');
|
||
return false;
|
||
}
|
||
if (!values.arrival_port_id) {
|
||
message.error('Vui lòng chọn cảng cập bến!');
|
||
return false;
|
||
}
|
||
const params: SgwModel.TripCreateParams = {
|
||
name: values.name,
|
||
departure_time: dayjs(values.departure_time as string | Date)
|
||
.utc()
|
||
.format(),
|
||
departure_port_id: values.departure_port_id,
|
||
arrival_time: dayjs(values.arrival_time as string | Date)
|
||
.utc()
|
||
.format(),
|
||
arrival_port_id: values.arrival_port_id,
|
||
fishing_ground_codes: Array.isArray(values.fishing_ground_codes)
|
||
? (values.fishing_ground_codes as (string | number)[])
|
||
.map((code: string | number) => Number(code))
|
||
.filter((n: number) => !isNaN(n))
|
||
: [],
|
||
fishing_gears: values.fishing_gear,
|
||
trip_cost: values.trip_cost || [],
|
||
};
|
||
|
||
const thingIdToUse = selectedShip.thing_id || selectedThing;
|
||
if (!thingIdToUse) {
|
||
message.error('Không tìm thấy thiết bị của tàu!');
|
||
return false;
|
||
}
|
||
|
||
const resp = await apiCreateTrip(thingIdToUse, params);
|
||
console.log('createTrip resp', resp);
|
||
if (resp && !resp?.trip_status) {
|
||
message.success('Tạo chuyến đi thành công!');
|
||
onSuccess?.(true);
|
||
setVisible(false);
|
||
setSelectedShip(null);
|
||
return true;
|
||
} else {
|
||
message.error(`Tạo chuyến đi thất bại!`);
|
||
return false;
|
||
}
|
||
} catch (err) {
|
||
console.error('Lỗi khi tạo chuyến đi:', err);
|
||
message.error('Tạo chuyến đi thất bại!');
|
||
return false;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<Button type="primary" key="primary" onClick={() => setVisible(true)}>
|
||
<PlusOutlined />{' '}
|
||
<FormattedMessage
|
||
id="pages.things.createTrip.text"
|
||
defaultMessage="Tạo chuyến đi"
|
||
/>
|
||
</Button>
|
||
|
||
<ModalForm
|
||
title="Tạo chuyến đi mới"
|
||
open={visible}
|
||
onOpenChange={(open) => {
|
||
setVisible(open);
|
||
if (!open) {
|
||
setSelectedShip(null);
|
||
}
|
||
}}
|
||
onFinish={handleSubmit}
|
||
modalProps={{
|
||
width: 900,
|
||
}}
|
||
layout="horizontal"
|
||
submitter={false}
|
||
>
|
||
<StepsForm<SgwModel.TripCreateParams>
|
||
stepsProps={{ size: 'small' }}
|
||
onFinish={(values) => handleSubmit(values as TripFormValues)}
|
||
submitter={{
|
||
render: (_: SubmitterProps, dom) => dom,
|
||
}}
|
||
formProps={{
|
||
layout: 'horizontal',
|
||
}}
|
||
formRef={formRef}
|
||
onCurrentChange={(current) => {
|
||
if (!lastTrip || !formRef.current) return;
|
||
|
||
if (current === 1) {
|
||
formRef.current.setFieldsValue({
|
||
name: lastTrip.name,
|
||
departure_time: dayjs(lastTrip.departure_time),
|
||
arrival_time: dayjs(lastTrip.arrival_time),
|
||
departure_port_id: lastTrip.departure_port_id,
|
||
arrival_port_id: lastTrip.arrival_port_id,
|
||
fishing_ground_codes: lastTrip.fishing_ground_codes || [],
|
||
});
|
||
}
|
||
|
||
if (current === 2) {
|
||
formRef.current.setFieldsValue({
|
||
fishing_gear: lastTrip.fishing_gears || [],
|
||
});
|
||
}
|
||
|
||
if (current === 3) {
|
||
formRef.current.setFieldsValue({
|
||
trip_cost: lastTrip.trip_cost || [],
|
||
});
|
||
}
|
||
}}
|
||
>
|
||
{/* Step 1: Chọn tàu */}
|
||
<StepsForm.StepForm
|
||
title="Chọn tàu"
|
||
preserve
|
||
onFinish={async () => {
|
||
if (!selectedShip) {
|
||
message.error('Vui lòng chọn một tàu!');
|
||
return false;
|
||
}
|
||
|
||
// Load last trip after selecting ship
|
||
const thingIdToUse = selectedShip.thing_id || selectedThing;
|
||
if (thingIdToUse) {
|
||
await loadLastTrip(thingIdToUse);
|
||
}
|
||
|
||
return true;
|
||
}}
|
||
>
|
||
<Typography.Title level={5}>Danh sách tàu</Typography.Title>
|
||
<Table
|
||
loading={loading}
|
||
dataSource={ships}
|
||
rowKey="id"
|
||
pagination={{ pageSize: 5 }}
|
||
rowSelection={{
|
||
type: 'radio',
|
||
selectedRowKeys: selectedShip ? [selectedShip.id!] : [],
|
||
onChange: (
|
||
_: React.Key[],
|
||
selectedRows: SgwModel.ShipDetail[],
|
||
) => {
|
||
setSelectedShip(selectedRows[0] || null);
|
||
},
|
||
}}
|
||
columns={[
|
||
{
|
||
title: 'Số đăng ký',
|
||
dataIndex: 'reg_number',
|
||
key: 'reg_number',
|
||
},
|
||
{ title: 'Tên tàu', dataIndex: 'name', key: 'name' },
|
||
{ title: 'Loại tàu', dataIndex: 'ship_type', key: 'ship_type' },
|
||
{ title: 'Cảng nhà', dataIndex: 'home_port', key: 'home_port' },
|
||
]}
|
||
/>
|
||
</StepsForm.StepForm>
|
||
|
||
{/* Step 2: Thông tin chuyến đi */}
|
||
<StepsForm.StepForm
|
||
title="Thông tin chuyến đi"
|
||
name="trip_info"
|
||
preserve
|
||
>
|
||
<ProFormText
|
||
name="name"
|
||
label="Tên chuyến đi"
|
||
rules={[
|
||
{ required: true, message: 'Vui lòng nhập tên chuyến đi' },
|
||
]}
|
||
/>
|
||
<ProForm.Group title="Thời gian chuyến đi">
|
||
<ProFormDatePicker
|
||
name="departure_time"
|
||
showTime
|
||
label="Thời gian bắt đầu"
|
||
width="md"
|
||
rules={[
|
||
{
|
||
required: true,
|
||
message: 'Vui lòng chọn thời gian bắt đầu',
|
||
},
|
||
{
|
||
validator: (_: unknown, value: unknown) => {
|
||
if (!value) return Promise.resolve();
|
||
const todayDate = dayjs().startOf('day');
|
||
const selectedDate = dayjs(value as string).startOf(
|
||
'day',
|
||
);
|
||
return selectedDate.isBefore(todayDate)
|
||
? Promise.reject(
|
||
new Error('Ngày bắt đầu không được trước hôm nay'),
|
||
)
|
||
: Promise.resolve();
|
||
},
|
||
},
|
||
]}
|
||
/>
|
||
<ProFormDatePicker
|
||
name="arrival_time"
|
||
showTime
|
||
label="Thời gian kết thúc"
|
||
width="md"
|
||
rules={[
|
||
{
|
||
required: true,
|
||
message: 'Vui lòng chọn thời gian kết thúc',
|
||
},
|
||
]}
|
||
/>
|
||
</ProForm.Group>
|
||
<ProForm.Group title="Cảng đi / Cảng đến">
|
||
<ProFormSelect
|
||
name="departure_port_id"
|
||
label="Cảng khởi hành"
|
||
width="md"
|
||
placeholder="Chọn cảng khởi hành"
|
||
options={
|
||
Array.isArray(homeports)
|
||
? homeports.map((p) => ({ label: p.name, value: p.id }))
|
||
: []
|
||
}
|
||
rules={[
|
||
{ required: true, message: 'Vui lòng chọn cảng khởi hành' },
|
||
]}
|
||
showSearch
|
||
/>
|
||
<ProFormSelect
|
||
name="arrival_port_id"
|
||
label="Cảng cập bến"
|
||
width="md"
|
||
placeholder="Chọn cảng cập bến"
|
||
options={
|
||
Array.isArray(homeports)
|
||
? homeports.map((p) => ({ label: p.name, value: p.id }))
|
||
: []
|
||
}
|
||
rules={[
|
||
{ required: true, message: 'Vui lòng chọn cảng cập bến' },
|
||
]}
|
||
showSearch
|
||
/>
|
||
</ProForm.Group>
|
||
<ProFormSelect
|
||
name="fishing_ground_codes"
|
||
label="Ô khai thác"
|
||
fieldProps={{
|
||
mode: 'tags',
|
||
style: { width: '100%' },
|
||
placeholder: 'Nhập các ô ngư trường, nhấn Enter để thêm',
|
||
}}
|
||
rules={[
|
||
{
|
||
required: true,
|
||
message: 'Vui lòng nhập ít nhất một ô khai thác!',
|
||
},
|
||
]}
|
||
options={[]}
|
||
/>
|
||
</StepsForm.StepForm>
|
||
|
||
{/* Step 3: Danh sách ngư cụ */}
|
||
<StepsForm.StepForm
|
||
title="Danh sách ngư cụ"
|
||
name="fishing_gear_step"
|
||
preserve
|
||
>
|
||
<ProForm.Group title="Danh sách ngư cụ">
|
||
<ProFormList
|
||
name="fishing_gear"
|
||
creatorButtonProps={{ creatorButtonText: 'Thêm ngư cụ' }}
|
||
copyIconProps={{ tooltipText: 'Sao chép' }}
|
||
deleteIconProps={{ tooltipText: 'Xóa' }}
|
||
>
|
||
<div style={{ marginBottom: 16 }}>
|
||
<div style={{ display: 'flex', marginBottom: 4 }}>
|
||
<Typography.Text
|
||
strong
|
||
style={{ width: 200, marginRight: 8 }}
|
||
>
|
||
Tên ngư cụ
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{ width: 200 }}>
|
||
Số lượng
|
||
</Typography.Text>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex' }}>
|
||
<ProFormText
|
||
name="name"
|
||
fieldProps={{ style: { width: 200, marginRight: 8 } }}
|
||
/>
|
||
<ProFormText
|
||
name="number"
|
||
fieldProps={{ style: { width: 200 } }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</ProFormList>
|
||
</ProForm.Group>
|
||
</StepsForm.StepForm>
|
||
|
||
{/* Step 4: Chi phí nguyên liệu */}
|
||
<StepsForm.StepForm
|
||
title="Chi phí nguyên liệu"
|
||
name="trip_cost_step"
|
||
preserve
|
||
>
|
||
<ProForm.Group title="Chi phí nguyên liệu">
|
||
<ProFormList
|
||
name="trip_cost"
|
||
creatorButtonProps={{ creatorButtonText: 'Thêm nguyên liệu' }}
|
||
copyIconProps={{ tooltipText: 'Sao chép' }}
|
||
deleteIconProps={{ tooltipText: 'Xóa' }}
|
||
>
|
||
<div style={{ marginBottom: 16 }}>
|
||
<div style={{ display: 'flex', marginBottom: 4 }}>
|
||
<Typography.Text
|
||
strong
|
||
style={{ width: 120, marginRight: 8 }}
|
||
>
|
||
Loại
|
||
</Typography.Text>
|
||
<Typography.Text
|
||
strong
|
||
style={{ width: 100, marginRight: 8 }}
|
||
>
|
||
Số lượng
|
||
</Typography.Text>
|
||
<Typography.Text
|
||
strong
|
||
style={{ width: 100, marginRight: 8 }}
|
||
>
|
||
Đơn vị
|
||
</Typography.Text>
|
||
<Typography.Text
|
||
strong
|
||
style={{ width: 100, marginRight: 8 }}
|
||
>
|
||
Chi phí/đơn vị
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{ width: 100 }}>
|
||
Tổng chi phí
|
||
</Typography.Text>
|
||
</div>
|
||
<div style={{ display: 'flex' }}>
|
||
<ProFormSelect
|
||
name="type"
|
||
placeholder="Chọn loại"
|
||
fieldProps={{ style: { width: 120, marginRight: 8 } }}
|
||
options={[
|
||
{ label: 'Nhiên liệu', value: 'fuel' },
|
||
{ label: 'Lương thuyền viên', value: 'crew_salary' },
|
||
{ label: 'Lương thực', value: 'food' },
|
||
{ label: 'Muối đá', value: 'salt_ice' },
|
||
]}
|
||
rules={[
|
||
{ required: true, message: 'Vui lòng chọn loại' },
|
||
]}
|
||
/>
|
||
<ProFormText
|
||
name="amount"
|
||
fieldProps={{
|
||
style: { width: 100, marginRight: 8 },
|
||
placeholder: 'Số lượng',
|
||
}}
|
||
rules={[{ required: true, message: 'Nhập số lượng' }]}
|
||
/>
|
||
<ProFormText
|
||
name="unit"
|
||
fieldProps={{
|
||
style: { width: 100, marginRight: 8 },
|
||
placeholder: 'Đơn vị',
|
||
}}
|
||
rules={[{ required: true, message: 'Nhập đơn vị' }]}
|
||
/>
|
||
<ProFormText
|
||
name="cost_per_unit"
|
||
fieldProps={{
|
||
style: { width: 100, marginRight: 8 },
|
||
placeholder: 'Chi phí',
|
||
}}
|
||
rules={[{ required: true, message: 'Nhập chi phí' }]}
|
||
/>
|
||
<ProFormText
|
||
name="total_cost"
|
||
fieldProps={{
|
||
style: { width: 100 },
|
||
placeholder: 'Tổng',
|
||
}}
|
||
rules={[{ required: true, message: 'Nhập tổng' }]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</ProFormList>
|
||
</ProForm.Group>
|
||
</StepsForm.StepForm>
|
||
</StepsForm>
|
||
</ModalForm>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default CreateTrip;
|