Compare commits

...

10 Commits

29 changed files with 334 additions and 114 deletions

View File

@ -1,8 +1,8 @@
//----------------------
// <auto-generated>
// Generated REST API Client Code Generator v1.18.0.0 on 19.01.2025 19:58:28
// Using the tool OpenAPI Generator v7.10.0
// Generated REST API Client Code Generator v1.19.0.0 on 05.02.2025 08:46:11
// Using the tool OpenAPI Generator v7.11.0
// </auto-generated>
//----------------------
@ -381,6 +381,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -505,6 +506,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -635,6 +637,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -757,6 +760,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -1148,6 +1152,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -1278,6 +1283,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -1402,6 +1408,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -1879,6 +1886,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -1991,6 +1999,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -2105,6 +2114,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -2223,6 +2233,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -2347,6 +2358,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -2794,6 +2806,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -2918,6 +2931,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -3036,6 +3050,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -3166,6 +3181,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -3521,6 +3537,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -3651,6 +3668,7 @@ namespace BreCalClient.misc.Api
"application/json"
};
var localVarContentType = BreCalClient.misc.Client.ClientUtils.SelectHeaderContentType(_contentTypes);
var localVarMultipartFormData = localVarContentType == "multipart/form-data";
if (localVarContentType != null)
{
localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType);
@ -4014,7 +4032,7 @@ namespace BreCalClient.misc.Client
{
foreach (var value in headerParam.Value)
{
request.AddHeader(headerParam.Key, value);
request.AddOrUpdateHeader(headerParam.Key, value);
}
}
}
@ -4075,6 +4093,13 @@ namespace BreCalClient.misc.Client
}
}
}
if (options.HeaderParameters != null)
{
if (options.HeaderParameters.TryGetValue("Content-Type", out var contentTypes) && contentTypes.Any(header => header.Contains("multipart/form-data")))
{
request.AlwaysMultipartFormData = true;
}
}
return request;
}
/// <summary>
@ -6915,7 +6940,8 @@ namespace BreCalClient.misc.Model
/// <param name="notifyPopup">notifyPopup.</param>
/// <param name="exp">exp.</param>
/// <param name="token">token.</param>
public LoginResult(int id = default(int), int participantId = default(int), string firstName = default(string), string lastName = default(string), string userName = default(string), string userPhone = default(string), string userEmail = default(string), bool? notifyEmail = default(bool?), bool? notifyWhatsapp = default(bool?), bool? notifySignal = default(bool?), bool? notifyPopup = default(bool?), float exp = default(float), string token = default(string))
/// <param name="notifyOn">notifyOn.</param>
public LoginResult(int id = default(int), int participantId = default(int), string firstName = default(string), string lastName = default(string), string userName = default(string), string userPhone = default(string), string userEmail = default(string), bool? notifyEmail = default(bool?), bool? notifyWhatsapp = default(bool?), bool? notifySignal = default(bool?), bool? notifyPopup = default(bool?), float exp = default(float), string token = default(string), List<NotificationType> notifyOn = default(List<NotificationType>))
{
this.Id = id;
this.ParticipantId = participantId;
@ -6930,6 +6956,7 @@ namespace BreCalClient.misc.Model
this.NotifyPopup = notifyPopup;
this.Exp = exp;
this.Token = token;
this.NotifyOn = notifyOn;
}
/// <summary>
/// Gets or Sets Id
@ -7036,6 +7063,14 @@ namespace BreCalClient.misc.Model
[DataMember(Name = "token", EmitDefaultValue = true)]
public string Token { get; set; }
/// <summary>
/// Gets or Sets NotifyOn
/// </summary>
/*
<example>[&quot;assignment&quot;,&quot;next24h&quot;]</example>
*/
[DataMember(Name = "notify_on", EmitDefaultValue = true)]
public List<NotificationType> NotifyOn { get; set; }
/// <summary>
/// Returns the string presentation of the object
/// </summary>
/// <returns>String presentation of the object</returns>
@ -7056,6 +7091,7 @@ namespace BreCalClient.misc.Model
sb.Append(" NotifyPopup: ").Append(NotifyPopup).Append("\n");
sb.Append(" Exp: ").Append(Exp).Append("\n");
sb.Append(" Token: ").Append(Token).Append("\n");
sb.Append(" NotifyOn: ").Append(NotifyOn).Append("\n");
sb.Append("}\n");
return sb.ToString();
}
@ -8804,7 +8840,8 @@ namespace BreCalClient.misc.Model
/// <param name="notifyPopup">notifyPopup.</param>
/// <param name="notifyWhatsapp">notifyWhatsapp.</param>
/// <param name="notifySignal">notifySignal.</param>
public UserDetails(int id = default(int), string oldPassword = default(string), string newPassword = default(string), string firstName = default(string), string lastName = default(string), string userPhone = default(string), string userEmail = default(string), bool? notifyEmail = default(bool?), bool? notifyPopup = default(bool?), bool? notifyWhatsapp = default(bool?), bool? notifySignal = default(bool?))
/// <param name="notifyOn">notifyOn.</param>
public UserDetails(int id = default(int), string oldPassword = default(string), string newPassword = default(string), string firstName = default(string), string lastName = default(string), string userPhone = default(string), string userEmail = default(string), bool? notifyEmail = default(bool?), bool? notifyPopup = default(bool?), bool? notifyWhatsapp = default(bool?), bool? notifySignal = default(bool?), List<NotificationType> notifyOn = default(List<NotificationType>))
{
this.Id = id;
this.OldPassword = oldPassword;
@ -8817,6 +8854,7 @@ namespace BreCalClient.misc.Model
this.NotifyPopup = notifyPopup;
this.NotifyWhatsapp = notifyWhatsapp;
this.NotifySignal = notifySignal;
this.NotifyOn = notifyOn;
}
/// <summary>
/// Gets or Sets Id
@ -8907,6 +8945,14 @@ namespace BreCalClient.misc.Model
[DataMember(Name = "notify_signal", EmitDefaultValue = true)]
public bool? NotifySignal { get; set; }
/// <summary>
/// Gets or Sets NotifyOn
/// </summary>
/*
<example>[&quot;assignment&quot;,&quot;next24h&quot;]</example>
*/
[DataMember(Name = "notify_on", EmitDefaultValue = true)]
public List<NotificationType> NotifyOn { get; set; }
/// <summary>
/// Returns the string presentation of the object
/// </summary>
/// <returns>String presentation of the object</returns>
@ -8925,6 +8971,7 @@ namespace BreCalClient.misc.Model
sb.Append(" NotifyPopup: ").Append(NotifyPopup).Append("\n");
sb.Append(" NotifyWhatsapp: ").Append(NotifyWhatsapp).Append("\n");
sb.Append(" NotifySignal: ").Append(NotifySignal).Append("\n");
sb.Append(" NotifyOn: ").Append(NotifyOn).Append("\n");
sb.Append("}\n");
return sb.ToString();
}

