Skip to main content

Overview

AndanDo provides comprehensive revenue tracking and analytics to help hosts understand their business performance, identify trends, and optimize their tours.

Statistics Service Methods

The platform provides several methods for retrieving analytics data:

Get Tour Reservation Stats

Retrieve detailed statistics for a specific tour:
Services/Tour/TourService.cs:1897
public async Task<TourReservationStatsDto> GetTourReservationStatsAsync(
    int tourId,
    int recentDays = 30,
    CancellationToken cancellationToken = default)
{
    var sql = $@"
    SELECT 
        CAST(r.CreatedAt AS date) AS DayDate,
        COUNT(*) AS ReservationCount,
        COALESCE(SUM(r.AmountPaid), 0) AS TotalAmount,
        SUM(CASE WHEN r.PaymentStatus = 1 THEN 1 ELSE 0 END) AS PaidCount,
        SUM(CASE WHEN r.PaymentStatus <> 1 THEN 1 ELSE 0 END) AS PendingCount,
        COALESCE(SUM(r.AmountPaid), 0) AS PaidAmount,
        COALESCE(SUM(r.TotalAmount - ISNULL(r.AmountPaid, 0)), 0) AS PendingAmount
    FROM {reservationTable} r
    WHERE r.TourId = @TourId
      AND r.CreatedAt >= DATEADD(day, -@RecentDays, SYSUTCDATETIME())
    GROUP BY CAST(r.CreatedAt AS date)
    ORDER BY DayDate ASC;";
    
    // Returns daily breakdown and totals
}

Get Owner Earnings

Calculate total earnings for a host:
Services/Tour/TourService.cs:1764
public async Task<decimal> GetOwnerEarningsAsync(
    int ownerUserId,
    CancellationToken cancellationToken = default)
{
    var sql = $@"
    SELECT COALESCE(SUM(r.AmountPaid), 0)
    FROM {reservationTable} r
    INNER JOIN {tourTable} t ON t.TourId = r.TourId
    WHERE t.OwnerUserId = @OwnerUserId";
    
    var result = await ExecuteScalarAsync<decimal>(sql, new { OwnerUserId = ownerUserId });
    return result;
}

Get Owner Tour Stats

Retrieve tour count statistics:
Services/Tour/TourService.cs:1716
public async Task<(int TotalServices, int RecentServices)> GetOwnerTourStatsAsync(
    int ownerUserId,
    int recentDays = 7,
    CancellationToken cancellationToken = default)
{
    var totalSql = $@"
    SELECT COUNT(*)
    FROM {tourTable}
    WHERE OwnerUserId = @OwnerUserId
      AND IsActive = 1";
    
    var recentSql = $@"
    SELECT COUNT(*)
    FROM {tourTable}
    WHERE OwnerUserId = @OwnerUserId
      AND IsActive = 1
      AND CreatedAt >= DATEADD(day, -@RecentDays, SYSUTCDATETIME())";
    
    // Returns (total, recent)
}

Statistics Data Transfer Objects

TourReservationStatsDto

Main statistics container:
Dtos/TourDtos.cs:91
public sealed record TourReservationStatsDto(
    int TourId,
    int TotalReservations,
    decimal TotalAmount,
    IReadOnlyList<TourReservationDayStat> Daily);

TourReservationDayStat

Daily breakdown of reservations and revenue:
Dtos/TourDtos.cs:82
public sealed record TourReservationDayStat(
    DateTime Date,
    int Count,
    decimal Amount,
    int PaidCount,
    int PendingCount,
    decimal PaidAmount,
    decimal PendingAmount);
The DayStat record includes separate tracking for paid and pending payments.

Dashboard Statistics Display

The dashboard shows three key metrics for selected tours:
1

Total Tickets

Displays the sum of all tickets sold for the event
2

Total Reservations

Shows the count of all reservation records
3

Total Sales

Presents the accumulated revenue in the tour’s currency

Metric Cards Implementation

