472 lines
14 KiB
TypeScript
472 lines
14 KiB
TypeScript
import { DeleteButton, EditButton } from '@/components/shared/Button';
|
|
import {
|
|
apiGetPhoto,
|
|
apiUploadPhoto,
|
|
} from '@/services/slave/sgw/PhotoController';
|
|
import {
|
|
apiDeleteTripCrew,
|
|
apiGetTripCrew,
|
|
} from '@/services/slave/sgw/TripController';
|
|
import { PlusOutlined, UserOutlined } from '@ant-design/icons';
|
|
import type { ActionType } from '@ant-design/pro-components';
|
|
import {
|
|
ModalForm,
|
|
ProCard,
|
|
ProColumns,
|
|
ProForm,
|
|
ProTable,
|
|
} from '@ant-design/pro-components';
|
|
import { FormattedMessage, useIntl } from '@umijs/max';
|
|
import { Button, Divider, message, Modal, Tooltip, Upload } from 'antd';
|
|
import type { UploadFile } from 'antd/es/upload/interface';
|
|
import React, { useRef, useState } from 'react';
|
|
import EditCrew from './EditCrew';
|
|
import AddCrew from './FormAddCrew';
|
|
import UploadPhoto from './UploadPhoto';
|
|
|
|
interface TripCrewsProps {
|
|
record: SgwModel.Trip;
|
|
onChange?: (items: SgwModel.TripCrews[]) => void;
|
|
}
|
|
|
|
const PAGE_SIZE = 10;
|
|
|
|
// Stub API functions - Now using real APIs with proper interfaces
|
|
const getTripCrew = async (
|
|
tripId: string,
|
|
): Promise<SgwModel.TripCrewQueryResponse> => {
|
|
return apiGetTripCrew(tripId);
|
|
};
|
|
|
|
const deleteTripCrew = async (
|
|
tripId: string,
|
|
personalId: string,
|
|
): Promise<void> => {
|
|
return apiDeleteTripCrew(tripId, personalId);
|
|
};
|
|
|
|
const uploadPhoto = async (
|
|
type: 'people',
|
|
id: string,
|
|
file: File,
|
|
): Promise<void> => {
|
|
return apiUploadPhoto(type, id, file);
|
|
};
|
|
|
|
// Component riêng để hiển thị ảnh thuyền viên
|
|
const CrewPhoto: React.FC<{ record: SgwModel.TripCrews }> = ({ record }) => {
|
|
const [photoSrc, setPhotoSrc] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Placeholder SVG for when there's no image or error
|
|
const placeholderSrc =
|
|
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y1ZjVmNSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5ObyBJbWFnZTwvdGV4dD48L3N2Zz4=';
|
|
|
|
React.useEffect(() => {
|
|
const loadPhoto = async () => {
|
|
if (!record.Person?.personal_id) {
|
|
setPhotoSrc(placeholderSrc);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const photoData = await apiGetPhoto(
|
|
'people',
|
|
record.Person.personal_id,
|
|
);
|
|
const blob = new Blob([photoData], { type: 'image/jpeg' });
|
|
const url = URL.createObjectURL(blob);
|
|
setPhotoSrc(url);
|
|
} catch (error) {
|
|
console.error('Failed to load photo:', error);
|
|
setPhotoSrc(placeholderSrc);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadPhoto();
|
|
|
|
// Cleanup function to revoke object URL
|
|
return () => {
|
|
if (photoSrc && photoSrc.startsWith('blob:')) {
|
|
URL.revokeObjectURL(photoSrc);
|
|
}
|
|
};
|
|
}, [record.Person?.personal_id]);
|
|
|
|
return (
|
|
<div style={{ textAlign: 'center' }}>
|
|
<img
|
|
src={photoSrc || placeholderSrc}
|
|
alt={`Ảnh thuyền viên ${record.Person?.name || ''}`}
|
|
style={{
|
|
width: '150px',
|
|
height: '100px',
|
|
objectFit: 'cover',
|
|
borderRadius: '4px',
|
|
border: '1px solid #d9d9d9',
|
|
cursor: 'pointer',
|
|
opacity: loading ? 0.5 : 1,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TripCrews: React.FC<TripCrewsProps> = ({ record, onChange }) => {
|
|
const intl = useIntl();
|
|
const actionRef = useRef<ActionType>();
|
|
const [open, setOpen] = useState(false);
|
|
const [selectedItems] = useState<SgwModel.TripCrews[]>([]);
|
|
|
|
const [uploadPhotoModalVisible, handleUploadPhotoModalVisible] =
|
|
useState(false);
|
|
const [currentRow, setCurrentRow] = useState<SgwModel.TripCrews | undefined>(
|
|
undefined,
|
|
);
|
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
const [addCrewModalVisible, setAddCrewModalVisible] = useState(false);
|
|
const [editCrewModalVisible, setEditCrewModalVisible] = useState(false);
|
|
|
|
// Handle upload photo
|
|
const handleUploadPhoto = async () => {
|
|
const key = 'upload_photo';
|
|
try {
|
|
message.loading({ content: 'Đang upload ảnh...', key, duration: 0 });
|
|
if (fileList.length > 0 && currentRow?.Person?.personal_id) {
|
|
const file = fileList[0].originFileObj || fileList[0];
|
|
await uploadPhoto(
|
|
'people',
|
|
currentRow.Person.personal_id,
|
|
file as File,
|
|
);
|
|
message.success({ content: 'Upload ảnh thành công!', key });
|
|
return true;
|
|
} else {
|
|
message.error({ content: 'Vui lòng chọn ảnh!', key });
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
message.error({ content: 'Upload ảnh thất bại!', key });
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleUploadChange = ({
|
|
fileList: newFileList,
|
|
}: {
|
|
fileList: UploadFile[];
|
|
}) => {
|
|
setFileList(newFileList);
|
|
};
|
|
|
|
const beforeUpload = (file: File) => {
|
|
const isImage = file.type.startsWith('image/');
|
|
if (!isImage) {
|
|
message.error('Chỉ cho phép upload ảnh!');
|
|
}
|
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
|
if (!isLt5M) {
|
|
message.error('Ảnh phải nhỏ hơn 5MB!');
|
|
}
|
|
return isImage && isLt5M;
|
|
};
|
|
|
|
const handleOk = () => {
|
|
if (onChange) {
|
|
onChange(selectedItems);
|
|
}
|
|
setOpen(false);
|
|
};
|
|
|
|
const columns: ProColumns<SgwModel.TripCrews>[] = [
|
|
{
|
|
title: 'Mã định danh',
|
|
dataIndex: ['Person', 'personal_id'],
|
|
},
|
|
{
|
|
title: 'Tên thuyền viên',
|
|
dataIndex: ['Person', 'name'],
|
|
},
|
|
{
|
|
title: 'SĐT',
|
|
dataIndex: ['Person', 'phone'],
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: 'Vai trò',
|
|
dataIndex: 'role', // ✅ Lấy trực tiếp từ TripCrews.role
|
|
hideInSearch: true,
|
|
render: (val: unknown) => {
|
|
const role = val as string;
|
|
switch (role) {
|
|
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>{role}</span>;
|
|
}
|
|
},
|
|
},
|
|
{
|
|
key: 'image',
|
|
title: 'Hình ảnh',
|
|
dataIndex: 'image',
|
|
hideInSearch: true,
|
|
render: (_, crewRecord) => <CrewPhoto record={crewRecord} />,
|
|
},
|
|
{
|
|
title: (
|
|
<FormattedMessage id="pages.things.option" defaultMessage="Operating" />
|
|
),
|
|
hideInSearch: true,
|
|
render: (_, crewRecord) => {
|
|
return (
|
|
<>
|
|
<EditButton
|
|
text="Edit"
|
|
onClick={() => {
|
|
setCurrentRow(crewRecord);
|
|
setEditCrewModalVisible(true);
|
|
}}
|
|
/>
|
|
|
|
<Divider type="vertical" />
|
|
|
|
<UploadPhoto
|
|
text={intl.formatMessage({
|
|
id: 'pages.ship.update.photo',
|
|
defaultMessage: 'Edit Photo',
|
|
})}
|
|
onClick={() => {
|
|
setCurrentRow(crewRecord);
|
|
handleUploadPhotoModalVisible(true);
|
|
}}
|
|
/>
|
|
|
|
<Divider type="vertical" />
|
|
|
|
<DeleteButton
|
|
title="Bạn có chắc muốn xoá người này không?"
|
|
text="Delete"
|
|
onOk={async () => {
|
|
try {
|
|
if (crewRecord.Person?.personal_id && record.id) {
|
|
await deleteTripCrew(
|
|
record.id,
|
|
crewRecord.Person.personal_id,
|
|
);
|
|
message.success('Xoá thành công');
|
|
actionRef.current?.reload();
|
|
}
|
|
} catch (err) {
|
|
console.error('Delete error:', err);
|
|
message.error('Xoá thất bại');
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Tooltip
|
|
title={intl.formatMessage({
|
|
id: 'pages.trips.crew.title',
|
|
defaultMessage: 'Danh sách thành viên',
|
|
})}
|
|
>
|
|
<Button
|
|
key="listCrew"
|
|
size="small"
|
|
onClick={() => setOpen(true)}
|
|
icon={<UserOutlined />}
|
|
/>
|
|
</Tooltip>
|
|
|
|
<Modal
|
|
open={open}
|
|
title="Danh sách thành viên"
|
|
onOk={handleOk}
|
|
onCancel={() => setOpen(false)}
|
|
width={1000}
|
|
>
|
|
<ProCard split="vertical" bodyStyle={{ padding: 0 }}>
|
|
<ProCard bodyStyle={{ padding: 0 }}>
|
|
<ProTable<SgwModel.TripCrews>
|
|
tableLayout="auto"
|
|
actionRef={actionRef}
|
|
columns={columns}
|
|
search={{
|
|
labelWidth: 'auto',
|
|
span: 12,
|
|
}}
|
|
pagination={{ pageSize: PAGE_SIZE }}
|
|
request={async (params) => {
|
|
const resp = await getTripCrew(record.id);
|
|
let crews = resp?.trip_crews || [];
|
|
if (params.name) {
|
|
crews = crews.filter((c) =>
|
|
c.Person?.name
|
|
?.toLowerCase()
|
|
.includes(params.name.toLowerCase()),
|
|
);
|
|
}
|
|
return {
|
|
success: true,
|
|
data: crews,
|
|
total: crews.length,
|
|
};
|
|
}}
|
|
toolBarRender={() => [
|
|
<Button
|
|
type="primary"
|
|
key="primary"
|
|
onClick={() => {
|
|
setAddCrewModalVisible(true);
|
|
}}
|
|
>
|
|
<UserOutlined />{' '}
|
|
<FormattedMessage
|
|
id="pages.things.create.text"
|
|
defaultMessage="New"
|
|
/>
|
|
</Button>,
|
|
]}
|
|
rowKey="id"
|
|
dateFormatter="string"
|
|
/>
|
|
|
|
{addCrewModalVisible && (
|
|
<AddCrew
|
|
tripId={record?.id}
|
|
visible={addCrewModalVisible}
|
|
onVisibleChange={setAddCrewModalVisible}
|
|
onSuccess={() => {
|
|
setAddCrewModalVisible(false);
|
|
actionRef.current?.reload();
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{editCrewModalVisible && currentRow && (
|
|
<EditCrew
|
|
record={currentRow}
|
|
tripId={record.id}
|
|
visible={editCrewModalVisible}
|
|
onVisibleChange={(visible) => {
|
|
setEditCrewModalVisible(visible);
|
|
if (!visible) setCurrentRow(undefined);
|
|
}}
|
|
onSuccess={() => {
|
|
setEditCrewModalVisible(false);
|
|
setCurrentRow(undefined);
|
|
actionRef.current?.reload();
|
|
}}
|
|
/>
|
|
)}
|
|
</ProCard>
|
|
</ProCard>
|
|
</Modal>
|
|
|
|
{uploadPhotoModalVisible && currentRow && (
|
|
<ModalForm
|
|
title={intl.formatMessage({
|
|
id: 'pages.ship.upload.photo.title',
|
|
defaultMessage: 'Upload Ship Photo',
|
|
})}
|
|
width="400px"
|
|
open={uploadPhotoModalVisible}
|
|
onOpenChange={(visible) => {
|
|
if (!visible) {
|
|
setCurrentRow(undefined);
|
|
setFileList([]);
|
|
}
|
|
handleUploadPhotoModalVisible(visible);
|
|
}}
|
|
onFinish={async () => {
|
|
const success = await handleUploadPhoto();
|
|
if (success) {
|
|
handleUploadPhotoModalVisible(false);
|
|
setFileList([]);
|
|
if (actionRef.current) {
|
|
actionRef.current.reload();
|
|
}
|
|
}
|
|
return success;
|
|
}}
|
|
>
|
|
<ProForm.Item
|
|
name="photo_crew"
|
|
label="Ảnh thuyền viên"
|
|
rules={[{ required: true, message: 'Vui lòng chọn ảnh' }]}
|
|
>
|
|
<Upload
|
|
listType="picture-card"
|
|
fileList={fileList}
|
|
onChange={handleUploadChange}
|
|
beforeUpload={beforeUpload}
|
|
onPreview={async (file) => {
|
|
let src = file.url;
|
|
if (!src && file.originFileObj) {
|
|
src = await new Promise<string>((resolve) => {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file.originFileObj as Blob);
|
|
reader.onload = () => resolve(reader.result as string);
|
|
});
|
|
}
|
|
const imgWindow = window.open('');
|
|
imgWindow?.document.write(`
|
|
<div style="display:flex;justify-content:center;align-items:center;min-height:100vh;background:#000;">
|
|
<img src="${src}" style="max-width:100%;max-height:100%;object-fit:contain;" />
|
|
</div>
|
|
`);
|
|
}}
|
|
maxCount={1}
|
|
accept="image/*"
|
|
>
|
|
{fileList.length < 1 && (
|
|
<div
|
|
style={{
|
|
width: '250px',
|
|
height: '100px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
border: '1px dashed #d9d9d9',
|
|
borderRadius: '6px',
|
|
backgroundColor: '#fafafa',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<PlusOutlined style={{ fontSize: '28px', color: '#999' }} />
|
|
<div
|
|
style={{ marginTop: 12, fontSize: '16px', color: '#666' }}
|
|
>
|
|
Tải ảnh lên
|
|
</div>
|
|
<div
|
|
style={{ fontSize: '12px', color: '#999', marginTop: 4 }}
|
|
>
|
|
JPG, PNG ≤ 5MB
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Upload>
|
|
</ProForm.Item>
|
|
</ModalForm>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default TripCrews;
|