| 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;
+}
|