View File

@ -1538,6 +1538,7 @@ components:
eta: '2023-08-21T08:23:35Z'
operation: update
type: shipcall
notification:
type: object
description: a notification created by the engine if a times entry violates a rule
@ -1735,6 +1736,14 @@ components:
token:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
notify_on:
type: array
nullable: true
items:
$ref: '#/components/schemas/NotificationType'
example:
- assignment
- next24h
example:
id: 42
participant_id: 5
@ -1798,6 +1807,14 @@ components:
type: boolean
nullable: true
example: false
notify_on:
type: array
nullable: true
items:
$ref: '#/components/schemas/NotificationType'
example:
- assignment
- next24h
example:
id: 42
old_password: oldpassword

View File

@ -0,0 +1,16 @@
# Versionshistorie
## 1.7
### YAML / API
1. Notifications GET: Der Parameter "shipcall_id" ist jetzt optional für den Abruf von Benachrichtigungen.
2. Notification: Enthält jetzt ein neues Feld "participant_id". Ist dieses gesetzt, richtet sich die Benachrichtigung an diesen Teilnehmer. Ist das Feld nicht vorhanden, richtet sich die Benachrichtigung an alle Beteiligten des shipcall
3. Die Benutzerdaten (login_result) enthalten jetzt die Felder (Flags) der Zuordnung für die verschiedenen Benachrichtigungs-Wege, aktuell implementiert ist notify_email und notify_popup. Diese können auch über user_details analog zu Telefonnummer, Name etc. gesetzt werden.
4. Die Enumeration NotificationType enthält jetzt nicht mehr den Benachrichtigungsweg, sondern den Typ des Ereignisses, das die Benachrichtigung ausgelöst hat. Aktuell werden 7 Ereignisse unterschieden.
5. Die Benutzerdaten enthalten eine Liste NotifyOn vom Typ NotificationType. In dieser Aufzählung sind die Ereignisse enthalten, über die der Benutzer benachrichtigt werden will. Wenn diese Liste leer oder nicht vorhanden ist erhält der Benutzer keine Nachrichten, auch wenn er einen Benachrichtigungsweg ausgewählt hat.

View File

@ -8,3 +8,6 @@ ADD CONSTRAINT `FK_NOTIFICATION_PARTICIPANT`
REFERENCES `participant` (`id`)
ON DELETE RESTRICT
ON UPDATE RESTRICT;
ALTER TABLE `user`
ADD COLUMN `notify_event` INT NULL COMMENT 'Bitflag of selected notification event types that the user wants to be notified of' AFTER `notify_popup`;

View File

@ -1 +1 @@
1.7.0.5
1.8.0.0

View File

@ -7,11 +7,12 @@
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:p = "clr-namespace:BreCalClient.Resources"
mc:Ignorable="d" Left="{local:SettingBinding W1Left}" Top="{local:SettingBinding W1Top}"
Title="Help" Height="496" Width="500" Loaded="Window_Loaded">
Title="Help" Height="512" Width="800" Loaded="Window_Loaded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
@ -25,6 +26,7 @@
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="10" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
@ -48,7 +50,7 @@
Informatikbüro Daniel Schick
</Hyperlink>
</TextBlock>
<Border BorderThickness="0 0 0 2" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" BorderBrush="Gray" />
<Border BorderThickness="0 0 0 2" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" BorderBrush="Gray" />
<Label FontWeight="DemiBold" Grid.Row="3" Grid.Column="0" Content="{x:Static p:Resources.textChangeContactInfo}" HorizontalContentAlignment="Right"/>
<Label Grid.Row="4" Grid.Column="0" Content="{x:Static p:Resources.textEmail}" HorizontalContentAlignment="Right" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static p:Resources.textPhone}" HorizontalContentAlignment="Right" />
@ -60,19 +62,21 @@
<Label Grid.Row="8" Grid.Column="1" Content="{x:Static p:Resources.textNotifyEmail}" />
<CheckBox HorizontalAlignment="Right" Margin="2" Grid.Column="0" Grid.Row="9" VerticalAlignment="Center" x:Name="checkboxPushNotify" />
<Label Grid.Row="9" Grid.Column="1" Content="{x:Static p:Resources.textNotifyPush}" />
<Button x:Name="buttonChangeUserFields" Click="buttonChangeUserFields_Click" Grid.Column="1" Grid.Row="10" Margin="2" Content="{x:Static p:Resources.textChange}" Width="80" HorizontalAlignment="Left" IsEnabled="True" />
<Label Grid.Row="7" Grid.Column="2" Content="{x:Static p:Resources.textNotifyOn}" />
<xctk:CheckListBox Grid.Column="2" Grid.Row="8" Grid.RowSpan="3" x:Name="checkListBoxEventSelection" Margin="2" />
<Button x:Name="buttonChangeUserFields" Click="buttonChangeUserFields_Click" Grid.Column="2" Grid.Row="11" Margin="2" Content="{x:Static p:Resources.textChange}" Width="80" HorizontalAlignment="Right" IsEnabled="True" />
<Border BorderThickness="0 0 0 2" Grid.Row="11" Grid.Column="0" Grid.ColumnSpan="2" BorderBrush="Gray" />
<Label FontWeight="DemiBold" Grid.Row="12" Grid.Column="0" Content="{x:Static p:Resources.textChangePassword}" HorizontalContentAlignment="Right"/>
<Border BorderThickness="0 0 0 2" Grid.Row="12" Grid.Column="0" Grid.ColumnSpan="3" BorderBrush="Gray" />
<Label FontWeight="DemiBold" Grid.Row="13" Grid.Column="0" Content="{x:Static p:Resources.textChangePassword}" HorizontalContentAlignment="Right"/>
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textOldPassword}" Grid.Column="1" Grid.Row="12" Margin="2" x:Name="wpBoxOldPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textNewPassword}" Grid.Column="1" Grid.Row="13" Margin="2" x:Name="wpBoxNewPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textRepeatNewPassword}" Grid.Column="1" Grid.Row="14" Margin="2" x:Name="wpBoxNewPasswordRepeat" TextChanged="wpBoxOldPassword_TextChanged"/>
<Button x:Name="buttonChangePassword" Click="buttonChangePassword_Click" Grid.Column="1" Grid.Row="15" Margin="2" Content="{x:Static p:Resources.textChangePassword}" Width="120" HorizontalAlignment="Left" IsEnabled="False" />
<Border BorderThickness="0 0 0 2" Grid.Row="16" Grid.Column="0" Grid.ColumnSpan="2" BorderBrush="Gray" />
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textOldPassword}" Grid.Column="1" Grid.Row="13" Margin="2" x:Name="wpBoxOldPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textNewPassword}" Grid.Column="1" Grid.Row="14" Margin="2" x:Name="wpBoxNewPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textRepeatNewPassword}" Grid.Column="1" Grid.Row="15" Margin="2" x:Name="wpBoxNewPasswordRepeat" TextChanged="wpBoxOldPassword_TextChanged"/>
<Button x:Name="buttonChangePassword" Click="buttonChangePassword_Click" Grid.Column="1" Grid.Row="16" Margin="2" Content="{x:Static p:Resources.textChangePassword}" Width="120" HorizontalAlignment="Right" IsEnabled="False" />
<Border BorderThickness="0 0 0 2" Grid.Row="17" Grid.Column="0" Grid.ColumnSpan="3" BorderBrush="Gray" />
<Button x:Name="buttonClose" Click="buttonClose_Click" Content="{x:Static p:Resources.textClose}" Width="80" Margin="2" Grid.Column="1" Grid.Row="18" HorizontalAlignment="Right" />
<Button x:Name="buttonClose" Click="buttonClose_Click" Content="{x:Static p:Resources.textClose}" Width="80" Margin="2" Grid.Column="2" Grid.Row="19" HorizontalAlignment="Right" />
</Grid>
</Window>

