diff --git a/src/GreenHome.Api/Controllers/DeviceMembersController.cs b/src/GreenHome.Api/Controllers/DeviceMembersController.cs new file mode 100644 index 0000000..e6a5a69 --- /dev/null +++ b/src/GreenHome.Api/Controllers/DeviceMembersController.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class DeviceMembersController : ControllerBase +{ + private readonly IDeviceMemberService _memberService; + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public DeviceMembersController( + IDeviceMemberService memberService, + IWebHostEnvironment environment, + ILogger logger) + { + _memberService = memberService; + _environment = environment; + _logger = logger; + } + + /// لیست اعضای تیم یک دستگاه + [HttpGet] + public async Task>> List( + [FromQuery] int deviceId, + CancellationToken cancellationToken) + { + var result = await _memberService.ListByDeviceAsync(deviceId, cancellationToken); + return Ok(result); + } + + /// افزودن یا ویرایش عضو تیم + [HttpPost] + public async Task> Upsert( + [FromBody] UpsertDeviceMemberRequest request, + CancellationToken cancellationToken) + { + try + { + var result = await _memberService.UpsertAsync(request, cancellationToken); + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error upserting device member"); + return BadRequest(new { error = ex.Message }); + } + } + + /// حذف عضو تیم + [HttpDelete] + public async Task Delete( + [FromQuery] int deviceId, + [FromQuery] int userId, + CancellationToken cancellationToken) + { + try + { + await _memberService.DeleteAsync(deviceId, userId, cancellationToken); + return Ok(new { message = "عضو با موفقیت حذف شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// بررسی مجوز مدیریت تیم + [HttpGet("can-manage")] + public async Task> CanManage( + [FromQuery] int actorUserId, + [FromQuery] int deviceId, + CancellationToken cancellationToken) + { + var canManage = await _memberService.CanManageDeviceAsync(actorUserId, deviceId, cancellationToken); + return Ok(new { canManage }); + } + + /// آپلود آواتار عضو + [HttpPost("avatar")] + [Consumes("multipart/form-data")] + public async Task> UploadAvatar( + [FromForm] IFormFile file, + CancellationToken cancellationToken) + { + try + { + if (file == null || file.Length == 0) + return BadRequest(new { error = "فایل انتخاب نشده است" }); + + var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/webp" }; + if (!allowedTypes.Contains(file.ContentType.ToLower())) + return BadRequest(new { error = "فقط فایل‌های تصویری مجاز هستند" }); + + if (file.Length > 2 * 1024 * 1024) + return BadRequest(new { error = "حجم آواتار نباید بیشتر از 2 مگابایت باشد" }); + + var uploadsFolder = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "avatars"); + Directory.CreateDirectory(uploadsFolder); + + var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(uploadsFolder, fileName); + + await using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream, cancellationToken); + } + + var relativePath = $"/uploads/avatars/{fileName}"; + return Ok(new { avatarPath = relativePath, message = "آواتار با موفقیت آپلود شد" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error uploading avatar"); + return StatusCode(500, new { error = "خطا در آپلود آواتار" }); + } + } +} diff --git a/src/GreenHome.Api/Controllers/DevicePostsController.cs b/src/GreenHome.Api/Controllers/DevicePostsController.cs index 8de3e35..1650039 100644 --- a/src/GreenHome.Api/Controllers/DevicePostsController.cs +++ b/src/GreenHome.Api/Controllers/DevicePostsController.cs @@ -208,5 +208,85 @@ public class DevicePostsController : ControllerBase var hasAccess = await _postService.CanUserAccessDeviceAsync(userId, deviceId, cancellationToken); return Ok(new { hasAccess }); } + + /// + /// ایجاد پست اسنپ‌شات وضعیت سنسورها و رله‌ها + /// + [HttpPost("sensor-snapshot")] + public async Task> CreateSensorSnapshot( + CreateSensorSnapshotRequest request, + CancellationToken cancellationToken) + { + try + { + var id = await _postService.CreateSensorSnapshotAsync(request, cancellationToken); + return Ok(new { id, message = "وضعیت سنسورها و رله‌ها ارسال شد" }); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(new { error = ex.Message }); + } + } + + /// + /// آپلود فایل صوتی (ویس) برای پست + /// + [HttpPost("{postId}/voice")] + [Consumes("multipart/form-data")] + public async Task UploadVoice( + int postId, + [FromForm] IFormFile file, + [FromForm] int? durationSeconds, + CancellationToken cancellationToken) + { + try + { + if (file == null || file.Length == 0) + return BadRequest(new { error = "فایل انتخاب نشده است" }); + + var allowedTypes = new[] + { + "audio/webm", "audio/ogg", "audio/mp4", "audio/mpeg", + "audio/wav", "audio/x-wav", "audio/amr", "audio/aac", "audio/m4a" + }; + if (!allowedTypes.Contains(file.ContentType.ToLower())) + return BadRequest(new { error = "فرمت فایل صوتی مجاز نیست" }); + + if (file.Length > 10 * 1024 * 1024) + return BadRequest(new { error = "حجم فایل صوتی نباید بیشتر از 10 مگابایت باشد" }); + + var uploadsFolder = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "voice"); + Directory.CreateDirectory(uploadsFolder); + + var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(uploadsFolder, fileName); + + await using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream, cancellationToken); + } + + var relativePath = $"/uploads/voice/{fileName}"; + await _postService.SetPostVoiceAsync( + postId, + file.FileName, + relativePath, + file.ContentType, + file.Length, + durationSeconds, + cancellationToken); + + return Ok(new { filePath = relativePath, message = "ویس با موفقیت آپلود شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error uploading voice for post {PostId}", postId); + return StatusCode(500, new { error = "خطا در آپلود ویس" }); + } + } } diff --git a/src/GreenHome.Api/wwwroot/uploads/avatars/1ffe0446-dd82-4b16-92d5-e1f25964c018.png b/src/GreenHome.Api/wwwroot/uploads/avatars/1ffe0446-dd82-4b16-92d5-e1f25964c018.png new file mode 100644 index 0000000..0bfe95b Binary files /dev/null and b/src/GreenHome.Api/wwwroot/uploads/avatars/1ffe0446-dd82-4b16-92d5-e1f25964c018.png differ diff --git a/src/GreenHome.Api/wwwroot/uploads/posts/42a6f162-af0a-4da6-a8bb-3d435bb84579_photo_2026-08-26_19-29-21.jpg b/src/GreenHome.Api/wwwroot/uploads/posts/42a6f162-af0a-4da6-a8bb-3d435bb84579_photo_2026-08-26_19-29-21.jpg new file mode 100644 index 0000000..73fd37f Binary files /dev/null and b/src/GreenHome.Api/wwwroot/uploads/posts/42a6f162-af0a-4da6-a8bb-3d435bb84579_photo_2026-08-26_19-29-21.jpg differ diff --git a/src/GreenHome.Api/wwwroot/uploads/posts/9918d5dd-65ac-4207-9637-4044abead7b4_photo_2026-08-26_19-29-21.jpg b/src/GreenHome.Api/wwwroot/uploads/posts/9918d5dd-65ac-4207-9637-4044abead7b4_photo_2026-08-26_19-29-21.jpg new file mode 100644 index 0000000..73fd37f Binary files /dev/null and b/src/GreenHome.Api/wwwroot/uploads/posts/9918d5dd-65ac-4207-9637-4044abead7b4_photo_2026-08-26_19-29-21.jpg differ diff --git a/src/GreenHome.Application/Dtos/Device/DeviceMemberDto.cs b/src/GreenHome.Application/Dtos/Device/DeviceMemberDto.cs new file mode 100644 index 0000000..3a3d19c --- /dev/null +++ b/src/GreenHome.Application/Dtos/Device/DeviceMemberDto.cs @@ -0,0 +1,26 @@ +using GreenHome.Domain; + +namespace GreenHome.Application; + +public sealed class DeviceMemberDto +{ + public int DeviceId { get; set; } + 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 DeviceMemberRole Role { get; set; } = DeviceMemberRole.Worker; + public string? AvatarPath { get; set; } + public bool ReceiveAlerts { get; set; } = true; +} + +public sealed class UpsertDeviceMemberRequest +{ + public required int DeviceId { get; set; } + public required string Mobile { get; set; } + public string Name { get; set; } = string.Empty; + public string Family { get; set; } = string.Empty; + public DeviceMemberRole Role { get; set; } = DeviceMemberRole.Worker; + public bool ReceiveAlerts { get; set; } = true; + public string? AvatarPath { get; set; } +} diff --git a/src/GreenHome.Application/Dtos/DevicePost/DevicePostDto.cs b/src/GreenHome.Application/Dtos/DevicePost/DevicePostDto.cs index 922ae42..122d43c 100644 --- a/src/GreenHome.Application/Dtos/DevicePost/DevicePostDto.cs +++ b/src/GreenHome.Application/Dtos/DevicePost/DevicePostDto.cs @@ -7,7 +7,12 @@ public sealed class DevicePostDto public int AuthorUserId { get; set; } public string AuthorName { get; set; } = string.Empty; public string AuthorFamily { get; set; } = string.Empty; + public string? AuthorAvatar { get; set; } + public Domain.DeviceMemberRole? AuthorRole { get; set; } public string Content { get; set; } = string.Empty; + public Domain.DevicePostType PostType { get; set; } = Domain.DevicePostType.Text; + public string? VoiceFilePath { get; set; } + public int? VoiceDurationSeconds { get; set; } public List Images { get; set; } = new(); public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } @@ -43,3 +48,9 @@ public sealed class DevicePostFilter public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } + +public sealed class CreateSensorSnapshotRequest +{ + public required int DeviceId { get; set; } + public required int AuthorUserId { get; set; } +} diff --git a/src/GreenHome.Application/Interfaces/IDeviceMemberService.cs b/src/GreenHome.Application/Interfaces/IDeviceMemberService.cs new file mode 100644 index 0000000..e724cb6 --- /dev/null +++ b/src/GreenHome.Application/Interfaces/IDeviceMemberService.cs @@ -0,0 +1,9 @@ +namespace GreenHome.Application; + +public interface IDeviceMemberService +{ + Task> ListByDeviceAsync(int deviceId, CancellationToken cancellationToken); + Task UpsertAsync(UpsertDeviceMemberRequest request, CancellationToken cancellationToken); + Task DeleteAsync(int deviceId, int userId, CancellationToken cancellationToken); + Task CanManageDeviceAsync(int actorUserId, int deviceId, CancellationToken cancellationToken); +} diff --git a/src/GreenHome.Application/Interfaces/IDevicePostService.cs b/src/GreenHome.Application/Interfaces/IDevicePostService.cs index f79320c..052c9e1 100644 --- a/src/GreenHome.Application/Interfaces/IDevicePostService.cs +++ b/src/GreenHome.Application/Interfaces/IDevicePostService.cs @@ -10,4 +10,6 @@ public interface IDevicePostService Task AddImageToPostAsync(int postId, string fileName, string filePath, string contentType, long fileSize, CancellationToken cancellationToken); Task DeleteImageAsync(int imageId, CancellationToken cancellationToken); Task CanUserAccessDeviceAsync(int userId, int deviceId, CancellationToken cancellationToken); + Task CreateSensorSnapshotAsync(CreateSensorSnapshotRequest request, CancellationToken cancellationToken); + Task SetPostVoiceAsync(int postId, string fileName, string filePath, string contentType, long fileSize, int? durationSeconds, CancellationToken cancellationToken); } diff --git a/src/GreenHome.Application/MappingProfile.cs b/src/GreenHome.Application/MappingProfile.cs index e87ed71..c292cac 100644 --- a/src/GreenHome.Application/MappingProfile.cs +++ b/src/GreenHome.Application/MappingProfile.cs @@ -77,5 +77,10 @@ public sealed class MappingProfile : Profile .ForMember(dest => dest.AuthorFamily, opt => opt.MapFrom(src => src.AuthorUser.Family)); CreateMap(); + + CreateMap() + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.Family, opt => opt.MapFrom(src => src.Family)) + .ForMember(dest => dest.Mobile, opt => opt.MapFrom(src => src.User.Mobile)); } } diff --git a/src/GreenHome.Domain/DeviceMemberRole.cs b/src/GreenHome.Domain/DeviceMemberRole.cs new file mode 100644 index 0000000..0292ad9 --- /dev/null +++ b/src/GreenHome.Domain/DeviceMemberRole.cs @@ -0,0 +1,22 @@ +namespace GreenHome.Domain; + +/// +/// نقش یک عضو در تیم گلخانه (دستگاه) +/// +public enum DeviceMemberRole +{ + /// مدیر + Manager = 0, + + /// ناظر + Supervisor = 1, + + /// مهندس آب و خاک + WaterSoilEngineer = 2, + + /// تکنسین + Technician = 3, + + /// کارگر + Worker = 4 +} diff --git a/src/GreenHome.Domain/DevicePost.cs b/src/GreenHome.Domain/DevicePost.cs index 535b025..119f1de 100644 --- a/src/GreenHome.Domain/DevicePost.cs +++ b/src/GreenHome.Domain/DevicePost.cs @@ -1,5 +1,23 @@ namespace GreenHome.Domain; +/// +/// نوع پیام در گروه مجازی دستگاه +/// +public enum DevicePostType +{ + /// متن + Text = 0, + + /// تصویر + Image = 1, + + /// ویس + Voice = 2, + + /// اسنپ‌شات وضعیت سنسورها و رله‌ها + SensorSnapshot = 3 +} + /// /// پست‌های گروه مجازی دستگاه (تایم‌لاین مشترک) /// @@ -23,6 +41,21 @@ public sealed class DevicePost /// متن پست /// public string Content { get; set; } = string.Empty; + + /// + /// نوع پیام + /// + public DevicePostType PostType { get; set; } = DevicePostType.Text; + + /// + /// مسیر نسبی فایل صوتی (برای پیام ویس) + /// + public string? VoiceFilePath { get; set; } + + /// + /// مدت‌زمان ویس (ثانیه) + /// + public int? VoiceDurationSeconds { get; set; } /// /// تصاویر پیوست diff --git a/src/GreenHome.Domain/DeviceSettings.cs b/src/GreenHome.Domain/DeviceSettings.cs index 68d8f94..9887c7c 100644 --- a/src/GreenHome.Domain/DeviceSettings.cs +++ b/src/GreenHome.Domain/DeviceSettings.cs @@ -89,6 +89,8 @@ public sealed class DeviceSettings /// public int RelayCount { get; set; } = 4; + // ponytail: legacy field kept only to avoid a destructive column drop. + // Recipients now come from the team (DeviceUsers.ReceiveAlerts) - see AlertService.BuildRecipients. [StringLength(120)] public string SmsRecievers { get; set; } = string.Empty; diff --git a/src/GreenHome.Domain/DeviceUser.cs b/src/GreenHome.Domain/DeviceUser.cs index 22f34f0..096ff2f 100644 --- a/src/GreenHome.Domain/DeviceUser.cs +++ b/src/GreenHome.Domain/DeviceUser.cs @@ -11,5 +11,25 @@ public sealed class DeviceUser /// آیا این کاربر باید هشدارهای این دستگاه را دریافت کند؟ /// public bool ReceiveAlerts { get; set; } = true; + + /// + /// نقش عضو در تیم گلخانه (مدیر، ناظر، مهندس آب و خاک، تکنسین، ...) + /// + public DeviceMemberRole Role { get; set; } = DeviceMemberRole.Worker; + + /// + /// نام عضو در همین گلخانه (می‌تواند در هر گلخانه متفاوت باشد) + /// + public string Name { get; set; } = string.Empty; + + /// + /// نام خانوادگی عضو در همین گلخانه + /// + public string Family { get; set; } = string.Empty; + + /// + /// مسیر نسبی آواتار عضو (ذخیره‌شده در uploads/avatars) + /// + public string? AvatarPath { get; set; } } diff --git a/src/GreenHome.Domain/MobileNormalizer.cs b/src/GreenHome.Domain/MobileNormalizer.cs new file mode 100644 index 0000000..631406f --- /dev/null +++ b/src/GreenHome.Domain/MobileNormalizer.cs @@ -0,0 +1,30 @@ +namespace GreenHome.Domain; + +/// +/// نرمال‌سازی یکپارچه شماره موبایل به فرمت «۰۹xxxxxxxxx». +/// تنها منبع یکسان‌سازی و اعتبارسنجی شماره موبایل در کل سیستم. +/// +public static class MobileNormalizer +{ + /// + /// شماره را به فرمت یکپارچه «۰۹xxxxxxxxx» برمی‌گرداند. + /// اگر شماره معتبر نباشد، رشته خالی برمی‌گرداند. + /// + public static string Normalize(string? mobile) + { + if (string.IsNullOrWhiteSpace(mobile)) + return string.Empty; + + var digits = new string(mobile.Where(char.IsDigit).ToArray()); + + digits = digits.Length switch + { + 10 when digits[0] == '9' => "0" + digits, + 12 when digits.StartsWith("98") => "0" + digits.Substring(2), + 14 when digits.StartsWith("0098") => "0" + digits.Substring(4), + _ => digits + }; + + return digits.Length == 11 && digits.StartsWith("09") ? digits : string.Empty; + } +} diff --git a/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs b/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs index 5148ac4..d40e435 100644 --- a/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs +++ b/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs @@ -167,6 +167,10 @@ public sealed class GreenHomeDbContext : DbContext b.ToTable("DeviceUsers"); b.HasKey(x => new { x.DeviceId, x.UserId }); b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true); + b.Property(x => x.Role).IsRequired().HasConversion(); + b.Property(x => x.Name).IsRequired().HasMaxLength(100); + b.Property(x => x.Family).IsRequired().HasMaxLength(100); + b.Property(x => x.AvatarPath).HasMaxLength(500); b.HasOne(x => x.Device) .WithMany(d => d.DeviceUsers) .HasForeignKey(x => x.DeviceId) @@ -383,6 +387,8 @@ public sealed class GreenHomeDbContext : DbContext b.ToTable("DevicePosts"); b.HasKey(x => x.Id); b.Property(x => x.Content).IsRequired().HasMaxLength(5000); + b.Property(x => x.PostType).IsRequired().HasConversion(); + b.Property(x => x.VoiceFilePath).HasMaxLength(500); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.Designer.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.Designer.cs new file mode 100644 index 0000000..2d2e930 --- /dev/null +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.Designer.cs @@ -0,0 +1,1441 @@ +// +using System; +using GreenHome.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GreenHome.Infrastructure.Data.Migrations.Migrations +{ + [DbContext(typeof(GreenHomeDbContext))] + [Migration("20260910115241_AddTeamAndPostTypes")] + partial class AddTeamAndPostTypes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GreenHome.Domain.AIQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompletionTokens") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PromptTokens") + .HasColumnType("int"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseTimeMs") + .HasColumnType("bigint"); + + b.Property("Temperature") + .HasColumnType("float"); + + b.Property("TotalTokens") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AIQueries", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CallCooldownMinutes") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("SmsCooldownMinutes") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("AlertConditions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("AlertType") + .HasColumnType("int"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProcessingTimeMs") + .HasColumnType("bigint"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.HasIndex("AlertType"); + + b.HasIndex("Status"); + + b.HasIndex("DeviceId", "SentAt"); + + b.HasIndex("UserId", "SentAt"); + + b.ToTable("AlertLogs", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsSent") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MessageOutboxIds") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt"); + + b.ToTable("AlertNotifications", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.ToTable("AlertRules", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("DeviceId", "IsActive"); + + b.ToTable("Checklists", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistId") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedByUserId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("CompletedByUserId"); + + b.HasIndex("ChecklistId", "PersianDate"); + + b.ToTable("ChecklistCompletions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsRequired") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId", "Order"); + + b.ToTable("ChecklistItems", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistCompletionId") + .HasColumnType("int"); + + b.Property("ChecklistItemId") + .HasColumnType("int"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistCompletionId"); + + b.HasIndex("ChecklistItemId"); + + b.ToTable("ChecklistItemCompletions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DailyReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Analysis") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompletionTokens") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianDay") + .HasColumnType("int"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("PromptTokens") + .HasColumnType("int"); + + b.Property("RecordCount") + .HasColumnType("int"); + + b.Property("ResponseTimeMs") + .HasColumnType("bigint"); + + b.Property("SampledRecordCount") + .HasColumnType("int"); + + b.Property("TotalTokens") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DeviceId", "PersianDate") + .IsUnique(); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("DailyReports", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("NeshanLocation") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Devices", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("PostType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("VoiceDurationSeconds") + .HasColumnType("int"); + + b.Property("VoiceFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("DeviceId", "CreatedAt"); + + b.ToTable("DevicePosts", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DevicePostId") + .HasColumnType("int"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DevicePostId"); + + b.ToTable("DevicePostImages", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceSettingsId") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RelayIndex") + .HasColumnType("int"); + + b.Property("RelayKind") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeviceSettingsId", "RelayIndex") + .IsUnique(); + + b.ToTable("DeviceRelays", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AreaSquareMeters") + .HasColumnType("decimal(18,2)"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("DeviceId1") + .HasColumnType("int"); + + b.Property("DevicePhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Latitude") + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasColumnType("decimal(9,6)"); + + b.Property("MinimumCallIntervalMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(60); + + b.Property("MinimumSmsIntervalMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(15); + + b.Property("ProductType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Province") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RelayCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(4); + + b.Property("SimCardType") + .HasColumnType("int"); + + b.Property("SmsRecievers") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("TokenCode") + .HasColumnType("nvarchar(max)"); + + b.Property("TokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UploadIntervalMin") + .HasColumnType("int"); + + b.Property("VerificationCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("DeviceId1"); + + b.ToTable("DeviceSettings", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("AvatarPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReceiveAlerts") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("DeviceId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("DeviceUsers", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.PowerOutageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CurrentPower") + .HasColumnType("tinyint"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("PreviousPower") + .HasColumnType("tinyint"); + + b.Property("ServerTimestampUtc") + .HasColumnType("datetime2"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("DeviceId", "TimestampUtc"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("PowerOutageLogs", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("DeviceRelayId") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceRelayId"); + + b.ToTable("RelayRules", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("datetime2"); + + b.Property("UserDailyReportId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserDailyReportId"); + + b.ToTable("ReportImages", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BatteryPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("BatteryVoltage") + .HasColumnType("decimal(18,2)"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("GasPPM") + .HasColumnType("int"); + + b.Property("HumidityPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Lux") + .HasColumnType("decimal(18,2)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("Power") + .HasColumnType("tinyint"); + + b.Property("RelayStates") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ServerTimestampUtc") + .HasColumnType("datetime2"); + + b.Property("SoilPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("TemperatureC") + .HasColumnType("decimal(18,2)"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.Property("Voltage") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId", "ServerTimestampUtc"); + + b.HasIndex("DeviceId", "TimestampUtc"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("Telemetry", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Family") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Mobile") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Observations") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Operations") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianDay") + .HasColumnType("int"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "PersianDate"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("UserDailyReports", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("nvarchar(4)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("UsedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Mobile", "Code", "IsUsed"); + + b.ToTable("VerificationCodes", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AIQuery", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertLog", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany() + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AlertCondition"); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany() + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AlertCondition"); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertRule", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany("Rules") + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AlertCondition"); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.HasOne("GreenHome.Domain.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.HasOne("GreenHome.Domain.Checklist", "Checklist") + .WithMany("Completions") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "CompletedByUser") + .WithMany() + .HasForeignKey("CompletedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("CompletedByUser"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b => + { + b.HasOne("GreenHome.Domain.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b => + { + b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion") + .WithMany("ItemCompletions") + .HasForeignKey("ChecklistCompletionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem") + .WithMany() + .HasForeignKey("ChecklistItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ChecklistCompletion"); + + b.Navigation("ChecklistItem"); + }); + + modelBuilder.Entity("GreenHome.Domain.DailyReport", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.HasOne("GreenHome.Domain.User", "AuthorUser") + .WithMany() + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AuthorUser"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b => + { + b.HasOne("GreenHome.Domain.DevicePost", "DevicePost") + .WithMany("Images") + .HasForeignKey("DevicePostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DevicePost"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.HasOne("GreenHome.Domain.DeviceSettings", "DeviceSettings") + .WithMany("Relays") + .HasForeignKey("DeviceSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceSettings"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", null) + .WithMany("DeviceSettings") + .HasForeignKey("DeviceId1"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany("DeviceUsers") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany("DeviceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.PowerOutageLog", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.HasOne("GreenHome.Domain.DeviceRelay", "DeviceRelay") + .WithMany("Rules") + .HasForeignKey("DeviceRelayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceRelay"); + }); + + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => + { + b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport") + .WithMany("Images") + .HasForeignKey("UserDailyReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserDailyReport"); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.Navigation("Completions"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.Navigation("ItemCompletions"); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.Navigation("DeviceSettings"); + + b.Navigation("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Navigation("Images"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Navigation("Relays"); + }); + + modelBuilder.Entity("GreenHome.Domain.User", b => + { + b.Navigation("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.Navigation("Images"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.cs new file mode 100644 index 0000000..4f82b9d --- /dev/null +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GreenHome.Infrastructure.Data.Migrations.Migrations +{ + /// + public partial class AddTeamAndPostTypes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AvatarPath", + table: "DeviceUsers", + type: "nvarchar(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "Role", + table: "DeviceUsers", + type: "int", + nullable: false, + defaultValue: 4); + + migrationBuilder.AddColumn( + name: "PostType", + table: "DevicePosts", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "VoiceDurationSeconds", + table: "DevicePosts", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "VoiceFilePath", + table: "DevicePosts", + type: "nvarchar(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AvatarPath", + table: "DeviceUsers"); + + migrationBuilder.DropColumn( + name: "Role", + table: "DeviceUsers"); + + migrationBuilder.DropColumn( + name: "PostType", + table: "DevicePosts"); + + migrationBuilder.DropColumn( + name: "VoiceDurationSeconds", + table: "DevicePosts"); + + migrationBuilder.DropColumn( + name: "VoiceFilePath", + table: "DevicePosts"); + } + } +} diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.Designer.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.Designer.cs new file mode 100644 index 0000000..8545480 --- /dev/null +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.Designer.cs @@ -0,0 +1,1451 @@ +// +using System; +using GreenHome.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GreenHome.Infrastructure.Data.Migrations.Migrations +{ + [DbContext(typeof(GreenHomeDbContext))] + [Migration("20260910151556_MoveMemberNamesToDeviceUser")] + partial class MoveMemberNamesToDeviceUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GreenHome.Domain.AIQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompletionTokens") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PromptTokens") + .HasColumnType("int"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseTimeMs") + .HasColumnType("bigint"); + + b.Property("Temperature") + .HasColumnType("float"); + + b.Property("TotalTokens") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AIQueries", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CallCooldownMinutes") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("SmsCooldownMinutes") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("AlertConditions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("AlertType") + .HasColumnType("int"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProcessingTimeMs") + .HasColumnType("bigint"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.HasIndex("AlertType"); + + b.HasIndex("Status"); + + b.HasIndex("DeviceId", "SentAt"); + + b.HasIndex("UserId", "SentAt"); + + b.ToTable("AlertLogs", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsSent") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MessageOutboxIds") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt"); + + b.ToTable("AlertNotifications", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.ToTable("AlertRules", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("DeviceId", "IsActive"); + + b.ToTable("Checklists", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistId") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedByUserId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("CompletedByUserId"); + + b.HasIndex("ChecklistId", "PersianDate"); + + b.ToTable("ChecklistCompletions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsRequired") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId", "Order"); + + b.ToTable("ChecklistItems", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistCompletionId") + .HasColumnType("int"); + + b.Property("ChecklistItemId") + .HasColumnType("int"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistCompletionId"); + + b.HasIndex("ChecklistItemId"); + + b.ToTable("ChecklistItemCompletions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DailyReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Analysis") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompletionTokens") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianDay") + .HasColumnType("int"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("PromptTokens") + .HasColumnType("int"); + + b.Property("RecordCount") + .HasColumnType("int"); + + b.Property("ResponseTimeMs") + .HasColumnType("bigint"); + + b.Property("SampledRecordCount") + .HasColumnType("int"); + + b.Property("TotalTokens") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DeviceId", "PersianDate") + .IsUnique(); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("DailyReports", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("NeshanLocation") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Devices", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("PostType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("VoiceDurationSeconds") + .HasColumnType("int"); + + b.Property("VoiceFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("DeviceId", "CreatedAt"); + + b.ToTable("DevicePosts", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DevicePostId") + .HasColumnType("int"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DevicePostId"); + + b.ToTable("DevicePostImages", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceSettingsId") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RelayIndex") + .HasColumnType("int"); + + b.Property("RelayKind") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeviceSettingsId", "RelayIndex") + .IsUnique(); + + b.ToTable("DeviceRelays", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AreaSquareMeters") + .HasColumnType("decimal(18,2)"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("DeviceId1") + .HasColumnType("int"); + + b.Property("DevicePhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Latitude") + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasColumnType("decimal(9,6)"); + + b.Property("MinimumCallIntervalMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(60); + + b.Property("MinimumSmsIntervalMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(15); + + b.Property("ProductType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Province") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RelayCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(4); + + b.Property("SimCardType") + .HasColumnType("int"); + + b.Property("SmsRecievers") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("TokenCode") + .HasColumnType("nvarchar(max)"); + + b.Property("TokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UploadIntervalMin") + .HasColumnType("int"); + + b.Property("VerificationCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("DeviceId1"); + + b.ToTable("DeviceSettings", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("AvatarPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Family") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ReceiveAlerts") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("DeviceId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("DeviceUsers", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.PowerOutageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CurrentPower") + .HasColumnType("tinyint"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("PreviousPower") + .HasColumnType("tinyint"); + + b.Property("ServerTimestampUtc") + .HasColumnType("datetime2"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("DeviceId", "TimestampUtc"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("PowerOutageLogs", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("DeviceRelayId") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceRelayId"); + + b.ToTable("RelayRules", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("datetime2"); + + b.Property("UserDailyReportId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserDailyReportId"); + + b.ToTable("ReportImages", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BatteryPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("BatteryVoltage") + .HasColumnType("decimal(18,2)"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("GasPPM") + .HasColumnType("int"); + + b.Property("HumidityPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Lux") + .HasColumnType("decimal(18,2)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("Power") + .HasColumnType("tinyint"); + + b.Property("RelayStates") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ServerTimestampUtc") + .HasColumnType("datetime2"); + + b.Property("SoilPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("TemperatureC") + .HasColumnType("decimal(18,2)"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.Property("Voltage") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId", "ServerTimestampUtc"); + + b.HasIndex("DeviceId", "TimestampUtc"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("Telemetry", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Family") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Mobile") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Observations") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Operations") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianDay") + .HasColumnType("int"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "PersianDate"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("UserDailyReports", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("nvarchar(4)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("UsedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Mobile", "Code", "IsUsed"); + + b.ToTable("VerificationCodes", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AIQuery", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertLog", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany() + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AlertCondition"); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany() + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AlertCondition"); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertRule", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany("Rules") + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AlertCondition"); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.HasOne("GreenHome.Domain.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.HasOne("GreenHome.Domain.Checklist", "Checklist") + .WithMany("Completions") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "CompletedByUser") + .WithMany() + .HasForeignKey("CompletedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("CompletedByUser"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b => + { + b.HasOne("GreenHome.Domain.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b => + { + b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion") + .WithMany("ItemCompletions") + .HasForeignKey("ChecklistCompletionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem") + .WithMany() + .HasForeignKey("ChecklistItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ChecklistCompletion"); + + b.Navigation("ChecklistItem"); + }); + + modelBuilder.Entity("GreenHome.Domain.DailyReport", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.HasOne("GreenHome.Domain.User", "AuthorUser") + .WithMany() + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AuthorUser"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b => + { + b.HasOne("GreenHome.Domain.DevicePost", "DevicePost") + .WithMany("Images") + .HasForeignKey("DevicePostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DevicePost"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.HasOne("GreenHome.Domain.DeviceSettings", "DeviceSettings") + .WithMany("Relays") + .HasForeignKey("DeviceSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceSettings"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", null) + .WithMany("DeviceSettings") + .HasForeignKey("DeviceId1"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany("DeviceUsers") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany("DeviceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.PowerOutageLog", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.HasOne("GreenHome.Domain.DeviceRelay", "DeviceRelay") + .WithMany("Rules") + .HasForeignKey("DeviceRelayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceRelay"); + }); + + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => + { + b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport") + .WithMany("Images") + .HasForeignKey("UserDailyReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserDailyReport"); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.Navigation("Completions"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.Navigation("ItemCompletions"); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.Navigation("DeviceSettings"); + + b.Navigation("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Navigation("Images"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Navigation("Relays"); + }); + + modelBuilder.Entity("GreenHome.Domain.User", b => + { + b.Navigation("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.Navigation("Images"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.cs new file mode 100644 index 0000000..c76eca2 --- /dev/null +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GreenHome.Infrastructure.Data.Migrations.Migrations +{ + /// + public partial class MoveMemberNamesToDeviceUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Family", + table: "DeviceUsers", + type: "nvarchar(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Name", + table: "DeviceUsers", + type: "nvarchar(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + // بک‌فیل نام/نام‌خانوادگی اعضای موجود از جدول Users + migrationBuilder.Sql( + "UPDATE du SET du.Name = COALESCE(NULLIF(u.Name, ''), u.Mobile), du.Family = u.Family " + + "FROM DeviceUsers du INNER JOIN Users u ON u.Id = du.UserId;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Family", + table: "DeviceUsers"); + + migrationBuilder.DropColumn( + name: "Name", + table: "DeviceUsers"); + } + } +} diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs index cd73bd5..8ec001d 100644 --- a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs @@ -517,9 +517,19 @@ namespace GreenHome.Infrastructure.Data.Migrations b.Property("DeviceId") .HasColumnType("int"); + b.Property("PostType") + .HasColumnType("int"); + b.Property("UpdatedAt") .HasColumnType("datetime2"); + b.Property("VoiceDurationSeconds") + .HasColumnType("int"); + + b.Property("VoiceFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + b.HasKey("Id"); b.HasIndex("AuthorUserId"); @@ -708,11 +718,28 @@ namespace GreenHome.Infrastructure.Data.Migrations b.Property("UserId") .HasColumnType("int"); + b.Property("AvatarPath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Family") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + b.Property("ReceiveAlerts") .ValueGeneratedOnAdd() .HasColumnType("bit") .HasDefaultValue(true); + b.Property("Role") + .HasColumnType("int"); + b.HasKey("DeviceId", "UserId"); b.HasIndex("UserId"); diff --git a/src/GreenHome.Infrastructure/DependencyInjection.cs b/src/GreenHome.Infrastructure/DependencyInjection.cs index d2f03bd..ee774f0 100644 --- a/src/GreenHome.Infrastructure/DependencyInjection.cs +++ b/src/GreenHome.Infrastructure/DependencyInjection.cs @@ -48,6 +48,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Telemetry services.AddScoped(); diff --git a/src/GreenHome.Infrastructure/Services/Alert/AlertService.cs b/src/GreenHome.Infrastructure/Services/Alert/AlertService.cs index 441d5fc..1cf0edc 100644 --- a/src/GreenHome.Infrastructure/Services/Alert/AlertService.cs +++ b/src/GreenHome.Infrastructure/Services/Alert/AlertService.cs @@ -64,7 +64,7 @@ public sealed class AlertService : IAlertService var settings = await _deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken); // 3. Build recipient list - var recipients = BuildRecipients(device, settings); + var recipients = BuildRecipients(device); if (recipients.Length == 0) { _logger.LogInformation("No recipients for device: DeviceId={DeviceId}", deviceId); @@ -220,11 +220,13 @@ public sealed class AlertService : IAlertService usersToAlert.Count, deviceId); } - private static string BuildRecipients(DomainDevice device, DeviceSettingsDto? settings) + private static string BuildRecipients(DomainDevice device) { + // Recipients come from the team: the device owner plus members whose + // "receive alerts" checkbox is ticked in the team section. return string.Join(",", - (device.User.Mobile + "," + (settings?.SmsRecievers ?? "")) - .Split(',') + new[] { device.User.Mobile } + .Concat(device.DeviceUsers.Select(du => du.User.Mobile)) .Select(x => x.Trim()) .Where(x => x.Length > 0) .Distinct() diff --git a/src/GreenHome.Infrastructure/Services/Auth/AuthService.cs b/src/GreenHome.Infrastructure/Services/Auth/AuthService.cs index 6d9ccab..59cc98e 100644 --- a/src/GreenHome.Infrastructure/Services/Auth/AuthService.cs +++ b/src/GreenHome.Infrastructure/Services/Auth/AuthService.cs @@ -34,9 +34,9 @@ public sealed class AuthService : IAuthService SendCodeRequest request, CancellationToken cancellationToken) { - var mobile = NormalizeMobile(request.Mobile); + var mobile = Domain.MobileNormalizer.Normalize(request.Mobile); - if (string.IsNullOrWhiteSpace(mobile) || mobile.Length != 11 || !mobile.StartsWith("09")) + if (mobile.Length == 0) { return new SendCodeResponse { @@ -117,7 +117,7 @@ public sealed class AuthService : IAuthService VerifyCodeRequest request, CancellationToken cancellationToken) { - var mobile = NormalizeMobile(request.Mobile); + var mobile = Domain.MobileNormalizer.Normalize(request.Mobile); var code = request.Code.Trim(); if (string.IsNullOrWhiteSpace(mobile) || string.IsNullOrWhiteSpace(code) || code.Length != 4) @@ -186,7 +186,7 @@ public sealed class AuthService : IAuthService public async Task CanResendCodeAsync(string mobile, CancellationToken cancellationToken) { - var normalizedMobile = NormalizeMobile(mobile); + var normalizedMobile = Domain.MobileNormalizer.Normalize(mobile); var lastCode = await _dbContext.VerificationCodes .Where(v => v.Mobile == normalizedMobile && !v.IsUsed) .OrderByDescending(v => v.CreatedAt) @@ -252,20 +252,4 @@ public sealed class AuthService : IAuthService return $"{payload}.{signature}"; } - private static string NormalizeMobile(string mobile) - { - if (string.IsNullOrWhiteSpace(mobile)) - return string.Empty; - - var normalized = new string(mobile.Where(char.IsDigit).ToArray()); - - if (normalized.StartsWith("9") && normalized.Length == 10) - normalized = "0" + normalized; - else if (normalized.StartsWith("0098") && normalized.Length == 13) - normalized = "0" + normalized.Substring(3); - else if (normalized.StartsWith("98") && normalized.Length == 12) - normalized = "0" + normalized.Substring(2); - - return normalized; - } } diff --git a/src/GreenHome.Infrastructure/Services/Device/DeviceMemberService.cs b/src/GreenHome.Infrastructure/Services/Device/DeviceMemberService.cs new file mode 100644 index 0000000..896d59d --- /dev/null +++ b/src/GreenHome.Infrastructure/Services/Device/DeviceMemberService.cs @@ -0,0 +1,184 @@ +using AutoMapper; +using GreenHome.Application; +using GreenHome.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace GreenHome.Infrastructure.Services.Device; + +public sealed class DeviceMemberService : IDeviceMemberService +{ + private readonly GreenHomeDbContext _dbContext; + private readonly IMapper _mapper; + + public DeviceMemberService(GreenHomeDbContext dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async Task> ListByDeviceAsync( + int deviceId, + CancellationToken cancellationToken) + { + var members = await _dbContext.DeviceUsers + .Include(du => du.User) + .AsNoTracking() + .Where(du => du.DeviceId == deviceId) + .OrderBy(du => du.Role) + .ToListAsync(cancellationToken); + + return _mapper.Map>(members); + } + + public async Task UpsertAsync( + UpsertDeviceMemberRequest request, + CancellationToken cancellationToken) + { + var mobile = Domain.MobileNormalizer.Normalize(request.Mobile); + + if (mobile.Length == 0) + throw new InvalidOperationException("شماره موبایل معتبر نیست"); + + var name = string.IsNullOrWhiteSpace(request.Name) ? mobile : request.Name.Trim(); + var family = request.Family?.Trim() ?? string.Empty; + + var user = await _dbContext.Users + .FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken); + + if (user == null) + { + user = new Domain.User + { + Mobile = mobile, + Name = string.Empty, + Family = string.Empty, + Role = Domain.UserRole.Normal + }; + _dbContext.Users.Add(user); + } + + var member = await _dbContext.DeviceUsers + .FirstOrDefaultAsync(du => du.DeviceId == request.DeviceId && du.UserId == user.Id, cancellationToken); + + if (member == null) + { + member = new Domain.DeviceUser + { + DeviceId = request.DeviceId, + UserId = user.Id, + Role = request.Role, + Name = name, + Family = family, + AvatarPath = request.AvatarPath, + ReceiveAlerts = request.ReceiveAlerts + }; + _dbContext.DeviceUsers.Add(member); + } + else + { + member.Role = request.Role; + member.Name = name; + member.Family = family; + member.AvatarPath = request.AvatarPath ?? member.AvatarPath; + member.ReceiveAlerts = request.ReceiveAlerts; + } + + try + { + await _dbContext.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException) + { + // مسابقه: کاربر یا عضویت هم‌زمان توسط درخواست دیگری ثبت شده؛ از ردیف موجود استفاده می‌کنیم. + _dbContext.ChangeTracker.Clear(); + + user = await _dbContext.Users + .FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken); + if (user == null) + throw; + + member = await _dbContext.DeviceUsers + .FirstOrDefaultAsync(du => du.DeviceId == request.DeviceId && du.UserId == user.Id, cancellationToken); + + if (member == null) + { + member = new Domain.DeviceUser + { + DeviceId = request.DeviceId, + UserId = user.Id, + Role = request.Role, + Name = name, + Family = family, + AvatarPath = request.AvatarPath, + ReceiveAlerts = request.ReceiveAlerts + }; + _dbContext.DeviceUsers.Add(member); + await _dbContext.SaveChangesAsync(cancellationToken); + } + } + + return await GetMemberDtoAsync(request.DeviceId, user.Id, cancellationToken) + ?? throw new InvalidOperationException("عضو پس از ثبت یافت نشد"); + } + + public async Task DeleteAsync(int deviceId, int userId, CancellationToken cancellationToken) + { + var member = await _dbContext.DeviceUsers + .FirstOrDefaultAsync(du => du.DeviceId == deviceId && du.UserId == userId, cancellationToken); + + if (member == null) + throw new InvalidOperationException("عضو مورد نظر یافت نشد"); + + _dbContext.DeviceUsers.Remove(member); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task CanManageDeviceAsync( + int actorUserId, + int deviceId, + CancellationToken cancellationToken) + { + var device = await _dbContext.Devices + .AsNoTracking() + .Where(d => d.Id == deviceId) + .Select(d => d.UserId) + .FirstOrDefaultAsync(cancellationToken); + + if (device == 0) + return false; + + if (device == actorUserId) + return true; + + var actorRole = await _dbContext.Users + .AsNoTracking() + .Where(u => u.Id == actorUserId) + .Select(u => (Domain.UserRole?)u.Role) + .FirstOrDefaultAsync(cancellationToken); + + if (actorRole == Domain.UserRole.Admin) + return true; + + var memberRole = await _dbContext.DeviceUsers + .AsNoTracking() + .Where(du => du.DeviceId == deviceId && du.UserId == actorUserId) + .Select(du => (Domain.DeviceMemberRole?)du.Role) + .FirstOrDefaultAsync(cancellationToken); + + return memberRole == Domain.DeviceMemberRole.Manager; + } + + private async Task GetMemberDtoAsync( + int deviceId, + int userId, + CancellationToken cancellationToken) + { + var member = await _dbContext.DeviceUsers + .Include(du => du.User) + .AsNoTracking() + .FirstOrDefaultAsync(du => du.DeviceId == deviceId && du.UserId == userId, cancellationToken); + + return member == null ? null : _mapper.Map(member); + } + +} diff --git a/src/GreenHome.Infrastructure/Services/Device/DevicePostService.cs b/src/GreenHome.Infrastructure/Services/Device/DevicePostService.cs index f6e9e8d..5943383 100644 --- a/src/GreenHome.Infrastructure/Services/Device/DevicePostService.cs +++ b/src/GreenHome.Infrastructure/Services/Device/DevicePostService.cs @@ -37,7 +37,8 @@ public sealed class DevicePostService : IDevicePostService .Take(filter.PageSize) .ToListAsync(cancellationToken); - var dtos = _mapper.Map>(posts); + var members = await GetMembersMapAsync(filter.DeviceId, cancellationToken); + var dtos = posts.Select(p => ToDto(p, members)).ToList(); return new PagedResult { @@ -56,7 +57,11 @@ public sealed class DevicePostService : IDevicePostService .AsNoTracking() .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); - return post != null ? _mapper.Map(post) : null; + if (post == null) + return null; + + var members = await GetMembersMapAsync(post.DeviceId, cancellationToken); + return ToDto(post, members); } public async Task CreatePostAsync( @@ -135,6 +140,10 @@ public sealed class DevicePostService : IDevicePostService }; _dbContext.DevicePostImages.Add(image); + + if (string.IsNullOrWhiteSpace(post.Content)) + post.PostType = Domain.DevicePostType.Image; + await _dbContext.SaveChangesAsync(cancellationToken); return image.Id; @@ -157,6 +166,15 @@ public sealed class DevicePostService : IDevicePostService int deviceId, CancellationToken cancellationToken) { + var role = await _dbContext.Users + .AsNoTracking() + .Where(u => u.Id == userId) + .Select(u => (Domain.UserRole?)u.Role) + .FirstOrDefaultAsync(cancellationToken); + + if (role == Domain.UserRole.Admin) + return true; + var hasAccess = await _dbContext.Devices .AnyAsync(d => d.Id == deviceId && d.UserId == userId, cancellationToken); @@ -168,4 +186,137 @@ public sealed class DevicePostService : IDevicePostService return hasAccess; } + + public async Task CreateSensorSnapshotAsync( + CreateSensorSnapshotRequest request, + CancellationToken cancellationToken) + { + var hasAccess = await CanUserAccessDeviceAsync(request.AuthorUserId, request.DeviceId, cancellationToken); + if (!hasAccess) + throw new UnauthorizedAccessException("کاربر به این دستگاه دسترسی ندارد"); + + var telemetry = await _dbContext.TelemetryRecords + .AsNoTracking() + .Where(t => t.DeviceId == request.DeviceId) + .OrderByDescending(t => t.ServerTimestampUtc) + .FirstOrDefaultAsync(cancellationToken); + + var relays = await _dbContext.DeviceRelays + .AsNoTracking() + .Where(r => r.DeviceSettings.DeviceId == request.DeviceId) + .OrderBy(r => r.RelayIndex) + .ToListAsync(cancellationToken); + + var post = new Domain.DevicePost + { + DeviceId = request.DeviceId, + AuthorUserId = request.AuthorUserId, + PostType = Domain.DevicePostType.SensorSnapshot, + Content = BuildSnapshotContent(telemetry, relays), + CreatedAt = DateTime.UtcNow + }; + + _dbContext.DevicePosts.Add(post); + await _dbContext.SaveChangesAsync(cancellationToken); + + return post.Id; + } + + public async Task SetPostVoiceAsync( + int postId, + string fileName, + string filePath, + string contentType, + long fileSize, + int? durationSeconds, + CancellationToken cancellationToken) + { + var post = await _dbContext.DevicePosts + .FirstOrDefaultAsync(p => p.Id == postId, cancellationToken); + + if (post == null) + throw new InvalidOperationException($"پست با شناسه {postId} یافت نشد"); + + post.VoiceFilePath = filePath; + post.VoiceDurationSeconds = durationSeconds; + post.PostType = Domain.DevicePostType.Voice; + post.UpdatedAt = DateTime.UtcNow; + + await _dbContext.SaveChangesAsync(cancellationToken); + } + + private async Task> GetMembersMapAsync( + int deviceId, + CancellationToken cancellationToken) + { + return await _dbContext.DeviceUsers + .AsNoTracking() + .Where(du => du.DeviceId == deviceId) + .ToDictionaryAsync(du => du.UserId, cancellationToken); + } + + private DevicePostDto ToDto(Domain.DevicePost post, IReadOnlyDictionary members) + { + var dto = _mapper.Map(post); + + if (members.TryGetValue(post.AuthorUserId, out var member)) + { + dto.AuthorAvatar = member.AvatarPath; + dto.AuthorRole = member.Role; + dto.AuthorName = member.Name; + dto.AuthorFamily = member.Family; + } + + return dto; + } + + private static string BuildSnapshotContent( + Domain.TelemetryRecord? telemetry, + IReadOnlyList relays) + { + if (telemetry == null) + return "📊 وضعیت فعلی: داده‌ای ثبت نشده است."; + + var lines = new List + { + "📊 وضعیت فعلی سنسورها:", + $"🌡 دما: {telemetry.TemperatureC} °C", + $"💧 رطوبت هوا: {telemetry.HumidityPercent}٪", + $"🌱 رطوبت خاک: {telemetry.SoilPercent}٪", + $"☁️ گاز: {telemetry.GasPPM} ppm", + $"💡 نور: {telemetry.Lux} lux" + }; + + if (telemetry.Voltage.HasValue) + lines.Add($"🔌 ولتاژ: {telemetry.Voltage} V"); + if (telemetry.BatteryPercent.HasValue) + lines.Add($"🔋 باتری: {telemetry.BatteryPercent}٪"); + if (telemetry.Power.HasValue) + lines.Add(telemetry.Power.Value == 1 ? "⚡ برق: وصل" : "⚡ برق: قطع"); + + if (relays.Count > 0) + { + lines.Add("🔀 رله‌ها:"); + foreach (var relay in relays) + { + var state = ParseRelayState(telemetry.RelayStates, relay.RelayIndex) ? "روشن" : "خاموش"; + var name = string.IsNullOrWhiteSpace(relay.Name) ? $"رله {relay.RelayIndex}" : relay.Name; + lines.Add($"• {name}: {state}"); + } + } + + return string.Join('\n', lines); + } + + private static bool ParseRelayState(string? relayStates, int relayIndex) + { + if (string.IsNullOrWhiteSpace(relayStates)) + return false; + + var parts = relayStates.Split(','); + if (relayIndex <= 0 || relayIndex > parts.Length) + return false; + + return parts[relayIndex - 1] == "1"; + } } diff --git a/src/GreenHome.Infrastructure/Services/Device/DeviceService.cs b/src/GreenHome.Infrastructure/Services/Device/DeviceService.cs index 040cad3..e3fa5b5 100644 --- a/src/GreenHome.Infrastructure/Services/Device/DeviceService.cs +++ b/src/GreenHome.Infrastructure/Services/Device/DeviceService.cs @@ -79,10 +79,8 @@ public sealed class DeviceService : IDeviceService .AsNoTracking() .Include(d => d.User); - if (user.Role == Domain.UserRole.Normal) - query = query.Where(d => d.UserId == user.Id); - else if (user.Role == Domain.UserRole.Supervisor) - query = query.Where(d => d.DeviceUsers.Any(du => du.UserId == user.Id)); + if (user.Role == Domain.UserRole.Normal || user.Role == Domain.UserRole.Supervisor) + query = query.Where(d => d.UserId == user.Id || d.DeviceUsers.Any(du => du.UserId == user.Id)); if (!string.IsNullOrWhiteSpace(filter.Search)) { diff --git a/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs b/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs index 8df3892..0bc7469 100644 --- a/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs +++ b/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs @@ -51,6 +51,7 @@ public sealed class DeviceSettingsService : IDeviceSettingsService var entity = await _dbContext.DeviceSettings .Include(x => x.Relays) .ThenInclude(r => r.Rules) + .Include(x => x.Device) .FirstOrDefaultAsync(x => x.DeviceId == dto.DeviceId, cancellationToken); if (entity == null) @@ -59,6 +60,9 @@ public sealed class DeviceSettingsService : IDeviceSettingsService _mapper.Map(dto, entity); entity.UpdatedAt = DateTime.UtcNow; + if (!string.IsNullOrWhiteSpace(dto.DeviceName)) + entity.Device.DeviceName = dto.DeviceName.Trim(); + SyncRelays(entity, dto.Relays ?? new List()); await _dbContext.SaveChangesAsync(cancellationToken); diff --git a/src/GreenHome.Web/src/App.tsx b/src/GreenHome.Web/src/App.tsx index c2a5294..0403f88 100644 --- a/src/GreenHome.Web/src/App.tsx +++ b/src/GreenHome.Web/src/App.tsx @@ -11,6 +11,7 @@ import DayDetailsPage from '@/app/day-details/page' import WeatherPage from '@/app/weather/page' import ProfilePage from '@/app/profile/page' import DevicePage from '@/app/device/page' +import GroupPage from '@/app/group/page' function DeviceRedirect() { const location = useLocation() @@ -34,6 +35,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/GreenHome.Web/src/app/device/page.tsx b/src/GreenHome.Web/src/app/device/page.tsx index 98b0745..4ecabdf 100644 --- a/src/GreenHome.Web/src/app/device/page.tsx +++ b/src/GreenHome.Web/src/app/device/page.tsx @@ -1,20 +1,22 @@ "use client" import { useState, Suspense } from 'react' import { useSearchParams } from '@/lib/router' -import { MonitorCog, Plug, Bell, Info } from 'lucide-react' +import { MonitorCog, Plug, Bell, Info, Users } from 'lucide-react' import Loading from '@/components/Loading' import { AppShell } from '@/components/layout' import { RelaySettingsTab } from '@/components/device-settings/RelaySettingsTab' import { DeviceInfoTab } from '@/components/device-settings/DeviceInfoTab' import { AlertSettingsTab } from '@/components/alert-settings/AlertSettingsTab' +import { TeamPanel } from '@/components/team/TeamPanel' import { cn } from '@/lib/utils' -type DeviceTab = 'relays' | 'alerts' | 'settings' +type DeviceTab = 'relays' | 'alerts' | 'settings' | 'team' const tabs: { key: DeviceTab; label: string; icon: typeof Plug }[] = [ { key: 'settings', label: 'تنظیمات', icon: Info }, { key: 'relays', label: 'رله‌ها', icon: Plug }, { key: 'alerts', label: 'هشدارها', icon: Bell }, + { key: 'team', label: 'تیم', icon: Users }, ] function DevicePageContent() { @@ -57,6 +59,7 @@ function DevicePageContent() { {tab === 'relays' && } {tab === 'alerts' && } {tab === 'settings' && } + {tab === 'team' && } ) diff --git a/src/GreenHome.Web/src/app/group/page.tsx b/src/GreenHome.Web/src/app/group/page.tsx new file mode 100644 index 0000000..18dc87d --- /dev/null +++ b/src/GreenHome.Web/src/app/group/page.tsx @@ -0,0 +1,38 @@ +"use client" +import { Suspense } from 'react' +import { useSearchParams } from '@/lib/router' +import { MessageCircle } from 'lucide-react' +import Loading from '@/components/Loading' +import { AppShell } from '@/components/layout' +import { GroupFeed } from '@/components/group/GroupFeed' + +function GroupPageContent() { + const searchParams = useSearchParams() + const deviceId = Number(searchParams.get('deviceId') ?? '0') + + return ( + +
+ {deviceId > 0 ? ( + + ) : ( +
+ ابتدا یک دستگاه انتخاب کنید. +
+ )} +
+
+ ) +} + +export default function GroupPage() { + return ( + }> + + + ) +} diff --git a/src/GreenHome.Web/src/components/device-settings/DeviceInfoTab.tsx b/src/GreenHome.Web/src/components/device-settings/DeviceInfoTab.tsx index 6c27158..ce07c75 100644 --- a/src/GreenHome.Web/src/components/device-settings/DeviceInfoTab.tsx +++ b/src/GreenHome.Web/src/components/device-settings/DeviceInfoTab.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState, useCallback, type ReactNode } from 'react' -import { Save, X, MapPin, Sprout, Smartphone, BellRing, PhoneCall, Clock, type LucideIcon } from 'lucide-react' +import { Save, MapPin, Sprout, type LucideIcon } from 'lucide-react' import { api, DeviceSettingsDto } from '@/lib/api' import Loading from '@/components/Loading' import { ErrorMessage, SuccessMessage, EmptyState, Button } from '@/components/common' @@ -49,7 +49,6 @@ export function DeviceInfoTab({ deviceId, deviceName }: { deviceId: number; devi const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) - const [smsInput, setSmsInput] = useState('') const loadSettings = useCallback(async () => { try { @@ -72,29 +71,6 @@ export function DeviceInfoTab({ deviceId, deviceName }: { deviceId: number; devi setSettings(s => s ? { ...s, ...patch } : s) } - const addReceiver = () => { - if (!settings) return - const num = smsInput.replace(/[^\d]/g, '').trim() - if (!/^09\d{9}$/.test(num)) { - window.alert('شماره موبایل معتبر وارد کنید (مثلاً 09123456789)') - return - } - const receivers = settings.smsRecievers.split(',').map(s => s.trim()).filter(Boolean) - if (receivers.includes(num)) { - window.alert('این شماره قبلاً اضافه شده است') - return - } - update({ smsRecievers: [...receivers, num].join(',') }) - setSmsInput('') - } - - const removeReceiver = (index: number) => { - if (!settings) return - const receivers = settings.smsRecievers.split(',').map(s => s.trim()).filter(Boolean) - receivers.splice(index, 1) - update({ smsRecievers: receivers.join(',') }) - } - const handleSave = async () => { if (!settings) return try { @@ -127,8 +103,6 @@ export function DeviceInfoTab({ deviceId, deviceName }: { deviceId: number; devi ) } - const receivers = settings.smsRecievers.split(',').map(s => s.trim()).filter(Boolean) - return (
{error && } @@ -171,14 +145,29 @@ export function DeviceInfoTab({ deviceId, deviceName }: { deviceId: number; devi
- {/* نوع دستگاه */} + {/* مشخصات دستگاه */}
+ + update({ deviceName: e.target.value })} + /> + + + + update({ productType: e.target.value })} /> @@ -197,95 +186,15 @@ export function DeviceInfoTab({ deviceId, deviceName }: { deviceId: number; devi
- {/* ارتباطات */} - -
- - update({ devicePhoneNumber: e.target.value })} - /> - - - update({ uploadIntervalMin: parseInt(e.target.value) || 0 })} - /> - -
-
- {/* اطلاع‌رسانی */} -
- -
-
- setSmsInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - addReceiver() - } - }} - placeholder="09123456789" - inputMode="tel" - dir="ltr" - /> - -
- -
- {receivers.length > 0 ? ( -
- {receivers.map((num, i) => ( - - {num} - - - ))} -
- ) : ( -

هنوز شماره‌ای اضافه نشده است

- )} -
- -
+
- + */}
+ +
+
+ + {/* Messages */} +
+ {error && ( +
+ {error} +
+ )} + + {loading ? ( +
در حال بارگذاری پیام‌ها...
+ ) : posts.length === 0 ? ( +
+ هنوز پیامی ثبت نشده است. اولین پیام را بفرستید. +
+ ) : ( + <> + {hasMore && ( +
+ +
+ )} + + {[...posts].reverse().map(post => ( + + ))} + + )} + +
+
+ + {/* Emoji row */} + {showEmoji && ( +
+ {EMOJIS.map(emoji => ( + + ))} +
+ )} + + {/* Composer */} +
+
+ + + + + + + + +