From f64d857fb4000b763a3a5b31598b3027bb1ebc4a Mon Sep 17 00:00:00 2001 From: Falanger-debug Date: Fri, 19 Jun 2026 18:20:16 +0200 Subject: [PATCH] feat: SKY-38: integrate UI with existing pilots endpoints --- .../application/pilot/PatchPilotUseCase.java | 4 + .../config/GlobalExceptionHandler.java | 16 ++- .../src/components/pilots/PilotDialog.vue | 18 ++- skyroster-frontend/src/data/mockData.js | 28 ++-- skyroster-frontend/src/stores/pilots.js | 127 +++++++++++++----- skyroster-frontend/src/views/PilotsView.vue | 38 ++++-- 6 files changed, 175 insertions(+), 56 deletions(-) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java index 5e8b88c..8c23020 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/pilot/PatchPilotUseCase.java @@ -37,6 +37,10 @@ public PilotResponse patchPilot(UUID id, PilotPatchRequest request) { if (surname != null) { pilot.setSurname(surname); } + String licence = request.getLicence(); + if (licence != null) { + pilot.setLicence(licence); + } String icaoCode = request.getOperationalBaseIcaoCode(); if (icaoCode != null) { pilot.setOperationalBase( diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java index fe78514..cfa9b65 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -71,8 +71,22 @@ public ResponseEntity handleRuleViolation(RuleViolat @ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity handleDataIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest request) { + String causeMessage = ex.getMostSpecificCause().getMessage(); + if (causeMessage != null) { + String lower = causeMessage.toLowerCase(); + if (lower.contains("value too long") || lower.contains("too long for type")) { + return buildResponse(HttpStatus.BAD_REQUEST, + "Podana wartość jest zbyt długa", + request); + } + if (lower.contains("flight") || lower.contains("pilot_id") || lower.contains("aircraft_id")) { + return buildResponse(HttpStatus.CONFLICT, + "Nie można usunąć samolotu, ponieważ ma przypisane loty", + request); + } + } return buildResponse(HttpStatus.CONFLICT, - "Aircraft cannot be deleted because it has assigned flights", + "Operacja narusza ograniczenia integralności danych", request); } diff --git a/skyroster-frontend/src/components/pilots/PilotDialog.vue b/skyroster-frontend/src/components/pilots/PilotDialog.vue index 4ddc4ab..e16aed5 100644 --- a/skyroster-frontend/src/components/pilots/PilotDialog.vue +++ b/skyroster-frontend/src/components/pilots/PilotDialog.vue @@ -18,6 +18,7 @@ const emit = defineEmits(['update:visible', 'save']) const pilotsStore = usePilotsStore() const formData = ref(getEmptyForm()) +const submitting = ref(false) const dialogVisible = computed({ get: () => props.visible, @@ -26,6 +27,16 @@ const dialogVisible = computed({ const dialogTitle = computed(() => props.isEditMode ? 'Edytuj pilota' : 'Dodaj pilota') +const isFormValid = computed(() => { + const f = formData.value + return ( + f.imie?.trim().length > 0 && + f.nazwisko?.trim().length > 0 && + f.bazaMacierzysta != null && + f.kwalifikacje?.length > 0 + ) +}) + function getEmptyForm() { return { imie: '', @@ -45,8 +56,11 @@ function resetForm() { } } -function handleSave() { +async function handleSave() { + if (!isFormValid.value) return + submitting.value = true emit('save', { ...formData.value }) + submitting.value = false dialogVisible.value = false } @@ -121,7 +135,7 @@ function handleCancel() { diff --git a/skyroster-frontend/src/data/mockData.js b/skyroster-frontend/src/data/mockData.js index 1d54ef5..f56335a 100644 --- a/skyroster-frontend/src/data/mockData.js +++ b/skyroster-frontend/src/data/mockData.js @@ -1,19 +1,19 @@ export const QUALIFICATIONS = [ - { label: 'PPL', value: 'PPL' }, - { label: 'CPL', value: 'CPL' }, - { label: 'ATPL', value: 'ATPL' }, - { label: 'IFR', value: 'IFR' }, - { label: 'Multi-Engine', value: 'ME' }, - { label: 'Night Rating', value: 'NR' } + { id: 'd0000000-0000-0000-0000-000000000001', label: 'PPL', value: 'PPL' }, + { id: 'd0000000-0000-0000-0000-000000000002', label: 'CPL', value: 'CPL' }, + { id: 'd0000000-0000-0000-0000-000000000003', label: 'ATPL', value: 'ATPL' }, + { id: 'd0000000-0000-0000-0000-000000000004', label: 'IFR', value: 'IFR' }, + { id: 'd0000000-0000-0000-0000-000000000005', label: 'Multi-Engine', value: 'Multi-Engine' }, + { id: 'd0000000-0000-0000-0000-000000000006', label: 'Night Rating', value: 'Night Rating' } ] export const AIRCRAFT_TYPES = [ - { label: 'Cessna 172', value: 'C172' }, - { label: 'Cessna 152', value: 'C152' }, - { label: 'Piper PA-28', value: 'PA28' }, - { label: 'Diamond DA40', value: 'DA40' }, - { label: 'Diamond DA42', value: 'DA42' }, - { label: 'Beechcraft King Air', value: 'BE90' } + { id: 'b0000000-0000-0000-0000-000000000004', label: 'Cessna 172', value: 'C172' }, + { id: 'b0000000-0000-0000-0000-000000000005', label: 'Cessna 152', value: 'C152' }, + { id: 'b0000000-0000-0000-0000-000000000006', label: 'Piper PA-28', value: 'PA28' }, + { id: 'b0000000-0000-0000-0000-000000000007', label: 'Diamond DA40', value: 'DA40' }, + { id: 'b0000000-0000-0000-0000-000000000008', label: 'Diamond DA42', value: 'DA42' }, + { id: 'b0000000-0000-0000-0000-000000000009', label: 'Beechcraft King Air', value: 'BE90' } ] export const BASES = [ @@ -48,7 +48,7 @@ export const initialPilots = [ imie: 'Jan', nazwisko: 'Kowalski', licencja: 'PL-CPL-12345', - kwalifikacje: ['CPL', 'IFR', 'ME'], + kwalifikacje: ['CPL', 'IFR', 'Multi-Engine'], typySamolotow: ['C172', 'PA28', 'DA42'], bazaMacierzysta: 'EPWA' }, @@ -57,7 +57,7 @@ export const initialPilots = [ imie: 'Anna', nazwisko: 'Nowak', licencja: 'PL-ATPL-67890', - kwalifikacje: ['ATPL', 'IFR', 'ME', 'NR'], + kwalifikacje: ['ATPL', 'IFR', 'Multi-Engine', 'Night Rating'], typySamolotow: ['DA42', 'BE90'], bazaMacierzysta: 'EPKK' }, diff --git a/skyroster-frontend/src/stores/pilots.js b/skyroster-frontend/src/stores/pilots.js index 5a5d49d..800ee0f 100644 --- a/skyroster-frontend/src/stores/pilots.js +++ b/skyroster-frontend/src/stores/pilots.js @@ -1,49 +1,112 @@ import { ref, computed } from 'vue' import { defineStore } from 'pinia' -import { initialPilots, QUALIFICATIONS, AIRCRAFT_TYPES, BASES } from '../data/mockData' +import { QUALIFICATIONS, AIRCRAFT_TYPES, BASES } from '../data/mockData' import apiClient from '../api/axios' export const usePilotsStore = defineStore('pilots', () => { - const pilots = ref([...initialPilots]) + const pilots = ref([]) const qualificationOptions = QUALIFICATIONS const aircraftTypeOptions = AIRCRAFT_TYPES const baseOptions = BASES const pilotsCount = computed(() => pilots.value.length) - function generateId() { - const maxId = pilots.value.reduce((max, pilot) => { - const num = parseInt(pilot.id.replace('P', '')) - return num > max ? num : max - }, 0) - return `P${String(maxId + 1).padStart(3, '0')}` + function resolveQualification(value, rawQualifications) { + const fromPilot = (rawQualifications ?? []).find(q => q.values === value || q.value === value) + if (fromPilot) return fromPilot + for (const pilot of pilots.value) { + const match = (pilot._rawQualifications ?? []).find(q => q.values === value || q.value === value) + if (match) return match + } + const opt = QUALIFICATIONS.find(o => o.value === value) + return { id: opt?.id ?? null, values: value, label: opt?.label ?? value } } - function addPilot(pilotData) { - const newPilot = { - ...pilotData, - id: generateId() + function resolveAircraftType(icaoCode, rawAircraftTypes) { + const fromPilot = (rawAircraftTypes ?? []).find(t => t.icaoCode === icaoCode) + if (fromPilot) return fromPilot + for (const pilot of pilots.value) { + const match = (pilot._rawAircraftTypes ?? []).find(t => t.icaoCode === icaoCode) + if (match) return match } - pilots.value.push(newPilot) - return newPilot + const opt = AIRCRAFT_TYPES.find(o => o.value === icaoCode) + return { id: opt?.id ?? null, icaoCode, name: opt?.label ?? icaoCode } } - function updatePilot(id, pilotData) { - const index = pilots.value.findIndex(p => p.id === id) - if (index !== -1) { - pilots.value[index] = { ...pilots.value[index], ...pilotData } - return pilots.value[index] + function mapToCreateRequest(pilotData) { + const base = BASES.find(b => b.value === pilotData.bazaMacierzysta) + return { + firstName: pilotData.imie, + lastName: pilotData.nazwisko, + licence: pilotData.licencja, + homeBase: base ? { id: base.id, icaoCode: base.icaoCode, name: base.label } : null, + qualifications: (pilotData.kwalifikacje ?? []).map(q => resolveQualification(q, pilotData._rawQualifications)), + aircraftTypes: (pilotData.typySamolotow ?? []).map(t => resolveAircraftType(t, pilotData._rawAircraftTypes)) } - return null } - function deletePilot(id) { - const index = pilots.value.findIndex(p => p.id === id) - if (index !== -1) { - pilots.value.splice(index, 1) - return true + function mapToPatchRequest(pilotData) { + const base = BASES.find(b => b.value === pilotData.bazaMacierzysta) + return { + name: pilotData.imie, + surname: pilotData.nazwisko, + licence: pilotData.licencja, + operationalBaseIcaoCode: base?.icaoCode ?? pilotData.bazaMacierzysta, + qualifications: (pilotData.kwalifikacje ?? []).map(q => resolveQualification(q, pilotData._rawQualifications)), + aircraftTypes: (pilotData.typySamolotow ?? []).map(t => resolveAircraftType(t, pilotData._rawAircraftTypes)) + } + } + + async function addPilot(pilotData) { + try { + const requestBody = mapToCreateRequest(pilotData) + const { data } = await apiClient.post('/pilots', requestBody) + const mapped = mapApiPilot(data) + pilots.value.push(mapped) + return { success: true, pilot: mapped } + } catch (error) { + console.error('addPilot failed', error) + return { + success: false, + message: error.response?.data?.message || 'Nie udało się dodać pilota' + } + } + } + + async function updatePilot(id, pilotData) { + try { + const requestBody = mapToPatchRequest(pilotData) + const { data } = await apiClient.patch(`/pilots/${id}`, requestBody) + const mapped = mapApiPilot(data) + const index = pilots.value.findIndex(p => p.id === id) + if (index !== -1) { + pilots.value[index] = mapped + } + return { success: true, pilot: mapped } + } catch (error) { + console.error('updatePilot failed', error) + return { + success: false, + message: error.response?.data?.message || 'Nie udało się zaktualizować pilota' + } + } + } + + async function deletePilot(id) { + try { + await apiClient.delete(`/pilots/${id}`) + const index = pilots.value.findIndex(p => p.id === id) + if (index !== -1) { + pilots.value.splice(index, 1) + } + return { success: true } + } catch (error) { + console.error('deletePilot failed', error) + return { + success: false, + message: error.response?.data?.message || 'Nie udało się usunąć pilota' + } } - return false } function getPilotById(id) { @@ -57,7 +120,7 @@ export const usePilotsStore = defineStore('pilots', () => { async function loadPilots() { try { - const { data } = await apiClient.get('/pilots', { params: { page: 0, size: 100 } }) + const { data } = await apiClient.get('/pilots', { params: { page: 0, size: 100, sort: 'surname,asc' } }) const items = data.content ?? data ?? [] pilots.value = items.map(mapApiPilot) return true @@ -75,14 +138,18 @@ export const usePilotsStore = defineStore('pilots', () => { } function mapApiPilot(p) { + const rawQualifications = p.qualifications ?? p.kwalifikacje ?? [] + const rawAircraftTypes = p.aircraftTypes ?? p.typySamolotow ?? [] return { id: p.id, imie: p.firstName ?? p.name ?? p.imie, nazwisko: p.surname ?? p.nazwisko, licencja: p.licence ?? p.licencja, - kwalifikacje: (p.qualifications ?? p.kwalifikacje ?? []).map(q => q.value ?? q), - typySamolotow: (p.aircraftTypes ?? p.typySamolotow ?? []).map(t => t.icaoCode ?? t), - bazaMacierzysta: p.homeBase?.icaoCode ?? p.operationalBase?.icaoCode ?? p.bazaMacierzysta ?? null + kwalifikacje: rawQualifications.map(q => q.values ?? q.value ?? q), + typySamolotow: rawAircraftTypes.map(t => t.icaoCode ?? t), + bazaMacierzysta: p.homeBase?.icaoCode ?? p.operationalBase?.icaoCode ?? p.bazaMacierzysta ?? null, + _rawQualifications: rawQualifications, + _rawAircraftTypes: rawAircraftTypes } } diff --git a/skyroster-frontend/src/views/PilotsView.vue b/skyroster-frontend/src/views/PilotsView.vue index cd450b9..0c5c234 100644 --- a/skyroster-frontend/src/views/PilotsView.vue +++ b/skyroster-frontend/src/views/PilotsView.vue @@ -1,5 +1,5 @@