diff --git a/src/ExpensesCalculator.UI/public/i18n/en.json b/src/ExpensesCalculator.UI/public/i18n/en.json
index f6edd69..2c420ab 100644
--- a/src/ExpensesCalculator.UI/public/i18n/en.json
+++ b/src/ExpensesCalculator.UI/public/i18n/en.json
@@ -1,4 +1,8 @@
{
+ "GENERAL": {
+ "CURRENCY_SYMBOL": "$",
+ "CURRENCY_CODE": "USD"
+ },
"NAV": {
"LOGIN": "Login",
"REGISTER": "Register",
@@ -13,7 +17,7 @@
"GITHUB": "Github"
},
"HOME": {
- "COPYRIGHT": "© 2024-2026 ExpensesCalculator"
+ "COPYRIGHT": "© 2024-2026 ExpensesTracker"
},
"LOGIN": {
"TITLE": "Log in",
@@ -154,9 +158,9 @@
"PAYER_LABEL": "Payer",
"SELECT_PAYER": "Select Payer",
"SUM_LABEL": "Total Sum",
- "ADD_BUTTON": "Add Check",
- "EDIT_BUTTON": "Edit Check",
- "DELETE_BUTTON": "Delete Check"
+ "ADD_BUTTON": "Add",
+ "EDIT_BUTTON": "Edit",
+ "DELETE_BUTTON": "Delete"
},
"TOAST": {
"SUCCESS": "Success!",
@@ -189,6 +193,7 @@
"PERSON": "person",
"PEOPLE": "people",
"NO_TAGS": "No tags",
+ "VIEW_IN_EXPENSES": "View in Expenses",
"SHOWING": "Showing items from",
"TO": "to",
"OF": "of",
@@ -203,7 +208,8 @@
"TOTAL_SUM": "Total Sum",
"USER_COUNT": "User Count",
"RATING": "Rating",
- "TAGS": "Tags"
+ "TAGS": "Tags",
+ "APPLY": "Apply"
},
"SORT": {
"CAPTION": "Sort",
@@ -254,8 +260,8 @@
},
"VALIDATION": {
"NAME_REQUIRED": "Item name is required.",
- "PRICE_INVALID": "Price > 0",
- "AMOUNT_INVALID": "Amount > 0",
+ "PRICE_INVALID": "Price > 0 & <= 10000",
+ "AMOUNT_INVALID": "Amount > 0 & <= 1000",
"RATING_REQUIRED": "Rating is required.",
"USERS_REQUIRED": "At least one user is required."
},
@@ -287,8 +293,15 @@
"EXPENSES_TABLE_CONTENT": "This table shows the expenses. Click on any column header to sort your expenses by that field. The table below displays all expense rows, which you can double-click to view details.",
"ACTIONS_MENU_TITLE": "Actions Menu",
"ACTIONS_MENU_CONTENT": "Click the three dots button to access actions for each expense. You can open details, calculate expenses, edit, share with other users, or delete the expense entry.",
+ "SORT_CONTROLS_TITLE": "Sort Controls",
+ "SORT_CONTROLS_CONTENT": "Use these buttons to sort your expenses. Click any button to sort by that field (Date, Location, Participants, or Total Sum). Click again to toggle between ascending and descending order.",
+ "EXPENSES_ACCORDION_TITLE": "Expenses List",
+ "EXPENSES_ACCORDION_CONTENT": "This accordion shows all your expenses. Each row displays the date and location. Click the arrow to expand and view participants, total sum, and action buttons to manage the expense.",
"PAGINATION_TITLE": "Pagination Controls",
- "PAGINATION_CONTENT": "Navigate through your expenses with these controls. Change the number of items displayed per page using the dropdown on the right."
+ "PAGINATION_CONTENT": "Navigate through your expenses with these controls. Change the number of items displayed per page using the dropdown on the right.",
+ "PREV_BTN": "Previous",
+ "NEXT_BTN": "Next",
+ "END_BTN": "End tour"
},
"TOUR_DETAILS": {
"BACK_BTN_TITLE": "Back to List",
@@ -300,7 +313,15 @@
"ADD_ITEM_TITLE": "Add Item",
"ADD_ITEM_CONTENT": "Click this button to add items to the check. Specify the item name, price, quantity, and select which participants consumed it. The check total will update automatically.",
"CALCULATOR_TITLE": "Calculate Expenses",
- "CALCULATOR_CONTENT": "Once you've added all checks and items, click this button to view the calculations showing how expenses are split among participants."
+ "CALCULATOR_CONTENT": "Once you've added all checks and items, click this button to view the calculations showing how expenses are split among participants.",
+ "FILTER_SORT_CONTROLS_TITLE": "Filter & Sort",
+ "FILTER_SORT_CONTROLS_CONTENT": "Use these controls to filter and sort your checks. The filter button lets you search by location, payer, or sum, while the sort dropdown helps organize checks by different criteria.",
+ "CHECKS_ACCORDION_TITLE": "Checks List",
+ "CHECKS_ACCORDION_CONTENT": "This shows all your checks in an accordion format optimized for mobile. Each check displays the location and total sum. Tap any check to expand it and view its items.",
+ "ACCORDION_CHECK_ITEM_TITLE": "Check Details",
+ "ACCORDION_CHECK_ITEM_CONTENT": "Tap the check header to expand or collapse it. When expanded, you can view the payer information, edit or delete the check, and see all items belonging to this check.",
+ "ACTIONS_MENU_TITLE": "Actions Menu",
+ "ACTIONS_MENU_CONTENT": "Click the three-dot menu to access additional actions for this expense entry, including edit, share, and delete options."
},
"TOUR_CALCULATIONS": {
"BACK_BTN_TITLE": "Back to Details",
@@ -317,6 +338,8 @@
"SEARCH_FILTER_CONTENT": "Search your items by different criteria. Use the dropdown to switch between filtering by name, description, price, amount, total sum, user count, or rating. Type in the search box to find specific items.",
"SORT_BAR_TITLE": "Sort Options",
"SORT_BAR_CONTENT": "Sort your items by different attributes. Click the dropdown to choose a column (name, price, amount, total price, user count, or rating) and toggle between ascending and descending order.",
+ "FILTER_SORT_CONTROLS_TITLE": "Filter & Sort",
+ "FILTER_SORT_CONTROLS_CONTENT": "Use the Filter button to search items, apply tag filters, and toggle showing only your items. Use the Sort dropdown to sort by name, price, amount, total price, user count, or rating.",
"ADD_ITEM_TITLE": "Add New Item",
"ADD_ITEM_CONTENT": "Click this button to add a new item to your recommendations. You can add items with name, description, price, amount, rating, and tags. These are your personal items that you can only access from this recommendations view.",
"ONLY_MY_ITEMS_TITLE": "Filter Your Items",
diff --git a/src/ExpensesCalculator.UI/public/i18n/ua.json b/src/ExpensesCalculator.UI/public/i18n/ua.json
index 9fd4254..e22b403 100644
--- a/src/ExpensesCalculator.UI/public/i18n/ua.json
+++ b/src/ExpensesCalculator.UI/public/i18n/ua.json
@@ -1,4 +1,8 @@
{
+ "GENERAL": {
+ "CURRENCY_SYMBOL": "₴",
+ "CURRENCY_CODE": "UAH"
+ },
"NAV": {
"LOGIN": "Увійти",
"REGISTER": "Зареєструватися",
@@ -13,7 +17,7 @@
"GITHUB": "Github"
},
"HOME": {
- "COPYRIGHT": "© 2024-2026 ExpensesCalculator"
+ "COPYRIGHT": "© 2024-2026 ExpensesTracker"
},
"LOGIN": {
"TITLE": "Увійти",
@@ -188,6 +192,7 @@
"PERSON": "користувач",
"PEOPLE": "користувачів",
"NO_TAGS": "Без тегів",
+ "VIEW_IN_EXPENSES": "Переглянути у витратах",
"SHOWING": "Показано товари з",
"TO": "до",
"OF": "з",
@@ -203,7 +208,8 @@
"TOTAL_SUM": "Загальна сума",
"USER_COUNT": "Користувачі",
"RATING": "Рейтинг",
- "TAGS": "Теги"
+ "TAGS": "Теги",
+ "APPLY": "Застосувати"
},
"SORT": {
"CAPTION": "Сортування",
@@ -254,8 +260,8 @@
},
"VALIDATION": {
"NAME_REQUIRED": "Назва товару обов'язкова.",
- "PRICE_INVALID": "Ціна > 0",
- "AMOUNT_INVALID": "Кількість > 0",
+ "PRICE_INVALID": "Ціна > 0 & <= 10000",
+ "AMOUNT_INVALID": "Кількість > 0 & <= 1000",
"RATING_REQUIRED": "Рейтинг обов'язковий.",
"USERS_REQUIRED": "Потрібен хоча б один користувач."
},
@@ -287,8 +293,15 @@
"EXPENSES_TABLE_CONTENT": "Ця таблиця показує витрати. Натисніть на будь-який заголовок стовпця, щоб відсортувати витрати за цим полем. Таблиця нижче відображає всі рядки витрат, які можна двічі клацнути для перегляду деталей.",
"ACTIONS_MENU_TITLE": "Меню дій",
"ACTIONS_MENU_CONTENT": "Натисніть кнопку з трьома крапками, щоб отримати доступ до дій для кожної витрати. Ви можете відкрити деталі, розрахувати витрати, редагувати, поділитися з іншими користувачами або видалити запис витрат.",
+ "SORT_CONTROLS_TITLE": "Елементи керування сортуванням",
+ "SORT_CONTROLS_CONTENT": "Використовуйте ці кнопки для сортування ваших витрат. Натисніть будь-яку кнопку, щоб відсортувати за цим полем (Дата, Місце, Учасники або Загальна сума). Натисніть ще раз, щоб перемикатися між зростанням та спаданням.",
+ "EXPENSES_ACCORDION_TITLE": "Список витрат",
+ "EXPENSES_ACCORDION_CONTENT": "Цей розкривний список показує всі ваші витрати. Кожен рядок відображає дату та місце. Натисніть стрілку, щоб розгорнути та переглянути учасників, загальну суму та кнопки дій для керування витратами.",
"PAGINATION_TITLE": "Сторінки",
- "PAGINATION_CONTENT": "Перемикайтеся між витратами за допомогою цих елементів керування. Змініть кількість елементів на сторінці, використовуючи випадаюче меню справа."
+ "PAGINATION_CONTENT": "Перемикайтеся між витратами за допомогою цих елементів керування. Змініть кількість елементів на сторінці, використовуючи випадаюче меню справа.",
+ "PREV_BTN": "Попередній",
+ "NEXT_BTN": "Далі",
+ "END_BTN": "Завершити"
},
"TOUR_DETAILS": {
"BACK_BTN_TITLE": "Назад до списку",
@@ -300,7 +313,15 @@
"ADD_ITEM_TITLE": "Додати товар",
"ADD_ITEM_CONTENT": "Натисніть цю кнопку, щоб додати товари до чека. Вкажіть назву товару, ціну, кількість та оберіть, які учасники його споживали. Загальна сума чека оновиться автоматично.",
"CALCULATOR_TITLE": "Розрахунки",
- "CALCULATOR_CONTENT": "Після того, як ви додали всі чеки та товари, натисніть цю кнопку, щоб переглянути розрахунки, які показують, як витрати розподіляються між учасниками."
+ "CALCULATOR_CONTENT": "Після того, як ви додали всі чеки та товари, натисніть цю кнопку, щоб переглянути розрахунки, які показують, як витрати розподіляються між учасниками.",
+ "FILTER_SORT_CONTROLS_TITLE": "Фільтр і Сортування",
+ "FILTER_SORT_CONTROLS_CONTENT": "Використовуйте ці елементи керування для фільтрації та сортування ваших чеків. Кнопка фільтра дозволяє шукати за місцем, платником або сумою, а випадаюче меню сортування допомагає організувати чеки за різними критеріями.",
+ "CHECKS_ACCORDION_TITLE": "Список Чеків",
+ "CHECKS_ACCORDION_CONTENT": "Це показує всі ваші чеки в форматі акордеона, оптимізованому для мобільних пристроїв. Кожен чек відображає місце та загальну суму. Торкніться будь-якого чека, щоб розгорнути його та переглянути його елементи.",
+ "ACCORDION_CHECK_ITEM_TITLE": "Деталі Чека",
+ "ACCORDION_CHECK_ITEM_CONTENT": "Торкніться заголовка чека, щоб розгорнути або згорнути його. Коли розгорнуто, ви можете переглянути інформацію про платника, редагувати або видалити чек, а також переглянути всі елементи, що належать до цього чека.",
+ "ACTIONS_MENU_TITLE": "Меню Дій",
+ "ACTIONS_MENU_CONTENT": "Натисніть меню з трьома крапками, щоб отримати доступ до додаткових дій для цього запису витрат, включаючи редагування, спільний доступ та видалення."
},
"TOUR_CALCULATIONS": {
"BACK_BTN_TITLE": "Назад до деталей",
@@ -317,6 +338,8 @@
"SEARCH_FILTER_CONTENT": "Шукайте свої товари за різними критеріями. Використовуйте випадаюче меню, щоб перемикатися між фільтрацією за назвою, описом, ціною, кількістю, загальною сумою, кількістю користувачів або рейтингом. Введіть текст у поле пошуку, щоб знайти конкретні товари.",
"SORT_BAR_TITLE": "Сортування",
"SORT_BAR_CONTENT": "Сортуйте свої товари за різними атрибутами. Натисніть на випадаюче меню, щоб вибрати колонку (назва, ціна, кількість, загальна ціна, кількість користувачів або рейтинг) та перемикайтеся між зростанням та спаданням.",
+ "FILTER_SORT_CONTROLS_TITLE": "Фільтр та сортування",
+ "FILTER_SORT_CONTROLS_CONTENT": "Використовуйте кнопку Фільтр для пошуку товарів, застосування фільтрів за тегами та перемикання показу лише ваших товарів. Використовуйте випадаюче меню Сортування для сортування за назвою, ціною, кількістю, загальною ціною, кількістю користувачів або рейтингом.",
"ADD_ITEM_TITLE": "Додати новий товар",
"ADD_ITEM_CONTENT": "Натисніть цю кнопку, щоб додати новий товар до ваших рекомендацій. Ви можете додавати товари з назвою, описом, ціною, кількістю, рейтингом та тегами. Це ваші персональні товари, до яких ви можете отримати доступ тільки з цього перегляду рекомендацій.",
"ONLY_MY_ITEMS_TITLE": "Тільки ваші товари",
diff --git a/src/ExpensesCalculator.UI/src/app/app.component.html b/src/ExpensesCalculator.UI/src/app/app.component.html
index f2548d7..43ea2cd 100644
--- a/src/ExpensesCalculator.UI/src/app/app.component.html
+++ b/src/ExpensesCalculator.UI/src/app/app.component.html
@@ -5,7 +5,7 @@
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/app.component.ts b/src/ExpensesCalculator.UI/src/app/app.component.ts
index 07a0e70..ca916e3 100644
--- a/src/ExpensesCalculator.UI/src/app/app.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/app.component.ts
@@ -3,6 +3,7 @@ import { isPlatformBrowser } from '@angular/common';
import { VerticalNavbarComponent } from "./shared/vertical-navbar/vertical-navbar.component";
import { HorizontalNavbarComponent } from './shared/horizontal-navbar/horizontal-navbar.component';
import { ToastComponent } from './shared/toast/toast.component';
+import { ModalWindowComponent } from './shared/modal-window/modal-window.component';
import { RouterOutlet } from "@angular/router";
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
@@ -11,7 +12,7 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core';
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.css',
- imports: [VerticalNavbarComponent, HorizontalNavbarComponent, ToastComponent, RouterOutlet, TranslatePipe]
+ imports: [VerticalNavbarComponent, HorizontalNavbarComponent, ToastComponent, ModalWindowComponent, RouterOutlet, TranslatePipe]
})
export class AppComponent {
title = 'Expenses Calculator';
diff --git a/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.css b/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.css
index 758af88..162c928 100644
--- a/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.css
+++ b/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.css
@@ -138,3 +138,127 @@ td {
background-color: #1f429d !important;
color: white;
}
+
+/* Accordion styles for small screens */
+.accordion-button {
+ background-color: transparent !important;
+ box-shadow: none !important;
+ border-radius: 0 !important;
+}
+
+.accordion-button:not(.collapsed) {
+ background-color: transparent !important;
+ color: white !important;
+}
+
+.accordion-button::after {
+ filter: invert(1);
+}
+
+.accordion-button:focus {
+ box-shadow: none !important;
+ border-color: transparent !important;
+}
+
+.accordion-item {
+ border-bottom: 1px solid rgba(13, 110, 253, 0.3) !important;
+}
+
+.accordion-item:last-child {
+ border-bottom: none !important;
+}
+
+.accordion-body {
+ border-top: 1px solid rgba(13, 110, 253, 0.2);
+}
+
+/* Payer dropdown styling */
+.form-group .dropdown .btn.form-control {
+ font-size: 0.95rem;
+ padding: 0.375rem 0.75rem;
+ background-color: white;
+ border-color: #ced4da;
+ color: #212529;
+ text-align: left;
+}
+
+.form-group .dropdown .btn.form-control:hover,
+.form-group .dropdown .btn.form-control:focus,
+.form-group .dropdown .btn.form-control:active {
+ background-color: white;
+ border-color: #ced4da;
+ color: #212529;
+ box-shadow: none;
+}
+
+.form-group .dropdown .btn.form-control.is-invalid {
+ border-color: #dc3545;
+ background-color: white;
+}
+
+.form-group .dropdown .btn.form-control.is-valid {
+ border-color: #198754;
+ background-color: white;
+}
+
+.form-group .dropdown-menu {
+ background-color: white;
+ border-color: #ced4da;
+ max-height: 250px;
+ overflow-y: auto;
+}
+
+.form-group .dropdown-item {
+ color: #212529;
+ font-size: 0.95rem;
+ padding: 0.5rem 0.75rem;
+}
+
+.form-group .dropdown-item:hover,
+.form-group .dropdown-item:focus {
+ background-color: #e9ecef;
+ color: #212529;
+}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ .pagination-info {
+ font-size: 0.8rem !important;
+ }
+
+ /* Reduce payer dropdown size on small screens */
+ .form-group .dropdown .btn.form-control {
+ font-size: 0.85rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ .form-group .dropdown-item {
+ font-size: 0.85rem !important;
+ padding: 0.375rem 0.5rem !important;
+ }
+
+ /* Filter and Sort group - side by side buttons */
+ .filter-sort-group {
+ gap: 0.5rem !important;
+ }
+
+ .filter-sort-group .filter-btn {
+ width: 50% !important;
+ font-size: 0.85rem !important;
+ padding: 0.2rem 0.5rem !important;
+ line-height: 1.2 !important;
+ }
+
+ .filter-sort-group app-sort-bar {
+ width: 50% !important;
+ flex: 0 0 50%;
+ }
+
+ .filter-sort-group app-sort-bar .sort-dropdown {
+ width: 100%;
+ }
+
+ .filter-sort-group app-sort-bar button {
+ width: 100% !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.html b/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.html
index 6f98de5..5d9cb72 100644
--- a/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.html
+++ b/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.html
@@ -2,7 +2,7 @@
-
+
{{ 'CHECKS.TITLE' | translate }}
@@ -19,42 +19,108 @@ {{ 'CHECKS.NO_DATA_HINT' | translate }}
-
-
0" class="d-flex justify-content-end pb-lg-2 px-2 my-3">
-
-
-
-
- {{ 'ITEMS.DATA' | translate }}
-
-
-
-
-
- {{ 'ITEMS.FILTER.CAPTION' | translate }}
-
-
-
-
+
+
+
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}
+
+
+
+
+
+
0" class="d-none d-sm-block pb-2 px-2 my-3">
+
+
+
+
+ {{ 'ITEMS.DATA' | translate }}
+
+
+
+
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}
+
+
+
+
+
+
+
+
+
0" class="d-block d-sm-none mb-2 px-2">
+
+
+
+
+
0" class="d-block d-sm-none mb-2 px-2" tourAnchor="filter-sort-controls">
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }} & {{ 'ITEMS.SORT.CAPTION' | translate }}
+
+
+
+
+
+
+
+
0 && filteredChecksList.length === 0" class="text-white text-center py-4">
{{ 'CHECKS.NO_SEARCH_RESULTS' | translate }}
-
-
0" class="table-responsive fix-table-height">
+
+
0" class="table-responsive fix-table-height d-none d-sm-block">
@@ -101,10 +167,10 @@ {{ 'CHECKS.NO_SEARCH_RESULTS' | translate }}
{{ check.location || '-' }} |
{{ check.payer || '-' }} |
- {{ check.totalSum | currency:'UAH':'₴' }} |
+ {{ check.totalSum | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }} |
- |
+
+
0" class="d-block d-sm-none px-2">
+
+ {{ 'ITEMS.DATA' | translate }}
+
+
+
+
+
0" class="accordion d-block d-sm-none px-2" id="checksAccordion">
+
+
+
+
+
+
+
+
+ {{ 'CHECKS.PAYER' | translate }}:
+
+
+ {{ check.payer || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0 && filteredChecksList.length > 0">
@@ -150,106 +298,4 @@ {{ 'CHECKS.NO_SEARCH_RESULTS' | translate }}
-
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.ts b/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.ts
index 1824f4d..dd84bc9 100644
--- a/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/checks/check-list/check-list.component.ts
@@ -1,29 +1,31 @@
-import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef, inject } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { ModalWindowComponent } from '../../shared/modal-window/modal-window.component';
-import { ValidationErrors, parseValidationErrors } from '../../shared/models/validation-errors.model';
-import { ChecksService, Check, DeleteCheckResponse } from '../../services/checks.service';
+import { ChecksService, Check } from '../../services/checks.service';
import { ItemListComponent } from '../../items/item-list/item-list.component';
import { ItemsService, Item } from '../../services/items.service';
-import { DayExpensesTotalSumUpdateService } from '../../services/day-expenses-total-sum-update.service';
-import { ToastService } from '../../services/toast.service';
-import { FormValidationService } from '../../services/form-validation.service';
+import { ModalService } from '../../services/modal.service';
import { FilterBarComponent, FilterOption } from '../../shared/filter-bar/filter-bar.component';
+import { SortBarComponent, SortOption } from '../../shared/sort-bar/sort-bar.component';
import { TourAnchorNgBootstrapDirective } from 'ngx-ui-tour-ng-bootstrap';
+import { CheckAddFormComponent } from '../../modals/check-form/check-add-form.component';
+import { CheckEditFormComponent } from '../../modals/check-form/check-edit-form.component';
+import { CheckDeleteFormComponent } from '../../modals/check-form/check-delete-form.component';
declare var bootstrap: any;
@Component({
selector: 'app-check-list',
standalone: true,
- imports: [CommonModule, FormsModule, ModalWindowComponent, TranslatePipe, ItemListComponent, FilterBarComponent, TourAnchorNgBootstrapDirective],
+ imports: [CommonModule, TranslatePipe, ItemListComponent, FilterBarComponent, SortBarComponent, TourAnchorNgBootstrapDirective],
templateUrl: './check-list.component.html',
styleUrl: './check-list.component.css'
})
export class CheckListComponent implements OnInit, OnChanges {
@Input() dayExpensesId!: string;
+ @Input() dayExpensesLocation?: string;
+ @Input() dayExpensesDate?: Date;
+ @Input() currentLocale: string = 'en';
@Input() participants: string[] = [];
@Input() checks?: Check[]; // Optional: if provided, use these instead of loading
@Input() scrollToCheckId?: string;
@@ -37,21 +39,6 @@ export class CheckListComponent implements OnInit, OnChanges {
checkItemsMap: Map
= new Map(); // Store items per check
checkLoadingMap: Map = new Map(); // Track loading state per check
- // Modal properties
- modalInstance: any;
- currentModalContent: 'add' | 'edit' | 'delete' = 'add';
- modalTitle: string = '';
-
- // Form properties
- id = '';
- location = '';
- payer = '';
- currentCheckTotalSum = 0;
-
- // Validation properties
- formErrors: ValidationErrors = {};
- formValidated = false;
-
// Filter and sort properties
filterText = '';
filterCriteria: string = 'Location';
@@ -62,6 +49,11 @@ export class CheckListComponent implements OnInit, OnChanges {
];
sortColumn: 'location' | 'totalSum' | 'payer' = 'totalSum';
sortOrder: 'asc' | 'desc' = 'desc';
+ sortOptions: SortOption[] = [
+ { value: 'location', labelKey: 'CHECKS.LOCATION' },
+ { value: 'payer', labelKey: 'CHECKS.PAYER' },
+ { value: 'totalSum', labelKey: 'CHECKS.SUM' }
+ ];
// UI state properties
isLoading = false;
@@ -73,16 +65,12 @@ export class CheckListComponent implements OnInit, OnChanges {
private shouldScrollToItem = false;
highlightedItemId?: string;
- // Inject services using inject() for better SSR compatibility
- private dayExpensesTotalSumUpdateService = inject(DayExpensesTotalSumUpdateService);
-
constructor(
private checksService: ChecksService,
private itemsService: ItemsService,
private translate: TranslateService,
private cdr: ChangeDetectorRef,
- private toastService: ToastService,
- private formValidationService: FormValidationService
+ private modalService: ModalService
) {}
ngOnInit(): void {
@@ -172,6 +160,12 @@ export class CheckListComponent implements OnInit, OnChanges {
this.applyLocalSorting();
}
+ onSortChange(event: { column: string; order: 'asc' | 'desc' }): void {
+ this.sortColumn = event.column as 'location' | 'totalSum' | 'payer';
+ this.sortOrder = event.order;
+ this.applyLocalSorting();
+ }
+
applyLocalFiltering(): void {
const searchTerm = this.filterText.toLowerCase().trim();
@@ -254,7 +248,10 @@ export class CheckListComponent implements OnInit, OnChanges {
}
loadItemsForCheck(checkId: string): void {
- // Set loading state
+ if (this.checkLoadingMap.get(checkId)) {
+ return;
+ }
+
this.checkLoadingMap.set(checkId, true);
this.itemsService.getAllCheckItems(checkId).subscribe({
@@ -312,187 +309,63 @@ export class CheckListComponent implements OnInit, OnChanges {
// Modal management methods
openModal(type: 'add' | 'edit' | 'delete', id: string = ''): void {
- this.currentModalContent = type;
- this.modalTitle = this.translate.instant(`CHECKS.MODAL.${type.toUpperCase()}_TITLE`);
-
- const modalElement = document.getElementById('checksModal');
- if (!modalElement) return;
-
if (type === 'add') {
- this.clearFormData();
- } else if (id) {
- // Load check data from local array (no server call needed)
- const check = this.checksList.find(c => c.id === id);
- if (check) {
- this.id = check.id;
- this.location = check.location;
- this.payer = check.payer;
- this.currentCheckTotalSum = check.totalSum;
- }
+ this.modalService.open(
+ CheckAddFormComponent,
+ this.translate.instant('CHECKS.MODAL.ADD_TITLE'),
+ {
+ participants: this.participants,
+ dayExpensesId: this.dayExpensesId,
+ onSuccess: () => this.refreshChecks()
+ },
+ 'md'
+ );
+ return;
}
- if (!this.modalInstance) {
- this.modalInstance = new bootstrap.Modal(modalElement, {
- backdrop: 'static',
- keyboard: false
- });
+ if (type === 'edit') {
+ const check = this.checksList.find(c => c.id === id);
+ if (!check) return;
+
+ this.modalService.open(
+ CheckEditFormComponent,
+ this.translate.instant('CHECKS.MODAL.EDIT_TITLE'),
+ {
+ participants: this.participants,
+ checkId: check.id,
+ location: check.location,
+ payer: check.payer,
+ onSuccess: () => this.refreshChecks()
+ },
+ 'md'
+ );
+ return;
}
- this.formErrors = {};
- this.formValidated = false;
-
- this.modalInstance.show();
- }
-
- hideModal(): void {
- if (this.modalInstance) {
- this.modalInstance.hide();
- this.formErrors = {};
- this.formValidated = false;
- this.clearFormData();
+ if (type === 'delete') {
+ const check = this.checksList.find(c => c.id === id);
+ if (!check) return;
+
+ this.modalService.open(
+ CheckDeleteFormComponent,
+ this.translate.instant('CHECKS.MODAL.DELETE_TITLE'),
+ {
+ checkId: check.id,
+ dayExpensesId: this.dayExpensesId,
+ location: check.location,
+ payer: check.payer,
+ totalSum: check.totalSum,
+ onSuccess: () => this.refreshChecks()
+ },
+ 'md'
+ );
+ return;
}
}
- clearFormData(): void {
- this.id = '';
- this.location = '';
- this.payer = '';
- this.currentCheckTotalSum = 0;
- }
-
- // CRUD operations
- validateCheckForm(): boolean {
- this.formErrors = {};
- this.formValidated = true;
-
- this.formErrors = this.formValidationService.validateCheckForm(this.location, this.payer);
-
- return !this.formValidationService.hasErrors(this.formErrors);
- }
-
- createCheck(): void {
- if (!this.validateCheckForm()) return;
- this.formValidated = true;
-
- this.checksService.createCheck(this.location, this.payer, this.dayExpensesId).subscribe({
- next: (createdCheck) => {
- // Add the new check to the list
- this.checksList.push(createdCheck);
- this.applyLocalFiltering();
-
- // Emit event to parent to re-initialize tour with updated step count
- this.checksLoaded.emit();
-
- this.hideModal();
- this.toastService.success(
- this.translate.instant('CHECKS.TOAST.SUCCESS'),
- this.translate.instant('CHECKS.TOAST.CREATE_SUCCESS')
- );
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('CHECKS.TOAST.CREATE_ERROR');
- this.toastService.error(
- this.translate.instant('CHECKS.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- }
- });
- }
-
- editCheck(): void {
- if (!this.validateCheckForm()) return;
- this.formValidated = true;
-
- this.checksService.editCheck(this.id, this.location, this.payer).subscribe({
- next: (updatedCheck) => {
- // Update the check in the local list
- const index = this.checksList.findIndex(c => c.id === this.id);
- if (index !== -1) {
- this.checksList[index] = updatedCheck;
- this.applyLocalFiltering();
- }
-
- this.hideModal();
- this.toastService.success(
- this.translate.instant('CHECKS.TOAST.SUCCESS'),
- this.translate.instant('CHECKS.TOAST.EDIT_SUCCESS')
- );
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('CHECKS.TOAST.EDIT_ERROR');
- this.toastService.error(
- this.translate.instant('CHECKS.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- }
- });
- }
-
- deleteCheck(): void {
- this.checksService.deleteCheck(this.id).subscribe({
- next: (response: DeleteCheckResponse) => {
- // Remove the check from the local list
- const index = this.checksList.findIndex(c => c.id === this.id);
- if (index !== -1) {
- this.checksList.splice(index, 1);
- // Also remove cached items for this check
- this.checkItemsMap.delete(this.id);
- this.checkLoadingMap.delete(this.id);
- this.applyLocalFiltering();
-
- // Emit day expenses total sum from backend response
- this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(this.dayExpensesId, response.dayExpensesTotalSum);
-
- // Emit event to parent to re-initialize tour with updated step count
- this.checksLoaded.emit();
- }
-
- this.hideModal();
- this.toastService.success(
- this.translate.instant('CHECKS.TOAST.SUCCESS'),
- this.translate.instant('CHECKS.TOAST.DELETE_SUCCESS')
- );
- },
- error: error => {
- console.log(error);
- const errorMessage = error?.error?.message || error?.message || this.translate.instant('CHECKS.TOAST.DELETE_ERROR');
- this.toastService.error(
- this.translate.instant('CHECKS.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- });
- }
-
- // Helper methods
- translateBackendError(errorMessage: string): string {
- if (!errorMessage) return '';
-
- const errorMap: Record = {
- 'Invalid data': 'CHECKS.BACKEND_ERRORS.INVALID_DATA',
- 'Unauthorized': 'CHECKS.BACKEND_ERRORS.UNAUTHORIZED'
- };
-
- const translationKey = errorMap[errorMessage];
- if (translationKey) {
- return this.translate.instant(translationKey);
- }
-
- for (const [key, value] of Object.entries(errorMap)) {
- if (errorMessage.toLowerCase().includes(key.toLowerCase())) {
- return this.translate.instant(value);
- }
- }
-
- return errorMessage;
+ refreshChecks(): void {
+ this.loadChecks();
+ this.checksLoaded.emit();
}
getIconOrderClass(column: string): string {
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.css b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.css
index c1a6fe4..9c3f585 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.css
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.css
@@ -35,3 +35,45 @@
background-color: #1f429d !important;
color: white;
}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ /* Back button sizing */
+ #backButton {
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ #backButton i {
+ transform: scaleX(1.2) !important;
+ }
+
+ /* Reduce title size */
+ h4 {
+ font-size: 0.9rem !important;
+ }
+
+ /* Reduce no data text size for small screens */
+ .no-data-section h2 {
+ font-size: 1.25rem !important;
+ }
+
+ .no-data-section h5 {
+ font-size: 0.85rem !important;
+ }
+
+ /* Reduce tab button text size */
+ .tab-btn {
+ font-size: 0.75rem !important;
+ padding: 0.5rem 0.75rem !important;
+ }
+
+ /* Reduce content text sizes */
+ .tab-content {
+ font-size: 0.85rem !important;
+ }
+
+ /* Reduce icon sizes */
+ .bi {
+ font-size: 0.9rem !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.html b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.html
index 94a1be7..4cc8214 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.html
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.html
@@ -1,14 +1,14 @@
-
+
-
@@ -21,7 +21,7 @@
+ class="no-data-section text-white text-center pt-4 pb-5">
{{ 'CALCULATIONS.NO_DATA' | translate }}
{{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
@@ -58,18 +58,18 @@ {{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
-
+ [attr.data-bs-title]="getCheckTooltip(checkCalc.check.totalSum, checkCalc.check.payer)">
+
{{ checkCalc.check.location }}
-
+
-
{{ itemCalc.item.name }}:
- {{ itemCalc.item.price * itemCalc.item.amount | currency:'UAH':'₴' }}
+ {{ itemCalc.item.price * itemCalc.item.amount | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
/
{{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
[attr.data-bs-title]="getItemUsersTooltip(itemCalc.item.users)">
{{ itemCalc.item.users.length }}
- =
- {{ itemCalc.pricePerUser | currency:'UAH':'₴' }}
+ =
+ {{ itemCalc.pricePerUser | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
@@ -86,7 +86,7 @@ {{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
{{ 'CALCULATIONS.SUM_PER_PARTICIPANT' | translate }}:
- {{ checkCalc.sumPerParticipant | currency:'UAH':'₴' }}
+ {{ checkCalc.sumPerParticipant | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
@@ -97,7 +97,7 @@ {{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
{{ 'CALCULATIONS.TOTAL_EXPENSES' | translate }}:
- {{ getTotalForUser(participant) | currency:'UAH':'₴' }}
+ {{ getTotalForUser(participant) | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
@@ -112,10 +112,10 @@ {{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
{{ 'CALCULATIONS.TRANSACTION_LIST' | translate }}:
-
+
-
{{ transaction.subjects.sender }}
- ({{ transaction.transferAmount | currency:'UAH':'₴'
+ ({{ transaction.transferAmount | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate)
}})
→
{{ transaction.subjects.recipient }}
@@ -140,11 +140,11 @@
{{ 'CALCULATIONS.NO_DATA_HINT' | translate }}
-
+
-
{{ transaction.checkName }}:
{{ transaction.subjects.sender }}
- ({{ transaction.transferAmount | currency:'UAH':'₴' }})
+ ({{ transaction.transferAmount | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }})
→
{{ transaction.subjects.recipient }}
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.ts b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.ts
index fe8bc15..ed57162 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-calculations/day-expenses-calculations.component.ts
@@ -128,9 +128,8 @@ export class DayExpensesCalculationsComponent implements OnInit, AfterViewInit,
return this.tooltipService.generateUsersTooltip(users, 3);
}
- getCheckTooltip(items: ItemCalculation[], payer: string): string {
- const sum = items.reduce((total, itemCalc) => total + itemCalc.item.price, 0);
- return this.tooltipService.generateCheckTooltip(sum, payer);
+ getCheckTooltip(totalSum: number, payer: string): string {
+ return this.tooltipService.generateCheckTooltip(totalSum, payer);
}
// Tour
@@ -168,7 +167,12 @@ export class DayExpensesCalculationsComponent implements OnInit, AfterViewInit,
);
}
- this.tourService.initialize(tourSteps);
+ // Initialize tour with global button title configuration
+ this.tourService.initialize(tourSteps, {
+ prevBtnTitle: this.translate.instant('TOUR.PREV_BTN'),
+ nextBtnTitle: this.translate.instant('TOUR.NEXT_BTN'),
+ endBtnTitle: this.translate.instant('TOUR.END_BTN')
+ });
}
startTour(): void {
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.css b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.css
index 51fccb7..3a64af0 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.css
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.css
@@ -101,3 +101,31 @@
align-items: center;
justify-content: center;
}
+
+/* Responsive card sizing for small screens */
+@media (max-width: 576px) {
+ /* Back button sizing */
+ #backButton {
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ #backButton i {
+ transform: scaleX(1.2) !important;
+ }
+
+ .info-card {
+ padding: 0.75rem !important;
+ }
+
+ .info-card .text-white {
+ font-size: 0.75rem !important;
+ }
+
+ .info-card .h6 {
+ font-size: 0.85rem !important;
+ }
+
+ .info-card .fs-5 {
+ font-size: 0.9rem !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.html b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.html
index 1ecd3e4..28ffccb 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.html
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.html
@@ -1,10 +1,10 @@
-
+
-
+
@@ -60,7 +60,7 @@
{{ 'EXPENSES.TOTAL_SUM' | translate }}
-
{{ dayExpenses.totalSum | currency:'UAH':'₴' }}
+
{{ dayExpenses.totalSum | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
@@ -69,111 +69,5 @@
-
-
-
-
-
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.ts b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.ts
index c92d61d..5005215 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-details/day-expenses-details.component.ts
@@ -1,41 +1,35 @@
import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
-import { ModalWindowComponent } from "../../shared/modal-window/modal-window.component";
-import { ValidationErrors, parseValidationErrors } from '../../shared/models/validation-errors.model';
-import { ExpensesService, DayExpenses, DayExpensesDetails } from '../../services/expenses.service';
+import { ExpensesService, DayExpenses } from '../../services/expenses.service';
import { Check } from '../../services/checks.service';
import { CheckListComponent } from '../../checks/check-list/check-list.component';
import { ToastService } from '../../services/toast.service';
import { TooltipService } from '../../services/tooltip.service';
-import { FormValidationService } from '../../services/form-validation.service';
+import { ModalService } from '../../services/modal.service';
import { DateRangeService } from '../../services/date-range.service';
import { DayExpensesTotalSumUpdateService } from '../../services/day-expenses-total-sum-update.service';
import { Subscription } from 'rxjs';
import { TourService, TourAnchorNgBootstrapDirective, TourStepTemplateComponent } from 'ngx-ui-tour-ng-bootstrap';
+import { DayExpensesEditFormComponent } from '../../modals/day-expenses-form/day-expenses-edit-form.component';
+import { DayExpensesDeleteFormComponent } from '../../modals/day-expenses-form/day-expenses-delete-form.component';
+import { DayExpensesShareFormComponent } from '../../modals/day-expenses-form/day-expenses-share-form.component';
declare var bootstrap: any;
@Component({
selector: 'app-day-expenses-details',
- imports: [TranslatePipe, CommonModule, FormsModule, ModalWindowComponent, CheckListComponent, TourAnchorNgBootstrapDirective, TourStepTemplateComponent],
+ imports: [TranslatePipe, CommonModule, CheckListComponent, TourAnchorNgBootstrapDirective, TourStepTemplateComponent],
standalone: true,
templateUrl: './day-expenses-details.component.html',
styleUrl: './day-expenses-details.component.css'
})
export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDestroy {
// Private variables
- private modalFlatpickrInstance: any;
private langChangeSub!: Subscription;
private dayExpensesTotalSumUpdateSub?: Subscription;
- // Modal properties
- modalInstance: any;
- currentModalContent: 'edit' | 'delete' | 'share' = 'edit';
- modalTitle: string = '';
-
// Locale
currentLocale: string = 'en';
@@ -50,15 +44,6 @@ export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDes
scrollToCheckId?: string;
scrollToItemId?: string;
- // Share functionality
- newUserWithAccess = '';
- shareIsSuccess = false;
- shareError = '';
-
- // Form validation
- formErrors: ValidationErrors = {};
- formValidated = false;
-
// Loading state
isLoading = false;
@@ -69,7 +54,7 @@ export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDes
private expensesService: ExpensesService,
private toastService: ToastService,
private tooltipService: TooltipService,
- private formValidationService: FormValidationService,
+ private modalService: ModalService,
private dateRangeService: DateRangeService,
private dayExpensesTotalSumUpdateService: DayExpensesTotalSumUpdateService,
public tourService: TourService
@@ -96,10 +81,6 @@ export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDes
this.tooltipService.destroy();
setTimeout(() => this.tooltipService.initialize({ html: true }), 0);
- if (this.modalFlatpickrInstance) {
- this.dateRangeService.updateLocale(this.modalFlatpickrInstance, event.lang);
- }
-
// Re-initialize tour with new language
this.initializeTour();
});
@@ -120,7 +101,6 @@ export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDes
}
ngOnDestroy(): void {
- this.dateRangeService.destroy(this.modalFlatpickrInstance);
this.tooltipService.destroy();
if (this.langChangeSub) {
this.langChangeSub.unsubscribe();
@@ -159,219 +139,71 @@ export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDes
// Modal management
openModal(type: 'edit' | 'delete' | 'share', id: string = '') {
- this.currentModalContent = type;
- this.modalTitle = this.translate.instant(`EXPENSES.MODAL.${type.toUpperCase()}_TITLE`);
-
- const modalElement = document.getElementById('staticBackdrop');
- if (!modalElement) return;
-
if (id && id !== this.id) {
- // If a different ID is provided, load that day expenses
this.id = id;
this.loadDayExpenses();
+ return;
}
- if (!this.modalInstance) {
- this.modalInstance = new bootstrap.Modal(modalElement, {
- backdrop: 'static',
- keyboard: false
- });
- }
-
- this.formErrors = {};
- this.formValidated = false;
- this.newUserWithAccess = '';
-
- this.modalInstance.show();
-
- // Initialize flatpickr for edit modal
- const readonly = type === 'delete' || type === 'share';
- setTimeout(() => this.initModalFlatpickr(readonly), 0);
- }
-
- hideModal() {
- if (this.modalInstance) {
- this.dateRangeService.destroy(this.modalFlatpickrInstance);
- this.modalInstance.hide();
- this.newUserWithAccess = '';
- this.formErrors = {};
- this.formValidated = false;
- }
- }
-
- // Flatpickr
- initModalFlatpickr(readonly: boolean = false) {
- this.dateRangeService.destroy(this.modalFlatpickrInstance);
-
- this.modalFlatpickrInstance = this.dateRangeService.initializeSingleDatePicker('modalDateInput', {
- readonly: readonly,
- defaultDate: this.date || undefined,
- onChange: (dates: Date[]) => {
- if (dates.length > 0) {
- this.date = this.dateRangeService.formatDate(dates[0]);
- }
- }
- });
- }
-
- // Form validation
- private setDateInputValidation(invalid: boolean, valid: boolean) {
- // Flatpickr altInput is the visible input next to the hidden #modalDateInput
- if (this.modalFlatpickrInstance?.altInput) {
- if (invalid) {
- this.modalFlatpickrInstance.altInput.classList.add('is-invalid');
- this.modalFlatpickrInstance.altInput.classList.remove('is-valid');
- } else if (valid) {
- this.modalFlatpickrInstance.altInput.classList.add('is-valid');
- this.modalFlatpickrInstance.altInput.classList.remove('is-invalid');
- } else {
- this.modalFlatpickrInstance.altInput.classList.remove('is-invalid');
- this.modalFlatpickrInstance.altInput.classList.remove('is-valid');
- }
+ if (type === 'edit') {
+ this.modalService.open(
+ DayExpensesEditFormComponent,
+ this.translate.instant('EXPENSES.MODAL.EDIT_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ id: this.id,
+ date: this.date,
+ location: this.location,
+ participants: this.participants,
+ totalSum: this.totalSum,
+ onSuccess: () => this.refreshDayExpenses()
+ },
+ 'md'
+ );
+ return;
}
- }
-
- private validateDayExpensesForm(): boolean {
- this.formErrors = {};
- this.formValidated = true;
- this.setDateInputValidation(false, false);
-
- this.formErrors = this.formValidationService.validateDayExpensesForm(this.date, this.participants);
- if (this.formErrors['date']) {
- this.setDateInputValidation(true, false);
- } else if (this.date) {
- this.setDateInputValidation(false, true);
+ if (type === 'delete') {
+ this.modalService.open(
+ DayExpensesDeleteFormComponent,
+ this.translate.instant('EXPENSES.MODAL.DELETE_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ id: this.id,
+ date: this.date,
+ location: this.location,
+ participants: this.participants,
+ totalSum: this.totalSum,
+ onSuccess: () => this.router.navigate(['/day-expenses'])
+ },
+ 'md'
+ );
+ return;
}
- return !this.formValidationService.hasErrors(this.formErrors);
- }
-
- // Data modification
- editDayExpenses() {
- if (!this.validateDayExpensesForm()) return;
- this.formValidated = true;
-
- const participantsList = this.participants.split(',').map(p => p.trim());
-
- this.expensesService.editDayExpenses(this.id, this.date, this.location, participantsList).subscribe({
- next: (updatedDay) => {
- // Update local data with returned values
- this.dayExpenses = updatedDay;
- this.date = (new Date(updatedDay.date)).toISOString().substring(0, 10);
- this.location = updatedDay.location;
- this.participants = updatedDay.participants.join(', ');
- this.totalSum = updatedDay.totalSum;
-
- this.hideModal();
- this.toastService.success(
- this.translate.instant('EXPENSES.TOAST.SUCCESS'),
- this.translate.instant('EXPENSES.TOAST.EDIT_SUCCESS')
- );
-
- // Re-initialize tooltips after data updates
- setTimeout(() => this.tooltipService.initialize({ html: true }), 0);
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.EDIT_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- }
- });
- }
-
- deleteDayExpenses() {
- this.expensesService.deleteDayExpenses(this.id).subscribe({
- next: () => {
- this.hideModal();
- this.router.navigate(['/day-expenses']);
- },
- error: error => {
- console.log(error);
- const errorMessage = error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.DELETE_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- });
- }
-
- shareDayExpenses() {
- this.formErrors = {};
- this.shareError = '';
- this.formValidated = true;
-
- this.formErrors = this.formValidationService.validateShareForm(this.newUserWithAccess);
- if (this.formValidationService.hasErrors(this.formErrors)) {
+ if (type === 'share') {
+ this.modalService.open(
+ DayExpensesShareFormComponent,
+ this.translate.instant('EXPENSES.MODAL.SHARE_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ id: this.id,
+ date: this.date,
+ location: this.location,
+ participants: this.participants,
+ totalSum: this.totalSum,
+ onSuccess: () => this.refreshDayExpenses()
+ },
+ 'md'
+ );
return;
}
-
- this.expensesService.shareDayExpenses(this.id, this.newUserWithAccess).subscribe({
- next: (data) => {
- if (data.isSuccess) {
- this.hideModal();
- this.loadDayExpenses();
- this.shareIsSuccess = false;
- this.toastService.success(
- this.translate.instant('EXPENSES.TOAST.SUCCESS'),
- this.translate.instant('EXPENSES.TOAST.SHARE_SUCCESS')
- );
- }
- else {
- this.shareIsSuccess = data.isSuccess;
- this.shareError = this.translateBackendError(data.error);
- this.formErrors['newUserWithAccess'] = this.shareError;
- }
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.BACKEND_ERRORS.UNKNOWN_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- }
- });
}
- // Helper methods
- translateBackendError(errorMessage: string): string {
- if (!errorMessage) return '';
-
- // Map common backend error messages to translation keys
- const errorMap: Record
= {
- 'User not found': 'EXPENSES.BACKEND_ERRORS.USER_NOT_FOUND',
- 'Invalid data': 'EXPENSES.BACKEND_ERRORS.INVALID_DATA',
- 'Unauthorized': 'EXPENSES.BACKEND_ERRORS.UNAUTHORIZED',
- 'Already shared': 'EXPENSES.BACKEND_ERRORS.ALREADY_SHARED',
- 'already has access': 'EXPENSES.BACKEND_ERRORS.ALREADY_HAS_ACCESS'
- };
-
- // Check if we have a translation for this error
- const translationKey = errorMap[errorMessage];
- if (translationKey) {
- return this.translate.instant(translationKey);
- }
-
- // If no exact match, check for partial matches
- for (const [key, value] of Object.entries(errorMap)) {
- if (errorMessage.toLowerCase().includes(key.toLowerCase())) {
- return this.translate.instant(value);
- }
- }
-
- // Return original message if no translation found
- return errorMessage;
+ refreshDayExpenses(): void {
+ this.loadDayExpenses();
+ this.tooltipService.destroy();
+ setTimeout(() => this.tooltipService.initialize({ html: true }), 100);
}
getTooltipContent() {
@@ -425,74 +257,148 @@ export class DayExpensesDetailsComponent implements OnInit, AfterViewInit, OnDes
// Tour
initializeTour() {
- // Check if checks exist by checking the checks array
+ // Detect small screen (same breakpoint as CSS: 576px)
+ const isSmallScreen = window.innerWidth < 576;
const checksExist = this.checks && this.checks.length > 0;
-
const tourSteps: any[] = [];
- // Always show these basic steps
- tourSteps.push(
- {
- anchorId: 'back-btn',
- content: this.translate.instant('TOUR_DETAILS.BACK_BTN_CONTENT'),
- title: this.translate.instant('TOUR_DETAILS.BACK_BTN_TITLE'),
+ // Step 1: Back button (all screens)
+ tourSteps.push({
+ anchorId: 'back-btn',
+ content: this.translate.instant('TOUR_DETAILS.BACK_BTN_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.BACK_BTN_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ });
+
+ // Step 2: Add check button (different anchor for small screens with data)
+ if (isSmallScreen && checksExist) {
+ tourSteps.push({
+ anchorId: 'add-check-btn-small',
+ content: this.translate.instant('TOUR_DETAILS.ADD_CHECK_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.ADD_CHECK_TITLE'),
placement: 'bottom',
enableBackdrop: true
- },
- {
+ });
+ } else {
+ tourSteps.push({
anchorId: 'add-check-btn',
content: this.translate.instant('TOUR_DETAILS.ADD_CHECK_CONTENT'),
title: this.translate.instant('TOUR_DETAILS.ADD_CHECK_TITLE'),
- placement: 'top',
+ placement: isSmallScreen ? 'bottom' : 'top',
enableBackdrop: true
- }
- );
+ });
+ }
- // Add workflow steps only if checks exist
+ // Steps 3-6: Screen-specific workflow (only if checks exist)
if (checksExist) {
- // Get the first check's ID to construct the dynamic add-item-btn anchor
- const firstCheckId = this.checks[0].id;
- const addItemBtnAnchorId = `add-item-btn-${firstCheckId}`;
-
- tourSteps.push(
- {
- anchorId: 'expand-check-btn',
- content: this.translate.instant('TOUR_DETAILS.EXPAND_CHECK_CONTENT'),
- title: this.translate.instant('TOUR_DETAILS.EXPAND_CHECK_TITLE'),
- placement: 'right',
- enableBackdrop: true
- },
- {
- anchorId: addItemBtnAnchorId,
- content: this.translate.instant('TOUR_DETAILS.ADD_ITEM_CONTENT'),
- title: this.translate.instant('TOUR_DETAILS.ADD_ITEM_TITLE'),
- placement: 'right',
- enableBackdrop: true
- },
- {
- anchorId: 'calculator-btn',
- content: this.translate.instant('TOUR_DETAILS.CALCULATOR_CONTENT'),
- title: this.translate.instant('TOUR_DETAILS.CALCULATOR_TITLE'),
- placement: 'left',
- enableBackdrop: true
- }
- );
+ if (isSmallScreen) {
+ // Small screen: accordion view
+ tourSteps.push(
+ {
+ anchorId: 'filter-sort-controls',
+ content: this.translate.instant('TOUR_DETAILS.FILTER_SORT_CONTROLS_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.FILTER_SORT_CONTROLS_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'checks-accordion',
+ content: this.translate.instant('TOUR_DETAILS.CHECKS_ACCORDION_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.CHECKS_ACCORDION_TITLE'),
+ placement: 'top',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'accordion-check-item',
+ content: this.translate.instant('TOUR_DETAILS.ACCORDION_CHECK_ITEM_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.ACCORDION_CHECK_ITEM_TITLE'),
+ placement: 'top',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'calculator-btn',
+ content: this.translate.instant('TOUR_DETAILS.CALCULATOR_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.CALCULATOR_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ }
+ );
+ } else {
+ // Large screen: table view
+ const firstCheckId = this.checks[0].id;
+ tourSteps.push(
+ {
+ anchorId: 'expand-check-btn',
+ content: this.translate.instant('TOUR_DETAILS.EXPAND_CHECK_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.EXPAND_CHECK_TITLE'),
+ placement: 'right',
+ enableBackdrop: true
+ },
+ {
+ anchorId: `add-item-btn-${firstCheckId}`,
+ content: this.translate.instant('TOUR_DETAILS.ADD_ITEM_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.ADD_ITEM_TITLE'),
+ placement: 'right',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'actions-menu',
+ content: this.translate.instant('TOUR_DETAILS.ACTIONS_MENU_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.ACTIONS_MENU_TITLE'),
+ placement: 'left',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'calculator-btn',
+ content: this.translate.instant('TOUR_DETAILS.CALCULATOR_CONTENT'),
+ title: this.translate.instant('TOUR_DETAILS.CALCULATOR_TITLE'),
+ placement: 'left',
+ enableBackdrop: true
+ }
+ );
+ }
}
- this.tourService.initialize(tourSteps);
+ // Initialize tour with global button title configuration
+ this.tourService.initialize(tourSteps, {
+ prevBtnTitle: this.translate.instant('TOUR.PREV_BTN'),
+ nextBtnTitle: this.translate.instant('TOUR.NEXT_BTN'),
+ endBtnTitle: this.translate.instant('TOUR.END_BTN')
+ });
}
startTour() {
- // Auto-expand the first check when tour starts so the Add Item button becomes visible
- const expandButton = document.querySelector('[touranchor="expand-check-btn"]') as HTMLElement;
- if (expandButton) {
- // Find the collapse target from the button's data-bs-target attribute
- const collapseTarget = expandButton.getAttribute('data-bs-target');
- if (collapseTarget) {
- const collapseElement = document.querySelector(collapseTarget);
+ const isSmallScreen = window.innerWidth < 576;
+ const checksExist = this.checks && this.checks.length > 0;
+
+ if (!checksExist) {
+ this.tourService.start();
+ return;
+ }
+
+ if (isSmallScreen) {
+ // For small screens (accordion view), expand the first accordion item
+ const firstCheck = this.checks[0];
+ const accordionButton = document.querySelector(
+ `[data-bs-target="#accordionCollapse${firstCheck.id}"]`
+ ) as HTMLElement;
+ if (accordionButton) {
+ const collapseElement = document.getElementById('accordionCollapse' + firstCheck.id);
if (collapseElement && !collapseElement.classList.contains('show')) {
- // Programmatically expand the first check
- expandButton.click();
+ accordionButton.click();
+ }
+ }
+ } else {
+ // For large screens (table view), expand the first check
+ const expandButton = document.querySelector('[touranchor="expand-check-btn"]') as HTMLElement;
+ if (expandButton) {
+ const collapseTarget = expandButton.getAttribute('data-bs-target');
+ if (collapseTarget) {
+ const collapseElement = document.querySelector(collapseTarget);
+ if (collapseElement && !collapseElement.classList.contains('show')) {
+ expandButton.click();
+ }
}
}
}
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.css b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.css
index e7b7c34..f95111f 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.css
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.css
@@ -13,6 +13,7 @@ th {
td {
vertical-align: middle !important;
+ white-space: nowrap;
}
.table tr:nth-child(odd) td {
@@ -117,14 +118,22 @@ td {
border-color: black;
}
-/* Override table-responsive overflow to prevent scrollbar issues with dropdowns */
+/* Enable horizontal scrolling while keeping dropdowns visible */
.fix-table-height {
- overflow: visible !important;
+ overflow-x: auto !important;
+ overflow-y: visible !important;
}
-/* Ensure dropdowns are positioned above other content */
+/* Ensure dropdowns are positioned above other content and can overlay */
.dropdown-menu {
z-index: 1050;
+ position: absolute !important;
+ will-change: transform;
+}
+
+/* Allow dropdown to escape overflow container */
+tr .dropdown {
+ position: static;
}
/* Truncate location column to approximately 15 characters */
@@ -133,4 +142,132 @@ td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ /* Reduce spacing around Add Expenses button */
+ hr.my-3 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+
+ .table {
+ font-size: 0.85rem !important;
+ }
+
+ /* Pagination elements */
+ .pagination-container {
+ font-size: 0.9rem !important;
+ flex-direction: column !important;
+ align-items: center !important;
+ gap: 15px !important;
+ }
+
+ .pagination-container > div:first-child {
+ width: 100% !important;
+ text-align: left !important;
+ }
+
+ .page-size-selector {
+ font-size: 0.9rem !important;
+ width: 100% !important;
+ justify-content: flex-end !important;
+ margin-top: 0 !important;
+ padding-right: 0.5rem !important;
+ }
+
+ .pagination-container .btn {
+ font-size: 0.8rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ /* Filter controls */
+ .input-group {
+ font-size: 0.8rem !important;
+ }
+
+ /* Date range control sizing */
+ #dateRangeInput {
+ font-size: 0.75rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ #calendarButton {
+ font-size: 0.8rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ /* Make date range input full width on small screens */
+ .input-group.text-center {
+ min-width: 100% !important;
+ width: 100% !important;
+ }
+
+ /* Filter and Sort group - side by side buttons */
+ .filter-sort-group {
+ gap: 0.5rem !important;
+ }
+
+ .filter-sort-group .filter-btn {
+ width: 50% !important;
+ font-size: 0.85rem !important;
+ padding: 0.2rem 0.5rem !important;
+ line-height: 1.2 !important;
+ }
+
+ .filter-sort-group app-sort-bar {
+ width: 50% !important;
+ flex: 0 0 50%;
+ }
+
+ .filter-sort-group app-sort-bar .sort-dropdown {
+ width: 100%;
+ }
+
+ .filter-sort-group app-sort-bar button {
+ width: 100% !important;
+ }
+
+ /* Reduce no data text size for small screens */
+ .no-data-section h2 {
+ font-size: 1.25rem !important;
+ }
+
+ .no-data-section h5 {
+ font-size: 0.85rem !important;
+ }
+}
+
+/* Accordion styles for small screens */
+.accordion-button {
+ background-color: transparent !important;
+ box-shadow: none !important;
+ border-radius: 0 !important;
+}
+
+.accordion-button:not(.collapsed) {
+ background-color: transparent !important;
+ color: white !important;
+}
+
+.accordion-button::after {
+ filter: invert(1);
+}
+
+.accordion-button:focus {
+ box-shadow: none !important;
+ border-color: transparent !important;
+}
+
+.accordion-item {
+ border-bottom: 1px solid rgba(13, 110, 253, 0.3) !important;
+}
+
+.accordion-item:last-child {
+ border-bottom: none !important;
+}
+
+.accordion-body {
+ border-top: 1px solid rgba(13, 110, 253, 0.2);
}
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.html b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.html
index 0805874..fb5a226 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.html
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.html
@@ -1,17 +1,17 @@
-
-
+
+
-
+
{{ 'EXPENSES.TITLE' | translate }}
-
+
-
+
@@ -21,18 +21,18 @@
-
+
{{ 'EXPENSES.NO_DATA' | translate }}
{{ 'EXPENSES.NO_DATA_HINT' | translate }}
{{ 'EXPENSES.ADD' | translate }}
-
+
-
+
-
+
{{ 'EXPENSES.DATE_FILTER' | translate }}
@@ -60,8 +60,8 @@
{{ 'EXPENSES.NO_DATA_HINT' | translate }}
-
-
+
+
{{ 'ITEMS.FILTER.CAPTION' | translate }}
@@ -77,10 +77,36 @@
{{ 'EXPENSES.NO_DATA_HINT' | translate }}
-
-
{{ 'EXPENSES.NO_SEARCH_RESULTS' | translate }}
+
+
+
+
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}
+
+
+
+
+ {{ 'ITEMS.FILTER.APPLY' | translate }}
+
+
+
-
0" class="table-responsive fix-table-height">
+
+
0" class="table-responsive fix-table-height d-none d-sm-block">
@@ -94,12 +120,12 @@ {{ 'EXPENSES.NO_SEARCH_RESULTS' | translate }}
{{ 'EXPENSES.LOCATION' | translate }}
-
+ |
{{ 'EXPENSES.PARTICIPANTS' | translate }}
|
-
+ |
{{ 'EXPENSES.TOTAL_SUM' | translate }}
@@ -115,7 +141,7 @@ {{ 'EXPENSES.NO_SEARCH_RESULTS' | translate }}
|
{{ day.location || '-' }}
|
-
+ |
|
-
- {{ day.totalSum | currency:'UAH':'₴' }}
+ |
+ {{ day.totalSum | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
|
@@ -170,6 +196,129 @@ {{ 'EXPENSES.NO_SEARCH_RESULTS' | translate }}
|
+
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }} & {{ 'ITEMS.SORT.CAPTION' | translate }}
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}
+
+
+
+
+
+
+
+
+
{{ 'EXPENSES.NO_SEARCH_RESULTS' | translate }}
+
+
+
+
0" class="d-block d-sm-none px-2">
+
+ {{ 'ITEMS.DATA' | translate }}
+
+
+
+
+
0" class="accordion d-block d-sm-none" id="expensesAccordion" tourAnchor="expenses-accordion">
+
+
+
+
+
+
+
+
+
+ {{ 'EXPENSES.PARTICIPANTS' | translate }}:
+
+
+
+
+
+
+
+
+ {{ 'EXPENSES.TOTAL_SUM' | translate }}:
+
+
+ {{ day.totalSum | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
+
+
+
+
+
+
+ {{ 'EXPENSES.OPEN' | translate }}
+
+
+ {{ 'EXPENSES.CALCULATE' | translate }}
+
+
+
+
+ {{ 'EXPENSES.EDIT' | translate }}
+
+
+ {{ 'EXPENSES.SHARE' | translate }}
+
+
+ {{ 'EXPENSES.DELETE' | translate }}
+
+
+
+
+
+
+
+
@@ -244,146 +393,4 @@
{{ 'EXPENSES.NO_SEARCH_RESULTS' | translate }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.ts b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.ts
index 94d8849..5a9f5eb 100644
--- a/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/dayExpenses/day-expenses-list/day-expenses-list.component.ts
@@ -1,28 +1,29 @@
import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExpensesService, DayExpenses } from '../../services/expenses.service';
-import { ToastService } from '../../services/toast.service';
import { DateRangeService } from '../../services/date-range.service';
import { TooltipService } from '../../services/tooltip.service';
-import { FormValidationService } from '../../services/form-validation.service';
-import { ModalWindowComponent } from "../../shared/modal-window/modal-window.component";
+import { ModalService } from '../../services/modal.service';
import { FilterBarComponent, FilterOption } from '../../shared/filter-bar/filter-bar.component';
-import { FormsModule } from '@angular/forms';
+import { SortBarComponent, SortOption } from '../../shared/sort-bar/sort-bar.component';
import { Router } from '@angular/router';
import { RouterLink } from "@angular/router";
import { DatePipe } from '@angular/common';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
-import { ValidationErrors, parseValidationErrors } from '../../shared/models/validation-errors.model';
import { TourService, TourAnchorNgBootstrapDirective, TourStepTemplateComponent } from 'ngx-ui-tour-ng-bootstrap';
+import { DayExpensesAddFormComponent } from '../../modals/day-expenses-form/day-expenses-add-form.component';
+import { DayExpensesEditFormComponent } from '../../modals/day-expenses-form/day-expenses-edit-form.component';
+import { DayExpensesDeleteFormComponent } from '../../modals/day-expenses-form/day-expenses-delete-form.component';
+import { DayExpensesShareFormComponent } from '../../modals/day-expenses-form/day-expenses-share-form.component';
declare var bootstrap: any;
@Component({
selector: 'app-day-expenses-list',
standalone: true,
- imports: [RouterLink, FormsModule, CommonModule, ModalWindowComponent, FilterBarComponent, TranslatePipe, TourAnchorNgBootstrapDirective, TourStepTemplateComponent],
+ imports: [RouterLink, CommonModule, FilterBarComponent, SortBarComponent, TranslatePipe, TourAnchorNgBootstrapDirective, TourStepTemplateComponent],
providers: [DatePipe],
templateUrl: './day-expenses-list.component.html',
styleUrl: './day-expenses-list.component.css'
@@ -30,7 +31,6 @@ declare var bootstrap: any;
export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestroy {
private flatpickrInitialized = false;
private flatpickrInstance: any;
- private modalFlatpickrInstance: any;
private settingDatesFromApiWrapper = { value: false };
private langChangeSub!: Subscription;
private filterTextSubject = new Subject
();
@@ -44,7 +44,6 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
ngOnDestroy(): void {
this.dateRangeService.destroy(this.flatpickrInstance);
- this.destroyModalFlatpickr();
if (this.langChangeSub) {
this.langChangeSub.unsubscribe();
}
@@ -92,26 +91,6 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
);
}
- initModalFlatpickr(readonly: boolean = false) {
- this.destroyModalFlatpickr();
-
- this.modalFlatpickrInstance = this.dateRangeService.initializeSingleDatePicker(
- 'modalDateInput',
- {
- defaultDate: this.date || undefined,
- readonly: readonly,
- onChange: (dates: Date[]) => {
- this.date = this.dateRangeService.formatDate(dates[0]);
- }
- }
- );
- }
-
- destroyModalFlatpickr() {
- this.dateRangeService.destroy(this.modalFlatpickrInstance);
- this.modalFlatpickrInstance = null;
- }
-
clearDateRange(): void {
if (this.flatpickrInstance) {
// Destroy and reinitialize to ensure clean state
@@ -162,38 +141,27 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
currentPage: number = 1;
pageSize: number = 10;
- // Expenses data
- id = '';
- date = '';
- location = '';
- participants = '';
- totalSum = 0;
-
- // Add new user with access
- newUserWithAccess = '';
- shareIsSuccess = false;
- shareError = '';
-
- // Form validation errors
- formErrors: ValidationErrors = {};
- formValidated = false;
-
// Searching
filterText = '';
// Sorting
sortColumn: 'date' | 'location' | 'participants' | 'totalSum' = 'date';
sortOrder: 'asc' | 'desc' = 'desc';
+ sortOptions: SortOption[] = [
+ { value: 'date', labelKey: 'EXPENSES.DATE' },
+ { value: 'location', labelKey: 'EXPENSES.LOCATION' },
+ { value: 'participants', labelKey: 'EXPENSES.PARTICIPANTS' },
+ { value: 'totalSum', labelKey: 'EXPENSES.TOTAL_SUM' }
+ ];
constructor(
private expensesService: ExpensesService,
private router: Router,
private datePipe: DatePipe,
private translate: TranslateService,
- private toastService: ToastService,
private dateRangeService: DateRangeService,
private tooltipService: TooltipService,
- private formValidationService: FormValidationService,
+ private modalService: ModalService,
public tourService: TourService
) {}
@@ -214,10 +182,6 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
}
}
- if (this.modalFlatpickrInstance) {
- this.dateRangeService.updateLocale(this.modalFlatpickrInstance, event.lang, false);
- }
-
// Re-initialize tour with new language
this.initializeTour();
});
@@ -275,11 +239,7 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
})
}
- modalInstance: any;
- currentModalContent: 'add' | 'edit' | 'delete' | 'share' = 'add';
- modalTitle: string = '';
-
- openModal(type: 'add' | 'edit' | 'delete' | 'share', id: string) {
+ openModal(type: 'add' | 'edit' | 'delete' | 'share', id: string = '') {
// End tour if it's running
if (this.tourService.getStatus() !== 0) {
this.tourService.end();
@@ -287,62 +247,83 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 100);
}
- this.currentModalContent = type;
- this.modalTitle = this.translate.instant(`EXPENSES.MODAL.${type.toUpperCase()}_TITLE`);
-
- const modalElement = document.getElementById('staticBackdrop');
- if (!modalElement) return;
-
- // Clear form fields for 'add' modal, populate from list for others
if (type === 'add') {
- this.clearFormData();
- } else if (id !== undefined) {
- const expense = this.expensesList.find(e => e.id === id);
- if (expense) {
- this.id = expense.id;
- this.date = (new Date(expense.date)).toISOString().substring(0, 10);
- this.location = expense.location;
- this.participants = expense.participants.join(', ');
- this.totalSum = expense.totalSum;
- }
+ this.modalService.open(
+ DayExpensesAddFormComponent,
+ this.translate.instant('EXPENSES.MODAL.ADD_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ onSuccess: () => { /* No callback needed as navigation happens in component */ }
+ },
+ 'md'
+ );
+ return;
}
- if (!this.modalInstance) {
- this.modalInstance = new bootstrap.Modal(modalElement, {
- backdrop: 'static',
- keyboard: false
- });
- }
+ const expense = this.expensesList.find(e => e.id === id);
+ if (!expense) return;
- this.formErrors = {};
- this.shareError = '';
- this.formValidated = false;
+ if (type === 'edit') {
+ this.modalService.open(
+ DayExpensesEditFormComponent,
+ this.translate.instant('EXPENSES.MODAL.EDIT_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ id: expense.id,
+ date: (new Date(expense.date)).toISOString().substring(0, 10),
+ location: expense.location,
+ participants: expense.participants.join(', '),
+ totalSum: expense.totalSum,
+ onSuccess: () => this.refreshExpenses()
+ },
+ 'md'
+ );
+ return;
+ }
- this.modalInstance.show();
+ if (type === 'delete') {
+ this.modalService.open(
+ DayExpensesDeleteFormComponent,
+ this.translate.instant('EXPENSES.MODAL.DELETE_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ id: expense.id,
+ date: (new Date(expense.date)).toISOString().substring(0, 10),
+ location: expense.location,
+ participants: expense.participants.join(', '),
+ totalSum: expense.totalSum,
+ onSuccess: () => this.refreshExpenses()
+ },
+ 'md'
+ );
+ return;
+ }
- const readonly = type === 'delete' || type === 'share';
- setTimeout(() => this.initModalFlatpickr(readonly), 0);
+ if (type === 'share') {
+ this.modalService.open(
+ DayExpensesShareFormComponent,
+ this.translate.instant('EXPENSES.MODAL.SHARE_TITLE'),
+ {
+ currentLocale: this.currentLocale,
+ id: expense.id,
+ date: (new Date(expense.date)).toISOString().substring(0, 10),
+ location: expense.location,
+ participants: expense.participants.join(', '),
+ totalSum: expense.totalSum,
+ onSuccess: () => this.refreshExpenses()
+ },
+ 'md'
+ );
+ return;
+ }
}
- private clearFormData(): void {
- this.date = '';
- this.location = '';
- this.participants = '';
- this.totalSum = 0;
- this.newUserWithAccess = '';
- this.formErrors = {};
- this.formValidated = false;
- this.shareError = '';
+ refreshExpenses(): void {
+ this.loadExpenses();
+ this.tooltipService.destroy();
+ setTimeout(() => this.tooltipService.initialize(), 100);
}
- hideModal() {
- if (this.modalInstance) {
- this.destroyModalFlatpickr();
- this.modalInstance.hide();
- this.clearFormData();
- }
- }
-
// Filtering
filterCriteria: string = 'Location';
isLoading = false;
@@ -388,6 +369,13 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
this.loadExpenses();
}
+ onSortChange(event: { column: string; order: 'asc' | 'desc' }): void {
+ this.sortColumn = event.column as 'date' | 'location' | 'participants' | 'totalSum';
+ this.sortOrder = event.order;
+ this.currentPage = 1;
+ this.loadExpenses();
+ }
+
getIconOrderClass(column: 'date' | 'location' | 'participants' | 'totalSum') {
if (this.sortColumn !== column)
return 'ps-1 bi bi-funnel-fill'
@@ -467,166 +455,6 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
}
// Data modification
- private setDateInputValidation(state: 'valid' | 'invalid' | 'none') {
- const altInput = this.modalFlatpickrInstance?.altInput;
- if (!altInput) return;
-
- altInput.classList.toggle('is-valid', state === 'valid');
- altInput.classList.toggle('is-invalid', state === 'invalid');
- }
-
- private validateDayExpensesForm(): boolean {
- this.formErrors = this.formValidationService.validateDayExpensesForm(this.date, this.participants);
- this.formValidated = true;
-
- // Update date input validation styling
- if (!this.date) {
- this.setDateInputValidation('invalid');
- } else {
- this.setDateInputValidation('valid');
- }
-
- return !this.formValidationService.hasErrors(this.formErrors);
- }
-
- createDayExpenses() {
- if (!this.validateDayExpensesForm()) return;
- this.formValidated = true;
-
- let participantsList = this.participants.split(',').map(p => p.trim())
-
- this.expensesService.createDayExpenses(this.date, this.location, participantsList).subscribe({
- next: (createdDay) => {
- this.hideModal();
- this.toastService.success(
- this.translate.instant('EXPENSES.TOAST.SUCCESS'),
- this.translate.instant('EXPENSES.TOAST.CREATE_SUCCESS'));
- this.router.navigate(['day-expenses-details', createdDay.id]);
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.CREATE_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage));
- }
- }
- })
- }
-
- editDayExpenses() {
- if (!this.validateDayExpensesForm()) return;
- this.formValidated = true;
-
- let participantsList = this.participants.split(',').map(p => p.trim())
-
- this.expensesService.editDayExpenses(this.id, this.date, this.location, participantsList).subscribe({
- next: (updatedDay) => {
- // Update the item in the local list
- const index = this.expensesList.findIndex(e => e.id === this.id);
- if (index !== -1) {
- this.expensesList[index] = updatedDay;
- }
-
- this.hideModal();
- this.toastService.success(
- this.translate.instant('EXPENSES.TOAST.SUCCESS'),
- this.translate.instant('EXPENSES.TOAST.EDIT_SUCCESS'));
-
- // Re-initialize tooltips after data updates
- setTimeout(() => this.tooltipService.initialize(), 0);
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.EDIT_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage));
- }
- }
- })
- }
-
- deleteDayExpenses() {
- this.expensesService.deleteDayExpenses(this.id).subscribe({
- next: () => {
- // Remove the item from the local list
- const index = this.expensesList.findIndex(e => e.id === this.id);
- if (index !== -1) {
- this.expensesList.splice(index, 1);
- }
-
- // If the current page is now empty and we're not on page 1, go to previous page
- if (this.expensesList.length === 0 && this.currentPage > 1) {
- this.currentPage--;
- this.loadExpenses();
- } else if (this.expensesList.length === 0) {
- // If we're on page 1 and it's empty, clear filters to show "no data" view
- this.filterText = '';
- this.fromDate = '';
- this.toDate = '';
- this.currentPage = 1;
- this.totalPages = 0;
- }
-
- this.hideModal();
- this.toastService.success(
- this.translate.instant('EXPENSES.TOAST.SUCCESS'),
- this.translate.instant('EXPENSES.TOAST.DELETE_SUCCESS'));
- },
- error: error =>
- {
- console.log(error);
- const errorMessage = error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.DELETE_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage));
- }
- })
- }
-
- shareDayExpenses() {
- this.formErrors = this.formValidationService.validateShareForm(this.newUserWithAccess);
- this.shareError = '';
- this.formValidated = true;
-
- if (this.formValidationService.hasErrors(this.formErrors)) {
- return;
- }
-
- this.expensesService.shareDayExpenses(this.id, this.newUserWithAccess).subscribe({
- next: (data) => {
- if (data.isSuccess) {
- this.hideModal();
- this.loadExpenses();
- this.shareIsSuccess = false;
- this.toastService.success(
- this.translate.instant('EXPENSES.TOAST.SUCCESS'),
- this.translate.instant('EXPENSES.TOAST.SHARE_SUCCESS'));
- }
- else {
- this.shareIsSuccess = data.isSuccess;
- this.shareError = this.translateBackendError(data.error);
- this.formErrors['newUserWithAccess'] = this.shareError;
- }
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.BACKEND_ERRORS.UNKNOWN_ERROR');
- this.toastService.error(
- this.translate.instant('EXPENSES.TOAST.ERROR'),
- this.translateBackendError(errorMessage));
- }
- }
- })
- }
-
openDetails(id: string) {
// Hide all tooltips before navigating
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
@@ -664,6 +492,7 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
initializeTour() {
const tourSteps: any[] = [];
+ const isSmallScreen = window.innerWidth < 576;
// If there's no data, show only the "Add Expense" step
if (this.expensesList.length === 0) {
@@ -675,7 +504,7 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
enableBackdrop: true
});
} else {
- // If there's data, show the full tour
+ // Common steps for all screen sizes
tourSteps.push(
{
anchorId: 'add-expense-btn',
@@ -690,42 +519,79 @@ export class DayExpensesListComponent implements OnInit, AfterViewInit, OnDestro
title: this.translate.instant('TOUR.DATE_FILTER_TITLE'),
placement: 'bottom',
enableBackdrop: true
- },
- {
- anchorId: 'search-filter',
- content: this.translate.instant('TOUR.SEARCH_FILTER_CONTENT'),
- title: this.translate.instant('TOUR.SEARCH_FILTER_TITLE'),
- placement: 'left',
- enableBackdrop: true
- },
- {
- // Highlights the table header only (tourAnchor on )
- anchorId: 'expenses-table',
- content: this.translate.instant('TOUR.EXPENSES_TABLE_CONTENT'),
- title: this.translate.instant('TOUR.EXPENSES_TABLE_TITLE'),
- placement: 'bottom',
- enableBackdrop: true
- },
- {
- // Highlights the actions menu column
- anchorId: 'actions-menu',
- content: this.translate.instant('TOUR.ACTIONS_MENU_CONTENT'),
- title: this.translate.instant('TOUR.ACTIONS_MENU_TITLE'),
- placement: 'left',
- enableBackdrop: true
- },
- {
- // Highlights pagination controls
- anchorId: 'pagination',
- content: this.translate.instant('TOUR.PAGINATION_CONTENT'),
- title: this.translate.instant('TOUR.PAGINATION_TITLE'),
- placement: 'top',
- enableBackdrop: true
}
);
+
+ // Different steps for small vs large screens
+ if (isSmallScreen) {
+ // Steps for small screens (accordion view)
+ tourSteps.push(
+ {
+ anchorId: 'sort-controls',
+ content: this.translate.instant('TOUR.SORT_CONTROLS_CONTENT'),
+ title: this.translate.instant('TOUR.SORT_CONTROLS_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'expenses-accordion',
+ content: this.translate.instant('TOUR.EXPENSES_ACCORDION_CONTENT'),
+ title: this.translate.instant('TOUR.EXPENSES_ACCORDION_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'pagination',
+ content: this.translate.instant('TOUR.PAGINATION_CONTENT'),
+ title: this.translate.instant('TOUR.PAGINATION_TITLE'),
+ placement: 'top',
+ enableBackdrop: true
+ }
+ );
+ } else {
+ // Steps for large screens (table view)
+ tourSteps.push(
+ {
+ anchorId: 'search-filter',
+ content: this.translate.instant('TOUR.SEARCH_FILTER_CONTENT'),
+ title: this.translate.instant('TOUR.SEARCH_FILTER_TITLE'),
+ placement: 'top',
+ enableBackdrop: true
+ },
+ {
+ // Highlights the table header only (tourAnchor on )
+ anchorId: 'expenses-table',
+ content: this.translate.instant('TOUR.EXPENSES_TABLE_CONTENT'),
+ title: this.translate.instant('TOUR.EXPENSES_TABLE_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ },
+ {
+ // Highlights the actions menu column
+ anchorId: 'actions-menu',
+ content: this.translate.instant('TOUR.ACTIONS_MENU_CONTENT'),
+ title: this.translate.instant('TOUR.ACTIONS_MENU_TITLE'),
+ placement: 'left',
+ enableBackdrop: true
+ },
+ {
+ // Highlights pagination controls
+ anchorId: 'pagination',
+ content: this.translate.instant('TOUR.PAGINATION_CONTENT'),
+ title: this.translate.instant('TOUR.PAGINATION_TITLE'),
+ placement: 'top',
+ enableBackdrop: true
+ }
+ );
+ }
}
- this.tourService.initialize(tourSteps);
+ // Initialize tour with global button title configuration
+ this.tourService.initialize(tourSteps, {
+ prevBtnTitle: this.translate.instant('TOUR.PREV_BTN'),
+ nextBtnTitle: this.translate.instant('TOUR.NEXT_BTN'),
+ endBtnTitle: this.translate.instant('TOUR.END_BTN')
+ });
}
startTour() {
diff --git a/src/ExpensesCalculator.UI/src/app/home/home/home.component.css b/src/ExpensesCalculator.UI/src/app/home/home/home.component.css
index ef421fd..8242e3c 100644
--- a/src/ExpensesCalculator.UI/src/app/home/home/home.component.css
+++ b/src/ExpensesCalculator.UI/src/app/home/home/home.component.css
@@ -619,3 +619,72 @@
font-size: 1rem;
}
}
+
+@media (max-width: 576px) {
+ /* Reduce card padding for small screens */
+ .feature-card,
+ .step-card,
+ .use-case-card,
+ .problem-card,
+ .solution-card {
+ padding: 0.7rem !important;
+ }
+
+ .use-case-example {
+ padding: 0.7rem !important;
+ }
+
+ .tab-content {
+ padding: 0.7rem !important;
+ }
+
+ /* Increase secondary text size */
+ .hero-section .lead,
+ .steps-section .lead,
+ .tabs-section .lead,
+ .use-cases-section .lead,
+ .problem-solution-section .lead {
+ font-size: 1.15rem !important;
+ }
+
+ .feature-card p,
+ .step-card p,
+ .use-case-card p,
+ .problem-card p,
+ .solution-card p {
+ font-size: 1rem !important;
+ }
+
+ .use-case-example small,
+ .text-white-50 {
+ font-size: 0.95rem !important;
+ }
+
+ .feature-list li {
+ font-size: 1.05rem !important;
+ }
+
+ /* Make tab buttons same width */
+ .tab-btn {
+ width: 100% !important;
+ max-width: 100% !important;
+ }
+
+ /* Center problem/solution card content */
+ .problem-card,
+ .solution-card {
+ text-align: center !important;
+ }
+
+ .problem-icon,
+ .solution-icon {
+ margin-left: auto !important;
+ margin-right: auto !important;
+ }
+
+ /* Make btn-lg bigger on small screens */
+ .btn-lg {
+ padding: 0.85rem 2.2rem !important;
+ font-size: 1.15rem !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/home/home/home.component.html b/src/ExpensesCalculator.UI/src/app/home/home/home.component.html
index 1c725fd..f23ec3c 100644
--- a/src/ExpensesCalculator.UI/src/app/home/home/home.component.html
+++ b/src/ExpensesCalculator.UI/src/app/home/home/home.component.html
@@ -5,8 +5,8 @@
OPTION 1: Hero + Feature Grid
======================================== -->
-
-
+
+
-
-
+
+
1
@@ -77,7 +77,7 @@
{{ 'HOME_PAGE.STEPS.STEP1.TITLE' | translate }}
-
+
2
@@ -88,7 +88,7 @@
{{ 'HOME_PAGE.STEPS.STEP2.TITLE' | translate }}
-
+
3
@@ -245,14 +245,14 @@
{{ 'HOME_PAGE.TABS.RECOMMENDATIONS.TITLE' | translate }}
-
+
{{ 'HOME_PAGE.USE_CASES.TITLE' | translate }}
{{ 'HOME_PAGE.USE_CASES.SUBTITLE' | translate }}
-
+
@@ -377,7 +377,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.TITLE' | trans
-
+
@@ -386,7 +386,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P1_TITLE' | translate }
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P1_DESC' | translate }}
-
+
@@ -395,7 +395,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P2_TITLE' | translate }
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P2_DESC' | translate }}
-
+
@@ -404,7 +404,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P3_TITLE' | translate }
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P3_DESC' | translate }}
-
+
@@ -413,7 +413,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P4_TITLE' | translate }
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P4_DESC' | translate }}
-
+
@@ -422,7 +422,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P5_TITLE' | translate }
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P5_DESC' | translate }}
-
+
@@ -447,7 +447,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.PROBLEMS.P6_TITLE' | translate }
-
+
@@ -456,7 +456,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S1_TITLE' | translate
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S1_DESC' | translate }}
-
+
@@ -465,7 +465,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S2_TITLE' | translate
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S2_DESC' | translate }}
-
+
@@ -474,7 +474,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S3_TITLE' | translate
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S3_DESC' | translate }}
-
+
@@ -483,7 +483,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S4_TITLE' | translate
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S4_DESC' | translate }}
-
+
@@ -492,7 +492,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S5_TITLE' | translate
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S5_DESC' | translate }}
-
+
@@ -505,7 +505,7 @@
{{ 'HOME_PAGE.PROBLEM_SOLUTION.SOLUTIONS.S6_TITLE' | translate
-
+
{{ 'HOME_PAGE.PROBLEM_SOLUTION.CTA_TITLE' | translate }}
{{ 'HOME_PAGE.PROBLEM_SOLUTION.CTA_BUTTON' | translate }}
diff --git a/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.css b/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.css
index 2bc1527..5e62bee 100644
--- a/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.css
+++ b/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.css
@@ -188,7 +188,7 @@ td {
/* Highlighted item animation */
.highlighted-item {
border: 2px solid #ffc107 !important;
- animation: highlightPulse 2s ease-in-out 3;
+ animation: highlightPulse 1s ease-in-out 1;
box-shadow: 0 0 20px rgba(255, 193, 7, 0.6);
}
@@ -203,3 +203,37 @@ td {
}
}
+/* Add Item and Filter/Sort button group for small screens */
+.add-filter-group {
+ gap: 0.5rem !important;
+}
+
+.add-filter-group .add-btn,
+.add-filter-group .filter-btn {
+ width: 50% !important;
+ font-size: 0.85rem !important;
+ padding: 0.375rem 0.5rem !important;
+ line-height: 1.2 !important;
+}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ /* Card sizing for small screens */
+ .card-body {
+ padding: 0.5rem !important;
+ }
+
+ .card-title {
+ font-size: 0.85rem !important;
+ }
+
+ .card-body small {
+ font-size: 0.65rem !important;
+ }
+
+ /* Pagination info text sizing */
+ .pagination-info {
+ font-size: 0.8rem !important;
+ }
+}
+
diff --git a/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.html b/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.html
index 78e522a..96f97a5 100644
--- a/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.html
+++ b/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.html
@@ -9,25 +9,69 @@
-
-
0" class="d-flex justify-content-between align-items-center mb-3">
-
-
- {{ 'ITEMS.DATA' | translate }}
-
-
+
+ 0" class="d-none d-lg-flex justify-content-end pb-2 my-3">
+
+
+
+
+ {{ 'ITEMS.DATA' | translate }}
+
+
+ {{ 'ITEMS.ADD' | translate }}
+
+
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}
+
+
+
+
+
+
+ {{ 'ITEMS.SORT.CAPTION' | translate }}
+
+
+
+
+
+
+
+
+
+
+ 0" class="d-block d-lg-none mb-2 px-0">
+
+
{{ 'ITEMS.ADD' | translate }}
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}/{{ 'ITEMS.SORT.CAPTION' | translate }}
+
-
-
-
-
- {{ 'ITEMS.FILTER.CAPTION' | translate }}
-
-
+
+
+
+
+ {{ 'ITEMS.FILTER.CAPTION' | translate }}
+
-
-
-
-
-
- {{ 'ITEMS.SORT.CAPTION' | translate }}
-
-
-
+
+
+ {{ 'ITEMS.SORT.CAPTION' | translate }}
+
+
+
+
+
+
0" class="d-block d-sm-none mb-1">
+
+ {{ 'ITEMS.DATA' | translate }}
+
+
+
{{ 'ITEMS.NO_ITEMS' | translate }}
-
+
+ {{ 'ITEMS.ADD' | translate }}
+
+
{{ 'ITEMS.ADD' | translate }}
@@ -66,10 +121,10 @@
{{ 'ITEMS.NO_SEARCH_RESULTS' | translate }}
0" class="d-flex align-items-center">
-
-
1" class="btn btn-outline-primary text-white me-1"
+
+ 1" class="btn btn-outline-primary text-white me-1 d-none d-md-flex"
(click)="goToPreviousPage()" [disabled]="currentPage === 1"
- style="width: 20px; height: 60px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0;">
+ style="width: 20px; height: 60px; align-items: center; justify-content: center; flex-shrink: 0; padding: 0;">
@@ -86,8 +141,9 @@ {{ 'ITEMS.NO_SEARCH_RESULTS' | translate }}
{{ item.name || '-' }}
-
-
+
+
@@ -100,13 +156,13 @@ {{ 'ITEMS.NO_SEARCH_RESULTS' | translate }}
-
-
{{ item.price | currency:'UAH':'₴' }}
+
{{ item.price | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
×
{{ item.amount }}
=
-
{{ getTotalPrice(item) | currency:'UAH':'₴' }}
+
{{ getTotalPrice(item) | currency:('GENERAL.CURRENCY_CODE' | translate):('GENERAL.CURRENCY_SYMBOL' | translate) }}
{{ 'ITEMS.NO_SEARCH_RESULTS' | translate }}
style="min-height: 10px; font-size: 0.9rem; color: rgba(255, 255, 255, 0.5);">
{{ 'ITEMS.NO_TAGS' | translate }}
+
+
+
+
+ {{ 'ITEMS.MODAL.SAVE' | translate }}
+
+
+ {{ 'ITEMS.MODAL.DELETE' | translate }}
+
+
-
- 1" class="btn btn-outline-primary text-white ms-1" (click)="goToNextPage()"
- [disabled]="currentPage === totalPages"
- style="width: 20px; height: 60px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0;">
+
+ 1" class="btn btn-outline-primary text-white ms-1 d-none d-md-flex"
+ (click)="goToNextPage()" [disabled]="currentPage === totalPages"
+ style="width: 20px; height: 60px; align-items: center; justify-content: center; flex-shrink: 0; padding: 0;">
+
+
0 && totalPages > 1" class="d-flex d-md-none justify-content-center mt-3 mb-2">
+
+
+
+
+
+
+
+
+
+
0 && filteredItemsList.length > 0">
@@ -157,369 +239,4 @@ {{ 'ITEMS.NO_SEARCH_RESULTS' | translate }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0 ? 'col-6' : 'col-12'">
-
-
-
-
-
-
-
-
- {{ 'ITEMS.MODAL.DELETE' | translate }}
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.ts b/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.ts
index 5393a21..d14ba05 100644
--- a/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/items/item-list/item-list.component.ts
@@ -1,26 +1,23 @@
-import { Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, SimpleChanges, inject } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { ModalWindowComponent } from '../../shared/modal-window/modal-window.component';
import { SortBarComponent, SortOption } from '../../shared/sort-bar/sort-bar.component';
import { FilterBarComponent, FilterOption } from '../../shared/filter-bar/filter-bar.component';
-import { ValidationErrors, parseValidationErrors } from '../../shared/models/validation-errors.model';
-import { ItemsService, Item, ItemResponse, DeleteItemResponse } from '../../services/items.service';
-import { DayExpensesTotalSumUpdateService } from '../../services/day-expenses-total-sum-update.service';
-import { ToastService } from '../../services/toast.service';
+import { ItemsService, Item } from '../../services/items.service';
+import { ModalService } from '../../services/modal.service';
import { TooltipService } from '../../services/tooltip.service';
-import { FormValidationService } from '../../services/form-validation.service';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { TourAnchorNgBootstrapDirective } from 'ngx-ui-tour-ng-bootstrap';
-
-declare var bootstrap: any;
+import { ItemAddFormComponent } from '../../modals/item-form/item-add-form/item-add-form.component';
+import { ItemEditFormComponent } from '../../modals/item-form/item-edit-form/item-edit-form.component';
+import { ItemDeleteFormComponent } from '../../modals/item-form/item-delete-form/item-delete-form.component';
@Component({
selector: 'app-item-list',
standalone: true,
- imports: [CommonModule, FormsModule, ModalWindowComponent, SortBarComponent, FilterBarComponent, TranslatePipe, TourAnchorNgBootstrapDirective],
+ imports: [CommonModule, FormsModule, SortBarComponent, FilterBarComponent, TranslatePipe, TourAnchorNgBootstrapDirective],
templateUrl: './item-list.component.html',
styleUrl: './item-list.component.css'
})
@@ -32,6 +29,7 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
@Input() isLoading?: boolean; // Optional: external loading state
@Input() highlightedItemId?: string;
@Input() disableTooltipManagement: boolean = false; // When true, parent handles tooltips
+ @Input() isFirstCheck: boolean = false; // Indicates if this is the first check (for tour anchor)
@Output() checkSumUpdated = new EventEmitter<{ checkId: string, newSum: number }>();
@Output() itemsChanged = new EventEmitter
(); // Emit when items are modified (checkId)
@@ -47,27 +45,6 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
itemsPerPage = 8;
totalPages = 1;
- // Modal properties
- modalInstance: any;
- currentModalContent: 'add' | 'edit' | 'delete' = 'add';
- modalTitle: string = '';
-
- // Form properties
- id = '';
- name = '';
- comment = '';
- price = 0;
- amount = 1;
- rating = 0;
- hoverRating = 0;
- tags: string[] = [];
- tagInput = '';
- selectedUsers: string[] = [];
-
- // Validation properties
- formErrors: ValidationErrors = {};
- formValidated = false;
-
// Filter and sort properties
filterText = '';
filterCriteria: string = 'Name';
@@ -103,15 +80,11 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
private filterTextSubject = new Subject();
private filterTextSubscription!: Subscription;
- // Inject services using inject() for better SSR compatibility
- private dayExpensesTotalSumUpdateService = inject(DayExpensesTotalSumUpdateService);
-
constructor(
private itemsService: ItemsService,
private translate: TranslateService,
- private toastService: ToastService,
- private tooltipService: TooltipService,
- private formValidationService: FormValidationService
+ private modalService: ModalService,
+ private tooltipService: TooltipService
) {}
ngOnInit(): void {
@@ -123,6 +96,9 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
this.applyLocalFiltering();
});
+ // Set items per page based on screen size
+ this.setItemsPerPageByScreenSize();
+
// If items are provided, use them; otherwise load them
if (this.items && this.items.length > 0) {
this.itemsList = this.items;
@@ -203,19 +179,6 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
});
}
- loadSingleItem(id: string): void {
- const item = this.itemsList.find(i => i.id === id);
- if (item) {
- this.id = item.id;
- this.name = item.name;
- this.comment = item.comment || '';
- this.price = item.price;
- this.amount = item.amount;
- this.rating = item.rating;
- this.tags = [...item.tags];
- this.selectedUsers = [...item.users];
- }
- }
// Local filtering and sorting methods
onFilterChange(filterText: string): void {
@@ -354,15 +317,10 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
scrollToHighlightedItem(): void {
if (!this.highlightedItemId) return;
- const itemElement = document.querySelector(`.highlighted-item`);
- if (itemElement) {
- itemElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
-
- // Remove highlight after animation completes (2s * 3 iterations = 6s)
- setTimeout(() => {
- this.highlightedItemId = undefined;
- }, 6000);
- }
+ // Remove highlight after animation completes (1s * 1 iteration = 1s)
+ setTimeout(() => {
+ this.highlightedItemId = undefined;
+ }, 1000);
}
goToNextPage(): void {
@@ -424,244 +382,112 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
// Modal management methods
openModal(type: 'add' | 'edit' | 'delete', id: string = ''): void {
- this.currentModalContent = type;
- this.modalTitle = this.translate.instant(`ITEMS.MODAL.${type.toUpperCase()}_TITLE`);
-
- const modalElement = document.getElementById('itemsModal-' + this.checkId);
- if (!modalElement) return;
-
if (type === 'add') {
- this.clearFormData();
- } else if (id) {
- this.loadSingleItem(id);
+ this.modalService.open(
+ ItemAddFormComponent,
+ this.translate.instant('ITEMS.MODAL.ADD_TITLE'),
+ {
+ users: this.users,
+ checkId: this.checkId,
+ dayExpensesId: this.dayExpensesId,
+ onSuccess: (checkId: string, newSum: number, dayExpensesTotalSum: number) =>
+ this.onItemCreated(checkId, newSum, dayExpensesTotalSum)
+ },
+ 'lg'
+ );
+ return;
}
- if (!this.modalInstance) {
- this.modalInstance = new bootstrap.Modal(modalElement, {
- backdrop: 'static',
- keyboard: false
- });
+ if (type === 'edit') {
+ const item = this.itemsList.find(i => i.id === id);
+ if (!item) return;
+
+ this.modalService.open(
+ ItemEditFormComponent,
+ this.translate.instant('ITEMS.MODAL.EDIT_TITLE'),
+ {
+ users: this.users,
+ checkId: this.checkId,
+ dayExpensesId: this.dayExpensesId,
+ itemId: item.id,
+ name: item.name,
+ comment: item.comment || '',
+ price: item.price,
+ amount: item.amount,
+ rating: item.rating,
+ tags: [...item.tags],
+ selectedUsers: [...item.users],
+ onSuccess: (checkId: string, newSum: number, dayExpensesTotalSum: number) =>
+ this.onItemUpdated(checkId, newSum, dayExpensesTotalSum)
+ },
+ 'lg'
+ );
+ return;
}
- this.formErrors = {};
- this.formValidated = false;
-
- this.modalInstance.show();
- }
-
- hideModal(): void {
- if (this.modalInstance) {
- this.modalInstance.hide();
- this.formErrors = {};
- this.formValidated = false;
- this.clearFormData();
+ if (type === 'delete') {
+ const item = this.itemsList.find(i => i.id === id);
+ if (!item) return;
+
+ this.modalService.open(
+ ItemDeleteFormComponent,
+ this.translate.instant('ITEMS.MODAL.DELETE_TITLE'),
+ {
+ checkId: this.checkId,
+ dayExpensesId: this.dayExpensesId,
+ itemId: item.id,
+ name: item.name,
+ comment: item.comment || '',
+ price: item.price,
+ amount: item.amount,
+ rating: item.rating,
+ tags: [...item.tags],
+ selectedUsers: [...item.users],
+ onSuccess: (checkId: string, newSum: number, dayExpensesTotalSum: number) =>
+ this.onItemDeleted(checkId, newSum, dayExpensesTotalSum)
+ },
+ 'lg'
+ );
+ return;
}
}
- clearFormData(): void {
- this.id = '';
- this.name = '';
- this.comment = '';
- this.price = 0;
- this.amount = 1;
- this.rating = 0;
- this.hoverRating = 0;
- this.tags = [];
- this.tagInput = '';
- this.selectedUsers = [];
- }
-
- // Rating methods
- setRating(rating: number): void {
- this.rating = rating;
- }
+ onItemCreated(checkId: string, newSum: number, _dayExpensesTotalSum: number): void {
+ // Emit check sum update
+ this.checkSumUpdated.emit({ checkId: checkId, newSum: newSum });
- // User selection methods
- onUserSelectionChange(user: string, event: any): void {
- if (event.target.checked) {
- if (!this.selectedUsers.includes(user)) {
- this.selectedUsers.push(user);
- }
+ // If items are provided from parent, emit event; otherwise reload
+ if (this.items !== undefined) {
+ this.itemsChanged.emit(this.checkId);
} else {
- const index = this.selectedUsers.indexOf(user);
- if (index > -1) {
- this.selectedUsers.splice(index, 1);
- }
+ this.loadItems();
}
}
- isAllUsersSelected(): boolean {
- return this.users.length > 0 && this.selectedUsers.length === this.users.length;
- }
+ onItemUpdated(checkId: string, newSum: number, _dayExpensesTotalSum: number): void {
+ // Emit check sum update
+ this.checkSumUpdated.emit({ checkId: checkId, newSum: newSum });
- toggleAllUsers(event: any): void {
- if (event.target.checked) {
- // Select all users
- this.selectedUsers = [...this.users];
+ // If items are provided from parent, emit event; otherwise reload
+ if (this.items !== undefined) {
+ this.itemsChanged.emit(this.checkId);
} else {
- // Deselect all users
- this.selectedUsers = [];
+ this.loadItems();
}
}
- // Tag management methods
- addTag(): void {
- const trimmedTag = this.tagInput.trim().replace(/\s+/g, '_').toLowerCase();
- if (trimmedTag && !this.tags.includes(trimmedTag)) {
- this.tags.push(trimmedTag);
- this.tagInput = '';
- }
- }
+ onItemDeleted(checkId: string, newSum: number, _dayExpensesTotalSum: number): void {
+ // Emit check sum update
+ this.checkSumUpdated.emit({ checkId: checkId, newSum: newSum });
- removeTag(tag: string): void {
- const index = this.tags.indexOf(tag);
- if (index > -1) {
- this.tags.splice(index, 1);
+ // If items are provided from parent, emit event; otherwise reload
+ if (this.items !== undefined) {
+ this.itemsChanged.emit(this.checkId);
+ } else {
+ this.loadItems();
}
}
- // CRUD operations
- validateItemForm(): boolean {
- this.formErrors = this.formValidationService.validateItemForm(
- this.name,
- this.price,
- this.amount,
- this.rating,
- this.selectedUsers
- );
- this.formValidated = true;
-
- return !this.formValidationService.hasErrors(this.formErrors);
- }
-
- createItem(): void {
- if (!this.validateItemForm()) return;
- this.formValidated = true;
-
- const newItem: Item = {
- id: '00000000-0000-0000-0000-000000000000',
- name: this.name,
- comment: this.comment,
- price: this.price,
- amount: this.amount,
- rating: this.rating,
- tags: this.tags,
- users: this.selectedUsers,
- checkId: this.checkId
- };
-
- this.itemsService.createItem(newItem).subscribe({
- next: (response: ItemResponse) => {
- this.hideModal();
- // Emit check sum update
- this.checkSumUpdated.emit({ checkId: this.checkId, newSum: response.checkTotalSum });
- // If items are provided from parent, emit event; otherwise reload
- if (this.items !== undefined) {
- this.itemsChanged.emit(this.checkId);
- } else {
- this.loadItems();
- }
- this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(this.dayExpensesId, response.dayExpensesTotalSum);
- this.toastService.success(
- this.translate.instant('ITEMS.TOAST.SUCCESS'),
- this.translate.instant('ITEMS.TOAST.CREATE_SUCCESS')
- );
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.CREATE_ERROR');
- this.toastService.error(
- this.translate.instant('ITEMS.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- }
- });
- }
-
- editItem(): void {
- if (!this.validateItemForm()) return;
- this.formValidated = true;
-
- const updatedItem: Item = {
- id: this.id,
- name: this.name,
- comment: this.comment,
- price: this.price,
- amount: this.amount,
- rating: this.rating,
- tags: this.tags,
- users: this.selectedUsers,
- checkId: this.checkId
- };
-
- this.itemsService.editItem(updatedItem).subscribe({
- next: (response: ItemResponse) => {
- this.hideModal();
- // Emit check sum update
- this.checkSumUpdated.emit({ checkId: this.checkId, newSum: response.checkTotalSum });
- // If items are provided from parent, emit event; otherwise reload
- if (this.items !== undefined) {
- this.itemsChanged.emit(this.checkId);
- } else {
- this.loadItems();
- }
- this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(this.dayExpensesId, response.dayExpensesTotalSum);
- this.toastService.success(
- this.translate.instant('ITEMS.TOAST.SUCCESS'),
- this.translate.instant('ITEMS.TOAST.EDIT_SUCCESS')
- );
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.EDIT_ERROR');
- this.toastService.error(
- this.translate.instant('ITEMS.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- }
- });
- }
-
- deleteItem(): void {
- this.itemsService.deleteItem(this.id).subscribe({
- next: (response: DeleteItemResponse) => {
- // Remove the item from the local list
- const index = this.itemsList.findIndex(i => i.id === this.id);
- if (index !== -1) {
- this.itemsList.splice(index, 1);
- this.applyLocalFiltering();
- }
-
- this.hideModal();
- // Emit check sum update
- this.checkSumUpdated.emit({ checkId: this.checkId, newSum: response.checkTotalSum });
- // If items are provided from parent, emit event
- if (this.items !== undefined) {
- this.itemsChanged.emit(this.checkId);
- }
- this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(this.dayExpensesId, response.dayExpensesTotalSum);
- this.toastService.success(
- this.translate.instant('ITEMS.TOAST.SUCCESS'),
- this.translate.instant('ITEMS.TOAST.DELETE_SUCCESS')
- );
- },
- error: error => {
- console.log(error);
- const errorMessage = error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.DELETE_ERROR');
- this.toastService.error(
- this.translate.instant('ITEMS.TOAST.ERROR'),
- this.translateBackendError(errorMessage)
- );
- }
- });
- }
-
// Helper methods
translateBackendError(errorMessage: string): string {
if (!errorMessage) return '';
@@ -718,4 +544,13 @@ export class ItemListComponent implements OnInit, OnChanges, OnDestroy, AfterVie
if (this.filteredItemsList.length === 0) return 0;
return Math.min(this.currentPage * this.itemsPerPage, this.filteredItemsList.length);
}
+
+ setItemsPerPageByScreenSize(): void {
+ // Bootstrap's md breakpoint is 768px
+ if (window.innerWidth < 768) {
+ this.itemsPerPage = 3;
+ } else {
+ this.itemsPerPage = 8;
+ }
+ }
}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/check-form/check-add-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-add-form.component.html
new file mode 100644
index 0000000..d5a82c6
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-add-form.component.html
@@ -0,0 +1,43 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/check-form/check-add-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-add-form.component.ts
new file mode 100644
index 0000000..d0f7a94
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-add-form.component.ts
@@ -0,0 +1,95 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../services/modal.service';
+import { ChecksService } from '../../services/checks.service';
+import { ToastService } from '../../services/toast.service';
+import { parseValidationErrors } from '../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-check-add-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './check-add-form.component.html'
+})
+export class CheckAddFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ participants: string[] = [];
+ dayExpensesId: string = '';
+
+ // Form fields
+ location: string = '';
+ payer: string = '';
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private checksService: ChecksService,
+ private toastService: ToastService
+ ) {}
+
+ validateForm(): boolean {
+ this.formErrors = {};
+
+ if (!this.location.trim()) {
+ this.formErrors['location'] = this.translate.instant('CHECKS.MODAL.LOCATION_REQUIRED');
+ }
+
+ if (!this.payer) {
+ this.formErrors['payer'] = this.translate.instant('CHECKS.MODAL.PAYER_REQUIRED');
+ }
+
+ return Object.keys(this.formErrors).length === 0;
+ }
+
+ submit(): void {
+ if (!this.validateForm()) {
+ this.formValidated = true;
+ return;
+ }
+
+ this.formValidated = true;
+
+ this.checksService.createCheck(this.location, this.payer, this.dayExpensesId).subscribe({
+ next: (createdCheck) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('CHECKS.TOAST.SUCCESS'),
+ this.translate.instant('CHECKS.TOAST.CREATE_SUCCESS')
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('CHECKS.TOAST.CREATE_ERROR');
+ this.toastService.error(
+ this.translate.instant('CHECKS.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/check-form/check-delete-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-delete-form.component.html
new file mode 100644
index 0000000..5083521
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-delete-form.component.html
@@ -0,0 +1,20 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/check-form/check-delete-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-delete-form.component.ts
new file mode 100644
index 0000000..d67290e
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-delete-form.component.ts
@@ -0,0 +1,72 @@
+import { Component, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../services/modal.service';
+import { ChecksService, DeleteCheckResponse } from '../../services/checks.service';
+import { ToastService } from '../../services/toast.service';
+import { DayExpensesTotalSumUpdateService } from '../../services/day-expenses-total-sum-update.service';
+
+@Component({
+ selector: 'app-check-delete-form',
+ standalone: true,
+ imports: [CommonModule, TranslatePipe],
+ templateUrl: './check-delete-form.component.html'
+})
+export class CheckDeleteFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ checkId: string = '';
+ dayExpensesId: string = '';
+ location: string = '';
+ payer: string = '';
+ totalSum: number = 0;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ private dayExpensesTotalSumUpdateService = inject(DayExpensesTotalSumUpdateService);
+
+ constructor(
+ private translate: TranslateService,
+ private checksService: ChecksService,
+ private toastService: ToastService
+ ) {}
+
+ submit(): void {
+ this.checksService.deleteCheck(this.checkId).subscribe({
+ next: (response: DeleteCheckResponse) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('CHECKS.TOAST.SUCCESS'),
+ this.translate.instant('CHECKS.TOAST.DELETE_SUCCESS')
+ );
+
+ // Emit day expenses total sum update
+ this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(
+ this.dayExpensesId,
+ response.dayExpensesTotalSum
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: error => {
+ const errorMessage = error?.error?.message || error?.message || this.translate.instant('CHECKS.TOAST.DELETE_ERROR');
+ this.toastService.error(
+ this.translate.instant('CHECKS.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/check-form/check-edit-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-edit-form.component.html
new file mode 100644
index 0000000..1cf7678
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-edit-form.component.html
@@ -0,0 +1,43 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/check-form/check-edit-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-edit-form.component.ts
new file mode 100644
index 0000000..4cc4e02
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/check-form/check-edit-form.component.ts
@@ -0,0 +1,95 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../services/modal.service';
+import { ChecksService } from '../../services/checks.service';
+import { ToastService } from '../../services/toast.service';
+import { parseValidationErrors } from '../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-check-edit-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './check-edit-form.component.html'
+})
+export class CheckEditFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ participants: string[] = [];
+ checkId: string = '';
+
+ // Form fields
+ location: string = '';
+ payer: string = '';
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private checksService: ChecksService,
+ private toastService: ToastService
+ ) {}
+
+ validateForm(): boolean {
+ this.formErrors = {};
+
+ if (!this.location.trim()) {
+ this.formErrors['location'] = this.translate.instant('CHECKS.MODAL.LOCATION_REQUIRED');
+ }
+
+ if (!this.payer) {
+ this.formErrors['payer'] = this.translate.instant('CHECKS.MODAL.PAYER_REQUIRED');
+ }
+
+ return Object.keys(this.formErrors).length === 0;
+ }
+
+ submit(): void {
+ if (!this.validateForm()) {
+ this.formValidated = true;
+ return;
+ }
+
+ this.formValidated = true;
+
+ this.checksService.editCheck(this.checkId, this.location, this.payer).subscribe({
+ next: (updatedCheck) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('CHECKS.TOAST.SUCCESS'),
+ this.translate.instant('CHECKS.TOAST.EDIT_SUCCESS')
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('CHECKS.TOAST.EDIT_ERROR');
+ this.toastService.error(
+ this.translate.instant('CHECKS.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-add-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-add-form.component.html
new file mode 100644
index 0000000..2d20ea7
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-add-form.component.html
@@ -0,0 +1,29 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-add-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-add-form.component.ts
new file mode 100644
index 0000000..dbf1f6a
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-add-form.component.ts
@@ -0,0 +1,122 @@
+import { Component, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { Router } from '@angular/router';
+import { ModalService } from '../../services/modal.service';
+import { ExpensesService } from '../../services/expenses.service';
+import { ToastService } from '../../services/toast.service';
+import { FormValidationService } from '../../services/form-validation.service';
+import { DateRangeService } from '../../services/date-range.service';
+import { parseValidationErrors } from '../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-day-expenses-add-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './day-expenses-add-form.component.html'
+})
+export class DayExpensesAddFormComponent implements OnDestroy {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ currentLocale: string = 'en';
+
+ // Form fields
+ date: string = '';
+ location: string = '';
+ participants: string = '';
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Flatpickr instance
+ private modalFlatpickrInstance: any;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private expensesService: ExpensesService,
+ private toastService: ToastService,
+ private formValidationService: FormValidationService,
+ private dateRangeService: DateRangeService,
+ private router: Router
+ ) {
+ // Initialize flatpickr after a short delay to ensure DOM is ready
+ setTimeout(() => this.initModalFlatpickr(), 0);
+ }
+
+ ngOnDestroy(): void {
+ this.destroyModalFlatpickr();
+ }
+
+ initModalFlatpickr() {
+ this.destroyModalFlatpickr();
+
+ this.modalFlatpickrInstance = this.dateRangeService.initializeSingleDatePicker(
+ 'modalDateInput',
+ {
+ defaultDate: this.date || undefined,
+ onChange: (dates: Date[]) => {
+ this.date = this.dateRangeService.formatDate(dates[0]);
+ }
+ }
+ );
+ }
+
+ destroyModalFlatpickr() {
+ this.dateRangeService.destroy(this.modalFlatpickrInstance);
+ this.modalFlatpickrInstance = null;
+ }
+
+ validateForm(): boolean {
+ this.formErrors = {};
+
+ this.formErrors = this.formValidationService.validateDayExpensesForm(this.date, this.participants);
+
+ return !this.formValidationService.hasErrors(this.formErrors);
+ }
+
+ submit(): void {
+ if (!this.validateForm()) {
+ this.formValidated = true;
+ return;
+ }
+
+ this.formValidated = true;
+
+ const participantsList = this.participants.split(',').map(p => p.trim());
+
+ this.expensesService.createDayExpenses(this.date, this.location, participantsList).subscribe({
+ next: (createdDay) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('EXPENSES.TOAST.SUCCESS'),
+ this.translate.instant('EXPENSES.TOAST.CREATE_SUCCESS')
+ );
+ this.router.navigate(['day-expenses-details', createdDay.id]);
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.CREATE_ERROR');
+ this.toastService.error(
+ this.translate.instant('EXPENSES.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-delete-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-delete-form.component.html
new file mode 100644
index 0000000..cec3b14
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-delete-form.component.html
@@ -0,0 +1,25 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-delete-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-delete-form.component.ts
new file mode 100644
index 0000000..ebdb9b0
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-delete-form.component.ts
@@ -0,0 +1,94 @@
+import { Component, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../services/modal.service';
+import { ExpensesService } from '../../services/expenses.service';
+import { ToastService } from '../../services/toast.service';
+import { DateRangeService } from '../../services/date-range.service';
+
+@Component({
+ selector: 'app-day-expenses-delete-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './day-expenses-delete-form.component.html'
+})
+export class DayExpensesDeleteFormComponent implements OnDestroy {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ currentLocale: string = 'en';
+ id: string = '';
+ date: string = '';
+ location: string = '';
+ participants: string = '';
+ totalSum: number = 0;
+
+ // Flatpickr instance
+ private modalFlatpickrInstance: any;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private expensesService: ExpensesService,
+ private toastService: ToastService,
+ private dateRangeService: DateRangeService
+ ) {
+ // Initialize flatpickr after a short delay to ensure DOM is ready
+ setTimeout(() => this.initModalFlatpickr(), 0);
+ }
+
+ ngOnDestroy(): void {
+ this.destroyModalFlatpickr();
+ }
+
+ initModalFlatpickr() {
+ this.destroyModalFlatpickr();
+
+ this.modalFlatpickrInstance = this.dateRangeService.initializeSingleDatePicker(
+ 'modalDateInput',
+ {
+ defaultDate: this.date || undefined,
+ readonly: true
+ }
+ );
+ }
+
+ destroyModalFlatpickr() {
+ this.dateRangeService.destroy(this.modalFlatpickrInstance);
+ this.modalFlatpickrInstance = null;
+ }
+
+ submit(): void {
+ this.expensesService.deleteDayExpenses(this.id).subscribe({
+ next: () => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('EXPENSES.TOAST.SUCCESS'),
+ this.translate.instant('EXPENSES.TOAST.DELETE_SUCCESS')
+ );
+
+ // Call success callback
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: error => {
+ const errorMessage = error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.DELETE_ERROR');
+ this.toastService.error(
+ this.translate.instant('EXPENSES.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-edit-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-edit-form.component.html
new file mode 100644
index 0000000..030565f
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-edit-form.component.html
@@ -0,0 +1,34 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-edit-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-edit-form.component.ts
new file mode 100644
index 0000000..978db91
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-edit-form.component.ts
@@ -0,0 +1,124 @@
+import { Component, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../services/modal.service';
+import { ExpensesService } from '../../services/expenses.service';
+import { ToastService } from '../../services/toast.service';
+import { FormValidationService } from '../../services/form-validation.service';
+import { DateRangeService } from '../../services/date-range.service';
+import { parseValidationErrors } from '../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-day-expenses-edit-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './day-expenses-edit-form.component.html'
+})
+export class DayExpensesEditFormComponent implements OnDestroy {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ currentLocale: string = 'en';
+ id: string = '';
+ date: string = '';
+ location: string = '';
+ participants: string = '';
+ totalSum: number = 0;
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Flatpickr instance
+ private modalFlatpickrInstance: any;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private expensesService: ExpensesService,
+ private toastService: ToastService,
+ private formValidationService: FormValidationService,
+ private dateRangeService: DateRangeService
+ ) {
+ // Initialize flatpickr after a short delay to ensure DOM is ready
+ setTimeout(() => this.initModalFlatpickr(), 0);
+ }
+
+ ngOnDestroy(): void {
+ this.destroyModalFlatpickr();
+ }
+
+ initModalFlatpickr() {
+ this.destroyModalFlatpickr();
+
+ this.modalFlatpickrInstance = this.dateRangeService.initializeSingleDatePicker(
+ 'modalDateInput',
+ {
+ defaultDate: this.date || undefined,
+ onChange: (dates: Date[]) => {
+ this.date = this.dateRangeService.formatDate(dates[0]);
+ }
+ }
+ );
+ }
+
+ destroyModalFlatpickr() {
+ this.dateRangeService.destroy(this.modalFlatpickrInstance);
+ this.modalFlatpickrInstance = null;
+ }
+
+ validateForm(): boolean {
+ this.formErrors = {};
+
+ this.formErrors = this.formValidationService.validateDayExpensesForm(this.date, this.participants);
+
+ return !this.formValidationService.hasErrors(this.formErrors);
+ }
+
+ submit(): void {
+ if (!this.validateForm()) {
+ this.formValidated = true;
+ return;
+ }
+
+ this.formValidated = true;
+
+ const participantsList = this.participants.split(',').map(p => p.trim());
+
+ this.expensesService.editDayExpenses(this.id, this.date, this.location, participantsList).subscribe({
+ next: (updatedDay) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('EXPENSES.TOAST.SUCCESS'),
+ this.translate.instant('EXPENSES.TOAST.EDIT_SUCCESS')
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.TOAST.EDIT_ERROR');
+ this.toastService.error(
+ this.translate.instant('EXPENSES.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-share-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-share-form.component.html
new file mode 100644
index 0000000..7895531
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-share-form.component.html
@@ -0,0 +1,31 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-share-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-share-form.component.ts
new file mode 100644
index 0000000..2da9789
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/day-expenses-form/day-expenses-share-form.component.ts
@@ -0,0 +1,123 @@
+import { Component, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../services/modal.service';
+import { ExpensesService } from '../../services/expenses.service';
+import { ToastService } from '../../services/toast.service';
+import { FormValidationService } from '../../services/form-validation.service';
+import { DateRangeService } from '../../services/date-range.service';
+import { parseValidationErrors } from '../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-day-expenses-share-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './day-expenses-share-form.component.html'
+})
+export class DayExpensesShareFormComponent implements OnDestroy {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ currentLocale: string = 'en';
+ id: string = '';
+ date: string = '';
+ location: string = '';
+ participants: string = '';
+ totalSum: number = 0;
+
+ // Share functionality
+ newUserWithAccess: string = '';
+ shareError: string = '';
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Flatpickr instance
+ private modalFlatpickrInstance: any;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private expensesService: ExpensesService,
+ private toastService: ToastService,
+ private formValidationService: FormValidationService,
+ private dateRangeService: DateRangeService
+ ) {
+ // Initialize flatpickr after a short delay to ensure DOM is ready
+ setTimeout(() => this.initModalFlatpickr(), 0);
+ }
+
+ ngOnDestroy(): void {
+ this.destroyModalFlatpickr();
+ }
+
+ initModalFlatpickr() {
+ this.destroyModalFlatpickr();
+
+ this.modalFlatpickrInstance = this.dateRangeService.initializeSingleDatePicker(
+ 'modalDateInput',
+ {
+ defaultDate: this.date || undefined,
+ readonly: true
+ }
+ );
+ }
+
+ destroyModalFlatpickr() {
+ this.dateRangeService.destroy(this.modalFlatpickrInstance);
+ this.modalFlatpickrInstance = null;
+ }
+
+ submit(): void {
+ this.formErrors = {};
+ this.shareError = '';
+ this.formValidated = true;
+
+ this.formErrors = this.formValidationService.validateShareForm(this.newUserWithAccess);
+ if (this.formValidationService.hasErrors(this.formErrors)) {
+ return;
+ }
+
+ this.expensesService.shareDayExpenses(this.id, this.newUserWithAccess).subscribe({
+ next: (data) => {
+ if (data.isSuccess) {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('EXPENSES.TOAST.SUCCESS'),
+ this.translate.instant('EXPENSES.TOAST.SHARE_SUCCESS')
+ );
+
+ // Call success callback to refresh data
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ } else {
+ this.shareError = this.translateBackendError(data.error);
+ this.formErrors['newUserWithAccess'] = this.shareError;
+ }
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('EXPENSES.BACKEND_ERRORS.UNKNOWN_ERROR');
+ this.toastService.error(
+ this.translate.instant('EXPENSES.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/item-form/item-add-form/item-add-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-add-form/item-add-form.component.html
new file mode 100644
index 0000000..aa66ebe
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-add-form/item-add-form.component.html
@@ -0,0 +1,141 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/item-form/item-add-form/item-add-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-add-form/item-add-form.component.ts
new file mode 100644
index 0000000..dbe3a0b
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-add-form/item-add-form.component.ts
@@ -0,0 +1,175 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../../services/modal.service';
+import { ItemsService, Item, ItemResponse } from '../../../services/items.service';
+import { ToastService } from '../../../services/toast.service';
+import { FormValidationService } from '../../../services/form-validation.service';
+import { DayExpensesTotalSumUpdateService } from '../../../services/day-expenses-total-sum-update.service';
+import { parseValidationErrors } from '../../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-item-add-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './item-add-form.component.html'
+})
+export class ItemAddFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ private _users: string[] = [];
+ get users(): string[] {
+ return this._users;
+ }
+ set users(value: string[]) {
+ this._users = value;
+ // Auto-select all users when they are set
+ this.selectedUsers = [...value];
+ }
+
+ checkId: string = '';
+ dayExpensesId: string = '';
+
+ // Form fields
+ name: string = '';
+ comment: string = '';
+ price: number | null = null;
+ amount: number = 1;
+ rating: number = 5;
+ hoverRating: number = 0;
+ tags: string[] = [];
+ tagInput: string = '';
+ selectedUsers: string[] = [];
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Callback functions passed from parent
+ onSuccess?: (checkId: string, newSum: number, dayExpensesTotalSum: number) => void;
+
+ constructor(
+ private translate: TranslateService,
+ private itemsService: ItemsService,
+ private toastService: ToastService,
+ private formValidationService: FormValidationService,
+ private dayExpensesTotalSumUpdateService: DayExpensesTotalSumUpdateService
+ ) {}
+
+ setRating(rating: number): void {
+ this.rating = rating;
+ }
+
+ addTag(): void {
+ const trimmedTag = this.tagInput.trim().replace(/\s+/g, '_').toLowerCase();
+ if (trimmedTag && !this.tags.includes(trimmedTag) && this.tags.length < 5) {
+ this.tags.push(trimmedTag);
+ this.tagInput = '';
+ }
+ }
+
+ removeTag(tag: string): void {
+ const index = this.tags.indexOf(tag);
+ if (index > -1) {
+ this.tags.splice(index, 1);
+ }
+ }
+
+ isAllUsersSelected(): boolean {
+ return this.users.length > 0 && this.selectedUsers.length === this.users.length;
+ }
+
+ toggleAllUsers(event: any): void {
+ if (event.target.checked) {
+ this.selectedUsers = [...this.users];
+ } else {
+ this.selectedUsers = [];
+ }
+ }
+
+ onUserSelectionChange(user: string, event: any): void {
+ if (event.target.checked) {
+ if (!this.selectedUsers.includes(user)) {
+ this.selectedUsers.push(user);
+ }
+ } else {
+ const index = this.selectedUsers.indexOf(user);
+ if (index > -1) {
+ this.selectedUsers.splice(index, 1);
+ }
+ }
+ }
+
+ validateForm(): boolean {
+ this.formErrors = this.formValidationService.validateItemForm(
+ this.name,
+ this.price,
+ this.amount,
+ this.rating,
+ this.selectedUsers
+ );
+ return Object.keys(this.formErrors).length === 0;
+ }
+
+ submit(): void {
+ if (!this.validateForm()) {
+ this.formValidated = true;
+ return;
+ }
+
+ this.formValidated = true;
+
+ const newItem: Item = {
+ id: '00000000-0000-0000-0000-000000000000',
+ name: this.name,
+ comment: this.comment,
+ price: this.price!,
+ amount: this.amount,
+ rating: this.rating,
+ tags: this.tags,
+ users: this.selectedUsers,
+ checkId: this.checkId
+ };
+
+ this.itemsService.createItem(newItem).subscribe({
+ next: (response: ItemResponse) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('ITEMS.TOAST.SUCCESS'),
+ this.translate.instant('ITEMS.TOAST.CREATE_SUCCESS')
+ );
+
+ // Update day expenses total sum
+ this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(
+ this.dayExpensesId,
+ response.dayExpensesTotalSum
+ );
+
+ // Call success callback to refresh the list and update check sum
+ if (this.onSuccess) {
+ this.onSuccess(this.checkId, response.checkTotalSum, response.dayExpensesTotalSum);
+ }
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.CREATE_ERROR');
+ this.toastService.error(
+ this.translate.instant('ITEMS.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/item-form/item-delete-form/item-delete-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-delete-form/item-delete-form.component.html
new file mode 100644
index 0000000..b5a9af4
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-delete-form/item-delete-form.component.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0 ? 'col-6' : 'col-12'">
+
+
+
+
+
+
+
+
+
+ {{ 'ITEMS.MODAL.DELETE' | translate }}
+
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/item-form/item-delete-form/item-delete-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-delete-form/item-delete-form.component.ts
new file mode 100644
index 0000000..ea7c16c
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-delete-form/item-delete-form.component.ts
@@ -0,0 +1,79 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../../services/modal.service';
+import { ItemsService, DeleteItemResponse } from '../../../services/items.service';
+import { ToastService } from '../../../services/toast.service';
+import { DayExpensesTotalSumUpdateService } from '../../../services/day-expenses-total-sum-update.service';
+
+@Component({
+ selector: 'app-item-delete-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './item-delete-form.component.html'
+})
+export class ItemDeleteFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ checkId: string = '';
+ dayExpensesId: string = '';
+ itemId: string = '';
+
+ // Display fields
+ name: string = '';
+ comment: string = '';
+ price: number = 0;
+ amount: number = 1;
+ rating: number = 5;
+ tags: string[] = [];
+ selectedUsers: string[] = [];
+
+ // Callback functions passed from parent
+ onSuccess?: (checkId: string, newSum: number, dayExpensesTotalSum: number) => void;
+
+ constructor(
+ private translate: TranslateService,
+ private itemsService: ItemsService,
+ private toastService: ToastService,
+ private dayExpensesTotalSumUpdateService: DayExpensesTotalSumUpdateService
+ ) {}
+
+ submit(): void {
+ this.itemsService.deleteItem(this.itemId).subscribe({
+ next: (response: DeleteItemResponse) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('ITEMS.TOAST.SUCCESS'),
+ this.translate.instant('ITEMS.TOAST.DELETE_SUCCESS')
+ );
+
+ // Update day expenses total sum
+ this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(
+ this.dayExpensesId,
+ response.dayExpensesTotalSum
+ );
+
+ // Call success callback to refresh the list and update check sum
+ if (this.onSuccess) {
+ this.onSuccess(this.checkId, response.checkTotalSum, response.dayExpensesTotalSum);
+ }
+ },
+ error: error => {
+ const errorMessage = error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.DELETE_ERROR');
+ this.toastService.error(
+ this.translate.instant('ITEMS.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/item-form/item-edit-form/item-edit-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-edit-form/item-edit-form.component.html
new file mode 100644
index 0000000..32fa696
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-edit-form/item-edit-form.component.html
@@ -0,0 +1,141 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/item-form/item-edit-form/item-edit-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-edit-form/item-edit-form.component.ts
new file mode 100644
index 0000000..a12bf80
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/item-form/item-edit-form/item-edit-form.component.ts
@@ -0,0 +1,167 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../../services/modal.service';
+import { ItemsService, Item, ItemResponse } from '../../../services/items.service';
+import { ToastService } from '../../../services/toast.service';
+import { FormValidationService } from '../../../services/form-validation.service';
+import { DayExpensesTotalSumUpdateService } from '../../../services/day-expenses-total-sum-update.service';
+import { parseValidationErrors } from '../../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-item-edit-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './item-edit-form.component.html'
+})
+export class ItemEditFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ users: string[] = [];
+ checkId: string = '';
+ dayExpensesId: string = '';
+ itemId: string = '';
+
+ // Form fields
+ name: string = '';
+ comment: string = '';
+ price: number = 0;
+ amount: number = 1;
+ rating: number = 5;
+ hoverRating: number = 0;
+ tags: string[] = [];
+ tagInput: string = '';
+ selectedUsers: string[] = [];
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Callback functions passed from parent
+ onSuccess?: (checkId: string, newSum: number, dayExpensesTotalSum: number) => void;
+
+ constructor(
+ private translate: TranslateService,
+ private itemsService: ItemsService,
+ private toastService: ToastService,
+ private formValidationService: FormValidationService,
+ private dayExpensesTotalSumUpdateService: DayExpensesTotalSumUpdateService
+ ) {}
+
+ setRating(rating: number): void {
+ this.rating = rating;
+ }
+
+ addTag(): void {
+ const trimmedTag = this.tagInput.trim().replace(/\s+/g, '_').toLowerCase();
+ if (trimmedTag && !this.tags.includes(trimmedTag) && this.tags.length < 5) {
+ this.tags.push(trimmedTag);
+ this.tagInput = '';
+ }
+ }
+
+ removeTag(tag: string): void {
+ const index = this.tags.indexOf(tag);
+ if (index > -1) {
+ this.tags.splice(index, 1);
+ }
+ }
+
+ isAllUsersSelected(): boolean {
+ return this.users.length > 0 && this.selectedUsers.length === this.users.length;
+ }
+
+ toggleAllUsers(event: any): void {
+ if (event.target.checked) {
+ this.selectedUsers = [...this.users];
+ } else {
+ this.selectedUsers = [];
+ }
+ }
+
+ onUserSelectionChange(user: string, event: any): void {
+ if (event.target.checked) {
+ if (!this.selectedUsers.includes(user)) {
+ this.selectedUsers.push(user);
+ }
+ } else {
+ const index = this.selectedUsers.indexOf(user);
+ if (index > -1) {
+ this.selectedUsers.splice(index, 1);
+ }
+ }
+ }
+
+ validateForm(): boolean {
+ this.formErrors = this.formValidationService.validateItemForm(
+ this.name,
+ this.price,
+ this.amount,
+ this.rating,
+ this.selectedUsers
+ );
+ return Object.keys(this.formErrors).length === 0;
+ }
+
+ submit(): void {
+ if (!this.validateForm()) {
+ this.formValidated = true;
+ return;
+ }
+
+ this.formValidated = true;
+
+ const updatedItem: Item = {
+ id: this.itemId,
+ name: this.name,
+ comment: this.comment,
+ price: this.price,
+ amount: this.amount,
+ rating: this.rating,
+ tags: this.tags,
+ users: this.selectedUsers,
+ checkId: this.checkId
+ };
+
+ this.itemsService.editItem(updatedItem).subscribe({
+ next: (response: ItemResponse) => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('ITEMS.TOAST.SUCCESS'),
+ this.translate.instant('ITEMS.TOAST.EDIT_SUCCESS')
+ );
+
+ // Update day expenses total sum
+ this.dayExpensesTotalSumUpdateService.emitDayExpensesTotalSumUpdate(
+ this.dayExpensesId,
+ response.dayExpensesTotalSum
+ );
+
+ // Call success callback to refresh the list and update check sum
+ if (this.onSuccess) {
+ this.onSuccess(this.checkId, response.checkTotalSum, response.dayExpensesTotalSum);
+ }
+ },
+ error: error => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.EDIT_ERROR');
+ this.toastService.error(
+ this.translate.instant('ITEMS.TOAST.ERROR'),
+ this.translateBackendError(errorMessage)
+ );
+ }
+ }
+ });
+ }
+
+ translateBackendError(error: string): string {
+ const errorKey = `BACKEND_ERRORS.${error.toUpperCase().replace(/\s+/g, '_')}`;
+ const translated = this.translate.instant(errorKey);
+ return translated !== errorKey ? translated : error;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-add-form/recommendation-add-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-add-form/recommendation-add-form.component.html
new file mode 100644
index 0000000..0b3beb2
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-add-form/recommendation-add-form.component.html
@@ -0,0 +1,77 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-add-form/recommendation-add-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-add-form/recommendation-add-form.component.ts
new file mode 100644
index 0000000..74a48b8
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-add-form/recommendation-add-form.component.ts
@@ -0,0 +1,123 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../../services/modal.service';
+import { ItemsService } from '../../../services/items.service';
+import { ToastService } from '../../../services/toast.service';
+import { parseValidationErrors } from '../../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-recommendation-add-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './recommendation-add-form.component.html'
+})
+export class RecommendationAddFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Form fields
+ name: string = '';
+ comment: string = '';
+ price: number | null = null;
+ amount: number = 1;
+ rating: number = 5;
+ hoverRating: number = 0;
+ tags: string[] = [];
+ tagInput: string = '';
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private itemsService: ItemsService,
+ private toastService: ToastService
+ ) {}
+
+ validateForm(): boolean {
+ this.formErrors = {};
+ this.formValidated = true;
+
+ if (!this.name.trim()) {
+ this.formErrors['name'] = this.translate.instant('ITEMS.VALIDATION.NAME_REQUIRED');
+ }
+ if (!this.price || this.price <= 0 || this.price > 10000) {
+ this.formErrors['price'] = this.translate.instant('ITEMS.VALIDATION.PRICE_INVALID');
+ }
+ if (this.rating <= 0) {
+ this.formErrors['rating'] = this.translate.instant('ITEMS.VALIDATION.RATING_REQUIRED');
+ }
+
+ return Object.keys(this.formErrors).length === 0;
+ }
+
+ submit(): void {
+ if (!this.validateForm()) return;
+ this.formValidated = true;
+
+ const newItem = {
+ name: this.name,
+ comment: this.comment,
+ price: this.price!,
+ amount: 1,
+ rating: this.rating,
+ tags: this.tags
+ };
+
+ this.itemsService.createRecommendationItem(newItem).subscribe({
+ next: () => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('ITEMS.TOAST.SUCCESS'),
+ this.translate.instant('ITEMS.TOAST.CREATE_SUCCESS')
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: (error: any) => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.CREATE_ERROR');
+ this.toastService.error(
+ this.translate.instant('ITEMS.TOAST.ERROR'),
+ errorMessage
+ );
+ }
+ }
+ });
+ }
+
+ setRating(value: number): void {
+ this.rating = value;
+ }
+
+ setHoverRating(value: number): void {
+ this.hoverRating = value;
+ }
+
+ addTag(): void {
+ const tag = this.tagInput.trim();
+ if (tag && !this.tags.includes(tag)) {
+ this.tags.push(tag);
+ this.tagInput = '';
+ }
+ }
+
+ removeTag(tag: string): void {
+ this.tags = this.tags.filter(t => t !== tag);
+ }
+
+ getTotalPrice(): number {
+ return (this.price || 0) * this.amount;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component.html
new file mode 100644
index 0000000..8198f65
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component.html
@@ -0,0 +1,51 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component.ts
new file mode 100644
index 0000000..0e1cb68
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component.ts
@@ -0,0 +1,64 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../../services/modal.service';
+import { ItemsService } from '../../../services/items.service';
+import { ToastService } from '../../../services/toast.service';
+
+@Component({
+ selector: 'app-recommendation-delete-form',
+ standalone: true,
+ imports: [CommonModule, TranslatePipe],
+ templateUrl: './recommendation-delete-form.component.html'
+})
+export class RecommendationDeleteFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ id: string = '';
+ name: string = '';
+ comment: string = '';
+ price: number = 0;
+ amount: number = 1;
+ rating: number = 0;
+ tags: string[] = [];
+ canDelete: boolean = true;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private itemsService: ItemsService,
+ private toastService: ToastService
+ ) {}
+
+ submit(): void {
+ this.itemsService.deleteRecommendationItem(this.id).subscribe({
+ next: () => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('ITEMS.TOAST.SUCCESS'),
+ this.translate.instant('ITEMS.TOAST.DELETE_SUCCESS')
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: (error: any) => {
+ const errorMessage = error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.DELETE_ERROR');
+ this.toastService.error(
+ this.translate.instant('ITEMS.TOAST.ERROR'),
+ errorMessage
+ );
+ }
+ });
+ }
+
+ getTotalPrice(): number {
+ return this.price * this.amount;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component.html b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component.html
new file mode 100644
index 0000000..2f0ee3d
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component.html
@@ -0,0 +1,78 @@
+
diff --git a/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component.ts b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component.ts
new file mode 100644
index 0000000..619ab45
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component.ts
@@ -0,0 +1,126 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { ModalService } from '../../../services/modal.service';
+import { ItemsService } from '../../../services/items.service';
+import { ToastService } from '../../../services/toast.service';
+import { parseValidationErrors } from '../../../shared/models/validation-errors.model';
+
+@Component({
+ selector: 'app-recommendation-edit-form',
+ standalone: true,
+ imports: [CommonModule, FormsModule, TranslatePipe],
+ templateUrl: './recommendation-edit-form.component.html'
+})
+export class RecommendationEditFormComponent {
+ // Injected by modal service
+ modalService!: ModalService;
+
+ // Data passed from parent component
+ id: string = '';
+ name: string = '';
+ comment: string = '';
+ price: number = 0;
+ amount: number = 1;
+ rating: number = 0;
+ hoverRating: number = 0;
+ tags: string[] = [];
+ tagInput: string = '';
+ canEdit: boolean = true;
+
+ // Form validation
+ formErrors: { [key: string]: string } = {};
+ formValidated: boolean = false;
+
+ // Callback functions passed from parent
+ onSuccess?: () => void;
+
+ constructor(
+ private translate: TranslateService,
+ private itemsService: ItemsService,
+ private toastService: ToastService
+ ) {}
+
+ validateForm(): boolean {
+ this.formErrors = {};
+ this.formValidated = true;
+
+ if (!this.name.trim()) {
+ this.formErrors['name'] = this.translate.instant('ITEMS.VALIDATION.NAME_REQUIRED');
+ }
+ if (this.price <= 0 || this.price > 10000) {
+ this.formErrors['price'] = this.translate.instant('ITEMS.VALIDATION.PRICE_INVALID');
+ }
+ if (this.rating <= 0) {
+ this.formErrors['rating'] = this.translate.instant('ITEMS.VALIDATION.RATING_REQUIRED');
+ }
+
+ return Object.keys(this.formErrors).length === 0;
+ }
+
+ submit(): void {
+ if (!this.validateForm()) return;
+ this.formValidated = true;
+
+ const updatedItem = {
+ id: this.id,
+ name: this.name,
+ comment: this.comment,
+ price: this.price,
+ amount: 1,
+ rating: this.rating,
+ tags: this.tags
+ };
+
+ this.itemsService.editRecommendationItem(updatedItem).subscribe({
+ next: () => {
+ this.modalService.close();
+ this.toastService.success(
+ this.translate.instant('ITEMS.TOAST.SUCCESS'),
+ this.translate.instant('ITEMS.TOAST.EDIT_SUCCESS')
+ );
+
+ // Call success callback to refresh the list
+ if (this.onSuccess) {
+ this.onSuccess();
+ }
+ },
+ error: (error: any) => {
+ this.formErrors = parseValidationErrors(error);
+ this.formValidated = true;
+ if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
+ const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.EDIT_ERROR');
+ this.toastService.error(
+ this.translate.instant('ITEMS.TOAST.ERROR'),
+ errorMessage
+ );
+ }
+ }
+ });
+ }
+
+ setRating(value: number): void {
+ this.rating = value;
+ }
+
+ setHoverRating(value: number): void {
+ this.hoverRating = value;
+ }
+
+ addTag(): void {
+ const tag = this.tagInput.trim();
+ if (tag && !this.tags.includes(tag)) {
+ this.tags.push(tag);
+ this.tagInput = '';
+ }
+ }
+
+ removeTag(tag: string): void {
+ this.tags = this.tags.filter(t => t !== tag);
+ }
+
+ getTotalPrice(): number {
+ return this.price * this.amount;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.css b/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.css
index 710ebf8..21fc716 100644
--- a/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.css
+++ b/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.css
@@ -98,3 +98,97 @@ input[type="search"]:-ms-input-placeholder {
.scrollbar-dark::-webkit-scrollbar-thumb:hover {
background-color: #aaaaaa;
}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ /* Reduce title size */
+ h4 {
+ font-size: 0.9rem !important;
+ }
+
+ /* Card sizing for small screens */
+ .card-body {
+ padding: 0.5rem !important;
+ }
+
+ .card-title {
+ font-size: 0.85rem !important;
+ }
+
+ .card-body small {
+ font-size: 0.65rem !important;
+ }
+
+ /* Pagination info text sizing */
+ .pagination-info {
+ font-size: 0.8rem !important;
+ }
+
+ /* Input groups */
+ .input-group {
+ font-size: 0.8rem !important;
+ }
+
+ /* Buttons */
+ .btn {
+ font-size: 0.85rem !important;
+ }
+
+ /* Filter and Sort group - side by side buttons */
+ .filter-sort-group {
+ gap: 0.5rem !important;
+ }
+
+ .filter-sort-group .filter-btn {
+ width: 50% !important;
+ font-size: 0.85rem !important;
+ padding: 0.2rem 0.5rem !important;
+ line-height: 1.2 !important;
+ }
+
+ .filter-sort-group app-sort-bar {
+ width: 50% !important;
+ flex: 0 0 50%;
+ }
+
+ .filter-sort-group app-sort-bar .sort-dropdown {
+ width: 100%;
+ }
+
+ .filter-sort-group app-sort-bar button {
+ width: 100% !important;
+ }
+
+ /* Badge text */
+ .badge {
+ font-size: 0.75rem !important;
+ }
+
+ /* Checkbox label */
+ .form-check-label {
+ font-size: 0.8rem !important;
+ }
+
+ /* Checkbox input */
+ .form-check-input {
+ width: 1rem !important;
+ height: 1rem !important;
+ }
+
+ /* Tag filter input - prevent overflow on small screens */
+ .position-relative {
+ min-width: 100% !important;
+ max-width: 100% !important;
+ }
+
+ /* Ensure consistent input padding */
+ .input-group .form-control {
+ padding: 0.375rem 0.75rem !important;
+ }
+
+ /* Tag filter dropdown suggestions */
+ .list-group-item {
+ font-size: 0.8rem !important;
+ padding: 0.5rem 0.75rem !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.html b/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.html
index b706ade..5856043 100644
--- a/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.html
+++ b/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.html
@@ -1,57 +1,164 @@
-
+
-
+
-
+
{{ 'SIDEBAR.RECOMMENDATIONS' | translate }}
-
+
-
-
-
-
-
- {{ 'ITEMS.FILTER.TAG_FILTER' | translate }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ 'ITEMS.MODAL.DELETE' | translate }}
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.ts b/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.ts
index df6875f..639b4a5 100644
--- a/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/recommendations/recommendations/recommendations.component.ts
@@ -3,10 +3,12 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { ModalWindowComponent } from '../../shared/modal-window/modal-window.component';
+import { ModalService } from '../../services/modal.service';
import { FilterBarComponent, FilterOption } from '../../shared/filter-bar/filter-bar.component';
import { SortBarComponent, SortOption } from '../../shared/sort-bar/sort-bar.component';
-import { ValidationErrors, parseValidationErrors } from '../../shared/models/validation-errors.model';
+import { RecommendationAddFormComponent } from '../../modals/recommendation-form/recommendation-add-form/recommendation-add-form.component';
+import { RecommendationEditFormComponent } from '../../modals/recommendation-form/recommendation-edit-form/recommendation-edit-form.component';
+import { RecommendationDeleteFormComponent } from '../../modals/recommendation-form/recommendation-delete-form/recommendation-delete-form.component';
import { ItemsService, Item } from '../../services/items.service';
import { ToastService } from '../../services/toast.service';
import { Subject, Subscription } from 'rxjs';
@@ -18,7 +20,7 @@ declare var bootstrap: any;
@Component({
selector: 'app-recommendations',
standalone: true,
- imports: [CommonModule, FormsModule, RouterLink, ModalWindowComponent, FilterBarComponent, SortBarComponent, TranslatePipe, TourAnchorNgBootstrapDirective, TourStepTemplateComponent],
+ imports: [CommonModule, FormsModule, RouterLink, FilterBarComponent, SortBarComponent, TranslatePipe, TourAnchorNgBootstrapDirective, TourStepTemplateComponent],
templateUrl: './recommendations.component.html',
styleUrl: './recommendations.component.css'
})
@@ -34,29 +36,6 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
totalPages = 1;
totalCount = 0;
- // Modal properties
- modalInstance: any;
- currentModalContent: 'add' | 'edit' | 'delete' = 'add';
- modalTitle: string = '';
-
- // Form properties
- id = '';
- name = '';
- comment = '';
- price = 0;
- amount = 1;
- rating = 0;
- hoverRating = 0;
- tags: string[] = [];
- tagInput = '';
- selectedUsers: string[] = [];
- users: string[] = []; // Will be populated from existing items
- canEditCurrentItem = false;
-
- // Validation properties
- formErrors: ValidationErrors = {};
- formValidated = false;
-
// Filter and sort properties
filterText = '';
filterCriteria: string = 'Name';
@@ -70,8 +49,6 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
private actualFilterCriteria = 'Name';
sortColumn: 'name' | 'price' | 'amount' | 'totalPrice' | 'userCount' | 'rating' = 'rating';
sortOrder: 'asc' | 'desc' = 'desc';
- tempSortColumn: 'name' | 'price' | 'amount' | 'totalPrice' | 'userCount' | 'rating' = 'rating';
- tempSortOrder: 'asc' | 'desc' = 'desc';
// Filter and sort options for shared components
filterOptions: FilterOption[] = [
@@ -103,13 +80,6 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
private filterTextSubject = new Subject
();
private filterTextSubscription!: Subscription;
- get sortDisplayText(): string {
- const columnKey = `ITEMS.SORT.${this.sortColumn.toUpperCase().replace('PRICE', 'PRICE').replace('TOTALPRICE', 'TOTAL_PRICE').replace('USERCOUNT', 'USER_COUNT')}`;
- const columnText = this.translate.instant(columnKey);
- const orderIcon = this.sortOrder === 'asc' ? '↑' : '↓';
- return `${columnText} ${orderIcon}`;
- }
-
get filterCriteriaKey(): string {
const keyMap: Record = {
'Name': 'ITEMS.FILTER.NAME',
@@ -140,6 +110,7 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
private itemsService: ItemsService,
private translate: TranslateService,
private toastService: ToastService,
+ private modalService: ModalService,
public tourService: TourService
) {}
@@ -153,6 +124,9 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
this.loadItems();
});
+ // Set items per page based on screen size
+ this.setItemsPerPageByScreenSize();
+
this.loadItems();
this.loadAllTags();
}
@@ -190,6 +164,15 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
}
}
+ setItemsPerPageByScreenSize(): void {
+ // Bootstrap's md breakpoint is 768px
+ if (window.innerWidth < 768) {
+ this.itemsPerPage = 3;
+ } else {
+ this.itemsPerPage = 12;
+ }
+ }
+
initializeTooltips(): void {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach((tooltipTriggerEl) => {
@@ -241,21 +224,6 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
});
}
- loadSingleItem(id: string): void {
- const item = this.itemsList.find(i => i.id === id);
- if (item) {
- this.id = item.id;
- this.name = item.name;
- this.comment = item.comment || '';
- this.price = item.price;
- this.amount = item.amount;
- this.rating = item.rating;
- this.tags = [...item.tags];
- this.selectedUsers = [...item.users];
- this.canEditCurrentItem = item.canEdit || false;
- }
- }
-
goToNextPage(): void {
if (this.currentPage < this.totalPages) {
this.currentPage++;
@@ -309,22 +277,7 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
this.loadItems();
}
- openSortModal(): void {
- this.tempSortColumn = this.sortColumn;
- this.tempSortOrder = this.sortOrder;
- }
-
- applySorting2(): void {
- this.sortColumn = this.tempSortColumn;
- this.sortOrder = this.tempSortOrder;
- this.currentPage = 1;
- this.loadItems();
- this.closeSortDropdown();
- }
-
resetSorting(): void {
- this.tempSortColumn = 'name';
- this.tempSortOrder = 'asc';
this.sortColumn = 'name';
this.sortOrder = 'asc';
this.currentPage = 1;
@@ -362,68 +315,53 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
}
openModal(type: 'add' | 'edit' | 'delete', id: string = ''): void {
- this.currentModalContent = type;
- this.modalTitle = this.translate.instant(`ITEMS.MODAL.${type.toUpperCase()}_TITLE`);
-
- const modalElement = document.getElementById('recommendationsModal');
- if (!modalElement) return;
-
if (type === 'add') {
- this.clearFormData();
- } else if (id) {
- this.loadSingleItem(id);
- }
-
- if (!this.modalInstance) {
- this.modalInstance = new bootstrap.Modal(modalElement, {
- backdrop: 'static',
- keyboard: false
- });
- }
-
- this.formErrors = {};
- this.formValidated = false;
- this.modalInstance.show();
- }
-
- hideModal(): void {
- if (this.modalInstance) {
- this.modalInstance.hide();
- this.clearFormData();
- }
- }
-
- private clearFormData(): void {
- this.id = '';
- this.name = '';
- this.comment = '';
- this.price = 0;
- this.amount = 1;
- this.rating = 0;
- this.hoverRating = 0;
- this.tags = [];
- this.tagInput = '';
- this.selectedUsers = [];
- this.formErrors = {};
- this.formValidated = false;
- }
-
- setRating(rating: number): void {
- this.rating = rating;
- }
-
- addTag(): void {
- const tag = this.tagInput.trim().replace(/\s+/g, '_').toLowerCase();
- if (tag && !this.tags.includes(tag)) {
- this.tags.push(tag);
- this.tagInput = '';
- }
- }
-
- removeTag(tag: string): void {
- const index = this.tags.indexOf(tag);
- if (index > -1) {
- this.tags.splice(index, 1);
+ this.modalService.open(
+ RecommendationAddFormComponent,
+ this.translate.instant('ITEMS.MODAL.ADD_TITLE'),
+ { onSuccess: () => this.loadItems() },
+ 'lg'
+ );
+ } else if (type === 'edit' && id) {
+ const item = this.itemsList.find(i => i.id === id);
+ if (!item) return;
+
+ this.modalService.open(
+ RecommendationEditFormComponent,
+ this.translate.instant('ITEMS.MODAL.EDIT_TITLE'),
+ {
+ id: item.id,
+ name: item.name,
+ comment: item.comment || '',
+ price: item.price,
+ amount: item.amount,
+ rating: item.rating,
+ tags: [...item.tags],
+ canEdit: item.canEdit || false,
+ onSuccess: () => this.loadItems()
+ },
+ 'lg'
+ );
+ } else if (type === 'delete' && id) {
+ const item = this.itemsList.find(i => i.id === id);
+ if (!item) return;
+
+ this.modalService.open(
+ RecommendationDeleteFormComponent,
+ this.translate.instant('ITEMS.MODAL.DELETE_TITLE'),
+ {
+ id: item.id,
+ name: item.name,
+ comment: item.comment || '',
+ price: item.price,
+ amount: item.amount,
+ rating: item.rating,
+ tags: [...item.tags],
+ canDelete: item.canEdit || false,
+ onSuccess: () => this.loadItems()
+ },
+ 'lg'
+ );
}
}
@@ -472,147 +410,6 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
this.loadItems();
}
- onUserSelectionChange(user: string, event: any): void {
- if (event.target.checked) {
- if (!this.selectedUsers.includes(user)) {
- this.selectedUsers.push(user);
- }
- } else {
- const index = this.selectedUsers.indexOf(user);
- if (index > -1) {
- this.selectedUsers.splice(index, 1);
- }
- }
- }
-
- isAllUsersSelected(): boolean {
- return this.users.length > 0 && this.selectedUsers.length === this.users.length;
- }
-
- toggleAllUsers(event: any): void {
- if (event.target.checked) {
- this.selectedUsers = [...this.users];
- } else {
- this.selectedUsers = [];
- }
- }
-
- validateItemForm(): boolean {
- this.formErrors = {};
- this.formValidated = true;
-
- if (!this.name.trim()) {
- this.formErrors['name'] = this.translate.instant('ITEMS.VALIDATION.NAME_REQUIRED');
- }
- if (this.price <= 0) {
- this.formErrors['price'] = this.translate.instant('ITEMS.VALIDATION.PRICE_INVALID');
- }
- if (this.amount <= 0) {
- this.formErrors['amount'] = this.translate.instant('ITEMS.VALIDATION.AMOUNT_INVALID');
- }
- if (this.rating <= 0) {
- this.formErrors['rating'] = this.translate.instant('ITEMS.VALIDATION.RATING_REQUIRED');
- }
-
- return Object.keys(this.formErrors).length === 0;
- }
-
- createItem(): void {
- if (!this.validateItemForm()) return;
- this.formValidated = true;
-
- const newItem = {
- name: this.name,
- comment: this.comment,
- price: this.price,
- amount: this.amount,
- rating: this.rating,
- tags: this.tags
- };
-
- this.itemsService.createRecommendationItem(newItem).subscribe({
- next: () => {
- this.hideModal();
- // Reload items to get fresh data from server
- this.loadItems();
- this.toastService.success(
- this.translate.instant('ITEMS.TOAST.SUCCESS'),
- this.translate.instant('ITEMS.TOAST.CREATE_SUCCESS')
- );
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.CREATE_ERROR');
- this.toastService.error(
- this.translate.instant('ITEMS.TOAST.ERROR'),
- errorMessage
- );
- }
- }
- });
- }
-
- editItem(): void {
- if (!this.validateItemForm()) return;
- this.formValidated = true;
-
- const updatedItem = {
- id: this.id,
- name: this.name,
- comment: this.comment,
- price: this.price,
- amount: this.amount,
- rating: this.rating,
- tags: this.tags
- };
-
- this.itemsService.editRecommendationItem(updatedItem).subscribe({
- next: () => {
- this.hideModal();
- // Reload items to get fresh data from server
- this.loadItems();
- this.toastService.success(
- this.translate.instant('ITEMS.TOAST.SUCCESS'),
- this.translate.instant('ITEMS.TOAST.EDIT_SUCCESS')
- );
- },
- error: error => {
- this.formErrors = parseValidationErrors(error);
- this.formValidated = true;
- if (Object.keys(this.formErrors).length === 0 || this.formErrors['general']) {
- const errorMessage = this.formErrors['general'] || error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.EDIT_ERROR');
- this.toastService.error(
- this.translate.instant('ITEMS.TOAST.ERROR'),
- errorMessage
- );
- }
- }
- });
- }
-
- deleteItem(): void {
- this.itemsService.deleteRecommendationItem(this.id).subscribe({
- next: () => {
- this.hideModal();
- // Reload items to get fresh data from server
- this.loadItems();
- this.toastService.success(
- this.translate.instant('ITEMS.TOAST.SUCCESS'),
- this.translate.instant('ITEMS.TOAST.DELETE_SUCCESS')
- );
- },
- error: error => {
- const errorMessage = error?.error?.message || error?.message || this.translate.instant('ITEMS.TOAST.DELETE_ERROR');
- this.toastService.error(
- this.translate.instant('ITEMS.TOAST.ERROR'),
- errorMessage
- );
- }
- });
- }
-
translateBackendError(errorMessage: string): string {
return errorMessage;
}
@@ -620,47 +417,62 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
// Tour
initializeTour(): void {
const hasItems = !!document.querySelector('[touranchor="items-grid"]');
+ const isSmallScreen = window.innerWidth < 576;
const tourSteps: any[] = [];
- // Always show basic steps
- tourSteps.push(
- {
- anchorId: 'tag-filter',
- content: this.translate.instant('TOUR_RECOMMENDATIONS.TAG_FILTER_CONTENT'),
- title: this.translate.instant('TOUR_RECOMMENDATIONS.TAG_FILTER_TITLE'),
- placement: 'bottom',
- enableBackdrop: true
- },
- {
- anchorId: 'search-filter',
- content: this.translate.instant('TOUR_RECOMMENDATIONS.SEARCH_FILTER_CONTENT'),
- title: this.translate.instant('TOUR_RECOMMENDATIONS.SEARCH_FILTER_TITLE'),
- placement: 'bottom',
- enableBackdrop: true
- },
- {
- anchorId: 'sort-bar',
- content: this.translate.instant('TOUR_RECOMMENDATIONS.SORT_BAR_CONTENT'),
- title: this.translate.instant('TOUR_RECOMMENDATIONS.SORT_BAR_TITLE'),
- placement: 'left',
- enableBackdrop: true
- },
- {
- anchorId: 'add-item-btn',
- content: this.translate.instant('TOUR_RECOMMENDATIONS.ADD_ITEM_CONTENT'),
- title: this.translate.instant('TOUR_RECOMMENDATIONS.ADD_ITEM_TITLE'),
+ // Add item button - always visible
+ tourSteps.push({
+ anchorId: 'add-item-btn',
+ content: this.translate.instant('TOUR_RECOMMENDATIONS.ADD_ITEM_CONTENT'),
+ title: this.translate.instant('TOUR_RECOMMENDATIONS.ADD_ITEM_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ });
+
+ // Different steps for small vs large screens
+ if (isSmallScreen) {
+ // Steps for small screens (filter/sort group)
+ tourSteps.push({
+ anchorId: 'filter-sort-controls',
+ content: this.translate.instant('TOUR_RECOMMENDATIONS.FILTER_SORT_CONTROLS_CONTENT'),
+ title: this.translate.instant('TOUR_RECOMMENDATIONS.FILTER_SORT_CONTROLS_TITLE'),
placement: 'bottom',
enableBackdrop: true
- },
- {
- anchorId: 'only-my-items',
- content: this.translate.instant('TOUR_RECOMMENDATIONS.ONLY_MY_ITEMS_CONTENT'),
- title: this.translate.instant('TOUR_RECOMMENDATIONS.ONLY_MY_ITEMS_TITLE'),
- placement: 'left',
- enableBackdrop: true
- }
- );
+ });
+ } else {
+ // Steps for large screens (individual controls)
+ tourSteps.push(
+ {
+ anchorId: 'tag-filter',
+ content: this.translate.instant('TOUR_RECOMMENDATIONS.TAG_FILTER_CONTENT'),
+ title: this.translate.instant('TOUR_RECOMMENDATIONS.TAG_FILTER_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'search-filter',
+ content: this.translate.instant('TOUR_RECOMMENDATIONS.SEARCH_FILTER_CONTENT'),
+ title: this.translate.instant('TOUR_RECOMMENDATIONS.SEARCH_FILTER_TITLE'),
+ placement: 'bottom',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'sort-bar',
+ content: this.translate.instant('TOUR_RECOMMENDATIONS.SORT_BAR_CONTENT'),
+ title: this.translate.instant('TOUR_RECOMMENDATIONS.SORT_BAR_TITLE'),
+ placement: 'left',
+ enableBackdrop: true
+ },
+ {
+ anchorId: 'only-my-items',
+ content: this.translate.instant('TOUR_RECOMMENDATIONS.ONLY_MY_ITEMS_CONTENT'),
+ title: this.translate.instant('TOUR_RECOMMENDATIONS.ONLY_MY_ITEMS_TITLE'),
+ placement: 'left',
+ enableBackdrop: true
+ }
+ );
+ }
// Add items grid step only if items exist
if (hasItems) {
@@ -668,12 +480,17 @@ export class RecommendationsComponent implements OnInit, AfterViewInit, OnDestro
anchorId: 'items-grid',
content: this.translate.instant('TOUR_RECOMMENDATIONS.ITEMS_GRID_CONTENT'),
title: this.translate.instant('TOUR_RECOMMENDATIONS.ITEMS_GRID_TITLE'),
- placement: 'right',
+ placement: isSmallScreen ? 'bottom' : 'right',
enableBackdrop: true
});
}
- this.tourService.initialize(tourSteps);
+ // Initialize tour with global button title configuration
+ this.tourService.initialize(tourSteps, {
+ prevBtnTitle: this.translate.instant('TOUR.PREV_BTN'),
+ nextBtnTitle: this.translate.instant('TOUR.NEXT_BTN'),
+ endBtnTitle: this.translate.instant('TOUR.END_BTN')
+ });
}
startTour(): void {
diff --git a/src/ExpensesCalculator.UI/src/app/services/auth.service.ts b/src/ExpensesCalculator.UI/src/app/services/auth.service.ts
index 263a7d5..d399afc 100644
--- a/src/ExpensesCalculator.UI/src/app/services/auth.service.ts
+++ b/src/ExpensesCalculator.UI/src/app/services/auth.service.ts
@@ -97,30 +97,34 @@ export class RefreshInterceptor implements HttpInterceptor {
constructor(
private auth: AuthService,
- private tokens: TokenService
+ private tokens: TokenService,
+ private router: Router
) {}
intercept(req: HttpRequest, next: HttpHandler) {
return next.handle(req).pipe(
catchError(err => {
- if (err.status === 401 && !this.isRefreshing){
+ // Only handle 401 errors, skip if already refreshing or if this is the refresh endpoint
+ if (err.status === 401 && !this.isRefreshing && !req.url.includes('/auth/refresh')) {
this.isRefreshing = true;
return this.auth.refreshToken().pipe(
switchMap(() => {
this.isRefreshing = false;
- const newReq = req.clone({
+ // Retry the original request with the new token
+ const retryReq = req.clone({
setHeaders: {
Authorization: `Bearer ${this.tokens.getAccessToken()}`
}
- })
+ });
- return next.handle(newReq);
+ return next.handle(retryReq);
}),
catchError(() => {
this.isRefreshing = false;
this.tokens.clear();
+ this.router.navigate(['/login']);
return throwError(() => err);
})
);
diff --git a/src/ExpensesCalculator.UI/src/app/services/expenses.service.ts b/src/ExpensesCalculator.UI/src/app/services/expenses.service.ts
index 4d0a4bc..786ed94 100644
--- a/src/ExpensesCalculator.UI/src/app/services/expenses.service.ts
+++ b/src/ExpensesCalculator.UI/src/app/services/expenses.service.ts
@@ -60,6 +60,7 @@ export interface CheckCalculation {
id: string;
location: string;
payer: string;
+ totalSum: number;
dayExpensesId: string;
};
items: ItemCalculation[];
diff --git a/src/ExpensesCalculator.UI/src/app/services/form-validation.service.ts b/src/ExpensesCalculator.UI/src/app/services/form-validation.service.ts
index 7e7152b..a0bbc60 100644
--- a/src/ExpensesCalculator.UI/src/app/services/form-validation.service.ts
+++ b/src/ExpensesCalculator.UI/src/app/services/form-validation.service.ts
@@ -58,16 +58,16 @@ export class FormValidationService {
/**
* Validate item form
*/
- validateItemForm(name: string, price: number, amount: number, rating: number, selectedUsers: string[]): ValidationErrors {
+ validateItemForm(name: string, price: number | null, amount: number, rating: number, selectedUsers: string[]): ValidationErrors {
const errors: ValidationErrors = {};
if (!name.trim()) {
errors['name'] = this.translate.instant('ITEMS.VALIDATION.NAME_REQUIRED');
}
- if (price <= 0) {
+ if (!price || price <= 0 || price > 10000) {
errors['price'] = this.translate.instant('ITEMS.VALIDATION.PRICE_INVALID');
}
- if (amount <= 0) {
+ if (amount <= 0 || amount > 1000) {
errors['amount'] = this.translate.instant('ITEMS.VALIDATION.AMOUNT_INVALID');
}
if (rating <= 0) {
diff --git a/src/ExpensesCalculator.UI/src/app/services/modal.service.ts b/src/ExpensesCalculator.UI/src/app/services/modal.service.ts
new file mode 100644
index 0000000..7de4d72
--- /dev/null
+++ b/src/ExpensesCalculator.UI/src/app/services/modal.service.ts
@@ -0,0 +1,96 @@
+import { Injectable, Type, ComponentRef } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+
+export interface ModalConfig {
+ component: Type;
+ title: string;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ data?: any;
+ show: boolean;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ModalService {
+ private modalSubject = new BehaviorSubject(null);
+ public modal$: Observable = this.modalSubject.asObservable();
+
+ private modalInstance: any = null;
+ private componentRef: ComponentRef | null = null;
+
+ /**
+ * Open a modal with a dynamic component
+ * @param component - The component class to load
+ * @param title - Modal title
+ * @param data - Data to pass to the component
+ * @param size - Modal size
+ */
+ open(component: Type, title: string, data?: any, size: 'sm' | 'md' | 'lg' | 'xl' = 'md'): void {
+ const config: ModalConfig = {
+ component,
+ title,
+ size,
+ data,
+ show: true
+ };
+
+ this.modalSubject.next(config);
+ }
+
+ /**
+ * Close the modal
+ */
+ close(): void {
+ if (this.modalInstance) {
+ this.modalInstance.hide();
+ this.modalInstance = null;
+ }
+
+ const currentModal = this.modalSubject.value;
+ if (currentModal) {
+ this.modalSubject.next({ ...currentModal, show: false });
+
+ // Clear the modal after animation
+ setTimeout(() => {
+ this.modalSubject.next(null);
+ this.componentRef = null;
+ }, 300);
+ }
+ }
+
+ /**
+ * Set the Bootstrap modal instance (called by modal-window component)
+ */
+ setModalInstance(instance: any): void {
+ this.modalInstance = instance;
+ }
+
+ /**
+ * Set the component ref (called by modal-window component)
+ */
+ setComponentRef(ref: ComponentRef): void {
+ this.componentRef = ref;
+ }
+
+ /**
+ * Get the current component ref
+ */
+ getComponentRef(): ComponentRef | null {
+ return this.componentRef;
+ }
+
+ /**
+ * Check if modal is open
+ */
+ isOpen(): boolean {
+ return this.modalSubject.value?.show ?? false;
+ }
+
+ /**
+ * Get current modal config
+ */
+ getCurrentConfig(): ModalConfig | null {
+ return this.modalSubject.value;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.css b/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.css
index f950a24..e000877 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.css
+++ b/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.css
@@ -44,3 +44,26 @@
.input-group-primary .input-group-text {
border-color: #0d6efd;
}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ .dropdown-toggle {
+ width: 100% !important;
+ }
+
+ .form-control {
+ line-height: 1.3 !important;
+ }
+
+ .input-group-primary .form-control::placeholder {
+ font-size: 0.75rem !important;
+ }
+
+ .input-group-primary {
+ width: 100% !important;
+ }
+
+ .dropdown-menu {
+ width: 100% !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.html b/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.html
index 18d3869..30d9661 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.html
+++ b/src/ExpensesCalculator.UI/src/app/shared/filter-bar/filter-bar.component.html
@@ -1,4 +1,4 @@
-
+
- -
-
+
+ -
+
{{ langLabel }}
diff --git a/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.html b/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.html
index da17b8a..47c89a8 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.html
+++ b/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.html
@@ -3,7 +3,22 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.ts b/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.ts
index 1ed2ca2..e12ec1c 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/shared/modal-window/modal-window.component.ts
@@ -1,5 +1,9 @@
-import { Component, Input } from '@angular/core';
+import { Component, Input, ViewChild, ViewContainerRef, ComponentRef, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { ModalService, ModalConfig } from '../../services/modal.service';
+import { Subscription } from 'rxjs';
+
+declare const bootstrap: any;
@Component({
selector: 'app-modal-window',
@@ -7,9 +11,95 @@ import { CommonModule } from '@angular/common';
templateUrl: './modal-window.component.html',
styleUrl: './modal-window.component.css'
})
-export class ModalWindowComponent {
+export class ModalWindowComponent implements OnInit, OnDestroy {
+ @ViewChild('dynamicContent', { read: ViewContainerRef }) dynamicContent!: ViewContainerRef;
+
+ // For backwards compatibility with old modal usage
@Input() modalId = 'myModal';
@Input() currentModalContent: 'add' | 'edit' | 'delete' | 'share' = 'add';
@Input() modalTitle: string = '';
@Input() modalSize: 'sm' | 'md' | 'lg' | 'xl' = 'md';
+
+ private modalInstance: any = null;
+ private componentRef: ComponentRef
| null = null;
+ private subscription!: Subscription;
+
+ constructor(private modalService: ModalService) {}
+
+ ngOnInit(): void {
+ // Set modalId to 'globalModal' for service-based usage
+ if (this.modalId === 'myModal') {
+ this.modalId = 'globalModal';
+ }
+
+ this.subscription = this.modalService.modal$.subscribe(
+ (config: ModalConfig | null) => {
+ if (config && config.show) {
+ this.openModal(config);
+ } else if (config && !config.show) {
+ this.closeModal();
+ }
+ }
+ );
+ }
+
+ ngOnDestroy(): void {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ if (this.componentRef) {
+ this.componentRef.destroy();
+ }
+ if (this.modalInstance) {
+ this.modalInstance.dispose();
+ }
+ }
+
+ private openModal(config: ModalConfig): void {
+ // Set modal properties
+ this.modalTitle = config.title;
+ this.modalSize = config.size || 'md';
+
+ // Clear previous content
+ if (this.componentRef) {
+ this.componentRef.destroy();
+ }
+ this.dynamicContent.clear();
+
+ // Create the component dynamically
+ this.componentRef = this.dynamicContent.createComponent(config.component);
+
+ // Pass data to the component if provided
+ if (config.data) {
+ Object.assign(this.componentRef.instance, config.data);
+ }
+
+ // Set the modal service as a property on the component instance
+ // so components can call modalService.close()
+ this.componentRef.instance.modalService = this.modalService;
+
+ // Notify the service about the component ref
+ this.modalService.setComponentRef(this.componentRef);
+
+ // Show the Bootstrap modal
+ const modalElement = document.getElementById(this.modalId);
+ if (modalElement) {
+ this.modalInstance = new bootstrap.Modal(modalElement, {
+ backdrop: 'static',
+ keyboard: false
+ });
+ this.modalService.setModalInstance(this.modalInstance);
+ this.modalInstance.show();
+ }
+ }
+
+ private closeModal(): void {
+ if (this.modalInstance) {
+ this.modalInstance.hide();
+ }
+ }
+
+ hideModal(): void {
+ this.modalService.close();
+ }
}
diff --git a/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.css b/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.css
index b875f0c..e7bd5b3 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.css
+++ b/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.css
@@ -43,3 +43,23 @@ hr {
border-color: #6c757d;
color: white;
}
+
+/* Component-specific responsive styles */
+@media (max-width: 576px) {
+ .sort-dropdown .dropdown-toggle {
+ width: 100% !important;
+ }
+
+ .sort-dropdown .dropdown-menu {
+ width: 100% !important;
+ }
+
+ .form-check-label {
+ font-size: 0.8rem !important;
+ }
+
+ .dropdown-menu .btn {
+ font-size: 0.75rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.ts b/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.ts
index de76877..7034a93 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.ts
+++ b/src/ExpensesCalculator.UI/src/app/shared/sort-bar/sort-bar.component.ts
@@ -26,6 +26,7 @@ export class SortBarComponent implements OnInit, OnChanges, OnDestroy, AfterView
sortButtonWidth: string = 'auto';
private sortButtonWidthCache: Map = new Map();
private langChangeSub!: Subscription;
+ private resizeListener: (() => void) | null = null;
// Temporary sort state for modal
tempSortColumn: string = '';
@@ -57,12 +58,21 @@ export class SortBarComponent implements OnInit, OnChanges, OnDestroy, AfterView
this.langChangeSub = this.translate.onLangChange.subscribe(() => {
setTimeout(() => this.syncSortButtonWidth(), 0);
});
+
+ // Re-sync button width when window is resized
+ this.resizeListener = () => {
+ this.syncSortButtonWidth();
+ };
+ window.addEventListener('resize', this.resizeListener);
}
ngOnDestroy(): void {
if (this.langChangeSub) {
this.langChangeSub.unsubscribe();
}
+ if (this.resizeListener) {
+ window.removeEventListener('resize', this.resizeListener);
+ }
}
get sortDisplayText(): string {
@@ -107,6 +117,12 @@ export class SortBarComponent implements OnInit, OnChanges, OnDestroy, AfterView
}
syncSortButtonWidth(): void {
+ // On small screens (< 576px), let CSS handle the width
+ if (window.innerWidth < 576) {
+ this.sortButtonWidth = 'auto';
+ return;
+ }
+
// Find the longest translated text with arrow
let longestText = '';
let maxLength = 0;
diff --git a/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.css b/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.css
index e69de29..ae67736 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.css
+++ b/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.css
@@ -0,0 +1,59 @@
+/* Make menu flex container to push GitHub to bottom */
+#menu {
+ display: flex !important;
+ flex-direction: column !important;
+ min-height: 100vh !important;
+}
+
+/* Reduce vertical navbar width on small screens */
+@media (max-width: 576px) {
+ /* Make the container narrower */
+ .d-flex.flex-column {
+ width: 50px;
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ /* Reduce icon size */
+ .nav-item.fs-5 {
+ font-size: 1rem !important;
+ }
+
+ /* Center icons and reduce spacing */
+ .nav-link {
+ padding: 0.5rem 0 !important;
+ text-align: center;
+ justify-content: center;
+ }
+
+ /* Reduce spacing between items */
+ .nav-pills .nav-item {
+ margin-bottom: 0.15rem;
+ }
+
+ /* Adjust title icon and align with horizontal navbar */
+ .bi-calculator {
+ font-size: 1.2rem;
+ }
+
+ .nav-item.fs-5:first-child {
+ margin-top: 0.5rem;
+ }
+
+ /* Align hr with horizontal navbar */
+ hr {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+ }
+
+ /* Center all content */
+ #menu {
+ align-items: center !important;
+ width: 100%;
+ }
+
+ /* Reduce bottom icon spacing */
+ .fixed-bottom {
+ text-align: center;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.html b/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.html
index 671947c..3fd423c 100644
--- a/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.html
+++ b/src/ExpensesCalculator.UI/src/app/shared/vertical-navbar/vertical-navbar.component.html
@@ -1,9 +1,9 @@
-
+
-
-
- ExpensesCalculator
+ ExpensesTracker
@@ -22,8 +22,8 @@
{{ 'SIDEBAR.RECOMMENDATIONS' | translate }}
- -
-
+
{{ 'SIDEBAR.GITHUB' | translate }}
diff --git a/src/ExpensesCalculator.UI/src/styles.css b/src/ExpensesCalculator.UI/src/styles.css
index 7f11558..cbe0e2e 100644
--- a/src/ExpensesCalculator.UI/src/styles.css
+++ b/src/ExpensesCalculator.UI/src/styles.css
@@ -13,6 +13,19 @@
#customProportion > .col-10 {
width: 84%;
}
+
+/* Reduce vertical navbar width on small screens */
+@media (max-width: 576px) {
+ #customProportion > .col-2 {
+ width: 50px !important;
+ flex: 0 0 50px;
+ }
+
+ #customProportion > .col-10 {
+ width: calc(100% - 50px) !important;
+ flex: 1 1 calc(100% - 50px);
+ }
+}
html, body { height: 100%; font-size: large; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
@@ -186,3 +199,120 @@ body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
background-color: #b52a3a;
border-color: #b52a3a;
}
+
+/* Responsive text sizing for modals and tour on small screens */
+@media (max-width: 576px) {
+ /* Modal sizing */
+ .modal-title {
+ font-size: 1rem !important;
+ }
+
+ .modal-body {
+ font-size: 0.85rem !important;
+ }
+
+ .modal-body label,
+ .modal-body .form-label {
+ font-size: 0.85rem !important;
+ }
+
+ .modal-body input,
+ .modal-body select,
+ .modal-body textarea,
+ .modal-body .form-control {
+ font-size: 0.85rem !important;
+ }
+
+ .modal-footer .btn {
+ font-size: 0.85rem !important;
+ padding: 0.3rem 0.6rem !important;
+ }
+
+ /* Tour popover sizing */
+ .popover {
+ max-width: 300px !important;
+ }
+
+ .popover-header {
+ font-size: 0.85rem !important;
+ padding: 3px 6px !important;
+ }
+
+ .popover-body {
+ font-size: 0.75rem !important;
+ padding: 3px 6px !important;
+ }
+
+ .popover .btn,
+ .popover button {
+ font-size: 0.7rem !important;
+ padding: 2px 8px !important;
+ }
+
+ .popover small,
+ .popover .text-muted {
+ font-size: 0.65rem !important;
+ }
+
+ /* Global button sizing */
+ .btn {
+ font-size: 0.85rem !important;
+ padding: 0.2rem 0.5rem !important;
+ }
+
+ .btn-sm {
+ font-size: 0.75rem !important;
+ padding: 0.2rem 0.4rem !important;
+ }
+
+ /* Dropdown controls */
+ .dropdown-toggle {
+ font-size: 0.85rem !important;
+ padding: 0.2rem 0.5rem !important;
+ line-height: 1.2 !important;
+ }
+
+ .dropdown-item {
+ font-size: 0.8rem !important;
+ }
+
+ .dropdown-menu-custom .dropdown-item {
+ font-size: 0.8rem !important;
+ }
+
+ /* Form controls */
+ .form-control {
+ font-size: 0.85rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ .input-group-text {
+ font-size: 0.75rem !important;
+ padding: 0.25rem 0.5rem !important;
+ }
+
+ /* Text elements */
+ small {
+ font-size: 0.65rem !important;
+ }
+
+ .text-white-50 {
+ font-size: 0.6rem !important;
+ }
+
+ /* Headings */
+ h4 {
+ font-size: 1.1rem !important;
+ }
+
+ h5 {
+ font-size: 1rem !important;
+ }
+
+ /* Spinner */
+ .spinner-border {
+ width: 1.5rem !important;
+ height: 1.5rem !important;
+ border-width: 0.15rem !important;
+ }
+}
diff --git a/src/ExpensesCalculator.UI/tsconfig.json b/src/ExpensesCalculator.UI/tsconfig.json
index 5525117..3e01b56 100644
--- a/src/ExpensesCalculator.UI/tsconfig.json
+++ b/src/ExpensesCalculator.UI/tsconfig.json
@@ -4,6 +4,7 @@
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
+ "rootDir": "./src",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,