View File

@ -57,7 +57,12 @@ namespace BreCalClient
this.LoginResult.UserPhone = this.textBoxUserPhone.Text.Trim();
this.LoginResult.UserEmail = this.textBoxUserEmail.Text.Trim();
this.LoginResult.NotifyEmail = this.checkboxEMailNotify.IsChecked ?? false;
this.LoginResult.NotifyPopup = this.checkboxPushNotify.IsChecked ?? false;
this.LoginResult.NotifyPopup = this.checkboxPushNotify.IsChecked ?? false;
if ((this.checkListBoxEventSelection.SelectedItems.Count > 0) && (this.LoginResult.NotifyOn == null))
this.LoginResult.NotifyOn = new();
this.LoginResult.NotifyOn.Clear();
foreach (NotificationType nt in this.checkListBoxEventSelection.SelectedItems)
this.LoginResult.NotifyOn.Add(nt);
this.ChangeUserSettingsRequested?.Invoke();
}
}
@ -80,12 +85,19 @@ namespace BreCalClient
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.checkListBoxEventSelection.ItemsSource = Enum.GetValues(typeof(BreCalClient.misc.Model.NotificationType));
if(LoginResult != null)
{
this.textBoxUserEmail.Text = LoginResult.UserEmail;
this.textBoxUserPhone.Text = LoginResult.UserPhone;
this.checkboxEMailNotify.IsChecked = LoginResult.NotifyEmail;
this.checkboxPushNotify.IsChecked = LoginResult.NotifyPopup;
if (LoginResult.NotifyOn != null)
{
foreach (NotificationType nt in LoginResult.NotifyOn)
this.checkListBoxEventSelection.SelectedItems.Add(nt);
}
}
}

View File

@ -32,7 +32,7 @@
<value>#1D751F</value>
</setting>
<setting name="APP_TITLE" serializeAs="String">
<value>!!Bremen calling Testversion!!</value>
<value>!!Bremen calling Entwicklungsversion!!</value>
</setting>
<setting name="LOGO_IMAGE_URL" serializeAs="String">
<value>https://www.textbausteine.net/</value>

View File

