From 8eb465c0e79aee52de48b61d43c81e4fb3c585b1 Mon Sep 17 00:00:00 2001 From: alireza Date: Thu, 10 Sep 2026 19:09:36 +0330 Subject: [PATCH] add chat and users --- .../Controllers/DeviceMembersController.cs | 120 ++ .../Controllers/DevicePostsController.cs | 80 + .../1ffe0446-dd82-4b16-92d5-e1f25964c018.png | Bin 0 -> 23837 bytes ...3d435bb84579_photo_2026-08-26_19-29-21.jpg | Bin 0 -> 30938 bytes ...4044abead7b4_photo_2026-08-26_19-29-21.jpg | Bin 0 -> 30938 bytes .../Dtos/Device/DeviceMemberDto.cs | 26 + .../Dtos/DevicePost/DevicePostDto.cs | 11 + .../Interfaces/IDeviceMemberService.cs | 9 + .../Interfaces/IDevicePostService.cs | 2 + src/GreenHome.Application/MappingProfile.cs | 5 + src/GreenHome.Domain/DeviceMemberRole.cs | 22 + src/GreenHome.Domain/DevicePost.cs | 33 + src/GreenHome.Domain/DeviceSettings.cs | 2 + src/GreenHome.Domain/DeviceUser.cs | 20 + src/GreenHome.Domain/MobileNormalizer.cs | 30 + .../Data/GreenHomeDbContext.cs | 6 + ...0910115241_AddTeamAndPostTypes.Designer.cs | 1441 ++++++++++++++++ .../20260910115241_AddTeamAndPostTypes.cs | 72 + ...56_MoveMemberNamesToDeviceUser.Designer.cs | 1451 +++++++++++++++++ ...60910151556_MoveMemberNamesToDeviceUser.cs | 47 + .../GreenHomeDbContextModelSnapshot.cs | 27 + .../DependencyInjection.cs | 1 + .../Services/Alert/AlertService.cs | 10 +- .../Services/Auth/AuthService.cs | 24 +- .../Services/Device/DeviceMemberService.cs | 184 +++ .../Services/Device/DevicePostService.cs | 155 +- .../Services/Device/DeviceService.cs | 6 +- .../Services/Device/DeviceSettingsService.cs | 4 + src/GreenHome.Web/src/App.tsx | 2 + src/GreenHome.Web/src/app/device/page.tsx | 7 +- src/GreenHome.Web/src/app/group/page.tsx | 38 + .../device-settings/DeviceInfoTab.tsx | 137 +- .../src/components/group/GroupFeed.tsx | 521 ++++++ .../src/components/layout/BottomNav.tsx | 8 +- .../src/components/team/TeamPanel.tsx | 395 +++++ .../src/features/team/constants.ts | 17 + src/GreenHome.Web/src/lib/api/assets.ts | 13 + src/GreenHome.Web/src/lib/api/client.ts | 52 +- src/GreenHome.Web/src/lib/api/types.ts | 53 + src/GreenHome.Web/vite.config.ts | 5 + 40 files changed, 4888 insertions(+), 148 deletions(-) create mode 100644 src/GreenHome.Api/Controllers/DeviceMembersController.cs create mode 100644 src/GreenHome.Api/wwwroot/uploads/avatars/1ffe0446-dd82-4b16-92d5-e1f25964c018.png create mode 100644 src/GreenHome.Api/wwwroot/uploads/posts/42a6f162-af0a-4da6-a8bb-3d435bb84579_photo_2026-08-26_19-29-21.jpg create mode 100644 src/GreenHome.Api/wwwroot/uploads/posts/9918d5dd-65ac-4207-9637-4044abead7b4_photo_2026-08-26_19-29-21.jpg create mode 100644 src/GreenHome.Application/Dtos/Device/DeviceMemberDto.cs create mode 100644 src/GreenHome.Application/Interfaces/IDeviceMemberService.cs create mode 100644 src/GreenHome.Domain/DeviceMemberRole.cs create mode 100644 src/GreenHome.Domain/MobileNormalizer.cs create mode 100644 src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.Designer.cs create mode 100644 src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910115241_AddTeamAndPostTypes.cs create mode 100644 src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.Designer.cs create mode 100644 src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260910151556_MoveMemberNamesToDeviceUser.cs create mode 100644 src/GreenHome.Infrastructure/Services/Device/DeviceMemberService.cs create mode 100644 src/GreenHome.Web/src/app/group/page.tsx create mode 100644 src/GreenHome.Web/src/components/group/GroupFeed.tsx create mode 100644 src/GreenHome.Web/src/components/team/TeamPanel.tsx create mode 100644 src/GreenHome.Web/src/features/team/constants.ts create mode 100644 src/GreenHome.Web/src/lib/api/assets.ts 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 0000000000000000000000000000000000000000..0bfe95b388825bf2a06861ed1964fcfc4f2b9462 GIT binary patch literal 23837 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 Nf~o7PRjSy%{U3%{XJ7yT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..73fd37f2bb0a5b4072bb831a47e745d58b12a883 GIT binary patch literal 30938 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..73fd37f2bb0a5b4072bb831a47e745d58b12a883 GIT binary patch literal 30938 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 literal 0 HcmV?d00001 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 */} +
+
+ + + + + + + + +