Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,22 @@ public ResponseEntity<RuleViolationErrorResponse> handleRuleViolation(RuleViolat
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) {
.requestMatchers(HttpMethod.DELETE, "/api/pilots/**").hasRole("operations_administrator")
.requestMatchers(HttpMethod.PATCH, "/api/pilots/**").hasRole("operations_administrator")
.requestMatchers(HttpMethod.POST, "/api/pilots/**").hasRole("operations_administrator")
.requestMatchers(HttpMethod.POST,"/api/rule/**").hasRole("compliance_officer")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
)
Expand Down
18 changes: 16 additions & 2 deletions skyroster-frontend/src/components/pilots/PilotDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: '',
Expand All @@ -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
}

Expand Down Expand Up @@ -121,7 +135,7 @@ function handleCancel() {

<template #footer>
<Button label="Anuluj" severity="secondary" @click="handleCancel" />
<Button label="Zapisz" @click="handleSave" />
<Button label="Zapisz" :disabled="!isFormValid || submitting" :loading="submitting" @click="handleSave" />
</template>
</Dialog>
</template>
Expand Down
22 changes: 18 additions & 4 deletions skyroster-frontend/src/components/rules/RuleDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const emit = defineEmits(['update:visible', 'save'])
const rulesStore = useRulesStore()

const formData = ref(getEmptyForm())
const submitting = ref(false)

const dialogVisible = computed({
get: () => props.visible,
Expand All @@ -27,6 +28,16 @@ const dialogVisible = computed({

const dialogTitle = computed(() => props.isEditMode ? 'Edytuj zasadę' : 'Dodaj zasadę')

const isFormValid = computed(() => {
const f = formData.value
return (
f.nazwa?.trim().length > 0 &&
f.typ != null &&
f.okres != null &&
f.wartosc != null && f.wartosc > 0
)
})

const valueLabel = computed(() => {
switch (formData.value.typ) {
case 'max_czas_pracy':
Expand Down Expand Up @@ -58,8 +69,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
}

Expand All @@ -79,7 +93,7 @@ function handleCancel() {
<div class="form-grid">
<div class="form-field full-width">
<label for="nazwa">Nazwa zasady</label>
<InputText id="nazwa" v-model="formData.nazwa" class="w-full" />
<InputText id="nazwa" v-model="formData.nazwa" class="w-full" :maxlength="100" />
</div>

<div class="form-field">
Expand Down Expand Up @@ -115,13 +129,13 @@ function handleCancel() {

<div class="form-field full-width">
<label for="opis">Opis</label>
<Textarea id="opis" v-model="formData.opis" rows="3" class="w-full" />
<Textarea id="opis" v-model="formData.opis" rows="3" class="w-full" :maxlength="500" />
</div>
</div>

<template #footer>
<Button label="Anuluj" severity="secondary" @click="handleCancel" />
<Button label="Zapisz" @click="handleSave" />
<Button label="Zapisz" :disabled="!isFormValid || submitting" :loading="submitting" @click="handleSave" />
</template>
</Dialog>
</template>
Expand Down
28 changes: 14 additions & 14 deletions skyroster-frontend/src/data/mockData.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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'
},
Expand All @@ -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'
},
Expand Down
127 changes: 97 additions & 30 deletions skyroster-frontend/src/stores/pilots.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down
Loading
Loading