admin frontend elkezdése, járművek tisztázása, frontend fejleszts

This commit is contained in:
Roo
2026-06-15 18:52:38 +00:00
parent ef8df9608c
commit 213ba3b0f1
80 changed files with 11615 additions and 2304 deletions

View File

@@ -180,6 +180,7 @@ const errorMessage = ref<string | null>(null)
interface CategoryOption {
id: number
name: string
code?: string
parent_id: number | null
}
@@ -208,14 +209,16 @@ async function fetchCategories() {
window.__allCostCategories = allCategories
} catch (err) {
console.error('[ComplexExpenseModal] Failed to load categories:', err)
// Fallback: provide basic categories
// Fallback: provide basic categories — IDs must match vehicle.cost_categories in DB
mainCategories.value = [
{ id: 1, name: 'Üzemanyag', parent_id: null },
{ id: 2, name: 'Szerviz / Karbantartás', parent_id: null },
{ id: 3, name: 'Parkolás / Útdíj', parent_id: null },
{ id: 1, name: 'Üzemanyag / Töltés', parent_id: null },
{ id: 2, name: 'Szerviz & Karbantartás', parent_id: null },
{ id: 3, name: 'Gumiabroncsok', parent_id: null },
{ id: 4, name: 'Biztosítás', parent_id: null },
{ id: 5, name: 'Adók / Illetékek', parent_id: null },
{ id: 6, name: 'Egyéb', parent_id: null },
{ id: 5, name: 'Adók', parent_id: null },
{ id: 6, name: 'Útdíj & Parkolás', parent_id: null },
{ id: 9, name: 'Ápolás & Kozmetika', parent_id: null },
{ id: 10, name: 'Egyéb', parent_id: null },
]
}
}
@@ -246,22 +249,23 @@ async function handleSubmit() {
let categoryId: number
if (props.isFeeMode) {
// Fee mode: pre-filled as FEES
// Fee mode: pre-filled as FEES — resolve dynamically by code, not by hardcoded ID
costType = 'fee'
categoryId = 3 // Parkolás / Útdíj
const allCats: CategoryOption[] = (window as any).__allCostCategories || []
const feesCat = allCats.find((c: CategoryOption) => c.code === 'FEES' && c.parent_id === null)
categoryId = feesCat?.id || 6 // Fallback to ID 6 if lookup fails
} else {
// Complex mode: use selected sub-category or main category
costType = 'other'
categoryId = Number(form.subCategory || form.mainCategory) || 6
categoryId = Number(form.subCategory || form.mainCategory) || 10
}
const payload = {
asset_id: props.vehicle.id,
// organization_id is omitted — backend auto-resolves from asset
category_id: categoryId,
cost_type: costType,
amount_local: form.amount,
currency_local: 'HUF' as const,
amount_net: form.amount,
currency: 'HUF' as const,
date: new Date(form.date).toISOString(),
mileage_at_cost: null,
description: form.description || (props.isFeeMode ? 'Parkolás / Útdíj' : 'Egyéb költség'),

View File

@@ -13,23 +13,13 @@
</div>
<div class="flex items-center gap-3">
<button
@click="handleAddNewClick"
:disabled="isCheckingQuota"
class="inline-flex items-center gap-2 rounded-xl bg-sf-accent px-5 py-2.5 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed"
@click="$emit('add-new')"
class="inline-flex items-center gap-2 rounded-xl bg-sf-accent px-5 py-2.5 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg"
>
<svg
v-if="isCheckingQuota"
class="h-4 w-4 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<svg v-else class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
{{ isCheckingQuota ? 'Ellenőrzés...' : 'Új jármű hozzáadása' }}
Új jármű hozzáadása
</button>
</div>
</div>
@@ -70,7 +60,7 @@
:key="vehicle.id"
:vehicle="vehicle"
@click="openDetailView(vehicle)"
@edit="openEditDirect(vehicle)"
@edit="$emit('edit-vehicle', vehicle)"
@set-primary="setPrimaryVehicle"
/>
</div>
@@ -86,41 +76,17 @@
<h3 class="text-lg font-semibold text-slate-500">Még nincsenek járművek</h3>
<p class="mt-1 text-sm text-slate-400">Kattints az "Új jármű hozzáadása" gombra az első jármű felvételéhez.</p>
<button
@click="handleAddNewClick"
:disabled="isCheckingQuota"
class="mt-6 inline-flex items-center gap-2 rounded-xl bg-sf-accent px-6 py-3 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed"
@click="$emit('add-new')"
class="mt-6 inline-flex items-center gap-2 rounded-xl bg-sf-accent px-6 py-3 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg"
>
<svg
v-if="isCheckingQuota"
class="h-4 w-4 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<svg v-else class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
{{ isCheckingQuota ? 'Ellenőrzés...' : 'Új jármű hozzáadása' }}
Új jármű hozzáadása
</button>
</div>
</template>
<!-- Vehicle Form Modal (Add) -->
<VehicleFormModal
:is-open="isAddFormOpen"
@close="isAddFormOpen = false"
@saved="onVehicleSaved"
/>
<!-- Vehicle Form Modal (Edit) -->
<VehicleFormModal
:is-open="isEditFormOpen"
:vehicle="editingVehicle"
@close="isEditFormOpen = false"
@saved="onVehicleEdited"
/>
<!-- Vehicle Detail Modal (read-only) -->
<VehicleDetailModal
:is-open="isDetailOpen"
@@ -129,8 +95,6 @@
@edit="openEditFromDetail"
@set-primary="setPrimaryVehicle"
/>
<!-- Task 3: Hidden trigger for direct edit from DashboardView -->
<div style="display: none">{{ triggerEditFromParent }}</div>
</div>
</template>
@@ -138,18 +102,16 @@
import { ref, computed, nextTick, onMounted, watch } from 'vue'
import type { VehicleData } from '../../types/vehicle'
import { useVehicleStore } from '../../stores/vehicle'
import api from '../../api/axios'
import VehicleFormModal from './VehicleFormModal.vue'
import VehicleDetailModal from '../vehicle/VehicleDetailModal.vue'
import VehicleCardStandard from '../vehicle/VehicleCardStandard.vue'
const props = defineProps<{
targetVehicleId?: string | null
editTargetVehicle?: VehicleData | null
}>()
const emit = defineEmits<{
'clear-edit-target': []
'add-new': []
'edit-vehicle': [vehicle: VehicleData]
}>()
const vehicleStore = useVehicleStore()
@@ -188,71 +150,12 @@ function openDetailView(vehicle: VehicleData) {
isDetailOpen.value = true
}
// ── Direct Edit (from card edit button) ──
function openEditDirect(vehicle: VehicleData) {
editingVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
// ── Bridge: Detail → Edit ──
// ── Bridge: Detail → Edit (emits to parent DashboardView) ──
async function openEditFromDetail(vehicle: VehicleData) {
isDetailOpen.value = false
// Use nextTick so detail modal closes before edit opens
await nextTick()
editingVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
// ── Modal state (Add) ──
const isAddFormOpen = ref(false)
const isCheckingQuota = ref(false)
/**
* Preemptive quota check before opening the add-vehicle modal.
* Calls GET /api/v1/assets/vehicles/quota-status.
* If the user has reached their limit, shows a blocking toast/alert instead of opening the form.
*/
async function handleAddNewClick() {
if (isCheckingQuota.value) return
isCheckingQuota.value = true
try {
const res = await api.get('/assets/vehicles/quota-status')
const { can_add, current_count, limit } = res.data
if (!can_add) {
// Block — show a prominent warning instead of opening the modal
alert(
`Elérted a csomagodhoz tartozó járműlimitet (${current_count}/${limit})! ` +
'Kérjük, válts magasabb csomagra az új jármű rögzítéséhez.'
)
return
}
// Quota OK — open the add form
isAddFormOpen.value = true
} catch (err: any) {
console.error('[PrivateVehicleManager] Quota check failed:', err)
// Fail open — if the API is unreachable, let the user proceed
isAddFormOpen.value = true
} finally {
isCheckingQuota.value = false
}
}
function onVehicleSaved() {
isAddFormOpen.value = false
// In real implementation, refresh the vehicle list from the store
}
// ── Modal state (Edit) ──
const isEditFormOpen = ref(false)
const editingVehicle = ref<VehicleData | null>(null)
function onVehicleEdited() {
isEditFormOpen.value = false
editingVehicle.value = null
// In real implementation, refresh the vehicle list from the store
emit('edit-vehicle', vehicle)
}
// ── Targeted scroll to vehicle card ──
@@ -276,24 +179,9 @@ watch(() => props.targetVehicleId, (newVal) => {
}
})
// ── Task 3: Watch for editTargetVehicle from parent (DashboardView) ──
const triggerEditFromParent = computed(() => {
if (props.editTargetVehicle) {
// Open the edit form directly with the vehicle data
editingVehicle.value = { ...props.editTargetVehicle } as VehicleData
isEditFormOpen.value = true
// Clear the prop so it doesn't re-trigger
emit('clear-edit-target')
}
return props.editTargetVehicle?.id || null
})
// ── Set Primary Vehicle ──
async function setPrimaryVehicle(vehicle: VehicleData) {
console.log('[PrivateVehicleManager] setPrimaryVehicle called for:', vehicle.id, vehicle.license_plate)
// TODO: Real API call — PUT /api/v1/assets/vehicles/{id} with { is_primary: true }
// This will also need to unset any other primary vehicle for this user.
// For now, just log and update local state optimistically.
vehicleStore.setPrimaryVehicle(vehicle.id)
}
</script>

View File

@@ -110,8 +110,9 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useCostStore } from '../../stores/cost'
import api from '../../api/axios'
import type { Vehicle } from '../../stores/vehicle'
const props = defineProps<{
@@ -135,6 +136,26 @@ const form = reactive({
const errorMessage = ref<string | null>(null)
// ── Category Resolution ──
const fuelCategoryId = ref<number>(1) // Default fallback
async function resolveFuelCategory() {
try {
const res = await api.get('/dictionaries/cost-categories')
const allCats = res.data as Array<{ id: number; code?: string; name: string; parent_id: number | null }>
const fuelCat = allCats.find((c) => c.code === 'FUEL' && c.parent_id === null)
if (fuelCat) {
fuelCategoryId.value = fuelCat.id
}
} catch (err) {
console.warn('[SimpleFuelModal] Could not fetch categories, using fallback ID 1:', err)
}
}
onMounted(() => {
resolveFuelCategory()
})
const vehicleLabel = computed(() => {
if (!props.vehicle) return 'Nincs kiválasztva jármű'
const plate = props.vehicle.license_plate || ''
@@ -151,14 +172,13 @@ async function handleSubmit() {
errorMessage.value = null
// Build payload: cost_type = 'fuel', category auto-set to FUEL
// Build payload: category auto-set to FUEL (resolved dynamically by code)
// organization_id is omitted — backend auto-resolves from asset
const payload = {
asset_id: props.vehicle.id,
category_id: 1, // FUEL category (default)
cost_type: 'fuel',
amount_local: form.amount,
currency_local: 'HUF' as const,
category_id: fuelCategoryId.value, // FUEL category (resolved by code)
amount_net: form.amount,
currency: 'HUF' as const,
date: new Date(form.date).toISOString(),
mileage_at_cost: form.odometer > 0 ? form.odometer : null,
description: `Tankolás: ${form.quantity} L/kWh`,

File diff suppressed because it is too large Load Diff