🔥 Unrestrict currency field

This commit is contained in:
itskovacs 2025-08-23 15:58:47 +02:00
parent 706892fcbe
commit 0abb938393
13 changed files with 58 additions and 38 deletions

View File

@ -0,0 +1,27 @@
"""Trip currency
Revision ID: 7e331b851cb7
Revises: 26c89b7466f2
Create Date: 2025-08-23 15:06:50.387366
"""
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op
# revision identifiers, used by Alembic.
revision = "7e331b851cb7"
down_revision = "26c89b7466f2"
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table("trip", schema=None) as batch_op:
batch_op.add_column(sa.Column("currency", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
def downgrade():
with op.batch_alter_table("trip", schema=None) as batch_op:
batch_op.drop_column("currency")

View File

@ -250,6 +250,7 @@ class PlaceRead(PlaceBase):
class TripBase(SQLModel): class TripBase(SQLModel):
name: str name: str
archived: bool | None = None archived: bool | None = None
currency: str | None = settings.DEFAULT_CURRENCY
class Trip(TripBase, table=True): class Trip(TripBase, table=True):
@ -294,6 +295,7 @@ class TripReadBase(TripBase):
image_id=obj.image_id, image_id=obj.image_id,
days=len(obj.days), days=len(obj.days),
collaborators=[TripMemberRead.serialize(m) for m in obj.memberships], collaborators=[TripMemberRead.serialize(m) for m in obj.memberships],
currency=obj.currency if obj.currency else settings.DEFAULT_CURRENCY,
) )
@ -318,6 +320,7 @@ class TripRead(TripBase):
places=[PlaceRead.serialize(place) for place in obj.places], places=[PlaceRead.serialize(place) for place in obj.places],
collaborators=[TripMemberRead.serialize(m) for m in obj.memberships], collaborators=[TripMemberRead.serialize(m) for m in obj.memberships],
shared=bool(obj.shares), shared=bool(obj.shares),
currency=obj.currency if obj.currency else settings.DEFAULT_CURRENCY,
) )

View File

@ -248,8 +248,7 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<p-floatlabel variant="in" class="md:col-span-2"> <p-floatlabel variant="in" class="md:col-span-2">
<p-select [options]="currencySigns" optionValue="s" optionLabel="c" inputId="currency" id="currency" <input id="currency" formControlName="currency" pInputText fluid />
class="capitalize" formControlName="currency" [checkmark]="true" [showClear]="true" fluid />
<label for="currency">Currency</label> <label for="currency">Currency</label>
</p-floatlabel> </p-floatlabel>
</div> </div>

View File