@ -8,24 +8,17 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using ToastNotifications.Core;
using BreCalClient.misc.Model;
using System.Runtime.CompilerServices;
namespace BreCalClient
{
internal class AppNotification
internal class AppNotification(int id)
{
private static readonly Dictionary<int, AppNotification> _notifications = new();
private readonly int _id;
private static readonly ObservableCollection<AppNotification> _notificationsCollection = new();
public AppNotification(int id)
{
_id = id;
}
private static readonly Dictionary<int, AppNotification> _notifications = [];
private static readonly ObservableCollection<AppNotification> _notificationsCollection = [];
#region Properties
public int Id { get { return _id; } }
public int Id { get { return id; } }
public string? NotificationType
{
@ -95,7 +88,7 @@ namespace BreCalClient
SaveNotifications();
}
internal static bool UpdateNotifications(List<Notification> notifications, System.Collections.Concurrent.ConcurrentDictionary<int, ShipcallControlModel> currentShipcalls, ToastViewModel vm)
internal static bool UpdateNotifications(List<Notification> notifications, System.Collections.Concurrent.ConcurrentDictionary<int, ShipcallControlModel> currentShipcalls, ToastViewModel vm, LoginResult loginResult)
{
bool result = false;
@ -121,6 +114,10 @@ namespace BreCalClient
if (!iAmAssigned) continue;
}
// filter out notifications the user is not interested in
if((notification.Type != null) && !loginResult.NotifyOn.Contains(notification.Type.Value))
continue;
if (!_notificationsCollection.Where(x => x.Id == notification.Id).Any())
{
List<AppNotification> newList = new(_notificationsCollection);
@ -176,9 +173,8 @@ namespace BreCalClient
if (!string.IsNullOrEmpty(ap.Message))
toastText += $" \n{ap.Message}";
if (!_notifications.ContainsKey(notification.Id))
if (_notifications.TryAdd(notification.Id, ap))
{
_notifications.Add(notification.Id, ap);
App.Current.Dispatcher.Invoke(() =>
{
vm.ShowAppNotification(toastText, options);
@ -197,7 +193,7 @@ namespace BreCalClient
internal static void SaveNotifications()
{
if (Properties.Settings.Default.Notifications == null)
Properties.Settings.Default.Notifications = new();
Properties.Settings.Default.Notifications = [];
else
Properties.Settings.Default.Notifications.Clear();
foreach (int notification_id in _notifications.Keys)

View File

@ -2,14 +2,14 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<SignAssembly>True</SignAssembly>
<StartupObject>BreCalClient.App</StartupObject>
<AssemblyOriginatorKeyFile>..\..\misc\brecal.snk</AssemblyOriginatorKeyFile>
<AssemblyVersion>1.7.0.5</AssemblyVersion>
<FileVersion>1.7.0.5</FileVersion>
<AssemblyVersion>1.8.0.0</AssemblyVersion>
<FileVersion>1.8.0.0</FileVersion>
<Title>Bremen calling client</Title>
<Description>A Windows WPF client for the Bremen calling API.</Description>
<ApplicationIcon>containership.ico</ApplicationIcon>
@ -122,8 +122,8 @@
<PackageReference Include="JsonSubTypes" Version="2.0.1" />
<PackageReference Include="log4net" Version="3.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="Polly" Version="8.5.1" />
<PackageReference Include="RestSharp" Version="112.0.0" />
<PackageReference Include="ToastNotifications" Version="2.5.1" />
<PackageReference Include="ToastNotifications.Messages" Version="2.5.1" />
</ItemGroup>

View File

@ -7,7 +7,7 @@
xmlns:p = "clr-namespace:BreCalClient.Resources"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d" Left="{local:SettingBinding W1Left}" Top="{local:SettingBinding W1Top}"
Title="{x:Static p:Resources.textEditTimes}" Height="331" Width="500" Loaded="Window_Loaded" ResizeMode="CanResizeWithGrip" Icon="Resources/containership.ico">
Title="{x:Static p:Resources.textEditTimes}" Height="415" Width="500" Loaded="Window_Loaded" ResizeMode="CanResizeWithGrip" Icon="Resources/containership.ico">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".20*" />
@ -22,6 +22,9 @@
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" x:Name="rowt1" />
<RowDefinition Height="28" x:Name="rowt2" />
<RowDefinition Height="28" x:Name="rowt3" />
<RowDefinition Height="*" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
@ -35,8 +38,10 @@
<Label Grid.Row="4" Grid.Column="0" Content="ATD" HorizontalContentAlignment="Right" x:Name="labelATD" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static p:Resources.textLockTime}" HorizontalContentAlignment="Right" />
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static p:Resources.textZoneEntryTime}" HorizontalContentAlignment="Right" />
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static p:Resources.textRemarks}" HorizontalContentAlignment="Right" />
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static p:Resources.textTidalWindow}" HorizontalContentAlignment="Right" />
<Label Grid.Row="8" Grid.Column="0" Content="{x:Static p:Resources.textFrom}" HorizontalContentAlignment="Right" />
<Label Grid.Row="9" Grid.Column="0" Content="{x:Static p:Resources.textTo}" HorizontalContentAlignment="Right" />
<Label Grid.Row="10" Grid.Column="0" Content="{x:Static p:Resources.textRemarks}" HorizontalContentAlignment="Right" />
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
@ -137,7 +142,7 @@
</ContextMenu>
</xctk:DateTimePicker.ContextMenu>
</local:DateTimePickerExt>
<!--CheckBox IsEnabled="False" Grid.Row="3" Grid.Column="2" Margin="4,0,0,0" Name="checkBoxLockTimeFixed" VerticalAlignment="Center" /-->
<local:DateTimePickerExt IsEnabled="False" Grid.Row="6" Grid.Column="1" Margin="2" x:Name="datePickerZoneEntry" Format="Custom" FormatString="dd.MM. yyyy HH:mm">
<xctk:DateTimePicker.ContextMenu>
<ContextMenu>
@ -149,10 +154,12 @@
</ContextMenu>
</xctk:DateTimePicker.ContextMenu>
</local:DateTimePickerExt>
<!--CheckBox IsEnabled="False" Grid.Row="4" Grid.Column="2" Margin="4,0,0,0" Name="checkBoxZoneEntryFixed" VerticalAlignment="Center" /-->
<xctk:DateTimePicker Name="datePickerTidalWindowFrom" Grid.Column="1" Grid.Row="8" Margin="2" IsEnabled="False" Format="Custom" FormatString="dd.MM. yyyy HH:mm"/>
<xctk:DateTimePicker Name="datePickerTidalWindowTo" Grid.Column="1" Grid.Row="9" Margin="2" IsEnabled="False" Format="Custom" FormatString="dd.MM. yyyy HH:mm"/>
<TextBox Grid.Row="7" Grid.Column="1" Margin="2" Name="textBoxRemarks" TextWrapping="Wrap" AcceptsReturn="True" SpellCheck.IsEnabled="True" AcceptsTab="False" IsReadOnly="True" MaxLength="512"/>
<StackPanel Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBox Grid.Row="10" Grid.Column="1" Margin="2" Name="textBoxRemarks" TextWrapping="Wrap" AcceptsReturn="True" SpellCheck.IsEnabled="True" AcceptsTab="False" IsReadOnly="True" MaxLength="512"/>
<StackPanel Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width= "80" Margin="2" Content="{x:Static p:Resources.textOK}" x:Name="buttonOK" Click="buttonOK_Click" />
<Button Width="80" Margin="2" Content="{x:Static p:Resources.textCancel}" x:Name="buttonCancel" Click="buttonCancel_Click"/>
<Button Width="28" x:Name="buttonClearAll" Click="buttonClearAll_Click" Margin="2" IsEnabled="False">

View File

@ -86,6 +86,8 @@ namespace BreCalClient
this.datePickerLockTime.Value = null;
this.datePickerZoneEntry.Value = null;
this.textBoxRemarks.Text = null;
this.datePickerTidalWindowFrom.Value = null;
this.datePickerTidalWindowTo.Value = null;
}
}
@ -153,6 +155,30 @@ namespace BreCalClient
return false;
}
if ((Extensions.ParticipantType)this.Times.ParticipantType == Extensions.ParticipantType.PILOT)
{
if ((this.datePickerTidalWindowFrom.Value != this.ShipcallModel.Shipcall?.TidalWindowFrom) || (this.datePickerTidalWindowTo.Value != this.ShipcallModel.Shipcall?.TidalWindowTo)) // something has changed
{
if (datePickerTidalWindowTo.Value.IsTooOld() || this.datePickerTidalWindowFrom.Value.IsTooOld())
{
message = BreCalClient.Resources.Resources.textTideTimesInThePast;
return false;
}
if (this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowFrom.Value > this.datePickerTidalWindowTo.Value)
{
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
return false;
}
if ((this.datePickerTidalWindowFrom.Value.HasValue && !this.datePickerTidalWindowTo.Value.HasValue) || (!this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue))
{
message = BreCalClient.Resources.Resources.textTidalBothValues;
return false;
}
}
}
return true;
}
@ -167,6 +193,13 @@ namespace BreCalClient
this.Times.ZoneEntry = this.datePickerZoneEntry.Value;
this.Times.Ata = this.datePickerATA.Value;
this.Times.Atd = this.datePickerATD.Value;
Extensions.ParticipantType pType = (Extensions.ParticipantType)this.Times.ParticipantType;
if ((pType == Extensions.ParticipantType.PILOT) && this.ShipcallModel.Shipcall != null)
{
this.ShipcallModel.Shipcall.TidalWindowFrom = this.datePickerTidalWindowFrom.Value;
this.ShipcallModel.Shipcall.TidalWindowTo = this.datePickerTidalWindowTo.Value;
}
}
private void CopyToControls()
@ -218,6 +251,19 @@ namespace BreCalClient
}
}
Extensions.ParticipantType pType = (Extensions.ParticipantType)this.Times.ParticipantType;
if ((pType == Extensions.ParticipantType.PILOT) && this.ShipcallModel.Shipcall != null)
{
this.datePickerTidalWindowFrom.Value = this.ShipcallModel.Shipcall.TidalWindowFrom;
this.datePickerTidalWindowTo.Value = this.ShipcallModel.Shipcall.TidalWindowTo;
}
else
{
this.rowt1.Height = new(0);
this.rowt2.Height = new(0);
this.rowt3.Height = new(0);
}
this.SetLockButton(this.Times.EtaBerthFixed ?? false);
}
@ -283,8 +329,12 @@ namespace BreCalClient
this.datePickerLockTime.IsEnabled = true;
break;
case Extensions.ParticipantType.TUG:
this.datePickerZoneEntry.IsEnabled = (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival);
break;
case Extensions.ParticipantType.PILOT:
this.datePickerZoneEntry.IsEnabled = (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival);
this.datePickerTidalWindowFrom.IsEnabled = true;
this.datePickerTidalWindowTo.IsEnabled = true;
break;
}
}