Components/Pages/Dashboard/Index.razor:71
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px;">
    <!-- Tickets Card -->
    <div style="background:linear-gradient(135deg,#f0fdf4,#dcfce7);">
        <div style="font-size:32px;font-weight:800;">@_reportTotalTickets</div>
        <div>Tickets registrados para el evento</div>
    </div>
    
    <!-- Reservations Card -->
    <div style="background:linear-gradient(135deg,#eff6ff,#dbeafe);">
        <div style="font-size:32px;font-weight:800;">@_reportReservationCount</div>
        <div>Incluye todas las reservas asociadas</div>
    </div>
    
    <!-- Sales Card -->
    <div style="background:linear-gradient(135deg,#fff8f5,#ffedd5);">
        <div style="font-size:24px;font-weight:800;">
            @FormatCurrency(_reportTotalAmount, _reportCurrency)
        </div>
        <div>Monto acumulado del evento</div>
    </div>
</div>

Sales Modal Analytics

The bookings page includes a detailed sales modal for each tour:

Opening the Modal

Components/Pages/Dashboard/MyBookings.razor:992
private async Task OpenStatsModalAsync(TourBookingViewModel model)
{
    _selectedService = model;
    _statsError = null;
    _salesStats = null;
    _showStatsModal = true;
    _loadingStats = true;

    await LoadSalesStatsAsync(model.Tour.TourId);
}

Loading Statistics

Components/Pages/Dashboard/MyBookings.razor:1003
private async Task LoadSalesStatsAsync(int tourId)
{
    try
    {
        _salesStats = await TourService.GetTourReservationStatsAsync(
            tourId, 
            _salesWindowDays
        );
        _reservationDetails = await TourService.GetReservationsByTourAsync(tourId);
    }
    catch (Exception ex)
    {
        _statsError = "No se pudieron cargar las ventas.";
        Console.WriteLine(ex);
    }
    finally
    {
        _loadingStats = false;
        await InvokeAsync(StateHasChanged);
        
        if (_salesStats is not null && _salesStats.Daily.Any())
        {
            await RenderChartAsync();
        }
    }
}

Sales Chart Visualization

The sales modal includes a Chart.js chart showing daily trends:

Chart Rendering

Components/Pages/Dashboard/MyBookings.razor:1027
private async Task RenderChartAsync()
{
    try 
    {
        var ordered = SortedSalesDaily.ToList();
        var labels = ordered.Select(d => d.Date.ToString("dd/MM")).ToArray();
        var paidCounts = ordered.Select(d => d.PaidCount).ToArray();
        var pendingCounts = ordered.Select(d => d.PendingCount).ToArray();
        var paidAmounts = ordered.Select(d => d.PaidAmount).ToArray();
        var pendingAmounts = ordered.Select(d => d.PendingAmount).ToArray();

        await JS.InvokeVoidAsync("chartHelper.renderSalesChart", 
            "salesChart", 
            labels, 
            paidCounts,
            pendingCounts,
            paidAmounts, 
            pendingAmounts,
            "Reservas Pagadas", 
            "Reservas Pendientes",
            "Dinero Pagado",
            "Dinero Pendiente");
    }
    catch(Exception ex)
    {
         Console.WriteLine($"Error rendering chart: {ex.Message}");
    }
}

Chart Data Structure

The chart displays four data series:

Paid Reservations

Count of reservations with PaymentStatus = 1

Pending Reservations

Count of reservations with PaymentStatus ≠ 1

Paid Amount

Sum of AmountPaid for all reservations

Pending Amount

Sum of (TotalAmount - AmountPaid)

Recent Reservations Table

