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.
343 lines
11 KiB
C#
343 lines
11 KiB
C#
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; }
|
||
}
|