View File

@ -52,7 +52,7 @@ namespace BreCalClient
private readonly ConcurrentDictionary<int, ShipcallControlModel> _allShipcallsDict = new();
private readonly ConcurrentDictionary<int, ShipcallControl> _allShipCallsControlDict = new();
private readonly List<ShipcallControlModel> _visibleControlModels = new();
private readonly List<ShipcallControlModel> _visibleControlModels = [];
private readonly ShipcallApi _shipcallApi;
private readonly UserApi _userApi;
@ -195,7 +195,7 @@ namespace BreCalClient
}
catch (ApiException ex)
{
if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith("{"))) {
if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith('{'))) {
Error? anError = JsonConvert.DeserializeObject<Error>((string)ex.ErrorContent);
if ((anError != null) && anError.ErrorField.Equals("invalid credentials"))
this.labelLoginResult.Content = BreCalClient.Resources.Resources.textWrongCredentials;
@ -309,7 +309,7 @@ namespace BreCalClient
scmOut.Shipcall.DepartureBerthId = esc.ShipcallModel.Shipcall?.ArrivalBerthId;
if (esc.ShipcallModel.Shipcall != null)
{
scmOut.Shipcall.Participants = new();
scmOut.Shipcall.Participants = [];
scmOut.Shipcall.Participants.AddRange(esc.ShipcallModel.Shipcall.Participants);
foreach(ParticipantType pType in esc.ShipcallModel.AssignedParticipants.Keys)
scmOut.AssignedParticipants[pType] = esc.ShipcallModel.AssignedParticipants[pType];
@ -368,8 +368,13 @@ namespace BreCalClient
NotifyEmail = _loginResult.NotifyEmail,
NotifyPopup = _loginResult.NotifyPopup,
NotifySignal = _loginResult.NotifySignal,
NotifyWhatsapp = _loginResult.NotifyWhatsapp
NotifyWhatsapp = _loginResult.NotifyWhatsapp,
};
if (_loginResult.NotifyOn != null)
{
ud.NotifyOn = new(_loginResult.NotifyOn);
}
try
{
await _userApi.UserUpdateAsync(ud);
@ -413,7 +418,7 @@ namespace BreCalClient
{
this.searchFilterControl.SearchFilter.Ports.Clear();
List<Berth> berths = new();
List<Berth> berths = [];
foreach (Port port in comboBoxPorts.SelectedItems)
{
this.searchFilterControl.SearchFilter.Ports.Add(port.Id);
@ -444,8 +449,8 @@ namespace BreCalClient
_historyDialog.Closed += (sender, e) => { this._historyDialog = null; };
_historyDialog.HistoryItemSelected += (x) =>
{
if(_allShipCallsControlDict.ContainsKey(x))
_allShipCallsControlDict[x].BringIntoView();
if(_allShipCallsControlDict.TryGetValue(x, out ShipcallControl? value))
value.BringIntoView();
};
_historyDialog.Show();
}
@ -509,14 +514,14 @@ namespace BreCalClient
SearchFilterModel? currentFilter = null;
if (SearchFilterModel.filterMap != null)
{
if((_loginResult != null) && SearchFilterModel.filterMap.ContainsKey(_loginResult.Id))
if((_loginResult != null) && SearchFilterModel.filterMap.TryGetValue(_loginResult.Id, out SearchFilterModel? value))
{
currentFilter = SearchFilterModel.filterMap[_loginResult.Id];
currentFilter = value;
}
}
else
{
SearchFilterModel.filterMap = new();
SearchFilterModel.filterMap = [];
}
if (currentFilter == null)
{
@ -602,7 +607,7 @@ namespace BreCalClient
// load times for each shipcall
List<Times> currentTimes = await _timesApi.TimesGetAsync(shipcall.Id);
if (!_allShipcallsDict.ContainsKey(shipcall.Id))
if (!_allShipcallsDict.TryGetValue(shipcall.Id, out ShipcallControlModel? value))
{
// add entry
ShipcallControlModel scm = new()
@ -614,10 +619,9 @@ namespace BreCalClient
}
else
{
// update entry
_allShipcallsDict[shipcall.Id].Shipcall = shipcall;
_allShipcallsDict[shipcall.Id].Times = currentTimes;
UpdateShipcall(_allShipcallsDict[shipcall.Id]);
value.Shipcall = shipcall;
value.Times = currentTimes;
UpdateShipcall(value);
}
}
@ -669,7 +673,7 @@ namespace BreCalClient
if (_loginResult?.NotifyPopup ?? false)
{
List<Notification> notifications = await _staticApi.NotificationsGetAsync();
AppNotification.UpdateNotifications(notifications, _allShipcallsDict, _vm);
AppNotification.UpdateNotifications(notifications, _allShipcallsDict, _vm, _loginResult);
}
}
}
@ -684,8 +688,8 @@ namespace BreCalClient
_allShipcallsDict[scm.Shipcall.Id] = scm;
Shipcall shipcall = scm.Shipcall;
if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId))
scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId].Ship;
if (BreCalLists.ShipLookupDict.TryGetValue(shipcall.ShipId, out ShipModel? value))
scm.Ship = value.Ship;
if (shipcall.Type == ShipcallType.Arrival)
{
@ -718,8 +722,8 @@ namespace BreCalClient
{
if(scm.Shipcall == null) return;
Shipcall shipcall = scm.Shipcall;
if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId))
scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId].Ship;
if (BreCalLists.ShipLookupDict.TryGetValue(shipcall.ShipId, out ShipModel? value))
scm.Ship = value.Ship;
if (shipcall.Type == ShipcallType.Arrival)
{
@ -964,10 +968,10 @@ namespace BreCalClient
foreach (ShipcallControlModel visibleModel in this._visibleControlModels)
{
if (visibleModel.Shipcall == null) continue; // should not happen
if (this._allShipCallsControlDict.ContainsKey(visibleModel.Shipcall.Id))
if (this._allShipCallsControlDict.TryGetValue(visibleModel.Shipcall.Id, out ShipcallControl? value))
{
this._allShipCallsControlDict[visibleModel.Shipcall.Id].RefreshData();
this.stackPanel.Children.Add(this._allShipCallsControlDict[visibleModel.Shipcall.Id]);
value.RefreshData();
this.stackPanel.Children.Add(value);
}
}
}
@ -1073,6 +1077,13 @@ namespace BreCalClient
etc.Times.Id = apiResultId.VarId;
obj.ShipcallControlModel?.Times.Add(etc.Times);
}
// a pilot may have changed the tidal window so we update the shipcall too in this case
if(((Extensions.ParticipantType)etc.Times.ParticipantType) == ParticipantType.PILOT)
{
await _shipcallApi.ShipcallUpdateAsync(obj.ShipcallControlModel?.Shipcall);
}
_refreshImmediately = true;
_tokenSource.Cancel();
}
@ -1110,8 +1121,8 @@ namespace BreCalClient
}
else
{
if(editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY))
editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId;
if(editControl.ShipcallModel.AssignedParticipants.TryGetValue(ParticipantType.AGENCY, out ParticipantAssignment? value))
editControl.Times.ParticipantId = value.ParticipantId;
}
editControl.Times.ParticipantType = (int)ParticipantType.AGENCY;
if(editControl.ShowDialog() ?? false)
@ -1127,9 +1138,9 @@ namespace BreCalClient
}
// always try to be the agent, even if we are BSMD
if (editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY))
if (editControl.ShipcallModel.AssignedParticipants.TryGetValue(ParticipantType.AGENCY, out ParticipantAssignment? value))
{
editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId;
editControl.Times.ParticipantId = value.ParticipantId;
}
else
{
@ -1178,7 +1189,7 @@ namespace BreCalClient
// (if the special-flag is enabled). Assigned Agency: ShipcallParticipantMap(id=628, shipcall_id=115, participant_id=10,
// type=8, created=datetime.datetime(2024, 8, 28, 15, 13, 14), modified=None) with Flags: 42\"}
Match m = Regex.Match(message, "\\{(.*)\\}");
Match m = ErrorRegex().Match(message);
if ((m != null) && m.Success)
{
try
@ -1223,7 +1234,10 @@ namespace BreCalClient
e.Handled = true;
}
[GeneratedRegex("\\{(.*)\\}")]
private static partial Regex ErrorRegex();
#endregion
}
}

