Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,346 changes: 1,346 additions & 0 deletions OVDB_database/Migrations/20260515122447_AddIncludeSpecials.Designer.cs

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions OVDB_database/Migrations/20260515122447_AddIncludeSpecials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace OVDB_database.Migrations
{
/// <inheritdoc />
public partial class AddIncludeSpecials : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "TelegramIncludeSpecials",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);

migrationBuilder.AddColumn<bool>(
name: "IncludeSpecials",
table: "StationGroupings",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TelegramIncludeSpecials",
table: "Users");

migrationBuilder.DropColumn(
name: "IncludeSpecials",
table: "StationGroupings");
}
}
}
6 changes: 6 additions & 0 deletions OVDB_database/Migrations/OVDBDatabaseContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)

MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));

b.Property<bool>("IncludeSpecials")
.HasColumnType("tinyint(1)");

b.Property<Guid>("MapGuid")
.HasColumnType("char(36)");

Expand Down Expand Up @@ -813,6 +816,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<string>("RefreshToken")
.HasColumnType("longtext");

b.Property<bool>("TelegramIncludeSpecials")
.HasColumnType("tinyint(1)");

b.Property<long?>("TelegramUserId")
.HasColumnType("bigint");

Expand Down
1 change: 1 addition & 0 deletions OVDB_database/Models/StationGrouping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
[JsonProperty]
public Guid MapGuid { get; set; }
[JsonProperty]
public string? SharingLinkName { get; set; }

Check warning on line 22 in OVDB_database/Models/StationGrouping.cs

View workflow job for this annotation

GitHub Actions / build-backend

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public int OrderNr { get; set; }
public bool IncludeSpecials { get; set; }
public User? User { get; set; }

Check warning on line 25 in OVDB_database/Models/StationGrouping.cs

View workflow job for this annotation

GitHub Actions / build-backend

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public List<Region> Regions { get; set; }
}
}
1 change: 1 addition & 0 deletions OVDB_database/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class User
public List<Map> Maps { get; set; }
public List<RouteType> RouteTypes { get; set; }
public long? TelegramUserId { get; set; }
public bool TelegramIncludeSpecials { get; set; }
public PreferredLanguage? PreferredLanguage { get; set; }

// Träwelling OAuth2 integration fields
Expand Down
10 changes: 8 additions & 2 deletions OV_DB/Controllers/StationMapsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public async Task<IActionResult> 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;
Expand Down Expand Up @@ -114,6 +115,7 @@ public async Task<ActionResult<Map>> PostMap(StationMapDTO stationMap)
SharingLinkName = stationMap.SharingLinkName,
Name = stationMap.Name,
NameNL = stationMap.NameNL,
IncludeSpecials = stationMap.IncludeSpecials,
UserId = userIdClaim,
MapGuid = Guid.NewGuid()
};
Expand Down Expand Up @@ -179,8 +181,12 @@ public async Task<IActionResult> 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
{
Expand Down
1 change: 1 addition & 0 deletions OV_DB/Mappings/MappingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public static IQueryable<StationMapDTO> SelectToStationMapDTO(this IQueryable<St
NameNL = m.NameNL,
MapGuid = m.MapGuid.ToString(),
SharingLinkName = m.SharingLinkName,
IncludeSpecials = m.IncludeSpecials,
RegionIds = m.Regions.Select(r => r.Id).ToList()
});
}
Expand Down
1 change: 1 addition & 0 deletions OV_DB/Models/StationMapDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> RegionIds { get; set; } = [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ <h3 mat-dialog-title>
<input matInput formControlName="sharingLinkName" />
</mat-form-field>

<mat-checkbox formControlName="includeSpecials" style="margin-bottom: 1rem">
{{ "STATIONMAP.INCLUDESPECIALS" | translate }}
</mat-checkbox>
<br />

@for (region of regions; track region.id) {
<mat-expansion-panel [disabled]="region.subRegions.length === 0">
<mat-expansion-panel-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand Down Expand Up @@ -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(() => {
Expand Down
1 change: 1 addition & 0 deletions OV_DB/OVDBFrontend/src/app/models/stationMap.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface StationMap {
nameNL: string;
mapGuid: string;
sharingLinkName: string | null;
includeSpecials: boolean;
regionIds: number[]
}

Expand Down
1 change: 1 addition & 0 deletions OV_DB/OVDBFrontend/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href=\"https://github.com/jjasloot/OVDB/\">GitHub</a>.",
"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.",
Expand Down
1 change: 1 addition & 0 deletions OV_DB/OVDBFrontend/src/assets/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href=\"https://github.com/jjasloot/OVDB/\">GitHub</a> 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.",
Expand Down
94 changes: 86 additions & 8 deletions OV_DB/Services/TelegramBotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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));
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -175,10 +248,15 @@ private async Task HandleUnknownUserAsync(Message message)
await _botClient.SendMessage(message.Chat.Id, responseText);
}

private async Task<List<StationDTO>> GetNearbyStationsAsync(double latitude, double longitude, int userId)
private async Task<List<StationDTO>> 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
Expand Down
Loading