@ -96,7 +96,6 @@ export class DashboardComponent implements OnInit, AfterViewInit {
markerClusterGroup?: L.MarkerClusterGroup; markerClusterGroup?: L.MarkerClusterGroup;
gpxLayerGroup?: L.LayerGroup; gpxLayerGroup?: L.LayerGroup;
settings?: Settings; settings?: Settings;
currencySigns = UtilsService.currencySigns();
doNotDisplayOptions: SelectItemGroup[] = []; doNotDisplayOptions: SelectItemGroup[] = [];
places: Place[] = []; places: Place[] = [];
@ -117,8 +116,6 @@ export class DashboardComponent implements OnInit, AfterViewInit {
private router: Router, private router: Router,
private fb: FormBuilder, private fb: FormBuilder,
) { ) {
this.currencySigns = UtilsService.currencySigns();
this.settingsForm = this.fb.group({ this.settingsForm = this.fb.group({
map_lat: [ map_lat: [
"", "",

View File

@ -19,7 +19,7 @@
severity="help" /> severity="help" />
<span <span
class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{ class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
(totalPrice | number:'1.0-2') || '-' }} {{ currency$ | async }}</span> (totalPrice | number:'1.0-2') || '-' }} @if (totalPrice) { {{ trip.currency }} }</span>
</div> </div>
</div> </div>
</section> </section>
@ -131,7 +131,7 @@
</td>} </td>}
@if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span @if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.price }} {{ currency$ | async }}</span>}</td>} tripitem.price }} @if (tripitem.price) { {{ trip.currency }} }</span>}</td>}
@if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span @if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color" [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
@ -180,7 +180,7 @@
</td>} </td>}
@if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span @if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.price }} {{ currency$ | async }}</span>}</td>} tripitem.price }} @if (tripitem.price) { {{ trip.currency }} }</span>}</td>}
@if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span @if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color" [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
@ -267,7 +267,7 @@
@if (selectedItem.price) { @if (selectedItem.price) {
<div class="rounded-md shadow p-4"> <div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Price</p> <p class="font-bold mb-1">Price</p>
<p class="text-sm text-gray-500">{{ selectedItem.price }} {{ currency$ | async }}</p> <p class="text-sm text-gray-500">{{ selectedItem.price }} @if (selectedItem.price) { {{ trip.currency }} }</p>
</div> </div>
} }
@ -353,7 +353,7 @@
<span <span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{
p.price || '-' p.price || '-'
}} {{ currency$ | async }}</span> }} @if (p.price) { {{ trip.currency }} }</span>
@if (trip.collaborators.length) { @if (trip.collaborators.length) {
<span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ p.user <span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ p.user
@ -402,7 +402,7 @@
</div> </div>
<div class="flex items-center gap-2 flex-none"> <div class="flex items-center gap-2 flex-none">
<span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit dark:bg-gray-100/85">{{ <span class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit dark:bg-gray-100/85">{{
getDayStats(d).price || '-' }} {{ currency$ | async }}</span> getDayStats(d).price || '-' }} @if (getDayStats(d).price) { {{ trip.currency }} }</span>
<span class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md dark:bg-blue-100/85">{{ <span class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md dark:bg-blue-100/85">{{
getDayStats(d).places }}</span> getDayStats(d).places }}</span>
</div> </div>

View File