View File

@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<ApplicationRevision>1</ApplicationRevision>
<ApplicationVersion>1.7.0.5</ApplicationVersion>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.8.0.0</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Debug</Configuration>
<CreateDesktopShortcut>True</CreateDesktopShortcut>
@ -21,7 +21,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<OpenBrowserOnPublish>False</OpenBrowserOnPublish>
<Platform>Any CPU</Platform>
<ProductName>Bremen calling development client</ProductName>
<PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
<PublishDir>bin\Debug\net8.0-windows7.0\win-x64\app.publish\</PublishDir>
<PublishUrl>bin\publish.devel\</PublishUrl>
<PublisherName>Informatikbüro Daniel Schick</PublisherName>
<PublishProtocol>ClickOnce</PublishProtocol>
@ -33,12 +33,12 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<SignManifests>False</SignManifests>
<SuiteName>Bremen calling</SuiteName>
<SupportUrl>https://www.textbausteine.net/</SupportUrl>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<UpdateEnabled>True</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateRequired>True</UpdateRequired>
<WebPageFileName>Publish.html</WebPageFileName>
<MinimumRequiredVersion>1.7.0.5</MinimumRequiredVersion>
<MinimumRequiredVersion>1.8.0.0</MinimumRequiredVersion>
<SkipPublishVerification>false</SkipPublishVerification>
</PropertyGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@ namespace BreCalClient.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@ -34,7 +34,7 @@ namespace BreCalClient.Properties {
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("!!Bremen calling Testversion!!")]
[global::System.Configuration.DefaultSettingValueAttribute("!!Bremen calling Entwicklungsversion!!")]
public string APP_TITLE {
get {
return ((string)(this["APP_TITLE"]));

View File

@ -6,7 +6,7 @@
<Value Profile="(Default)">#1D751F</Value>
</Setting>
<Setting Name="APP_TITLE" Type="System.String" Scope="Application">
<Value Profile="(Default)">!!Bremen calling Testversion!!</Value>
<Value Profile="(Default)">!!Bremen calling Entwicklungsversion!!</Value>
</Setting>
<Setting Name="LOGO_IMAGE_URL" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://www.textbausteine.net/</Value>

View File

@ -948,6 +948,15 @@ namespace BreCalClient.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Notify on.
/// </summary>
public static string textNotifyOn {
get {
return ResourceManager.GetString("textNotifyOn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Notify by push notification in app.
/// </summary>

View File

@ -595,4 +595,7 @@
<data name="textShipcallCancelled" xml:space="preserve">
<value>Der Anlauf wurde storniert</value>
</data>
<data name="textNotifyOn" xml:space="preserve">
<value>Benachrichtigung bei</value>
</data>
</root>

View File

@ -646,4 +646,7 @@
<data name="textShipcallCancelled" xml:space="preserve">
<value>The shipcall was cancelled</value>
</data>
<data name="textNotifyOn" xml:space="preserve">
<value>Notify on</value>
</data>
</root>

View File

@ -6,8 +6,8 @@
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon>
<FileVersion>1.7.0.5</FileVersion>
<AssemblyVersion>1.7.0.5</AssemblyVersion>
<FileVersion>1.8.0.0</FileVersion>
<AssemblyVersion>1.8.0.0</AssemblyVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -2,8 +2,7 @@ from flask import Blueprint, request
from ..schemas import model
from .. import impl
from ..services.auth_guard import auth_guard
import json
import logging
from marshmallow import ValidationError
from . import verify_if_request_is_json
from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response
@ -16,11 +15,11 @@ def PutUser():
try:
verify_if_request_is_json(request)
content = request.get_json(force=True)
loadedModel = model.UserSchema().load(data=content, many=False, partial=True)
return impl.user.PutUser(loadedModel)
except ValidationError as ex:
return create_validation_error_response(ex=ex, status_code=400)

View File

@ -237,7 +237,7 @@ class SQLQuery():
@staticmethod
def get_user()->str:
query = "SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +\
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +\
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, notify_event, created, modified FROM user " +\
"WHERE user_name = ?username? OR user_email = ?username?"
return query

View File

@ -8,6 +8,7 @@ from .. import local_db
from ..services import jwt_handler
from BreCal.database.sql_queries import SQLQuery
def GetUser(options):
try:
@ -18,7 +19,7 @@ def GetUser(options):
# query = SQLQuery.get_user()
# data = commands.query(query, model=model.User, param={"username" : options["username"]})
data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, notify_event, created, modified FROM user " +
"WHERE user_name = ?username? OR user_email = ?username?",
model=model.User, param={"username" : options["username"]})
@ -35,7 +36,8 @@ def GetUser(options):
"notify_email": data[0].notify_email,
"notify_whatsapp": data[0].notify_whatsapp,
"notify_signal": data[0].notify_signal,
"notify_popup": data[0].notify_popup
"notify_popup": data[0].notify_popup,
"notify_on": model.bitflag_to_list(data[0].notify_event)
}
token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins
result["token"] = token # add token to user data

View File

@ -35,7 +35,7 @@ def PutUser(schemaModel):
# should this be refactored?
# Also, what about the 'user_name'?
# 'participant_id' would also not trigger an update in isolation
if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel or "notify_email" in schemaModel or "notify_whatsapp" in schemaModel or "notify_signal" in schemaModel or "notify_popup" in schemaModel:
if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel or "notify_email" in schemaModel or "notify_whatsapp" in schemaModel or "notify_signal" in schemaModel or "notify_popup" in schemaModel or "notify_on" in schemaModel:
# query = SQLQuery.get_user_put(schemaModel)
query = "UPDATE user SET "
isNotFirst = False
@ -49,7 +49,14 @@ def PutUser(schemaModel):
if isNotFirst:
query += ", "
isNotFirst = True
query += key + " = ?" + key + "? "
if key != "notify_on":
query += key + " = ?" + key + "? "
else:
flag_value = model.list_to_bitflag(schemaModel["notify_on"])
query += "notify_event = " + str(flag_value) + " "
query += "WHERE id = ?id?"
affected_rows = commands.execute(query, param=schemaModel)

View File

@ -10,11 +10,12 @@ from typing import List
import json
import re
import datetime
from BreCal.validators.time_logic import validate_time_is_in_not_too_distant_future
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
from BreCal.database.enums import ParticipantType, ParticipantFlag
# from BreCal. ... import check_if_user_is_bsmd_type
def obj_dict(obj):
if isinstance(obj, datetime.datetime):
@ -84,6 +85,21 @@ class NotificationType(IntEnum):
def _missing_(cls, value):
return cls.undefined
def bitflag_to_list(bitflag: int) -> list[NotificationType]:
if bitflag is None:
return []
"""Converts an integer bitflag to a list of NotificationType enums."""
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
def list_to_bitflag(notifications: fields.List) -> int:
"""Converts a list of NotificationType enums to an integer bitflag."""
try:
iter(notifications)
return sum(1 << (nt.value - 1) for nt in notifications)
except TypeError as te:
return 0
class ShipcallType(IntEnum):
undefined = 0
arrival = 1
@ -497,6 +513,7 @@ class UserSchema(Schema):
notify_whatsapp = fields.Bool(allow_none=True, required=False)
notify_signal = fields.Bool(allow_none=True, required=False)
notify_popup = fields.Bool(allow_none=True, required=False)
notify_on = fields.List(fields.Enum(NotificationType), required=False, allow_none=True)
@validates("user_phone")
def validate_user_phone(self, value):
@ -507,7 +524,7 @@ class UserSchema(Schema):
@validates("user_email")
def validate_user_email(self, value):
if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value) in value:
if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value):
raise ValidationError({"user_email":f"invalid email address"})
@ -556,10 +573,16 @@ class User:
notify_popup: bool
created: datetime
modified: datetime
ports: List[NotificationType] = field(default_factory=list)
notify_event: List[NotificationType] = field(default_factory=list)
def __hash__(self):
return hash(id)
def wants_notifications(self, notification_type: NotificationType):
events = bitflag_to_list(self.notify_event)
return notification_type in events
@dataclass
class Ship:
id: int

View File

@ -229,14 +229,14 @@ def SendNotifications():
users = users_dict[notification.participant_id]
for user in users:
# send notification to user
if user.notify_email:
if user.notify_email and user.wants_notifications(notification.type):
if user not in email_dict:
email_dict[user] = []
email_dict[user].append(notification)
if user.notify_whatsapp:
if user.notify_whatsapp and user.wants_notifications(notification.type):
# TBD
pass
if user.notify_signal:
if user.notify_signal and user.wants_notifications(notification.type):
# TBD
pass

View File

@ -18,18 +18,19 @@ def get_user_simple():
created = datetime.datetime.now()
modified = created+datetime.timedelta(seconds=10)
notify_email = True
notify_whatsapp = True
notify_signal = True
notify_popup = True
user = User(
user_id,
participant_id,
first_name,
last_name,
user_name,
user_id,
participant_id,
first_name,
last_name,
user_name,
user_email,
user_phone,
password_hash,

View File

@ -546,12 +546,18 @@ class InputValidationShipcall():
# query = 'SELECT * FROM shipcall_participant_map where (shipcall_id = ?shipcall_id? AND type=?participant_type?)'
# assigned_agency = execute_sql_query_standalone(query=query, model=ShipcallParticipantMap, param={"shipcall_id" : shipcall_id, "participant_type":int(ParticipantType.AGENCY)})
assigned_agency = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.AGENCY)
assigned_pilot = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.PILOT)
an_agency_is_assigned = True if assigned_agency is not None else False
a_pilot_is_assigned = True if assigned_pilot is not None else False
else:
# Agency assigned? User must belong to the assigned agency or be a BSMD user, in case the flag is set
assigned_agency = [spm for spm in shipcall_participant_map if int(spm.type) == int(ParticipantType.AGENCY)]
assigned_pilot = [spm for spm in shipcall_participant_map if int(spm.type) == int(ParticipantType.PILOT)]
an_agency_is_assigned = len(assigned_agency)==1
a_pilot_is_assigned = len(assigned_pilot)==1
if a_pilot_is_assigned:
assigned_pilot = assigned_pilot[0]
if len(assigned_agency)>1:
raise ValidationError({"internal_error":f"Internal error? Found more than one assigned agency for the shipcall with ID {shipcall_id}. Found: {assigned_agency}"})
@ -567,18 +573,19 @@ class InputValidationShipcall():
### USER authority ###
# determine, whether the user is a) the assigned agency or b) a BSMD participant
user_is_assigned_agency = (user_participant_id == assigned_agency.id)
user_is_assigned_pilot = a_pilot_is_assigned and (user_participant_id == assigned_pilot.id)
# when the BSMD flag is set: the user must be either BSMD or the assigned agency
# when the BSMD flag is not set: the user must be the assigned agency
user_is_authorized = (user_is_bsmd or user_is_assigned_agency) #if agency_has_bsmd_flag else user_is_assigned_agency
user_is_authorized = (user_is_bsmd or user_is_assigned_agency or user_is_assigned_pilot) #if agency_has_bsmd_flag else user_is_assigned_agency
if not user_is_authorized:
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {assigned_agency.flags}") # Forbidden: 403
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY / BSMD / PILOT (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {assigned_agency.flags}") # Forbidden: 403
else:
# when there is no assigned agency, only BSMD users can update the shipcall
if not user_is_bsmd:
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.") # part of a pytest.raises. Forbidden: 403
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY / BSMD / PILOT users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.") # part of a pytest.raises. Forbidden: 403
return

View File

@ -637,7 +637,7 @@ def test_shipcall_put_request_fails_when_different_participant_id_is_assigned(ge
# agency with different participant id is assigned
ivs = InputValidationShipcall()
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users"):
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users"):
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
return
@ -706,7 +706,7 @@ def test_shipcall_put_request_fails_when_no_agency_is_assigned(get_shipcall_id_a
# no agency assigned
ivs = InputValidationShipcall()
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
return
@ -738,7 +738,7 @@ def test_shipcall_put_request_fails_when_user_is_not_authorized(get_shipcall_id_
# current user is not authorized
ivs = InputValidationShipcall()
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users"):
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users"):
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
return
@ -769,7 +769,7 @@ def test_shipcall_put_request_fails_when_user_tries_self_assignment(get_shipcall
# self-assignment. User is participant 6, and wants to assign participant 6.
ivs = InputValidationShipcall()
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape("PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape("PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
# previous error message: An agency cannot self-register for a shipcall. The request is issued by an agency-user and tries to assign an AGENCY as the participant of the shipcall.""
# however, self-assignment is no longer possible, because the SPM is verified beforehand.
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
@ -846,7 +846,7 @@ def test_shipcall_put_request_fails_input_validation_shipcall_when_shipcall_is_c
assert user.participant_id == 4
#### verification should fail, because participant_id 4 is ParticipantType.PILOT (neither an assigned agency, nor bsmd)
with pytest.raises(werkzeug.exceptions.Forbidden, match="PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD user"):
with pytest.raises(werkzeug.exceptions.Forbidden, match="PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT user"):
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
### PASSES: