Files
GreenHomeBack/src/GreenHome.Application/Dtos/Admin/AdminDtos.cs
alireza e98b48bfba feat: device reset via RST token + Postgres code-first migrations
Device reset: Device.ResetRequested is a one-shot flag claimed atomically with a single UPDATE, so a queued reset is delivered exactly once. GET api/telemetry/AddData answers it with the bare token RST and nothing else; subsequent requests resume the normal routine. Adds POST api/admin/devices/{id}/request-reset (audited) plus an admin panel button, pending badge and confirm dialog.

Postgres migrations: PostgresGreenHomeDbContext owns Data/Migrations/Postgres while the SqlServer set stays untouched. Registers one context per provider with the base type forwarded, replaces EnsureCreated() with Migrate() for both, adds a complete InitialPostgres migration and an incremental AddDeviceResetRequested for SqlServer, and adds design-time factories so dotnet ef does not have to boot the API host.

Timestamps: Postgres DateTime columns are now timestamp with time zone, because Npgsql rejects Kind=Utc values on a naive timestamp column, which broke every write path (auth, settings, posts, checklists, alerts, audit logs). Removes the SpecifyKind(Unspecified) workarounds, fixes a 3:30 offset where already-UTC filters were passed through ToLocalTime(), and makes DataCopy migrate instead of EnsureCreated while passing timestamps through verbatim.

Tests: Test/GreenHome.SelfCheck applies the Postgres migrations and asserts that a queued reset is consumed exactly once and that timestamps round-trip as Utc.
2026-10-17 16:20:52 +03:30

