diff --git a/backend/trip/__init__.py b/backend/trip/__init__.py index 67bc602..9c73af2 100644 --- a/backend/trip/__init__.py +++ b/backend/trip/__init__.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.3.1" diff --git a/src/src/app/components/trip/trip.component.html b/src/src/app/components/trip/trip.component.html index 86e8ebc..d9d8c61 100644 --- a/src/src/app/components/trip/trip.component.html +++ b/src/src/app/components/trip/trip.component.html @@ -22,8 +22,8 @@
- - + +
} @@ -62,8 +62,8 @@ @defer { @if (flattenedTripItems.length) { - + Day @@ -97,7 +97,7 @@ } @else {-} - {{ tripitem.comment || '-' }} + {{ tripitem.comment || '-' }}
@if (tripitem.lat) { {{ tripitem.lat }}, {{ tripitem.lng }} } @@ -214,50 +214,31 @@
- } @else { -
+ } +
-

Days

- {{ trip?.name }} days +

Map

+ {{ trip?.name }} places
- +
-
- @defer { - @for (d of trip?.days; track d.id) { -
- {{ d.label }} -
- {{ - getDayStats(d).price || '-' }} {{ currency$ | async }} - {{ - getDayStats(d).places }} -
- -
- } @empty { - - } - } @placeholder (minimum 0.4s) { -
- -
- } -
+
+ @if (!selectedItem) {
-
+

Places

{{ trip?.name }} places + +
@@ -270,6 +251,7 @@
+ @if (!collapsedTripPlaces) {
@defer { @for (p of places; track p.id) { @@ -313,20 +295,104 @@
}
+ }
- } -
+
-
-

Map

- {{ trip?.name }} places +
+

Days

+ {{ trip?.name }} days + +
- +
-
+ @if (!collapsedTripDays) { +
+ @defer { + @for (d of trip?.days; track d.id) { +
+
+ {{ d.label }} +
+
+ {{ + getDayStats(d).price || '-' }} {{ currency$ | async }} + {{ + getDayStats(d).places }} + +
+ +
+
+ + +
+ } @empty { + + } + } @placeholder (minimum 0.4s) { +
+ +
+ } +
+ }
+ +
+
+

Review

+ {{ trip?.name }} pending/constraints + + +
+ + @if (!collapsedTripStatuses) { +
+ @defer { + @for (item of getReviewData; track item.id) { +
+
+ {{ item.text }} +
+
+ @if (item.status) { + {{ + item.status.label }} + } +
+
+ } @empty { +

+ Nothing to review +

+ } + } @placeholder (minimum 0.4s) { +
+ +
+ } +
+ } +
+ }
- \ No newline at end of file + + \ No newline at end of file diff --git a/src/src/app/components/trip/trip.component.scss b/src/src/app/components/trip/trip.component.scss index e69de29..7c6b484 100644 --- a/src/src/app/components/trip/trip.component.scss +++ b/src/src/app/components/trip/trip.component.scss @@ -0,0 +1,10 @@ +@media print { + .print-striped-rows tr:nth-child(even) { + background-color: #f9f9f9 !important; + } + + .print-striped-rows tr:nth-child(even) td:first-child.truncate { + //HACK: The "day" column is truncated + background-color: white !important; + } +} diff --git a/src/src/app/components/trip/trip.component.ts b/src/src/app/components/trip/trip.component.ts index 5d0f7d7..f8cf9cc 100644 --- a/src/src/app/components/trip/trip.component.ts +++ b/src/src/app/components/trip/trip.component.ts @@ -69,7 +69,14 @@ export class TripComponent implements AfterViewInit { places: PlaceWithUsage[] = []; flattenedTripItems: FlattenedTripItem[] = []; - menuItems: MenuItem[] = []; + menuTripActionsItems: MenuItem[] = []; + + menuTripDayActionsItems: MenuItem[] = []; + selectedTripDayForMenu: TripDay | undefined; + + collapsedTripPlaces: boolean = false; + collapsedTripDays: boolean = false; + collapsedTripStatuses: boolean = false; constructor( private apiService: ApiService, @@ -81,7 +88,7 @@ export class TripComponent implements AfterViewInit { this.currency$ = this.utilsService.currency$; this.statuses = this.utilsService.statuses; - this.menuItems = [ + this.menuTripActionsItems = [ { label: "Actions", items: [ @@ -112,6 +119,38 @@ export class TripComponent implements AfterViewInit { ], }, ]; + this.menuTripDayActionsItems = [ + { + label: "Actions", + items: [ + { + label: "Item", + icon: "pi pi-plus", + iconClass: "text-blue-500!", + command: () => { + this.addItem(); + }, + }, + { + label: "Edit", + icon: "pi pi-pencil", + command: () => { + if (!this.selectedTripDayForMenu) return; + this.editDay(this.selectedTripDayForMenu); + }, + }, + { + label: "Delete", + icon: "pi pi-trash", + iconClass: "text-red-500!", + command: () => { + if (!this.selectedTripDayForMenu) return; + this.deleteDay(this.selectedTripDayForMenu); + }, + }, + ], + }, + ]; } back() { @@ -166,6 +205,20 @@ export class TripComponent implements AfterViewInit { return stats; } + get getReviewData(): (TripItem & { status: TripStatus })[] { + if (!this.trip?.days) return []; + + const data = this.trip!.days.map((day) => + day.items.filter((item) => item.status), + ).flat(); + if (!data.length) return []; + + return data.map((item) => ({ + ...item, + status: this.statusToTripStatus(item.status as string), + })) as (TripItem & { status: TripStatus })[]; + } + statusToTripStatus(status?: string): TripStatus | undefined { if (!status) return undefined; return this.statuses.find((s) => s.label == status) as TripStatus; @@ -576,6 +629,7 @@ export class TripComponent implements AfterViewInit { this.trip?.days!, ); } + if (item.price) this.updateTotalPrice(item.price); }, }); }, diff --git a/src/src/app/modals/place-create-modal/place-create-modal.component.html b/src/src/app/modals/place-create-modal/place-create-modal.component.html index 7231410..2500f05 100644 --- a/src/src/app/modals/place-create-modal/place-create-modal.component.html +++ b/src/src/app/modals/place-create-modal/place-create-modal.component.html @@ -21,15 +21,19 @@ - + + [checkmark]="true" class="capitalize" fluid> + +
{{ category.name }}
+
+
diff --git a/src/src/app/modals/place-create-modal/place-create-modal.component.ts b/src/src/app/modals/place-create-modal/place-create-modal.component.ts index ec6a418..11d6270 100644 --- a/src/src/app/modals/place-create-modal/place-create-modal.component.ts +++ b/src/src/app/modals/place-create-modal/place-create-modal.component.ts @@ -21,6 +21,7 @@ import { FocusTrapModule } from "primeng/focustrap"; import { Category, Place } from "../../types/poi"; import { CheckboxModule } from "primeng/checkbox"; import { TooltipModule } from "primeng/tooltip"; +import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser"; @Component({ selector: "app-place-create-modal", @@ -114,30 +115,18 @@ export class PlaceCreateModalComponent { this.placeForm.get("lat")?.valueChanges.subscribe({ next: (value: string) => { - if (/\-?\d+\.\d+,\s\-?\d+\.\d+/.test(value)) { - let [lat, lng] = value.split(", "); - const latLength = lat.split(".")[1].length; - const lngLength = lng.split(".")[1].length; + const result = checkAndParseLatLng(value); + if (!result) return; + const [lat, lng] = result; - const latControl = this.placeForm.get("lat"); - const lngControl = this.placeForm.get("lng"); + const latControl = this.placeForm.get("lat"); + const lngControl = this.placeForm.get("lng"); - latControl?.setValue( - parseFloat(lat).toFixed(latLength > 5 ? 5 : latLength), - { - emitEvent: false, - }, - ); - lngControl?.setValue( - parseFloat(lng).toFixed(lngLength > 5 ? 5 : lngLength), - { - emitEvent: false, - }, - ); + latControl?.setValue(formatLatLng(lat), { emitEvent: false }); + lngControl?.setValue(formatLatLng(lng), { emitEvent: false }); - lngControl?.markAsDirty(); - lngControl?.updateValueAndValidity(); - } + lngControl?.markAsDirty(); + lngControl?.updateValueAndValidity(); }, }); } diff --git a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html index b48eb58..b9f640b 100644 --- a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html +++ b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.html @@ -3,7 +3,11 @@
+ formControlName="day_id" [checkmark]="true" class="capitalize" fluid> + +
{{ day.label }}
+
+
@@ -26,7 +30,7 @@ formControlName="place" [showClear]="true" class="capitalize" fluid> -
+
{{ place.name }}
@@ -61,6 +65,9 @@
}
+ +
{{ option.label }}
+
diff --git a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts index 03c3809..8f6687a 100644 --- a/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts +++ b/src/src/app/modals/trip-create-day-item-modal/trip-create-day-item-modal.component.ts @@ -15,6 +15,7 @@ import { SelectModule } from "primeng/select"; import { TextareaModule } from "primeng/textarea"; import { InputMaskModule } from "primeng/inputmask"; import { UtilsService } from "../../services/utils.service"; +import { checkAndParseLatLng, formatLatLng } from "../../shared/latlng-parser"; @Component({ selector: "app-trip-create-day-item-modal", @@ -70,6 +71,7 @@ export class TripCreateDayItemModalComponent { "", { validators: Validators.pattern("-?(90(\\.0+)?|[1-8]?\\d(\\.\\d+)?)"), + updateOn: "blur", }, ], lng: [ @@ -104,17 +106,26 @@ export class TripCreateDayItemModalComponent { this.itemForm.get("lat")?.setValue(p.lat); this.itemForm.get("lng")?.setValue(p.lng); this.itemForm.get("price")?.setValue(p.price || 0); + if (!this.itemForm.get("text")?.value) + this.itemForm.get("text")?.setValue(p.name); } }, }); this.itemForm.get("lat")?.valueChanges.subscribe({ next: (value: string) => { - if (/\-?\d+\.\d+,\s\-?\d+\.\d+/.test(value)) { - let [lat, lng] = value.split(", "); - this.itemForm.get("lat")?.setValue(parseFloat(lat).toFixed(5)); - this.itemForm.get("lng")?.setValue(parseFloat(lng).toFixed(5)); - } + const result = checkAndParseLatLng(value); + if (!result) return; + + const [lat, lng] = result; + const latControl = this.itemForm.get("lat"); + const lngControl = this.itemForm.get("lng"); + + latControl?.setValue(formatLatLng(lat), { emitEvent: false }); + lngControl?.setValue(formatLatLng(lng), { emitEvent: false }); + + lngControl?.markAsDirty(); + lngControl?.updateValueAndValidity(); }, }); } diff --git a/src/src/app/modals/trip-create-items-modal/trip-create-items-modal.component.html b/src/src/app/modals/trip-create-items-modal/trip-create-items-modal.component.html index eec7e8e..2f68c89 100644 --- a/src/src/app/modals/trip-create-items-modal/trip-create-items-modal.component.html +++ b/src/src/app/modals/trip-create-items-modal/trip-create-items-modal.component.html @@ -1,7 +1,11 @@
+ formControlName="day_id" [checkmark]="true" class="w-full capitalize" fluid> + +
{{ day.label }}
+
+
diff --git a/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.ts b/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.ts index d0e7197..77da70c 100644 --- a/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.ts +++ b/src/src/app/modals/trip-place-select-modal/trip-place-select-modal.component.ts @@ -10,7 +10,13 @@ import { SkeletonModule } from "primeng/skeleton"; @Component({ selector: "app-trip-place-select-modal", - imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, SkeletonModule], + imports: [ + FloatLabelModule, + InputTextModule, + ButtonModule, + ReactiveFormsModule, + SkeletonModule, + ], standalone: true, templateUrl: "./trip-place-select-modal.component.html", styleUrl: "./trip-place-select-modal.component.scss", @@ -27,11 +33,11 @@ export class TripPlaceSelectModalComponent { constructor( private apiService: ApiService, private ref: DynamicDialogRef, - private config: DynamicDialogConfig + private config: DynamicDialogConfig, ) { this.apiService.getPlaces().subscribe({ next: (places) => { - this.places = places; + this.places = places.sort((a, b) => a.name.localeCompare(b.name)); this.displayedPlaces = places; }, }); @@ -51,7 +57,9 @@ export class TripPlaceSelectModalComponent { let v = value.toLowerCase(); this.displayedPlaces = this.places.filter( - (p) => p.name.toLowerCase().includes(v) || p.description?.toLowerCase().includes(v) + (p) => + p.name.toLowerCase().includes(v) || + p.description?.toLowerCase().includes(v), ); }, }); @@ -62,7 +70,7 @@ export class TripPlaceSelectModalComponent { this.selectedPlacesID.splice(this.selectedPlacesID.indexOf(p.id), 1); this.selectedPlaces.splice( this.selectedPlaces.findIndex((place) => place.id === p.id), - 1 + 1, ); return; } diff --git a/src/src/app/shared/latlng-parser.ts b/src/src/app/shared/latlng-parser.ts new file mode 100644 index 0000000..638fa8c --- /dev/null +++ b/src/src/app/shared/latlng-parser.ts @@ -0,0 +1,64 @@ +const patternDEC = /^\s*(-?\d{1,3}(?:\.\d+)?)\s*,\s*(-?\d{1,3}(?:\.\d+)?)\s*$/; +const patternDD = + /^\s*(\d{1,3}(?:\.\d+)?)°?\s*([NS])\s*,\s*(\d{1,3}(?:\.\d+)?)°?\s*([EW])\s*$/i; +const patternDMS = + /^\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2})['′]\s*(\d{1,2}(?:\.\d+)?)["″]?\s*([EW])\s*$/i; +const patternDMM = + /^\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([NS])\s*,\s*(\d{1,3})°\s*(\d{1,2}(?:\.\d+)?)['′]?\s*([EW])\s*$/i; + +function _dmsToDecimal( + deg: number, + min: number, + sec: number, + dir: string, +): number { + let dec = deg + min / 60 + sec / 3600; + return /[SW]/i.test(dir) ? -dec : dec; +} + +function _dmmToDecimal(deg: number, min: number, dir: string): number { + let dec = deg + min / 60; + return /[SW]/i.test(dir) ? -dec : dec; +} + +export function formatLatLng(num: number): string { + const decimals = num.toString().split(".")[1]?.length || 0; + return num.toFixed(Math.min(decimals, 5)); +} + +export function checkAndParseLatLng(str: string): [number, number] | undefined { + // Parse DMS, DD, DDM to decimal [Lat, Lng] + const dec = str.match(patternDEC); + if (dec) { + const lat = parseFloat(dec[1]); + const lng = parseFloat(dec[2]); + if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) { + return [lat, lng]; + } + } + + const dd = str.match(patternDD); + if (dd) { + let lat = parseFloat(dd[1]); + let lng = parseFloat(dd[3]); + lat *= /S/i.test(dd[2]) ? -1 : 1; + lng *= /W/i.test(dd[4]) ? -1 : 1; + return [lat, lng]; + } + + const dms = str.match(patternDMS); + if (dms) { + const lat = _dmsToDecimal(+dms[1], +dms[2], +dms[3], dms[4]); + const lng = _dmsToDecimal(+dms[5], +dms[6], +dms[7], dms[8]); + return [lat, lng]; + } + + const dmm = str.match(patternDMM); + if (dmm) { + const lat = _dmmToDecimal(+dmm[1], +dmm[2], dmm[3]); + const lng = _dmmToDecimal(+dmm[4], +dmm[5], dmm[6]); + return [lat, lng]; + } + + return undefined; +} diff --git a/src/src/styles.scss b/src/src/styles.scss index 02f1138..35dadff 100644 --- a/src/src/styles.scss +++ b/src/src/styles.scss @@ -46,17 +46,6 @@ html { line-height: normal; } -@keyframes slideY { - 0% { - opacity: 0; - transform: translateY(20px); - } -} - -.slideY { - animation: slideY 0.3s both; -} - .class-tooltip { background: white; border-radius: 8px; @@ -112,35 +101,22 @@ html { border-top: 1px solid transparent; border-bottom: 1px solid transparent; - cursor: default; + cursor: pointer; outline: none; } -.leaflet-contextmenu a.leaflet-contextmenu-item-disabled { - opacity: 0.5; -} - .leaflet-contextmenu a.leaflet-contextmenu-item.over { background-color: #f1f3f7; border-top: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; } -.leaflet-contextmenu a.leaflet-contextmenu-item-disabled.over { - background-color: inherit; -} - .leaflet-contextmenu-icon { display: flex; align-items: center; width: 24px; } -.leaflet-contextmenu-separator { - border-bottom: 1px solid #ccc; - margin: 5px 0; -} - .image-marker { border-radius: 50%; border: 2px solid #405cf5; @@ -167,3 +143,14 @@ html { transform: translateX(-50%) translateY(0); } } + +@keyframes slide-x { + 0% { + opacity: 0; + transform: translateX(-20px); + } +} + +.slide-x { + animation: slide-x 0.3s both; +}