Initial commit after fork. Moving Budget categories to Database and adding subcategories, with customization options
This commit is contained in:
@@ -7,11 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.23.17] - 2026-04-25
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Italian (it) locale: translated all missing strings in the recipes section (`nav.recipes`, `meals.savedRecipeLabel`, `meals.savedRecipePlaceholder`, `meals.saveAsRecipe`, `meals.recipeScaleLabel`, and all `recipes.*` keys) — contributed by @albanobattistella
|
|
||||||
|
|
||||||
## [0.23.16] - 2026-04-24
|
## [0.23.16] - 2026-04-24
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.23.17",
|
"version": "0.23.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.23.17",
|
"version": "0.23.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.23.17",
|
"version": "0.23.16",
|
||||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- مثل {{month}}",
|
"trendNeutral": "- مثل {{month}}",
|
||||||
"validAmountRequired": "أدخل مبلغاً صحيحاً",
|
"validAmountRequired": "أدخل مبلغاً صحيحاً",
|
||||||
"dateRequired": "التاريخ مطلوب",
|
"dateRequired": "التاريخ مطلوب",
|
||||||
"catFood": "الطعام",
|
"catFood": "Food",
|
||||||
"catRent": "الإيجار",
|
"catRent": "الإيجار",
|
||||||
"catInsurance": "التأمين",
|
"catInsurance": "التأمين",
|
||||||
"catMobility": "التنقل",
|
"catMobility": "التنقل",
|
||||||
"catLeisure": "الترفيه",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "الملابس",
|
"catClothing": "الملابس",
|
||||||
"catHealth": "الصحة",
|
"catHealth": "الصحة",
|
||||||
"catEducation": "التعليم",
|
"catEducation": "Education",
|
||||||
"catMisc": "متنوع",
|
"catMisc": "متنوع",
|
||||||
"catEarnedIncome": "دخل العمل",
|
"catEarnedIncome": "دخل العمل",
|
||||||
"catInvestmentIncome": "دخل الاستثمار",
|
"catInvestmentIncome": "دخل الاستثمار",
|
||||||
"catTransferGiftIncome": "التحويلات والهدايا",
|
"catTransferGiftIncome": "التحويلات والهدايا",
|
||||||
"catGovernmentBenefits": "المزايا الاجتماعية",
|
"catGovernmentBenefits": "المزايا الاجتماعية",
|
||||||
"catOtherIncome": "دخل آخر",
|
"catOtherIncome": "دخل آخر",
|
||||||
"loadingIndicator": "جارٍ التحميل…"
|
"loadingIndicator": "جارٍ التحميل…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "الإعدادات",
|
"title": "الإعدادات",
|
||||||
|
|||||||
+50
-3
@@ -469,11 +469,11 @@
|
|||||||
"trendNeutral": "- wie {{month}}",
|
"trendNeutral": "- wie {{month}}",
|
||||||
"validAmountRequired": "Gültigen Betrag eingeben",
|
"validAmountRequired": "Gültigen Betrag eingeben",
|
||||||
"dateRequired": "Datum ist erforderlich",
|
"dateRequired": "Datum ist erforderlich",
|
||||||
"catFood": "Lebensmittel",
|
"catFood": "Ernährung",
|
||||||
"catRent": "Miete",
|
"catRent": "Miete",
|
||||||
"catInsurance": "Versicherung",
|
"catInsurance": "Versicherung",
|
||||||
"catMobility": "Mobilität",
|
"catMobility": "Mobilität",
|
||||||
"catLeisure": "Freizeit",
|
"catLeisure": "Freizeit und Unterhaltung",
|
||||||
"catClothing": "Kleidung",
|
"catClothing": "Kleidung",
|
||||||
"catHealth": "Gesundheit",
|
"catHealth": "Gesundheit",
|
||||||
"catEducation": "Bildung",
|
"catEducation": "Bildung",
|
||||||
@@ -483,7 +483,54 @@
|
|||||||
"catTransferGiftIncome": "Geschenke & Transfers",
|
"catTransferGiftIncome": "Geschenke & Transfers",
|
||||||
"catGovernmentBenefits": "Sozialleistungen",
|
"catGovernmentBenefits": "Sozialleistungen",
|
||||||
"catOtherIncome": "Sonstiges Einkommen",
|
"catOtherIncome": "Sonstiges Einkommen",
|
||||||
"loadingIndicator": "Lade…"
|
"loadingIndicator": "Lade…",
|
||||||
|
"subcategoryLabel": "Unterkategorie",
|
||||||
|
"catHousing": "Wohnen / Zuhause",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Körperpflege / Gesundheit",
|
||||||
|
"catShoppingClothing": "Einkäufe und Kleidung",
|
||||||
|
"catFinancialOther": "Finanzdienstleistungen und Sonstiges",
|
||||||
|
"subcatRentMortgage": "Miete / Kreditrate",
|
||||||
|
"subcatCondominium": "Hausgeld",
|
||||||
|
"subcatUtilities": "Strom / Wasser / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Telefon",
|
||||||
|
"subcatRenovationMaintenance": "Renovierung / Instandhaltung",
|
||||||
|
"subcatCleaning": "Reinigung",
|
||||||
|
"subcatGroceries": "Supermarkt",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bäckerei",
|
||||||
|
"subcatFuel": "Kraftstoff",
|
||||||
|
"subcatParkingTolls": "Parken / Maut",
|
||||||
|
"subcatPublicTransport": "Öffentliche Verkehrsmittel",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Wartung / Versicherung",
|
||||||
|
"subcatPharmacy": "Apotheke",
|
||||||
|
"subcatHealthInsurance": "Krankenversicherung",
|
||||||
|
"subcatGymSports": "Fitnessstudio / Sport",
|
||||||
|
"subcatBeautyCosmetics": "Schönheit / Kosmetik",
|
||||||
|
"subcatTravel": "Reisen",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Veranstaltungen",
|
||||||
|
"subcatHobbies": "Hobbys",
|
||||||
|
"subcatClothesShoes": "Kleidung / Schuhe",
|
||||||
|
"subcatElectronics": "Elektronik",
|
||||||
|
"subcatGifts": "Geschenke",
|
||||||
|
"subcatCoursesCollege": "Kurse / Hochschule",
|
||||||
|
"subcatSchoolSupplies": "Schulmaterial",
|
||||||
|
"subcatLanguages": "Sprachen",
|
||||||
|
"subcatLoansInterest": "Kredite / Zinsen",
|
||||||
|
"subcatBankFees": "Bankgebühren",
|
||||||
|
"subcatInsuranceOther": "Versicherungen",
|
||||||
|
"subcatInvestments": "Investitionen",
|
||||||
|
"subcatTaxes": "Steuern",
|
||||||
|
"metaLoadError": "Budget-Kategorien konnten nicht geladen werden.",
|
||||||
|
"addCategory": "+ Kategorie",
|
||||||
|
"addSubcategory": "+ Unterkategorie",
|
||||||
|
"newCategoryPrompt": "Name der neuen Kategorie:",
|
||||||
|
"newSubcategoryPrompt": "Name der neuen Unterkategorie:",
|
||||||
|
"categoryAddedToast": "Kategorie hinzugefügt.",
|
||||||
|
"subcategoryAddedToast": "Unterkategorie hinzugefügt."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Einstellungen",
|
"title": "Einstellungen",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- ίδιο με {{month}}",
|
"trendNeutral": "- ίδιο με {{month}}",
|
||||||
"validAmountRequired": "Παρακαλώ εισαγάγετε έγκυρο ποσό",
|
"validAmountRequired": "Παρακαλώ εισαγάγετε έγκυρο ποσό",
|
||||||
"dateRequired": "Η ημερομηνία είναι υποχρεωτική",
|
"dateRequired": "Η ημερομηνία είναι υποχρεωτική",
|
||||||
"catFood": "Τρόφιμα",
|
"catFood": "Food",
|
||||||
"catRent": "Ενοίκιο",
|
"catRent": "Ενοίκιο",
|
||||||
"catInsurance": "Ασφάλεια",
|
"catInsurance": "Ασφάλεια",
|
||||||
"catMobility": "Μεταφορές",
|
"catMobility": "Μεταφορές",
|
||||||
"catLeisure": "Ελεύθερος χρόνος",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "Ρουχισμός",
|
"catClothing": "Ρουχισμός",
|
||||||
"catHealth": "Υγεία",
|
"catHealth": "Υγεία",
|
||||||
"catEducation": "Εκπαίδευση",
|
"catEducation": "Education",
|
||||||
"catMisc": "Διάφορα",
|
"catMisc": "Διάφορα",
|
||||||
"catEarnedIncome": "Εισόδημα από εργασία",
|
"catEarnedIncome": "Εισόδημα από εργασία",
|
||||||
"catInvestmentIncome": "Επενδυτικό εισόδημα",
|
"catInvestmentIncome": "Επενδυτικό εισόδημα",
|
||||||
"catTransferGiftIncome": "Μεταφορές και δώρα",
|
"catTransferGiftIncome": "Μεταφορές και δώρα",
|
||||||
"catGovernmentBenefits": "Κοινωνικές παροχές",
|
"catGovernmentBenefits": "Κοινωνικές παροχές",
|
||||||
"catOtherIncome": "Άλλο εισόδημα",
|
"catOtherIncome": "Άλλο εισόδημα",
|
||||||
"loadingIndicator": "Φόρτωση…"
|
"loadingIndicator": "Φόρτωση…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Ρυθμίσεις",
|
"title": "Ρυθμίσεις",
|
||||||
|
|||||||
+50
-3
@@ -463,11 +463,11 @@
|
|||||||
"trendNeutral": "- same as {{month}}",
|
"trendNeutral": "- same as {{month}}",
|
||||||
"validAmountRequired": "Please enter a valid amount",
|
"validAmountRequired": "Please enter a valid amount",
|
||||||
"dateRequired": "Date is required",
|
"dateRequired": "Date is required",
|
||||||
"catFood": "Groceries",
|
"catFood": "Food",
|
||||||
"catRent": "Rent",
|
"catRent": "Rent",
|
||||||
"catInsurance": "Insurance",
|
"catInsurance": "Insurance",
|
||||||
"catMobility": "Transport",
|
"catMobility": "Transport",
|
||||||
"catLeisure": "Leisure",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "Clothing",
|
"catClothing": "Clothing",
|
||||||
"catHealth": "Health",
|
"catHealth": "Health",
|
||||||
"catEducation": "Education",
|
"catEducation": "Education",
|
||||||
@@ -477,7 +477,54 @@
|
|||||||
"catTransferGiftIncome": "Transfer & Gift Income",
|
"catTransferGiftIncome": "Transfer & Gift Income",
|
||||||
"catGovernmentBenefits": "Government & Social Benefits",
|
"catGovernmentBenefits": "Government & Social Benefits",
|
||||||
"catOtherIncome": "Other Income",
|
"catOtherIncome": "Other Income",
|
||||||
"loadingIndicator": "Loading…"
|
"loadingIndicator": "Loading…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
|
|||||||
+49
-2
@@ -467,7 +467,7 @@
|
|||||||
"catRent": "Alquiler",
|
"catRent": "Alquiler",
|
||||||
"catInsurance": "Seguro",
|
"catInsurance": "Seguro",
|
||||||
"catMobility": "Movilidad",
|
"catMobility": "Movilidad",
|
||||||
"catLeisure": "Ocio",
|
"catLeisure": "Ocio y entretenimiento",
|
||||||
"catClothing": "Ropa",
|
"catClothing": "Ropa",
|
||||||
"catHealth": "Salud",
|
"catHealth": "Salud",
|
||||||
"catEducation": "Educación",
|
"catEducation": "Educación",
|
||||||
@@ -477,7 +477,54 @@
|
|||||||
"catTransferGiftIncome": "Transferencias y Regalos",
|
"catTransferGiftIncome": "Transferencias y Regalos",
|
||||||
"catGovernmentBenefits": "Prestaciones Sociales",
|
"catGovernmentBenefits": "Prestaciones Sociales",
|
||||||
"catOtherIncome": "Otros Ingresos",
|
"catOtherIncome": "Otros Ingresos",
|
||||||
"loadingIndicator": "Cargando…"
|
"loadingIndicator": "Cargando…",
|
||||||
|
"subcategoryLabel": "Subcategoría",
|
||||||
|
"catHousing": "Vivienda / Casa",
|
||||||
|
"catTransport": "Transporte",
|
||||||
|
"catPersonalHealth": "Cuidado personal / Salud",
|
||||||
|
"catShoppingClothing": "Compras y ropa",
|
||||||
|
"catFinancialOther": "Servicios financieros y otros",
|
||||||
|
"subcatRentMortgage": "Alquiler / Hipoteca",
|
||||||
|
"subcatCondominium": "Comunidad",
|
||||||
|
"subcatUtilities": "Luz / Agua / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Teléfono",
|
||||||
|
"subcatRenovationMaintenance": "Reforma / Mantenimiento",
|
||||||
|
"subcatCleaning": "Limpieza",
|
||||||
|
"subcatGroceries": "Supermercado",
|
||||||
|
"subcatRestaurantsBars": "Restaurantes / Bares",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Comida rápida",
|
||||||
|
"subcatBakery": "Panadería",
|
||||||
|
"subcatFuel": "Combustible",
|
||||||
|
"subcatParkingTolls": "Aparcamiento / Peajes",
|
||||||
|
"subcatPublicTransport": "Transporte público",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Mantenimiento / Seguro",
|
||||||
|
"subcatPharmacy": "Farmacia",
|
||||||
|
"subcatHealthInsurance": "Seguro médico",
|
||||||
|
"subcatGymSports": "Gimnasio / Deportes",
|
||||||
|
"subcatBeautyCosmetics": "Belleza / Cosmética",
|
||||||
|
"subcatTravel": "Viajes",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Eventos",
|
||||||
|
"subcatHobbies": "Aficiones",
|
||||||
|
"subcatClothesShoes": "Ropa / Calzado",
|
||||||
|
"subcatElectronics": "Electrónica",
|
||||||
|
"subcatGifts": "Regalos",
|
||||||
|
"subcatCoursesCollege": "Cursos / Universidad",
|
||||||
|
"subcatSchoolSupplies": "Material escolar",
|
||||||
|
"subcatLanguages": "Idiomas",
|
||||||
|
"subcatLoansInterest": "Préstamos / Intereses",
|
||||||
|
"subcatBankFees": "Comisiones bancarias",
|
||||||
|
"subcatInsuranceOther": "Seguros",
|
||||||
|
"subcatInvestments": "Inversiones",
|
||||||
|
"subcatTaxes": "Impuestos",
|
||||||
|
"metaLoadError": "No se pudieron cargar las categorías del presupuesto.",
|
||||||
|
"addCategory": "+ categoría",
|
||||||
|
"addSubcategory": "+ subcategoría",
|
||||||
|
"newCategoryPrompt": "Nombre de la nueva categoría:",
|
||||||
|
"newSubcategoryPrompt": "Nombre de la nueva subcategoría:",
|
||||||
|
"categoryAddedToast": "Categoría añadida.",
|
||||||
|
"subcategoryAddedToast": "Subcategoría añadida."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Ajustes",
|
"title": "Ajustes",
|
||||||
|
|||||||
+49
-2
@@ -467,7 +467,7 @@
|
|||||||
"catRent": "Loyer",
|
"catRent": "Loyer",
|
||||||
"catInsurance": "Assurance",
|
"catInsurance": "Assurance",
|
||||||
"catMobility": "Transport",
|
"catMobility": "Transport",
|
||||||
"catLeisure": "Loisirs",
|
"catLeisure": "Loisirs et divertissement",
|
||||||
"catClothing": "Vêtements",
|
"catClothing": "Vêtements",
|
||||||
"catHealth": "Santé",
|
"catHealth": "Santé",
|
||||||
"catEducation": "Éducation",
|
"catEducation": "Éducation",
|
||||||
@@ -477,7 +477,54 @@
|
|||||||
"catTransferGiftIncome": "Transferts et Cadeaux",
|
"catTransferGiftIncome": "Transferts et Cadeaux",
|
||||||
"catGovernmentBenefits": "Allocations Sociales",
|
"catGovernmentBenefits": "Allocations Sociales",
|
||||||
"catOtherIncome": "Autres Revenus",
|
"catOtherIncome": "Autres Revenus",
|
||||||
"loadingIndicator": "Chargement…"
|
"loadingIndicator": "Chargement…",
|
||||||
|
"subcategoryLabel": "Sous-catégorie",
|
||||||
|
"catHousing": "Logement / Maison",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Soins personnels / Santé",
|
||||||
|
"catShoppingClothing": "Achats et vêtements",
|
||||||
|
"catFinancialOther": "Services financiers et autres",
|
||||||
|
"subcatRentMortgage": "Loyer / Crédit immobilier",
|
||||||
|
"subcatCondominium": "Copropriété",
|
||||||
|
"subcatUtilities": "Électricité / Eau / Gaz",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Téléphone",
|
||||||
|
"subcatRenovationMaintenance": "Rénovation / Entretien",
|
||||||
|
"subcatCleaning": "Nettoyage",
|
||||||
|
"subcatGroceries": "Supermarché",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast-food",
|
||||||
|
"subcatBakery": "Boulangerie",
|
||||||
|
"subcatFuel": "Carburant",
|
||||||
|
"subcatParkingTolls": "Parking / Péages",
|
||||||
|
"subcatPublicTransport": "Transports publics",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Entretien / Assurance",
|
||||||
|
"subcatPharmacy": "Pharmacie",
|
||||||
|
"subcatHealthInsurance": "Assurance santé",
|
||||||
|
"subcatGymSports": "Salle de sport / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauté / Cosmétiques",
|
||||||
|
"subcatTravel": "Voyages",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Événements",
|
||||||
|
"subcatHobbies": "Loisirs",
|
||||||
|
"subcatClothesShoes": "Vêtements / Chaussures",
|
||||||
|
"subcatElectronics": "Électronique",
|
||||||
|
"subcatGifts": "Cadeaux",
|
||||||
|
"subcatCoursesCollege": "Cours / Université",
|
||||||
|
"subcatSchoolSupplies": "Fournitures scolaires",
|
||||||
|
"subcatLanguages": "Langues",
|
||||||
|
"subcatLoansInterest": "Prêts / Intérêts",
|
||||||
|
"subcatBankFees": "Frais bancaires",
|
||||||
|
"subcatInsuranceOther": "Assurances",
|
||||||
|
"subcatInvestments": "Investissements",
|
||||||
|
"subcatTaxes": "Impôts",
|
||||||
|
"metaLoadError": "Impossible de charger les catégories du budget.",
|
||||||
|
"addCategory": "+ catégorie",
|
||||||
|
"addSubcategory": "+ sous-catégorie",
|
||||||
|
"newCategoryPrompt": "Nom de la nouvelle catégorie :",
|
||||||
|
"newSubcategoryPrompt": "Nom de la nouvelle sous-catégorie :",
|
||||||
|
"categoryAddedToast": "Catégorie ajoutée.",
|
||||||
|
"subcategoryAddedToast": "Sous-catégorie ajoutée."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- {{month}} जैसा",
|
"trendNeutral": "- {{month}} जैसा",
|
||||||
"validAmountRequired": "वैध राशि दर्ज करें",
|
"validAmountRequired": "वैध राशि दर्ज करें",
|
||||||
"dateRequired": "तारीख आवश्यक है",
|
"dateRequired": "तारीख आवश्यक है",
|
||||||
"catFood": "भोजन",
|
"catFood": "Food",
|
||||||
"catRent": "किराया",
|
"catRent": "किराया",
|
||||||
"catInsurance": "बीमा",
|
"catInsurance": "बीमा",
|
||||||
"catMobility": "परिवहन",
|
"catMobility": "परिवहन",
|
||||||
"catLeisure": "मनोरंजन",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "कपड़े",
|
"catClothing": "कपड़े",
|
||||||
"catHealth": "स्वास्थ्य",
|
"catHealth": "स्वास्थ्य",
|
||||||
"catEducation": "शिक्षा",
|
"catEducation": "Education",
|
||||||
"catMisc": "विविध",
|
"catMisc": "विविध",
|
||||||
"catEarnedIncome": "कमाई आय",
|
"catEarnedIncome": "कमाई आय",
|
||||||
"catInvestmentIncome": "निवेश आय",
|
"catInvestmentIncome": "निवेश आय",
|
||||||
"catTransferGiftIncome": "स्थानांतरण और उपहार",
|
"catTransferGiftIncome": "स्थानांतरण और उपहार",
|
||||||
"catGovernmentBenefits": "सामाजिक लाभ",
|
"catGovernmentBenefits": "सामाजिक लाभ",
|
||||||
"catOtherIncome": "अन्य आय",
|
"catOtherIncome": "अन्य आय",
|
||||||
"loadingIndicator": "लोड हो रहा है…"
|
"loadingIndicator": "लोड हो रहा है…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "सेटिंग्स",
|
"title": "सेटिंग्स",
|
||||||
|
|||||||
+76
-29
@@ -43,7 +43,7 @@
|
|||||||
"main": "Navigazione principale",
|
"main": "Navigazione principale",
|
||||||
"navigation": "Navigazione",
|
"navigation": "Navigazione",
|
||||||
"quickActions": "Azioni rapide",
|
"quickActions": "Azioni rapide",
|
||||||
"recipes": "Ricette",
|
"recipes": "Recipes",
|
||||||
"more": "Altro"
|
"more": "Altro"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
@@ -255,10 +255,10 @@
|
|||||||
"recipeUrlLabel": "Link ricetta (opzionale)",
|
"recipeUrlLabel": "Link ricetta (opzionale)",
|
||||||
"recipeUrlPlaceholder": "https://…",
|
"recipeUrlPlaceholder": "https://…",
|
||||||
"openRecipe": "Apri ricetta",
|
"openRecipe": "Apri ricetta",
|
||||||
"savedRecipeLabel": "Ricette salvate",
|
"savedRecipeLabel": "Saved recipe",
|
||||||
"savedRecipePlaceholder": "Seleziona ricetta",
|
"savedRecipePlaceholder": "Select recipe",
|
||||||
"saveAsRecipe": "Salva come ricetta",
|
"saveAsRecipe": "Save as recipe",
|
||||||
"recipeScaleLabel": "Scala ingredienti"
|
"recipeScaleLabel": "Scale ingredients"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendario",
|
"title": "Calendario",
|
||||||
@@ -463,11 +463,11 @@
|
|||||||
"trendNeutral": "- come {{month}}",
|
"trendNeutral": "- come {{month}}",
|
||||||
"validAmountRequired": "Inserisci un importo valido",
|
"validAmountRequired": "Inserisci un importo valido",
|
||||||
"dateRequired": "La data è obbligatoria",
|
"dateRequired": "La data è obbligatoria",
|
||||||
"catFood": "Spesa alimentare",
|
"catFood": "Alimentazione",
|
||||||
"catRent": "Affitto",
|
"catRent": "Affitto",
|
||||||
"catInsurance": "Assicurazione",
|
"catInsurance": "Assicurazione",
|
||||||
"catMobility": "Trasporti",
|
"catMobility": "Trasporti",
|
||||||
"catLeisure": "Tempo libero",
|
"catLeisure": "Tempo libero e intrattenimento",
|
||||||
"catClothing": "Abbigliamento",
|
"catClothing": "Abbigliamento",
|
||||||
"catHealth": "Salute",
|
"catHealth": "Salute",
|
||||||
"catEducation": "Istruzione",
|
"catEducation": "Istruzione",
|
||||||
@@ -477,7 +477,54 @@
|
|||||||
"catTransferGiftIncome": "Trasferimenti e Regali",
|
"catTransferGiftIncome": "Trasferimenti e Regali",
|
||||||
"catGovernmentBenefits": "Prestazioni Sociali",
|
"catGovernmentBenefits": "Prestazioni Sociali",
|
||||||
"catOtherIncome": "Altro Reddito",
|
"catOtherIncome": "Altro Reddito",
|
||||||
"loadingIndicator": "Caricamento…"
|
"loadingIndicator": "Caricamento…",
|
||||||
|
"subcategoryLabel": "Sottocategoria",
|
||||||
|
"catHousing": "Abitazione / Casa",
|
||||||
|
"catTransport": "Trasporti",
|
||||||
|
"catPersonalHealth": "Cura personale / Salute",
|
||||||
|
"catShoppingClothing": "Acquisti e abbigliamento",
|
||||||
|
"catFinancialOther": "Servizi finanziari e altro",
|
||||||
|
"subcatRentMortgage": "Affitto / Mutuo",
|
||||||
|
"subcatCondominium": "Condominio",
|
||||||
|
"subcatUtilities": "Luce / Acqua / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Telefono",
|
||||||
|
"subcatRenovationMaintenance": "Ristrutturazione / Manutenzione",
|
||||||
|
"subcatCleaning": "Pulizia",
|
||||||
|
"subcatGroceries": "Supermercato",
|
||||||
|
"subcatRestaurantsBars": "Ristoranti / Bar",
|
||||||
|
"subcatSnacksFastFood": "Snack / Fast food",
|
||||||
|
"subcatBakery": "Panetteria",
|
||||||
|
"subcatFuel": "Carburante",
|
||||||
|
"subcatParkingTolls": "Parcheggio / Pedaggi",
|
||||||
|
"subcatPublicTransport": "Trasporto pubblico",
|
||||||
|
"subcatAppsTaxi": "App / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Manutenzione / Assicurazione",
|
||||||
|
"subcatPharmacy": "Farmacia",
|
||||||
|
"subcatHealthInsurance": "Assicurazione sanitaria",
|
||||||
|
"subcatGymSports": "Palestra / Sport",
|
||||||
|
"subcatBeautyCosmetics": "Bellezza / Cosmetici",
|
||||||
|
"subcatTravel": "Viaggi",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Eventi",
|
||||||
|
"subcatHobbies": "Hobby",
|
||||||
|
"subcatClothesShoes": "Vestiti / Scarpe",
|
||||||
|
"subcatElectronics": "Elettronica",
|
||||||
|
"subcatGifts": "Regali",
|
||||||
|
"subcatCoursesCollege": "Corsi / Università",
|
||||||
|
"subcatSchoolSupplies": "Materiale scolastico",
|
||||||
|
"subcatLanguages": "Lingue",
|
||||||
|
"subcatLoansInterest": "Prestiti / Interessi",
|
||||||
|
"subcatBankFees": "Commissioni bancarie",
|
||||||
|
"subcatInsuranceOther": "Assicurazioni",
|
||||||
|
"subcatInvestments": "Investimenti",
|
||||||
|
"subcatTaxes": "Imposte",
|
||||||
|
"metaLoadError": "Impossibile caricare le categorie del budget.",
|
||||||
|
"addCategory": "+ categoria",
|
||||||
|
"addSubcategory": "+ sottocategoria",
|
||||||
|
"newCategoryPrompt": "Nome della nuova categoria:",
|
||||||
|
"newSubcategoryPrompt": "Nome della nuova sottocategoria:",
|
||||||
|
"categoryAddedToast": "Categoria aggiunta.",
|
||||||
|
"subcategoryAddedToast": "Sottocategoria aggiunta."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Impostazioni",
|
"title": "Impostazioni",
|
||||||
@@ -659,28 +706,28 @@
|
|||||||
"unitMonths": "mesi"
|
"unitMonths": "mesi"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "Ricette",
|
"title": "Recipes",
|
||||||
"addRecipe": "Aggiungi ricetta",
|
"addRecipe": "Add recipe",
|
||||||
"editRecipe": "Modifica ricetta",
|
"editRecipe": "Edit recipe",
|
||||||
"emptyTitle": "Nessuna ricetta",
|
"emptyTitle": "No recipes yet",
|
||||||
"emptyDescription": "Salva le tue ricette preferite e riutilizzale nella pianificazione dei pasti.",
|
"emptyDescription": "Save your favorite recipes and reuse them in meal planning.",
|
||||||
"titleLabel": "Titolo *",
|
"titleLabel": "Title *",
|
||||||
"titlePlaceholder": "es. Pasta Carbonara",
|
"titlePlaceholder": "e.g. Pasta Carbonara",
|
||||||
"notesLabel": "Note",
|
"notesLabel": "Notes",
|
||||||
"notesPlaceholder": "Opzionale...",
|
"notesPlaceholder": "Optional...",
|
||||||
"urlLabel": "Link della ricetta",
|
"urlLabel": "Recipe link",
|
||||||
"urlPlaceholder": "https://...",
|
"urlPlaceholder": "https://...",
|
||||||
"ingredientsLabel": "Ingredienti",
|
"ingredientsLabel": "Ingredients",
|
||||||
"addToMeals": "Aggiungi al piano alimentare",
|
"addToMeals": "Add to meal plan",
|
||||||
"openLink": "Apri il link della ricetta",
|
"openLink": "Open recipe link",
|
||||||
"deleteConfirm": "Eliminare la ricetta \"{{title}}\"?",
|
"deleteConfirm": "Delete recipe \"{{title}}\"?",
|
||||||
"created": "Ricetta salvata.",
|
"created": "Recipe saved.",
|
||||||
"updated": "Ricetta aggiornata.",
|
"updated": "Recipe updated.",
|
||||||
"deleted": "Ricetta eliminata.",
|
"deleted": "Recipe deleted.",
|
||||||
"titleRequired": "È richiesto il titolo",
|
"titleRequired": "Title is required",
|
||||||
"duplicate": "Duplicato",
|
"duplicate": "Duplicate",
|
||||||
"duplicated": "Ricetta duplicata.",
|
"duplicated": "Recipe duplicated.",
|
||||||
"copySuffix": "copia"
|
"copySuffix": "copy"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Ricerca",
|
"title": "Ricerca",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- {{month}} と同じ",
|
"trendNeutral": "- {{month}} と同じ",
|
||||||
"validAmountRequired": "有効な金額を入力してください",
|
"validAmountRequired": "有効な金額を入力してください",
|
||||||
"dateRequired": "日付は必須です",
|
"dateRequired": "日付は必須です",
|
||||||
"catFood": "食費",
|
"catFood": "Food",
|
||||||
"catRent": "家賃",
|
"catRent": "家賃",
|
||||||
"catInsurance": "保険",
|
"catInsurance": "保険",
|
||||||
"catMobility": "交通費",
|
"catMobility": "交通費",
|
||||||
"catLeisure": "娯楽",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "衣服",
|
"catClothing": "衣服",
|
||||||
"catHealth": "医療",
|
"catHealth": "医療",
|
||||||
"catEducation": "教育",
|
"catEducation": "Education",
|
||||||
"catMisc": "その他",
|
"catMisc": "その他",
|
||||||
"catEarnedIncome": "給与・報酬",
|
"catEarnedIncome": "給与・報酬",
|
||||||
"catInvestmentIncome": "投資収入",
|
"catInvestmentIncome": "投資収入",
|
||||||
"catTransferGiftIncome": "譲渡・贈与",
|
"catTransferGiftIncome": "譲渡・贈与",
|
||||||
"catGovernmentBenefits": "社会保障給付",
|
"catGovernmentBenefits": "社会保障給付",
|
||||||
"catOtherIncome": "その他の収入",
|
"catOtherIncome": "その他の収入",
|
||||||
"loadingIndicator": "読み込み中…"
|
"loadingIndicator": "読み込み中…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
|
|||||||
+49
-2
@@ -467,7 +467,7 @@
|
|||||||
"catRent": "Aluguel",
|
"catRent": "Aluguel",
|
||||||
"catInsurance": "Seguro",
|
"catInsurance": "Seguro",
|
||||||
"catMobility": "Transporte",
|
"catMobility": "Transporte",
|
||||||
"catLeisure": "Lazer",
|
"catLeisure": "Lazer e Entretenimento",
|
||||||
"catClothing": "Roupas",
|
"catClothing": "Roupas",
|
||||||
"catHealth": "Saúde",
|
"catHealth": "Saúde",
|
||||||
"catEducation": "Educação",
|
"catEducation": "Educação",
|
||||||
@@ -477,7 +477,54 @@
|
|||||||
"catTransferGiftIncome": "Transferências e Presentes",
|
"catTransferGiftIncome": "Transferências e Presentes",
|
||||||
"catGovernmentBenefits": "Benefícios Sociais",
|
"catGovernmentBenefits": "Benefícios Sociais",
|
||||||
"catOtherIncome": "Outras Rendas",
|
"catOtherIncome": "Outras Rendas",
|
||||||
"loadingIndicator": "Carregando…"
|
"loadingIndicator": "Carregando…",
|
||||||
|
"subcategoryLabel": "Subcategoria",
|
||||||
|
"catHousing": "Habitação / Casa",
|
||||||
|
"catTransport": "Transporte",
|
||||||
|
"catPersonalHealth": "Cuidados Pessoais / Saúde",
|
||||||
|
"catShoppingClothing": "Compras e Vestuário",
|
||||||
|
"catFinancialOther": "Serviços Financeiros e Outros",
|
||||||
|
"subcatRentMortgage": "Aluguel / Prestação",
|
||||||
|
"subcatCondominium": "Condomínio",
|
||||||
|
"subcatUtilities": "Luz / Água / Gás",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Telefone",
|
||||||
|
"subcatRenovationMaintenance": "Reforma / Manutenção",
|
||||||
|
"subcatCleaning": "Limpeza",
|
||||||
|
"subcatGroceries": "Supermercado",
|
||||||
|
"subcatRestaurantsBars": "Restaurante / Bares",
|
||||||
|
"subcatSnacksFastFood": "Lanches / Fast Food",
|
||||||
|
"subcatBakery": "Padaria",
|
||||||
|
"subcatFuel": "Combustível",
|
||||||
|
"subcatParkingTolls": "Estacionamento / Pedágio",
|
||||||
|
"subcatPublicTransport": "Transporte Público",
|
||||||
|
"subcatAppsTaxi": "Aplicativos / Táxi",
|
||||||
|
"subcatMaintenanceInsurance": "Manutenção / Seguro",
|
||||||
|
"subcatPharmacy": "Farmácia",
|
||||||
|
"subcatHealthInsurance": "Plano de Saúde",
|
||||||
|
"subcatGymSports": "Academia / Esportes",
|
||||||
|
"subcatBeautyCosmetics": "Beleza / Cosméticos",
|
||||||
|
"subcatTravel": "Viagens",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Eventos",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Roupas / Calçados",
|
||||||
|
"subcatElectronics": "Eletrônicos",
|
||||||
|
"subcatGifts": "Presentes",
|
||||||
|
"subcatCoursesCollege": "Cursos / Faculdade",
|
||||||
|
"subcatSchoolSupplies": "Material Escolar",
|
||||||
|
"subcatLanguages": "Idiomas",
|
||||||
|
"subcatLoansInterest": "Empréstimos / Juros",
|
||||||
|
"subcatBankFees": "Tarifas Bancárias",
|
||||||
|
"subcatInsuranceOther": "Seguros",
|
||||||
|
"subcatInvestments": "Investimentos",
|
||||||
|
"subcatTaxes": "Impostos",
|
||||||
|
"metaLoadError": "Categorias do orçamento não puderam ser carregadas.",
|
||||||
|
"addCategory": "+ categoria",
|
||||||
|
"addSubcategory": "+ subcategoria",
|
||||||
|
"newCategoryPrompt": "Nome da nova categoria:",
|
||||||
|
"newSubcategoryPrompt": "Nome da nova subcategoria:",
|
||||||
|
"categoryAddedToast": "Categoria adicionada.",
|
||||||
|
"subcategoryAddedToast": "Subcategoria adicionada."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Configurações",
|
"title": "Configurações",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "— как в {{month}}",
|
"trendNeutral": "— как в {{month}}",
|
||||||
"validAmountRequired": "Введите корректную сумму",
|
"validAmountRequired": "Введите корректную сумму",
|
||||||
"dateRequired": "Дата обязательна",
|
"dateRequired": "Дата обязательна",
|
||||||
"catFood": "Продукты",
|
"catFood": "Food",
|
||||||
"catRent": "Аренда",
|
"catRent": "Аренда",
|
||||||
"catInsurance": "Страховка",
|
"catInsurance": "Страховка",
|
||||||
"catMobility": "Транспорт",
|
"catMobility": "Транспорт",
|
||||||
"catLeisure": "Досуг",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "Одежда",
|
"catClothing": "Одежда",
|
||||||
"catHealth": "Здоровье",
|
"catHealth": "Здоровье",
|
||||||
"catEducation": "Образование",
|
"catEducation": "Education",
|
||||||
"catMisc": "Разное",
|
"catMisc": "Разное",
|
||||||
"catEarnedIncome": "Трудовой доход",
|
"catEarnedIncome": "Трудовой доход",
|
||||||
"catInvestmentIncome": "Инвестиционный доход",
|
"catInvestmentIncome": "Инвестиционный доход",
|
||||||
"catTransferGiftIncome": "Переводы и подарки",
|
"catTransferGiftIncome": "Переводы и подарки",
|
||||||
"catGovernmentBenefits": "Социальные пособия",
|
"catGovernmentBenefits": "Социальные пособия",
|
||||||
"catOtherIncome": "Прочие доходы",
|
"catOtherIncome": "Прочие доходы",
|
||||||
"loadingIndicator": "Загрузка…"
|
"loadingIndicator": "Загрузка…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Настройки",
|
"title": "Настройки",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- samma som {{month}}",
|
"trendNeutral": "- samma som {{month}}",
|
||||||
"validAmountRequired": "Ange ett giltigt belopp",
|
"validAmountRequired": "Ange ett giltigt belopp",
|
||||||
"dateRequired": "Datum krävs",
|
"dateRequired": "Datum krävs",
|
||||||
"catFood": "Specerier",
|
"catFood": "Food",
|
||||||
"catRent": "Hyra",
|
"catRent": "Hyra",
|
||||||
"catInsurance": "Försäkring",
|
"catInsurance": "Försäkring",
|
||||||
"catMobility": "Transport",
|
"catMobility": "Transport",
|
||||||
"catLeisure": "Fritid",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "Kläder",
|
"catClothing": "Kläder",
|
||||||
"catHealth": "Hälsa",
|
"catHealth": "Hälsa",
|
||||||
"catEducation": "Utbildning",
|
"catEducation": "Education",
|
||||||
"catMisc": "Diverse",
|
"catMisc": "Diverse",
|
||||||
"catEarnedIncome": "Arbetsinkomst",
|
"catEarnedIncome": "Arbetsinkomst",
|
||||||
"catInvestmentIncome": "Investeringsinkomst",
|
"catInvestmentIncome": "Investeringsinkomst",
|
||||||
"catTransferGiftIncome": "Överföringar och gåvor",
|
"catTransferGiftIncome": "Överföringar och gåvor",
|
||||||
"catGovernmentBenefits": "Socialförmåner",
|
"catGovernmentBenefits": "Socialförmåner",
|
||||||
"catOtherIncome": "Övrig inkomst",
|
"catOtherIncome": "Övrig inkomst",
|
||||||
"loadingIndicator": "Laddar…"
|
"loadingIndicator": "Laddar…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Inställningar",
|
"title": "Inställningar",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- {{month}} ile aynı",
|
"trendNeutral": "- {{month}} ile aynı",
|
||||||
"validAmountRequired": "Lütfen geçerli bir tutar girin",
|
"validAmountRequired": "Lütfen geçerli bir tutar girin",
|
||||||
"dateRequired": "Tarih zorunludur",
|
"dateRequired": "Tarih zorunludur",
|
||||||
"catFood": "Market",
|
"catFood": "Food",
|
||||||
"catRent": "Kira",
|
"catRent": "Kira",
|
||||||
"catInsurance": "Sigorta",
|
"catInsurance": "Sigorta",
|
||||||
"catMobility": "Ulaşım",
|
"catMobility": "Ulaşım",
|
||||||
"catLeisure": "Eğlence",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "Giyim",
|
"catClothing": "Giyim",
|
||||||
"catHealth": "Sağlık",
|
"catHealth": "Sağlık",
|
||||||
"catEducation": "Eğitim",
|
"catEducation": "Education",
|
||||||
"catMisc": "Diğer",
|
"catMisc": "Diğer",
|
||||||
"catEarnedIncome": "Kazanç Geliri",
|
"catEarnedIncome": "Kazanç Geliri",
|
||||||
"catInvestmentIncome": "Yatırım Geliri",
|
"catInvestmentIncome": "Yatırım Geliri",
|
||||||
"catTransferGiftIncome": "Transferler ve Hediyeler",
|
"catTransferGiftIncome": "Transferler ve Hediyeler",
|
||||||
"catGovernmentBenefits": "Sosyal Yardımlar",
|
"catGovernmentBenefits": "Sosyal Yardımlar",
|
||||||
"catOtherIncome": "Diğer Gelir",
|
"catOtherIncome": "Diğer Gelir",
|
||||||
"loadingIndicator": "Yükleniyor…"
|
"loadingIndicator": "Yükleniyor…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Ayarlar",
|
"title": "Ayarlar",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "— так само, як у {{month}}",
|
"trendNeutral": "— так само, як у {{month}}",
|
||||||
"validAmountRequired": "Будь ласка, введіть коректну суму",
|
"validAmountRequired": "Будь ласка, введіть коректну суму",
|
||||||
"dateRequired": "Дата є обов'язковою",
|
"dateRequired": "Дата є обов'язковою",
|
||||||
"catFood": "Продукти",
|
"catFood": "Food",
|
||||||
"catRent": "Оренда",
|
"catRent": "Оренда",
|
||||||
"catInsurance": "Страхування",
|
"catInsurance": "Страхування",
|
||||||
"catMobility": "Транспорт",
|
"catMobility": "Транспорт",
|
||||||
"catLeisure": "Дозвілля",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "Одяг",
|
"catClothing": "Одяг",
|
||||||
"catHealth": "Здоров'я",
|
"catHealth": "Здоров'я",
|
||||||
"catEducation": "Освіта",
|
"catEducation": "Education",
|
||||||
"catMisc": "Різне",
|
"catMisc": "Різне",
|
||||||
"catEarnedIncome": "Трудовий дохід",
|
"catEarnedIncome": "Трудовий дохід",
|
||||||
"catInvestmentIncome": "Інвестиційний дохід",
|
"catInvestmentIncome": "Інвестиційний дохід",
|
||||||
"catTransferGiftIncome": "Переводи та подарунки",
|
"catTransferGiftIncome": "Переводи та подарунки",
|
||||||
"catGovernmentBenefits": "Соціальні виплати",
|
"catGovernmentBenefits": "Соціальні виплати",
|
||||||
"catOtherIncome": "Інші доходи",
|
"catOtherIncome": "Інші доходи",
|
||||||
"loadingIndicator": "Завантаження…"
|
"loadingIndicator": "Завантаження…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Налаштування",
|
"title": "Налаштування",
|
||||||
|
|||||||
+51
-4
@@ -463,21 +463,68 @@
|
|||||||
"trendNeutral": "- 与 {{month}} 相同",
|
"trendNeutral": "- 与 {{month}} 相同",
|
||||||
"validAmountRequired": "请输入有效金额",
|
"validAmountRequired": "请输入有效金额",
|
||||||
"dateRequired": "日期为必填项",
|
"dateRequired": "日期为必填项",
|
||||||
"catFood": "食品",
|
"catFood": "Food",
|
||||||
"catRent": "租金",
|
"catRent": "租金",
|
||||||
"catInsurance": "保险",
|
"catInsurance": "保险",
|
||||||
"catMobility": "出行",
|
"catMobility": "出行",
|
||||||
"catLeisure": "休闲",
|
"catLeisure": "Leisure and Entertainment",
|
||||||
"catClothing": "服装",
|
"catClothing": "服装",
|
||||||
"catHealth": "健康",
|
"catHealth": "健康",
|
||||||
"catEducation": "教育",
|
"catEducation": "Education",
|
||||||
"catMisc": "其他",
|
"catMisc": "其他",
|
||||||
"catEarnedIncome": "劳动收入",
|
"catEarnedIncome": "劳动收入",
|
||||||
"catInvestmentIncome": "投资收入",
|
"catInvestmentIncome": "投资收入",
|
||||||
"catTransferGiftIncome": "转账和礼物",
|
"catTransferGiftIncome": "转账和礼物",
|
||||||
"catGovernmentBenefits": "社会福利",
|
"catGovernmentBenefits": "社会福利",
|
||||||
"catOtherIncome": "其他收入",
|
"catOtherIncome": "其他收入",
|
||||||
"loadingIndicator": "加载中…"
|
"loadingIndicator": "加载中…",
|
||||||
|
"subcategoryLabel": "Subcategory",
|
||||||
|
"catHousing": "Housing / Home",
|
||||||
|
"catTransport": "Transport",
|
||||||
|
"catPersonalHealth": "Personal Care / Health",
|
||||||
|
"catShoppingClothing": "Shopping and Clothing",
|
||||||
|
"catFinancialOther": "Financial Services and Other",
|
||||||
|
"subcatRentMortgage": "Rent / Mortgage",
|
||||||
|
"subcatCondominium": "Condominium fees",
|
||||||
|
"subcatUtilities": "Electricity / Water / Gas",
|
||||||
|
"subcatInternetTvPhone": "Internet / TV / Phone",
|
||||||
|
"subcatRenovationMaintenance": "Renovation / Maintenance",
|
||||||
|
"subcatCleaning": "Cleaning",
|
||||||
|
"subcatGroceries": "Groceries",
|
||||||
|
"subcatRestaurantsBars": "Restaurants / Bars",
|
||||||
|
"subcatSnacksFastFood": "Snacks / Fast Food",
|
||||||
|
"subcatBakery": "Bakery",
|
||||||
|
"subcatFuel": "Fuel",
|
||||||
|
"subcatParkingTolls": "Parking / Tolls",
|
||||||
|
"subcatPublicTransport": "Public transport",
|
||||||
|
"subcatAppsTaxi": "Apps / Taxi",
|
||||||
|
"subcatMaintenanceInsurance": "Maintenance / Insurance",
|
||||||
|
"subcatPharmacy": "Pharmacy",
|
||||||
|
"subcatHealthInsurance": "Health insurance",
|
||||||
|
"subcatGymSports": "Gym / Sports",
|
||||||
|
"subcatBeautyCosmetics": "Beauty / Cosmetics",
|
||||||
|
"subcatTravel": "Travel",
|
||||||
|
"subcatStreaming": "Streaming",
|
||||||
|
"subcatEvents": "Events",
|
||||||
|
"subcatHobbies": "Hobbies",
|
||||||
|
"subcatClothesShoes": "Clothes / Shoes",
|
||||||
|
"subcatElectronics": "Electronics",
|
||||||
|
"subcatGifts": "Gifts",
|
||||||
|
"subcatCoursesCollege": "Courses / College",
|
||||||
|
"subcatSchoolSupplies": "School supplies",
|
||||||
|
"subcatLanguages": "Languages",
|
||||||
|
"subcatLoansInterest": "Loans / Interest",
|
||||||
|
"subcatBankFees": "Bank fees",
|
||||||
|
"subcatInsuranceOther": "Insurance",
|
||||||
|
"subcatInvestments": "Investments",
|
||||||
|
"subcatTaxes": "Taxes",
|
||||||
|
"metaLoadError": "Budget categories could not be loaded.",
|
||||||
|
"addCategory": "+ category",
|
||||||
|
"addSubcategory": "+ subcategory",
|
||||||
|
"newCategoryPrompt": "Name of the new category:",
|
||||||
|
"newSubcategoryPrompt": "Name of the new subcategory:",
|
||||||
|
"categoryAddedToast": "Category added.",
|
||||||
|
"subcategoryAddedToast": "Subcategory added."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
|
|||||||
+190
-37
@@ -15,29 +15,16 @@ import { esc } from '/utils/html.js';
|
|||||||
// Konstanten
|
// Konstanten
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
const EXPENSE_CATEGORIES = [
|
const CATEGORY_I18N = () => ({
|
||||||
'Lebensmittel', 'Miete', 'Versicherung', 'Mobilität',
|
|
||||||
'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges',
|
|
||||||
];
|
|
||||||
|
|
||||||
const INCOME_CATEGORIES = [
|
|
||||||
'Erwerbseinkommen', 'Kapitalerträge', 'Geschenke & Transfers',
|
|
||||||
'Sozialleistungen', 'Sonstiges Einkommen',
|
|
||||||
];
|
|
||||||
|
|
||||||
const CATEGORIES = [...EXPENSE_CATEGORIES, ...INCOME_CATEGORIES];
|
|
||||||
|
|
||||||
const CATEGORY_LABELS = () => ({
|
|
||||||
// Expense categories
|
// Expense categories
|
||||||
'Lebensmittel': t('budget.catFood'),
|
housing: t('budget.catHousing'),
|
||||||
'Miete': t('budget.catRent'),
|
food: t('budget.catFood'),
|
||||||
'Versicherung': t('budget.catInsurance'),
|
transport: t('budget.catTransport'),
|
||||||
'Mobilität': t('budget.catMobility'),
|
personal_health: t('budget.catPersonalHealth'),
|
||||||
'Freizeit': t('budget.catLeisure'),
|
leisure: t('budget.catLeisure'),
|
||||||
'Kleidung': t('budget.catClothing'),
|
shopping_clothing: t('budget.catShoppingClothing'),
|
||||||
'Gesundheit': t('budget.catHealth'),
|
education: t('budget.catEducation'),
|
||||||
'Bildung': t('budget.catEducation'),
|
financial_other: t('budget.catFinancialOther'),
|
||||||
'Sonstiges': t('budget.catMisc'),
|
|
||||||
// Income categories
|
// Income categories
|
||||||
'Erwerbseinkommen': t('budget.catEarnedIncome'),
|
'Erwerbseinkommen': t('budget.catEarnedIncome'),
|
||||||
'Kapitalerträge': t('budget.catInvestmentIncome'),
|
'Kapitalerträge': t('budget.catInvestmentIncome'),
|
||||||
@@ -46,6 +33,82 @@ const CATEGORY_LABELS = () => ({
|
|||||||
'Sonstiges Einkommen': t('budget.catOtherIncome'),
|
'Sonstiges Einkommen': t('budget.catOtherIncome'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SUBCATEGORY_I18N = () => ({
|
||||||
|
rent_mortgage: t('budget.subcatRentMortgage'),
|
||||||
|
condominium: t('budget.subcatCondominium'),
|
||||||
|
utilities: t('budget.subcatUtilities'),
|
||||||
|
internet_tv_phone: t('budget.subcatInternetTvPhone'),
|
||||||
|
renovation_maintenance: t('budget.subcatRenovationMaintenance'),
|
||||||
|
cleaning: t('budget.subcatCleaning'),
|
||||||
|
groceries: t('budget.subcatGroceries'),
|
||||||
|
restaurants_bars: t('budget.subcatRestaurantsBars'),
|
||||||
|
snacks_fast_food: t('budget.subcatSnacksFastFood'),
|
||||||
|
bakery: t('budget.subcatBakery'),
|
||||||
|
fuel: t('budget.subcatFuel'),
|
||||||
|
parking_tolls: t('budget.subcatParkingTolls'),
|
||||||
|
public_transport: t('budget.subcatPublicTransport'),
|
||||||
|
apps_taxi: t('budget.subcatAppsTaxi'),
|
||||||
|
maintenance_insurance: t('budget.subcatMaintenanceInsurance'),
|
||||||
|
pharmacy: t('budget.subcatPharmacy'),
|
||||||
|
health_insurance: t('budget.subcatHealthInsurance'),
|
||||||
|
gym_sports: t('budget.subcatGymSports'),
|
||||||
|
beauty_cosmetics: t('budget.subcatBeautyCosmetics'),
|
||||||
|
travel: t('budget.subcatTravel'),
|
||||||
|
streaming: t('budget.subcatStreaming'),
|
||||||
|
events: t('budget.subcatEvents'),
|
||||||
|
hobbies: t('budget.subcatHobbies'),
|
||||||
|
clothes_shoes: t('budget.subcatClothesShoes'),
|
||||||
|
electronics: t('budget.subcatElectronics'),
|
||||||
|
gifts: t('budget.subcatGifts'),
|
||||||
|
courses_college: t('budget.subcatCoursesCollege'),
|
||||||
|
school_supplies: t('budget.subcatSchoolSupplies'),
|
||||||
|
languages: t('budget.subcatLanguages'),
|
||||||
|
loans_interest: t('budget.subcatLoansInterest'),
|
||||||
|
bank_fees: t('budget.subcatBankFees'),
|
||||||
|
insurance_other: t('budget.subcatInsuranceOther'),
|
||||||
|
investments: t('budget.subcatInvestments'),
|
||||||
|
taxes: t('budget.subcatTaxes'),
|
||||||
|
});
|
||||||
|
|
||||||
|
function categoryLabel(category) {
|
||||||
|
const item = typeof category === 'object'
|
||||||
|
? category
|
||||||
|
: [...expenseCategories(), ...incomeCategories()].find((c) => c.key === category);
|
||||||
|
const key = item?.key ?? category;
|
||||||
|
const name = item?.name ?? category;
|
||||||
|
return CATEGORY_I18N()[key] ?? name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subcategoryLabel(subcategory) {
|
||||||
|
const item = typeof subcategory === 'object'
|
||||||
|
? subcategory
|
||||||
|
: Object.values(state.meta.expenseSubcategories ?? {}).flat().find((s) => s.key === subcategory);
|
||||||
|
const key = item?.key ?? subcategory;
|
||||||
|
const name = item?.name ?? subcategory;
|
||||||
|
return SUBCATEGORY_I18N()[key] ?? name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expenseCategories() {
|
||||||
|
return state.meta.expenseCategories ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function incomeCategories() {
|
||||||
|
return state.meta.incomeCategories ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubcategories(category) {
|
||||||
|
return state.meta.expenseSubcategories?.[category] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultSubcategory(category) {
|
||||||
|
return getSubcategories(category)[0]?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultCategory(type) {
|
||||||
|
const cats = type === 'income' ? incomeCategories() : expenseCategories();
|
||||||
|
return cats[0]?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
function getMonthName(monthIndex) {
|
function getMonthName(monthIndex) {
|
||||||
// monthIndex: 0-based (0=Januar, 11=Dezember)
|
// monthIndex: 0-based (0=Januar, 11=Dezember)
|
||||||
const date = new Date(2000, monthIndex, 1);
|
const date = new Date(2000, monthIndex, 1);
|
||||||
@@ -62,6 +125,7 @@ let state = {
|
|||||||
summary: null,
|
summary: null,
|
||||||
prevSummary: null, // Vormonat für Monatsvergleich
|
prevSummary: null, // Vormonat für Monatsvergleich
|
||||||
currency: 'EUR',
|
currency: 'EUR',
|
||||||
|
meta: { expenseCategories: [], incomeCategories: [], expenseSubcategories: {} },
|
||||||
};
|
};
|
||||||
let _container = null;
|
let _container = null;
|
||||||
|
|
||||||
@@ -110,6 +174,21 @@ async function loadMonth(month) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadBudgetMeta() {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/budget/meta');
|
||||||
|
state.meta = {
|
||||||
|
expenseCategories: res.data?.expenseCategories ?? [],
|
||||||
|
incomeCategories: res.data?.incomeCategories ?? [],
|
||||||
|
expenseSubcategories: res.data?.expenseSubcategories ?? {},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Budget] meta Fehler:', err);
|
||||||
|
state.meta = { expenseCategories: [], incomeCategories: [], expenseSubcategories: {} };
|
||||||
|
window.oikos?.showToast(t('budget.metaLoadError'), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Entry Point
|
// Entry Point
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -120,7 +199,10 @@ export async function render(container, { user }) {
|
|||||||
state.month = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
|
state.month = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prefsRes = await api.get('/preferences');
|
const [prefsRes] = await Promise.all([
|
||||||
|
api.get('/preferences'),
|
||||||
|
loadBudgetMeta(),
|
||||||
|
]);
|
||||||
state.currency = prefsRes.data?.currency ?? 'EUR';
|
state.currency = prefsRes.data?.currency ?? 'EUR';
|
||||||
} catch (_) { /* Fallback auf EUR */ }
|
} catch (_) { /* Fallback auf EUR */ }
|
||||||
|
|
||||||
@@ -274,7 +356,7 @@ function renderCategoryBars(byCategory) {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="budget-bar-row">
|
<div class="budget-bar-row">
|
||||||
<div class="budget-bar-row__label" title="${esc(CATEGORY_LABELS()[c.category] ?? c.category)}">${esc(CATEGORY_LABELS()[c.category] ?? c.category)}</div>
|
<div class="budget-bar-row__label" title="${esc(categoryLabel(c.category))}">${esc(categoryLabel(c.category))}</div>
|
||||||
<div class="budget-bar-row__track">
|
<div class="budget-bar-row__track">
|
||||||
<div class="budget-bar-row__fill ${cls}" style="width:${pct}%;"></div>
|
<div class="budget-bar-row__fill ${cls}" style="width:${pct}%;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,13 +387,16 @@ function renderEntries() {
|
|||||||
const sign = isIncome ? '+' : '';
|
const sign = isIncome ? '+' : '';
|
||||||
const date = formatEntryDate(e.date);
|
const date = formatEntryDate(e.date);
|
||||||
const recurTag = e.is_recurring ? ' 🔁' : (e.recurrence_parent_id ? ' ↩' : '');
|
const recurTag = e.is_recurring ? ' 🔁' : (e.recurrence_parent_id ? ' ↩' : '');
|
||||||
|
const categoryMeta = isIncome || !e.subcategory
|
||||||
|
? categoryLabel(e.category)
|
||||||
|
: `${categoryLabel(e.category)} · ${subcategoryLabel(e.subcategory)}`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="budget-entry" data-id="${e.id}">
|
<div class="budget-entry" data-id="${e.id}">
|
||||||
<div class="budget-entry__indicator ${indClass}"></div>
|
<div class="budget-entry__indicator ${indClass}"></div>
|
||||||
<div class="budget-entry__body">
|
<div class="budget-entry__body">
|
||||||
<div class="budget-entry__title">${esc(e.title)}</div>
|
<div class="budget-entry__title">${esc(e.title)}</div>
|
||||||
<div class="budget-entry__meta">${date} · ${esc(CATEGORY_LABELS()[e.category] ?? e.category)}${recurTag}</div>
|
<div class="budget-entry__meta">${date} · ${esc(categoryMeta)}${recurTag}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="budget-entry__amount ${amtClass}">${sign}${formatAmount(e.amount)}</div>
|
<div class="budget-entry__amount ${amtClass}">${sign}${formatAmount(e.amount)}</div>
|
||||||
<button class="budget-entry__delete" data-action="delete" data-id="${e.id}" aria-label="${t('budget.deleteLabel')}">
|
<button class="budget-entry__delete" data-action="delete" data-id="${e.id}" aria-label="${t('budget.deleteLabel')}">
|
||||||
@@ -359,10 +444,14 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
const isExpense = isEdit ? entry.amount < 0 : true;
|
const isExpense = isEdit ? entry.amount < 0 : true;
|
||||||
const absAmount = isEdit ? Math.abs(entry.amount).toFixed(2) : '';
|
const absAmount = isEdit ? Math.abs(entry.amount).toFixed(2) : '';
|
||||||
|
|
||||||
const catLabels = CATEGORY_LABELS();
|
const initialCats = isExpense ? expenseCategories() : incomeCategories();
|
||||||
const initialCats = isExpense ? EXPENSE_CATEGORIES : INCOME_CATEGORIES;
|
|
||||||
const catOpts = initialCats.map((c) =>
|
const catOpts = initialCats.map((c) =>
|
||||||
`<option value="${c}" ${isEdit && entry.category === c ? 'selected' : ''}>${catLabels[c] || c}</option>`
|
`<option value="${esc(c.key)}" ${isEdit && entry.category === c.key ? 'selected' : ''}>${esc(categoryLabel(c))}</option>`
|
||||||
|
).join('');
|
||||||
|
const initialCategory = isEdit ? entry.category : initialCats[0]?.key;
|
||||||
|
const initialSubcategory = isEdit ? entry.subcategory : defaultSubcategory(initialCategory);
|
||||||
|
const subcatOpts = getSubcategories(initialCategory).map((s) =>
|
||||||
|
`<option value="${esc(s.key)}" ${initialSubcategory === s.key ? 'selected' : ''}>${esc(subcategoryLabel(s))}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const content = `
|
const content = `
|
||||||
@@ -387,10 +476,21 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<div class="budget-field-header">
|
||||||
<label class="form-label" for="bm-category">${t('budget.categoryLabel')}</label>
|
<label class="form-label" for="bm-category">${t('budget.categoryLabel')}</label>
|
||||||
|
<button class="btn btn--secondary budget-inline-add" type="button" id="bm-add-category">${t('budget.addCategory')}</button>
|
||||||
|
</div>
|
||||||
<select class="form-input" id="bm-category">${catOpts}</select>
|
<select class="form-input" id="bm-category">${catOpts}</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="bm-subcategory-group" ${isExpense ? '' : 'hidden'}>
|
||||||
|
<div class="budget-field-header">
|
||||||
|
<label class="form-label" for="bm-subcategory">${t('budget.subcategoryLabel')}</label>
|
||||||
|
<button class="btn btn--secondary budget-inline-add" type="button" id="bm-add-subcategory">${t('budget.addSubcategory')}</button>
|
||||||
|
</div>
|
||||||
|
<select class="form-input" id="bm-subcategory">${subcatOpts}</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="bm-date">${t('budget.dateLabel')}</label>
|
<label class="form-label" for="bm-date">${t('budget.dateLabel')}</label>
|
||||||
<input type="date" class="form-input" id="bm-date"
|
<input type="date" class="form-input" id="bm-date"
|
||||||
@@ -422,20 +522,70 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
onSave(panel) {
|
onSave(panel) {
|
||||||
let currentType = isExpense ? 'expense' : 'income';
|
let currentType = isExpense ? 'expense' : 'income';
|
||||||
|
|
||||||
const updateCategoryOptions = () => {
|
const updateCategoryOptions = (preferredCategory = '') => {
|
||||||
const catLabels = CATEGORY_LABELS();
|
const cats = currentType === 'income' ? incomeCategories() : expenseCategories();
|
||||||
const cats = currentType === 'income' ? INCOME_CATEGORIES : EXPENSE_CATEGORIES;
|
|
||||||
const catSelect = panel.querySelector('#bm-category');
|
const catSelect = panel.querySelector('#bm-category');
|
||||||
const currentValue = catSelect.value;
|
const currentValue = preferredCategory || catSelect.value;
|
||||||
|
|
||||||
const options = cats.map((c) => {
|
const options = cats.map((c) => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = c;
|
opt.value = c.key;
|
||||||
opt.textContent = catLabels[c] || c;
|
opt.textContent = categoryLabel(c);
|
||||||
opt.selected = currentValue === c;
|
opt.selected = currentValue === c.key;
|
||||||
return opt;
|
return opt;
|
||||||
});
|
});
|
||||||
catSelect.replaceChildren(...options);
|
catSelect.replaceChildren(...options);
|
||||||
|
if (!cats.some((c) => c.key === catSelect.value)) catSelect.value = cats[0]?.key || '';
|
||||||
|
updateSubcategoryOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSubcategoryOptions = (preferredSubcategory = '') => {
|
||||||
|
const catSelect = panel.querySelector('#bm-category');
|
||||||
|
const subcatGroup = panel.querySelector('#bm-subcategory-group');
|
||||||
|
const subcatSelect = panel.querySelector('#bm-subcategory');
|
||||||
|
const subcategories = currentType === 'expense' ? getSubcategories(catSelect.value) : [];
|
||||||
|
const currentValue = preferredSubcategory || subcatSelect.value;
|
||||||
|
|
||||||
|
subcatGroup.hidden = currentType !== 'expense';
|
||||||
|
subcatSelect.replaceChildren(...subcategories.map((s) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s.key;
|
||||||
|
opt.textContent = subcategoryLabel(s);
|
||||||
|
opt.selected = currentValue === s.key;
|
||||||
|
return opt;
|
||||||
|
}));
|
||||||
|
if (subcategories.length && !subcategories.some((s) => s.key === subcatSelect.value)) {
|
||||||
|
subcatSelect.value = subcategories[0].key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCategory = async () => {
|
||||||
|
const name = window.prompt(t('budget.newCategoryPrompt'));
|
||||||
|
if (!name?.trim()) return;
|
||||||
|
try {
|
||||||
|
const res = await api.post('/budget/categories', { name: name.trim(), type: currentType });
|
||||||
|
await loadBudgetMeta();
|
||||||
|
updateCategoryOptions(res.data.key);
|
||||||
|
window.oikos?.showToast(t('budget.categoryAddedToast'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSubcategory = async () => {
|
||||||
|
if (currentType !== 'expense') return;
|
||||||
|
const category = panel.querySelector('#bm-category').value;
|
||||||
|
if (!category) return;
|
||||||
|
const name = window.prompt(t('budget.newSubcategoryPrompt'));
|
||||||
|
if (!name?.trim()) return;
|
||||||
|
try {
|
||||||
|
const res = await api.post(`/budget/categories/${encodeURIComponent(category)}/subcategories`, { name: name.trim() });
|
||||||
|
await loadBudgetMeta();
|
||||||
|
updateSubcategoryOptions(res.data.key);
|
||||||
|
window.oikos?.showToast(t('budget.subcategoryAddedToast'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
panel.querySelector('#type-expense').addEventListener('click', () => {
|
panel.querySelector('#type-expense').addEventListener('click', () => {
|
||||||
@@ -450,6 +600,9 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
panel.querySelector('#type-expense').classList.remove('amount-type-btn--active');
|
panel.querySelector('#type-expense').classList.remove('amount-type-btn--active');
|
||||||
updateCategoryOptions();
|
updateCategoryOptions();
|
||||||
});
|
});
|
||||||
|
panel.querySelector('#bm-category').addEventListener('change', () => updateSubcategoryOptions());
|
||||||
|
panel.querySelector('#bm-add-category').addEventListener('click', addCategory);
|
||||||
|
panel.querySelector('#bm-add-subcategory').addEventListener('click', addSubcategory);
|
||||||
|
|
||||||
panel.querySelector('#bm-cancel').addEventListener('click', closeModal);
|
panel.querySelector('#bm-cancel').addEventListener('click', closeModal);
|
||||||
|
|
||||||
@@ -463,6 +616,7 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
const title = panel.querySelector('#bm-title').value.trim();
|
const title = panel.querySelector('#bm-title').value.trim();
|
||||||
const absVal = parseFloat(panel.querySelector('#bm-amount').value);
|
const absVal = parseFloat(panel.querySelector('#bm-amount').value);
|
||||||
const category = panel.querySelector('#bm-category').value;
|
const category = panel.querySelector('#bm-category').value;
|
||||||
|
const subcategory = currentType === 'expense' ? panel.querySelector('#bm-subcategory').value : '';
|
||||||
const date = panel.querySelector('#bm-date').value;
|
const date = panel.querySelector('#bm-date').value;
|
||||||
const recurring = panel.querySelector('#bm-recurring').checked ? 1 : 0;
|
const recurring = panel.querySelector('#bm-recurring').checked ? 1 : 0;
|
||||||
|
|
||||||
@@ -476,7 +630,7 @@ function openBudgetModal({ mode, entry = null }) {
|
|||||||
saveBtn.textContent = '…';
|
saveBtn.textContent = '…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = { title, amount, category, date, is_recurring: recurring };
|
const body = { title, amount, category, subcategory, date, is_recurring: recurring };
|
||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
const res = await api.post('/budget', body);
|
const res = await api.post('/budget', body);
|
||||||
state.entries.unshift(res.data);
|
state.entries.unshift(res.data);
|
||||||
@@ -523,4 +677,3 @@ async function deleteEntry(id) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Hilfsfunktion
|
// Hilfsfunktion
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -331,3 +331,20 @@
|
|||||||
color: var(--color-text-on-accent);
|
color: var(--color-text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.budget-field-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-field-header .form-label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-inline-add {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -13,9 +13,9 @@
|
|||||||
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
|
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHELL_CACHE = 'oikos-shell-v50';
|
const SHELL_CACHE = 'oikos-shell-v52';
|
||||||
const PAGES_CACHE = 'oikos-pages-v45';
|
const PAGES_CACHE = 'oikos-pages-v47';
|
||||||
const ASSETS_CACHE = 'oikos-assets-v45';
|
const ASSETS_CACHE = 'oikos-assets-v47';
|
||||||
const BYPASS_CACHE = 'oikos-bypass-flag';
|
const BYPASS_CACHE = 'oikos-bypass-flag';
|
||||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const MIGRATIONS_SQL = {
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
amount REAL NOT NULL,
|
amount REAL NOT NULL,
|
||||||
category TEXT NOT NULL DEFAULT 'Sonstiges',
|
category TEXT NOT NULL DEFAULT 'Sonstiges',
|
||||||
|
subcategory TEXT NOT NULL DEFAULT '',
|
||||||
date TEXT NOT NULL,
|
date TEXT NOT NULL,
|
||||||
is_recurring INTEGER NOT NULL DEFAULT 0,
|
is_recurring INTEGER NOT NULL DEFAULT 0,
|
||||||
recurrence_rule TEXT,
|
recurrence_rule TEXT,
|
||||||
@@ -128,6 +129,21 @@ const MIGRATIONS_SQL = {
|
|||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS budget_categories (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('expense', 'income')),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS budget_subcategories (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
category_key TEXT NOT NULL REFERENCES budget_categories(key) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
|
UNIQUE(category_key, name)
|
||||||
|
);
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_users_updated_at
|
CREATE TRIGGER IF NOT EXISTS trg_users_updated_at
|
||||||
AFTER UPDATE ON users FOR EACH ROW
|
AFTER UPDATE ON users FOR EACH ROW
|
||||||
BEGIN UPDATE users SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END;
|
BEGIN UPDATE users SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END;
|
||||||
|
|||||||
+127
@@ -544,6 +544,133 @@ const MIGRATIONS = [
|
|||||||
CREATE INDEX IF NOT EXISTS idx_cal_events_ref ON calendar_events(calendar_ref_id);
|
CREATE INDEX IF NOT EXISTS idx_cal_events_ref ON calendar_events(calendar_ref_id);
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: 15,
|
||||||
|
description: 'Budget-Ausgabenkategorien als stabile Schlüssel mit Unterkategorien',
|
||||||
|
up: `
|
||||||
|
ALTER TABLE budget_entries ADD COLUMN subcategory TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
UPDATE budget_entries
|
||||||
|
SET category = CASE category
|
||||||
|
WHEN 'Lebensmittel' THEN 'food'
|
||||||
|
WHEN 'Miete' THEN 'housing'
|
||||||
|
WHEN 'Versicherung' THEN 'financial_other'
|
||||||
|
WHEN 'Mobilität' THEN 'transport'
|
||||||
|
WHEN 'Freizeit' THEN 'leisure'
|
||||||
|
WHEN 'Kleidung' THEN 'shopping_clothing'
|
||||||
|
WHEN 'Gesundheit' THEN 'personal_health'
|
||||||
|
WHEN 'Bildung' THEN 'education'
|
||||||
|
WHEN 'Sonstiges' THEN 'financial_other'
|
||||||
|
ELSE category
|
||||||
|
END
|
||||||
|
WHERE amount < 0;
|
||||||
|
|
||||||
|
UPDATE budget_entries
|
||||||
|
SET subcategory = CASE category
|
||||||
|
WHEN 'housing' THEN 'rent_mortgage'
|
||||||
|
WHEN 'food' THEN 'groceries'
|
||||||
|
WHEN 'transport' THEN 'fuel'
|
||||||
|
WHEN 'personal_health' THEN 'pharmacy'
|
||||||
|
WHEN 'leisure' THEN 'events'
|
||||||
|
WHEN 'shopping_clothing' THEN 'clothes_shoes'
|
||||||
|
WHEN 'education' THEN 'courses_college'
|
||||||
|
WHEN 'financial_other' THEN 'insurance_other'
|
||||||
|
ELSE ''
|
||||||
|
END
|
||||||
|
WHERE amount < 0 AND subcategory = '';
|
||||||
|
|
||||||
|
UPDATE budget_entries
|
||||||
|
SET category = 'Sonstiges Einkommen'
|
||||||
|
WHERE amount > 0 AND category = 'Sonstiges';
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 16,
|
||||||
|
description: 'Budget-Kategorien und Unterkategorien in eigene Tabellen auslagern',
|
||||||
|
up: `
|
||||||
|
CREATE TABLE IF NOT EXISTS budget_categories (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('expense', 'income')),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS budget_subcategories (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
category_key TEXT NOT NULL REFERENCES budget_categories(key) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
|
UNIQUE(category_key, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO budget_categories (key, name, type, sort_order) VALUES
|
||||||
|
('housing', 'Habitação / Casa', 'expense', 0),
|
||||||
|
('food', 'Alimentação', 'expense', 1),
|
||||||
|
('transport', 'Transporte', 'expense', 2),
|
||||||
|
('personal_health', 'Cuidados Pessoais / Saúde', 'expense', 3),
|
||||||
|
('leisure', 'Lazer e Entretenimento', 'expense', 4),
|
||||||
|
('shopping_clothing', 'Compras e Vestuário', 'expense', 5),
|
||||||
|
('education', 'Educação', 'expense', 6),
|
||||||
|
('financial_other', 'Serviços Financeiros e Outros', 'expense', 7),
|
||||||
|
('Erwerbseinkommen', 'Erwerbseinkommen', 'income', 0),
|
||||||
|
('Kapitalerträge', 'Kapitalerträge', 'income', 1),
|
||||||
|
('Geschenke & Transfers', 'Geschenke & Transfers', 'income', 2),
|
||||||
|
('Sozialleistungen', 'Sozialleistungen', 'income', 3),
|
||||||
|
('Sonstiges Einkommen', 'Sonstiges Einkommen', 'income', 4);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO budget_subcategories (key, category_key, name, sort_order) VALUES
|
||||||
|
('rent_mortgage', 'housing', 'Aluguel / Prestação', 0),
|
||||||
|
('condominium', 'housing', 'Condomínio', 1),
|
||||||
|
('utilities', 'housing', 'Luz / Água / Gás', 2),
|
||||||
|
('internet_tv_phone', 'housing', 'Internet / TV / Telefone', 3),
|
||||||
|
('renovation_maintenance', 'housing', 'Reforma / Manutenção', 4),
|
||||||
|
('cleaning', 'housing', 'Limpeza', 5),
|
||||||
|
('groceries', 'food', 'Supermercado', 0),
|
||||||
|
('restaurants_bars', 'food', 'Restaurante / Bares', 1),
|
||||||
|
('snacks_fast_food', 'food', 'Lanches / Fast Food', 2),
|
||||||
|
('bakery', 'food', 'Padaria', 3),
|
||||||
|
('fuel', 'transport', 'Combustível', 0),
|
||||||
|
('parking_tolls', 'transport', 'Estacionamento / Pedágio', 1),
|
||||||
|
('public_transport', 'transport', 'Transporte Público', 2),
|
||||||
|
('apps_taxi', 'transport', 'Aplicativos / Táxi', 3),
|
||||||
|
('maintenance_insurance', 'transport', 'Manutenção / Seguro', 4),
|
||||||
|
('pharmacy', 'personal_health', 'Farmácia', 0),
|
||||||
|
('health_insurance', 'personal_health', 'Plano de Saúde', 1),
|
||||||
|
('gym_sports', 'personal_health', 'Academia / Esportes', 2),
|
||||||
|
('beauty_cosmetics', 'personal_health', 'Beleza / Cosméticos', 3),
|
||||||
|
('travel', 'leisure', 'Viagens', 0),
|
||||||
|
('streaming', 'leisure', 'Streaming', 1),
|
||||||
|
('events', 'leisure', 'Eventos', 2),
|
||||||
|
('hobbies', 'leisure', 'Hobbies', 3),
|
||||||
|
('clothes_shoes', 'shopping_clothing', 'Roupas / Calçados', 0),
|
||||||
|
('electronics', 'shopping_clothing', 'Eletrônicos', 1),
|
||||||
|
('gifts', 'shopping_clothing', 'Presentes', 2),
|
||||||
|
('courses_college', 'education', 'Cursos / Faculdade', 0),
|
||||||
|
('school_supplies', 'education', 'Material Escolar', 1),
|
||||||
|
('languages', 'education', 'Idiomas', 2),
|
||||||
|
('loans_interest', 'financial_other', 'Empréstimos / Juros', 0),
|
||||||
|
('bank_fees', 'financial_other', 'Tarifas Bancárias', 1),
|
||||||
|
('insurance_other', 'financial_other', 'Seguros', 2),
|
||||||
|
('investments', 'financial_other', 'Investimentos', 3),
|
||||||
|
('taxes', 'financial_other', 'Impostos', 4);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO budget_categories (key, name, type, sort_order)
|
||||||
|
SELECT category, category, CASE WHEN amount < 0 THEN 'expense' ELSE 'income' END, 1000
|
||||||
|
FROM budget_entries
|
||||||
|
WHERE category NOT IN (SELECT key FROM budget_categories)
|
||||||
|
GROUP BY category;
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO budget_subcategories (key, category_key, name, sort_order)
|
||||||
|
SELECT subcategory, category, subcategory, 1000
|
||||||
|
FROM budget_entries
|
||||||
|
WHERE subcategory != ''
|
||||||
|
AND subcategory NOT IN (SELECT key FROM budget_subcategories)
|
||||||
|
AND category IN (SELECT key FROM budget_categories WHERE type = 'expense')
|
||||||
|
GROUP BY category, subcategory;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+165
-23
@@ -7,7 +7,7 @@
|
|||||||
import { createLogger } from '../logger.js';
|
import { createLogger } from '../logger.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import * as db from '../db.js';
|
import * as db from '../db.js';
|
||||||
import { str, oneOf, date as validateDate, num, rrule, collectErrors, MAX_TITLE, MONTH_RE } from '../middleware/validate.js';
|
import { str, oneOf, date as validateDate, num, rrule, collectErrors, MAX_TITLE, MAX_SHORT, MONTH_RE } from '../middleware/validate.js';
|
||||||
|
|
||||||
const log = createLogger('Budget');
|
const log = createLogger('Budget');
|
||||||
|
|
||||||
@@ -57,23 +57,87 @@ function generateRecurringInstances(database, month) {
|
|||||||
|
|
||||||
database.prepare(`
|
database.prepare(`
|
||||||
INSERT INTO budget_entries
|
INSERT INTO budget_entries
|
||||||
(title, amount, category, date, is_recurring, recurrence_parent_id, created_by)
|
(title, amount, category, subcategory, date, is_recurring, recurrence_parent_id, created_by)
|
||||||
VALUES (?, ?, ?, ?, 0, ?, ?)
|
VALUES (?, ?, ?, ?, ?, 0, ?, ?)
|
||||||
`).run(orig.title, orig.amount, orig.category, instanceDate, orig.id, orig.created_by);
|
`).run(orig.title, orig.amount, orig.category, orig.subcategory || '', instanceDate, orig.id, orig.created_by);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXPENSE_CATEGORIES = [
|
function slugify(value) {
|
||||||
'Lebensmittel', 'Miete', 'Versicherung', 'Mobilität',
|
return String(value || '')
|
||||||
'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges',
|
.normalize('NFD')
|
||||||
];
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.slice(0, 48) || 'category';
|
||||||
|
}
|
||||||
|
|
||||||
const INCOME_CATEGORIES = [
|
function uniqueKey(table, base) {
|
||||||
'Erwerbseinkommen', 'Kapitalerträge', 'Geschenke & Transfers',
|
const normalized = slugify(base);
|
||||||
'Sozialleistungen', 'Sonstiges Einkommen',
|
let key = normalized;
|
||||||
];
|
let i = 2;
|
||||||
|
const exists = db.get().prepare(`SELECT 1 FROM ${table} WHERE key = ?`);
|
||||||
|
while (exists.get(key)) {
|
||||||
|
key = `${normalized}_${i}`;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
const VALID_CATEGORIES = [...EXPENSE_CATEGORIES, ...INCOME_CATEGORIES];
|
function loadBudgetMeta() {
|
||||||
|
const categories = db.get().prepare(`
|
||||||
|
SELECT key, name, type, sort_order
|
||||||
|
FROM budget_categories
|
||||||
|
ORDER BY type DESC, sort_order ASC, name COLLATE NOCASE ASC
|
||||||
|
`).all();
|
||||||
|
const subcategories = db.get().prepare(`
|
||||||
|
SELECT key, category_key, name, sort_order
|
||||||
|
FROM budget_subcategories
|
||||||
|
ORDER BY sort_order ASC, name COLLATE NOCASE ASC
|
||||||
|
`).all();
|
||||||
|
|
||||||
|
const expenseCategories = categories.filter((c) => c.type === 'expense');
|
||||||
|
const incomeCategories = categories.filter((c) => c.type === 'income');
|
||||||
|
const expenseSubcategories = {};
|
||||||
|
for (const sub of subcategories) {
|
||||||
|
if (!expenseSubcategories[sub.category_key]) expenseSubcategories[sub.category_key] = [];
|
||||||
|
expenseSubcategories[sub.category_key].push(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { categories, expenseCategories, incomeCategories, expenseSubcategories };
|
||||||
|
}
|
||||||
|
|
||||||
|
function validCategoryKeys() {
|
||||||
|
return db.get().prepare('SELECT key FROM budget_categories').all().map((c) => c.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validExpenseCategoryKeys() {
|
||||||
|
return db.get().prepare("SELECT key FROM budget_categories WHERE type = 'expense'").all().map((c) => c.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultCategory(type) {
|
||||||
|
const row = db.get().prepare(`
|
||||||
|
SELECT key FROM budget_categories WHERE type = ? ORDER BY sort_order ASC, name COLLATE NOCASE ASC LIMIT 1
|
||||||
|
`).get(type);
|
||||||
|
return row?.key || (type === 'expense' ? 'financial_other' : 'Sonstiges Einkommen');
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultSubcategory(category) {
|
||||||
|
const row = db.get().prepare(`
|
||||||
|
SELECT key FROM budget_subcategories WHERE category_key = ? ORDER BY sort_order ASC, name COLLATE NOCASE ASC LIMIT 1
|
||||||
|
`).get(category);
|
||||||
|
return row?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSubcategory(category, subcategory) {
|
||||||
|
if (!validExpenseCategoryKeys().includes(category)) return '';
|
||||||
|
if (!subcategory) return defaultSubcategory(category);
|
||||||
|
const row = db.get().prepare(`
|
||||||
|
SELECT 1 FROM budget_subcategories WHERE category_key = ? AND key = ?
|
||||||
|
`).get(category, subcategory);
|
||||||
|
return row ? subcategory : null;
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Statische Routen vor /:id
|
// Statische Routen vor /:id
|
||||||
@@ -155,7 +219,7 @@ router.get('/export', (req, res) => {
|
|||||||
ORDER BY b.date ASC
|
ORDER BY b.date ASC
|
||||||
`).all(from, to);
|
`).all(from, to);
|
||||||
|
|
||||||
const header = 'Datum,Titel,Betrag,Kategorie,Wiederkehrend,Erstellt von\n';
|
const header = 'Datum,Titel,Betrag,Kategorie,Unterkategorie,Wiederkehrend,Erstellt von\n';
|
||||||
const csvSafe = (val) => {
|
const csvSafe = (val) => {
|
||||||
let s = String(val || '').replace(/"/g, '""');
|
let s = String(val || '').replace(/"/g, '""');
|
||||||
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
|
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
|
||||||
@@ -167,6 +231,7 @@ router.get('/export', (req, res) => {
|
|||||||
csvSafe(e.title),
|
csvSafe(e.title),
|
||||||
e.amount.toFixed(2).replace('.', ','),
|
e.amount.toFixed(2).replace('.', ','),
|
||||||
e.category,
|
e.category,
|
||||||
|
e.subcategory || '',
|
||||||
e.is_recurring ? 'Ja' : 'Nein',
|
e.is_recurring ? 'Ja' : 'Nein',
|
||||||
csvSafe(e.creator_name),
|
csvSafe(e.creator_name),
|
||||||
].join(',')
|
].join(',')
|
||||||
@@ -187,7 +252,70 @@ router.get('/export', (req, res) => {
|
|||||||
* Response: { data: { categories } }
|
* Response: { data: { categories } }
|
||||||
*/
|
*/
|
||||||
router.get('/meta', (req, res) => {
|
router.get('/meta', (req, res) => {
|
||||||
res.json({ data: { categories: VALID_CATEGORIES } });
|
res.json({ data: loadBudgetMeta() });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/categories', (req, res) => {
|
||||||
|
try {
|
||||||
|
const vName = str(req.body.name, 'Name', { max: MAX_SHORT });
|
||||||
|
const vType = oneOf(req.body.type || 'expense', ['expense', 'income'], 'Typ');
|
||||||
|
const errors = collectErrors([vName, vType]);
|
||||||
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||||
|
|
||||||
|
const conflict = db.get().prepare(`
|
||||||
|
SELECT key FROM budget_categories WHERE type = ? AND name = ? COLLATE NOCASE
|
||||||
|
`).get(vType.value, vName.value);
|
||||||
|
if (conflict) return res.status(409).json({ error: 'Kategorie existiert bereits.', code: 409 });
|
||||||
|
|
||||||
|
const maxOrder = db.get().prepare(`
|
||||||
|
SELECT COALESCE(MAX(sort_order), -1) AS m FROM budget_categories WHERE type = ?
|
||||||
|
`).get(vType.value).m;
|
||||||
|
const key = uniqueKey('budget_categories', vName.value);
|
||||||
|
|
||||||
|
db.get().prepare(`
|
||||||
|
INSERT INTO budget_categories (key, name, type, sort_order) VALUES (?, ?, ?, ?)
|
||||||
|
`).run(key, vName.value, vType.value, maxOrder + 1);
|
||||||
|
|
||||||
|
const cat = db.get().prepare('SELECT key, name, type, sort_order FROM budget_categories WHERE key = ?').get(key);
|
||||||
|
res.status(201).json({ data: cat });
|
||||||
|
} catch (err) {
|
||||||
|
log.error('POST /categories Fehler:', err);
|
||||||
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/categories/:categoryKey/subcategories', (req, res) => {
|
||||||
|
try {
|
||||||
|
const cat = db.get().prepare(`
|
||||||
|
SELECT * FROM budget_categories WHERE key = ? AND type = 'expense'
|
||||||
|
`).get(req.params.categoryKey);
|
||||||
|
if (!cat) return res.status(404).json({ error: 'Kategorie nicht gefunden.', code: 404 });
|
||||||
|
|
||||||
|
const vName = str(req.body.name, 'Name', { max: MAX_SHORT });
|
||||||
|
if (vName.error) return res.status(400).json({ error: vName.error, code: 400 });
|
||||||
|
|
||||||
|
const conflict = db.get().prepare(`
|
||||||
|
SELECT key FROM budget_subcategories WHERE category_key = ? AND name = ? COLLATE NOCASE
|
||||||
|
`).get(cat.key, vName.value);
|
||||||
|
if (conflict) return res.status(409).json({ error: 'Unterkategorie existiert bereits.', code: 409 });
|
||||||
|
|
||||||
|
const maxOrder = db.get().prepare(`
|
||||||
|
SELECT COALESCE(MAX(sort_order), -1) AS m FROM budget_subcategories WHERE category_key = ?
|
||||||
|
`).get(cat.key).m;
|
||||||
|
const key = uniqueKey('budget_subcategories', `${cat.key}_${vName.value}`);
|
||||||
|
|
||||||
|
db.get().prepare(`
|
||||||
|
INSERT INTO budget_subcategories (key, category_key, name, sort_order) VALUES (?, ?, ?, ?)
|
||||||
|
`).run(key, cat.key, vName.value, maxOrder + 1);
|
||||||
|
|
||||||
|
const sub = db.get().prepare(`
|
||||||
|
SELECT key, category_key, name, sort_order FROM budget_subcategories WHERE key = ?
|
||||||
|
`).get(key);
|
||||||
|
res.status(201).json({ data: sub });
|
||||||
|
} catch (err) {
|
||||||
|
log.error('POST /categories/:categoryKey/subcategories Fehler:', err);
|
||||||
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -220,7 +348,7 @@ router.get('/', (req, res) => {
|
|||||||
`;
|
`;
|
||||||
const params = [from, to];
|
const params = [from, to];
|
||||||
|
|
||||||
if (req.query.category && VALID_CATEGORIES.includes(req.query.category)) {
|
if (req.query.category && validCategoryKeys().includes(req.query.category)) {
|
||||||
sql += ' AND b.category = ?';
|
sql += ' AND b.category = ?';
|
||||||
params.push(req.query.category);
|
params.push(req.query.category);
|
||||||
}
|
}
|
||||||
@@ -238,24 +366,29 @@ router.get('/', (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* POST /api/v1/budget
|
* POST /api/v1/budget
|
||||||
* Neuen Eintrag anlegen.
|
* Neuen Eintrag anlegen.
|
||||||
* Body: { title, amount, category?, date, is_recurring?, recurrence_rule? }
|
* Body: { title, amount, category?, subcategory?, date, is_recurring?, recurrence_rule? }
|
||||||
* Response: { data: Entry }
|
* Response: { data: Entry }
|
||||||
*/
|
*/
|
||||||
router.post('/', (req, res) => {
|
router.post('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
|
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
|
||||||
const vAmount = num(req.body.amount, 'Betrag', { required: true });
|
const vAmount = num(req.body.amount, 'Betrag', { required: true });
|
||||||
const vCat = oneOf(req.body.category || 'Sonstiges', VALID_CATEGORIES, 'Kategorie');
|
const fallbackCategory = defaultCategory(Number(req.body.amount) < 0 ? 'expense' : 'income');
|
||||||
|
const vCat = oneOf(req.body.category || fallbackCategory, validCategoryKeys(), 'Kategorie');
|
||||||
const vDate = validateDate(req.body.date, 'Datum', true);
|
const vDate = validateDate(req.body.date, 'Datum', true);
|
||||||
const vRrule = rrule(req.body.recurrence_rule, 'Wiederholung');
|
const vRrule = rrule(req.body.recurrence_rule, 'Wiederholung');
|
||||||
const errors = collectErrors([vTitle, vAmount, vCat, vDate, vRrule]);
|
const errors = collectErrors([vTitle, vAmount, vCat, vDate, vRrule]);
|
||||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||||
|
const subcategory = validateSubcategory(vCat.value, req.body.subcategory);
|
||||||
|
if (subcategory === null) {
|
||||||
|
return res.status(400).json({ error: 'Ungültige Unterkategorie.', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const result = db.get().prepare(`
|
const result = db.get().prepare(`
|
||||||
INSERT INTO budget_entries (title, amount, category, date, is_recurring, recurrence_rule, created_by)
|
INSERT INTO budget_entries (title, amount, category, subcategory, date, is_recurring, recurrence_rule, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
vTitle.value, vAmount.value, vCat.value || 'Sonstiges', vDate.value,
|
vTitle.value, vAmount.value, vCat.value || fallbackCategory, subcategory, vDate.value,
|
||||||
req.body.is_recurring ? 1 : 0, vRrule.value,
|
req.body.is_recurring ? 1 : 0, vRrule.value,
|
||||||
req.session.userId
|
req.session.userId
|
||||||
);
|
);
|
||||||
@@ -288,18 +421,26 @@ router.put('/:id', (req, res) => {
|
|||||||
const checks = [];
|
const checks = [];
|
||||||
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
|
if (req.body.title !== undefined) checks.push(str(req.body.title, 'Titel', { max: MAX_TITLE, required: false }));
|
||||||
if (req.body.amount !== undefined) checks.push(num(req.body.amount, 'Betrag'));
|
if (req.body.amount !== undefined) checks.push(num(req.body.amount, 'Betrag'));
|
||||||
if (req.body.category !== undefined) checks.push(oneOf(req.body.category, VALID_CATEGORIES, 'Kategorie'));
|
if (req.body.category !== undefined) checks.push(oneOf(req.body.category, validCategoryKeys(), 'Kategorie'));
|
||||||
if (req.body.date !== undefined) checks.push(validateDate(req.body.date, 'Datum'));
|
if (req.body.date !== undefined) checks.push(validateDate(req.body.date, 'Datum'));
|
||||||
if (req.body.recurrence_rule !== undefined) checks.push(rrule(req.body.recurrence_rule, 'Wiederholung'));
|
if (req.body.recurrence_rule !== undefined) checks.push(rrule(req.body.recurrence_rule, 'Wiederholung'));
|
||||||
const errors = collectErrors(checks);
|
const errors = collectErrors(checks);
|
||||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||||
const { title, amount, category, date, is_recurring, recurrence_rule } = req.body;
|
const { title, amount, category, subcategory: requestedSubcategory, date, is_recurring, recurrence_rule } = req.body;
|
||||||
|
const nextCategory = category ?? entry.category;
|
||||||
|
const subcategory = requestedSubcategory !== undefined || category !== undefined
|
||||||
|
? validateSubcategory(nextCategory, requestedSubcategory ?? entry.subcategory)
|
||||||
|
: undefined;
|
||||||
|
if (subcategory === null) {
|
||||||
|
return res.status(400).json({ error: 'Ungültige Unterkategorie.', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
UPDATE budget_entries
|
UPDATE budget_entries
|
||||||
SET title = COALESCE(?, title),
|
SET title = COALESCE(?, title),
|
||||||
amount = COALESCE(?, amount),
|
amount = COALESCE(?, amount),
|
||||||
category = COALESCE(?, category),
|
category = COALESCE(?, category),
|
||||||
|
subcategory = COALESCE(?, subcategory),
|
||||||
date = COALESCE(?, date),
|
date = COALESCE(?, date),
|
||||||
is_recurring = COALESCE(?, is_recurring),
|
is_recurring = COALESCE(?, is_recurring),
|
||||||
recurrence_rule = ?
|
recurrence_rule = ?
|
||||||
@@ -308,6 +449,7 @@ router.put('/:id', (req, res) => {
|
|||||||
title?.trim() ?? null,
|
title?.trim() ?? null,
|
||||||
amount !== undefined ? Number(amount) : null,
|
amount !== undefined ? Number(amount) : null,
|
||||||
category ?? null,
|
category ?? null,
|
||||||
|
subcategory !== undefined ? subcategory : null,
|
||||||
date ?? null,
|
date ?? null,
|
||||||
is_recurring !== undefined ? (is_recurring ? 1 : 0) : null,
|
is_recurring !== undefined ? (is_recurring ? 1 : 0) : null,
|
||||||
recurrence_rule !== undefined ? (recurrence_rule || null) : entry.recurrence_rule,
|
recurrence_rule !== undefined ? (recurrence_rule || null) : entry.recurrence_rule,
|
||||||
|
|||||||
@@ -178,30 +178,30 @@ console.log('\n[Budget-Test] Einnahmen, Ausgaben, Saldo, Aggregation, CSV-Vorber
|
|||||||
|
|
||||||
let bId1, bId2, bId3, bId4;
|
let bId1, bId2, bId3, bId4;
|
||||||
|
|
||||||
test('Ausgabe eintragen (Lebensmittel)', () => {
|
test('Ausgabe eintragen (Supermarkt)', () => {
|
||||||
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by)
|
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, subcategory, date, created_by)
|
||||||
VALUES ('REWE', -85.40, 'Lebensmittel', '2026-03-10', ?)`).run(uid);
|
VALUES ('REWE', -85.40, 'food', 'groceries', '2026-03-10', ?)`).run(uid);
|
||||||
bId1 = r.lastInsertRowid;
|
bId1 = r.lastInsertRowid;
|
||||||
assert(bId1 > 0);
|
assert(bId1 > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Einnahme eintragen (Gehalt)', () => {
|
test('Einnahme eintragen (Gehalt)', () => {
|
||||||
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by)
|
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by)
|
||||||
VALUES ('Gehalt März', 2800.00, 'Sonstiges', '2026-03-01', ?)`).run(uid);
|
VALUES ('Gehalt März', 2800.00, 'Sonstiges Einkommen', '2026-03-01', ?)`).run(uid);
|
||||||
bId2 = r.lastInsertRowid;
|
bId2 = r.lastInsertRowid;
|
||||||
assert(bId2 > 0);
|
assert(bId2 > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Ausgabe (Miete)', () => {
|
test('Ausgabe (Aluguel / Prestação)', () => {
|
||||||
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, is_recurring, created_by)
|
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, subcategory, date, is_recurring, created_by)
|
||||||
VALUES ('Miete', -950.00, 'Miete', '2026-03-01', 1, ?)`).run(uid);
|
VALUES ('Miete', -950.00, 'housing', 'rent_mortgage', '2026-03-01', 1, ?)`).run(uid);
|
||||||
bId3 = r.lastInsertRowid;
|
bId3 = r.lastInsertRowid;
|
||||||
assert(bId3 > 0);
|
assert(bId3 > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Ausgabe im anderen Monat (April)', () => {
|
test('Ausgabe im anderen Monat (April)', () => {
|
||||||
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by)
|
const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, subcategory, date, created_by)
|
||||||
VALUES ('Strom April', -55.00, 'Sonstiges', '2026-04-15', ?)`).run(uid);
|
VALUES ('Strom April', -55.00, 'housing', 'utilities', '2026-04-15', ?)`).run(uid);
|
||||||
bId4 = r.lastInsertRowid;
|
bId4 = r.lastInsertRowid;
|
||||||
assert(bId4 > 0);
|
assert(bId4 > 0);
|
||||||
});
|
});
|
||||||
@@ -258,12 +258,18 @@ test('Aggregation nach Kategorie', () => {
|
|||||||
GROUP BY category ORDER BY ABS(SUM(amount)) DESC
|
GROUP BY category ORDER BY ABS(SUM(amount)) DESC
|
||||||
`).all();
|
`).all();
|
||||||
assert(cats.length >= 2, `Mindestens 2 Kategorien, erhalten ${cats.length}`);
|
assert(cats.length >= 2, `Mindestens 2 Kategorien, erhalten ${cats.length}`);
|
||||||
// Miete sollte die größte Ausgabe sein
|
// Housing should be the largest expense category.
|
||||||
const miete = cats.find((c) => c.category === 'Miete');
|
const miete = cats.find((c) => c.category === 'housing');
|
||||||
assert(miete, 'Miete in Kategorien vorhanden');
|
assert(miete, 'Housing in Kategorien vorhanden');
|
||||||
assert(Math.abs(miete.expenses + 950.00) < 0.01, `Miete-Ausgaben: ${miete.expenses}`);
|
assert(Math.abs(miete.expenses + 950.00) < 0.01, `Miete-Ausgaben: ${miete.expenses}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Unterkategorie gespeichert', () => {
|
||||||
|
const r = db.prepare('SELECT category, subcategory FROM budget_entries WHERE id = ?').get(bId1);
|
||||||
|
assert(r.category === 'food', `Kategorie: ${r.category}`);
|
||||||
|
assert(r.subcategory === 'groceries', `Unterkategorie: ${r.subcategory}`);
|
||||||
|
});
|
||||||
|
|
||||||
test('Wiederkehrend-Flag korrekt', () => {
|
test('Wiederkehrend-Flag korrekt', () => {
|
||||||
const r = db.prepare('SELECT is_recurring FROM budget_entries WHERE id = ?').get(bId3);
|
const r = db.prepare('SELECT is_recurring FROM budget_entries WHERE id = ?').get(bId3);
|
||||||
assert(r.is_recurring === 1, 'Miete ist wiederkehrend');
|
assert(r.is_recurring === 1, 'Miete ist wiederkehrend');
|
||||||
|
|||||||
Reference in New Issue
Block a user