diff --git a/.umirc.sgw.ts b/.umirc.sgw.ts index 0c801fd..864ebb0 100644 --- a/.umirc.sgw.ts +++ b/.umirc.sgw.ts @@ -23,6 +23,13 @@ export default defineConfig({ path: '/map', component: './Slave/SGW/Map', }, + { + name: 'sgw.ships', + icon: 'icon-ship', + path: '/ships', + component: './Slave/SGW/Ship', + access: 'canEndUser_User', + }, { name: 'sgw.trips', icon: 'icon-trip', diff --git a/ZONE_MIGRATION.md b/ZONE_MIGRATION.md new file mode 100644 index 0000000..50ffdb5 --- /dev/null +++ b/ZONE_MIGRATION.md @@ -0,0 +1,185 @@ +# ✅ Zone (Banzone) API Migration - Complete + +## Migration Banzone/Zone API vào SGW Module + +### Files Created + +1. **ZoneController.ts** - `src/services/slave/sgw/ZoneController.ts` + + ```typescript + ✅ apiGetAllBanzones(body) - Get all banzones with pagination + ✅ apiRemoveBanzone(id, groupID) - Remove a banzone + ✅ apiGetZoneById(zoneId) - Get banzone by ID + ✅ apiCreateBanzone(body) - Create new banzone + ✅ apiUpdateBanzone(id, body) - Update banzone + ``` + +2. **Type Definitions** - Added to `src/services/slave/sgw/sgw.typing.d.ts` + + ```typescript + ✅ Banzone + ✅ Condition + ✅ Geom + ✅ ZoneResponse + ✅ ZoneBodyRequest + ``` + +3. **Route Constants** - Added to `src/constants/slave/sgw/routes.ts` + ```typescript + ✅ SGW_ROUTE_BANZONES = '/api/sgw/banzones' + ✅ SGW_ROUTE_BANZONES_LIST = '/api/sgw/banzones/list' + ``` + +### Files Updated + +- ✅ `src/pages/Slave/SGW/Map/components/ShipDetail.tsx` + - Updated import: `@/services/controller/ZoneController` → `@/services/slave/sgw/ZoneController` + - Updated types: `API.Thing` → `SgwModel.SgwThing` + - Updated types: `API.UserResponse` → `MasterModel.UserResponse` + - Updated types: `API.Geom` → `SgwModel.Geom` + +### Migration Changes + +**Before:** + +```typescript +import { apiGetZoneById } from '@/services/controller/ZoneController'; + +thing: API.Thing +const zone_geom: API.Geom = ... +``` + +**After:** + +```typescript +import { apiGetZoneById } from '@/services/slave/sgw/ZoneController'; + +thing: SgwModel.SgwThing +const zone_geom: SgwModel.Geom = ... +``` + +### Type Definitions + +```typescript +declare namespace SgwModel { + interface Banzone { + id?: string; + name?: string; + province_code?: string; + type?: number; + conditions?: Condition[]; + description?: string; + geometry?: string; + enabled?: boolean; + created_at?: Date; + updated_at?: Date; + } + + interface Condition { + max?: number; + min?: number; + type?: 'length_limit' | 'month_range' | 'date_range'; + to?: number; + from?: number; + } + + interface Geom { + geom_type?: number; + geom_poly?: string; + geom_lines?: string; + geom_point?: string; + geom_radius?: number; + } + + interface ZoneResponse { + total?: number; + offset?: number; + limit?: number; + banzones?: Banzone[]; + } + + interface ZoneBodyRequest { + name?: string; + province_code?: string; + type?: number; + conditions?: Condition[]; + description?: string; + geometry?: string; + enabled?: boolean; + } +} +``` + +### API Usage Examples + +**Get Zone by ID:** + +```typescript +import { apiGetZoneById } from '@/services/slave/sgw/ZoneController'; + +const zone = await apiGetZoneById('zone-123'); +const geometry: SgwModel.Geom = JSON.parse(zone.geometry || '{}'); +``` + +**Create Banzone:** + +```typescript +import { apiCreateBanzone } from '@/services/slave/sgw/ZoneController'; + +const zoneData: SgwModel.ZoneBodyRequest = { + name: 'Vùng cấm mùa sinh sản', + province_code: 'QN', + type: 1, + enabled: true, + conditions: [ + { + type: 'month_range', + from: 4, + to: 8, + }, + ], + geometry: JSON.stringify({ + geom_type: 1, + geom_poly: 'POLYGON(...)', + }), +}; + +await apiCreateBanzone(zoneData); +``` + +**Get All Banzones:** + +```typescript +import { apiGetAllBanzones } from '@/services/slave/sgw/ZoneController'; + +const response = await apiGetAllBanzones({ + offset: 0, + limit: 20, + order: 'name', + dir: 'asc', +}); +``` + +## Status: ✅ Complete + +- ✅ 5 API functions migrated +- ✅ 5 type definitions added +- ✅ 2 route constants added +- ✅ 1 file updated (ShipDetail.tsx) +- ✅ 0 compilation errors + +## Total SGW Migration Progress + +| Module | APIs | Types | Status | +| --------- | ------ | ------- | ------ | +| Ship | 12 | 15+ | ✅ | +| Trip | 17 | 21+ | ✅ | +| Photo | 2 | 2 | ✅ | +| Zone | 5 | 5 | ✅ | +| **TOTAL** | **36** | **43+** | ✅ | + +--- + +**Migration Date:** January 23, 2026 +**Status:** ✅ Complete +**Ready for Testing:** YES diff --git a/src/assets/alarm_icon.png b/src/assets/alarm_icon.png new file mode 100644 index 0000000..7cbf415 Binary files /dev/null and b/src/assets/alarm_icon.png differ diff --git a/src/assets/exclamation.png b/src/assets/exclamation.png new file mode 100644 index 0000000..7994675 Binary files /dev/null and b/src/assets/exclamation.png differ diff --git a/src/assets/marker.png b/src/assets/marker.png new file mode 100644 index 0000000..e8b957a Binary files /dev/null and b/src/assets/marker.png differ diff --git a/src/assets/ship_alarm.png b/src/assets/ship_alarm.png new file mode 100644 index 0000000..65e18ae Binary files /dev/null and b/src/assets/ship_alarm.png differ diff --git a/src/assets/ship_alarm_2.png b/src/assets/ship_alarm_2.png new file mode 100644 index 0000000..5c9dace Binary files /dev/null and b/src/assets/ship_alarm_2.png differ diff --git a/src/assets/ship_alarm_fishing.png b/src/assets/ship_alarm_fishing.png new file mode 100644 index 0000000..3613e36 Binary files /dev/null and b/src/assets/ship_alarm_fishing.png differ diff --git a/src/assets/ship_online.png b/src/assets/ship_online.png new file mode 100644 index 0000000..de442dd Binary files /dev/null and b/src/assets/ship_online.png differ diff --git a/src/assets/ship_online_fishing.png b/src/assets/ship_online_fishing.png new file mode 100644 index 0000000..009a227 Binary files /dev/null and b/src/assets/ship_online_fishing.png differ diff --git a/src/assets/ship_undefine.png b/src/assets/ship_undefine.png new file mode 100644 index 0000000..bdec138 Binary files /dev/null and b/src/assets/ship_undefine.png differ diff --git a/src/assets/ship_warning.png b/src/assets/ship_warning.png new file mode 100644 index 0000000..0ddeb79 Binary files /dev/null and b/src/assets/ship_warning.png differ diff --git a/src/assets/ship_warning_fishing.png b/src/assets/ship_warning_fishing.png new file mode 100644 index 0000000..fb8838b Binary files /dev/null and b/src/assets/ship_warning_fishing.png differ diff --git a/src/assets/sos_icon.png b/src/assets/sos_icon.png new file mode 100644 index 0000000..906cb4d Binary files /dev/null and b/src/assets/sos_icon.png differ diff --git a/src/assets/warning_icon.png b/src/assets/warning_icon.png new file mode 100644 index 0000000..48f853d Binary files /dev/null and b/src/assets/warning_icon.png differ diff --git a/src/components/shared/Button.tsx b/src/components/shared/Button.tsx new file mode 100644 index 0000000..057ad95 --- /dev/null +++ b/src/components/shared/Button.tsx @@ -0,0 +1,62 @@ +import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; +import { Button, Popconfirm, Tooltip } from 'antd'; +import { useState } from 'react'; + +/* ======================= + DeleteButton +======================= */ + +interface DeleteButtonProps { + title: string; + text: string; + onOk: () => void | Promise; +} + +export const DeleteButton: React.FC = ({ + title, + text, + onOk, +}) => { + const [visible, setVisible] = useState(false); + + const handleConfirm = async () => { + await onOk(); + setVisible(false); + }; + + return ( + setVisible(false)} + > + + + + + ); +}; diff --git a/src/pages/Slave/SGW/Map/components/ShipIconStyle.tsx b/src/pages/Slave/SGW/Map/components/ShipIconStyle.tsx new file mode 100644 index 0000000..1bc8508 --- /dev/null +++ b/src/pages/Slave/SGW/Map/components/ShipIconStyle.tsx @@ -0,0 +1,69 @@ +import { getShipIcon } from '@/services/slave/sgw/MapService'; +import { Fill, Icon, Stroke, Style, Text } from 'ol/style'; +import CircleStyle from 'ol/style/Circle'; +import { GPSParseResult, PointData } from '../type'; + +export const getShipStyleFromData = ({ + state_level, + gpsData, + scale, + pointData, +}: { + state_level: number; + gpsData: GPSParseResult; + scale: number; + pointData: PointData; +}) => { + if (pointData.type === 'main-point') { + return new Style({ + text: pointData.description + ? new Text({ + text: pointData.description, + font: '14px Arial', + fill: new Fill({ color: 'black' }), + stroke: new Stroke({ + color: 'white', + width: 2, + }), + offsetY: -40, + textAlign: 'center', + textBaseline: 'bottom', + }) + : undefined, + image: new Icon({ + anchor: [0.5, 30], // Điểm neo: giữa theo X, 30px theo Y + anchorOrigin: 'top-left', + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + scale: scale, + src: getShipIcon(state_level, gpsData.fishing || false), + rotateWithView: false, + rotation: ((gpsData.h || 0) * Math.PI) / 180, + crossOrigin: 'anonymous', + }), + }); + } else if (pointData.type === 'sos-point') { + // Style cơ bản cho SOS point - hình tròn đỏ ở giữa + return new Style({ + image: new Icon({ + anchor: [0.5, 30], // Điểm neo: giữa theo X, 30px theo Y + anchorOrigin: 'top-left', + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + scale: scale, + src: getShipIcon(state_level, gpsData.fishing || false), + rotateWithView: false, + rotation: ((gpsData.h || 0) * Math.PI) / 180, + crossOrigin: 'anonymous', + }), + }); + } else { + return new Style({ + image: new CircleStyle({ + radius: 5, + fill: new Fill({ color: 'red' }), + stroke: new Stroke({ color: 'white', width: 2 }), + }), + }); + } +}; diff --git a/src/pages/Slave/SGW/Map/components/ShipSearchForm.tsx b/src/pages/Slave/SGW/Map/components/ShipSearchForm.tsx new file mode 100644 index 0000000..d70fe54 --- /dev/null +++ b/src/pages/Slave/SGW/Map/components/ShipSearchForm.tsx @@ -0,0 +1,407 @@ +import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup'; +import { apiGetShipGroups } from '@/services/slave/sgw/ShipController'; +import { + ModalForm, + ProForm, + ProFormInstance, + ProFormSlider, +} from '@ant-design/pro-components'; +import { FormattedMessage, useIntl, useModel } from '@umijs/max'; +import { AutoComplete, Col, Flex, Row, Select, Tooltip } from 'antd'; +import { useEffect, useRef, useState } from 'react'; + +interface SearchShipProps { + initialValues?: Partial; + things?: SgwModel.SgwThing[]; + isModalOpen: boolean; + onClose: any; + onSubmit: (values: SearchShipResponse) => void; +} + +export interface SearchShipResponse { + ship_name?: string; + ship_length?: [number, number]; + reg_number?: string; + ship_power?: [number, number]; + ship_type?: string | number; + alarm_list?: string; + ship_group_id?: string; + group_id?: string | string[]; +} + +const ShipSearchForm = ({ + initialValues, + things, + isModalOpen, + onClose, + onSubmit, +}: SearchShipProps) => { + const intl = useIntl(); + const { initialState } = useModel('@@initialState'); + const currentUser = initialState?.currentUserProfile; + const [group_id, setGroupId] = useState(null); + + const [groupShips, setGroupShips] = useState< + SgwModel.GroupShipResponse[] | [] + >([]); + const [shipNames, setShipNames] = useState([]); + const [shipNameOptions, setShipNameOptions] = useState< + { value: string; key: number }[] + >([]); + const [shipRegNumberOptions, setShipRegNumberOptions] = useState< + { value: string; key: number }[] + >([]); + const formRef = useRef>(); + // console.log('InitialValue ', initialValues); + + useEffect(() => { + if (initialValues === undefined && formRef.current) { + formRef.current.resetFields(); + } + }, [initialValues]); + const [shipRegNumbers, setShipRegNumbers] = useState([]); + + const { shipTypes, getShipTypes } = useModel('slave.sgw.useShipTypes'); + + useEffect(() => { + if (!shipTypes) { + getShipTypes(); + } + }, [shipTypes]); + + const getListShipNames = () => { + if (things && Array.isArray(things)) { + const names = things + .map((item) => item?.metadata?.ship_name) + .filter((name): name is string => Boolean(name)); // bỏ null/undefined/"" nếu có + setShipNames(names); + } + }; + const handleGroupSelect = (group: string | string[] | null) => { + // console.log("Selected group key:", value); + setGroupId(group); + }; + + const handleSearchShipName = (value: string) => { + if (!value) { + setShipNameOptions([]); // không hiện gợi ý khi chưa nhập + return; + } + const filtered = shipNames + .filter((name) => name.toLowerCase().includes(value.toLowerCase())) + .map((name, index) => ({ value: name, key: index })); + setShipNameOptions(filtered); + }; + + const getListShipRegNumbers = () => { + if (things && Array.isArray(things)) { + const regNumbers = things + .map((item) => item?.metadata?.ship_reg_number) + .filter((reg): reg is string => Boolean(reg)); // bỏ null/undefined/"" nếu có + setShipRegNumbers(regNumbers); + } + }; + const handleSearchShipRegNumber = (value: string) => { + if (!value) { + setShipRegNumberOptions([]); // không hiện gợi ý khi chưa nhập + return; + } + const filtered = shipRegNumbers + .filter((regNumber) => + regNumber.toLowerCase().includes(value.toLowerCase()), + ) + .map((regNumber, index) => ({ value: regNumber, key: index })); + setShipRegNumberOptions(filtered); + }; + + const getShipGroupByOwner = async () => { + // console.log("Gọi hàm lấy ra đội tàu"); + + try { + const groups = await apiGetShipGroups(); + setGroupShips(groups); + // console.log("Groups: ", groups); + } catch (error) { + console.error('Error when get ShipGroup: ', error); + } + }; + + useEffect(() => { + if (isModalOpen) { + getListShipNames(); + getListShipRegNumbers(); + if (currentUser?.metadata?.user_type === 'enduser') { + getShipGroupByOwner(); + } + } + // console.log("InitialData: ", initialData); + }, [isModalOpen, things]); + + const alarmListLabel = [ + { + label: 'Tiếp cận vùng hạn chế', + value: '50:10', + }, + { + label: 'Đã ra (vào) vùng hạn chế)', + value: '50:11', + }, + { + label: 'Đang đánh bắt trong vùng hạn chế', + value: '50:12', + }, + ]; + // console.log( + // 'ShipSearchForm render - isModalOpen:', + // isModalOpen, + // 'things length:', + // things?.length, + // ); + + return ( + + {intl.formatMessage({ + id: 'map.filter.name', + defaultMessage: 'Filter', + })} + + } + open={isModalOpen} + onOpenChange={(open) => { + // console.log('Modal onOpenChange:', open); + if (!open) { + onClose(false); + } + }} + formRef={formRef} + width={600} + initialValues={initialValues} + onFinish={async (values) => { + // console.log("Values: ", values); + // await onSubmit(values); + const dataFinal: SearchShipResponse = { + ship_name: values.ship_name, + ship_length: values.ship_length, + reg_number: values.reg_number, + ship_power: values.ship_power, + ship_type: values.ship_type, + alarm_list: values.alarm_list, + ship_group_id: values.ship_group_id, + group_id: Array.isArray(group_id) + ? group_id.join(',') + : group_id || undefined, + }; + // console.log("Values: ", dataFinal); + onSubmit(dataFinal); + onClose(false); + }} + modalProps={{ + cancelText: ( + + ), + okText: ( + + ), + }} + layout="vertical" + style={{ padding: '24px' }} + > + + + + + + + + + + + + + `${value}m`, + }, + }} + /> + + + `${value}kW`, + }, + }} + /> + + + + + + + + + + + + + + + + {currentUser?.metadata?.user_type === 'enduser' && ( + + +