feat: Add manual membership payment page and week picker component
All checks were successful
Build and Deploy / build (push) Successful in 2m36s

This commit is contained in:
masoodafar-web
2025-12-12 10:24:43 +03:30
parent 2fc7733c84
commit cc721da65f
16 changed files with 448 additions and 66 deletions

View File

@@ -0,0 +1,131 @@
@page "/payment/membership"
@using BackOffice.BFF.ManualPayment.Protobuf
@using BackOffice.Main.Pages.AutoComplete
@attribute [Authorize(Roles = "Admin,SuperAdmin")]
<PageTitle>پرداخت دستی عضویت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">
<MudIcon Icon="@Icons.Material.Filled.Payment" Class="ml-2" />
پرداخت دستی عضویت
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<!-- انتخاب کاربر -->
<MudItem xs="12" md="6">
<UserAutoComplete
Label="انتخاب کاربر *"
@bind-SelectedUserId="_userId"
HelperText="جستجو با نام، موبایل یا شناسه" />
</MudItem>
<!-- مبلغ -->
<MudItem xs="12" md="6">
<MudNumericField
@bind-Value="_amount"
Label="مبلغ (ریال) *"
Min="0"
Step="10000"
Format="N0"
Variant="Variant.Outlined"
HelperText="مبلغ پرداختی به ریال" />
</MudItem>
<!-- شماره مرجع -->
<MudItem xs="12" md="6">
<MudTextField
@bind-Value="_referenceNumber"
Label="شماره مرجع *"
Variant="Variant.Outlined"
MaxLength="50"
HelperText="شماره مرجع تراکنش یا فیش" />
</MudItem>
<!-- توضیحات -->
<MudItem xs="12" md="6">
<MudTextField
@bind-Value="_description"
Label="توضیحات"
Variant="Variant.Outlined"
Lines="3"
MaxLength="500"
HelperText="توضیحات اختیاری" />
</MudItem>
</MudGrid>
@if (_showResult)
{
<MudAlert Severity="Severity.Success" Class="mt-4" ShowCloseIcon CloseIconClicked="@(() => _showResult = false)">
<MudText>@_resultMessage</MudText>
<MudText Typo="Typo.caption">شماره تراکنش: @_transactionId</MudText>
<MudText Typo="Typo.caption">شماره سفارش: @_orderId</MudText>
<MudText Typo="Typo.caption">موجودی جدید کیف پول: @_newWalletBalance.ToString("N0") ریال</MudText>
</MudAlert>
}
</MudCardContent>
<MudCardActions>
<MudButton
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="ProcessPayment"
Disabled="@_isProcessing">
@if (_isProcessing)
{
<MudProgressCircular Class="ml-2" Size="Size.Small" Indeterminate="true" />
<MudText>در حال ثبت...</MudText>
}
else
{
<MudText>ثبت پرداخت</MudText>
}
</MudButton>
<MudButton
Variant="Variant.Outlined"
Color="Color.Default"
StartIcon="@Icons.Material.Filled.Clear"
OnClick="ResetForm"
Disabled="@_isProcessing">
پاک کردن فرم
</MudButton>
</MudCardActions>
</MudCard>
<!-- راهنما -->
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="ml-2" />
راهنما
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.body2" Class="mb-2">این صفحه برای ثبت پرداخت‌های دستی عضویت استفاده می‌شود. پس از ثبت:</MudText>
<MudList Dense="true">
<MudListItem Icon="@Icons.Material.Filled.AccountBalance">
<MudText Typo="Typo.body2">مبلغ به کیف پول کاربر (Balance و DiscountBalance) اضافه می‌شود</MudText>
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.History">
<MudText Typo="Typo.body2">لاگ تغییرات کیف پول ثبت می‌شود</MudText>
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Receipt">
<MudText Typo="Typo.body2">تراکنش با وضعیت موفق ثبت می‌شود</MudText>
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.ShoppingCart">
<MudText Typo="Typo.body2">سفارش خرید پکیج عضویت ثبت می‌شود</MudText>
</MudListItem>
</MudList>
</MudCardContent>
</MudCard>
</MudContainer>

View File

