From ce2024306a219644aaece06767a5d888aa9451ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 12:26:34 +0000 Subject: [PATCH] Add IncludeSpecials toggle for station maps and Telegram bot settings Agent-Logs-Url: https://github.com/jjasloot/OVDB/sessions/d59c16da-f087-485a-98b1-5b039ffe8e65 Co-authored-by: jjasloot <5612709+jjasloot@users.noreply.github.com> --- ...60515122447_AddIncludeSpecials.Designer.cs | 1346 +++++++++++++++++ .../20260515122447_AddIncludeSpecials.cs | 40 + .../OVDBDatabaseContextModelSnapshot.cs | 6 + OVDB_database/Models/StationGrouping.cs | 1 + OVDB_database/Models/User.cs | 1 + OV_DB/Controllers/StationMapsController.cs | 10 +- OV_DB/Mappings/MappingExtensions.cs | 1 + OV_DB/Models/StationMapDTO.cs | 1 + .../station-maps-edit.component.html | 5 + .../station-maps-edit.component.ts | 2 + .../src/app/models/stationMap.model.ts | 1 + OV_DB/OVDBFrontend/src/assets/i18n/en.json | 1 + OV_DB/OVDBFrontend/src/assets/i18n/nl.json | 1 + OV_DB/Services/TelegramBotService.cs | 94 +- 14 files changed, 1500 insertions(+), 10 deletions(-) create mode 100644 OVDB_database/Migrations/20260515122447_AddIncludeSpecials.Designer.cs create mode 100644 OVDB_database/Migrations/20260515122447_AddIncludeSpecials.cs diff --git a/OVDB_database/Migrations/20260515122447_AddIncludeSpecials.Designer.cs b/OVDB_database/Migrations/20260515122447_AddIncludeSpecials.Designer.cs new file mode 100644 index 00000000..bd5a4dd3 --- /dev/null +++ b/OVDB_database/Migrations/20260515122447_AddIncludeSpecials.Designer.cs @@ -0,0 +1,1346 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using OVDB_database.Database; + +#nullable disable + +namespace OVDB_database.Migrations +{ + [DbContext(typeof(OVDBDatabaseContext))] + [Migration("20260515122447_AddIncludeSpecials")] + partial class AddIncludeSpecials + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("OVDB_database.Models.InviteCode", b => + { + b.Property("InviteCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InviteCodeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("DoesNotExpire") + .HasColumnType("tinyint(1)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("InviteCodeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("UserId"); + + b.ToTable("InviteCodes"); + }); + + modelBuilder.Entity("OVDB_database.Models.Map", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("Completed") + .HasColumnType("tinyint(1)"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("MapGuid") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OrderNr") + .HasColumnType("int"); + + b.Property("SharingLinkName") + .HasColumnType("longtext"); + + b.Property("ShowRouteInfo") + .HasColumnType("tinyint(1)"); + + b.Property("ShowRouteOutline") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("MapId"); + + b.HasIndex("MapGuid") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Maps"); + }); + + modelBuilder.Entity("OVDB_database.Models.Operator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("LogoContentType") + .HasColumnType("longtext"); + + b.Property("LogoFilePath") + .HasColumnType("longtext"); + + b.Property("Names") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Operators"); + }); + + modelBuilder.Entity("OVDB_database.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DeviceInfo") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("LastUsedAt") + .HasColumnType("datetime(6)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("IsRevoked"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("OVDB_database.Models.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FlagEmoji") + .HasColumnType("longtext"); + + b.Property("Geometry") + .HasColumnType("multipolygon"); + + b.Property("IsoCode") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OriginalName") + .HasColumnType("longtext"); + + b.Property("OsmRelationId") + .HasColumnType("bigint"); + + b.Property("ParentRegionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentRegionId"); + + b.ToTable("Regions"); + }); + + modelBuilder.Entity("OVDB_database.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("Responded") + .HasColumnType("datetime(6)"); + + b.Property("Response") + .HasColumnType("longtext"); + + b.Property("ResponseRead") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("OVDB_database.Models.Route", b => + { + b.Property("RouteId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RouteId")); + + b.Property("CalculatedDistance") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DescriptionNL") + .HasColumnType("longtext"); + + b.Property("FirstDateTime") + .HasColumnType("datetime(6)"); + + b.Property("From") + .HasColumnType("longtext"); + + b.Property("LineNumber") + .HasColumnType("longtext"); + + b.Property("LineString") + .HasColumnType("linestring"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OperatingCompany") + .HasColumnType("longtext"); + + b.Property("OverrideColour") + .HasColumnType("longtext"); + + b.Property("OverrideDistance") + .HasColumnType("double"); + + b.Property("RouteTypeId") + .HasColumnType("int"); + + b.Property("Share") + .HasColumnType("char(36)"); + + b.Property("To") + .HasColumnType("longtext"); + + b.Property("TrainlogType") + .HasColumnType("longtext"); + + b.HasKey("RouteId"); + + b.HasIndex("Name"); + + b.HasIndex("RouteTypeId"); + + b.HasIndex("Share"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstance", b => + { + b.Property("RouteInstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RouteInstanceId")); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DurationHours") + .HasColumnType("double"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("RouteId") + .HasColumnType("int"); + + b.Property("ScheduledEndTime") + .HasColumnType("datetime(6)"); + + b.Property("ScheduledStartTime") + .HasColumnType("datetime(6)"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TrawellingStatusId") + .HasColumnType("int"); + + b.HasKey("RouteInstanceId"); + + b.HasIndex("Date"); + + b.HasIndex("TrawellingStatusId"); + + b.HasIndex(new[] { "Date", "RouteId" }, "idx_routeinstances_date_routeid"); + + b.HasIndex(new[] { "RouteId", "Date" }, "idx_routeinstances_routeid_date"); + + b.ToTable("RouteInstances"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstanceMap", b => + { + b.Property("RouteMapId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RouteMapId")); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RouteInstanceId") + .HasColumnType("int"); + + b.HasKey("RouteMapId"); + + b.HasIndex("MapId"); + + b.HasIndex(new[] { "RouteInstanceId", "MapId" }, "idx_routeinstancemap_routeinstanceid_mapid"); + + b.ToTable("RouteInstanceMap"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstanceProperty", b => + { + b.Property("RouteInstancePropertyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RouteInstancePropertyId")); + + b.Property("Bool") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RouteInstanceId") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("RouteInstancePropertyId"); + + b.HasIndex("RouteInstanceId", "Key"); + + b.ToTable("RouteInstanceProperties"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteMap", b => + { + b.Property("RouteMapId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RouteMapId")); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("RouteId") + .HasColumnType("int"); + + b.HasKey("RouteMapId"); + + b.HasIndex("MapId"); + + b.HasIndex("RouteId"); + + b.ToTable("RoutesMaps"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteType", b => + { + b.Property("TypeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TypeId")); + + b.Property("Colour") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsTrain") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OrderNr") + .HasColumnType("int"); + + b.Property("TrainlogType") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("TypeId"); + + b.HasIndex("UserId"); + + b.ToTable("RouteTypes"); + }); + + modelBuilder.Entity("OVDB_database.Models.Station", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Elevation") + .HasColumnType("double"); + + b.Property("Hidden") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("Lattitude") + .HasColumnType("double"); + + b.Property("Longitude") + .HasColumnType("double"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Network") + .HasColumnType("longtext"); + + b.Property("Operator") + .HasColumnType("longtext"); + + b.Property("OsmId") + .HasColumnType("bigint"); + + b.Property("Special") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("StationCountryId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OsmId"); + + b.HasIndex("StationCountryId"); + + b.ToTable("Stations"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationCountry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OsmId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("StationCountries"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationGrouping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IncludeSpecials") + .HasColumnType("tinyint(1)"); + + b.Property("MapGuid") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OrderNr") + .HasColumnType("int"); + + b.Property("SharingLinkName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MapGuid") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("StationGroupings"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMap", b => + { + b.Property("StationMapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StationMapId")); + + b.Property("MapGuid") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NameNL") + .HasColumnType("longtext"); + + b.Property("OrderNr") + .HasColumnType("int"); + + b.Property("SharingLinkName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("StationMapId"); + + b.HasIndex("MapGuid") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("StationMaps"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMapCountry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IncludeSpecials") + .HasColumnType("tinyint(1)"); + + b.Property("StationCountryId") + .HasColumnType("int"); + + b.Property("StationMapId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StationCountryId"); + + b.HasIndex("StationMapId"); + + b.ToTable("StationMapCountries"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMergeIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Station1Id") + .HasColumnType("int"); + + b.Property("Station2Id") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Station2Id"); + + b.HasIndex("Station1Id", "Station2Id") + .IsUnique(); + + b.ToTable("StationMergeIgnores"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationVisit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("StationId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("StationId", "UserId") + .IsUnique(); + + b.ToTable("StationVisits"); + }); + + modelBuilder.Entity("OVDB_database.Models.TrawellingIgnoredStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IgnoredAt") + .HasColumnType("datetime(6)"); + + b.Property("TrawellingStatusId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "TrawellingStatusId") + .IsUnique(); + + b.ToTable("TrawellingIgnoredStatuses"); + }); + + modelBuilder.Entity("OVDB_database.Models.TrawellingStation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created_at"); + + b.Property("Ibnr") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("ibnr"); + + b.Property("Latitude") + .HasColumnType("double") + .HasColumnName("latitude"); + + b.Property("Longitude") + .HasColumnType("double") + .HasColumnName("longitude"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("name"); + + b.Property("RilIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("ril_identifier"); + + b.Property("TrawellingId") + .HasColumnType("int") + .HasColumnName("traewelling_id"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("TrawellingId") + .IsUnique(); + + b.ToTable("trawelling_stations"); + }); + + modelBuilder.Entity("OVDB_database.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EnableTrainlogExport") + .HasColumnType("tinyint(1)"); + + b.Property("Guid") + .HasColumnType("char(36)"); + + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("LastLogin") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("PreferredLanguage") + .HasColumnType("int"); + + b.Property("RefreshToken") + .HasColumnType("longtext"); + + b.Property("TelegramIncludeSpecials") + .HasColumnType("tinyint(1)"); + + b.Property("TelegramUserId") + .HasColumnType("bigint"); + + b.Property("TraewellingTagMappings") + .HasColumnType("longtext"); + + b.Property("TrainlogMaterialKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TrainlogOperatorMappings") + .HasColumnType("longtext"); + + b.Property("TrainlogRegistrationKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TrainlogSeatKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("TrawellingAccessToken") + .HasColumnType("longtext"); + + b.Property("TrawellingRefreshToken") + .HasColumnType("longtext"); + + b.Property("TrawellingTokenExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("TrawellingUsername") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("TelegramUserId") + .IsUnique() + .HasFilter("[TelegramUserId] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("OperatorRegion", b => + { + b.Property("OperatorsRunningTrainsId") + .HasColumnType("int"); + + b.Property("RunsTrainsInRegionsId") + .HasColumnType("int"); + + b.HasKey("OperatorsRunningTrainsId", "RunsTrainsInRegionsId"); + + b.HasIndex("RunsTrainsInRegionsId"); + + b.ToTable("OperatorRegion"); + }); + + modelBuilder.Entity("OperatorRegion1", b => + { + b.Property("OperatorsRestrictedToRegionId") + .HasColumnType("int"); + + b.Property("RestrictToRegionsId") + .HasColumnType("int"); + + b.HasKey("OperatorsRestrictedToRegionId", "RestrictToRegionsId"); + + b.HasIndex("RestrictToRegionsId"); + + b.ToTable("OperatorRegion1"); + }); + + modelBuilder.Entity("OperatorRoute", b => + { + b.Property("OperatorsId") + .HasColumnType("int"); + + b.Property("RoutesRouteId") + .HasColumnType("int"); + + b.HasKey("OperatorsId", "RoutesRouteId"); + + b.HasIndex("RoutesRouteId"); + + b.ToTable("OperatorRoute"); + }); + + modelBuilder.Entity("RegionRoute", b => + { + b.Property("RegionsId") + .HasColumnType("int"); + + b.Property("RoutesRouteId") + .HasColumnType("int"); + + b.HasKey("RegionsId", "RoutesRouteId"); + + b.HasIndex("RoutesRouteId"); + + b.ToTable("RegionRoute"); + }); + + modelBuilder.Entity("RegionStation", b => + { + b.Property("RegionsId") + .HasColumnType("int"); + + b.Property("StationsId") + .HasColumnType("int"); + + b.HasKey("RegionsId", "StationsId"); + + b.HasIndex("StationsId"); + + b.ToTable("RegionStation"); + }); + + modelBuilder.Entity("RegionStationGrouping", b => + { + b.Property("RegionsId") + .HasColumnType("int"); + + b.Property("StationGroupingsId") + .HasColumnType("int"); + + b.HasKey("RegionsId", "StationGroupingsId"); + + b.HasIndex("StationGroupingsId"); + + b.ToTable("RegionStationGrouping"); + }); + + modelBuilder.Entity("OVDB_database.Models.InviteCode", b => + { + b.HasOne("OVDB_database.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("OVDB_database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CreatedByUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.Map", b => + { + b.HasOne("OVDB_database.Models.User", "User") + .WithMany("Maps") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.RefreshToken", b => + { + b.HasOne("OVDB_database.Models.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.Region", b => + { + b.HasOne("OVDB_database.Models.Region", "ParentRegion") + .WithMany("SubRegions") + .HasForeignKey("ParentRegionId"); + + b.Navigation("ParentRegion"); + }); + + modelBuilder.Entity("OVDB_database.Models.Request", b => + { + b.HasOne("OVDB_database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.Route", b => + { + b.HasOne("OVDB_database.Models.RouteType", "RouteType") + .WithMany("Routes") + .HasForeignKey("RouteTypeId"); + + b.Navigation("RouteType"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstance", b => + { + b.HasOne("OVDB_database.Models.Route", "Route") + .WithMany("RouteInstances") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstanceMap", b => + { + b.HasOne("OVDB_database.Models.Map", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.RouteInstance", "RouteInstance") + .WithMany("RouteInstanceMaps") + .HasForeignKey("RouteInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("RouteInstance"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstanceProperty", b => + { + b.HasOne("OVDB_database.Models.RouteInstance", "RouteInstance") + .WithMany("RouteInstanceProperties") + .HasForeignKey("RouteInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RouteInstance"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteMap", b => + { + b.HasOne("OVDB_database.Models.Map", "Map") + .WithMany("RouteMaps") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Route", "Route") + .WithMany("RouteMaps") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteType", b => + { + b.HasOne("OVDB_database.Models.User", null) + .WithMany("RouteTypes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OVDB_database.Models.Station", b => + { + b.HasOne("OVDB_database.Models.StationCountry", "StationCountry") + .WithMany("Stations") + .HasForeignKey("StationCountryId"); + + b.Navigation("StationCountry"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationGrouping", b => + { + b.HasOne("OVDB_database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMap", b => + { + b.HasOne("OVDB_database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMapCountry", b => + { + b.HasOne("OVDB_database.Models.StationCountry", "StationCountry") + .WithMany("StationMapCountries") + .HasForeignKey("StationCountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.StationMap", "StationMap") + .WithMany("StationMapCountries") + .HasForeignKey("StationMapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("StationCountry"); + + b.Navigation("StationMap"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMergeIgnore", b => + { + b.HasOne("OVDB_database.Models.Station", "Station1") + .WithMany() + .HasForeignKey("Station1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Station", "Station2") + .WithMany() + .HasForeignKey("Station2Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Station1"); + + b.Navigation("Station2"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationVisit", b => + { + b.HasOne("OVDB_database.Models.Station", "Station") + .WithMany("StationVisits") + .HasForeignKey("StationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Station"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OVDB_database.Models.TrawellingIgnoredStatus", b => + { + b.HasOne("OVDB_database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OperatorRegion", b => + { + b.HasOne("OVDB_database.Models.Operator", null) + .WithMany() + .HasForeignKey("OperatorsRunningTrainsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Region", null) + .WithMany() + .HasForeignKey("RunsTrainsInRegionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OperatorRegion1", b => + { + b.HasOne("OVDB_database.Models.Operator", null) + .WithMany() + .HasForeignKey("OperatorsRestrictedToRegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Region", null) + .WithMany() + .HasForeignKey("RestrictToRegionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OperatorRoute", b => + { + b.HasOne("OVDB_database.Models.Operator", null) + .WithMany() + .HasForeignKey("OperatorsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Route", null) + .WithMany() + .HasForeignKey("RoutesRouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RegionRoute", b => + { + b.HasOne("OVDB_database.Models.Region", null) + .WithMany() + .HasForeignKey("RegionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Route", null) + .WithMany() + .HasForeignKey("RoutesRouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RegionStation", b => + { + b.HasOne("OVDB_database.Models.Region", null) + .WithMany() + .HasForeignKey("RegionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.Station", null) + .WithMany() + .HasForeignKey("StationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RegionStationGrouping", b => + { + b.HasOne("OVDB_database.Models.Region", null) + .WithMany() + .HasForeignKey("RegionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OVDB_database.Models.StationGrouping", null) + .WithMany() + .HasForeignKey("StationGroupingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OVDB_database.Models.Map", b => + { + b.Navigation("RouteMaps"); + }); + + modelBuilder.Entity("OVDB_database.Models.Region", b => + { + b.Navigation("SubRegions"); + }); + + modelBuilder.Entity("OVDB_database.Models.Route", b => + { + b.Navigation("RouteInstances"); + + b.Navigation("RouteMaps"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteInstance", b => + { + b.Navigation("RouteInstanceMaps"); + + b.Navigation("RouteInstanceProperties"); + }); + + modelBuilder.Entity("OVDB_database.Models.RouteType", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("OVDB_database.Models.Station", b => + { + b.Navigation("StationVisits"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationCountry", b => + { + b.Navigation("StationMapCountries"); + + b.Navigation("Stations"); + }); + + modelBuilder.Entity("OVDB_database.Models.StationMap", b => + { + b.Navigation("StationMapCountries"); + }); + + modelBuilder.Entity("OVDB_database.Models.User", b => + { + b.Navigation("Maps"); + + b.Navigation("RefreshTokens"); + + b.Navigation("RouteTypes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OVDB_database/Migrations/20260515122447_AddIncludeSpecials.cs b/OVDB_database/Migrations/20260515122447_AddIncludeSpecials.cs new file mode 100644 index 00000000..2af62033 --- /dev/null +++ b/OVDB_database/Migrations/20260515122447_AddIncludeSpecials.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OVDB_database.Migrations +{ + /// + public partial class AddIncludeSpecials : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TelegramIncludeSpecials", + table: "Users", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IncludeSpecials", + table: "StationGroupings", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TelegramIncludeSpecials", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IncludeSpecials", + table: "StationGroupings"); + } + } +} diff --git a/OVDB_database/Migrations/OVDBDatabaseContextModelSnapshot.cs b/OVDB_database/Migrations/OVDBDatabaseContextModelSnapshot.cs index dc788632..5e878604 100644 --- a/OVDB_database/Migrations/OVDBDatabaseContextModelSnapshot.cs +++ b/OVDB_database/Migrations/OVDBDatabaseContextModelSnapshot.cs @@ -561,6 +561,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("IncludeSpecials") + .HasColumnType("tinyint(1)"); + b.Property("MapGuid") .HasColumnType("char(36)"); @@ -813,6 +816,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RefreshToken") .HasColumnType("longtext"); + b.Property("TelegramIncludeSpecials") + .HasColumnType("tinyint(1)"); + b.Property("TelegramUserId") .HasColumnType("bigint"); diff --git a/OVDB_database/Models/StationGrouping.cs b/OVDB_database/Models/StationGrouping.cs index 59307ae0..af11ca7f 100644 --- a/OVDB_database/Models/StationGrouping.cs +++ b/OVDB_database/Models/StationGrouping.cs @@ -21,6 +21,7 @@ public class StationGrouping [JsonProperty] public string? SharingLinkName { get; set; } public int OrderNr { get; set; } + public bool IncludeSpecials { get; set; } public User? User { get; set; } public List Regions { get; set; } } diff --git a/OVDB_database/Models/User.cs b/OVDB_database/Models/User.cs index f519f219..06e41d2d 100644 --- a/OVDB_database/Models/User.cs +++ b/OVDB_database/Models/User.cs @@ -33,6 +33,7 @@ public class User public List Maps { get; set; } public List RouteTypes { get; set; } public long? TelegramUserId { get; set; } + public bool TelegramIncludeSpecials { get; set; } public PreferredLanguage? PreferredLanguage { get; set; } // Träwelling OAuth2 integration fields diff --git a/OV_DB/Controllers/StationMapsController.cs b/OV_DB/Controllers/StationMapsController.cs index 25c1dca4..c4ac5518 100644 --- a/OV_DB/Controllers/StationMapsController.cs +++ b/OV_DB/Controllers/StationMapsController.cs @@ -77,6 +77,7 @@ public async Task PutMap(StationMapDTO stationMap) dbStationMap.SharingLinkName = stationMap.SharingLinkName; dbStationMap.Name = stationMap.Name; dbStationMap.NameNL = stationMap.NameNL; + dbStationMap.IncludeSpecials = stationMap.IncludeSpecials; var regions = await _context.Regions.Where(r => stationMap.RegionIds.Contains(r.Id)).ToListAsync(); dbStationMap.Regions = regions; @@ -114,6 +115,7 @@ public async Task> PostMap(StationMapDTO stationMap) SharingLinkName = stationMap.SharingLinkName, Name = stationMap.Name, NameNL = stationMap.NameNL, + IncludeSpecials = stationMap.IncludeSpecials, UserId = userIdClaim, MapGuid = Guid.NewGuid() }; @@ -179,8 +181,12 @@ public async Task GetVisitedStations(string id) var regionIds = stationMap.Regions.Select(r => r.Id).ToList(); var stationsQuery = _context.Stations.AsQueryable(); stationsQuery = stationsQuery.Where(s => s.Regions.Any(r => regionIds.Contains(r.Id))) - .Where(s => s.Hidden == false) - .Where(s => s.Special == false); + .Where(s => s.Hidden == false); + + if (!stationMap.IncludeSpecials) + { + stationsQuery = stationsQuery.Where(s => s.Special == false); + } var stations = await stationsQuery.Select(s => new StationDTO { diff --git a/OV_DB/Mappings/MappingExtensions.cs b/OV_DB/Mappings/MappingExtensions.cs index a0cc309f..f26f1495 100644 --- a/OV_DB/Mappings/MappingExtensions.cs +++ b/OV_DB/Mappings/MappingExtensions.cs @@ -200,6 +200,7 @@ public static IQueryable SelectToStationMapDTO(this IQueryable r.Id).ToList() }); } diff --git a/OV_DB/Models/StationMapDTO.cs b/OV_DB/Models/StationMapDTO.cs index 184fdb7a..86380083 100644 --- a/OV_DB/Models/StationMapDTO.cs +++ b/OV_DB/Models/StationMapDTO.cs @@ -12,6 +12,7 @@ public class StationMapDTO public string NameNL { get; set; } public string MapGuid { get; set; } public string? SharingLinkName { get; set; } + public bool IncludeSpecials { get; set; } public List RegionIds { get; set; } = []; } } diff --git a/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.html b/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.html index 67631275..b21f1706 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.html +++ b/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.html @@ -26,6 +26,11 @@

+ + {{ "STATIONMAP.INCLUDESPECIALS" | translate }} + +
+ @for (region of regions; track region.id) { diff --git a/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.ts b/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.ts index ba39c7f1..c2e6af7a 100644 --- a/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.ts +++ b/OV_DB/OVDBFrontend/src/app/admin/station-maps-edit/station-maps-edit.component.ts @@ -64,6 +64,7 @@ export class StationMapsEditComponent implements OnInit { name: [this.map?.name ?? "", Validators.required], nameNL: this.map?.nameNL ?? "", sharingLinkName: this.map?.sharingLinkName ?? "", + includeSpecials: this.map?.includeSpecials ?? false, }); } @@ -96,6 +97,7 @@ export class StationMapsEditComponent implements OnInit { this.map.name = this.form.value.name; this.map.nameNL = this.form.value.nameNL; this.map.sharingLinkName = this.form.value.sharingLinkName; + this.map.includeSpecials = this.form.value.includeSpecials; this.map.regionIds = this.selectedOptions; if (this.map.id) { this.apiService.updateStationMap(this.map).subscribe(() => { diff --git a/OV_DB/OVDBFrontend/src/app/models/stationMap.model.ts b/OV_DB/OVDBFrontend/src/app/models/stationMap.model.ts index 5d4c608c..0c34c3c8 100644 --- a/OV_DB/OVDBFrontend/src/app/models/stationMap.model.ts +++ b/OV_DB/OVDBFrontend/src/app/models/stationMap.model.ts @@ -6,6 +6,7 @@ export interface StationMap { nameNL: string; mapGuid: string; sharingLinkName: string | null; + includeSpecials: boolean; regionIds: number[] } diff --git a/OV_DB/OVDBFrontend/src/assets/i18n/en.json b/OV_DB/OVDBFrontend/src/assets/i18n/en.json index b48ea9be..5d5678fc 100644 --- a/OV_DB/OVDBFrontend/src/assets/i18n/en.json +++ b/OV_DB/OVDBFrontend/src/assets/i18n/en.json @@ -283,6 +283,7 @@ "STATIONMAP.DELETEFRONT": "delete the stations map", "STATIONMAP.DELETEREAR": "from the database", "STATIONMAP.DESCRIPTION": "Here you can create stations maps. These are maps showing all stations in a certain region. You can then tick off the stations. If you are missing a certain region please create an issue on GitHub.", + "STATIONMAP.INCLUDESPECIALS": "Include special/historic stations", "FILTER.INCLUDELINECOLOURS": "Show line colours", "SAVE_GO_TO_INSTANCES": "Save and go to trips", "FILTER.LIMITTOSELECTEDAREAS": "Cut routes to selected areas. Attention: this will take longer.", diff --git a/OV_DB/OVDBFrontend/src/assets/i18n/nl.json b/OV_DB/OVDBFrontend/src/assets/i18n/nl.json index d12380a9..de6be696 100644 --- a/OV_DB/OVDBFrontend/src/assets/i18n/nl.json +++ b/OV_DB/OVDBFrontend/src/assets/i18n/nl.json @@ -281,6 +281,7 @@ "STATIONMAP.DELETEFRONT": "de stationskaart", "STATIONMAP.DELETEREAR": "uit de database wilt verwijderen", "STATIONMAP.DESCRIPTION": "Hier kun je stationskaarten aanmaken. Dat zijn kaarten waarop alle stations in een regio staan. Deze kun je dan afvinken. Mis je een gebied dat je wel graag wilt gebruiken, maak dan een issue aan op GitHub of stuur me een bericht.", + "STATIONMAP.INCLUDESPECIALS": "Bijzondere/historische stations tonen", "FILTER.INCLUDELINECOLOURS": "Toon lijnkleuren", "SAVE_GO_TO_INSTANCES": "Opslaan en naar ritten", "FILTER.LIMITTOSELECTEDAREAS": "Knip routes bij tot geselecteerde gebieden. Let op! Dit duurt wat langer.", diff --git a/OV_DB/Services/TelegramBotService.cs b/OV_DB/Services/TelegramBotService.cs index 65433763..54617473 100644 --- a/OV_DB/Services/TelegramBotService.cs +++ b/OV_DB/Services/TelegramBotService.cs @@ -84,6 +84,12 @@ public async Task HandleUpdateAsync(Update update) { await HandleCallbackQueryAsync(update.CallbackQuery); } + else if (update.Type == UpdateType.Message && + (update.Message.Text?.StartsWith("/settings") == true || + update.Message.Text == "⚙️ Settings")) + { + await HandleSettingsMessageAsync(update.Message); + } else if (update.Type == UpdateType.Message) { await HandleUnknownMessageAsync(update.Message); @@ -102,7 +108,7 @@ private async Task HandleLocationMessageAsync(Message message) return; } - var nearbyStations = await GetNearbyStationsAsync(location.Latitude, location.Longitude, user.Id); + var nearbyStations = await GetNearbyStationsAsync(location.Latitude, location.Longitude, user.Id, user.TelegramIncludeSpecials); var responseText = "Nearby stations:\n"; await _botClient.SendMessage(message.Chat.Id, responseText, replyMarkup: GetStationsInlineKeyboard(nearbyStations)); @@ -116,6 +122,12 @@ private string FormatStation(StationDTO station) private async Task HandleCallbackQueryAsync(CallbackQuery callbackQuery) { + if (callbackQuery.Data == "toggle_specials") + { + await HandleToggleSpecialsAsync(callbackQuery); + return; + } + var stationId = int.Parse(callbackQuery.Data); var userId = callbackQuery.From.Id; @@ -147,14 +159,21 @@ private async Task HandleCallbackQueryAsync(CallbackQuery callbackQuery) var percentageMessage = string.Empty; foreach (var region in regionIds) { - var totalStationsInRegion = await _dbContext.Stations.Where(s=>!s.Special && !s.Hidden).CountAsync(s => s.Regions.Any(r => r.Id== region)); - var visitedStationsInRegion = await _dbContext.StationVisits.CountAsync(sv => sv.UserId == user.Id && sv.Station.Regions.Any(r => r.Id == region) && !sv.Station.Special && !sv.Station.Hidden); + var totalQuery = _dbContext.Stations.Where(s => !s.Hidden).Where(s => s.Regions.Any(r => r.Id == region)); + var visitedQuery = _dbContext.StationVisits.Where(sv => sv.UserId == user.Id && sv.Station.Regions.Any(r => r.Id == region) && !sv.Station.Hidden); + if (!user.TelegramIncludeSpecials) + { + totalQuery = totalQuery.Where(s => !s.Special); + visitedQuery = visitedQuery.Where(sv => !sv.Station.Special); + } + var totalStationsInRegion = await totalQuery.CountAsync(); + var visitedStationsInRegion = await visitedQuery.CountAsync(); var regionName = await _dbContext.Regions.Where(r => r.Id == region).Select(r => r.Name).FirstOrDefaultAsync(); var percentageVisited = Math.Round((double)visitedStationsInRegion / totalStationsInRegion * 100, 2); percentageMessage += $"{regionName}: {percentageVisited}%\n\r"; } - await _botClient.SendMessage(callbackQuery.Message.Chat.Id, $"""{station.Name}: {(visited? "✅": "❌")}"""+ $"\n\r{percentageMessage}", replyMarkup: KeyboardButton.WithRequestLocation("Share your location")); + await _botClient.SendMessage(callbackQuery.Message.Chat.Id, $"""{station.Name}: {(visited? "✅": "❌")}"""+ $"\n\r{percentageMessage}", replyMarkup: GetMainReplyKeyboard()); await _botClient.AnswerCallbackQuery(callbackQuery.Id, "✅"); } else @@ -163,10 +182,64 @@ private async Task HandleCallbackQueryAsync(CallbackQuery callbackQuery) } } + private async Task HandleSettingsMessageAsync(Message message) + { + var userId = message.From.Id; + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.TelegramUserId == userId); + if (user == null) + { + await HandleUnknownUserAsync(message); + return; + } + await _botClient.SendMessage(message.Chat.Id, GetSettingsText(user.TelegramIncludeSpecials), replyMarkup: GetSettingsInlineKeyboard(user.TelegramIncludeSpecials)); + } + + private async Task HandleToggleSpecialsAsync(CallbackQuery callbackQuery) + { + var userId = callbackQuery.From.Id; + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.TelegramUserId == userId); + if (user == null) + { + await HandleUnknownUserAsync(callbackQuery.Message); + return; + } + + user.TelegramIncludeSpecials = !user.TelegramIncludeSpecials; + await _dbContext.SaveChangesAsync(); + + await _botClient.EditMessageText(callbackQuery.Message.Chat.Id, callbackQuery.Message.MessageId, GetSettingsText(user.TelegramIncludeSpecials), replyMarkup: GetSettingsInlineKeyboard(user.TelegramIncludeSpecials)); + await _botClient.AnswerCallbackQuery(callbackQuery.Id, user.TelegramIncludeSpecials ? "Special stations enabled ✅" : "Special stations disabled ❌"); + } + + private static string GetSettingsText(bool includeSpecials) + { + var status = includeSpecials ? "✅ Enabled" : "❌ Disabled"; + return $"⚙️ Settings\n\nSpecial/historic stations: {status}"; + } + + private static InlineKeyboardMarkup GetSettingsInlineKeyboard(bool includeSpecials) + { + var toggleLabel = includeSpecials ? "❌ Disable special/historic stations" : "✅ Enable special/historic stations"; + return new InlineKeyboardMarkup(new[] + { + new[] { InlineKeyboardButton.WithCallbackData(toggleLabel, "toggle_specials") } + }); + } + + private static ReplyKeyboardMarkup GetMainReplyKeyboard() + { + return new ReplyKeyboardMarkup(new[] + { + new[] { KeyboardButton.WithRequestLocation("📍 Share your location") }, + new[] { new KeyboardButton("⚙️ Settings") } + }) + { ResizeKeyboard = true }; + } + private async Task HandleUnknownMessageAsync(Message message) { var responseText = "Sorry, I didn't understand that. Please share your location to find nearby stations."; - await _botClient.SendMessage(message.Chat.Id, responseText, replyMarkup: KeyboardButton.WithRequestLocation("Share your location")); + await _botClient.SendMessage(message.Chat.Id, responseText, replyMarkup: GetMainReplyKeyboard()); } private async Task HandleUnknownUserAsync(Message message) @@ -175,10 +248,15 @@ private async Task HandleUnknownUserAsync(Message message) await _botClient.SendMessage(message.Chat.Id, responseText); } - private async Task> GetNearbyStationsAsync(double latitude, double longitude, int userId) + private async Task> GetNearbyStationsAsync(double latitude, double longitude, int userId, bool includeSpecials) { - var nearbyStations = await _dbContext.Stations - .Where(s => !s.Special && !s.Hidden) + var stationsQuery = _dbContext.Stations + .Where(s => !s.Hidden); + if (!includeSpecials) + { + stationsQuery = stationsQuery.Where(s => !s.Special); + } + var nearbyStations = await stationsQuery .OrderBy(s => (s.Lattitude - latitude) * (s.Lattitude - latitude) + (s.Longitude - longitude) * (s.Longitude - longitude)) .Take(5) .Select(s => new StationDTO