feat(core): sgw-device-ui
This commit is contained in:
103
src/pages/Trip/components/AlarmTable.tsx
Normal file
103
src/pages/Trip/components/AlarmTable.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
DATE_TIME_FORMAT,
|
||||
DURATION_POLLING_PRESENTATIONS,
|
||||
STATUS_DANGEROUS,
|
||||
STATUS_NORMAL,
|
||||
STATUS_SOS,
|
||||
STATUS_WARNING,
|
||||
} from '@/utils/format';
|
||||
import { ProList, ProListMetas } from '@ant-design/pro-components';
|
||||
import { Badge, theme, Typography } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const getBadgeLevel = (level: number) => {
|
||||
switch (level) {
|
||||
case STATUS_NORMAL:
|
||||
return <Badge status="success" />;
|
||||
case STATUS_WARNING:
|
||||
return <Badge status="warning" />;
|
||||
case STATUS_DANGEROUS:
|
||||
return <Badge status="error" />;
|
||||
case STATUS_SOS:
|
||||
return <Badge status="error" />;
|
||||
default:
|
||||
return <Badge status="default" />;
|
||||
}
|
||||
};
|
||||
|
||||
interface AlarmTableProps {
|
||||
alarmList?: API.Alarm[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const AlarmTable: React.FC<AlarmTableProps> = ({
|
||||
alarmList,
|
||||
isLoading,
|
||||
}) => {
|
||||
// const intl = useIntl();
|
||||
const actionRef = useRef();
|
||||
const { token } = theme.useToken();
|
||||
// const [messageApi, contextHolder] = message.useMessage();
|
||||
// const [confirmModalVisible, handleConfirmModalVisible] = useState(false);
|
||||
// const [currentRow, setCurrentRow] = useState({});
|
||||
|
||||
const getTitleAlarmColor = (level: number) => {
|
||||
switch (level) {
|
||||
case STATUS_NORMAL:
|
||||
return token.colorSuccess;
|
||||
case STATUS_WARNING:
|
||||
return token.colorWarning;
|
||||
case STATUS_DANGEROUS:
|
||||
return token.colorError;
|
||||
case STATUS_SOS:
|
||||
return token.colorError;
|
||||
default:
|
||||
return token.colorText;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProListMetas<API.Alarm> = {
|
||||
title: {
|
||||
dataIndex: 'name',
|
||||
render(_, item) {
|
||||
return (
|
||||
<Typography.Text style={{ color: getTitleAlarmColor(item.level) }}>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
avatar: {
|
||||
render: (_, item) => getBadgeLevel(item.level),
|
||||
},
|
||||
description: {
|
||||
dataIndex: 'time',
|
||||
render: (_, item) => {
|
||||
return (
|
||||
<>
|
||||
<div>{moment.unix(item?.t).format(DATE_TIME_FORMAT)}</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProList<API.Alarm>
|
||||
// bordered
|
||||
actionRef={actionRef}
|
||||
metas={columns}
|
||||
polling={DURATION_POLLING_PRESENTATIONS}
|
||||
loading={isLoading}
|
||||
dataSource={alarmList}
|
||||
search={false}
|
||||
dateFormatter="string"
|
||||
cardProps={{
|
||||
bodyStyle: { paddingInline: 16, paddingBlock: 8 },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
46
src/pages/Trip/components/BadgeTripStatus.tsx
Normal file
46
src/pages/Trip/components/BadgeTripStatus.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { BadgeProps } from 'antd';
|
||||
import { Badge } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface BadgeTripStatusProps {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
const BadgeTripStatus: React.FC<BadgeTripStatusProps> = ({ status }) => {
|
||||
// Khai báo kiểu cho map
|
||||
const statusBadgeMap: Record<number, BadgeProps> = {
|
||||
0: { status: 'default' }, // Đã khởi tạo
|
||||
1: { status: 'processing' }, // Chờ duyệt
|
||||
2: { status: 'success' }, // Đã duyệt
|
||||
3: { status: 'success' }, // Đang hoạt động
|
||||
4: { status: 'success' }, // Hoàn thành
|
||||
5: { status: 'error' }, // Đã huỷ
|
||||
};
|
||||
|
||||
const getBadgeProps = (status: number | undefined) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 'Chưa được phê duyệt';
|
||||
case 1:
|
||||
return 'Đang chờ duyệt';
|
||||
case 2:
|
||||
return 'Đã duyệt';
|
||||
case 3:
|
||||
return 'Đang hoạt động';
|
||||
case 4:
|
||||
return 'Đã hoàn thành';
|
||||
case 5:
|
||||
return 'Đã huỷ';
|
||||
default:
|
||||
return 'Trạng thái không xác định';
|
||||
}
|
||||
};
|
||||
|
||||
const badgeProps: BadgeProps = statusBadgeMap[status ?? -1] || {
|
||||
status: 'default',
|
||||
};
|
||||
|
||||
return <Badge {...badgeProps} text={getBadgeProps(status)} />;
|
||||
};
|
||||
|
||||
export default BadgeTripStatus;
|
||||
38
src/pages/Trip/components/CancelTrip.tsx
Normal file
38
src/pages/Trip/components/CancelTrip.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ModalForm, ProFormTextArea } from '@ant-design/pro-components';
|
||||
import { Button, Form } from 'antd';
|
||||
interface CancelTripProps {
|
||||
onFinished?: (note: string) => void;
|
||||
}
|
||||
const CancelTrip: React.FC<CancelTripProps> = ({ onFinished }) => {
|
||||
const [form] = Form.useForm<{ note: string }>();
|
||||
return (
|
||||
<ModalForm
|
||||
title="Xác nhận huỷ chuyến đi"
|
||||
form={form}
|
||||
modalProps={{
|
||||
destroyOnHidden: true,
|
||||
onCancel: () => console.log('run'),
|
||||
}}
|
||||
trigger={
|
||||
<Button color="danger" variant="solid">
|
||||
Huỷ chuyến đi
|
||||
</Button>
|
||||
}
|
||||
onFinish={async (values) => {
|
||||
onFinished?.(values.note);
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ProFormTextArea
|
||||
name="note"
|
||||
label="Lý do: "
|
||||
placeholder={'Nhập lý do huỷ chuyến đi...'}
|
||||
rules={[
|
||||
{ required: true, message: 'Vui lòng nhập lý do huỷ chuyến đi' },
|
||||
]}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancelTrip;
|
||||
133
src/pages/Trip/components/CreateNewHaulOrTrip.tsx
Normal file
133
src/pages/Trip/components/CreateNewHaulOrTrip.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { STATUS } from '@/constants/enums';
|
||||
import { getGPS } from '@/services/controller/DeviceController';
|
||||
import {
|
||||
startNewHaul,
|
||||
updateTripState,
|
||||
} from '@/services/controller/TripController';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { Button, message, theme } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import CreateOrUpdateFishingLog from './CreateOrUpdateFishingLog';
|
||||
|
||||
interface CreateNewHaulOrTripProps {
|
||||
trips?: API.Trip;
|
||||
onCallBack?: (success: string) => void;
|
||||
}
|
||||
|
||||
const CreateNewHaulOrTrip: React.FC<CreateNewHaulOrTripProps> = ({
|
||||
trips,
|
||||
onCallBack,
|
||||
}) => {
|
||||
const [isFinishHaulModalOpen, setIsFinishHaulModalOpen] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
const { getApi } = useModel('getTrip');
|
||||
const checkHaulFinished = () => {
|
||||
return trips?.fishing_logs?.some((h) => h.status === 0);
|
||||
};
|
||||
|
||||
const createNewHaul = async () => {
|
||||
if (trips?.fishing_logs?.some((f) => f.status === 0)) {
|
||||
message.warning(
|
||||
'Vui lòng kết thúc mẻ lưới hiện tại trước khi bắt đầu mẻ mới',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const gpsData = await getGPS();
|
||||
console.log('GPS Data:', gpsData);
|
||||
|
||||
const body: API.NewFishingLogRequest = {
|
||||
trip_id: trips?.id || '',
|
||||
start_at: new Date(),
|
||||
start_lat: gpsData.lat,
|
||||
start_lon: gpsData.lon,
|
||||
weather_description: 'Nắng đẹp',
|
||||
};
|
||||
|
||||
const resp = await startNewHaul(body);
|
||||
onCallBack?.(STATUS.CREATE_FISHING_LOG_SUCCESS);
|
||||
getApi();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
onCallBack?.(STATUS.CREATE_FISHING_LOG_FAIL);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTrip = async (state: number, note?: string) => {
|
||||
if (trips?.trip_status !== 2) {
|
||||
message.warning('Chuyến đi đã được bắt đầu hoặc hoàn thành.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await updateTripState({ status: state, note: note || '' });
|
||||
onCallBack?.(STATUS.START_TRIP_SUCCESS);
|
||||
getApi();
|
||||
} catch (error) {
|
||||
console.error('Error stating trip :', error);
|
||||
onCallBack?.(STATUS.START_TRIP_FAIL);
|
||||
}
|
||||
};
|
||||
|
||||
// Không render gì nếu trip đã hoàn thành hoặc bị hủy
|
||||
if (trips?.trip_status === 4 || trips?.trip_status === 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{trips?.trip_status === 2 ? (
|
||||
<Button
|
||||
color="green"
|
||||
variant="solid"
|
||||
onClick={async () => handleStartTrip(3)}
|
||||
>
|
||||
Bắt đầu chuyến đi
|
||||
</Button>
|
||||
) : checkHaulFinished() ? (
|
||||
<Button
|
||||
key="button"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={async () => {
|
||||
setIsFinishHaulModalOpen(true);
|
||||
}}
|
||||
color="geekblue"
|
||||
variant="solid"
|
||||
>
|
||||
Kết thúc mẻ lưới
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key="button"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={async () => {
|
||||
createNewHaul();
|
||||
}}
|
||||
color="cyan"
|
||||
variant="solid"
|
||||
>
|
||||
Bắt đầu mẻ lưới
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CreateOrUpdateFishingLog
|
||||
trip={trips!}
|
||||
isFinished={false}
|
||||
fishingLogs={undefined}
|
||||
isOpen={isFinishHaulModalOpen}
|
||||
onOpenChange={setIsFinishHaulModalOpen}
|
||||
onFinished={(success) => {
|
||||
if (success) {
|
||||
onCallBack?.(STATUS.UPDATE_FISHING_LOG_SUCCESS);
|
||||
getApi();
|
||||
} else {
|
||||
onCallBack?.(STATUS.UPDATE_FISHING_LOG_FAIL);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNewHaulOrTrip;
|
||||
359
src/pages/Trip/components/CreateOrUpdateFishingLog.tsx
Normal file
359
src/pages/Trip/components/CreateOrUpdateFishingLog.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import { getGPS } from '@/services/controller/DeviceController';
|
||||
import {
|
||||
getFishSpecies,
|
||||
updateFishingLogs,
|
||||
} from '@/services/controller/TripController';
|
||||
import { getColorByRarityLevel, getRarityById } from '@/utils/fishRarity';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { EditableProTable, ProColumns } from '@ant-design/pro-components';
|
||||
import { Button, Flex, message, Modal, Tag, Tooltip, Typography } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface CreateOrUpdateFishingLogProps {
|
||||
trip: API.Trip;
|
||||
fishingLogs?: API.FishingLog;
|
||||
isFinished: boolean;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onFinished?: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface FishingLogInfoWithKey extends API.FishingLogInfo {
|
||||
key: React.Key;
|
||||
}
|
||||
|
||||
const CreateOrUpdateFishingLog: React.FC<CreateOrUpdateFishingLogProps> = ({
|
||||
trip,
|
||||
fishingLogs,
|
||||
isFinished,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onFinished,
|
||||
}) => {
|
||||
const [dataSource, setDataSource] = useState<
|
||||
readonly FishingLogInfoWithKey[]
|
||||
>([]);
|
||||
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
|
||||
const [fishDatas, setFishDatas] = useState<API.FishSpeciesResponse[]>([]);
|
||||
useEffect(() => {
|
||||
getAllFish();
|
||||
if (isOpen) {
|
||||
console.log('Modal opened with fishingLogs:', fishingLogs);
|
||||
|
||||
if (fishingLogs?.info && fishingLogs.info.length > 0) {
|
||||
const dataWithKeys: FishingLogInfoWithKey[] = fishingLogs.info.map(
|
||||
(item, index) => ({
|
||||
...item,
|
||||
key: index,
|
||||
}),
|
||||
);
|
||||
setDataSource(dataWithKeys);
|
||||
setEditableRowKeys(dataWithKeys.map((item) => item.key));
|
||||
} else {
|
||||
// Nếu không có info thì reset table
|
||||
setDataSource([]);
|
||||
setEditableRowKeys([]);
|
||||
}
|
||||
}
|
||||
}, [isOpen, fishingLogs]);
|
||||
|
||||
const getAllFish = async () => {
|
||||
try {
|
||||
const resp = await getFishSpecies();
|
||||
setFishDatas(resp);
|
||||
console.log('Fetched fish species:', resp);
|
||||
} catch (error) {
|
||||
console.error('Error fetching fish species:', error);
|
||||
}
|
||||
};
|
||||
const columns: ProColumns<FishingLogInfoWithKey>[] = [
|
||||
{
|
||||
title: 'Tên cá',
|
||||
dataIndex: 'fish_species_id',
|
||||
valueType: 'select',
|
||||
fieldProps: {
|
||||
showSearch: true,
|
||||
options: fishDatas.map((f) => ({
|
||||
label: f.name,
|
||||
value: f.id,
|
||||
data: JSON.stringify(f),
|
||||
})),
|
||||
optionRender: (option: any) => {
|
||||
const fish: API.FishSpeciesResponse = JSON.parse(option.data.data);
|
||||
const fishRarity = getRarityById(fish.rarity_level || 1);
|
||||
return (
|
||||
<Flex align="center" gap={8}>
|
||||
<Typography.Text>{fish.name}</Typography.Text>
|
||||
<Tooltip title={fishRarity?.rarityDescription || ''}>
|
||||
<Tag color={getColorByRarityLevel(fish.rarity_level || 1)}>
|
||||
{fishRarity?.rarityLabel}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
},
|
||||
formItemProps: {
|
||||
rules: [{ required: true, message: 'Vui lòng chọn tên cá' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Kích thước (cm)',
|
||||
dataIndex: 'fish_size',
|
||||
valueType: 'digit',
|
||||
width: '15%',
|
||||
formItemProps: {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Vui lòng nhập kích thước',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Số lượng',
|
||||
dataIndex: 'catch_number',
|
||||
valueType: 'digit',
|
||||
width: '15%',
|
||||
formItemProps: {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Vui lòng nhập số lượng',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Đơn vị',
|
||||
dataIndex: 'catch_unit',
|
||||
valueType: 'select',
|
||||
width: '15%',
|
||||
valueEnum: {
|
||||
kg: 'kg',
|
||||
con: 'con',
|
||||
tấn: 'tấn',
|
||||
},
|
||||
formItemProps: {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Vui lòng chọn đơn vị',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Thao tác',
|
||||
valueType: 'option',
|
||||
width: 120,
|
||||
render: () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function createOrCreateOrUpdateFishingLog(
|
||||
fishingLog: FishingLogInfoWithKey[],
|
||||
) {
|
||||
console.log('Is finished:', isFinished);
|
||||
console.log('Trip:', trip);
|
||||
|
||||
try {
|
||||
const gpsData = await getGPS();
|
||||
if (gpsData) {
|
||||
if (isFinished == false) {
|
||||
// Tạo mẻ mới
|
||||
const logStatus0 = trip.fishing_logs?.find((log) => log.status === 0);
|
||||
console.log('ok', logStatus0);
|
||||
console.log('ok', fishingLog);
|
||||
const body: API.FishingLog = {
|
||||
fishing_log_id: logStatus0?.fishing_log_id || '',
|
||||
trip_id: trip.id,
|
||||
start_at: logStatus0?.start_at!,
|
||||
start_lat: logStatus0?.start_lat!,
|
||||
start_lon: logStatus0?.start_lon!,
|
||||
haul_lat: gpsData.lat,
|
||||
haul_lon: gpsData.lon,
|
||||
end_at: new Date(),
|
||||
status: 1,
|
||||
weather_description: logStatus0?.weather_description || '',
|
||||
info: fishingLog.map((item) => ({
|
||||
fish_species_id: item.fish_species_id,
|
||||
fish_name: item.fish_name,
|
||||
catch_number: item.catch_number,
|
||||
catch_unit: item.catch_unit,
|
||||
fish_size: item.fish_size,
|
||||
fish_rarity: item.fish_rarity,
|
||||
fish_condition: '',
|
||||
gear_usage: '',
|
||||
})),
|
||||
sync: true,
|
||||
};
|
||||
const resp = await updateFishingLogs(body);
|
||||
console.log('Resp', resp);
|
||||
|
||||
onFinished?.(true);
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
const body: API.FishingLog = {
|
||||
fishing_log_id: fishingLogs?.fishing_log_id || '',
|
||||
trip_id: fishingLogs?.trip_id!,
|
||||
start_at: fishingLogs?.start_at!,
|
||||
start_lat: fishingLogs?.start_lat!,
|
||||
start_lon: fishingLogs?.start_lon!,
|
||||
haul_lat: fishingLogs?.haul_lat!,
|
||||
haul_lon: fishingLogs?.haul_lon!,
|
||||
end_at: fishingLogs?.end_at!,
|
||||
status: fishingLogs?.status!,
|
||||
weather_description: fishingLogs?.weather_description || '',
|
||||
info: fishingLog.map((item) => ({
|
||||
fish_species_id: item.fish_species_id,
|
||||
fish_name: item.fish_name,
|
||||
catch_number: item.catch_number,
|
||||
catch_unit: item.catch_unit,
|
||||
fish_size: item.fish_size,
|
||||
fish_rarity: item.fish_rarity,
|
||||
fish_condition: '',
|
||||
gear_usage: '',
|
||||
})),
|
||||
sync: true,
|
||||
};
|
||||
// console.log('Update body:', body);
|
||||
|
||||
const resp = await updateFishingLogs(body);
|
||||
console.log('Resp', resp);
|
||||
|
||||
onFinished?.(true);
|
||||
onOpenChange(false);
|
||||
}
|
||||
setDataSource([]);
|
||||
setEditableRowKeys([]);
|
||||
} else {
|
||||
message.error('Không thể lấy dữ liệu GPS. Vui lòng thử lại.');
|
||||
}
|
||||
} catch (error) {
|
||||
onFinished?.(false);
|
||||
console.error('Error creating/updating haul:', error);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
width="70%"
|
||||
title={isFinished ? 'Cập nhật mẻ lưới' : 'Kết thúc mẻ lưới'}
|
||||
open={isOpen}
|
||||
cancelText="Huỷ"
|
||||
maskClosable={false}
|
||||
okText={isFinished ? 'Cập nhật' : 'Kết thúc'}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onOk={async () => {
|
||||
// Validate data trước khi submit
|
||||
const validData = dataSource.filter(
|
||||
(item) =>
|
||||
item.fish_name &&
|
||||
item.fish_size &&
|
||||
item.catch_number &&
|
||||
item.catch_unit,
|
||||
);
|
||||
|
||||
if (validData.length === 0) {
|
||||
message.error(
|
||||
'Vui lòng nhập ít nhất một loài cá với đầy đủ thông tin',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await createOrCreateOrUpdateFishingLog(validData);
|
||||
}}
|
||||
>
|
||||
<EditableProTable<FishingLogInfoWithKey>
|
||||
key={fishingLogs?.fishing_log_id}
|
||||
headerTitle="Danh sách cá đánh bắt"
|
||||
columns={columns}
|
||||
rowKey="key"
|
||||
scroll={{
|
||||
x: 960,
|
||||
}}
|
||||
value={dataSource}
|
||||
onChange={setDataSource}
|
||||
recordCreatorProps={{
|
||||
newRecordType: 'dataSource',
|
||||
creatorButtonText: 'Thêm loài',
|
||||
record: () => ({
|
||||
key: Date.now(),
|
||||
}),
|
||||
}}
|
||||
editable={{
|
||||
type: 'multiple',
|
||||
editableKeys,
|
||||
actionRender: (row, config, defaultDoms) => {
|
||||
return [defaultDoms.delete];
|
||||
},
|
||||
deletePopconfirmMessage: 'Bạn chắc chắn muốn xoá?',
|
||||
onValuesChange: (
|
||||
record: Partial<FishingLogInfoWithKey> | undefined,
|
||||
recordList: FishingLogInfoWithKey[],
|
||||
) => {
|
||||
// Nếu không có record (sự kiện không liên quan tới 1 dòng cụ thể) thì chỉ cập nhật dataSource
|
||||
if (!record) {
|
||||
setDataSource([...recordList]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lấy giá trị species id (cẩn trọng string/number)
|
||||
const speciesId = (record as any).fish_species_id;
|
||||
if (speciesId === undefined || speciesId === null) {
|
||||
// Nếu không phải là thay đổi chọn loài cá, chỉ cập nhật dataSource
|
||||
setDataSource([...recordList]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tìm loài cá tương ứng, so sánh bằng String để tránh khác kiểu number/string
|
||||
const fish = fishDatas.find(
|
||||
(f) => String(f.id) === String(speciesId),
|
||||
);
|
||||
if (!fish) {
|
||||
setDataSource([...recordList]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tạo record mới (merge thông tin loài cá vào dòng hiện tại)
|
||||
const mergedRecord: FishingLogInfoWithKey = {
|
||||
...(record as FishingLogInfoWithKey),
|
||||
fish_species_id: fish.id,
|
||||
fish_name: fish.name,
|
||||
catch_unit: fish.default_unit,
|
||||
fish_rarity: fish.rarity_level,
|
||||
};
|
||||
|
||||
// Áp lại vào recordList dựa theo key (so khớp key bằng String để an toàn)
|
||||
const newList = recordList.map((r) =>
|
||||
String(r.key) === String(mergedRecord.key) ? mergedRecord : r,
|
||||
);
|
||||
|
||||
// Cập nhật state (sao chép mảng để tránh vấn đề readonly/type)
|
||||
setDataSource([...newList]);
|
||||
|
||||
// Đảm bảo dòng này đang ở trạng thái editable (nếu cần)
|
||||
setEditableRowKeys((prev) =>
|
||||
prev.includes(mergedRecord.key)
|
||||
? prev
|
||||
: [...prev, mergedRecord.key],
|
||||
);
|
||||
},
|
||||
onChange: setEditableRowKeys,
|
||||
deleteText: (
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
shape="circle"
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrUpdateFishingLog;
|
||||
83
src/pages/Trip/components/HaulFishList.tsx
Normal file
83
src/pages/Trip/components/HaulFishList.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ProCard, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Form, Modal } from 'antd';
|
||||
|
||||
interface HaulFishListProp {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
fishList?: API.FishingLogInfo[];
|
||||
}
|
||||
|
||||
const HaulFishList: React.FC<HaulFishListProp> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
fishList,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const fish_columns: ProColumns<API.FishingLogInfo>[] = [
|
||||
{
|
||||
title: 'Tên cá',
|
||||
dataIndex: 'fish_name',
|
||||
key: 'fish_name',
|
||||
render: (value) => value, // bạn có thể sửa render để hiển thị tên thiết bị nếu cần
|
||||
},
|
||||
{
|
||||
title: 'Trạng thái',
|
||||
dataIndex: 'fish_condition',
|
||||
key: 'fish_condition',
|
||||
render: (value) => value, // bạn có thể sửa render để hiển thị tên thiết bị nếu cần
|
||||
},
|
||||
{
|
||||
title: 'Độ hiếm',
|
||||
dataIndex: 'fish_rarity',
|
||||
key: 'fish_rarity',
|
||||
render: (value) => value, // bạn có thể sửa render để hiển thị tên thiết bị nếu cần
|
||||
},
|
||||
{
|
||||
title: 'Kích thước (cm)',
|
||||
dataIndex: 'fish_size',
|
||||
key: 'fish_size',
|
||||
render: (value) => value, // bạn có thể sửa render để hiển thị tên thiết bị nếu cần
|
||||
},
|
||||
{
|
||||
title: 'Cân nặng (kg)',
|
||||
dataIndex: 'catch_number',
|
||||
key: 'catch_number',
|
||||
render: (value) => value, // bạn có thể sửa render để hiển thị tên thiết bị nếu cần
|
||||
},
|
||||
{
|
||||
title: 'Ngư cụ sử dụng',
|
||||
dataIndex: 'gear_usage',
|
||||
key: 'gear_usage',
|
||||
render: (value) => value, // bạn có thể sửa render để hiển thị tên thiết bị nếu cần
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Modal
|
||||
title="Danh sách cá"
|
||||
open={open}
|
||||
footer={null}
|
||||
closable
|
||||
afterClose={() => onOpenChange(false)}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
width={1000}
|
||||
>
|
||||
<ProCard split="vertical">
|
||||
<ProTable<API.FishingLogInfo>
|
||||
columns={fish_columns}
|
||||
dataSource={fishList}
|
||||
search={false}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
options={false}
|
||||
bordered
|
||||
size="middle"
|
||||
scroll={{ x: 600 }}
|
||||
/>
|
||||
</ProCard>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HaulFishList;
|
||||
183
src/pages/Trip/components/HaulTable.tsx
Normal file
183
src/pages/Trip/components/HaulTable.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { EditOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Button, Flex, message } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import CreateOrUpdateFishingLog from './CreateOrUpdateFishingLog';
|
||||
import HaulFishList from './HaulFishList';
|
||||
|
||||
export interface HaulTableProps {
|
||||
hauls: API.FishingLog[];
|
||||
trip?: API.Trip;
|
||||
onReload?: (isTrue: boolean) => void;
|
||||
}
|
||||
const HaulTable: React.FC<HaulTableProps> = ({ hauls, trip, onReload }) => {
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editFishingLogOpen, setEditFishingLogOpen] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.FishingLogInfo[]>([]);
|
||||
const [currentFishingLog, setCurrentFishingLog] =
|
||||
useState<API.FishingLog | null>(null);
|
||||
const intl = useIntl();
|
||||
const { getApi } = useModel('getTrip');
|
||||
console.log('HaulTable received hauls:', hauls);
|
||||
|
||||
const fishing_logs_columns: ProColumns<API.FishingLog>[] = [
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>STT</div>,
|
||||
dataIndex: 'fishing_log_id',
|
||||
align: 'center',
|
||||
render: (_, __, index, action) => {
|
||||
return `Mẻ ${hauls.length - index}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Trạng Thái</div>,
|
||||
dataIndex: ['status'], // 👈 lấy từ status 1: đang
|
||||
align: 'center',
|
||||
valueEnum: {
|
||||
0: {
|
||||
text: intl.formatMessage({
|
||||
id: 'pages.trips.status.fishing',
|
||||
defaultMessage: 'Đang đánh bắt',
|
||||
}),
|
||||
status: 'Processing',
|
||||
},
|
||||
1: {
|
||||
text: intl.formatMessage({
|
||||
id: 'pages.trips.status.end_fishing',
|
||||
defaultMessage: 'Đã hoàn thành',
|
||||
}),
|
||||
status: 'Success',
|
||||
},
|
||||
2: {
|
||||
text: intl.formatMessage({
|
||||
id: 'pages.trips.status.cancel_fishing',
|
||||
defaultMessage: 'Đã huỷ',
|
||||
}),
|
||||
status: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Thời tiết</div>,
|
||||
dataIndex: ['weather_description'], // 👈 lấy từ weather
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Thời điểm bắt đầu</div>,
|
||||
dataIndex: ['start_at'], // birth_date là date of birth
|
||||
align: 'center',
|
||||
render: (start_at: any) => {
|
||||
if (!start_at) return '-';
|
||||
const date = new Date(start_at);
|
||||
return date.toLocaleString('vi-VN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Thời điểm kết thúc</div>,
|
||||
dataIndex: ['end_at'], // birth_date là date of birth
|
||||
align: 'center',
|
||||
render: (end_at: any) => {
|
||||
// console.log('End at value:', end_at);
|
||||
if (end_at == '0001-01-01T00:00:00Z') return '-';
|
||||
const date = new Date(end_at);
|
||||
return date.toLocaleString('vi-VN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Thao tác',
|
||||
align: 'center',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
console.log('Rendering action column for record:', record);
|
||||
return (
|
||||
<Flex align="center" justify="center" gap={5}>
|
||||
{/* Nút Edit */}
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => {
|
||||
if (record.info) {
|
||||
setCurrentRow(record.info!); // record là dòng hiện tại trong table
|
||||
setEditOpen(true);
|
||||
} else {
|
||||
message.warning('Không có dữ liệu cá trong mẻ lưới này');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{editOpen && currentRow && (
|
||||
<HaulFishList
|
||||
fishList={currentRow} // truyền luôn cả record nếu cần
|
||||
open={editOpen}
|
||||
onOpenChange={setEditOpen}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentFishingLog(record);
|
||||
setEditFishingLogOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable<API.FishingLog>
|
||||
style={{ width: '90%' }}
|
||||
columns={fishing_logs_columns}
|
||||
dataSource={hauls.slice().reverse()} // đảo ngược thứ tự hiển thị
|
||||
search={false}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total, range) => `${range[0]}-${range[1]} trên ${total}`,
|
||||
}}
|
||||
options={false}
|
||||
bordered
|
||||
size="middle"
|
||||
scroll={{ x: 600 }}
|
||||
/>
|
||||
<CreateOrUpdateFishingLog
|
||||
trip={trip!}
|
||||
isFinished={currentFishingLog?.status === 0 ? false : true}
|
||||
fishingLogs={currentFishingLog || undefined}
|
||||
isOpen={editFishingLogOpen}
|
||||
onOpenChange={setEditFishingLogOpen}
|
||||
onFinished={(success) => {
|
||||
if (success) {
|
||||
message.success('Cập nhật mẻ lưới thành công');
|
||||
getApi();
|
||||
} else {
|
||||
message.error('Cập nhật mẻ lưới thất bại');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HaulTable;
|
||||
77
src/pages/Trip/components/ListSkeleton.tsx
Normal file
77
src/pages/Trip/components/ListSkeleton.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Col, Row, Skeleton } from 'antd';
|
||||
const ListSkeleton = ({}) => {
|
||||
return (
|
||||
<>
|
||||
<Row justify="space-between">
|
||||
<Col span={2}>
|
||||
<Skeleton.Avatar active={true} size="small" />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Skeleton.Input active={true} size="small" block={true} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Skeleton.Button
|
||||
active={true}
|
||||
size="small"
|
||||
shape="round"
|
||||
block={true}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<p />
|
||||
<Row justify="space-between">
|
||||
<Col span={2}>
|
||||
<Skeleton.Avatar active={true} size="small" />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Skeleton.Input active={true} size="small" block={true} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Skeleton.Button
|
||||
active={true}
|
||||
size="small"
|
||||
shape="round"
|
||||
block={true}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<p />
|
||||
<Row justify="space-between">
|
||||
<Col span={2}>
|
||||
<Skeleton.Avatar active={true} size="small" />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Skeleton.Input active={true} size="small" block={true} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Skeleton.Button
|
||||
active={true}
|
||||
size="small"
|
||||
shape="round"
|
||||
block={true}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<p />
|
||||
<Row justify="space-between">
|
||||
<Col span={2}>
|
||||
<Skeleton.Avatar active={true} size="small" />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Skeleton.Input active={true} size="small" block={true} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Skeleton.Button
|
||||
active={true}
|
||||
size="small"
|
||||
shape="round"
|
||||
block={true}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<p />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListSkeleton;
|
||||
158
src/pages/Trip/components/MainTripBody.tsx
Normal file
158
src/pages/Trip/components/MainTripBody.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { Flex } from 'antd';
|
||||
import HaulTable from './HaulTable';
|
||||
import TripCostTable from './TripCost';
|
||||
import TripCrews from './TripCrews';
|
||||
import TripFishingGearTable from './TripFishingGear';
|
||||
// Props cho component
|
||||
interface MainTripBodyProps {
|
||||
trip_id?: string;
|
||||
tripInfo: API.Trip | null;
|
||||
onReload?: (isTrue: boolean) => void;
|
||||
}
|
||||
|
||||
const MainTripBody: React.FC<MainTripBodyProps> = ({
|
||||
trip_id,
|
||||
tripInfo,
|
||||
onReload,
|
||||
}) => {
|
||||
// console.log('MainTripBody received:');
|
||||
// console.log("trip_id:", trip_id);
|
||||
// console.log('tripInfo:', tripInfo);
|
||||
const { data, getApi } = useModel('getTrip');
|
||||
const tripCosts = Array.isArray(tripInfo?.trip_cost)
|
||||
? tripInfo.trip_cost
|
||||
: [];
|
||||
const fishingGears = Array.isArray(tripInfo?.fishing_gears)
|
||||
? tripInfo.fishing_gears
|
||||
: [];
|
||||
|
||||
const fishing_logs_columns = [
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Tên</div>,
|
||||
dataIndex: 'name',
|
||||
valueType: 'select',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Số lượng</div>,
|
||||
dataIndex: 'number',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
const tranship_columns = [
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Tên</div>,
|
||||
dataIndex: 'name',
|
||||
valueType: 'select',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Chức vụ</div>,
|
||||
dataIndex: 'role',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex gap={10}>
|
||||
<ProCard
|
||||
ghost
|
||||
gutter={{
|
||||
xs: 8,
|
||||
sm: 8,
|
||||
md: 8,
|
||||
lg: 8,
|
||||
xl: 8,
|
||||
xxl: 8,
|
||||
}}
|
||||
direction="column"
|
||||
bodyStyle={{
|
||||
padding: 0,
|
||||
paddingInline: 0,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<ProCard bodyStyle={{ padding: 0, gap: 5 }}>
|
||||
<ProCard
|
||||
colSpan={{ xs: 2, sm: 4, md: 6, lg: 8, xl: 12 }}
|
||||
layout="center"
|
||||
bordered
|
||||
headStyle={{
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
title="Chi phí chuyến đi"
|
||||
style={{ minHeight: 300 }}
|
||||
>
|
||||
<TripCostTable tripCosts={tripCosts} />
|
||||
</ProCard>
|
||||
<ProCard
|
||||
colSpan={{ xs: 2, sm: 4, md: 6, lg: 8, xl: 12 }}
|
||||
layout="center"
|
||||
bordered
|
||||
headStyle={{
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
title="Danh sách ngư cụ"
|
||||
style={{ minHeight: 300 }}
|
||||
>
|
||||
<TripFishingGearTable fishingGears={fishingGears} />
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
<ProCard
|
||||
style={{ paddingInlineEnd: 0 }}
|
||||
ghost={true}
|
||||
colSpan={{ xs: 4, sm: 8, md: 12, lg: 16, xl: 24 }}
|
||||
layout="center"
|
||||
bordered
|
||||
headStyle={{
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
title="Danh sách thuyền viên"
|
||||
>
|
||||
<TripCrews crew={tripInfo?.crews} />
|
||||
</ProCard>
|
||||
<ProCard
|
||||
style={{ paddingInlineEnd: 0 }}
|
||||
ghost={true}
|
||||
colSpan={{ xs: 4, sm: 8, md: 12, lg: 16, xl: 24 }}
|
||||
layout="center"
|
||||
bordered
|
||||
headStyle={{
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
title="Danh sách mẻ lưới"
|
||||
>
|
||||
<HaulTable
|
||||
trip={tripInfo!}
|
||||
hauls={tripInfo?.fishing_logs || []}
|
||||
onReload={(isTrue) => {
|
||||
if (isTrue) {
|
||||
// onReload?.(true);
|
||||
getApi();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainTripBody;
|
||||
60
src/pages/Trip/components/TripCancelOrFinishButton.tsx
Normal file
60
src/pages/Trip/components/TripCancelOrFinishButton.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { updateTripState } from '@/services/controller/TripController';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { Button, message, Popconfirm } from 'antd';
|
||||
import React from 'react';
|
||||
import CancelTrip from './CancelTrip';
|
||||
|
||||
interface TripCancleOrFinishedButtonProps {
|
||||
tripStatus?: number;
|
||||
onCallBack?: (success: boolean) => void;
|
||||
}
|
||||
|
||||
const TripCancleOrFinishedButton: React.FC<TripCancleOrFinishedButtonProps> = ({
|
||||
tripStatus,
|
||||
onCallBack,
|
||||
}) => {
|
||||
const { getApi } = useModel('getTrip');
|
||||
const handleClickButton = async (state: number, note?: string) => {
|
||||
try {
|
||||
const resp = await updateTripState({ status: state, note: note || '' });
|
||||
message.success('Cập nhật trạng thái thành công');
|
||||
getApi();
|
||||
onCallBack?.(true);
|
||||
} catch (error) {
|
||||
console.error('Error updating trip status:', error);
|
||||
message.error('Cập nhật trạng thái thất bại');
|
||||
onCallBack?.(false);
|
||||
}
|
||||
};
|
||||
const renderButton = () => {
|
||||
switch (tripStatus) {
|
||||
case 3: // Đang hoạt động
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<CancelTrip
|
||||
onFinished={async (note) => {
|
||||
await handleClickButton(5, note);
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="Thông báo"
|
||||
description="Bạn chắc chắn muốn kết thúc chuyến đi?"
|
||||
onConfirm={async () => handleClickButton(4)}
|
||||
okText="Chắc chắn"
|
||||
cancelText="Không"
|
||||
>
|
||||
<Button color="orange" variant="solid">
|
||||
Kết thúc
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return <div>{renderButton()}</div>;
|
||||
};
|
||||
|
||||
export default TripCancleOrFinishedButton;
|
||||
86
src/pages/Trip/components/TripCost.tsx
Normal file
86
src/pages/Trip/components/TripCost.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
interface TripCostTableProps {
|
||||
tripCosts: API.TripCost[];
|
||||
}
|
||||
|
||||
const TripCostTable: React.FC<TripCostTableProps> = ({ tripCosts }) => {
|
||||
// Tính tổng chi phí
|
||||
const total_trip_cost = tripCosts.reduce(
|
||||
(sum, item) => sum + (Number(item.total_cost) || 0),
|
||||
0,
|
||||
);
|
||||
const trip_cost_columns: ProColumns<API.TripCost>[] = [
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Loại</div>,
|
||||
dataIndex: 'type',
|
||||
valueEnum: {
|
||||
fuel: { text: 'Nhiên liệu' },
|
||||
crew_salary: { text: 'Lương thuyền viên' },
|
||||
food: { text: 'Lương thực' },
|
||||
ice_salt_cost: { text: 'Muối đá' },
|
||||
},
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Số lượng</div>,
|
||||
dataIndex: 'amount',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Đơn vị</div>,
|
||||
dataIndex: 'unit',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Chi phí</div>,
|
||||
dataIndex: 'cost_per_unit',
|
||||
align: 'center',
|
||||
render: (val: any) => (val ? Number(val).toLocaleString() : ''),
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Tổng chi phí</div>,
|
||||
dataIndex: 'total_cost',
|
||||
align: 'center',
|
||||
render: (val: any) =>
|
||||
val ? (
|
||||
<b style={{ color: '#fa541c' }}>{Number(val).toLocaleString()}</b>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<ProTable<API.TripCost>
|
||||
columns={trip_cost_columns}
|
||||
dataSource={tripCosts}
|
||||
search={false}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total, range) => `${range[0]}-${range[1]} trên ${total}`,
|
||||
}}
|
||||
options={false}
|
||||
bordered
|
||||
size="middle"
|
||||
scroll={{ x: 600 }}
|
||||
summary={() => (
|
||||
<ProTable.Summary.Row>
|
||||
<ProTable.Summary.Cell index={0} colSpan={4} align="right">
|
||||
<Typography.Text strong style={{ color: '#1890ff' }}>
|
||||
Tổng cộng
|
||||
</Typography.Text>
|
||||
</ProTable.Summary.Cell>
|
||||
<ProTable.Summary.Cell index={4} align="center">
|
||||
<Typography.Text strong style={{ color: '#fa541c', fontSize: 16 }}>
|
||||
{total_trip_cost.toLocaleString()}
|
||||
</Typography.Text>
|
||||
</ProTable.Summary.Cell>
|
||||
</ProTable.Summary.Row>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TripCostTable;
|
||||
84
src/pages/Trip/components/TripCrews.tsx
Normal file
84
src/pages/Trip/components/TripCrews.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import React from 'react';
|
||||
|
||||
interface TripCrewsProps {
|
||||
crew?: API.TripCrews[];
|
||||
}
|
||||
|
||||
const TripCrews: React.FC<TripCrewsProps> = ({ crew }) => {
|
||||
console.log('TripCrews received crew:', crew);
|
||||
|
||||
const crew_columns: ProColumns<API.TripCrews>[] = [
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Mã định danh</div>,
|
||||
dataIndex: ['Person', 'personal_id'], // 👈 lấy từ Person.personal_id
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Tên</div>,
|
||||
dataIndex: ['Person', 'name'], // 👈 lấy từ Person.name
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Chức vụ</div>,
|
||||
dataIndex: 'role',
|
||||
align: 'center',
|
||||
render: (val: any) => {
|
||||
switch (val) {
|
||||
case 'captain':
|
||||
return <span style={{ color: 'blue' }}>Thuyền trưởng</span>;
|
||||
case 'crew':
|
||||
return <span style={{ color: 'green' }}>Thuyền viên</span>;
|
||||
default:
|
||||
return <span>{val}</span>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Email</div>,
|
||||
dataIndex: ['Person', 'email'], // 👈 lấy từ Person.email
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Số điện thoại</div>,
|
||||
dataIndex: ['Person', 'phone'], // 👈 lấy từ Person.phone
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Ngày sinh</div>,
|
||||
dataIndex: ['Person', 'birth_date'], // birth_date là date of birth
|
||||
align: 'center',
|
||||
render: (birth_date: any) => {
|
||||
if (!birth_date) return '-';
|
||||
const date = new Date(birth_date);
|
||||
return date.toLocaleDateString('vi-VN'); // 👉 tự động ra dd/mm/yyyy
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Địa chỉ</div>,
|
||||
dataIndex: ['Person', 'address'], // 👈 lấy từ Person.address
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProTable<API.TripCrews>
|
||||
style={{ width: '90%' }}
|
||||
columns={crew_columns}
|
||||
dataSource={crew}
|
||||
search={false}
|
||||
rowKey={(record, idx: any) => record.Person.personal_id || idx.toString()}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total, range) => `${range[0]}-${range[1]} trên ${total}`,
|
||||
}}
|
||||
options={false}
|
||||
bordered
|
||||
size="middle"
|
||||
scroll={{ x: 600 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TripCrews;
|
||||
44
src/pages/Trip/components/TripFishingGear.tsx
Normal file
44
src/pages/Trip/components/TripFishingGear.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import React from 'react';
|
||||
|
||||
interface TripFishingGearTableProps {
|
||||
fishingGears: API.FishingGear[];
|
||||
}
|
||||
|
||||
const TripFishingGearTable: React.FC<TripFishingGearTableProps> = ({
|
||||
fishingGears,
|
||||
}) => {
|
||||
const fishing_gears_columns: ProColumns<API.FishingGear>[] = [
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Tên</div>,
|
||||
dataIndex: 'name',
|
||||
valueType: 'select',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>Số lượng</div>,
|
||||
dataIndex: 'number',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProTable<API.FishingGear>
|
||||
columns={fishing_gears_columns}
|
||||
dataSource={fishingGears}
|
||||
search={false}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total, range) => `${range[0]}-${range[1]} trên ${total}`,
|
||||
}}
|
||||
options={false}
|
||||
bordered
|
||||
size="middle"
|
||||
scroll={{ x: 600 }}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TripFishingGearTable;
|
||||
170
src/pages/Trip/index.tsx
Normal file
170
src/pages/Trip/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { STATUS } from '@/constants/enums';
|
||||
import { queryAlarms } from '@/services/controller/DeviceController';
|
||||
import { PageContainer, ProCard } from '@ant-design/pro-components';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Flex, message, theme } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AlarmTable } from './components/AlarmTable';
|
||||
import BadgeTripStatus from './components/BadgeTripStatus';
|
||||
import CreateNewHaulOrTrip from './components/CreateNewHaulOrTrip';
|
||||
import ListSkeleton from './components/ListSkeleton';
|
||||
import MainTripBody from './components/MainTripBody';
|
||||
import TripCancleOrFinishedButton from './components/TripCancelOrFinishButton';
|
||||
const DetailTrip = () => {
|
||||
const intl = useIntl();
|
||||
const [responsive, setResponsive] = useState(false);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
// const [tripInfo, setTripInfo] = useState<API.Trip | null>(null);
|
||||
const [showAlarmList, setShowAlarmList] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [alarmList, setAlarmList] = useState<API.Alarm[]>([]);
|
||||
const { token } = theme.useToken();
|
||||
const { data, getApi } = useModel('getTrip');
|
||||
const queryDataSource = async (): Promise<API.AlarmResponse> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const resp: API.AlarmResponse = await queryAlarms();
|
||||
if (resp.alarms.length == 0) {
|
||||
setShowAlarmList(false);
|
||||
} else {
|
||||
setAlarmList(resp.alarms);
|
||||
}
|
||||
setIsLoading(false);
|
||||
return resp;
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
setShowAlarmList(false);
|
||||
return { alarms: [], level: 0 };
|
||||
}
|
||||
};
|
||||
// const fetchTrip = async () => {
|
||||
// try {
|
||||
// const resp = await getTrip();
|
||||
// setTripInfo(resp);
|
||||
// } catch (error) {
|
||||
// console.error('Error when get Trip: ', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
// fetchTrip();
|
||||
getApi();
|
||||
queryDataSource();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
console.log('Rendering with tripInfo:', data),
|
||||
(
|
||||
<PageContainer
|
||||
header={{
|
||||
title: data ? data.name : 'Chuyến đi',
|
||||
tags: <BadgeTripStatus status={data?.trip_status || 0} />,
|
||||
}}
|
||||
loading={isLoading}
|
||||
extra={[
|
||||
<CreateNewHaulOrTrip
|
||||
trips={data || undefined}
|
||||
onCallBack={async (success) => {
|
||||
switch (success) {
|
||||
case STATUS.CREATE_FISHING_LOG_SUCCESS:
|
||||
message.success('Tạo mẻ lưới thành công');
|
||||
// await fetchTrip();
|
||||
break;
|
||||
case STATUS.CREATE_FISHING_LOG_FAIL:
|
||||
message.error('Tạo mẻ lưới thất bại');
|
||||
break;
|
||||
case STATUS.START_TRIP_SUCCESS:
|
||||
message.success('Bắt đầu chuyến đi thành công');
|
||||
// await fetchTrip();
|
||||
break;
|
||||
case STATUS.START_TRIP_FAIL:
|
||||
message.error('Bắt đầu chuyến đi thất bại');
|
||||
break;
|
||||
case STATUS.UPDATE_FISHING_LOG_SUCCESS:
|
||||
message.success('Cập nhật mẻ lưới thành công');
|
||||
// await fetchTrip();
|
||||
break;
|
||||
case STATUS.UPDATE_FISHING_LOG_FAIL:
|
||||
message.error('Cập nhật mẻ lưới thất bại');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
{contextHolder}
|
||||
<ProCard
|
||||
// bordered={false}
|
||||
split={responsive ? 'horizontal' : 'vertical'}
|
||||
// style={{ backgroundColor: 'transparent' }}
|
||||
>
|
||||
{/* Bên trái */}
|
||||
{showAlarmList ? (
|
||||
<ProCard
|
||||
title={intl.formatMessage({
|
||||
id: 'pages.gmsv5.alarm.list',
|
||||
defaultMessage: 'Cảnh báo',
|
||||
})}
|
||||
colSpan={{ xs: 24, sm: 24, lg: 5 }}
|
||||
bodyStyle={{ paddingInline: 0, paddingBlock: 8 }}
|
||||
bordered
|
||||
>
|
||||
{data ? (
|
||||
<AlarmTable alarmList={alarmList} isLoading={isLoading} />
|
||||
) : (
|
||||
<ListSkeleton />
|
||||
)}
|
||||
</ProCard>
|
||||
) : null}
|
||||
{/* */}
|
||||
|
||||
{/* Bên phải */}
|
||||
<ProCard
|
||||
colSpan={
|
||||
showAlarmList
|
||||
? { xs: 24, sm: 24, lg: 19 }
|
||||
: { xs: 24, sm: 24, lg: 24 }
|
||||
}
|
||||
// bodyStyle={{ padding: 0 }}
|
||||
// style={{ backgroundColor: 'transparent' }}
|
||||
>
|
||||
<MainTripBody
|
||||
trip_id={data?.id}
|
||||
tripInfo={data || null}
|
||||
onReload={(isReload) => {
|
||||
console.log('Nhanaj dduowcj hàm, onReload:', isReload);
|
||||
|
||||
// if (isReload) {
|
||||
// fetchTrip();
|
||||
// }
|
||||
}}
|
||||
/>
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
<Flex
|
||||
style={{
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
backgroundColor: token.colorBgContainer,
|
||||
}}
|
||||
justify="center"
|
||||
gap={10}
|
||||
>
|
||||
<TripCancleOrFinishedButton
|
||||
tripStatus={data?.trip_status}
|
||||
onCallBack={(value) => async () => {
|
||||
if (value) {
|
||||
// await fetchTrip();
|
||||
await queryDataSource();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailTrip;
|
||||
Reference in New Issue
Block a user