343 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Domain = GreenHome.Domain;
namespace GreenHome.Application.Dtos.Admin;
// ==================== Dashboard ====================
public sealed class AdminDashboardDto
{
public int TotalUsers { get; set; }
public int TotalDevices { get; set; }
public int OnlineDevices { get; set; }
public int OfflineDevices { get; set; }
public int AlertsLast24h { get; set; }
public int SmsLast7Days { get; set; }
public int AiQueriesLast7Days { get; set; }
/// <summary>تعداد SMS بر اساس روز برای ۷ روز اخیر (sparkline)</summary>
public IReadOnlyList<UsageDayDto> SmsDailyLast7Days { get; set; } = [];
}
// ==================== Users ====================
public sealed class AdminUserFilter
{
public string? Search { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class AdminUserListItemDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Family { get; set; } = string.Empty;
public string Mobile { get; set; } = string.Empty;
public Domain.UserRole Role { get; set; }
public bool IsDisabled { get; set; }
public int DeviceCount { get; set; }
public DateTime? LastLoginAt { get; set; }
public DateTime CreatedAt { get; set; }
}
public sealed class AdminUserDetailDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Family { get; set; } = string.Empty;
public string Mobile { get; set; } = string.Empty;
public Domain.UserRole Role { get; set; }
public bool IsDisabled { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public IReadOnlyList<AdminDeviceListItemDto> Devices { get; set; } = [];
}
public sealed class ChangeUserRoleRequest
{
public Domain.UserRole Role { get; set; }
}
public sealed class ChangeUserStatusRequest
{
public bool IsEnabled { get; set; }
}
/// <summary>
/// نتیجه تغییر نقش/وضعیت کاربر توسط admin.
/// </summary>
public enum UserMutationOutcome
{
Ok,
NotFound,
ConflictSelf,
ConflictLastAdmin
}
// ==================== Devices ====================
public sealed class AdminDeviceFilter
{
public string? Search { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class AdminDeviceListItemDto
{
public int Id { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string OwnerName { get; set; } = string.Empty;
public string OwnerMobile { get; set; } = string.Empty;
public bool IsOnline { get; set; }
public DateTime? LastTelemetryAt { get; set; }
public decimal? TemperatureC { get; set; }
public decimal? HumidityPercent { get; set; }
}
public sealed class AdminDeviceDetailDto
{
public int Id { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string OwnerName { get; set; } = string.Empty;
public string OwnerFamily { get; set; } = string.Empty;
public string OwnerMobile { get; set; } = string.Empty;
public bool IsOnline { get; set; }
public int MemberCount { get; set; }
/// <summary>درخواست ریست ثبت شده و هنوز مصرف نشده است (RST در آپلود بعدی ارسال می‌شود)</summary>
public bool ResetRequested { get; set; }
public string? ProductType { get; set; }
public string? Province { get; set; }
public string? City { get; set; }
public string? DevicePhoneNumber { get; set; }
public int? UploadIntervalMin { get; set; }
public int? RelayCount { get; set; }
public string? NeshanLocation { get; set; }
public decimal? Latitude { get; set; }
public decimal? Longitude { get; set; }
public int? MinimumSmsIntervalMinutes { get; set; }
public int? MinimumCallIntervalMinutes { get; set; }
public decimal? AreaSquareMeters { get; set; }
public Domain.SimCardType? SimCardType { get; set; }
public string? SmsRecievers { get; set; }
public IReadOnlyList<TelemetryDto> LastTelemetry { get; set; } = [];
}
// ==================== Devices: create / settings / members ====================
public sealed class AdminDeviceCreateRequest
{
public int UserId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string? NeshanLocation { get; set; }
}
public enum AdminDeviceCreateOutcome
{
Ok,
UserNotFound,
ConflictDuplicateName,
Validation
}
public sealed class AdminDeviceCreateResult
{
public AdminDeviceCreateOutcome Outcome { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
/// <summary>پیام فارسی برای خطاهای Validation</summary>
public string Message { get; set; } = string.Empty;
}
public sealed class AdminDeviceSettingsUpdateRequest
{
public string DeviceName { get; set; } = string.Empty;
public string Province { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public decimal? Latitude { get; set; }
public decimal? Longitude { get; set; }
public string ProductType { get; set; } = string.Empty;
public int MinimumSmsIntervalMinutes { get; set; }
public int MinimumCallIntervalMinutes { get; set; }
public decimal? AreaSquareMeters { get; set; }
public int UploadIntervalMin { get; set; }
public string DevicePhoneNumber { get; set; } = string.Empty;
public Domain.SimCardType? SimCardType { get; set; }
public int RelayCount { get; set; }
public string SmsRecievers { get; set; } = string.Empty;
}
/// <summary>
/// Success = !NotFound && Message == null
/// </summary>
public sealed class AdminSettingsUpdateResult
{
public bool NotFound { get; set; }
public string? Message { get; set; }
}
public sealed class AdminDeviceMemberDto
{
public int UserId { get; set; }
public string Name { get; set; } = string.Empty;
public string Family { get; set; } = string.Empty;
public string Mobile { get; set; } = string.Empty;
public string MemberName { get; set; } = string.Empty;
public string MemberFamily { get; set; } = string.Empty;
public Domain.DeviceMemberRole Role { get; set; }
public bool ReceiveAlerts { get; set; }
public string? AvatarPath { get; set; }
public bool IsOwner { get; set; }
}
// ==================== Usage ====================
public sealed class UsageDayDto
{
public DateTime Date { get; set; }
public int SmsCount { get; set; }
public int VoiceCallCount { get; set; }
public int AiQueryCount { get; set; }
public int AiTokenCount { get; set; }
}
// ==================== Alerts ====================
public sealed class AdminAlertFilter
{
public int? DeviceId { get; set; }
public Domain.AlertStatus? Status { get; set; }
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class AdminAlertItemDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string OwnerName { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public Domain.AlertNotificationType NotificationType { get; set; }
public Domain.AlertStatus Status { get; set; }
public string? ErrorMessage { get; set; }
public DateTime SentAt { get; set; }
}
// ==================== Posts ====================
public sealed class AdminPostFilter
{
public int? DeviceId { get; set; }
public int? AuthorUserId { get; set; }
public Domain.DevicePostType? PostType { get; set; }
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class AdminPostItemDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int AuthorUserId { get; set; }
public string AuthorName { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public Domain.DevicePostType PostType { get; set; }
public int ImageCount { get; set; }
public bool HasVoice { get; set; }
public int? VoiceDurationSeconds { get; set; }
public DateTime CreatedAt { get; set; }
}
public sealed class AdminPostDetailDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public string AuthorFamily { get; set; } = string.Empty;
public Domain.DevicePostType PostType { get; set; }
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public IReadOnlyList<AdminPostImageDto> Images { get; set; } = [];
public string? VoiceFilePath { get; set; }
public int? VoiceDurationSeconds { get; set; }
}
public sealed class AdminPostImageDto
{
public int Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
}
// ==================== Device Debug ====================
public sealed class AdminDeviceTestResultDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
// ==================== Reports ====================
public sealed class AdminUserGrowthRowDto
{
public DateOnly Date { get; set; }
public int NewUsers { get; set; }
public int ActiveUsers { get; set; }
}
public sealed class AdminDeviceUptimeRowDto
{
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string OwnerName { get; set; } = string.Empty;
public int TotalDays { get; set; }
public int OnlineDays { get; set; }
public int OnlinePercent { get; set; }
}
public sealed class AdminUsageByUserRowDto
{
public int UserId { get; set; }
public string Name { get; set; } = string.Empty;
public string Family { get; set; } = string.Empty;
public string Mobile { get; set; } = string.Empty;
public int Sms { get; set; }
public int VoiceCall { get; set; }
public int AiQueries { get; set; }
public int AiTokens { get; set; }
}
// ==================== Audit Log ====================
public sealed class AdminAuditLogFilter
{
public int? AdminId { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class AdminAuditLogDto
{
public int Id { get; set; }
public int AdminUserId { get; set; }
public string AdminName { get; set; } = string.Empty;
public string Action { get; set; } = string.Empty;
public string TargetId { get; set; } = string.Empty;
public string Details { get; set; } = string.Empty;
public DateTime At { get; set; }
}