FeedbackComponentService
Documentație pentru serviciul FeedbackComponentService
1. Descriere Generală
FeedbackComponentService
este un serviciu pentru gestionarea feedback-ului utilizatorilor asupra serviciilor guvernamentale. Permite colectarea evaluărilor și comentariilor, afișarea statisticilor agregate și navigarea către portalul extern de feedback.
Caracteristici principale: - Colectare feedback cu rating 1-5 stele - Mesaje opționale cu limită de 500 caractere - Suport pentru utilizatori autentificați și anonimi - Statistici agregate (medie rating, număr recenzii) - Integrare cu portal extern de feedback - Validare date contact și demografice
2. Configurare și Înregistrare
// Program.cs sau Startup.cs
// Înregistrare automată prin AddFodComponents
builder.Services.AddFodComponents(configuration);
// Pentru server-side
builder.Services.AddFodComponentsServer(configuration, connectionString);
// Sau înregistrare manuală
builder.Services.AddScoped<IFeedbackComponentService, FeedbackComponentService>();
builder.Services.AddHttpClient<IFeedbackComponentService, FeedbackComponentService>(client =>
{
client.BaseAddress = new Uri(configuration["ApiSettings:BaseUrl"]);
});
3. Interfața IFeedbackComponentService
namespace Fod.Components.Shared.Business.Feedback
{
public interface IFeedbackComponentService
{
// Trimite feedback
Task<int> Add(FeedbackComponentModel feedbackComponentModel);
// Obține statistici agregate pentru un serviciu
Task<GetFeedbackModel> Get(string serviceInternalCode);
// Obține lista de feedback-uri pentru un serviciu
Task<List<FeedbackComponentModel>> GetList(string serviceInternalCode);
// Obține URL portal feedback frontoffice
Task<string> GetFoFeedbackAddress();
// Obține URL termeni și condiții
Task<string> GetTacAddress();
}
}
4. Metode Disponibile
Metodă | Parametri | Return | Descriere |
---|---|---|---|
Add |
FeedbackComponentModel feedbackComponentModel |
Task<int> |
Adaugă un feedback nou |
Get |
string serviceInternalCode |
Task<GetFeedbackModel> |
Obține statistici agregate |
GetList |
string serviceInternalCode |
Task<List<FeedbackComponentModel>> |
Obține lista de feedback-uri |
GetFoFeedbackAddress |
- | Task<string> |
Obține URL portal feedback |
GetTacAddress |
- | Task<string> |
Obține URL termeni și condiții |
5. Modele de Date
FeedbackComponentModel
public class FeedbackComponentModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
[EmailAddress]
public string Email { get; set; }
[Phone]
public string Phone { get; set; }
public int? Age { get; set; }
public SexEnum? Sex { get; set; }
[Required]
[Range(1, 5)]
public int Score { get; set; }
[MaxLength(500)]
public string Message { get; set; }
public string FodServiceProviderInternalCode { get; set; }
public string ServiceInternalCode { get; set; }
public string ServiceRequestNumber { get; set; }
public string UserId { get; set; }
}
public enum SexEnum
{
Male = 0,
Female = 1
}
GetFeedbackModel
public class GetFeedbackModel
{
public double? AverageScore { get; set; }
public int TotalCount { get; set; }
public string ServiceName { get; set; }
}
6. Exemple de Utilizare
Formular feedback simplu
@page "/servicii/feedback"
@inject IFeedbackComponentService FeedbackService
@inject IFodNotificationService NotificationService
@inject ICurrentUserContextService UserContext
<FodContainer>
<FodText Typo="Typo.h4" GutterBottom="true">
Lăsați-ne un feedback
</FodText>
<FodCard>
<FodCardContent>
<!-- Rating -->
<FodText Typo="Typo.h6" GutterBottom="true">
Cum evaluați experiența dvs?
</FodText>
<FodRating @bind-Value="feedback.Score"
Size="FodSize.Large"
ShowLabels="true" />
<!-- Mesaj opțional -->
<FodTextArea @bind-Value="feedback.Message"
Label="Mesaj (opțional)"
Rows="4"
MaxLength="500"
HelperText="@($"{feedback.Message?.Length ?? 0}/500 caractere")"
Class="mt-4" />
<!-- Date contact pentru utilizatori neautentificați -->
@if (!isAuthenticated)
{
<FodGrid Container="true" Spacing="3" Class="mt-4">
<FodGrid Item="true" xs="12" sm="6">
<FodInput @bind-Value="feedback.FirstName"
Label="Prenume"
Required="true" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="6">
<FodInput @bind-Value="feedback.LastName"
Label="Nume"
Required="true" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="6">
<FodInput @bind-Value="feedback.Email"
Label="Email"
Type="email"
Required="true" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="6">
<FodInput @bind-Value="feedback.Phone"
Label="Telefon"
Mask="+373 (__) ___-___" />
</FodGrid>
</FodGrid>
}
<!-- Date demografice opționale -->
<FodExpansionPanels Class="mt-4">
<FodExpansionPanel Title="Date demografice (opțional)">
<FodGrid Container="true" Spacing="3">
<FodGrid Item="true" xs="12" sm="6">
<FodInputNumber @bind-Value="feedback.Age"
Label="Vârstă"
Min="1"
Max="120" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="6">
<FodRadio @bind-Value="feedback.Sex"
Label="Gen"
Items="@genderOptions" />
</FodGrid>
</FodGrid>
</FodExpansionPanel>
</FodExpansionPanels>
<FodButton OnClick="SubmitFeedback"
Color="FodColor.Primary"
Class="mt-4"
Disabled="@(feedback.Score == 0 || isSubmitting)">
@if (isSubmitting)
{
<FodLoadingButton />
}
else
{
<text>Trimite Feedback</text>
}
</FodButton>
</FodCardContent>
</FodCard>
</FodContainer>
@code {
[Parameter] public string ServiceCode { get; set; }
[Parameter] public string RequestNumber { get; set; }
private FeedbackComponentModel feedback = new();
private bool isAuthenticated;
private bool isSubmitting;
private List<SelectableItem> genderOptions = new()
{
new(SexEnum.Male, "Masculin"),
new(SexEnum.Female, "Feminin")
};
protected override async Task OnInitializedAsync()
{
var userContext = await UserContext.GetCurrentUserContext();
isAuthenticated = userContext?.IsAuthenticated ?? false;
if (isAuthenticated)
{
feedback.FirstName = userContext.FirstName;
feedback.LastName = userContext.LastName;
feedback.Email = userContext.Email;
feedback.Phone = userContext.PhoneNumber;
feedback.UserId = userContext.UserId;
}
feedback.ServiceInternalCode = ServiceCode;
feedback.ServiceRequestNumber = RequestNumber;
}
private async Task SubmitFeedback()
{
if (!await ValidateFeedback())
return;
isSubmitting = true;
try
{
var result = await FeedbackService.Add(feedback);
NotificationService.Success("Mulțumim pentru feedback!");
// Reset form sau redirecționare
feedback = new FeedbackComponentModel
{
ServiceInternalCode = ServiceCode,
ServiceRequestNumber = RequestNumber
};
}
catch (Exception ex)
{
NotificationService.Error($"Eroare la trimiterea feedback-ului: {ex.Message}");
}
finally
{
isSubmitting = false;
}
}
private async Task<bool> ValidateFeedback()
{
if (feedback.Score == 0)
{
NotificationService.Warning("Vă rugăm selectați un rating");
return false;
}
if (!isAuthenticated)
{
if (string.IsNullOrWhiteSpace(feedback.FirstName) ||
string.IsNullOrWhiteSpace(feedback.LastName))
{
NotificationService.Warning("Numele este obligatoriu");
return false;
}
if (string.IsNullOrWhiteSpace(feedback.Email))
{
NotificationService.Warning("Email-ul este obligatoriu");
return false;
}
}
return true;
}
}
Afișare statistici feedback
@inject IFeedbackComponentService FeedbackService
<FodCard>
<FodCardContent>
@if (feedbackStats == null)
{
<FodSkeleton Type="SkeletonType.Rectangle" Height="150" />
}
else
{
<FodGrid Container="true" AlignItems="Align.Center" Spacing="3">
<FodGrid Item="true" xs="12" sm="6">
<div class="text-center">
<FodText Typo="Typo.h3" Color="FodColor.Primary">
@feedbackStats.AverageScore?.ToString("F1") ?? "-"
</FodText>
<FodRating Value="@((int?)feedbackStats.AverageScore ?? 0)"
ReadOnly="true"
Size="FodSize.Small" />
<FodText Typo="Typo.body2" Class="mt-2">
din @feedbackStats.TotalCount recenzii
</FodText>
</div>
</FodGrid>
<FodGrid Item="true" xs="12" sm="6">
<FodText Typo="Typo.h6" GutterBottom="true">
@feedbackStats.ServiceName
</FodText>
<FodButton Variant="FodVariant.Text"
OnClick="ShowAllReviews"
EndIcon="@FodIcons.Material.Filled.ArrowForward">
Vezi toate recenziile
</FodButton>
</FodGrid>
</FodGrid>
}
</FodCardContent>
</FodCard>
@code {
[Parameter] public string ServiceCode { get; set; }
private GetFeedbackModel feedbackStats;
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(ServiceCode))
{
feedbackStats = await FeedbackService.Get(ServiceCode);
}
}
private void ShowAllReviews()
{
// Navighează la pagina cu toate recenziile
}
}
Componenta FodFeedback integrată
@page "/servicii/{ServiceCode}/feedback"
<FodContainer>
<FodText Typo="Typo.h4" GutterBottom="true">
Evaluare Serviciu
</FodText>
<!-- Statistici generale -->
<FodFeedbackBadge ServiceInternalCode="@ServiceCode"
ShowServiceName="true"
Class="mb-4" />
<!-- Formular feedback -->
<FodFeedback ServiceInternalCode="@ServiceCode"
ShowTitle="true"
ShowDemographics="true"
OnFeedbackSubmitted="OnFeedbackSubmitted" />
<!-- Lista recenzii recente -->
@if (recentFeedbacks?.Any() == true)
{
<FodCard Class="mt-4">
<FodCardContent>
<FodText Typo="Typo.h6" GutterBottom="true">
Recenzii recente
</FodText>
<FodList>
@foreach (var review in recentFeedbacks.Take(5))
{
<FodListItem>
<FodListItemAvatar>
<FodAvatar>
@review.FirstName?.FirstOrDefault()
</FodAvatar>
</FodListItemAvatar>
<FodListItemText>
<FodText>
<strong>@review.FirstName @review.LastName?.FirstOrDefault().</strong>
</FodText>
<FodRating Value="review.Score"
ReadOnly="true"
Size="FodSize.Small" />
@if (!string.IsNullOrEmpty(review.Message))
{
<FodText Typo="Typo.body2" Class="mt-1">
@review.Message
</FodText>
}
</FodListItemText>
</FodListItem>
}
</FodList>
</FodCardContent>
</FodCard>
}
</FodContainer>
@code {
[Parameter] public string ServiceCode { get; set; }
private List<FeedbackComponentModel> recentFeedbacks;
protected override async Task OnInitializedAsync()
{
await LoadRecentFeedbacks();
}
private async Task LoadRecentFeedbacks()
{
recentFeedbacks = await FeedbackService.GetList(ServiceCode);
}
private async Task OnFeedbackSubmitted()
{
// Reîncarcă lista după trimitere
await LoadRecentFeedbacks();
}
}
Dashboard analitică feedback
@inject IFeedbackComponentService FeedbackService
@inject IServiceProviderService ServiceProviderService
<FodContainer>
<FodText Typo="Typo.h4" GutterBottom="true">
Analitică Feedback Servicii
</FodText>
<FodGrid Container="true" Spacing="3">
@foreach (var service in services)
{
<FodGrid Item="true" xs="12" md="6" lg="4">
<FodCard Height="100%">
<FodCardContent>
<FodText Typo="Typo.h6" GutterBottom="true">
@service.Name
</FodText>
@if (feedbackData.ContainsKey(service.Code))
{
var data = feedbackData[service.Code];
<div class="d-flex align-items-center mb-2">
<FodIcon Color="FodColor.Warning" Class="me-2">
@FodIcons.Material.Filled.Star
</FodIcon>
<FodText Typo="Typo.h4">
@data.AverageScore?.ToString("F1") ?? "-"
</FodText>
<FodText Typo="Typo.body2" Class="ms-2">
/ 5.0
</FodText>
</div>
<FodText Typo="Typo.body2" GutterBottom="true">
Total recenzii: @data.TotalCount
</FodText>
<!-- Mini grafic distribuție rating -->
<RatingDistribution ServiceCode="@service.Code" />
}
else
{
<FodLoadingLinear Indeterminate="true" />
}
</FodCardContent>
<FodCardActions>
<FodButton Size="FodSize.Small"
OnClick="() => ViewDetails(service.Code)">
Detalii
</FodButton>
<FodButton Size="FodSize.Small"
OnClick="() => ExportData(service.Code)">
Export
</FodButton>
</FodCardActions>
</FodCard>
</FodGrid>
}
</FodGrid>
</FodContainer>
@code {
private List<ServiceModel> services = new();
private Dictionary<string, GetFeedbackModel> feedbackData = new();
protected override async Task OnInitializedAsync()
{
services = await ServiceProviderService.GetActiveServices();
// Încarcă date feedback pentru toate serviciile
var tasks = services.Select(async service =>
{
var feedback = await FeedbackService.Get(service.Code);
feedbackData[service.Code] = feedback;
});
await Task.WhenAll(tasks);
}
}
7. Integrare cu portal extern
public class FeedbackPortalIntegration
{
private readonly IFeedbackComponentService _feedbackService;
private readonly NavigationManager _navigation;
public async Task NavigateToExternalPortal(string serviceCode, string requestNumber)
{
var portalUrl = await _feedbackService.GetFoFeedbackAddress();
// Construiește URL cu parametri
var uriBuilder = new UriBuilder(portalUrl);
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["service"] = serviceCode;
query["request"] = requestNumber;
uriBuilder.Query = query.ToString();
// Navighează în tab nou
_navigation.NavigateTo(uriBuilder.ToString(), true);
}
public async Task ShowTermsAndConditions()
{
var tacUrl = await _feedbackService.GetTacAddress();
_navigation.NavigateTo(tacUrl, true);
}
}
8. Validare avansată
public class FeedbackValidator
{
public ValidationResult Validate(FeedbackComponentModel model)
{
var errors = new List<string>();
// Validare rating
if (model.Score < 1 || model.Score > 5)
{
errors.Add("Rating-ul trebuie să fie între 1 și 5");
}
// Validare email
if (!string.IsNullOrEmpty(model.Email))
{
var emailRegex = new Regex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
if (!emailRegex.IsMatch(model.Email))
{
errors.Add("Email invalid");
}
}
// Validare telefon Moldova
if (!string.IsNullOrEmpty(model.Phone))
{
var phoneRegex = new Regex(@"^\+?373\s?\d{8}$");
if (!phoneRegex.IsMatch(model.Phone.Replace(" ", "").Replace("-", "")))
{
errors.Add("Număr de telefon invalid pentru Moldova");
}
}
// Validare vârstă
if (model.Age.HasValue && (model.Age < 1 || model.Age > 120))
{
errors.Add("Vârsta trebuie să fie între 1 și 120");
}
// Validare mesaj
if (!string.IsNullOrEmpty(model.Message))
{
if (model.Message.Length > 500)
{
errors.Add("Mesajul nu poate depăși 500 de caractere");
}
// Verificare conținut inadecvat
if (ContainsInappropriateContent(model.Message))
{
errors.Add("Mesajul conține conținut inadecvat");
}
}
return new ValidationResult
{
IsValid = !errors.Any(),
Errors = errors
};
}
private bool ContainsInappropriateContent(string message)
{
// Implementare verificare conținut
return false;
}
}
9. Agregare și raportare
@inject IFeedbackComponentService FeedbackService
@inject IReportingService ReportingService
<FodCard>
<FodCardContent>
<FodText Typo="Typo.h6" GutterBottom="true">
Raport Feedback - @selectedPeriod
</FodText>
<!-- Filtre perioadă -->
<FodGrid Container="true" Spacing="2" AlignItems="Align.End" Class="mb-4">
<FodGrid Item="true" xs="12" sm="4">
<FodDatePicker @bind-Date="startDate"
Label="De la"
MaxDate="DateTime.Today" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="4">
<FodDatePicker @bind-Date="endDate"
Label="Până la"
MaxDate="DateTime.Today" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="4">
<FodButton OnClick="GenerateReport"
Color="FodColor.Primary"
FullWidth="true">
Generează Raport
</FodButton>
</FodGrid>
</FodGrid>
@if (reportData != null)
{
<!-- Metrici principale -->
<FodGrid Container="true" Spacing="3" Class="mb-4">
<FodGrid Item="true" xs="12" sm="3">
<MetricCard Title="Rating Mediu"
Value="@reportData.AverageRating.ToString("F2")"
Icon="@FodIcons.Material.Filled.Star"
Color="FodColor.Warning" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="3">
<MetricCard Title="Total Feedback"
Value="@reportData.TotalFeedbacks"
Icon="@FodIcons.Material.Filled.RateReview"
Color="FodColor.Primary" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="3">
<MetricCard Title="Servicii Evaluate"
Value="@reportData.ServicesCount"
Icon="@FodIcons.Material.Filled.BusinessCenter"
Color="FodColor.Success" />
</FodGrid>
<FodGrid Item="true" xs="12" sm="3">
<MetricCard Title="Cu Comentarii"
Value="@reportData.WithComments"
Icon="@FodIcons.Material.Filled.Comment"
Color="FodColor.Info" />
</FodGrid>
</FodGrid>
<!-- Grafic evoluție -->
<FeedbackTrendChart Data="@reportData.TrendData" />
<!-- Top servicii -->
<div class="mt-4">
<FodText Typo="Typo.h6" GutterBottom="true">
Top 10 Servicii după Rating
</FodText>
<TopServicesTable Services="@reportData.TopServices" />
</div>
<!-- Export options -->
<div class="mt-4">
<FodButton OnClick="ExportPDF"
StartIcon="@FodIcons.Material.Filled.PictureAsPdf">
Export PDF
</FodButton>
<FodButton OnClick="ExportExcel"
StartIcon="@FodIcons.Material.Filled.GridOn"
Class="ms-2">
Export Excel
</FodButton>
<FodButton OnClick="SendEmail"
StartIcon="@FodIcons.Material.Filled.Email"
Class="ms-2">
Trimite pe Email
</FodButton>
</div>
}
</FodCardContent>
</FodCard>
@code {
private DateTime? startDate = DateTime.Today.AddMonths(-1);
private DateTime? endDate = DateTime.Today;
private string selectedPeriod => $"{startDate:dd.MM.yyyy} - {endDate:dd.MM.yyyy}";
private FeedbackReportData reportData;
private async Task GenerateReport()
{
// Agregare date pentru toate serviciile
var allServices = await GetAllServices();
var feedbackTasks = allServices.Select(async service =>
{
var feedbacks = await FeedbackService.GetList(service.Code);
return new { Service = service, Feedbacks = feedbacks };
});
var results = await Task.WhenAll(feedbackTasks);
// Procesare și agregare
reportData = ProcessFeedbackData(results, startDate.Value, endDate.Value);
}
}
10. Monitorizare și alerting
public class FeedbackMonitoringService
{
private readonly IFeedbackComponentService _feedbackService;
private readonly INotificationService _notificationService;
private readonly ILogger<FeedbackMonitoringService> _logger;
public async Task MonitorFeedbackTrends(string serviceCode)
{
var currentStats = await _feedbackService.Get(serviceCode);
var previousStats = await GetPreviousPeriodStats(serviceCode);
// Detectează scăderi semnificative în rating
if (currentStats.AverageScore.HasValue &&
previousStats.AverageScore.HasValue)
{
var difference = previousStats.AverageScore.Value -
currentStats.AverageScore.Value;
if (difference > 0.5) // Scădere de peste 0.5 puncte
{
await _notificationService.SendAlertAsync(new Alert
{
Type = AlertType.Warning,
Title = "Scădere Rating Serviciu",
Message = $"Rating-ul pentru {currentStats.ServiceName} a scăzut " +
$"de la {previousStats.AverageScore:F1} la {currentStats.AverageScore:F1}",
ServiceCode = serviceCode,
Timestamp = DateTime.UtcNow
});
_logger.LogWarning(
"Rating scăzut detectat pentru serviciul {ServiceCode}: {OldRating} -> {NewRating}",
serviceCode,
previousStats.AverageScore,
currentStats.AverageScore);
}
}
// Monitorizare feedback negativ
var recentFeedbacks = await _feedbackService.GetList(serviceCode);
var negativeFeedbacks = recentFeedbacks
.Where(f => f.Score <= 2)
.Take(10)
.ToList();
if (negativeFeedbacks.Count >= 5)
{
await _notificationService.SendAlertAsync(new Alert
{
Type = AlertType.Error,
Title = "Multiple Feedback-uri Negative",
Message = $"{negativeFeedbacks.Count} feedback-uri negative recente pentru {currentStats.ServiceName}",
ServiceCode = serviceCode,
Timestamp = DateTime.UtcNow
});
}
}
}
11. Testare
[TestClass]
public class FeedbackComponentServiceTests
{
private Mock<HttpMessageHandler> _httpHandler;
private IFeedbackComponentService _service;
[TestMethod]
public async Task Add_ValidFeedback_ReturnsId()
{
// Arrange
var feedback = new FeedbackComponentModel
{
FirstName = "Ion",
LastName = "Popescu",
Email = "ion@example.com",
Score = 5,
Message = "Serviciu excelent!",
ServiceInternalCode = "SERVICE_001"
};
SetupHttpResponse(1); // Return feedback ID
// Act
var result = await _service.Add(feedback);
// Assert
Assert.AreEqual(1, result);
}
[TestMethod]
public async Task Get_ExistingService_ReturnsStats()
{
// Arrange
var serviceCode = "SERVICE_001";
var expectedStats = new GetFeedbackModel
{
AverageScore = 4.5,
TotalCount = 150,
ServiceName = "Eliberare Certificate"
};
SetupHttpResponse(expectedStats);
// Act
var result = await _service.Get(serviceCode);
// Assert
Assert.AreEqual(4.5, result.AverageScore);
Assert.AreEqual(150, result.TotalCount);
Assert.AreEqual("Eliberare Certificate", result.ServiceName);
}
[TestMethod]
[ExpectedException(typeof(ValidationException))]
public async Task Add_InvalidEmail_ThrowsException()
{
// Arrange
var feedback = new FeedbackComponentModel
{
Email = "invalid-email",
Score = 3
};
// Act
await _service.Add(feedback);
}
}
12. Best Practices
- Validare completă - Validați toate datele înainte de trimitere
- Feedback anonim - Permiteți feedback fără autentificare
- Limite rate - Preveniți spam prin limitare submisii
- Moderare - Verificați conținut inadecvat în mesaje
- Răspuns rapid - Confirmați primirea feedback-ului
- Analiză continuă - Monitorizați tendințe și alerte
- GDPR compliant - Respectați confidențialitatea datelor
13. Concluzie
FeedbackComponentService
oferă o soluție completă pentru colectarea și analiza feedback-ului utilizatorilor asupra serviciilor guvernamentale. Cu suport pentru rating, comentarii, statistici agregate și integrare cu portal extern, serviciul facilitează îmbunătățirea continuă a calității serviciilor publice prin ascultarea vocii cetățenilor.