Skip to content

Instantly share code, notes, and snippets.

@tjdaley
Created January 5, 2025 21:21
Show Gist options
  • Save tjdaley/fef9cc275235e62578fbb288221fe2f7 to your computer and use it in GitHub Desktop.
Save tjdaley/fef9cc275235e62578fbb288221fe2f7 to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:viewmodels="clr-namespace:TrakerManager.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:TrakerManager.Converters"
x:Class="TrakerManager.Views.MainPage"
Title="Traker Manager">
<ContentPage.Resources>
<converters:IntToBoolConverter x:Key="IntToBoolConverter"/>
<converters:InvertedBoolConverter x:Key="InvertedBoolConverter"/>
<converters:SortIndicatorConverter x:Key="SortIndicatorConverter"/>
<!-- Style for client items in left panel -->
<Style x:Key="ClientItemStyle" TargetType="Grid">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="Background" Value="Transparent"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="Background" Value="LightYellow"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="PointerOver">
<VisualState.Setters>
<Setter Property="Background" Value="#e3f2fd"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!-- Style for folder items in right panel -->
<Style x:Key="FolderItemStyle" TargetType="Grid">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="Background" Value="Transparent"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="Background" Value="#e3f2fd"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="PointerOver">
<VisualState.Setters>
<Setter Property="Background" Value="#f5f5f5"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style x:Key="ClassificationFolderItemStyle" TargetType="Grid">
<Setter Property="Background" Value="Orange"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="Background" Value="Orange"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="Background" Value="Pink"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="PointerOver">
<VisualState.Setters>
<Setter Property="Background" Value="IndianRed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style x:Key="DocumentRowStyle" TargetType="Grid">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#2A2A2A"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="PointerOver">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#232323"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style x:Key="DocumentGridLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="black"/>
<Style.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={RelativeSource AncestorType={x:Type Grid}}, Path=Background.Color}"
Value="#2A2A2A">
<Setter Property="TextColor" Value="#00FFFF"/>
</DataTrigger>
<DataTrigger TargetType="Label"
Binding="{Binding Source={RelativeSource AncestorType={x:Type Grid}}, Path=Background.Color}"
Value="#232323">
<Setter Property="TextColor" Value="#E0E0E0"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="SortHeaderButtonStyle" TargetType="Button">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="TextColor" Value="Black"/>
<Setter Property="Padding" Value="1,1"/>
<Setter Property="HorizontalOptions" Value="Start"/>
</Style>
<Style x:Key="DetailLabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="11"/>
<Setter Property="TextColor" Value="#00FFFF"/>
<Setter Property="HorizontalOptions" Value="End"/>
<Setter Property="VerticalOptions" Value="Center"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<!-- When the value is shown in an entry field -->
<Style x:Key="DetailValueStyle" TargetType="Entry">
<Setter Property="FontSize" Value="11"/>
<Setter Property="TextColor" Value="White"/>
<Setter Property="BackgroundColor" Value="#2A2A2A"/>
<Setter Property="Margin" Value="2"/>
<Setter Property="HeightRequest" Value="35"/>
<Setter Property="VerticalTextAlignment" Value="Center"/>
</Style>
<!-- When the Value is shown in a label -->
<Style x:Key="DetailValueLabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="11"/>
<Setter Property="TextColor" Value="White"/>
<Setter Property="VerticalOptions" Value="Center"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<!-- For the Copy Icons -->
<Style x:Key="CopyButtonStyle" TargetType="Button">
<Setter Property="HeightRequest" Value="20"/>
<Setter Property="WidthRequest" Value="20"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="#666666"/>
<Setter Property="Text" Value="📋"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="VerticalOptions" Value="Center"/>
<Setter Property="HorizontalOptions" Value="Start"/>
<Setter Property="ToolTipProperties.Text" Value="Click to copy field value."/>
</Style>
<!-- For the Copy All and Edit Buttons -->
<Style x:Key="ActionButtonStyle" TargetType="Button">
<Setter Property="HeightRequest" Value="0"/>
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Padding" Value="2,2,2,2"/>
<Setter Property="FontSize" Value="11"/>
</Style>
</ContentPage.Resources>
<Grid ColumnDefinitions="300, *"
BackgroundColor="LightGray"
Padding="10">
<!-- Left Panel with Clients -->
<Border Grid.Column="0"
BackgroundColor="White"
Stroke="Gray"
StrokeThickness="1"
Padding="5"
HorizontalOptions="Fill"
VerticalOptions="Fill">
<CollectionView ItemsSource="{Binding TreeItems}"
SelectionMode="Single"
SelectedItem="{Binding SelectedTreeItem}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Style="{StaticResource ClientItemStyle}"
Padding="10,5">
<Grid.GestureRecognizers>
<PointerGestureRecognizer PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited"/>
</Grid.GestureRecognizers>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="{Binding Name}"
TextColor="Black"
FontSize="14">
<Label.Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="#0066cc"/>
<Setter Property="FontSize" Value="14"/>
<Style.Triggers>
<!-- Make the selected client bold and change its color -->
<DataTrigger TargetType="Label" Binding="{Binding IsSelected}" Value="True">
<Setter Property="BackgroundColor" Value="#0066cc"/>
<Setter Property="TextColor" Value="White"/>
<Setter Property="FontAttributes" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<!--Label Grid.Column="1"
Text="{Binding SubItems.Count, StringFormat='({0})'}"
TextColor="Gray"
FontSize="12"/-->
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Border>
<!-- Right Panel -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Breadcrumb Navigation -->
<Border Grid.Row="0"
Margin="0,0,0,10"
Background="Pink"
Stroke="LightGray"
StrokeThickness="1"
Padding="10,5">
<CollectionView ItemsSource="{Binding Breadcrumbs}"
HorizontalOptions="Start">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
ItemSpacing="5"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Orientation="Horizontal">
<Label >
<Label.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:MainViewModel}}, Path=BreadcrumbSelectedCommand}"
CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
<Label.Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="#0066cc"/>
<Setter Property="FontSize" Value="14"/>
<Style.Triggers>
<!-- Make the last breadcrumb bold and change its color -->
<DataTrigger TargetType="Label" Binding="{Binding IsLast}" Value="True">
<Setter Property="TextColor" Value="#333333"/>
<Setter Property="FontAttributes" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
<Label.Text>
<MultiBinding StringFormat="{}{0} {1}">
<!-- The actual breadcrumb text -->
<Binding Path="Text"/>
<!-- The separator (>) -->
<Binding Path="IsLast" Converter="{StaticResource InvertedBoolConverter}">
<Binding.ConverterParameter>></Binding.ConverterParameter>
</Binding>
</MultiBinding>
</Label.Text>
</Label>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Border>
<!-- Dynamic Content Area -->
<Grid Grid.Row="1">
<!-- Tracker View -->
<CollectionView ItemsSource="{Binding SelectedClientTrackers}"
IsVisible="{Binding ShowTrackers}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="4"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Style="{StaticResource FolderItemStyle}"
Padding="10"
WidthRequest="120">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:MainViewModel}}, Path=TrackerSelectedCommand}"
CommandParameter="{Binding .}"/>
<PointerGestureRecognizer PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited"/>
</Grid.GestureRecognizers>
<Grid.RowDefinitions>
<RowDefinition Height="64"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Source="folder.png"
Grid.Row="0"
HeightRequest="64"
WidthRequest="64"
HorizontalOptions="Center"/>
<Label Text="{Binding Name}"
Grid.Row="1"
TextColor="Black"
HorizontalOptions="Center"
LineBreakMode="WordWrap"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Classifications View -->
<CollectionView ItemsSource="{Binding SelectedTrackerClassifications}"
IsVisible="{Binding ShowClassifications}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="4"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Style="{StaticResource FolderItemStyle}"
Padding="10"
WidthRequest="120">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:MainViewModel}}, Path=ClassificationSelectedCommand}"
CommandParameter="{Binding .}" />
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:MainViewModel}}, Path=ShowContextMenuCommand}"
CommandParameter="{Binding .}"
NumberOfTapsRequired="2" />
<PointerGestureRecognizer PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited" />
</Grid.GestureRecognizers>
<Grid.RowDefinitions>
<RowDefinition Height="64" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Source="folder.png"
Grid.Row="0"
HeightRequest="64"
WidthRequest="64"
HorizontalOptions="Center" />
<Label Text="{Binding Name}"
Grid.Row="1"
TextColor="Black"
HorizontalOptions="Center"
LineBreakMode="WordWrap" />
<Label Text="{Binding DocumentCount, StringFormat='({0} documents)'}"
Grid.Row="2"
TextColor="Gray"
FontSize="12"
HorizontalOptions="Center" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Sub-Classifications View -->
<CollectionView ItemsSource="{Binding SubClassificationFolders}"
IsVisible="{Binding ShowSubClassifications}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="4"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Style="{StaticResource FolderItemStyle}"
Padding="10"
WidthRequest="120">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:MainViewModel}}, Path=SubClassificationSelectedCommand}"
CommandParameter="{Binding .}"/>
<PointerGestureRecognizer PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited"/>
</Grid.GestureRecognizers>
<Grid.RowDefinitions>
<RowDefinition Height="64"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Source="folder.png"
Grid.Row="0"
HeightRequest="64"
WidthRequest="64"
HorizontalOptions="Center"/>
<Label Text="{Binding DisplayName}"
Grid.Row="1"
TextColor="Black"
HorizontalOptions="Center"
LineBreakMode="WordWrap"/>
<Label Text="{Binding Documents.Count, StringFormat='({0} documents)'}"
Grid.Row="2"
TextColor="Gray"
FontSize="12"
HorizontalOptions="Center"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Documents Grid View -->
<Grid IsVisible="{Binding ShowDocuments}" RowDefinitions="*, Auto">
<!-- Documents Grid -->
<CollectionView Grid.Row="0"
SelectionMode="Single"
ItemsSource="{Binding DocumentsForSelectedClassification}"
SelectedItem="{Binding SelectedDocument}">
<CollectionView.Header>
<Grid ColumnDefinitions="1*, 1*, 2*, 1*, 1*, 1*, 1*, 1*"
Padding="10,5">
<Button Grid.Column="0"
Command="{Binding SortCommand}"
CommandParameter="DocumentDate"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="DocumentDate">
<Binding Source="Document Date"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="1"
Command="{Binding SortCommand}"
CommandParameter="Classification"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="Classification">
<Binding Source="Classification"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="2"
Command="{Binding SortCommand}"
CommandParameter="Title"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="Title">
<Binding Source="Title"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="3"
Command="{Binding SortCommand}"
CommandParameter="BeginningBates"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="BeginningBates">
<Binding Source="Beg Bates"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="4"
Command="{Binding SortCommand}"
CommandParameter="EndingBates"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="EndingBates">
<Binding Source="End Bates"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="5"
Command="{Binding SortCommand}"
CommandParameter="Institution"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="Institution">
<Binding Source="Institution"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="6"
Command="{Binding SortCommand}"
CommandParameter="AccountNumber"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="AccountNumber">
<Binding Source="Account"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
<Button Grid.Column="7"
Command="{Binding SortCommand}"
CommandParameter="Version"
Style="{StaticResource SortHeaderButtonStyle}">
<Button.Text>
<MultiBinding Converter="{StaticResource SortIndicatorConverter}"
ConverterParameter="Version">
<Binding Source="Version"/>
<Binding Path="CurrentSortField"/>
<Binding Path="IsAscending"/>
</MultiBinding>
</Button.Text>
</Button>
</Grid>
</CollectionView.Header>
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="1*, 1*, 2*, 1*, 1*, 1*, 1*, 1*" Padding="10,5">
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#2A2A2A"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="PointerOver">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#232323"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
<Grid.GestureRecognizers>
<PointerGestureRecognizer PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited"/>
</Grid.GestureRecognizers>
<Label Text="{Binding DocumentDate}"
Grid.Column="0"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding Classification}"
Grid.Column="1"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding Title}"
Grid.Column="2"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding BeginningBates}"
Grid.Column="3"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding EndingBates}"
Grid.Column="4"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding SubClassification[financial institution]}"
Grid.Column="5"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding SubClassification[account number]}"
Grid.Column="6"
Style="{StaticResource DocumentGridLabelStyle}"/>
<Label Text="{Binding Version}"
Grid.Column="7"
Style="{StaticResource DocumentGridLabelStyle}"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Document Details Panel -->
<Border Grid.Row="1"
IsVisible="{Binding ShowDocumentDetails}"
Stroke="Gray"
Background="#1a1a1a"
StrokeThickness="1"
Margin="0,5,0,0"
Padding="5">
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,Auto">
<Label Text="Document Details"
Grid.Row="0"
Grid.Column="0"
FontSize="12"
FontAttributes="Bold"
TextColor="White"
Margin="0,0,0,5"/>
<Button Text="Copy All"
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource ActionButtonStyle}"
Command="{Binding CopyDocumentDetailsCommand}"/>
<ScrollView Grid.Row="1" Grid.ColumnSpan="2">
<Grid RowDefinitions="1*,1*,1*,1*,1*,1*,1*,1*,1*"
ColumnDefinitions="Auto,*, *, *, *, *"
RowSpacing="3"
ColumnSpacing="5">
<!-- ROW 0-->
<!-- Document ID -->
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto,Auto" ColumnSpacing="2">
<Button Grid.Column="0"
Style="{StaticResource CopyButtonStyle}"
Command="{Binding CopyFieldCommand}"
CommandParameter="{Binding SelectedDocument.Id}"/>
<Label Text="Document ID:" VerticalOptions="Center" Style="{StaticResource DetailLabelStyle}" Grid.Column="1"/>
</Grid>
<Label Text="{Binding SelectedDocument.Id}"
Grid.Row="0" Grid.Column="1"
Style="{StaticResource DetailValueLabelStyle}"/>
<!-- Client Reference -->
<Label Text="Client Reference:" Grid.Row="0" Grid.Column="2" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.ClientReference, Mode=TwoWay}"
Grid.Row="0" Grid.Column="3" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- Classification -->
<Label Text="Classification:" Grid.Row="0" Grid.Column="4" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.Classification, Mode=TwoWay}"
Grid.Row="0" Grid.Column="5" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- ROW 1 -->
<!-- Sub Classification (Read-only for now) -->
<Label Text="Sub Classification:" Grid.Row="1" Grid.Column="0" Style="{StaticResource DetailLabelStyle}"/>
<Grid Grid.Row="1" Grid.Column="1" ColumnDefinitions="*,Auto" Grid.ColumnSpan="5">
<Entry Text="{Binding SubClassificationDisplay, Mode=TwoWay}"
Style="{StaticResource DetailValueStyle}"
TextColor="Gray"
IsReadOnly="True"/>
<Button Text="Edit"
Style="{StaticResource ActionButtonStyle}"
Grid.Column="2"
Command="{Binding EditSubClassificationCommand}"/>
</Grid>
<!-- ROW 2 -->
<!-- Title -->
<Label Text="Title:"
Grid.Row="2" Grid.Column="0"
Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.Title, Mode=TwoWay}"
Grid.Row="2" Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- Document Date -->
<Label Text="Document Date:" Grid.Row="2" Grid.Column="2" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.DocumentDate, Mode=TwoWay}"
Grid.Row="2" Grid.Column="3" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- ROW 3 -->
<!-- Produced Date -->
<Label Text="Produced Date:" Grid.Row="3" Grid.Column="0" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.ProducedDate, Mode=TwoWay}"
Grid.Row="3" Grid.Column="1" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- Beginning Bates -->
<Label Text="Beginning Bates:" Grid.Row="3" Grid.Column="2" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.BeginningBates, Mode=TwoWay}"
Grid.Row="3" Grid.Column="3" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- Ending Bates -->
<Label Text="Ending Bates:" Grid.Row="3" Grid.Column="4" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.EndingBates, Mode=TwoWay}"
Grid.Row="3" Grid.Column="5" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<!-- ROW 4 -->
<!-- Page Max -->
<!-- Page Count -->
<Label Text="Page Count:" Grid.Row="4" Grid.Column="0" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.PageCount}"
Grid.Row="4" Grid.Column="1" Style="{StaticResource DetailValueLabelStyle}"/>
<!-- Missing Pages -->
<Label Text="Missing Pages:" Grid.Row="4" Grid.Column="2" Style="{StaticResource DetailLabelStyle}"/>
<Entry Text="{Binding SelectedDocument.MissingPages, Mode=TwoWay}"
Grid.Row="4" Grid.Column="3" Style="{StaticResource DetailValueStyle}"
TextChanged="OnDocumentFieldChanged"/>
<Label Text="Page Max:" Grid.Row="4" Grid.Column="4" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.PageMax}"
Grid.Row="4" Grid.Column="5" Style="{StaticResource DetailValueLabelStyle}"/>
<!-- ROW 5 -->
<!-- File Path -->
<Grid Grid.Row="5" Grid.Column="0" ColumnDefinitions="Auto,Auto" ColumnSpacing="2">
<Button Grid.Column="0"
Style="{StaticResource CopyButtonStyle}"
Command="{Binding CopyFieldCommand}"
CommandParameter="{Binding SelectedDocument.Path}"/>
<Label Text="File Path:" VerticalOptions="Center" Style="{StaticResource DetailLabelStyle}" Grid.Column="1"/>
</Grid>
<Label Text="{Binding SelectedDocument.Path}"
Grid.Row="5" Grid.Column="1" Style="{StaticResource DetailValueLabelStyle}"
Grid.ColumnSpan="5"/>
<!-- ROW 6 -->
<Label Text="File name:"
Grid.Row="6" Grid.Column="0"
Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.Filename}"
Grid.Row="6" Grid.Column="1"
Style="{StaticResource DetailValueLabelStyle}"/>
<Label Text="File type:"
Grid.Row="6" Grid.Column="2"
Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.FileType}"
Grid.Row="6" Grid.Column="3"
Style="{StaticResource DetailValueLabelStyle}"/>
<!-- File Create Date -->
<Label Text="File Create Date:"
Grid.Row="6" Grid.Column="4"
Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.FileCreateDate}"
Grid.Row="6" Grid.Column="5"
Style="{StaticResource DetailValueLabelStyle}"/>
<!-- ROW 7 -->
<!-- Added By-->
<Label Text="Added By:" Grid.Row="7" Grid.Column="0" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.AddedBy}"
Grid.Row="7" Grid.Column="1" Style="{StaticResource DetailValueLabelStyle}"/>
<!-- Added Date-->
<Label Text="Added Date:" Grid.Row="7" Grid.Column="2" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.AddedDate}"
Grid.Row="7" Grid.Column="3" Style="{StaticResource DetailValueLabelStyle}"/>
<!-- ROW 8 -->
<!-- Updated By-->
<Label Text="Updated By:" Grid.Row="8" Grid.Column="0" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.UpdatedBy}"
Grid.Row="8" Grid.Column="1" Style="{StaticResource DetailValueLabelStyle}"/>
<!-- Updated Date-->
<Label Text="Updated Date:" Grid.Row="8" Grid.Column="2" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.UpdatedDate}"
Grid.Row="8" Grid.Column="3" Style="{StaticResource DetailValueLabelStyle}"/>
<!-- Version -->
<Label Text="Version:" Grid.Row="8" Grid.Column="4" Style="{StaticResource DetailLabelStyle}"/>
<Label Text="{Binding SelectedDocument.Version}"
Grid.Row="8" Grid.Column="5" Style="{StaticResource DetailValueLabelStyle}"/>
</Grid>
</ScrollView>
</Grid>
</Border>
</Grid>
</Grid>
</Grid>
</Grid>
</ContentPage>
using System.Diagnostics;
using CommunityToolkit.Maui.Views;
namespace TrakerManager.Views
{
public partial class MainPage : ContentPage
{
private LoginPopup? _loginPopup;
private readonly IServiceProvider _serviceProvider;
private Document? _selectedDocument;
public MainPage(IServiceProvider serviceProvider, MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
_serviceProvider = serviceProvider;
}
protected override async void OnAppearing()
{
base.OnAppearing();
_loginPopup = _serviceProvider.GetRequiredService<LoginPopup>();
var result = await this.ShowPopupAsync(_loginPopup);
if (result is bool success && success)
{
if (BindingContext is MainViewModel viewModel)
{
await viewModel.LoadClientsCommand.ExecuteAsync(null);
}
}
}
public async Task CloseLoginPopup(bool success)
{
if (_loginPopup != null)
{
await _loginPopup.CloseAsync(success);
}
}
private void OnPointerEntered(object sender, PointerEventArgs e)
{
if (sender is Grid grid && !IsSelectedGrid(grid))
{
VisualStateManager.GoToState(grid, "PointerOver");
}
}
private void OnPointerExited(object sender, PointerEventArgs e)
{
if (sender is Grid grid)
{
// Only go to Normal state if this isn't the selected grid
if (!IsSelectedGrid(grid))
{
VisualStateManager.GoToState(grid, "Normal");
}
}
}
private bool IsSelectedGrid(Grid grid)
{
if (grid.BindingContext is Document doc &&
BindingContext is MainViewModel viewModel)
{
return doc == viewModel.SelectedDocument;
}
return false;
}
private void OnDocumentFieldChanged(object sender, TextChangedEventArgs e)
{
Debug.WriteLine("MainPage.xaml::OnDocumentFieldChanged: Entered method");
// Explicitly call SaveDocument command in MainViewModel
if (BindingContext is not MainViewModel viewModel)
return;
Debug.WriteLine($"MainPage.xaml::OnDocumentFieldChanged: IsPopulatingForm = {viewModel.IsPopulatingForm}");
if (viewModel.IsPopulatingForm)
{
Debug.WriteLine("MainPage.xaml::OnDocumentFieldChanged: Skipping update because form is populating");
return;
}
Debug.WriteLine("MainPage.xaml::OnDocumentFieldChanged: Updating selected document");
viewModel.DebounceSaveDocument();
}
}
}
using System.Collections.ObjectModel;
using System.Diagnostics;
using TrakerManager.Models;
using TrakerManager.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Maui.Alerts; // Add this using directive
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Timers;
using TrakerManager.Utilities;
namespace TrakerManager.ViewModels
{
#pragma warning disable IDE0028 // Initialization can be simplified. Suppress because it lures us away from type safety.
public partial class MainViewModel : ObservableObject
{
private readonly TrakerService _trakerService;
private readonly bool _confirmCopy = false; // Set to true to confirm copy actions with a dialog
public MainViewModel(TrakerService trakerService)
{
_trakerService = trakerService;
TreeItems = new ObservableCollection<TreeViewItem>();
SelectedClientTrackers = new ObservableCollection<TreeViewItem>();
SelectedTrackerClassifications = new ObservableCollection<ClassificationItem>();
DocumentsForSelectedClassification = new ObservableCollection<Document>();
// Initialize commands
LoadClientsCommand = new AsyncRelayCommand(LoadClients);
LoadTrackersCommand = new AsyncRelayCommand<string>(LoadTrackersForClient);
LoadDocumentsCommand = new AsyncRelayCommand<string>(LoadDocumentsForTracker);
}
[ObservableProperty]
private ObservableCollection<TreeViewItem> _treeItems;
[ObservableProperty]
private ObservableCollection<TreeViewItem> _selectedClientTrackers;
[ObservableProperty]
private ObservableCollection<ClassificationItem> _selectedTrackerClassifications;
[ObservableProperty]
private ObservableCollection<Document> _documentsForSelectedClassification;
[ObservableProperty]
private TreeViewItem? _selectedTreeItem;
[ObservableProperty]
private bool _showTrackers;
[ObservableProperty]
private bool _showClassifications;
[ObservableProperty]
private bool _showSubClassifications;
[ObservableProperty]
private bool _showDocuments;
[ObservableProperty]
private int _currentClassificationLevel = 0;
[ObservableProperty]
private ClassificationDefinition? _currentClassificationDefinition;
[ObservableProperty]
private ObservableCollection<SubClassificationFolder> _subClassificationFolders = new();
[ObservableProperty]
private ObservableCollection<BreadcrumbItem> _breadcrumbs = new();
[ObservableProperty]
private Document? _selectedDocument;
[ObservableProperty]
private bool _showDocumentDetails;
[ObservableProperty]
private string _subClassificationDisplay = string.Empty;
[ObservableProperty]
private string _currentSortField = string.Empty;
[ObservableProperty]
private bool _isAscending = true;
[ObservableProperty]
private bool _isPopulatingForm; // Set to true when loading data into form fields
// Add the collection as an ObservableCollection
//[ObservableProperty]
//private ObservableCollection<Document> _documentsForSelectedClassification = new();
public IAsyncRelayCommand LoadClientsCommand { get; }
public IAsyncRelayCommand<string> LoadTrackersCommand { get; }
public IAsyncRelayCommand<string> LoadDocumentsCommand { get; }
private System.Timers.Timer? _debounceTimer;
private async Task LoadClients()
{
try
{
Debug.WriteLine("Loading clients...");
var clients = await _trakerService.GetClientsAsync();
Debug.WriteLine($"Retrieved {clients.Count} clients from service.");
TreeItems.Clear();
Debug.WriteLine("Cleared TreeItems collection.");
foreach (var client in clients)
{
Debug.WriteLine($"Processing client {client.Name}.");
var clientItem = new TreeViewItem
{
Id = client.Id,
Name = client.Name,
ItemType = TreeItemType.Client
};
TreeItems.Add(clientItem);
Debug.WriteLine($"Added client to tree {client.Name}.");
}
Debug.WriteLine($"TreeItems count {TreeItems.Count}.");
}
catch (Exception ex)
{
Debug.WriteLine($"Error in LoadClients: {ex.Message}");
if (Application.Current?.Windows?.Count > 0)
{
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to load clients. Please try again.",
"OK");
}
}
}
[RelayCommand]
private async Task LoadTrackersForClient(string? clientId)
{
if (string.IsNullOrEmpty(clientId))
{
// Handle null or empty clientId here, if applicable.
Debug.WriteLine("Client ID is null or empty.");
return;
}
try
{
var trackers = await _trakerService.GetClientTrackersAsync(clientId);
var clientItem = TreeItems.First(x => x.Id == clientId);
clientItem.SubItems.Clear();
Breadcrumbs.Clear();
foreach (var tracker in trackers)
{
Debug.WriteLine($"Creating TreeViewItem for tracker: {tracker.Name}");
var trackerItem = new TreeViewItem
{
Id = tracker.Id,
Name = tracker.Name,
ItemType = TreeItemType.Tracker
};
clientItem.SubItems.Add(trackerItem);
}
}
catch (Exception ex)
{
if (Application.Current?.Windows?.Count > 0)
{
Debug.WriteLine(ex.Message);
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to load trackers. Please try again.",
"OK");
}
}
}
[RelayCommand]
private async Task LoadDocumentsForTracker(string? trackerId)
{
if (string.IsNullOrEmpty(trackerId))
{
// Handle null or empty clientId here, if applicable.
Debug.WriteLine("Tracker ID is null or empty.");
return;
}
try
{
Debug.WriteLine($"Loading documents for tracker: {trackerId}");
var documents = await _trakerService.GetTrackerDocumentsAsync(trackerId);
// Find the tracker item in the tree
TreeViewItem? trackerItem = null;
foreach (var client in TreeItems)
{
trackerItem = client.SubItems.FirstOrDefault(t => t.Id == trackerId);
if (trackerItem != null)
break;
}
if (trackerItem == null)
{
Debug.WriteLine("Tracker item not found in tree.");
return;
}
Debug.WriteLine($"Found tracker in tree: {trackerItem.Name}");
trackerItem.SubItems.Clear();
// Group by classification first
var classificationGroups = documents
.Where(d => !string.IsNullOrEmpty(d.Classification))
.GroupBy(d => d.Classification);
foreach (var classGroup in classificationGroups)
{
Debug.WriteLine($"Processing classification group: {classGroup.Key}");
var classItem = new TreeViewItem
{
Id = $"class_{classGroup.Key}",
Name = classGroup.Key ?? string.Empty, // Fix for CS8601
ItemType = TreeItemType.Classification
};
trackerItem.SubItems.Add(classItem);
}
}
catch (Exception ex)
{
if (Application.Current?.Windows?.Count > 0)
{
Debug.WriteLine(ex.Message);
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to load document classifications. Please try again.",
"OK");
}
}
}
async partial void OnSelectedTreeItemChanged(TreeViewItem? value)
{
Debug.WriteLine($"\nSelection changed to: {value?.Name}, Type: {value?.ItemType}");
if (value != null)
{
try
{
// Reset IsSelected on each clientItem
foreach (var treeItem in TreeItems)
{
treeItem.IsSelected = false;
}
value.IsSelected = true;
foreach (var treeItem in TreeItems)
{
Debug.WriteLine($"Tree Items: {treeItem.Name} - Selected? {treeItem.IsSelected}");
}
// When client selected, show their trackers in right panel
var trackers = await _trakerService.GetClientTrackersAsync(value.Id);
Debug.WriteLine($"Retrieved {trackers.Count} trackers for client");
SelectedClientTrackers.Clear();
foreach (var tracker in trackers)
{
SelectedClientTrackers.Add(new TreeViewItem
{
Id = tracker.Id,
Name = tracker.Name,
ItemType = TreeItemType.Tracker
});
}
ShowTrackers = true;
ShowClassifications = false;
ShowDocuments = false;
Debug.WriteLine($"Added {SelectedClientTrackers.Count} trackers to right panel");
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading trackers: {ex.Message}");
if (Application.Current?.Windows?.Count > 0)
{
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to load trackers. Please try again.",
"OK");
}
}
}
}
[RelayCommand]
private async Task TrackerSelected(TreeViewItem tracker)
{
try
{
Debug.WriteLine($"Tracker selected: {tracker.Name}");
var documents = await _trakerService.GetTrackerDocumentsAsync(tracker.Id);
Debug.WriteLine($"Retrieved {documents.Count} documents");
// Reset navigation state
CurrentClassificationLevel = 0;
CurrentClassificationDefinition = null;
// Add tracker to breadcrumbs
Breadcrumbs.Clear();
Breadcrumbs.Add(new BreadcrumbItem
{
Text = tracker.Name!,
Level = "0",
IsLast = true
});
// Group documents by classification
var classifications = documents
.Where(d => !string.IsNullOrEmpty(d.Classification))
.GroupBy(d => d.Classification)
.Select(g => new ClassificationItem
{
Name = g.Key!,
DocumentCount = g.Count(),
Documents = [.. g] // same as g.ToList()
})
.ToList();
SelectedTrackerClassifications.Clear();
foreach (var classification in classifications)
{
SelectedTrackerClassifications.Add(classification);
Debug.WriteLine($"Added classification {classification.Name} with {classification.DocumentCount} documents");
}
ShowTrackers = false;
ShowClassifications = true;
ShowDocuments = false;
ShowSubClassifications = false;
CurrentClassificationLevel++;
}
catch (Exception ex)
{
Debug.WriteLine($"Error in TrackerSelected: {ex.Message}");
if (Application.Current?.Windows?.Count > 0)
{
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to load classifications. Please try again.",
"OK");
}
}
}
[RelayCommand]
private async Task ClassificationSelected(ClassificationItem? classification)
{
if (classification == null)
{
Debug.WriteLine("Selected classification is null.");
return;
}
try
{
Debug.WriteLine($"Classification selected: {classification.Name}");
// Add the classification name to breadcrumbs
UpdateBreadcrumbs(classification.Name);
if (ClassificationConfig.Definitions.TryGetValue(classification.Name, out var definition))
{
CurrentClassificationDefinition = definition;
if (definition.HasSubClassifications)
{
Debug.WriteLine($"Processing sub-classifications for level {CurrentClassificationLevel}");
var fieldName = definition.GetFieldNameForLevel(CurrentClassificationLevel);
var displayName = definition.GetDisplayNameForLevel(CurrentClassificationLevel);
var groups = classification.Documents
.Where(d => d.SubClassification != null &&
d.SubClassification.ContainsKey(fieldName) &&
d.SubClassification[fieldName] != null)
.GroupBy(d => d.SubClassification![fieldName]?.ToString())
.Where(g => g.Key != null);
SubClassificationFolders.Clear();
foreach (var group in groups)
{
SubClassificationFolders.Add(new SubClassificationFolder
{
DisplayName = $"{displayName}: {group.Key}",
Value = group.Key!,
ParentClassification = classification.Name,
Level = CurrentClassificationLevel,
Documents = [.. group]
});
}
ShowTrackers = false;
ShowClassifications = false;
ShowDocuments = false;
ShowSubClassifications = true;
CurrentClassificationLevel++;
}
else
{
DocumentsForSelectedClassification = new ObservableCollection<Document>(classification.Documents);
ShowTrackers = false;
ShowClassifications = false;
ShowSubClassifications = false;
ShowDocuments = true;
CurrentClassificationLevel = 0;
CurrentClassificationDefinition = null;
}
}
else
{
DocumentsForSelectedClassification = new ObservableCollection<Document>(classification.Documents);
ShowTrackers = false;
ShowClassifications = false;
ShowSubClassifications = false;
ShowDocuments = true;
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in ClassificationSelected: {ex.Message}");
if (Application.Current?.Windows?.Count > 0)
{
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to process classification. Please try again.",
"OK");
}
}
}
[RelayCommand]
private async Task SubClassificationSelected(SubClassificationFolder? folder)
{
if (folder == null)
{
Debug.WriteLine("MainViewModel::SubClassificationSelected: Selected sub-classification folder is null.");
return;
}
Debug.WriteLine($"MainViewModel::SubClassificationSelected: {folder?.DisplayName}");
try
{
Debug.WriteLine($"MainViewModel::SubClassificationSelected: {folder?.DisplayName}");
UpdateBreadcrumbs(folder!.DisplayName);
if (CurrentClassificationDefinition != null &&
CurrentClassificationLevel < CurrentClassificationDefinition.MaxDepth)
{
// Get the next level's field name
var nextFieldName = CurrentClassificationDefinition.GetFieldNameForLevel(CurrentClassificationLevel);
var nextDisplayName = CurrentClassificationDefinition.GetDisplayNameForLevel(CurrentClassificationLevel);
// Filter documents by the current selection and group by next level
var groups = folder.Documents
.Where(d => d.SubClassification != null &&
d.SubClassification.ContainsKey(nextFieldName) &&
d.SubClassification[nextFieldName] != null)
.GroupBy(d => d.SubClassification![nextFieldName]?.ToString())
.Where(g => g.Key != null);
SubClassificationFolders.Clear();
foreach (var group in groups)
{
SubClassificationFolders.Add(new SubClassificationFolder
{
DisplayName = $"{nextDisplayName}: {group.Key}",
Value = group.Key!,
ParentClassification = folder.ParentClassification,
Level = CurrentClassificationLevel,
Documents = [.. group]
});
Debug.WriteLine($"MainViewModel::SubClassificationSelected: Added next level folder: {nextDisplayName}: {group.Key} ({group.Count()} documents)");
}
CurrentClassificationLevel++;
}
else
{
// At leaf level, show documents
DocumentsForSelectedClassification = new ObservableCollection<Document>(folder.Documents);
ShowSubClassifications = false;
ShowDocuments = true;
CurrentClassificationLevel++;
}
}
catch (Exception ex)
{
Debug.WriteLine($"MainViewModel::SubClassificationSelected: Error in SubClassificationSelected: {ex.Message}");
await PopupMessage.Show("Unable to process sub-classification. Please try again.", "Error");
}
}
[RelayCommand]
private async Task ChangeClassification(TreeViewItem classificationFolder)
{
try
{
// Prompt user for new classification value
var mainPage = Application.Current?.Windows[0].Page;
if (mainPage == null)
{
Debug.WriteLine("MainViewModel::ChangeClassification: Main page is null.");
return;
}
string? newClassification = await mainPage.DisplayPromptAsync(
"Change Classification",
$"Enter the new classification for {classificationFolder.Name}:",
placeholder: "New Classification");
if (string.IsNullOrWhiteSpace(newClassification))
{
await PopupMessage.Show("Classification cannot be empty.");
return;
}
// Collect document IDs from the folder
var documentIds = classificationFolder.Documents.Select(d => d.Id).ToArray();
if (documentIds.Length == 0)
{
await PopupMessage.Show("No documents found in this folder.");
return;
}
// Update classification
var failedUpdates = await _trakerService.UpdateDocumentClassificationAsync(documentIds, newClassification);
// Provide feedback
if (failedUpdates.Count != 0)
{
string failedMessage = string.Join("\n", failedUpdates.Select(f => $"ID: {f.Id}, Reason: {f.Reason}"));
await PopupMessage.Show($"Some documents failed to update:\n{failedMessage}", "Partial Success");
}
else
{
await PopupMessage.Show("All documents updated successfully.", "Success");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in ChangeClassification: {ex.Message}");
await PopupMessage.Show("An error occurred. Please try again.");
}
}
[RelayCommand]
private void DocumentSelected(Document document)
{
SelectedDocument = document;
ShowDocumentDetails = true;
// Format sub-classification for display
if (document.SubClassification != null)
{
SubClassificationDisplay = string.Join(", ",
document.SubClassification.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
}
else
{
SubClassificationDisplay = "No sub-classification data";
}
}
[RelayCommand]
private static async Task EditSubClassification()
{
// TODO: Implement sub-classification editing in future phase
await PopupMessage.Show("Sub-classification editing will be implemented in a future update.", "Coming Soon!!");
}
// Optional: Add property change handlers to save changes
partial void OnSelectedDocumentChanged(Document? value)
{
if (value == null) return;
Debug.WriteLine("MainViewModel::OnSelectedDocumentChanged: Setting IsPopulatingForm to True");
IsPopulatingForm = true;
ShowDocumentDetails = true;
// Force the current method to complete and allow the binding updates to occur
Application.Current?.Dispatcher.Dispatch(() => {
// Now set IsPopulatingForm back to false
Debug.WriteLine("MainViewModel::OnSelectedDocumentChanged: Setting IsPopulatingForm to False");
IsPopulatingForm = false;
});
}
private void UpdateDocumentAcrossCollections(Document sourceDocument)
{
Debug.WriteLine($"MainViewModel::UpdateDocumentAcrossCollections: Updating document {sourceDocument.Id} across collections");
// Update in DocumentsForSelectedClassification (grid view)
var gridDoc = DocumentsForSelectedClassification.FirstOrDefault(d => d.Id == sourceDocument.Id);
if (gridDoc != null)
{
CopyDocumentProperties(sourceDocument, gridDoc);
}
// Update in ClassificationItems
foreach (var classItem in SelectedTrackerClassifications)
{
var doc = classItem.Documents.FirstOrDefault(d => d.Id == sourceDocument.Id);
if (doc != null)
{
CopyDocumentProperties(sourceDocument, doc);
break;
}
}
// Update in SubClassificationFolders
foreach (var folder in SubClassificationFolders)
{
var doc = folder.Documents.FirstOrDefault(d => d.Id == sourceDocument.Id);
if (doc != null)
{
CopyDocumentProperties(sourceDocument, doc);
break;
}
}
}
// Helper method to copy properties from one document to another
private void CopyDocumentProperties(Document source, Document target)
{
target.Version = source.Version;
target.Title = source.Title;
target.DocumentDate = source.DocumentDate;
target.BeginningBates = source.BeginningBates;
target.EndingBates = source.EndingBates;
target.Classification = source.Classification;
target.SubClassification = source.SubClassification;
target.ClientReference = source.ClientReference;
target.MissingPages = source.MissingPages;
target.ProducedDate = source.ProducedDate;
target.UpdatedBy = source.UpdatedBy;
target.UpdatedDate = source.UpdatedDate;
}
[RelayCommand]
private void Sort(string fieldName)
{
if (CurrentSortField == fieldName)
{
// Toggle direction if clicking same field
IsAscending = !IsAscending;
}
else
{
// New field, default to ascending
CurrentSortField = fieldName;
IsAscending = true;
}
var sorted = IsAscending
? DocumentsForSelectedClassification.OrderBy(GetSortValue(fieldName))
: DocumentsForSelectedClassification.OrderByDescending(GetSortValue(fieldName));
var newList = new ObservableCollection<Document>(sorted);
DocumentsForSelectedClassification = newList;
}
[RelayCommand]
private Task BreadcrumbSelected(BreadcrumbItem item)
{
Debug.WriteLine($"@@@@@ Breadcrumb selected: {item.Text}, Level: {item.Level}");
// Navigate back to this level
CurrentClassificationLevel = int.Parse(item.Level);
return NavigateToLevel(item.Level);
}
private static Func<Document, IComparable> GetSortValue(string fieldName) => fieldName switch
{
"DocumentDate" => d => d.DocumentDate ?? "",
"Classification" => d => d.Classification ?? "",
"Title" => d => d.Title ?? "",
"BeginningBates" => d => d.BeginningBates ?? "",
"EndingBates" => d => d.EndingBates ?? "",
"Institution" => d => d.SubClassification?["financial institution"]?.ToString() ?? "",
"Account" => d => d.SubClassification?["account number"]?.ToString() ?? "",
_ => d => d.DocumentDate ?? ""
};
private async Task NavigateToLevel(string level)
{
try
{
int targetLevel = int.Parse(level) - 1;
Debug.WriteLine($"@@@@@ Breadcrumb target level: {level} {targetLevel}");
if (targetLevel >= 0 && targetLevel < Breadcrumbs.Count)
{
// Reset to the selected level
CurrentClassificationLevel = targetLevel;
// Clear any items after this level in breadcrumbs
while (Breadcrumbs.Count > targetLevel) // + 1)
{
Breadcrumbs.RemoveAt(Breadcrumbs.Count - 1);
}
// Re-show the appropriate view based on the level
if (targetLevel == 0)
{
Breadcrumbs.Clear();
ShowTrackers = true;
ShowClassifications = false;
ShowSubClassifications = false;
ShowDocuments = false;
}
else if (targetLevel == 1) // Classification
{
ShowTrackers = false;
ShowClassifications = true;
ShowSubClassifications = false;
ShowDocuments = false;
}
else
{
// Rebuild the sub-classification view for this level
ShowTrackers = false;
ShowClassifications = false;
ShowSubClassifications = true;
ShowDocuments = false;
// TODO: Rebuild the folder view for this level
// This will depend on maintaining a navigation history
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error navigating to level: {ex.Message}");
if (Application.Current?.Windows?.Count > 0)
{
await Application.Current.Windows[0].Page!.DisplayAlert(
"Error",
"Unable to navigate to selected level. Please try again.",
"OK");
}
}
}
[RelayCommand]
private async Task CopyField(string value)
{
if (string.IsNullOrEmpty(value)) return;
try
{
await Clipboard.SetTextAsync(value);
}
catch (Exception ex)
{
Debug.WriteLine($"Error copying to clipboard: {ex.Message}");
}
try
{
// Optional: Show brief feedback
if (_confirmCopy) // This is a private field set in the constructor
{
var page = Application.Current?.Windows[0].Page;
page?.DisplayAlert("Copied", "Value copied to clipboard", "OK");
}
else
{
// Use a toast notification instead of an alert
await Toast.Make("Value copied to clipboard").Show();
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error notifying user: {ex.Message}");
}
}
[RelayCommand]
private async Task SaveDocument()
{
if (SelectedDocument == null) return;
DumpObjectProperties(SelectedDocument);
try
{
Debug.WriteLine($"MainViewModel::SaveDocument: Saving document: {SelectedDocument.Id}");
var isSuccess = await _trakerService.UpdateDocumentAsync(SelectedDocument);
if (isSuccess)
{
Debug.WriteLine("MainViewModel::SaveDocument: Document saved successfully.");
// Get the latest version
var latestVersion = await _trakerService.GetDocumentVersion(SelectedDocument.Id);
if (latestVersion != null)
{
// Update version in SelectedDocument
SelectedDocument.Version = latestVersion.Version;
// Update across all collections
UpdateDocumentAcrossCollections(SelectedDocument);
}
}
else
{
Debug.WriteLine("MainViewModel::SaveDocument: Failed to save document.");
}
}
catch (Exception ex)
{
Debug.WriteLine($"MainViewModel::SaveDocument: Error in SaveDocument: {ex.Message}");
await PopupMessage.Show("An error occurred when trying to save the change. Please try again.", "Error");
}
}
private static void DumpObjectProperties(object obj, string objectName = "Object")
{
if (obj == null)
{
Debug.WriteLine($"MainViewModel::DumpObjectProperties: {objectName} is null.");
return;
}
Debug.WriteLine($"MainViewModel::DumpObjectProperties: --- Dumping properties for {objectName} ---");
foreach (var prop in obj.GetType().GetProperties())
{
try
{
var value = prop.GetValue(obj, null);
Debug.WriteLine($"{prop.Name}: {value}");
}
catch (Exception ex)
{
Debug.WriteLine($"{prop.Name}: Unable to retrieve value ({ex.Message})");
}
}
Debug.WriteLine($"MainViewModel::DumpObjectProperties: --- End of {objectName} properties ---");
}
private void UpdateBreadcrumbs(string newItem)
{
Debug.WriteLine($"MainViewModel::UpdateBreadcrumbs: Updating breadcrumbs with: {newItem}");
if (CurrentClassificationLevel == 0)
{
Breadcrumbs.Clear();
Debug.WriteLine("MainViewModel::UpdateBreadcrumbs: Cleared breadcrumbs");
}
else
{
while (Breadcrumbs.Count > CurrentClassificationLevel)
{
Breadcrumbs.RemoveAt(Breadcrumbs.Count - 1);
}
Debug.WriteLine($"MainViewModel::UpdateBreadcrumbs: Trimmed breadcrumbs to level {CurrentClassificationLevel}");
}
Breadcrumbs.Add(new BreadcrumbItem
{
Text = newItem,
Level = CurrentClassificationLevel.ToString(),
IsLast = true
});
// Update IsLast for all items except the last
for (int i = 0; i < Breadcrumbs.Count - 1; i++)
{
Breadcrumbs[i].IsLast = false; // This will now notify the UI
}
Breadcrumbs[^1].IsLast = true; // Set the last item explicitly
// Debug output
foreach (var breadcrumb in Breadcrumbs)
{
Debug.WriteLine($"@@@@@Breadcrumb: ({breadcrumb.Level}) {breadcrumb.Text}, IsLast: {breadcrumb.IsLast}");
}
}
public void DebounceSaveDocument()
{
_debounceTimer?.Stop();
_debounceTimer = new System.Timers.Timer(500); // Delay for 500 ms
_debounceTimer.Elapsed += async (s, e) =>
{
_debounceTimer?.Stop();
await SaveDocument(); // Save the document after debounce delay
};
_debounceTimer.Start();
}
partial void OnBreadcrumbsChanged(ObservableCollection<BreadcrumbItem>? oldValue, ObservableCollection<BreadcrumbItem> newValue)
{
Debug.WriteLine("MainViewModel::OnBreadcrumbsChanged: Breadcrumbs collection updated.");
OnPropertyChanged(nameof(Breadcrumbs));
}
partial void OnTreeItemsChanged(ObservableCollection<TreeViewItem>? oldValue, ObservableCollection<TreeViewItem> newValue)
{
Debug.WriteLine("MainViewModel::OnTreeItemsChanged: TreeItems changed.");
OnPropertyChanged(nameof(TreeItems));
}
partial void OnSubClassificationFoldersChanged(ObservableCollection<SubClassificationFolder>? oldValue, ObservableCollection<SubClassificationFolder> newValue)
{
Debug.WriteLine($"MainViewModel::OnSubClassificationFoldersChanged: SubClassificationFolders updated. Count: {newValue.Count}");
}
}
public class ClassificationItem
{
public string Name { get; set; }
public int DocumentCount { get; set; }
public List<Document> Documents { get; set; }
public Dictionary<string, object> SubClassificationData { get; set; }
public ClassificationItem()
{
Name = string.Empty;
Documents = new List<Document>();
SubClassificationData = new Dictionary<string, object>();
}
}
public class SubClassificationFolder
{
public string DisplayName { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string ParentClassification { get; set; } = string.Empty;
public int Level { get; set; }
public List<Document> Documents { get; set; } = new();
}
public partial class BreadcrumbItem : ObservableObject
{
[ObservableProperty]
private string text = string.Empty;
[ObservableProperty]
private string level = string.Empty;
[ObservableProperty]
private bool isLast;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment