backend admin flotta bekötése
This commit is contained in:
@@ -2351,11 +2351,13 @@ var garages$1 = {
|
||||
title: "Garázsok CRM",
|
||||
subtitle: "Garázsok listája, csomagkezelés és státuszmódosítás",
|
||||
total_garages: "Összes Garázs",
|
||||
active_garages: "Aktív Garázsok",
|
||||
premium_garages: "Premium Garázsok",
|
||||
enterprise_garages: "Enterprise Garázsok",
|
||||
private_garages: "Privát Garázsok",
|
||||
corporate_garages: "Céges Garázsok",
|
||||
search_placeholder: "Keresés név, kapcsolattartó vagy e-mail alapján...",
|
||||
all_tiers: "Összes csomag",
|
||||
all_types: "Összes típus",
|
||||
type_individual: "Privát (Egyéni)",
|
||||
type_corporate: "Céges (Vállalati)",
|
||||
company_name: "Cégnév",
|
||||
email: "E-mail",
|
||||
status: "Státusz",
|
||||
@@ -2411,9 +2413,51 @@ var garages$1 = {
|
||||
contact_name: "Kapcsolattartó neve",
|
||||
contact_role: "Szerepkör",
|
||||
contact_department: "Osztály",
|
||||
contact_phone: "Telefonszám",
|
||||
contact_phone: "Kapcsolattartó telefon",
|
||||
no_contact: "Nincs elsődleges kapcsolattartó",
|
||||
coming_soon: "Ez a funkció hamarosan elérhető lesz..."
|
||||
edit_details: "Adatok Szerkesztése",
|
||||
save: "Mentés",
|
||||
generalTab: "Általános",
|
||||
employeesTab: "Dolgozók",
|
||||
fleetTab: "Flotta",
|
||||
analyticsTab: "Analitika",
|
||||
editBasicTab: "Alapadatok",
|
||||
editContactTab: "Kapcsolat",
|
||||
editAddressTab: "Címek",
|
||||
zip: "Irányítószám",
|
||||
city: "Város",
|
||||
street_name: "Utca",
|
||||
street_type: "Utca típusa",
|
||||
house_number: "Házszám",
|
||||
coming_soon: "Ez a funkció hamarosan elérhető lesz...",
|
||||
missing_data_title: "Hiányzó adatok",
|
||||
missing_data_desc: "A garázs profilja hiányos. Kérjük, egészítse ki a hiányzó adatokat.",
|
||||
missing_data_activation_required: "A garázs aktiválásához az alábbi adatok pótlása kötelező:",
|
||||
missing_tax_number: "Adószám",
|
||||
missing_billing: "Számlázási cím",
|
||||
missing_contact_email: "Kapcsolattartó e-mail",
|
||||
missing_contact_phone: "Kapcsolattartó telefon",
|
||||
missing_data_count: "{count} mező hiányzik",
|
||||
contact_person_name: "Kapcsolattartó neve",
|
||||
contact_email: "Kapcsolattartó e-mail",
|
||||
billing_address: "Számlázási cím",
|
||||
billing_zip: "Irányítószám",
|
||||
billing_city: "Város",
|
||||
billing_street_name: "Utca",
|
||||
billing_street_type: "Utca típusa",
|
||||
billing_house_number: "Házszám",
|
||||
notification_address: "Értesítési cím",
|
||||
notification_zip: "Irányítószám",
|
||||
notification_city: "Város",
|
||||
notification_street_name: "Utca",
|
||||
notification_street_type: "Utca típusa",
|
||||
notification_house_number: "Házszám",
|
||||
tab_basic: "Alapadatok",
|
||||
tab_contact: "Kapcsolat",
|
||||
tab_addresses: "Címek",
|
||||
basic_info: "Alapadatok",
|
||||
contact_section: "Kapcsolattartó",
|
||||
addresses_section: "Címek"
|
||||
},
|
||||
employees: {
|
||||
title: "Dolgozók",
|
||||
@@ -2427,13 +2471,43 @@ var garages$1 = {
|
||||
remove: "Eltávolítás",
|
||||
no_employees: "Még nincsenek tagok ehhez a garázshoz.",
|
||||
edit_role_title: "Tag szerepkörének szerkesztése",
|
||||
editRoleTitle: "Tag szerepkörének szerkesztése",
|
||||
editRoleFor: "{name} szerepkörének módosítása",
|
||||
cancel: "Mégse",
|
||||
save: "Mentés",
|
||||
saveRole: "Szerepkör mentése",
|
||||
saving: "Mentés...",
|
||||
status_active: "Aktív",
|
||||
status_inactive: "Inaktív",
|
||||
remove_title: "Tag eltávolítása",
|
||||
removeTitle: "Tag eltávolítása",
|
||||
remove_confirm: "Biztosan eltávolítod {name} tagot a garázsból?",
|
||||
remove_btn: "Eltávolítás"
|
||||
removeConfirm: "Biztosan eltávolítod {name} tagot a garázsból?",
|
||||
remove_btn: "Eltávolítás",
|
||||
removing: "Eltávolítás...",
|
||||
add_employee: "Új dolgozó hozzáadása",
|
||||
addTitle: "Új dolgozó hozzáadása",
|
||||
email: "E-mail cím",
|
||||
emailPlaceholder: "Add meg az e-mail címet...",
|
||||
roleMember: "Tag",
|
||||
roleManager: "Menedzser",
|
||||
roleAdmin: "Admin",
|
||||
roleOwner: "Tulajdonos",
|
||||
add: "Hozzáadás",
|
||||
adding: "Hozzáadás..."
|
||||
},
|
||||
fleet: {
|
||||
title: "Flotta",
|
||||
total_vehicles: "Összesen {count} jármű",
|
||||
plate: "Rendszám",
|
||||
brand: "Márka",
|
||||
model: "Modell",
|
||||
brand_model: "Márka & Modell",
|
||||
year: "Évjárat",
|
||||
status: "Státusz",
|
||||
vin: "Alvázszám",
|
||||
no_vehicles: "Ehhez a garázshoz még nincsenek járművek rendelve.",
|
||||
no_fleet_access: "Nincs jogosultságod a flotta adatok megtekintéséhez."
|
||||
}
|
||||
};
|
||||
const locale_hu_46json_ee06c965 = {
|
||||
@@ -2541,11 +2615,13 @@ var garages = {
|
||||
title: "Garages CRM",
|
||||
subtitle: "Garage list, package management and status changes",
|
||||
total_garages: "Total Garages",
|
||||
active_garages: "Active Garages",
|
||||
premium_garages: "Premium Garages",
|
||||
enterprise_garages: "Enterprise Garages",
|
||||
private_garages: "Private Garages",
|
||||
corporate_garages: "Corporate Garages",
|
||||
search_placeholder: "Search by name, contact or email...",
|
||||
all_tiers: "All packages",
|
||||
all_types: "All types",
|
||||
type_individual: "Private (Individual)",
|
||||
type_corporate: "Corporate",
|
||||
company_name: "Company Name",
|
||||
email: "Email",
|
||||
status: "Status",
|
||||
@@ -2601,9 +2677,51 @@ var garages = {
|
||||
contact_name: "Contact Name",
|
||||
contact_role: "Role",
|
||||
contact_department: "Department",
|
||||
contact_phone: "Phone",
|
||||
contact_phone: "Contact Phone",
|
||||
no_contact: "No primary contact person",
|
||||
coming_soon: "This feature will be available soon..."
|
||||
edit_details: "Edit Details",
|
||||
save: "Save",
|
||||
generalTab: "General",
|
||||
employeesTab: "Employees",
|
||||
fleetTab: "Fleet",
|
||||
analyticsTab: "Analytics",
|
||||
editBasicTab: "Basic Info",
|
||||
editContactTab: "Contact",
|
||||
editAddressTab: "Addresses",
|
||||
zip: "ZIP Code",
|
||||
city: "City",
|
||||
street_name: "Street",
|
||||
street_type: "Street Type",
|
||||
house_number: "House Number",
|
||||
coming_soon: "This feature will be available soon...",
|
||||
missing_data_title: "Missing Data",
|
||||
missing_data_desc: "The garage profile is incomplete. Please fill in the missing fields.",
|
||||
missing_data_activation_required: "To activate the garage, the following data must be provided:",
|
||||
missing_tax_number: "Tax Number",
|
||||
missing_billing: "Billing Address",
|
||||
missing_contact_email: "Contact Email",
|
||||
missing_contact_phone: "Contact Phone",
|
||||
missing_data_count: "{count} fields missing",
|
||||
contact_person_name: "Contact Person Name",
|
||||
contact_email: "Contact Email",
|
||||
billing_address: "Billing Address",
|
||||
billing_zip: "ZIP Code",
|
||||
billing_city: "City",
|
||||
billing_street_name: "Street",
|
||||
billing_street_type: "Street Type",
|
||||
billing_house_number: "House Number",
|
||||
notification_address: "Notification Address",
|
||||
notification_zip: "ZIP Code",
|
||||
notification_city: "City",
|
||||
notification_street_name: "Street",
|
||||
notification_street_type: "Street Type",
|
||||
notification_house_number: "House Number",
|
||||
tab_basic: "Basic Info",
|
||||
tab_contact: "Contact",
|
||||
tab_addresses: "Addresses",
|
||||
basic_info: "Basic Info",
|
||||
contact_section: "Contact Person",
|
||||
addresses_section: "Addresses"
|
||||
},
|
||||
employees: {
|
||||
title: "Employees",
|
||||
@@ -2617,13 +2735,43 @@ var garages = {
|
||||
remove: "Remove",
|
||||
no_employees: "No employees assigned to this garage.",
|
||||
edit_role_title: "Edit Employee Role",
|
||||
editRoleTitle: "Edit Employee Role",
|
||||
editRoleFor: "Change role for {name}",
|
||||
cancel: "Cancel",
|
||||
save: "Save Changes",
|
||||
saveRole: "Save Role",
|
||||
saving: "Saving...",
|
||||
status_active: "Active",
|
||||
status_inactive: "Inactive",
|
||||
remove_title: "Remove Employee",
|
||||
removeTitle: "Remove Employee",
|
||||
remove_confirm: "Are you sure you want to remove \"{name}\" from this garage?",
|
||||
remove_btn: "Remove"
|
||||
removeConfirm: "Are you sure you want to remove \"{name}\" from this garage?",
|
||||
remove_btn: "Remove",
|
||||
removing: "Removing...",
|
||||
add_employee: "Add Employee",
|
||||
addTitle: "Add New Employee",
|
||||
email: "Email Address",
|
||||
emailPlaceholder: "Enter email address...",
|
||||
roleMember: "Member",
|
||||
roleManager: "Manager",
|
||||
roleAdmin: "Admin",
|
||||
roleOwner: "Owner",
|
||||
add: "Add Employee",
|
||||
adding: "Adding..."
|
||||
},
|
||||
fleet: {
|
||||
title: "Fleet",
|
||||
total_vehicles: "{count} vehicles total",
|
||||
plate: "Plate",
|
||||
brand: "Brand",
|
||||
model: "Model",
|
||||
brand_model: "Brand & Model",
|
||||
year: "Year",
|
||||
status: "Status",
|
||||
vin: "VIN",
|
||||
no_vehicles: "No vehicles assigned to this garage yet.",
|
||||
no_fleet_access: "You don't have permission to view fleet data."
|
||||
}
|
||||
};
|
||||
const locale_en_46json_0b63539c = {
|
||||
@@ -3278,16 +3426,16 @@ _wH6JrtIxmaSoA8lCPWFnE9z4lQeXW6H5z3l5aymEQw
|
||||
const assets = {
|
||||
"/index.mjs": {
|
||||
"type": "text/javascript; charset=utf-8",
|
||||
"etag": "\"23532-gRN4chechZSOn9PdQdfcKj3v+Ek\"",
|
||||
"mtime": "2026-06-25T19:56:11.463Z",
|
||||
"size": 144690,
|
||||
"etag": "\"24b7a-X7Gi3Oi5IkHbqD0x6I8saqEhvJo\"",
|
||||
"mtime": "2026-06-26T00:29:32.171Z",
|
||||
"size": 150394,
|
||||
"path": "index.mjs"
|
||||
},
|
||||
"/index.mjs.map": {
|
||||
"type": "application/json",
|
||||
"etag": "\"83172-ssEX0StaUacfa43NRfqyNY4W7FM\"",
|
||||
"mtime": "2026-06-25T19:56:11.464Z",
|
||||
"size": 536946,
|
||||
"etag": "\"83219-k/zre5TDT2FcQBXF7JC+SUbYHHs\"",
|
||||
"mtime": "2026-06-26T00:29:32.171Z",
|
||||
"size": 537113,
|
||||
"path": "index.mjs.map"
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"date": "2026-06-25T19:56:47.228Z",
|
||||
"date": "2026-06-26T00:29:33.690Z",
|
||||
"preset": "nitro-dev",
|
||||
"framework": {
|
||||
"name": "nuxt",
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": {
|
||||
"pid": 19,
|
||||
"workerAddress": {
|
||||
"socketPath": "\u0000nitro-worker-19-19-19-6745.sock"
|
||||
"socketPath": "\u0000nitro-worker-19-37-37-3295.sock"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 6/25/2026, 7:56:46 PM
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 6/26/2026, 1:03:38 AM
|
||||
import "@nuxtjs/tailwindcss/config-ctx"
|
||||
import configMerger from "@nuxtjs/tailwindcss/merger";
|
||||
|
||||
|
||||
1
frontend_admin/.nuxt/types/plugins.d.ts
vendored
1
frontend_admin/.nuxt/types/plugins.d.ts
vendored
@@ -29,6 +29,7 @@ type NuxtAppInjections =
|
||||
InjectionType<typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/plugins/i18n.js")> &
|
||||
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/warn.dev.server.js")> &
|
||||
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/check-if-layout-used.js")> &
|
||||
InjectionType<typeof import("../../plugins/api")> &
|
||||
InjectionType<typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/plugins/ssg-detect.js")>
|
||||
|
||||
declare module '#app' {
|
||||
|
||||
@@ -98,11 +98,13 @@
|
||||
"title": "Garages CRM",
|
||||
"subtitle": "Garage list, package management and status changes",
|
||||
"total_garages": "Total Garages",
|
||||
"active_garages": "Active Garages",
|
||||
"premium_garages": "Premium Garages",
|
||||
"enterprise_garages": "Enterprise Garages",
|
||||
"private_garages": "Private Garages",
|
||||
"corporate_garages": "Corporate Garages",
|
||||
"search_placeholder": "Search by name, contact or email...",
|
||||
"all_tiers": "All packages",
|
||||
"all_types": "All types",
|
||||
"type_individual": "Private (Individual)",
|
||||
"type_corporate": "Corporate",
|
||||
"company_name": "Company Name",
|
||||
"email": "Email",
|
||||
"status": "Status",
|
||||
@@ -160,7 +162,50 @@
|
||||
"contact_department": "Department",
|
||||
"contact_phone": "Phone",
|
||||
"no_contact": "No primary contact person",
|
||||
"coming_soon": "This feature will be available soon..."
|
||||
"edit_details": "Edit Details",
|
||||
"save": "Save",
|
||||
"generalTab": "General",
|
||||
"employeesTab": "Employees",
|
||||
"fleetTab": "Fleet",
|
||||
"analyticsTab": "Analytics",
|
||||
"editBasicTab": "Basic Info",
|
||||
"editContactTab": "Contact",
|
||||
"editAddressTab": "Addresses",
|
||||
"zip": "ZIP Code",
|
||||
"city": "City",
|
||||
"street_name": "Street",
|
||||
"street_type": "Street Type",
|
||||
"house_number": "House Number",
|
||||
"coming_soon": "This feature will be available soon...",
|
||||
"missing_data_title": "Missing Data",
|
||||
"missing_data_desc": "The garage profile is incomplete. Please fill in the missing fields.",
|
||||
"missing_data_activation_required": "To activate the garage, the following data must be provided:",
|
||||
"missing_tax_number": "Tax Number",
|
||||
"missing_billing": "Billing Address",
|
||||
"missing_contact_email": "Contact Email",
|
||||
"missing_contact_phone": "Contact Phone",
|
||||
"missing_data_count": "{count} fields missing",
|
||||
"contact_person_name": "Contact Person Name",
|
||||
"contact_email": "Contact Email",
|
||||
"contact_phone": "Contact Phone",
|
||||
"billing_address": "Billing Address",
|
||||
"billing_zip": "ZIP Code",
|
||||
"billing_city": "City",
|
||||
"billing_street_name": "Street",
|
||||
"billing_street_type": "Street Type",
|
||||
"billing_house_number": "House Number",
|
||||
"notification_address": "Notification Address",
|
||||
"notification_zip": "ZIP Code",
|
||||
"notification_city": "City",
|
||||
"notification_street_name": "Street",
|
||||
"notification_street_type": "Street Type",
|
||||
"notification_house_number": "House Number",
|
||||
"tab_basic": "Basic Info",
|
||||
"tab_contact": "Contact",
|
||||
"tab_addresses": "Addresses",
|
||||
"basic_info": "Basic Info",
|
||||
"contact_section": "Contact Person",
|
||||
"addresses_section": "Addresses"
|
||||
},
|
||||
"employees": {
|
||||
"title": "Employees",
|
||||
@@ -174,13 +219,43 @@
|
||||
"remove": "Remove",
|
||||
"no_employees": "No employees assigned to this garage.",
|
||||
"edit_role_title": "Edit Employee Role",
|
||||
"editRoleTitle": "Edit Employee Role",
|
||||
"editRoleFor": "Change role for {name}",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save Changes",
|
||||
"saveRole": "Save Role",
|
||||
"saving": "Saving...",
|
||||
"status_active": "Active",
|
||||
"status_inactive": "Inactive",
|
||||
"remove_title": "Remove Employee",
|
||||
"removeTitle": "Remove Employee",
|
||||
"remove_confirm": "Are you sure you want to remove \"{name}\" from this garage?",
|
||||
"remove_btn": "Remove"
|
||||
"removeConfirm": "Are you sure you want to remove \"{name}\" from this garage?",
|
||||
"remove_btn": "Remove",
|
||||
"removing": "Removing...",
|
||||
"add_employee": "Add Employee",
|
||||
"addTitle": "Add New Employee",
|
||||
"email": "Email Address",
|
||||
"emailPlaceholder": "Enter email address...",
|
||||
"roleMember": "Member",
|
||||
"roleManager": "Manager",
|
||||
"roleAdmin": "Admin",
|
||||
"roleOwner": "Owner",
|
||||
"add": "Add Employee",
|
||||
"adding": "Adding..."
|
||||
},
|
||||
"fleet": {
|
||||
"title": "Fleet",
|
||||
"total_vehicles": "{count} vehicles total",
|
||||
"plate": "Plate",
|
||||
"brand": "Brand",
|
||||
"model": "Model",
|
||||
"brand_model": "Brand & Model",
|
||||
"year": "Year",
|
||||
"status": "Status",
|
||||
"vin": "VIN",
|
||||
"no_vehicles": "No vehicles assigned to this garage yet.",
|
||||
"no_fleet_access": "You don't have permission to view fleet data."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +98,13 @@
|
||||
"title": "Garázsok CRM",
|
||||
"subtitle": "Garázsok listája, csomagkezelés és státuszmódosítás",
|
||||
"total_garages": "Összes Garázs",
|
||||
"active_garages": "Aktív Garázsok",
|
||||
"premium_garages": "Premium Garázsok",
|
||||
"enterprise_garages": "Enterprise Garázsok",
|
||||
"private_garages": "Privát Garázsok",
|
||||
"corporate_garages": "Céges Garázsok",
|
||||
"search_placeholder": "Keresés név, kapcsolattartó vagy e-mail alapján...",
|
||||
"all_tiers": "Összes csomag",
|
||||
"all_types": "Összes típus",
|
||||
"type_individual": "Privát (Egyéni)",
|
||||
"type_corporate": "Céges (Vállalati)",
|
||||
"company_name": "Cégnév",
|
||||
"email": "E-mail",
|
||||
"status": "Státusz",
|
||||
@@ -160,7 +162,50 @@
|
||||
"contact_department": "Osztály",
|
||||
"contact_phone": "Telefonszám",
|
||||
"no_contact": "Nincs elsődleges kapcsolattartó",
|
||||
"coming_soon": "Ez a funkció hamarosan elérhető lesz..."
|
||||
"edit_details": "Adatok Szerkesztése",
|
||||
"save": "Mentés",
|
||||
"generalTab": "Általános",
|
||||
"employeesTab": "Dolgozók",
|
||||
"fleetTab": "Flotta",
|
||||
"analyticsTab": "Analitika",
|
||||
"editBasicTab": "Alapadatok",
|
||||
"editContactTab": "Kapcsolat",
|
||||
"editAddressTab": "Címek",
|
||||
"zip": "Irányítószám",
|
||||
"city": "Város",
|
||||
"street_name": "Utca",
|
||||
"street_type": "Utca típusa",
|
||||
"house_number": "Házszám",
|
||||
"coming_soon": "Ez a funkció hamarosan elérhető lesz...",
|
||||
"missing_data_title": "Hiányzó adatok",
|
||||
"missing_data_desc": "A garázs profilja hiányos. Kérjük, egészítse ki a hiányzó adatokat.",
|
||||
"missing_data_activation_required": "A garázs aktiválásához az alábbi adatok pótlása kötelező:",
|
||||
"missing_tax_number": "Adószám",
|
||||
"missing_billing": "Számlázási cím",
|
||||
"missing_contact_email": "Kapcsolattartó e-mail",
|
||||
"missing_contact_phone": "Kapcsolattartó telefon",
|
||||
"missing_data_count": "{count} mező hiányzik",
|
||||
"contact_person_name": "Kapcsolattartó neve",
|
||||
"contact_email": "Kapcsolattartó e-mail",
|
||||
"contact_phone": "Kapcsolattartó telefon",
|
||||
"billing_address": "Számlázási cím",
|
||||
"billing_zip": "Irányítószám",
|
||||
"billing_city": "Város",
|
||||
"billing_street_name": "Utca",
|
||||
"billing_street_type": "Utca típusa",
|
||||
"billing_house_number": "Házszám",
|
||||
"notification_address": "Értesítési cím",
|
||||
"notification_zip": "Irányítószám",
|
||||
"notification_city": "Város",
|
||||
"notification_street_name": "Utca",
|
||||
"notification_street_type": "Utca típusa",
|
||||
"notification_house_number": "Házszám",
|
||||
"tab_basic": "Alapadatok",
|
||||
"tab_contact": "Kapcsolat",
|
||||
"tab_addresses": "Címek",
|
||||
"basic_info": "Alapadatok",
|
||||
"contact_section": "Kapcsolattartó",
|
||||
"addresses_section": "Címek"
|
||||
},
|
||||
"employees": {
|
||||
"title": "Dolgozók",
|
||||
@@ -174,13 +219,43 @@
|
||||
"remove": "Eltávolítás",
|
||||
"no_employees": "Még nincsenek tagok ehhez a garázshoz.",
|
||||
"edit_role_title": "Tag szerepkörének szerkesztése",
|
||||
"editRoleTitle": "Tag szerepkörének szerkesztése",
|
||||
"editRoleFor": "{name} szerepkörének módosítása",
|
||||
"cancel": "Mégse",
|
||||
"save": "Mentés",
|
||||
"saveRole": "Szerepkör mentése",
|
||||
"saving": "Mentés...",
|
||||
"status_active": "Aktív",
|
||||
"status_inactive": "Inaktív",
|
||||
"remove_title": "Tag eltávolítása",
|
||||
"removeTitle": "Tag eltávolítása",
|
||||
"remove_confirm": "Biztosan eltávolítod {name} tagot a garázsból?",
|
||||
"remove_btn": "Eltávolítás"
|
||||
"removeConfirm": "Biztosan eltávolítod {name} tagot a garázsból?",
|
||||
"remove_btn": "Eltávolítás",
|
||||
"removing": "Eltávolítás...",
|
||||
"add_employee": "Új dolgozó hozzáadása",
|
||||
"addTitle": "Új dolgozó hozzáadása",
|
||||
"email": "E-mail cím",
|
||||
"emailPlaceholder": "Add meg az e-mail címet...",
|
||||
"roleMember": "Tag",
|
||||
"roleManager": "Menedzser",
|
||||
"roleAdmin": "Admin",
|
||||
"roleOwner": "Tulajdonos",
|
||||
"add": "Hozzáadás",
|
||||
"adding": "Hozzáadás..."
|
||||
},
|
||||
"fleet": {
|
||||
"title": "Flotta",
|
||||
"total_vehicles": "Összesen {count} jármű",
|
||||
"plate": "Rendszám",
|
||||
"brand": "Márka",
|
||||
"model": "Modell",
|
||||
"brand_model": "Márka & Modell",
|
||||
"year": "Évjárat",
|
||||
"status": "Státusz",
|
||||
"vin": "Alvázszám",
|
||||
"no_vehicles": "Ehhez a garázshoz még nincsenek járművek rendelve.",
|
||||
"no_fleet_access": "Nincs jogosultságod a flotta adatok megtekintéséhez."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ const menuGroups: MenuGroup[] = [
|
||||
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
|
||||
children: [
|
||||
{
|
||||
path: '/users',
|
||||
path: '/', /* P0 HOTFIX: placeholder until /users page is built */
|
||||
label: 'Felhasználók listája',
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
|
||||
},
|
||||
@@ -371,7 +371,7 @@ const menuGroups: MenuGroup[] = [
|
||||
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
|
||||
children: [
|
||||
{
|
||||
path: '/logs',
|
||||
path: '/', /* P0 HOTFIX: placeholder until /logs page is built */
|
||||
label: 'Rendszernaplók',
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,43 @@
|
||||
<h1 class="text-2xl font-bold text-white">{{ $t('garages.title') }}</h1>
|
||||
<p class="text-slate-400 mt-1">{{ $t('garages.subtitle') }}</p>
|
||||
</div>
|
||||
<!-- Search & Filter Bar (always rendered — stable DOM for input focus) -->
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="$t('garages.search_placeholder')"
|
||||
class="w-full pl-10 pr-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
v-model="filterType"
|
||||
class="px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition"
|
||||
>
|
||||
<option value="all">{{ $t('garages.all_types') }}</option>
|
||||
<option value="individual">{{ $t('garages.type_individual') }}</option>
|
||||
<option value="corporate">{{ $t('garages.type_corporate') }}</option>
|
||||
</select>
|
||||
<select
|
||||
v-model="selectedPackage"
|
||||
class="px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition"
|
||||
>
|
||||
<option value="all">{{ $t('garages.all_tiers') }}</option>
|
||||
<option
|
||||
v-for="tier in uniqueTiers"
|
||||
:key="tier"
|
||||
:value="tier"
|
||||
>
|
||||
{{ tier }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-20">
|
||||
@@ -30,8 +67,8 @@
|
||||
|
||||
<!-- Content -->
|
||||
<template v-else>
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<!-- P0: Dynamic Stats Cards - based on real org_type data, not fake subscription tiers -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
||||
@@ -49,12 +86,12 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-emerald-500/10 text-emerald-400">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.active_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-emerald-400">{{ activeCount }}</p>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.private_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-emerald-400">{{ individualCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,54 +99,15 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.premium_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-amber-400">{{ premiumCount }}</p>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.corporate_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-amber-400">{{ corporateCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-purple-500/10 text-purple-400">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 uppercase tracking-wider">{{ $t('garages.enterprise_garages') }}</p>
|
||||
<p class="text-2xl font-bold text-purple-400">{{ enterpriseCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter Bar -->
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="$t('garages.search_placeholder')"
|
||||
class="w-full pl-10 pr-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
v-model="filterTier"
|
||||
class="px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition"
|
||||
>
|
||||
<option value="all">{{ $t('garages.all_tiers') }}</option>
|
||||
<option value="free">Free</option>
|
||||
<option value="premium">Premium</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
@@ -347,7 +345,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
@@ -401,7 +399,8 @@ const error = ref<string | null>(null)
|
||||
const garages = ref<Garage[]>([])
|
||||
const availableTiers = ref<Tier[]>([])
|
||||
const searchQuery = ref('')
|
||||
const filterTier = ref('all')
|
||||
const filterType = ref('all')
|
||||
const selectedPackage = ref('all')
|
||||
|
||||
// Modal state
|
||||
const showSubscriptionModal = ref(false)
|
||||
@@ -414,45 +413,62 @@ const saveError = ref<string | null>(null)
|
||||
// Notification
|
||||
const saveNotification = ref<{ type: 'success' | 'error'; message: string } | null>(null)
|
||||
|
||||
// ── Debounced Search Timer ──────────────────────────────────────────
|
||||
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/**
|
||||
* P0: Debounced search — waits 400ms after the user stops typing
|
||||
* before triggering a server-side API call. This prevents excessive
|
||||
* requests on every keystroke while keeping the UI responsive.
|
||||
*/
|
||||
function debouncedFetchGarages() {
|
||||
if (searchDebounceTimer) {
|
||||
clearTimeout(searchDebounceTimer)
|
||||
}
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
fetchGarages()
|
||||
}, 400)
|
||||
}
|
||||
|
||||
// ── Watchers ────────────────────────────────────────────────────────
|
||||
|
||||
// Watch searchQuery with debounce — only fires API call after 400ms idle
|
||||
watch(searchQuery, () => {
|
||||
debouncedFetchGarages()
|
||||
})
|
||||
|
||||
// Watch filterType — immediate API call (no debounce needed for dropdowns)
|
||||
watch(filterType, () => {
|
||||
fetchGarages()
|
||||
})
|
||||
|
||||
// Watch selectedPackage — immediate API call
|
||||
watch(selectedPackage, () => {
|
||||
fetchGarages()
|
||||
})
|
||||
|
||||
// ── Computed ───────────────────────────────────────────────────────
|
||||
|
||||
const activeCount = computed(() => garages.value.filter(g => g.is_active).length)
|
||||
const premiumCount = computed(() => {
|
||||
return garages.value.filter(g => {
|
||||
const name = g.subscription_tier_name.toLowerCase()
|
||||
return name.includes('premium') || name.includes('pro')
|
||||
}).length
|
||||
})
|
||||
const enterpriseCount = computed(() => {
|
||||
return garages.value.filter(g => {
|
||||
const name = g.subscription_tier_name.toLowerCase()
|
||||
return name.includes('enterprise') || name.includes('vip') || name.includes('corp')
|
||||
}).length
|
||||
// P0: Dynamic stats based on real org_type from the database
|
||||
const individualCount = computed(() => garages.value.filter(g => g.org_type === 'individual').length)
|
||||
const corporateCount = computed(() => garages.value.filter(g => g.org_type !== 'individual').length)
|
||||
|
||||
// P0: Extract unique subscription tier names for the filter dropdown
|
||||
const uniqueTiers = computed(() => {
|
||||
const tiers = new Set<string>()
|
||||
for (const g of garages.value) {
|
||||
if (g.subscription_tier_name) {
|
||||
tiers.add(g.subscription_tier_name)
|
||||
}
|
||||
}
|
||||
return Array.from(tiers).sort()
|
||||
})
|
||||
|
||||
const filteredGarages = computed(() => {
|
||||
return garages.value.filter(g => {
|
||||
// Search filter — search across display_name, name, full_name, and city
|
||||
if (searchQuery.value) {
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
const displayName = garageDisplayName(g).toLowerCase()
|
||||
const matchesSearch =
|
||||
displayName.includes(q) ||
|
||||
g.full_name.toLowerCase().includes(q) ||
|
||||
g.name.toLowerCase().includes(q) ||
|
||||
(g.city && g.city.toLowerCase().includes(q))
|
||||
if (!matchesSearch) return false
|
||||
}
|
||||
// Tier filter
|
||||
if (filterTier.value !== 'all') {
|
||||
const name = g.subscription_tier_name.toLowerCase()
|
||||
if (filterTier.value === 'free' && !name.includes('free') && !name.includes('fallback')) return false
|
||||
if (filterTier.value === 'premium' && !name.includes('premium') && !name.includes('pro')) return false
|
||||
if (filterTier.value === 'enterprise' && !name.includes('enterprise') && !name.includes('vip') && !name.includes('corp')) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
// P0: Server-side filtered — the API handles all filtering.
|
||||
// This computed is now a simple pass-through; the real filtering
|
||||
// happens in fetchGarages() via query params.
|
||||
const filteredGarages = computed(() => garages.value)
|
||||
|
||||
// ── Auth Helper ────────────────────────────────────────────────────
|
||||
|
||||
@@ -468,8 +484,26 @@ async function fetchGarages() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
// P0: Server-side search — pass all filter params to the API
|
||||
const params: Record<string, any> = { limit: 200 }
|
||||
|
||||
// Debounced search query → server-side omni-search
|
||||
if (searchQuery.value) {
|
||||
params.search_term = searchQuery.value
|
||||
}
|
||||
|
||||
// Org type filter
|
||||
if (filterType.value !== 'all') {
|
||||
params.org_type_filter = filterType.value
|
||||
}
|
||||
|
||||
// Subscription tier filter
|
||||
if (selectedPackage.value !== 'all') {
|
||||
params.tier_name_filter = selectedPackage.value
|
||||
}
|
||||
|
||||
const response = await $fetch<{ total: number; skip: number; limit: number; garages: Garage[] }>('/api/v1/admin/organizations', {
|
||||
params: { limit: 200 },
|
||||
params,
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
garages.value = response.garages
|
||||
@@ -552,9 +586,28 @@ async function saveSubscription() {
|
||||
// ── Status Toggle ──────────────────────────────────────────────────
|
||||
|
||||
async function toggleStatus(garage: Garage) {
|
||||
// This would call a PATCH endpoint to toggle is_active
|
||||
// For now, we just show a notification that this is not yet implemented
|
||||
showNotification('error', `Status toggle for "${garageDisplayName(garage)}" not yet implemented via API`)
|
||||
// Determine the new status based on current state
|
||||
const newStatus = garage.is_active ? 'inactive' : 'active'
|
||||
const actionLabel = garage.is_active ? 'deaktiválása' : 'aktiválása'
|
||||
|
||||
try {
|
||||
await $fetch(`/api/v1/admin/organizations/${garage.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: { status: newStatus },
|
||||
})
|
||||
|
||||
showNotification('success', `"${garageDisplayName(garage)}" ${actionLabel} sikeresen megtörtént.`)
|
||||
|
||||
// Refresh the list to reflect changes
|
||||
await fetchGarages()
|
||||
} catch (err: any) {
|
||||
console.error('[Garages] Failed to toggle status:', err)
|
||||
showNotification('error', err?.data?.detail || err?.message || `A garázs ${actionLabel} sikertelen.`)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
54
frontend_admin/plugins/api.ts
Normal file
54
frontend_admin/plugins/api.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 🌐 Global API Plugin — 401 Unauthorized Interceptor
|
||||
*
|
||||
* This Nuxt plugin wraps the global `$fetch` to intercept 401 responses.
|
||||
* When a 401 is detected, it:
|
||||
* 1. Calls the auth store's logout() method (clears tokens)
|
||||
* 2. Redirects to /login
|
||||
*
|
||||
* This fixes the P0 bug where expired tokens did not force a logout on page reload.
|
||||
*/
|
||||
|
||||
import { defineNuxtPlugin, navigateTo, useCookie } from '#app'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
// We hook into the global $fetch via onResponse interceptor
|
||||
// Nuxt's $fetch is built on ofetch which supports interceptors
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
// Override global fetch to intercept 401 responses
|
||||
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const response = await originalFetch(input, init)
|
||||
|
||||
// Intercept 401 Unauthorized
|
||||
if (response.status === 401) {
|
||||
// Avoid intercepting login endpoint itself (would cause redirect loops)
|
||||
const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url)
|
||||
if (url.includes('/auth/login') || url.includes('/auth/refresh')) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Clear auth state
|
||||
try {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
} catch {
|
||||
// Fallback: manually clear the cookie
|
||||
const tokenCookie = useCookie('access_token')
|
||||
tokenCookie.value = null
|
||||
}
|
||||
|
||||
// Redirect to login
|
||||
if (process.client) {
|
||||
// Use window.location for hard redirect to ensure full state reset
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
await navigateTo('/login')
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user