Files
SMATEC-FRONTEND/src/pages/Slave/SGW/Trip/components/TripCrews.tsx

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;