@ -54,14 +54,12 @@ import { InputTextModule } from "primeng/inputtext";
FormsModule, FormsModule,
MultiSelectModule, MultiSelectModule,
CheckboxModule, CheckboxModule,
AsyncPipe,
], ],
templateUrl: "./shared-trip.component.html", templateUrl: "./shared-trip.component.html",
styleUrls: ["./shared-trip.component.scss"], styleUrls: ["./shared-trip.component.scss"],
}) })
export class SharedTripComponent implements AfterViewInit { export class SharedTripComponent implements AfterViewInit {
token?: string; token?: string;
currency$: Observable<string>;
statuses: TripStatus[] = []; statuses: TripStatus[] = [];
trip?: Trip; trip?: Trip;
places: Place[] = []; places: Place[] = [];
@ -180,7 +178,6 @@ export class SharedTripComponent implements AfterViewInit {
private utilsService: UtilsService, private utilsService: UtilsService,
private route: ActivatedRoute, private route: ActivatedRoute,
) { ) {
this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses; this.statuses = this.utilsService.statuses;
this.tripTableSearchInput.valueChanges this.tripTableSearchInput.valueChanges
.pipe(takeUntilDestroyed(), debounceTime(300)) .pipe(takeUntilDestroyed(), debounceTime(300))

View File

@ -35,7 +35,7 @@
<span <span
class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{ class="bg-gray-100 text-gray-800 text-xs md:text-sm font-medium me-2 px-2.5 py-0.5 rounded min-w-fit dark:bg-gray-400">{{
(totalPrice | number:'1.0-2') || '-' }} {{ currency$ | async }}</span> (totalPrice | number:'1.0-2') || '-' }} @if (totalPrice) { {{ trip?.currency }} }</span>
</div> </div>
</div> </div>
</section> </section>
@ -161,7 +161,7 @@
</td>} </td>}
@if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span @if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.price }} {{ currency$ | async }}</span>}</td>} tripitem.price }} @if (tripitem.price) { {{ trip?.currency }} }</span>}</td>}
@if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span @if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color" [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
@ -210,7 +210,7 @@
</td>} </td>}
@if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span @if (tripTableSelectedColumns.includes('price')) {<td class="truncate">@if (tripitem.price) {<span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded">{{
tripitem.price }} {{ currency$ | async }}</span>}</td>} tripitem.price }} @if (tripitem.price) { {{ trip?.currency }} }</span>}</td>}
@if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span @if (tripTableSelectedColumns.includes('status')) {<td class="truncate">@if (tripitem.status) {<span
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color" [style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{ class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
@ -310,7 +310,8 @@
@if (selectedItem.price) { @if (selectedItem.price) {
<div class="rounded-md shadow p-4"> <div class="rounded-md shadow p-4">
<p class="font-bold mb-1">Price</p> <p class="font-bold mb-1">Price</p>
<p class="text-sm text-gray-500">{{ selectedItem.price }} {{ currency$ | async }}</p> <p class="text-sm text-gray-500">{{ selectedItem.price }} @if (selectedItem.price) { {{ trip?.currency }} }
</p>
</div> </div>
} }
@ -401,7 +402,7 @@
<span <span
class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ class="bg-gray-100 text-gray-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{
p.price || '-' p.price || '-'
}} {{ currency$ | async }}</span> }} @if (p.price) { {{ trip?.currency }} }</span>
@if (trip?.collaborators?.length) { @if (trip?.collaborators?.length) {
<span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ p.user <span class="bg-gray-100 text-gray-800 text-sm me-2 px-2.5 py-0.5 rounded dark:bg-gray-100/85">{{ p.user
@ -454,7 +455,7 @@
<div class="flex items-center gap-2 flex-none"> <div class="flex items-center gap-2 flex-none">
<span <span
class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit group-hover:hidden dark:bg-gray-100/85">{{ class="bg-gray-100 text-gray-800 text-sm px-2.5 py-0.5 rounded-md min-w-fit group-hover:hidden dark:bg-gray-100/85">{{
getDayStats(d).price || '-' }} {{ currency$ | async }}</span> getDayStats(d).price || '-' }} @if (getDayStats(d).price) { {{ trip?.currency }} }</span>
<span <span
class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-blue-100/85">{{ class="bg-blue-100 text-blue-800 text-sm px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-blue-100/85">{{
getDayStats(d).places }}</span> getDayStats(d).places }}</span>
@ -649,7 +650,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
class="text-center bg-gray-100 text-gray-800 text-xs px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-gray-100/85">- class="text-center bg-gray-100 text-gray-800 text-xs px-2.5 py-0.5 rounded-md group-hover:hidden dark:bg-gray-100/85">-
{{ currency$ | async }}</span> {{ trip?.currency }}</span>
<div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end"> <div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
@if (!m.invited_at) { @if (!m.invited_at) {

View File

@ -86,7 +86,6 @@ import { TripInviteMemberModalComponent } from "../../modals/trip-invite-member-
styleUrls: ["./trip.component.scss"], styleUrls: ["./trip.component.scss"],
}) })
export class TripComponent implements AfterViewInit { export class TripComponent implements AfterViewInit {
currency$: Observable<string>;
tripSharedURL$?: Observable<string>; tripSharedURL$?: Observable<string>;
statuses: TripStatus[] = []; statuses: TripStatus[] = [];
trip?: Trip; trip?: Trip;
@ -284,7 +283,6 @@ export class TripComponent implements AfterViewInit {
private utilsService: UtilsService, private utilsService: UtilsService,
private route: ActivatedRoute, private route: ActivatedRoute,
) { ) {
this.currency$ = this.utilsService.currency$;
this.statuses = this.utilsService.statuses; this.statuses = this.utilsService.statuses;
this.tripTableSearchInput.valueChanges this.tripTableSearchInput.valueChanges
.pipe(takeUntilDestroyed(), debounceTime(300)) .pipe(takeUntilDestroyed(), debounceTime(300))

View File

@ -1,11 +1,16 @@
<div pFocusTrap class="grid items-center gap-4" [formGroup]="tripForm"> <div pFocusTrap class="grid md:grid-cols-4 items-center gap-4" [formGroup]="tripForm">
<p-floatlabel variant="in"> <p-floatlabel class="md:col-span-3" variant="in">
<input id="name" formControlName="name" pInputText fluid (keyup.enter)="closeDialog()" /> <input id="name" formControlName="name" pInputText fluid (keyup.enter)="closeDialog()" />
<label for="name">Name</label> <label for="name">Name</label>
</p-floatlabel> </p-floatlabel>
<p-floatlabel variant="in">
<input id="currency" formControlName="currency" pInputText fluid />
<label for="currency">Currency</label>
</p-floatlabel>
@if (tripForm.get("id")?.value === -1) { @if (tripForm.get("id")?.value === -1) {
<div class="grid grid-cols-2 gap-2"> <div class="grid md:col-span-4 grid-cols-2 gap-2">
<p-floatlabel variant="in"> <p-floatlabel variant="in">
<p-datepicker id="from" formControlName="from" [iconDisplay]="'input'" [showIcon]="true" appendTo="body" fluid /> <p-datepicker id="from" formControlName="from" [iconDisplay]="'input'" [showIcon]="true" appendTo="body" fluid />
<label for="from">From</label> <label for="from">From</label>
@ -18,7 +23,7 @@
</div> </div>
} }
<div class="grid place-items-center"> <div class="md:col-span-4 grid place-items-center">
@if (tripForm.get("image_id")?.value) { @if (tripForm.get("image_id")?.value) {
<div class="max-w-80 max-h-80 relative group cursor-pointer" (click)="fileInput.click()"> <div class="max-w-80 max-h-80 relative group cursor-pointer" (click)="fileInput.click()">
<img [src]="tripForm.get('image')?.value" <img [src]="tripForm.get('image')?.value"

View File

@ -40,6 +40,7 @@ export class TripCreateModalComponent {
id: -1, id: -1,
name: ["", Validators.required], name: ["", Validators.required],
image: "", image: "",
currency: null,
image_id: null, image_id: null,
from: null, from: null,
to: null, to: null,

View File

@ -61,14 +61,4 @@ export class UtilsService {
const latlng = `${latMatch[1]},${lngMatch[1]}`; const latlng = `${latMatch[1]},${lngMatch[1]}`;
return [place, latlng]; return [place, latlng];
} }
static currencySigns(): { c: string; s: string }[] {
return [
{ c: "EUR", s: "€" },
{ c: "GBP", s: "£" },
{ c: "JPY", s: "¥" },
{ c: "USD", s: "$" },
{ c: "CHF", s: "CHF" },
];
}
} }

View File

@ -77,7 +77,7 @@
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">
<span class="text-gray-500">Price</span> <span class="text-gray-500">Price</span>
<span>{{ selectedPlace.price || '-' }} {{ currency$ | async }}</span> <span>{{ selectedPlace.price || '-' }} @if (selectedPlace.price) { {{ currency$ | async }} }</span>
</div> </div>
<div class="flex flex-col mb-4"> <div class="flex flex-col mb-4">

View File

@ -8,6 +8,7 @@ export interface TripBase {
user: string; user: string;
days: number; days: number;
collaborators: TripMember[]; collaborators: TripMember[];
currency: string;
} }
export interface Trip { export interface Trip {
@ -18,6 +19,7 @@ export interface Trip {
user: string; user: string;
days: TripDay[]; days: TripDay[];
collaborators: TripMember[]; collaborators: TripMember[];
currency: string;
// POST / PUT // POST / PUT
places: Place[]; places: Place[];