@@ -0,0 +1,103 @@
using BackOffice.BFF.ManualPayment.Protobuf;
using BackOffice.Main.Components;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Main.Pages.Payment;
public partial class ManualMembershipPayment
{
[Inject] private ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!;
[Inject] private ISnackbar Snackbar { get; set; } = default!;
private long? _userId;
private long _amount = 0;
private string _referenceNumber = string.Empty;
private string? _description;
private bool _isProcessing = false;
private bool _showResult = false;
private string _resultMessage = string.Empty;
private long _transactionId = 0;
private long _orderId = 0;
private long _newWalletBalance = 0;
private async Task ProcessPayment()
{
// Validation
if (!_userId.HasValue || _userId.Value <= 0)
{
Snackbar.Add("لطفا کاربر را انتخاب کنید", Severity.Warning);
return;
}
if (_amount <= 0)
{
Snackbar.Add("لطفا مبلغ معتبری وارد کنید", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(_referenceNumber))
{
Snackbar.Add("لطفا شماره مرجع را وارد کنید", Severity.Warning);
return;
}
try
{
_isProcessing = true;
_showResult = false;
var request = new ProcessManualMembershipPaymentRequest
{
UserId = _userId.Value,
Amount = _amount,
ReferenceNumber = _referenceNumber
};
if (!string.IsNullOrWhiteSpace(_description))
{
request.Description = _description;
}
var response = await ManualPaymentClient.ProcessManualMembershipPaymentAsync(request);
_resultMessage = response.Message;
_transactionId = response.TransactionId;
_orderId = response.OrderId;
_newWalletBalance = response.NewWalletBalance;
_showResult = true;
Snackbar.Add("پرداخت دستی با موفقیت ثبت شد", Severity.Success);
// Reset form
await Task.Delay(2000);
ResetForm();
}
catch (RpcException ex)
{
Snackbar.Add($"خطا در ثبت پرداخت: {ex.Status.Detail}", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"خطای غیرمنتظره: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
}
}
private void ResetForm()
{
_userId = null;
_amount = 0;
_referenceNumber = string.Empty;
_description = null;
_showResult = false;
_resultMessage = string.Empty;
_transactionId = 0;
_orderId = 0;
_newWalletBalance = 0;
}
}

View File

@@ -118,7 +118,7 @@
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.6" />
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.7" />
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.12" />
<PackageReference Include="Foursat.BackOffice.BFF.Common.Protobuf" Version="0.0.2" />
@@ -134,7 +134,7 @@
<PackageReference Include="Foursat.BackOffice.BFF.Health.Protobuf" Version="1.0.4" />
<PackageReference Include="Foursat.BackOffice.BFF.ManualPayment.Protobuf" Version="0.0.2" />
<PackageReference Include="Foursat.BackOffice.BFF.ManualPayment.Protobuf" Version="0.0.3" />
<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.7" />

View File

@@ -0,0 +1,21 @@
@using Foursat.BackOffice.BFF.Commission.Protos
<MudAutocomplete T="WeekInfo"
Label="@Label"
Value="@_selectedWeek"
DebounceInterval="300"
ValueChanged="@OnSelected"
ToStringFunc="@(e => e?.WeekNumber ?? string.Empty)"
SearchFunc="@Search"
Variant="Variant.Outlined"
Clearable="true"
ShowProgressIndicator="true"
CoerceText="@CoerceText"
ResetValueOnEmptyText="true"
OnClearButtonClick="@OnClear">
<ItemTemplate Context="week">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>@week.DisplayText</MudText>
</MudStack>
</ItemTemplate>
</MudAutocomplete>

View File

@@ -0,0 +1,107 @@
using BackOffice.Services;
using Foursat.BackOffice.BFF.Commission.Protos;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
public partial class WeekNumberPicker
{
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; } = default!;
[Inject] public IPersianDateTimeService PersianDateTime { get; set; } = default!;
[Parameter] public string Label { get; set; } = "انتخاب هفته";
[Parameter] public string? SelectedWeekNumber { get; set; }
[Parameter] public EventCallback<string?> SelectedWeekNumberChanged { get; set; }
[Parameter] public bool CoerceText { get; set; } = true; // اجازه ورود دستی
[Parameter] public int FutureWeeksCount { get; set; } = 4;
[Parameter] public int PastWeeksCount { get; set; } = 12;
private WeekInfo? _selectedWeek;
private List<WeekInfo> _allWeeks = new();
private bool _isLoaded = false;
protected override async Task OnInitializedAsync()
{
await LoadWeeks();
if (!string.IsNullOrWhiteSpace(SelectedWeekNumber) && _allWeeks.Any())
{
_selectedWeek = _allWeeks.FirstOrDefault(w => w.WeekNumber == SelectedWeekNumber);
}
}
protected override async Task OnParametersSetAsync()
{
if (!string.IsNullOrWhiteSpace(SelectedWeekNumber) &&
_selectedWeek?.WeekNumber != SelectedWeekNumber &&
_isLoaded)
{
_selectedWeek = _allWeeks.FirstOrDefault(w => w.WeekNumber == SelectedWeekNumber);
}
}
private async Task LoadWeeks()
{
try
{
var request = new GetAvailableWeeksRequest
{
FutureWeeksCount = FutureWeeksCount,
PastWeeksCount = PastWeeksCount
};
var response = await CommissionContract.GetAvailableWeeksAsync(request);
_allWeeks = new List<WeekInfo>();
// ترتیب: هفته جاری، محاسبه شده، در انتظار، آینده
if (response.CurrentWeek != null)
_allWeeks.Add(response.CurrentWeek);
if (response.CalculatedWeeks != null)
_allWeeks.AddRange(response.CalculatedWeeks);
if (response.PendingWeeks != null)
_allWeeks.AddRange(response.PendingWeeks);
if (response.FutureWeeks != null)
_allWeeks.AddRange(response.FutureWeeks);
_isLoaded = true;
}
catch
{
_allWeeks = new List<WeekInfo>();
_isLoaded = true;
}
}
private async Task<IEnumerable<WeekInfo>> Search(string value, CancellationToken cancellationToken)
{
if (!_isLoaded)
await LoadWeeks();
if (string.IsNullOrWhiteSpace(value))
return _allWeeks;
// جستجو در شماره هفته
return _allWeeks
.Where(w => w.WeekNumber.Contains(value, StringComparison.OrdinalIgnoreCase))
.ToList();
}
private async Task OnSelected(WeekInfo? selected)
{
_selectedWeek = selected;
SelectedWeekNumber = selected?.WeekNumber;
await SelectedWeekNumberChanged.InvokeAsync(SelectedWeekNumber);
}
private async Task OnClear()
{
_selectedWeek = null;
SelectedWeekNumber = null;
await SelectedWeekNumberChanged.InvokeAsync(null);
}
}

View File

@@ -3,6 +3,7 @@
@using Foursat.BackOffice.BFF.Commission.Protos
@using MudBlazor
@using BackOffice.Pages.AutoComplete
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
@@ -151,12 +152,10 @@
}
else
{
<MudTextField @bind-Value="_currentWeekNumber"
Label="شماره هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
HelperText="فرمت: YYYY-Www (مثلاً 2025-W48)"
Style="max-width: 200px;" />
<WeekNumberPicker Label="شماره هفته"
@bind-SelectedWeekNumber="_currentWeekNumber"
CoerceText="true"
Style="max-width: 250px;" />
}
<MudButton Variant="Variant.Filled"

View File

@@ -11,6 +11,11 @@ public partial class Dashboard
{
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
[Inject] public IPersianDateTimeService PersianDateTime { get; set; }
[Inject] public NavigationManager NavigationManager { get; set; }
[SupplyParameterFromQuery(Name = "week")]
public string? WeekQueryParam { get; set; }
private bool _isLoading = true;
private string _currentWeekNumber = string.Empty;
@@ -26,8 +31,17 @@ public partial class Dashboard
// بارگذاری لیست هفته‌های قابل انتخاب
await LoadAvailableWeeks();
// محاسبه شماره هفته جاری (میلادی برای API)
_currentWeekNumber = GetCurrentWeekNumber();
// اگر query string آمده، از آن استفاده کن
if (!string.IsNullOrWhiteSpace(WeekQueryParam))
{
_currentWeekNumber = WeekQueryParam;
}
else
{
// محاسبه شماره هفته جاری (میلادی برای API)
_currentWeekNumber = GetCurrentWeekNumber();
}
// تبدیل به شمسی برای نمایش
_currentWeekNumberPersian = PersianDateTime.ConvertWeekNumberToPersian(_currentWeekNumber);
@@ -66,6 +80,11 @@ public partial class Dashboard
{
_currentWeekNumber = weekNumber;
_currentWeekNumberPersian = PersianDateTime.ConvertWeekNumberToPersian(weekNumber);
// به‌روزرسانی URL با query string
var uri = NavigationManager.GetUriWithQueryParameter("week", weekNumber);
NavigationManager.NavigateTo(uri, false);
await LoadPoolData();
}

View File

@@ -4,9 +4,10 @@
@using Foursat.BackOffice.BFF.Commission.Protos
@using Google.Protobuf.WellKnownTypes
@using MudBlazor
@using BackOffice.Pages.AutoComplete
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">Payout های کاربران</MudText>
<MudText Typo="Typo.h4" Class="mb-4"> پرداخت های کاربران</MudText>
<MudDataGrid T="UserCommissionPayoutModel"
ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))"
@@ -14,22 +15,15 @@
@ref="_gridData"
Height="75vh">
<ToolBarContent>
<MudText Typo="Typo.h6">لیست Payout ها</MudText>
<MudText Typo="Typo.h6">لیست پرداخت ها</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<MudTextField @bind-Value="_filterUserId"
Label="شناسه کاربر"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Style="max-width: 150px;" />
<MudTextField @bind-Value="_filterWeekNumber"
Label="شماره هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Placeholder="2025-W48"
Style="max-width: 150px;" />
<UserAutoComplete Label="جستجوی کاربر"
@bind-SelectedUserId="_filterUserId" />
<WeekNumberPicker Label="شماره هفته"
@bind-SelectedWeekNumber="_filterWeekNumber"
CoerceText="true"
/>
<MudSelect @bind-Value="_filterStatus"
Label="وضعیت"
Variant="Variant.Outlined"

View File

@@ -1,3 +1,4 @@
using BackOffice.BFF.Protobuf.Common;
using Foursat.BackOffice.BFF.Commission.Protos;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -23,23 +24,27 @@ public partial class UserPayouts
{
var request = new GetUserCommissionPayoutsRequest
{
PageIndex = state.Page + 1,
PageSize = state.PageSize
PaginationState = new PaginationState()
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = new GetUserPayoutsFilter()
};
if (_filterUserId.HasValue)
if (_filterUserId.HasValue && _filterUserId.Value > 0)
{
request.UserId = _filterUserId.Value;
request.Filter.UserId = _filterUserId.Value;
}
if (!string.IsNullOrEmpty(_filterWeekNumber))
if (!string.IsNullOrWhiteSpace(_filterWeekNumber))
{
request.WeekNumber = _filterWeekNumber;
request.Filter.WeekNumber = _filterWeekNumber;
}
if (_filterStatus.HasValue)
{
request.Status = _filterStatus.Value;
request.Filter.Status = _filterStatus.Value;
}
var result = await CommissionContract.GetUserCommissionPayoutsAsync(request);

View File

@@ -6,6 +6,7 @@
@using Google.Protobuf.WellKnownTypes
@using Microsoft.JSInterop
@using System.Text
@using BackOffice.Pages.AutoComplete
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-1">گزارش برداشت‌ها</MudText>
@@ -41,10 +42,8 @@
</MudSelect>
</MudItem>
<MudItem xs="12" md="3">
<MudNumericField TValue="long?" @bind-Value="_userId"
Label="شناسه کاربر (اختیاری)"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<UserAutoComplete Label="جستجوی کاربر (اختیاری)"
@bind-SelectedUserId="_userId" />
</MudItem>
</MudGrid>

View File

@@ -2,6 +2,7 @@
@attribute [Authorize]
@using Foursat.BackOffice.BFF.Commission.Protos
@using BackOffice.Pages.AutoComplete
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">درخواست‌های برداشت</MudText>
@@ -15,14 +16,8 @@
<MudText Typo="Typo.h6">درخواست‌های برداشت</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<MudNumericField T="long?"
HideSpinButtons="true"
Clearable="true"
Label="شناسه کاربر"
@bind-Value="_filterUserId"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="max-width: 140px;" />
<UserAutoComplete Label="جستجوی کاربر"
@bind-SelectedUserId="_filterUserId" />
<MudTextField T="string"
Clearable="true"
Label="شماره شبا (IBAN)"

View File

@@ -22,10 +22,9 @@
@bind-SelectedUserId="_filterUserId" />
</MudItem>
<MudItem xs="12" md="3">
<MudTextField @bind-Value="_filterWeekNumber"
Label="شماره هفته"
Placeholder="2025-W48"
Variant="Variant.Outlined" />
<WeekNumberPicker Label="شماره هفته"
@bind-SelectedWeekNumber="_filterWeekNumber"
CoerceText="true" />
</MudItem>
<MudItem xs="12" md="2">
<MudNumericField @bind-Value="_minBalance"

View File

@@ -2,16 +2,12 @@
@using BackOffice.BFF.ManualPayment.Protobuf
@using BackOffice.Pages.Payment.Components
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudNumericField T="long?"
HideSpinButtons="true"
Clearable="true"
Label="شناسه کاربر"
Variant="Variant.Outlined"
Margin="Margin.Dense"
@bind-Value="_userIdFilter" />
<UserAutoComplete Label="جستجوی کاربر"
@bind-SelectedUserId="_userIdFilter" />
<MudTextField T="string"
Label="شماره مرجع"

View File

@@ -2,6 +2,7 @@
@using MudBlazor
@using BackOffice.Pages.SystemManagement
@using BackOffice.Pages.AutoComplete
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">کنترل Worker محاسبات</MudText>
@@ -59,10 +60,9 @@
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudTextField @bind-Value="_manualWeekNumber"
Label="شماره هفته برای اجرای دستی"
Placeholder="2025-W48"
Variant="Variant.Outlined" />
<WeekNumberPicker Label="شماره هفته برای اجرای دستی"
@bind-SelectedWeekNumber="_manualWeekNumber"
CoerceText="true" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"

View File

@@ -4,6 +4,7 @@
@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
@using BackOffice.Pages.UserOrder.Components
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using DataModel = BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder.GetAllUserOrderByFilterResponseModel
<BackOffice.Common.BaseComponents.BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared"
@@ -17,13 +18,8 @@
Variant="Variant.Outlined"
Margin="Margin.Dense"/>
<MudNumericField HideSpinButtons="true"
T="long?"
Clearable="true"
Label="شناسه کاربر"
@bind-Value="@_userIdFilter"
Variant="Variant.Outlined"
Margin="Margin.Dense"/>
<UserAutoComplete Label="جستجوی کاربر"
@bind-SelectedUserId="@_userIdFilter" />
<MudNumericField HideSpinButtons="true"
T="long?"

View File

@@ -157,6 +157,22 @@
مدیریت نقش
</MudNavLink>
}
@if (CanManagePayments)
{
<MudNavGroup Title="پرداخت‌ها" Icon="@Icons.Material.Filled.Payment" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/manual-payments"
Icon="@Icons.Material.Filled.Payments">
پرداخت‌های دستی
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/membership"
Icon="@Icons.Material.Filled.CardMembership">
پرداخت دستی عضویت
</MudNavLink>
</MudNavGroup>
}
</Authorized>
</AuthorizeView>
@@ -273,6 +289,7 @@
private bool CanManageTags;
private bool CanManageUsers;
private bool CanManageRoles;
private bool CanManagePayments;
private bool CanManageDiscountShop;
private bool CanManagePublicMessages;
private bool CanViewSystemAlerts;
@@ -300,6 +317,7 @@
CanManageTags = await AuthorizationService.HasPermissionAsync("tags.manage");
CanManageUsers = await AuthorizationService.HasPermissionAsync("users.view");
CanManageRoles = await AuthorizationService.HasPermissionAsync("roles.manage");
CanManagePayments = await AuthorizationService.HasPermissionAsync("manualpayments.create");
CanManageDiscountShop = await AuthorizationService.HasPermissionAsync("discountshop.manage");
CanManagePublicMessages = await AuthorizationService.HasPermissionAsync("publicmessages.view");
CanViewSystemAlerts = await AuthorizationService.HasPermissionAsync("system.alerts.view");