The modal includes a table of recent bookings:
Components/Pages/Dashboard/MyBookings.razor:357
<div style="overflow-x:auto;max-height:220px;overflow-y:auto;">
    @if (_reservationDetails is null || !_reservationDetails.Any())
    {
        <p>No hay detalles disponibles.</p>
    }
    else
    {
        <table class="bk-table">
            <thead>
                <tr>
                    <th>Fecha</th>
                    <th>Cliente</th>
                    <th>Cant.</th>
                    <th>Total</th>
                    <th>Estado</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var r in _reservationDetails.OrderByDescending(r => r.TravelDate))
                {
                    <tr>
                        <td>@r.TravelDate.ToString("dd MMM HH:mm")</td>
                        <td>@r.ContactName</td>
                        <td>@r.Tickets.Sum(t => t.Quantity)</td>
                        <td>@FormatCurrency(r.TotalAmount, r.CurrencyCode)</td>
                        <td>
                            <span class="bk-badge @(r.PaymentStatus == 1 ? "bk-badge--success" : "bk-badge--warning")">
                                @GetPaymentLabel(r.PaymentStatus)
                            </span>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    }
</div>

Date Range Support

Statistics can be filtered by date range:

Get Tour Date Range

Services/Tour/TourService.cs:1800
public async Task<(DateTime? StartDate, DateTime? EndDate)> GetTourDateRangeAsync(
    int tourId,
    CancellationToken cancellationToken = default)
{
    const string sql = @"
    SELECT 
        MIN(FechaInicio) AS StartDate,
        MAX(FechaFin) AS EndDate
    FROM TourFechas
    WHERE EventoId = @TourId";
    
    // Returns date range from TourFechas table
}
The default analysis window is 30 days, but this can be customized in the GetTourReservationStatsAsync call.

Performance Metrics

Key performance indicators tracked:

Per-Tour Metrics

MetricSourceCalculation
Total ReservationsTourReservation.TourReservationIdCOUNT(*)
Total RevenueTourReservation.AmountPaidSUM(AmountPaid)
Average Order ValueCalculatedTotalRevenue / TotalReservations
Conversion RateMultiple tables(Reservations / Views) * 100

Multi-Tour Metrics

MetricDescription
Total ServicesCount of active tours owned by user
Recent ServicesTours created in last N days
Portfolio RevenueSum of AmountPaid across all tours
Active BookingsCount of future reservations

Payment Status Breakdown

Statistics distinguish between payment states:
var paidCount = reservations.Count(r => r.PaymentStatus == 1);
var pendingCount = reservations.Count(r => r.PaymentStatus == 0);
var partialCount = reservations.Count(r => r.PaymentStatus == 2);
var cancelledCount = reservations.Count(r => r.PaymentStatus == 4);

Status Codes

CodeLabelDescription
0PendienteNo payment received
1PagadoFully paid
2Pago ParcialPartial payment received
3ReembolsadoRefunded
4CanceladoCancelled

Currency Handling

Statistics respect currency settings per tour:
private static string FormatCurrency(decimal amount, string currencyCode)
{
    var symbol = currencyCode switch
    {
        "USD" => "$",
        "EUR" => "EUR",
        _ => "RD$"
    };

    return $"{symbol}{amount:N0}";
}
Mixing multiple currencies in aggregated statistics may produce incorrect totals. Each tour’s statistics use its configured currency.

Real-Time Updates

Statistics are loaded dynamically:
1

On Page Load

Statistics are fetched when the dashboard or bookings page loads
2

On Tour Selection

Dashboard reloads stats when a different tour is selected
3

Manual Refresh

Use the “Actualizar” button to reload latest data

Error Handling

Statistics methods handle errors gracefully:
try
{
    _salesStats = await TourService.GetTourReservationStatsAsync(tourId, 30);
}
catch (Exception ex)
{
    Console.WriteLine(ex);
    _statsError = "No se pudieron cargar las ventas.";
    return new TourReservationStatsDto(
        TourId: tourId,
        TotalReservations: 0,
        TotalAmount: 0,
        Daily: Array.Empty<TourReservationDayStat>()
    );
}

Dashboard Overview

View real-time metrics on your dashboard

Managing Bookings

Access the bookings page for detailed analytics

Database Schema

Understand the underlying data structure

Build docs developers (and LLMs) love