From 74e8480a6807a6b6c189d23e690d69f50d5ccafc Mon Sep 17 00:00:00 2001 From: rahimi rahimi Date: Wed, 17 Dec 2025 00:34:41 +0330 Subject: [PATCH] version 3 --- .../Controllers/AlertLogsController.cs | 62 + .../Controllers/ChecklistsController.cs | 98 ++ .../Controllers/DailyReportController.cs | 66 + .../Controllers/DevicePostsController.cs | 212 +++ .../Controllers/MonthlyReportController.cs | 62 + .../Controllers/PowerOutageController.cs | 60 + .../Controllers/UserDailyReportsController.cs | 210 +++ src/GreenHome.Api/GreenHome.Api.csproj | 2 +- src/GreenHome.Api/Program.cs | 15 +- src/GreenHome.Application/ChecklistDtos.cs | 80 ++ src/GreenHome.Application/DevicePostDtos.cs | 46 + src/GreenHome.Application/Dtos.cs | 110 ++ src/GreenHome.Application/IAlertLogService.cs | 9 + src/GreenHome.Application/IAlertService.cs | 1 + .../IChecklistService.cs | 12 + .../IDailyReportService.cs | 4 + .../IDevicePostService.cs | 14 + .../IMonthlyReportService.cs | 45 + .../IUserDailyReportService.cs | 13 + src/GreenHome.Application/MappingProfile.cs | 35 + src/GreenHome.Domain/AlertLog.cs | 110 ++ src/GreenHome.Domain/AlertNotification.cs | 4 +- src/GreenHome.Domain/Checklist.cs | 156 +++ src/GreenHome.Domain/DevicePost.cs | 81 ++ src/GreenHome.Domain/DeviceSettings.cs | 20 + src/GreenHome.Domain/DeviceUser.cs | 5 + src/GreenHome.Domain/UserDailyReport.cs | 121 ++ .../AlertLogService.cs | 100 ++ src/GreenHome.Infrastructure/AlertService.cs | 278 +++- .../ChecklistService.cs | 152 +++ .../DailyReportService.cs | 256 +++- .../DevicePostService.cs | 185 +++ .../GreenHomeDbContext.cs | 178 +++ ...251216204600_AddAllNewFeatures.Designer.cs | 1199 +++++++++++++++++ .../20251216204600_AddAllNewFeatures.cs | 479 +++++++ .../GreenHomeDbContextModelSnapshot.cs | 579 +++++++- .../MonthlyReportService.cs | 185 +++ .../UserDailyReportService.cs | 225 ++++ 38 files changed, 5399 insertions(+), 70 deletions(-) create mode 100644 src/GreenHome.Api/Controllers/AlertLogsController.cs create mode 100644 src/GreenHome.Api/Controllers/ChecklistsController.cs create mode 100644 src/GreenHome.Api/Controllers/DevicePostsController.cs create mode 100644 src/GreenHome.Api/Controllers/MonthlyReportController.cs create mode 100644 src/GreenHome.Api/Controllers/PowerOutageController.cs create mode 100644 src/GreenHome.Api/Controllers/UserDailyReportsController.cs create mode 100644 src/GreenHome.Application/ChecklistDtos.cs create mode 100644 src/GreenHome.Application/DevicePostDtos.cs create mode 100644 src/GreenHome.Application/IAlertLogService.cs create mode 100644 src/GreenHome.Application/IChecklistService.cs create mode 100644 src/GreenHome.Application/IDevicePostService.cs create mode 100644 src/GreenHome.Application/IMonthlyReportService.cs create mode 100644 src/GreenHome.Application/IUserDailyReportService.cs create mode 100644 src/GreenHome.Domain/AlertLog.cs create mode 100644 src/GreenHome.Domain/Checklist.cs create mode 100644 src/GreenHome.Domain/DevicePost.cs create mode 100644 src/GreenHome.Domain/UserDailyReport.cs create mode 100644 src/GreenHome.Infrastructure/AlertLogService.cs create mode 100644 src/GreenHome.Infrastructure/ChecklistService.cs create mode 100644 src/GreenHome.Infrastructure/DevicePostService.cs create mode 100644 src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs create mode 100644 src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.cs create mode 100644 src/GreenHome.Infrastructure/MonthlyReportService.cs create mode 100644 src/GreenHome.Infrastructure/UserDailyReportService.cs diff --git a/src/GreenHome.Api/Controllers/AlertLogsController.cs b/src/GreenHome.Api/Controllers/AlertLogsController.cs new file mode 100644 index 0000000..100b202 --- /dev/null +++ b/src/GreenHome.Api/Controllers/AlertLogsController.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AlertLogsController : ControllerBase +{ + private readonly IAlertLogService _alertLogService; + + public AlertLogsController(IAlertLogService alertLogService) + { + _alertLogService = alertLogService; + } + + /// + /// دریافت لیست لاگ‌های هشدار با فیلتر و صفحه‌بندی + /// + [HttpGet] + public async Task>> GetAlertLogs( + [FromQuery] int? deviceId, + [FromQuery] int? userId, + [FromQuery] Domain.AlertType? alertType, + [FromQuery] Domain.AlertStatus? status, + [FromQuery] DateTime? startDate, + [FromQuery] DateTime? endDate, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken cancellationToken = default) + { + var filter = new AlertLogFilter + { + DeviceId = deviceId, + UserId = userId, + AlertType = alertType, + Status = status, + StartDate = startDate, + EndDate = endDate, + Page = page, + PageSize = pageSize + }; + + var result = await _alertLogService.GetAlertLogsAsync(filter, cancellationToken); + return Ok(result); + } + + /// + /// دریافت جزئیات کامل یک لاگ هشدار + /// + [HttpGet("{id}")] + public async Task> GetAlertLogById(int id, CancellationToken cancellationToken) + { + var result = await _alertLogService.GetAlertLogByIdAsync(id, cancellationToken); + if (result == null) + { + return NotFound(new { error = $"لاگ هشدار با شناسه {id} یافت نشد" }); + } + return Ok(result); + } +} + diff --git a/src/GreenHome.Api/Controllers/ChecklistsController.cs b/src/GreenHome.Api/Controllers/ChecklistsController.cs new file mode 100644 index 0000000..cb5a328 --- /dev/null +++ b/src/GreenHome.Api/Controllers/ChecklistsController.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ChecklistsController : ControllerBase +{ + private readonly IChecklistService _checklistService; + + public ChecklistsController(IChecklistService checklistService) + { + _checklistService = checklistService; + } + + /// + /// دریافت چک‌لیست فعال یک دستگاه + /// + [HttpGet("active/{deviceId}")] + public async Task> GetActiveChecklist(int deviceId, CancellationToken cancellationToken) + { + var result = await _checklistService.GetActiveChecklistByDeviceIdAsync(deviceId, cancellationToken); + if (result == null) + { + return NotFound(new { error = "چک‌لیست فعالی برای این دستگاه یافت نشد" }); + } + return Ok(result); + } + + /// + /// دریافت تمام چک‌لیست‌های یک دستگاه + /// + [HttpGet("device/{deviceId}")] + public async Task>> GetChecklists(int deviceId, CancellationToken cancellationToken) + { + var result = await _checklistService.GetChecklistsByDeviceIdAsync(deviceId, cancellationToken); + return Ok(result); + } + + /// + /// دریافت جزئیات یک چک‌لیست + /// + [HttpGet("{id}")] + public async Task> GetChecklistById(int id, CancellationToken cancellationToken) + { + var result = await _checklistService.GetChecklistByIdAsync(id, cancellationToken); + if (result == null) + { + return NotFound(new { error = $"چک‌لیست با شناسه {id} یافت نشد" }); + } + return Ok(result); + } + + /// + /// ایجاد چک‌لیست جدید (چک‌لیست قبلی غیرفعال می‌شود) + /// + [HttpPost] + public async Task> CreateChecklist( + CreateChecklistRequest request, + CancellationToken cancellationToken) + { + var id = await _checklistService.CreateChecklistAsync(request, cancellationToken); + return Ok(new { id, message = "چک‌لیست با موفقیت ایجاد شد و چک‌لیست قبلی غیرفعال شد" }); + } + + /// + /// دریافت سابقه تکمیل‌های یک چک‌لیست + /// + [HttpGet("{checklistId}/completions")] + public async Task>> GetCompletions( + int checklistId, + CancellationToken cancellationToken) + { + var result = await _checklistService.GetCompletionsByChecklistIdAsync(checklistId, cancellationToken); + return Ok(result); + } + + /// + /// ثبت تکمیل چک‌لیست + /// + [HttpPost("complete")] + public async Task> CompleteChecklist( + CompleteChecklistRequest request, + CancellationToken cancellationToken) + { + try + { + var id = await _checklistService.CompleteChecklistAsync(request, cancellationToken); + return Ok(new { id, message = "چک‌لیست با موفقیت تکمیل شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } +} + diff --git a/src/GreenHome.Api/Controllers/DailyReportController.cs b/src/GreenHome.Api/Controllers/DailyReportController.cs index a97c8ea..be7b6cc 100644 --- a/src/GreenHome.Api/Controllers/DailyReportController.cs +++ b/src/GreenHome.Api/Controllers/DailyReportController.cs @@ -73,5 +73,71 @@ public class DailyReportController : ControllerBase return StatusCode(500, new { error = "خطای سرور در پردازش درخواست" }); } } + + /// + /// دریافت تحلیل هفتگی + /// + [HttpGet("weekly")] + public async Task> GetWeeklyAnalysis( + [FromQuery] int deviceId, + [FromQuery] string startDate, + [FromQuery] string endDate, + CancellationToken cancellationToken) + { + try + { + var request = new WeeklyAnalysisRequest + { + DeviceId = deviceId, + StartDate = startDate.Trim(), + EndDate = endDate.Trim() + }; + + var result = await _dailyReportService.GetWeeklyAnalysisAsync(request, cancellationToken); + return Ok(result); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting weekly analysis"); + return StatusCode(500, new { error = "خطا در دریافت تحلیل هفتگی" }); + } + } + + /// + /// دریافت تحلیل ماهانه + /// + [HttpGet("monthly")] + public async Task> GetMonthlyAnalysis( + [FromQuery] int deviceId, + [FromQuery] int year, + [FromQuery] int month, + CancellationToken cancellationToken) + { + try + { + var request = new MonthlyAnalysisRequest + { + DeviceId = deviceId, + Year = year, + Month = month + }; + + var result = await _dailyReportService.GetMonthlyAnalysisAsync(request, cancellationToken); + return Ok(result); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting monthly analysis"); + return StatusCode(500, new { error = "خطا در دریافت تحلیل ماهانه" }); + } + } } diff --git a/src/GreenHome.Api/Controllers/DevicePostsController.cs b/src/GreenHome.Api/Controllers/DevicePostsController.cs new file mode 100644 index 0000000..8de3e35 --- /dev/null +++ b/src/GreenHome.Api/Controllers/DevicePostsController.cs @@ -0,0 +1,212 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class DevicePostsController : ControllerBase +{ + private readonly IDevicePostService _postService; + private readonly ILogger _logger; + private readonly IWebHostEnvironment _environment; + + public DevicePostsController( + IDevicePostService postService, + ILogger logger, + IWebHostEnvironment environment) + { + _postService = postService; + _logger = logger; + _environment = environment; + } + + /// + /// دریافت پست‌های گروه مجازی دستگاه (تایم‌لاین) + /// + [HttpGet] + public async Task>> GetPosts( + [FromQuery] int deviceId, + [FromQuery] int? authorUserId, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken cancellationToken = default) + { + var filter = new DevicePostFilter + { + DeviceId = deviceId, + AuthorUserId = authorUserId, + Page = page, + PageSize = pageSize + }; + + var result = await _postService.GetPostsAsync(filter, cancellationToken); + return Ok(result); + } + + /// + /// دریافت جزئیات یک پست + /// + [HttpGet("{id}")] + public async Task> GetPostById(int id, CancellationToken cancellationToken) + { + var result = await _postService.GetPostByIdAsync(id, cancellationToken); + if (result == null) + { + return NotFound(new { error = $"پست با شناسه {id} یافت نشد" }); + } + return Ok(result); + } + + /// + /// ایجاد پست جدید در گروه مجازی + /// + [HttpPost] + public async Task> CreatePost( + CreateDevicePostRequest request, + CancellationToken cancellationToken) + { + try + { + var id = await _postService.CreatePostAsync(request, cancellationToken); + return Ok(new { id, message = "پست با موفقیت ایجاد شد" }); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(new { error = ex.Message }); + } + } + + /// + /// ویرایش پست + /// + [HttpPut] + public async Task UpdatePost( + UpdateDevicePostRequest request, + CancellationToken cancellationToken) + { + try + { + await _postService.UpdatePostAsync(request, cancellationToken); + return Ok(new { message = "پست با موفقیت ویرایش شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// حذف پست + /// + [HttpDelete("{id}")] + public async Task DeletePost(int id, CancellationToken cancellationToken) + { + try + { + await _postService.DeletePostAsync(id, cancellationToken); + return Ok(new { message = "پست با موفقیت حذف شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// آپلود تصویر برای پست + /// + [HttpPost("{postId}/images")] + [Consumes("multipart/form-data")] + public async Task> UploadImage( + int postId, + [FromForm] IFormFile file, + CancellationToken cancellationToken) + { + try + { + if (file == null || file.Length == 0) + { + return BadRequest(new { error = "فایل انتخاب نشده است" }); + } + + // Validate file type + var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp" }; + if (!allowedTypes.Contains(file.ContentType.ToLower())) + { + return BadRequest(new { error = "فقط فایل‌های تصویری مجاز هستند" }); + } + + // Validate file size (max 5MB) + if (file.Length > 5 * 1024 * 1024) + { + return BadRequest(new { error = "حجم فایل نباید بیشتر از 5 مگابایت باشد" }); + } + + // Create upload directory + var uploadsFolder = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "posts"); + Directory.CreateDirectory(uploadsFolder); + + // Generate unique filename + var fileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}"; + var filePath = Path.Combine(uploadsFolder, fileName); + + // Save file + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream, cancellationToken); + } + + // Save to database + var relativePath = $"/uploads/posts/{fileName}"; + var imageId = await _postService.AddImageToPostAsync( + postId, + file.FileName, + relativePath, + file.ContentType, + file.Length, + cancellationToken); + + _logger.LogInformation("Image uploaded for post {PostId}: {ImageId}", postId, imageId); + + return Ok(new { imageId, filePath = relativePath, message = "تصویر با موفقیت آپلود شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error uploading image for post {PostId}", postId); + return StatusCode(500, new { error = "خطا در آپلود تصویر" }); + } + } + + /// + /// حذف تصویر از پست + /// + [HttpDelete("images/{imageId}")] + public async Task DeleteImage(int imageId, CancellationToken cancellationToken) + { + try + { + await _postService.DeleteImageAsync(imageId, cancellationToken); + return Ok(new { message = "تصویر با موفقیت حذف شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// بررسی دسترسی کاربر به دستگاه + /// + [HttpGet("access/{userId}/{deviceId}")] + public async Task> CheckAccess(int userId, int deviceId, CancellationToken cancellationToken) + { + var hasAccess = await _postService.CanUserAccessDeviceAsync(userId, deviceId, cancellationToken); + return Ok(new { hasAccess }); + } +} + diff --git a/src/GreenHome.Api/Controllers/MonthlyReportController.cs b/src/GreenHome.Api/Controllers/MonthlyReportController.cs new file mode 100644 index 0000000..ba0f376 --- /dev/null +++ b/src/GreenHome.Api/Controllers/MonthlyReportController.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class MonthlyReportController : ControllerBase +{ + private readonly IMonthlyReportService _monthlyReportService; + private readonly ILogger _logger; + + public MonthlyReportController( + IMonthlyReportService monthlyReportService, + ILogger logger) + { + _monthlyReportService = monthlyReportService; + _logger = logger; + } + + /// + /// دریافت گزارش آماری ماهانه + /// + [HttpGet] + public async Task> GetMonthlyReport( + [FromQuery] int deviceId, + [FromQuery] int year, + [FromQuery] int month, + CancellationToken cancellationToken) + { + try + { + if (deviceId <= 0) + { + return BadRequest(new { error = "شناسه دستگاه نامعتبر است" }); + } + + if (month < 1 || month > 12) + { + return BadRequest(new { error = "ماه باید بین 1 تا 12 باشد" }); + } + + var result = await _monthlyReportService.GetMonthlyReportAsync(deviceId, year, month, cancellationToken); + + _logger.LogInformation( + "گزارش ماهانه برای دستگاه {DeviceId} و ماه {Month}/{Year} ایجاد شد", + deviceId, month, year); + + return Ok(result); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating monthly report"); + return StatusCode(500, new { error = "خطا در ایجاد گزارش ماهانه" }); + } + } +} + diff --git a/src/GreenHome.Api/Controllers/PowerOutageController.cs b/src/GreenHome.Api/Controllers/PowerOutageController.cs new file mode 100644 index 0000000..b98fa67 --- /dev/null +++ b/src/GreenHome.Api/Controllers/PowerOutageController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class PowerOutageController : ControllerBase +{ + private readonly IAlertService _alertService; + private readonly ILogger _logger; + + public PowerOutageController( + IAlertService alertService, + ILogger logger) + { + _alertService = alertService; + _logger = logger; + } + + /// + /// ارسال هشدار قطع برق برای یک دستگاه + /// + /// شناسه دستگاه + /// Cancellation token + /// نتیجه عملیات + [HttpPost] + public async Task SendPowerOutageAlert( + [FromQuery] int deviceId, + CancellationToken cancellationToken) + { + try + { + if (deviceId <= 0) + { + return BadRequest(new { error = "شناسه دستگاه نامعتبر است" }); + } + + await _alertService.SendPowerOutageAlertAsync(deviceId, cancellationToken); + + _logger.LogInformation("Power outage alert processed for device {DeviceId}", deviceId); + + return Ok(new { + success = true, + message = "هشدار قطع برق با موفقیت ارسال شد" + }); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, "Invalid operation for power outage alert: DeviceId={DeviceId}", deviceId); + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending power outage alert: DeviceId={DeviceId}", deviceId); + return StatusCode(500, new { error = "خطا در ارسال هشدار قطع برق" }); + } + } +} + diff --git a/src/GreenHome.Api/Controllers/UserDailyReportsController.cs b/src/GreenHome.Api/Controllers/UserDailyReportsController.cs new file mode 100644 index 0000000..002aa6b --- /dev/null +++ b/src/GreenHome.Api/Controllers/UserDailyReportsController.cs @@ -0,0 +1,210 @@ +using Microsoft.AspNetCore.Mvc; +using GreenHome.Application; + +namespace GreenHome.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class UserDailyReportsController : ControllerBase +{ + private readonly IUserDailyReportService _reportService; + private readonly ILogger _logger; + private readonly IWebHostEnvironment _environment; + + public UserDailyReportsController( + IUserDailyReportService reportService, + ILogger logger, + IWebHostEnvironment environment) + { + _reportService = reportService; + _logger = logger; + _environment = environment; + } + + /// + /// دریافت لیست گزارش‌های روزانه کاربران با فیلتر + /// + [HttpGet] + public async Task>> GetReports( + [FromQuery] int? deviceId, + [FromQuery] int? userId, + [FromQuery] string? persianDate, + [FromQuery] int? year, + [FromQuery] int? month, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken cancellationToken = default) + { + var filter = new UserDailyReportFilter + { + DeviceId = deviceId, + UserId = userId, + PersianDate = persianDate, + Year = year, + Month = month, + Page = page, + PageSize = pageSize + }; + + var result = await _reportService.GetReportsAsync(filter, cancellationToken); + return Ok(result); + } + + /// + /// دریافت جزئیات یک گزارش روزانه + /// + [HttpGet("{id}")] + public async Task> GetReportById(int id, CancellationToken cancellationToken) + { + var result = await _reportService.GetReportByIdAsync(id, cancellationToken); + if (result == null) + { + return NotFound(new { error = $"گزارش با شناسه {id} یافت نشد" }); + } + return Ok(result); + } + + /// + /// ایجاد گزارش روزانه جدید + /// + [HttpPost] + public async Task> CreateReport( + CreateUserDailyReportRequest request, + CancellationToken cancellationToken) + { + try + { + var id = await _reportService.CreateReportAsync(request, cancellationToken); + return Ok(new { id, message = "گزارش با موفقیت ایجاد شد" }); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// ویرایش گزارش روزانه + /// + [HttpPut] + public async Task UpdateReport( + UpdateUserDailyReportRequest request, + CancellationToken cancellationToken) + { + try + { + await _reportService.UpdateReportAsync(request, cancellationToken); + return Ok(new { message = "گزارش با موفقیت ویرایش شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// حذف گزارش روزانه + /// + [HttpDelete("{id}")] + public async Task DeleteReport(int id, CancellationToken cancellationToken) + { + try + { + await _reportService.DeleteReportAsync(id, cancellationToken); + return Ok(new { message = "گزارش با موفقیت حذف شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } + + /// + /// آپلود تصویر برای گزارش روزانه + /// + [HttpPost("{reportId}/images")] + [Consumes("multipart/form-data")] + public async Task> UploadImage( + int reportId, + [FromForm] IFormFile file, + [FromForm] string? description, + CancellationToken cancellationToken) + { + try + { + if (file == null || file.Length == 0) + { + return BadRequest(new { error = "فایل انتخاب نشده است" }); + } + + // Validate file type + var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp" }; + if (!allowedTypes.Contains(file.ContentType.ToLower())) + { + return BadRequest(new { error = "فقط فایل‌های تصویری مجاز هستند" }); + } + + // Validate file size (max 5MB) + if (file.Length > 5 * 1024 * 1024) + { + return BadRequest(new { error = "حجم فایل نباید بیشتر از 5 مگابایت باشد" }); + } + + // Create upload directory + var uploadsFolder = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "reports"); + Directory.CreateDirectory(uploadsFolder); + + // Generate unique filename + var fileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}"; + var filePath = Path.Combine(uploadsFolder, fileName); + + // Save file + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream, cancellationToken); + } + + // Save to database + var relativePath = $"/uploads/reports/{fileName}"; + var imageId = await _reportService.AddImageToReportAsync( + reportId, + file.FileName, + relativePath, + file.ContentType, + file.Length, + description, + cancellationToken); + + _logger.LogInformation("Image uploaded for report {ReportId}: {ImageId}", reportId, imageId); + + return Ok(new { imageId, filePath = relativePath, message = "تصویر با موفقیت آپلود شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error uploading image for report {ReportId}", reportId); + return StatusCode(500, new { error = "خطا در آپلود تصویر" }); + } + } + + /// + /// حذف تصویر از گزارش + /// + [HttpDelete("images/{imageId}")] + public async Task DeleteImage(int imageId, CancellationToken cancellationToken) + { + try + { + await _reportService.DeleteImageAsync(imageId, cancellationToken); + return Ok(new { message = "تصویر با موفقیت حذف شد" }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } + } +} + diff --git a/src/GreenHome.Api/GreenHome.Api.csproj b/src/GreenHome.Api/GreenHome.Api.csproj index 054198c..14c7f53 100644 --- a/src/GreenHome.Api/GreenHome.Api.csproj +++ b/src/GreenHome.Api/GreenHome.Api.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/GreenHome.Api/Program.cs b/src/GreenHome.Api/Program.cs index 9c3c8f7..e1fa0e3 100644 --- a/src/GreenHome.Api/Program.cs +++ b/src/GreenHome.Api/Program.cs @@ -5,12 +5,13 @@ using GreenHome.Infrastructure; using GreenHome.Sms.Ippanel; using GreenHome.VoiceCall.Avanak; using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddOpenApi(); // Application/Infrastructure DI builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile)); @@ -60,6 +61,11 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // SMS Service Configuration builder.Services.AddIppanelSms(builder.Configuration); @@ -89,11 +95,8 @@ using (var scope = app.Services.CreateScope()) } // Configure the HTTP request pipeline. -//if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} +app.MapOpenApi(); +app.MapScalarApiReference(); // HTTPS Redirection فقط در Production if (!app.Environment.IsDevelopment()) diff --git a/src/GreenHome.Application/ChecklistDtos.cs b/src/GreenHome.Application/ChecklistDtos.cs new file mode 100644 index 0000000..ffa3739 --- /dev/null +++ b/src/GreenHome.Application/ChecklistDtos.cs @@ -0,0 +1,80 @@ +namespace GreenHome.Application; + +public sealed class ChecklistDto +{ + public int Id { get; set; } + public int DeviceId { get; set; } + public string DeviceName { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public bool IsActive { get; set; } + public List Items { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public int CreatedByUserId { get; set; } + public string CreatedByUserName { get; set; } = string.Empty; +} + +public sealed class ChecklistItemDto +{ + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public int Order { get; set; } + public bool IsRequired { get; set; } +} + +public sealed class CreateChecklistRequest +{ + public required int DeviceId { get; set; } + public required int CreatedByUserId { get; set; } + public required string Title { get; set; } + public string? Description { get; set; } + public required List Items { get; set; } +} + +public sealed class CreateChecklistItemRequest +{ + public required string Title { get; set; } + public string? Description { get; set; } + public int Order { get; set; } + public bool IsRequired { get; set; } +} + +public sealed class ChecklistCompletionDto +{ + public int Id { get; set; } + public int ChecklistId { get; set; } + public string ChecklistTitle { get; set; } = string.Empty; + public int CompletedByUserId { get; set; } + public string CompletedByUserName { get; set; } = string.Empty; + public string PersianDate { get; set; } = string.Empty; + public List ItemCompletions { get; set; } = new(); + public string? Notes { get; set; } + public DateTime CompletedAt { get; set; } +} + +public sealed class ChecklistItemCompletionDto +{ + public int Id { get; set; } + public int ChecklistItemId { get; set; } + public string ItemTitle { get; set; } = string.Empty; + public bool IsChecked { get; set; } + public string? Note { get; set; } +} + +public sealed class CompleteChecklistRequest +{ + public required int ChecklistId { get; set; } + public required int CompletedByUserId { get; set; } + public required string PersianDate { get; set; } + public required List ItemCompletions { get; set; } + public string? Notes { get; set; } +} + +public sealed class CompleteChecklistItemRequest +{ + public required int ChecklistItemId { get; set; } + public bool IsChecked { get; set; } + public string? Note { get; set; } +} + diff --git a/src/GreenHome.Application/DevicePostDtos.cs b/src/GreenHome.Application/DevicePostDtos.cs new file mode 100644 index 0000000..6fe21fb --- /dev/null +++ b/src/GreenHome.Application/DevicePostDtos.cs @@ -0,0 +1,46 @@ +namespace GreenHome.Application; + +public sealed class DevicePostDto +{ + public int Id { get; set; } + public int DeviceId { get; set; } + public int AuthorUserId { get; set; } + public string AuthorName { get; set; } = string.Empty; + public string AuthorFamily { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public List Images { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public sealed class DevicePostImageDto +{ + public int Id { get; set; } + public string FileName { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public long FileSize { get; set; } + public DateTime UploadedAt { get; set; } +} + +public sealed class CreateDevicePostRequest +{ + public required int DeviceId { get; set; } + public required int AuthorUserId { get; set; } + public required string Content { get; set; } +} + +public sealed class UpdateDevicePostRequest +{ + public required int Id { get; set; } + public required string Content { get; set; } +} + +public sealed class DevicePostFilter +{ + public required int DeviceId { get; set; } + public int? AuthorUserId { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; +} + diff --git a/src/GreenHome.Application/Dtos.cs b/src/GreenHome.Application/Dtos.cs index add50e0..975cf2c 100644 --- a/src/GreenHome.Application/Dtos.cs +++ b/src/GreenHome.Application/Dtos.cs @@ -123,6 +123,11 @@ public sealed class DeviceSettingsDto public decimal? Latitude { get; set; } public decimal? Longitude { get; set; } + public string ProductType { get; set; } = string.Empty; + public int MinimumSmsIntervalMinutes { get; set; } = 15; + public int MinimumCallIntervalMinutes { get; set; } = 60; + public decimal? AreaSquareMeters { get; set; } + public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } @@ -202,4 +207,109 @@ public sealed class DailyReportResponse public int TotalTokens { get; set; } public DateTime CreatedAt { get; set; } public bool FromCache { get; set; } +} + +public sealed class WeeklyAnalysisRequest +{ + public required int DeviceId { get; set; } + public required string StartDate { get; set; } // yyyy/MM/dd + public required string EndDate { get; set; } // yyyy/MM/dd +} + +public sealed class MonthlyAnalysisRequest +{ + public required int DeviceId { get; set; } + public required int Year { get; set; } + public required int Month { get; set; } +} + +public sealed class AlertLogDto +{ + public int Id { get; set; } + public int DeviceId { get; set; } + public string DeviceName { get; set; } = string.Empty; + public int UserId { get; set; } + public string UserName { get; set; } = string.Empty; + public string UserMobile { get; set; } = string.Empty; + public int? AlertConditionId { get; set; } + public Domain.AlertType AlertType { get; set; } + public Domain.AlertNotificationType NotificationType { get; set; } + public string Message { get; set; } = string.Empty; + public Domain.AlertStatus Status { get; set; } + public string? ErrorMessage { get; set; } + public string PhoneNumber { get; set; } = string.Empty; + public DateTime SentAt { get; set; } + public long ProcessingTimeMs { get; set; } +} + +public sealed class AlertLogFilter +{ + public int? DeviceId { get; set; } + public int? UserId { get; set; } + public Domain.AlertType? AlertType { get; set; } + public Domain.AlertStatus? Status { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; +} + +public sealed class UserDailyReportDto +{ + public int Id { get; set; } + public int DeviceId { get; set; } + public string DeviceName { get; set; } = string.Empty; + public int UserId { get; set; } + public string UserName { get; set; } = string.Empty; + public string UserFamily { get; set; } = string.Empty; + public string PersianDate { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Observations { get; set; } = string.Empty; + public string Operations { get; set; } = string.Empty; + public string? Notes { get; set; } + public List Images { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} + +public sealed class ReportImageDto +{ + public int Id { get; set; } + public string FileName { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public long FileSize { get; set; } + public string? Description { get; set; } + public DateTime UploadedAt { get; set; } +} + +public sealed class CreateUserDailyReportRequest +{ + public required int DeviceId { get; set; } + public required int UserId { get; set; } + public required string PersianDate { get; set; } + public required string Title { get; set; } + public required string Observations { get; set; } + public required string Operations { get; set; } + public string? Notes { get; set; } +} + +public sealed class UpdateUserDailyReportRequest +{ + public required int Id { get; set; } + public required string Title { get; set; } + public required string Observations { get; set; } + public required string Operations { get; set; } + public string? Notes { get; set; } +} + +public sealed class UserDailyReportFilter +{ + public int? DeviceId { get; set; } + public int? UserId { get; set; } + public string? PersianDate { get; set; } + public int? Year { get; set; } + public int? Month { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; } \ No newline at end of file diff --git a/src/GreenHome.Application/IAlertLogService.cs b/src/GreenHome.Application/IAlertLogService.cs new file mode 100644 index 0000000..ee065ed --- /dev/null +++ b/src/GreenHome.Application/IAlertLogService.cs @@ -0,0 +1,9 @@ +namespace GreenHome.Application; + +public interface IAlertLogService +{ + Task> GetAlertLogsAsync(AlertLogFilter filter, CancellationToken cancellationToken); + Task GetAlertLogByIdAsync(int id, CancellationToken cancellationToken); + Task CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken); +} + diff --git a/src/GreenHome.Application/IAlertService.cs b/src/GreenHome.Application/IAlertService.cs index 30ec9e3..ffd8aae 100644 --- a/src/GreenHome.Application/IAlertService.cs +++ b/src/GreenHome.Application/IAlertService.cs @@ -3,5 +3,6 @@ namespace GreenHome.Application; public interface IAlertService { Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken); + Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken); } diff --git a/src/GreenHome.Application/IChecklistService.cs b/src/GreenHome.Application/IChecklistService.cs new file mode 100644 index 0000000..e05fe61 --- /dev/null +++ b/src/GreenHome.Application/IChecklistService.cs @@ -0,0 +1,12 @@ +namespace GreenHome.Application; + +public interface IChecklistService +{ + Task GetActiveChecklistByDeviceIdAsync(int deviceId, CancellationToken cancellationToken); + Task> GetChecklistsByDeviceIdAsync(int deviceId, CancellationToken cancellationToken); + Task GetChecklistByIdAsync(int id, CancellationToken cancellationToken); + Task CreateChecklistAsync(CreateChecklistRequest request, CancellationToken cancellationToken); + Task> GetCompletionsByChecklistIdAsync(int checklistId, CancellationToken cancellationToken); + Task CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken); +} + diff --git a/src/GreenHome.Application/IDailyReportService.cs b/src/GreenHome.Application/IDailyReportService.cs index a022ccf..c2b7a14 100644 --- a/src/GreenHome.Application/IDailyReportService.cs +++ b/src/GreenHome.Application/IDailyReportService.cs @@ -9,5 +9,9 @@ public interface IDailyReportService /// Cancellation token /// Daily report with AI analysis Task GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken); + + Task GetWeeklyAnalysisAsync(WeeklyAnalysisRequest request, CancellationToken cancellationToken); + + Task GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken); } diff --git a/src/GreenHome.Application/IDevicePostService.cs b/src/GreenHome.Application/IDevicePostService.cs new file mode 100644 index 0000000..1349a72 --- /dev/null +++ b/src/GreenHome.Application/IDevicePostService.cs @@ -0,0 +1,14 @@ +namespace GreenHome.Application; + +public interface IDevicePostService +{ + Task> GetPostsAsync(DevicePostFilter filter, CancellationToken cancellationToken); + Task GetPostByIdAsync(int id, CancellationToken cancellationToken); + Task CreatePostAsync(CreateDevicePostRequest request, CancellationToken cancellationToken); + Task UpdatePostAsync(UpdateDevicePostRequest request, CancellationToken cancellationToken); + Task DeletePostAsync(int id, CancellationToken cancellationToken); + 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); +} + diff --git a/src/GreenHome.Application/IMonthlyReportService.cs b/src/GreenHome.Application/IMonthlyReportService.cs new file mode 100644 index 0000000..16911a5 --- /dev/null +++ b/src/GreenHome.Application/IMonthlyReportService.cs @@ -0,0 +1,45 @@ +namespace GreenHome.Application; + +public interface IMonthlyReportService +{ + Task GetMonthlyReportAsync(int deviceId, int year, int month, CancellationToken cancellationToken); +} + +public sealed class MonthlyReportDto +{ + public int DeviceId { get; set; } + public string DeviceName { get; set; } = string.Empty; + public int Year { get; set; } + public int Month { get; set; } + + // Alert Statistics + public int TotalAlerts { get; set; } + public int SmsAlerts { get; set; } + public int CallAlerts { get; set; } + public int SuccessfulAlerts { get; set; } + public int FailedAlerts { get; set; } + public int PowerOutageAlerts { get; set; } + + // Telemetry Statistics + public int TotalTelemetryRecords { get; set; } + public decimal AverageTemperature { get; set; } + public decimal MinTemperature { get; set; } + public decimal MaxTemperature { get; set; } + public decimal AverageHumidity { get; set; } + public decimal MinHumidity { get; set; } + public decimal MaxHumidity { get; set; } + public decimal AverageLux { get; set; } + public int AverageGasPPM { get; set; } + public int MaxGasPPM { get; set; } + + // User Activity + public int UserDailyReportsCount { get; set; } + public int ChecklistCompletionsCount { get; set; } + public int DailyAnalysesCount { get; set; } + + // Performance Summary + public string PerformanceSummary { get; set; } = string.Empty; + + public DateTime GeneratedAt { get; set; } +} + diff --git a/src/GreenHome.Application/IUserDailyReportService.cs b/src/GreenHome.Application/IUserDailyReportService.cs new file mode 100644 index 0000000..512a271 --- /dev/null +++ b/src/GreenHome.Application/IUserDailyReportService.cs @@ -0,0 +1,13 @@ +namespace GreenHome.Application; + +public interface IUserDailyReportService +{ + Task> GetReportsAsync(UserDailyReportFilter filter, CancellationToken cancellationToken); + Task GetReportByIdAsync(int id, CancellationToken cancellationToken); + Task CreateReportAsync(CreateUserDailyReportRequest request, CancellationToken cancellationToken); + Task UpdateReportAsync(UpdateUserDailyReportRequest request, CancellationToken cancellationToken); + Task DeleteReportAsync(int id, CancellationToken cancellationToken); + Task AddImageToReportAsync(int reportId, string fileName, string filePath, string contentType, long fileSize, string? description, CancellationToken cancellationToken); + Task DeleteImageAsync(int imageId, CancellationToken cancellationToken); +} + diff --git a/src/GreenHome.Application/MappingProfile.cs b/src/GreenHome.Application/MappingProfile.cs index 464900c..f4aabbf 100644 --- a/src/GreenHome.Application/MappingProfile.cs +++ b/src/GreenHome.Application/MappingProfile.cs @@ -32,5 +32,40 @@ public sealed class MappingProfile : Profile CreateMap(); CreateMap().ReverseMap(); + + CreateMap() + .ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName)) + .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name)) + .ForMember(dest => dest.UserMobile, opt => opt.MapFrom(src => src.User.Mobile)) + .ReverseMap() + .ForMember(dest => dest.Device, opt => opt.Ignore()) + .ForMember(dest => dest.User, opt => opt.Ignore()) + .ForMember(dest => dest.AlertCondition, opt => opt.Ignore()); + + CreateMap() + .ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName)) + .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name)) + .ForMember(dest => dest.UserFamily, opt => opt.MapFrom(src => src.User.Family)); + + CreateMap(); + + CreateMap() + .ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName)) + .ForMember(dest => dest.CreatedByUserName, opt => opt.MapFrom(src => src.CreatedByUser.Name + " " + src.CreatedByUser.Family)); + + CreateMap(); + + CreateMap() + .ForMember(dest => dest.ChecklistTitle, opt => opt.MapFrom(src => src.Checklist.Title)) + .ForMember(dest => dest.CompletedByUserName, opt => opt.MapFrom(src => src.CompletedByUser.Name + " " + src.CompletedByUser.Family)); + + CreateMap() + .ForMember(dest => dest.ItemTitle, opt => opt.MapFrom(src => src.ChecklistItem.Title)); + + CreateMap() + .ForMember(dest => dest.AuthorName, opt => opt.MapFrom(src => src.AuthorUser.Name)) + .ForMember(dest => dest.AuthorFamily, opt => opt.MapFrom(src => src.AuthorUser.Family)); + + CreateMap(); } } diff --git a/src/GreenHome.Domain/AlertLog.cs b/src/GreenHome.Domain/AlertLog.cs new file mode 100644 index 0000000..6f89a42 --- /dev/null +++ b/src/GreenHome.Domain/AlertLog.cs @@ -0,0 +1,110 @@ +namespace GreenHome.Domain; + +/// +/// لاگ هشدارهای ارسال شده +/// +public sealed class AlertLog +{ + public int Id { get; set; } + + /// + /// شناسه دستگاه + /// + public int DeviceId { get; set; } + public Device Device { get; set; } = null!; + + /// + /// شناسه کاربری که هشدار به او ارسال شده + /// + public int UserId { get; set; } + public User User { get; set; } = null!; + + /// + /// شناسه شرط هشدار (اگر مربوط به شرط خاصی بود) + /// + public int? AlertConditionId { get; set; } + public AlertCondition? AlertCondition { get; set; } + + /// + /// نوع هشدار (SMS, Call, PowerOutage) + /// + public AlertType AlertType { get; set; } + + /// + /// نوع اعلان (SMS یا Call) + /// + public AlertNotificationType NotificationType { get; set; } + + /// + /// پیام هشدار + /// + public string Message { get; set; } = string.Empty; + + /// + /// وضعیت ارسال + /// + public AlertStatus Status { get; set; } + + /// + /// پیام خطا (در صورت شکست) + /// + public string? ErrorMessage { get; set; } + + /// + /// شماره تماس یا پیامک + /// + public string PhoneNumber { get; set; } = string.Empty; + + /// + /// زمان ارسال + /// + public DateTime SentAt { get; set; } + + /// + /// مدت زمان پردازش (میلی‌ثانیه) + /// + public long ProcessingTimeMs { get; set; } +} + +/// +/// نوع هشدار +/// +public enum AlertType +{ + /// + /// هشدار بر اساس شرط + /// + Condition = 1, + + /// + /// هشدار قطع برق + /// + PowerOutage = 2, + + /// + /// هشدار دستی + /// + Manual = 3 +} + +/// +/// وضعیت ارسال هشدار +/// +public enum AlertStatus +{ + /// + /// با موفقیت ارسال شد + /// + Success = 1, + + /// + /// با خطا مواجه شد + /// + Failed = 2, + + /// + /// در صف ارسال + /// + Pending = 3 +} + diff --git a/src/GreenHome.Domain/AlertNotification.cs b/src/GreenHome.Domain/AlertNotification.cs index ea9d948..de7cdb1 100644 --- a/src/GreenHome.Domain/AlertNotification.cs +++ b/src/GreenHome.Domain/AlertNotification.cs @@ -7,8 +7,8 @@ public sealed class AlertNotification public Device Device { get; set; } = null!; public int UserId { get; set; } public User User { get; set; } = null!; - public int AlertConditionId { get; set; } - public AlertCondition AlertCondition { get; set; } = null!; + public int? AlertConditionId { get; set; } + public AlertCondition? AlertCondition { get; set; } public AlertNotificationType NotificationType { get; set; } // Call or SMS public string Message { get; set; } = string.Empty; public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS) diff --git a/src/GreenHome.Domain/Checklist.cs b/src/GreenHome.Domain/Checklist.cs new file mode 100644 index 0000000..275695b --- /dev/null +++ b/src/GreenHome.Domain/Checklist.cs @@ -0,0 +1,156 @@ +namespace GreenHome.Domain; + +/// +/// چک‌لیست دستگاه +/// +public sealed class Checklist +{ + public int Id { get; set; } + + /// + /// شناسه دستگاه + /// + public int DeviceId { get; set; } + public Device Device { get; set; } = null!; + + /// + /// عنوان چک‌لیست + /// + public string Title { get; set; } = string.Empty; + + /// + /// توضیحات + /// + public string? Description { get; set; } + + /// + /// آیا این چک‌لیست فعال است؟ (فقط یک چک‌لیست فعال برای هر دستگاه) + /// + public bool IsActive { get; set; } = true; + + /// + /// آیتم‌های چک‌لیست + /// + public ICollection Items { get; set; } = new List(); + + /// + /// سابقه تکمیل‌های چک‌لیست + /// + public ICollection Completions { get; set; } = new List(); + + /// + /// زمان ایجاد + /// + public DateTime CreatedAt { get; set; } + + /// + /// شناسه کاربر ایجاد کننده + /// + public int CreatedByUserId { get; set; } + public User CreatedByUser { get; set; } = null!; +} + +/// +/// آیتم چک‌لیست +/// +public sealed class ChecklistItem +{ + public int Id { get; set; } + + /// + /// شناسه چک‌لیست + /// + public int ChecklistId { get; set; } + public Checklist Checklist { get; set; } = null!; + + /// + /// عنوان آیتم + /// + public string Title { get; set; } = string.Empty; + + /// + /// توضیحات + /// + public string? Description { get; set; } + + /// + /// ترتیب نمایش + /// + public int Order { get; set; } + + /// + /// آیا اجباری است؟ + /// + public bool IsRequired { get; set; } = false; +} + +/// +/// سابقه تکمیل چک‌لیست +/// +public sealed class ChecklistCompletion +{ + public int Id { get; set; } + + /// + /// شناسه چک‌لیست + /// + public int ChecklistId { get; set; } + public Checklist Checklist { get; set; } = null!; + + /// + /// شناسه کاربر انجام دهنده + /// + public int CompletedByUserId { get; set; } + public User CompletedByUser { get; set; } = null!; + + /// + /// تاریخ شمسی تکمیل + /// + public string PersianDate { get; set; } = string.Empty; + + /// + /// آیتم‌های چک شده + /// + public ICollection ItemCompletions { get; set; } = new List(); + + /// + /// یادداشت‌های اضافی + /// + public string? Notes { get; set; } + + /// + /// زمان تکمیل + /// + public DateTime CompletedAt { get; set; } +} + +/// +/// تکمیل آیتم چک‌لیست +/// +public sealed class ChecklistItemCompletion +{ + public int Id { get; set; } + + /// + /// شناسه تکمیل چک‌لیست + /// + public int ChecklistCompletionId { get; set; } + public ChecklistCompletion ChecklistCompletion { get; set; } = null!; + + /// + /// شناسه آیتم چک‌لیست + /// + public int ChecklistItemId { get; set; } + public ChecklistItem ChecklistItem { get; set; } = null!; + + /// + /// آیا چک شده؟ + /// + public bool IsChecked { get; set; } + + /// + /// یادداشت برای این آیتم + /// + public string? Note { get; set; } +} + diff --git a/src/GreenHome.Domain/DevicePost.cs b/src/GreenHome.Domain/DevicePost.cs new file mode 100644 index 0000000..535b025 --- /dev/null +++ b/src/GreenHome.Domain/DevicePost.cs @@ -0,0 +1,81 @@ +namespace GreenHome.Domain; + +/// +/// پست‌های گروه مجازی دستگاه (تایم‌لاین مشترک) +/// +public sealed class DevicePost +{ + public int Id { get; set; } + + /// + /// شناسه دستگاه + /// + public int DeviceId { get; set; } + public Device Device { get; set; } = null!; + + /// + /// شناسه کاربر نویسنده پست + /// + public int AuthorUserId { get; set; } + public User AuthorUser { get; set; } = null!; + + /// + /// متن پست + /// + public string Content { get; set; } = string.Empty; + + /// + /// تصاویر پیوست + /// + public ICollection Images { get; set; } = new List(); + + /// + /// زمان ایجاد + /// + public DateTime CreatedAt { get; set; } + + /// + /// زمان آخرین ویرایش + /// + public DateTime? UpdatedAt { get; set; } +} + +/// +/// تصاویر پیوست پست +/// +public sealed class DevicePostImage +{ + public int Id { get; set; } + + /// + /// شناسه پست + /// + public int DevicePostId { get; set; } + public DevicePost DevicePost { get; set; } = null!; + + /// + /// نام فایل + /// + public string FileName { get; set; } = string.Empty; + + /// + /// مسیر ذخیره فایل + /// + public string FilePath { get; set; } = string.Empty; + + /// + /// نوع فایل (MIME type) + /// + public string ContentType { get; set; } = string.Empty; + + /// + /// حجم فایل (بایت) + /// + public long FileSize { get; set; } + + /// + /// زمان آپلود + /// + public DateTime UploadedAt { get; set; } +} + diff --git a/src/GreenHome.Domain/DeviceSettings.cs b/src/GreenHome.Domain/DeviceSettings.cs index a93199a..cb94240 100644 --- a/src/GreenHome.Domain/DeviceSettings.cs +++ b/src/GreenHome.Domain/DeviceSettings.cs @@ -26,6 +26,26 @@ public sealed class DeviceSettings /// public decimal? Longitude { get; set; } + /// + /// نوع محصول + /// + public string ProductType { get; set; } = string.Empty; + + /// + /// حداقل فاصله زمانی ارسال پیامک (به دقیقه) + /// + public int MinimumSmsIntervalMinutes { get; set; } = 15; + + /// + /// حداقل فاصله زمانی تماس (به دقیقه) + /// + public int MinimumCallIntervalMinutes { get; set; } = 60; + + /// + /// مساحت گلخانه (متر مربع) + /// + public decimal? AreaSquareMeters { get; set; } + public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } \ No newline at end of file diff --git a/src/GreenHome.Domain/DeviceUser.cs b/src/GreenHome.Domain/DeviceUser.cs index 06fb4c1..22f34f0 100644 --- a/src/GreenHome.Domain/DeviceUser.cs +++ b/src/GreenHome.Domain/DeviceUser.cs @@ -6,5 +6,10 @@ public sealed class DeviceUser public Device Device { get; set; } = null!; public int UserId { get; set; } public User User { get; set; } = null!; + + /// + /// آیا این کاربر باید هشدارهای این دستگاه را دریافت کند؟ + /// + public bool ReceiveAlerts { get; set; } = true; } diff --git a/src/GreenHome.Domain/UserDailyReport.cs b/src/GreenHome.Domain/UserDailyReport.cs new file mode 100644 index 0000000..2b24a58 --- /dev/null +++ b/src/GreenHome.Domain/UserDailyReport.cs @@ -0,0 +1,121 @@ +namespace GreenHome.Domain; + +/// +/// گزارش روزانه کاربر (مشاهدات و عملیات انجام شده) +/// +public sealed class UserDailyReport +{ + public int Id { get; set; } + + /// + /// شناسه دستگاه + /// + public int DeviceId { get; set; } + public Device Device { get; set; } = null!; + + /// + /// شناسه کاربر گزارش‌دهنده + /// + public int UserId { get; set; } + public User User { get; set; } = null!; + + /// + /// تاریخ شمسی (yyyy/MM/dd) + /// + public string PersianDate { get; set; } = string.Empty; + + /// + /// سال شمسی + /// + public int PersianYear { get; set; } + + /// + /// ماه شمسی + /// + public int PersianMonth { get; set; } + + /// + /// روز شمسی + /// + public int PersianDay { get; set; } + + /// + /// عنوان گزارش + /// + public string Title { get; set; } = string.Empty; + + /// + /// شرح مشاهدات + /// + public string Observations { get; set; } = string.Empty; + + /// + /// عملیات انجام شده + /// + public string Operations { get; set; } = string.Empty; + + /// + /// یادداشت‌های اضافی + /// + public string? Notes { get; set; } + + /// + /// تصاویر پیوست + /// + public ICollection Images { get; set; } = new List(); + + /// + /// زمان ایجاد + /// + public DateTime CreatedAt { get; set; } + + /// + /// زمان آخرین ویرایش + /// + public DateTime UpdatedAt { get; set; } +} + +/// +/// تصاویر پیوست گزارش روزانه +/// +public sealed class ReportImage +{ + public int Id { get; set; } + + /// + /// شناسه گزارش + /// + public int UserDailyReportId { get; set; } + public UserDailyReport UserDailyReport { get; set; } = null!; + + /// + /// نام فایل + /// + public string FileName { get; set; } = string.Empty; + + /// + /// مسیر ذخیره فایل + /// + public string FilePath { get; set; } = string.Empty; + + /// + /// نوع فایل (MIME type) + /// + public string ContentType { get; set; } = string.Empty; + + /// + /// حجم فایل (بایت) + /// + public long FileSize { get; set; } + + /// + /// توضیحات تصویر + /// + public string? Description { get; set; } + + /// + /// زمان آپلود + /// + public DateTime UploadedAt { get; set; } +} + diff --git a/src/GreenHome.Infrastructure/AlertLogService.cs b/src/GreenHome.Infrastructure/AlertLogService.cs new file mode 100644 index 0000000..2699de1 --- /dev/null +++ b/src/GreenHome.Infrastructure/AlertLogService.cs @@ -0,0 +1,100 @@ +using AutoMapper; +using GreenHome.Application; +using Microsoft.EntityFrameworkCore; + +namespace GreenHome.Infrastructure; + +public sealed class AlertLogService : IAlertLogService +{ + private readonly GreenHomeDbContext _context; + private readonly IMapper _mapper; + + public AlertLogService(GreenHomeDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> GetAlertLogsAsync( + AlertLogFilter filter, + CancellationToken cancellationToken) + { + var query = _context.AlertLogs + .Include(x => x.Device) + .Include(x => x.User) + .AsNoTracking() + .AsQueryable(); + + // Apply filters + if (filter.DeviceId.HasValue) + { + query = query.Where(x => x.DeviceId == filter.DeviceId.Value); + } + + if (filter.UserId.HasValue) + { + query = query.Where(x => x.UserId == filter.UserId.Value); + } + + if (filter.AlertType.HasValue) + { + query = query.Where(x => x.AlertType == filter.AlertType.Value); + } + + if (filter.Status.HasValue) + { + query = query.Where(x => x.Status == filter.Status.Value); + } + + if (filter.StartDate.HasValue) + { + query = query.Where(x => x.SentAt >= filter.StartDate.Value); + } + + if (filter.EndDate.HasValue) + { + query = query.Where(x => x.SentAt <= filter.EndDate.Value); + } + + // Get total count + var totalCount = await query.CountAsync(cancellationToken); + + // Apply pagination and ordering + var items = await query + .OrderByDescending(x => x.SentAt) + .Skip((filter.Page - 1) * filter.PageSize) + .Take(filter.PageSize) + .ToListAsync(cancellationToken); + + var dtos = _mapper.Map>(items); + + return new PagedResult + { + Items = dtos, + TotalCount = totalCount, + Page = filter.Page, + PageSize = filter.PageSize + }; + } + + public async Task GetAlertLogByIdAsync(int id, CancellationToken cancellationToken) + { + var log = await _context.AlertLogs + .Include(x => x.Device) + .Include(x => x.User) + .Include(x => x.AlertCondition) + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + + return log != null ? _mapper.Map(log) : null; + } + + public async Task CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken) + { + var entity = _mapper.Map(dto); + _context.AlertLogs.Add(entity); + await _context.SaveChangesAsync(cancellationToken); + return entity.Id; + } +} + diff --git a/src/GreenHome.Infrastructure/AlertService.cs b/src/GreenHome.Infrastructure/AlertService.cs index c2cb752..922a103 100644 --- a/src/GreenHome.Infrastructure/AlertService.cs +++ b/src/GreenHome.Infrastructure/AlertService.cs @@ -35,14 +35,28 @@ public sealed class AlertService : IAlertService public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken) { - // Get device with settings and user + // Get device with all users who should receive alerts var device = await dbContext.Devices .Include(d => d.User) + .Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts)) + .ThenInclude(du => du.User) .FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken); - if (device == null || device.User == null) + if (device == null) { - logger.LogWarning("Device or user not found: DeviceId={DeviceId}", deviceId); + logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId); + return; + } + + // Get all users who should receive alerts + var usersToAlert = device.DeviceUsers + .Where(du => du.ReceiveAlerts) + .Select(du => du.User) + .ToList(); + + if (usersToAlert.Count == 0) + { + logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId); return; } @@ -87,7 +101,7 @@ public sealed class AlertService : IAlertService if (allRulesMatch && condition.Rules.Any()) { // All rules passed, send alert if cooldown period has passed - await SendAlertForConditionAsync(condition, device, telemetry, cancellationToken); + await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken); } } } @@ -119,6 +133,7 @@ public sealed class AlertService : IAlertService private async Task SendAlertForConditionAsync( Domain.AlertCondition condition, Domain.Device device, + List usersToAlert, TelemetryDto telemetry, CancellationToken cancellationToken) { @@ -127,68 +142,95 @@ public sealed class AlertService : IAlertService ? condition.CallCooldownMinutes : condition.SmsCooldownMinutes; - // Check if alert was sent recently - var cooldownTime = DateTime.UtcNow.AddMinutes(-cooldownMinutes); - var recentAlert = await dbContext.AlertNotifications - .Where(a => a.DeviceId == device.Id && - a.UserId == device.User.Id && - a.AlertConditionId == condition.Id && - a.SentAt >= cooldownTime) - .FirstOrDefaultAsync(cancellationToken); - - if (recentAlert != null) - { - logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}", - device.Id, condition.Id, condition.NotificationType); - return; - } - - // Build alert message + // Build alert message once var message = BuildAlertMessage(condition, device.DeviceName, telemetry); + var sentAt = DateTime.UtcNow; - // Send notification - string? messageOutboxIds = null; - string? errorMessage = null; - bool isSent = false; + // Send alert to each user + foreach (var user in usersToAlert) + { + // Check if alert was sent recently to this user + var cooldownTime = sentAt.AddMinutes(-cooldownMinutes); + var recentAlert = await dbContext.AlertNotifications + .Where(a => a.DeviceId == device.Id && + a.UserId == user.Id && + a.AlertConditionId == condition.Id && + a.SentAt >= cooldownTime) + .FirstOrDefaultAsync(cancellationToken); - try - { - if (condition.NotificationType == Domain.AlertNotificationType.SMS) + if (recentAlert != null) { - (isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken); + logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}", + device.Id, user.Id, condition.Id); + continue; } - else // Call + + // Send notification + var startTime = DateTime.UtcNow; + string? messageOutboxIds = null; + string? errorMessage = null; + bool isSent = false; + + try { - (isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken); + if (condition.NotificationType == Domain.AlertNotificationType.SMS) + { + (isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken); + } + else // Call + { + (isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken); + } } - } - catch (Exception ex) - { - errorMessage = $"Exception: {ex.Message}"; - if (ex.InnerException != null) + catch (Exception ex) { - errorMessage += $" | InnerException: {ex.InnerException.Message}"; + errorMessage = $"Exception: {ex.Message}"; + if (ex.InnerException != null) + { + errorMessage += $" | InnerException: {ex.InnerException.Message}"; + } + isSent = false; + logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}", + device.Id, user.Id, condition.Id); } - isSent = false; - logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}", - device.Id, condition.Id, condition.NotificationType); + + var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds; + + // Save notification to database (old table for backwards compatibility) + var notification = new Domain.AlertNotification + { + DeviceId = device.Id, + UserId = user.Id, + AlertConditionId = condition.Id, + NotificationType = condition.NotificationType, + Message = message, + MessageOutboxIds = messageOutboxIds, + ErrorMessage = errorMessage, + SentAt = sentAt, + IsSent = isSent + }; + + dbContext.AlertNotifications.Add(notification); + + // Log the alert + var alertLog = new Domain.AlertLog + { + DeviceId = device.Id, + UserId = user.Id, + AlertConditionId = condition.Id, + AlertType = Domain.AlertType.Condition, + NotificationType = condition.NotificationType, + Message = message, + Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed, + ErrorMessage = errorMessage, + PhoneNumber = user.Mobile, + SentAt = sentAt, + ProcessingTimeMs = processingTime + }; + + dbContext.AlertLogs.Add(alertLog); } - // Save notification to database - var notification = new Domain.AlertNotification - { - DeviceId = device.Id, - UserId = device.User.Id, - AlertConditionId = condition.Id, - NotificationType = condition.NotificationType, - Message = message, - MessageOutboxIds = messageOutboxIds, - ErrorMessage = errorMessage, - SentAt = DateTime.UtcNow, - IsSent = isSent - }; - - dbContext.AlertNotifications.Add(notification); await dbContext.SaveChangesAsync(cancellationToken); } @@ -323,5 +365,131 @@ public sealed class AlertService : IAlertService return (false, null, errorMsg); } } + + public async Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken) + { + // Get device with all users who should receive alerts + var device = await dbContext.Devices + .Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts)) + .ThenInclude(du => du.User) + .FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken); + + if (device == null) + { + logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId); + throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد"); + } + + // Get all users who should receive alerts + var usersToAlert = device.DeviceUsers + .Where(du => du.ReceiveAlerts) + .Select(du => du.User) + .ToList(); + + if (usersToAlert.Count == 0) + { + logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId); + return; + } + + var message = $"⚠️ هشدار قطع برق! دستگاه {device.DeviceName} از برق قطع شده است."; + var sentAt = DateTime.UtcNow; + + // Send to all users (both SMS and Call for power outage - it's critical!) + foreach (var user in usersToAlert) + { + // Send SMS + await SendPowerOutageNotificationAsync( + device, user, message, sentAt, + Domain.AlertNotificationType.SMS, + cancellationToken); + + // Send Call (important alert) + await SendPowerOutageNotificationAsync( + device, user, message, sentAt, + Domain.AlertNotificationType.Call, + cancellationToken); + } + + await dbContext.SaveChangesAsync(cancellationToken); + + logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}", + usersToAlert.Count, deviceId); + } + + private async Task SendPowerOutageNotificationAsync( + Domain.Device device, + Domain.User user, + string message, + DateTime sentAt, + Domain.AlertNotificationType notificationType, + CancellationToken cancellationToken) + { + var startTime = DateTime.UtcNow; + string? messageOutboxIds = null; + string? errorMessage = null; + bool isSent = false; + + try + { + if (notificationType == Domain.AlertNotificationType.SMS) + { + (isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync( + user.Mobile, device.DeviceName, message, cancellationToken); + } + else + { + (isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync( + user.Mobile, device.DeviceName, message, cancellationToken); + } + } + catch (Exception ex) + { + errorMessage = $"Exception: {ex.Message}"; + if (ex.InnerException != null) + { + errorMessage += $" | InnerException: {ex.InnerException.Message}"; + } + isSent = false; + logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}", + device.Id, user.Id, notificationType); + } + + var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds; + + // Save notification (old table) + var notification = new Domain.AlertNotification + { + DeviceId = device.Id, + UserId = user.Id, + AlertConditionId = null, + NotificationType = notificationType, + Message = message, + MessageOutboxIds = messageOutboxIds, + ErrorMessage = errorMessage, + SentAt = sentAt, + IsSent = isSent + }; + + dbContext.AlertNotifications.Add(notification); + + // Log the alert + var alertLog = new Domain.AlertLog + { + DeviceId = device.Id, + UserId = user.Id, + AlertConditionId = null, + AlertType = Domain.AlertType.PowerOutage, + NotificationType = notificationType, + Message = message, + Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed, + ErrorMessage = errorMessage, + PhoneNumber = user.Mobile, + SentAt = sentAt, + ProcessingTimeMs = processingTime + }; + + dbContext.AlertLogs.Add(alertLog); + } } diff --git a/src/GreenHome.Infrastructure/ChecklistService.cs b/src/GreenHome.Infrastructure/ChecklistService.cs new file mode 100644 index 0000000..a15f45e --- /dev/null +++ b/src/GreenHome.Infrastructure/ChecklistService.cs @@ -0,0 +1,152 @@ +using AutoMapper; +using GreenHome.Application; +using Microsoft.EntityFrameworkCore; + +namespace GreenHome.Infrastructure; + +public sealed class ChecklistService : IChecklistService +{ + private readonly GreenHomeDbContext _context; + private readonly IMapper _mapper; + + public ChecklistService(GreenHomeDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task GetActiveChecklistByDeviceIdAsync(int deviceId, CancellationToken cancellationToken) + { + var checklist = await _context.Checklists + .Include(c => c.Device) + .Include(c => c.CreatedByUser) + .Include(c => c.Items.OrderBy(i => i.Order)) + .AsNoTracking() + .FirstOrDefaultAsync(c => c.DeviceId == deviceId && c.IsActive, cancellationToken); + + return checklist != null ? _mapper.Map(checklist) : null; + } + + public async Task> GetChecklistsByDeviceIdAsync(int deviceId, CancellationToken cancellationToken) + { + var checklists = await _context.Checklists + .Include(c => c.Device) + .Include(c => c.CreatedByUser) + .Include(c => c.Items.OrderBy(i => i.Order)) + .AsNoTracking() + .Where(c => c.DeviceId == deviceId) + .OrderByDescending(c => c.IsActive) + .ThenByDescending(c => c.CreatedAt) + .ToListAsync(cancellationToken); + + return _mapper.Map>(checklists); + } + + public async Task GetChecklistByIdAsync(int id, CancellationToken cancellationToken) + { + var checklist = await _context.Checklists + .Include(c => c.Device) + .Include(c => c.CreatedByUser) + .Include(c => c.Items.OrderBy(i => i.Order)) + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + + return checklist != null ? _mapper.Map(checklist) : null; + } + + public async Task CreateChecklistAsync(CreateChecklistRequest request, CancellationToken cancellationToken) + { + // Deactivate existing active checklist for this device + var existingActiveChecklist = await _context.Checklists + .FirstOrDefaultAsync(c => c.DeviceId == request.DeviceId && c.IsActive, cancellationToken); + + if (existingActiveChecklist != null) + { + existingActiveChecklist.IsActive = false; + } + + // Create new checklist + var checklist = new Domain.Checklist + { + DeviceId = request.DeviceId, + CreatedByUserId = request.CreatedByUserId, + Title = request.Title, + Description = request.Description, + IsActive = true, + CreatedAt = DateTime.UtcNow + }; + + // Add items + foreach (var itemRequest in request.Items) + { + var item = new Domain.ChecklistItem + { + Title = itemRequest.Title, + Description = itemRequest.Description, + Order = itemRequest.Order, + IsRequired = itemRequest.IsRequired + }; + checklist.Items.Add(item); + } + + _context.Checklists.Add(checklist); + await _context.SaveChangesAsync(cancellationToken); + + return checklist.Id; + } + + public async Task> GetCompletionsByChecklistIdAsync( + int checklistId, + CancellationToken cancellationToken) + { + var completions = await _context.ChecklistCompletions + .Include(cc => cc.Checklist) + .Include(cc => cc.CompletedByUser) + .Include(cc => cc.ItemCompletions) + .ThenInclude(ic => ic.ChecklistItem) + .AsNoTracking() + .Where(cc => cc.ChecklistId == checklistId) + .OrderByDescending(cc => cc.CompletedAt) + .ToListAsync(cancellationToken); + + return _mapper.Map>(completions); + } + + public async Task CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken) + { + var checklist = await _context.Checklists + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.Id == request.ChecklistId, cancellationToken); + + if (checklist == null) + { + throw new InvalidOperationException($"چک‌لیست با شناسه {request.ChecklistId} یافت نشد"); + } + + var completion = new Domain.ChecklistCompletion + { + ChecklistId = request.ChecklistId, + CompletedByUserId = request.CompletedByUserId, + PersianDate = request.PersianDate, + Notes = request.Notes, + CompletedAt = DateTime.UtcNow + }; + + foreach (var itemCompletion in request.ItemCompletions) + { + var item = new Domain.ChecklistItemCompletion + { + ChecklistItemId = itemCompletion.ChecklistItemId, + IsChecked = itemCompletion.IsChecked, + Note = itemCompletion.Note + }; + completion.ItemCompletions.Add(item); + } + + _context.ChecklistCompletions.Add(completion); + await _context.SaveChangesAsync(cancellationToken); + + return completion.Id; + } +} + diff --git a/src/GreenHome.Infrastructure/DailyReportService.cs b/src/GreenHome.Infrastructure/DailyReportService.cs index ddd113f..b6154d0 100644 --- a/src/GreenHome.Infrastructure/DailyReportService.cs +++ b/src/GreenHome.Infrastructure/DailyReportService.cs @@ -70,6 +70,10 @@ public class DailyReportService : IDailyReportService throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد"); } + // Get device settings (including ProductType if available) + var deviceSettings = await _context.DeviceSettings + .FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken); + // Query telemetry data for the specified date var telemetryRecords = await _context.TelemetryRecords .Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate) @@ -115,7 +119,15 @@ public class DailyReportService : IDailyReportService } // Prepare the question for AI - var question = $@"این داده‌های تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند: + var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType) + ? $" محصول کشت شده: {deviceSettings.ProductType}." + : string.Empty; + + var areaInfo = deviceSettings?.AreaSquareMeters != null + ? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع." + : string.Empty; + + var question = $@"این داده‌های تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo} {dataBuilder} @@ -123,7 +135,7 @@ public class DailyReportService : IDailyReportService 1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا 2. روندهای مشاهده شده در طول روز 3. هر گونه نکته یا هشدار مهم -4. پیشنهادات برای بهبود شرایط گلخانه +4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)} خلاصه و مفید باش (حداکثر 300 کلمه)."; @@ -133,12 +145,16 @@ public class DailyReportService : IDailyReportService try { + var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType) + ? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی." + : "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی."; + var chatRequest = new ChatRequest { Model = "deepseek-chat", Messages = new List { - new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی." }, + new() { Role = "system", Content = systemMessage }, new() { Role = "user", Content = question } }, Temperature = 0.7 @@ -225,5 +241,239 @@ public class DailyReportService : IDailyReportService return true; } + + public async Task GetWeeklyAnalysisAsync( + WeeklyAnalysisRequest request, + CancellationToken cancellationToken) + { + // Get device info + var device = await _context.Devices + .FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken); + + if (device == null) + { + throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد"); + } + + // Get device settings + var deviceSettings = await _context.DeviceSettings + .FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken); + + // Query telemetry data for the week + var telemetryRecords = await _context.TelemetryRecords + .Where(t => t.DeviceId == request.DeviceId && + string.Compare(t.PersianDate, request.StartDate) >= 0 && + string.Compare(t.PersianDate, request.EndDate) <= 0) + .OrderBy(t => t.TimestampUtc) + .Select(t => new + { + t.TimestampUtc, + t.TemperatureC, + t.HumidityPercent, + t.Lux, + t.GasPPM, + t.PersianDate + }) + .ToListAsync(cancellationToken); + + if (telemetryRecords.Count == 0) + { + throw new InvalidOperationException( + $"هیچ رکوردی برای دستگاه {request.DeviceId} در بازه {request.StartDate} تا {request.EndDate} یافت نشد"); + } + + // Sample 1 per 100 records + var sampledRecords = telemetryRecords + .Select((record, index) => new { record, index }) + .Where(x => x.index % 100 == 0) + .Select(x => x.record) + .ToList(); + + _logger.LogInformation( + "تعداد {TotalCount} رکورد یافت شد. نمونه‌برداری هفتگی: {SampledCount} رکورد", + telemetryRecords.Count, sampledRecords.Count); + + // Build the data string + var dataBuilder = new StringBuilder(); + dataBuilder.AppendLine("تاریخ | زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)"); + dataBuilder.AppendLine("---------|----------|----------|-----------|-----------|----------"); + + foreach (var record in sampledRecords) + { + var localTime = record.TimestampUtc.AddHours(3.5); + dataBuilder.AppendLine( + $"{record.PersianDate} | {localTime:HH:mm} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}"); + } + + // Prepare AI prompt + var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType) + ? $" محصول کشت شده: {deviceSettings.ProductType}." + : string.Empty; + + var areaInfo = deviceSettings?.AreaSquareMeters != null + ? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع." + : string.Empty; + + var question = $@"این داده‌های تلمتری یک هفته ({request.StartDate} تا {request.EndDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo} + +{dataBuilder} + +لطفاً یک تحلیل جامع هفتگی بده که شامل: +1. خلاصه روند هفتگی دما، رطوبت، نور و کیفیت هوا +2. مقایسه شرایط در روزهای مختلف هفته +3. نکات و هشدارهای مهم +4. توصیه‌ها برای هفته آینده + +خلاصه و کاربردی باش (حداکثر 500 کلمه)."; + + var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType) + ? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی." + : "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی."; + + // Send to DeepSeek + var stopwatch = Stopwatch.StartNew(); + var chatRequest = new ChatRequest + { + Model = "deepseek-chat", + Messages = new List + { + new() { Role = "system", Content = systemMessage }, + new() { Role = "user", Content = question } + }, + Temperature = 0.7 + }; + + var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken); + stopwatch.Stop(); + + if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 || + string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content)) + { + throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی"); + } + + return new DailyReportResponse + { + Id = 0, + DeviceId = request.DeviceId, + DeviceName = device.DeviceName, + PersianDate = $"{request.StartDate} تا {request.EndDate}", + Analysis = aiResponse.Choices[0].Message!.Content, + RecordCount = telemetryRecords.Count, + SampledRecordCount = sampledRecords.Count, + TotalTokens = aiResponse.Usage?.TotalTokens ?? 0, + CreatedAt = DateTime.UtcNow, + FromCache = false + }; + } + + public async Task GetMonthlyAnalysisAsync( + MonthlyAnalysisRequest request, + CancellationToken cancellationToken) + { + // Get device info + var device = await _context.Devices + .FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken); + + if (device == null) + { + throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد"); + } + + // Get device settings + var deviceSettings = await _context.DeviceSettings + .FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken); + + // Get all daily reports for this month + var dailyReports = await _context.DailyReports + .Where(dr => dr.DeviceId == request.DeviceId && + dr.PersianYear == request.Year && + dr.PersianMonth == request.Month) + .OrderBy(dr => dr.PersianDay) + .Select(dr => new { dr.PersianDate, dr.Analysis }) + .ToListAsync(cancellationToken); + + if (dailyReports.Count == 0) + { + throw new InvalidOperationException( + $"هیچ تحلیل روزانه‌ای برای دستگاه {request.DeviceId} در ماه {request.Month} سال {request.Year} یافت نشد"); + } + + // Build summary of daily analyses + var summaryBuilder = new StringBuilder(); + summaryBuilder.AppendLine($"تحلیل‌های روزانه ماه {request.Month} سال {request.Year}:"); + summaryBuilder.AppendLine(); + + foreach (var report in dailyReports) + { + summaryBuilder.AppendLine($"📅 {report.PersianDate}:"); + summaryBuilder.AppendLine(report.Analysis); + summaryBuilder.AppendLine(); + summaryBuilder.AppendLine("---"); + summaryBuilder.AppendLine(); + } + + // Prepare AI prompt + var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType) + ? $" محصول کشت شده: {deviceSettings.ProductType}." + : string.Empty; + + var areaInfo = deviceSettings?.AreaSquareMeters != null + ? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع." + : string.Empty; + + var question = $@"این تحلیل‌های روزانه یک ماه ({request.Month}/{request.Year}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo} + +{summaryBuilder} + +لطفاً یک تحلیل جامع ماهانه بده که شامل: +1. خلاصه کلی عملکرد ماه +2. روندهای اصلی و تغییرات مهم +3. نقاط قوت و ضعف +4. توصیه‌های کلیدی برای ماه آینده +5. نکات مهم برای بهبود بهره‌وری + +جامع و کاربردی باش (حداکثر 800 کلمه)."; + + var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType) + ? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری. تحلیل‌های روزانه رو بررسی کن و یک جمع‌بندی ماهانه جامع ارائه بده." + : "تو یک متخصص کشاورزی و گلخانه هستی. تحلیل‌های روزانه رو بررسی کن و یک جمع‌بندی ماهانه جامع ارائه بده."; + + // Send to DeepSeek + var stopwatch = Stopwatch.StartNew(); + var chatRequest = new ChatRequest + { + Model = "deepseek-chat", + Messages = new List + { + new() { Role = "system", Content = systemMessage }, + new() { Role = "user", Content = question } + }, + Temperature = 0.7 + }; + + var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken); + stopwatch.Stop(); + + if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 || + string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content)) + { + throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی"); + } + + return new DailyReportResponse + { + Id = 0, + DeviceId = request.DeviceId, + DeviceName = device.DeviceName, + PersianDate = $"ماه {request.Month} سال {request.Year}", + Analysis = aiResponse.Choices[0].Message!.Content, + RecordCount = dailyReports.Count, + SampledRecordCount = dailyReports.Count, + TotalTokens = aiResponse.Usage?.TotalTokens ?? 0, + CreatedAt = DateTime.UtcNow, + FromCache = false + }; + } } diff --git a/src/GreenHome.Infrastructure/DevicePostService.cs b/src/GreenHome.Infrastructure/DevicePostService.cs new file mode 100644 index 0000000..39b9139 --- /dev/null +++ b/src/GreenHome.Infrastructure/DevicePostService.cs @@ -0,0 +1,185 @@ +using AutoMapper; +using GreenHome.Application; +using Microsoft.EntityFrameworkCore; + +namespace GreenHome.Infrastructure; + +public sealed class DevicePostService : IDevicePostService +{ + private readonly GreenHomeDbContext _context; + private readonly IMapper _mapper; + + public DevicePostService(GreenHomeDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> GetPostsAsync( + DevicePostFilter filter, + CancellationToken cancellationToken) + { + var query = _context.DevicePosts + .Include(p => p.AuthorUser) + .Include(p => p.Images) + .AsNoTracking() + .Where(p => p.DeviceId == filter.DeviceId); + + if (filter.AuthorUserId.HasValue) + { + query = query.Where(p => p.AuthorUserId == filter.AuthorUserId.Value); + } + + var totalCount = await query.CountAsync(cancellationToken); + + var posts = await query + .OrderByDescending(p => p.CreatedAt) + .Skip((filter.Page - 1) * filter.PageSize) + .Take(filter.PageSize) + .ToListAsync(cancellationToken); + + var dtos = _mapper.Map>(posts); + + return new PagedResult + { + Items = dtos, + TotalCount = totalCount, + Page = filter.Page, + PageSize = filter.PageSize + }; + } + + public async Task GetPostByIdAsync(int id, CancellationToken cancellationToken) + { + var post = await _context.DevicePosts + .Include(p => p.AuthorUser) + .Include(p => p.Images) + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + + return post != null ? _mapper.Map(post) : null; + } + + public async Task CreatePostAsync( + CreateDevicePostRequest request, + CancellationToken cancellationToken) + { + // Verify user has access to device + var hasAccess = await CanUserAccessDeviceAsync(request.AuthorUserId, request.DeviceId, cancellationToken); + if (!hasAccess) + { + throw new UnauthorizedAccessException("کاربر به این دستگاه دسترسی ندارد"); + } + + var post = new Domain.DevicePost + { + DeviceId = request.DeviceId, + AuthorUserId = request.AuthorUserId, + Content = request.Content, + CreatedAt = DateTime.UtcNow + }; + + _context.DevicePosts.Add(post); + await _context.SaveChangesAsync(cancellationToken); + + return post.Id; + } + + public async Task UpdatePostAsync( + UpdateDevicePostRequest request, + CancellationToken cancellationToken) + { + var post = await _context.DevicePosts + .FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken); + + if (post == null) + { + throw new InvalidOperationException($"پست با شناسه {request.Id} یافت نشد"); + } + + post.Content = request.Content; + post.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task DeletePostAsync(int id, CancellationToken cancellationToken) + { + var post = await _context.DevicePosts + .Include(p => p.Images) + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + + if (post == null) + { + throw new InvalidOperationException($"پست با شناسه {id} یافت نشد"); + } + + _context.DevicePosts.Remove(post); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task AddImageToPostAsync( + int postId, + string fileName, + string filePath, + string contentType, + long fileSize, + CancellationToken cancellationToken) + { + var post = await _context.DevicePosts + .FirstOrDefaultAsync(p => p.Id == postId, cancellationToken); + + if (post == null) + { + throw new InvalidOperationException($"پست با شناسه {postId} یافت نشد"); + } + + var image = new Domain.DevicePostImage + { + DevicePostId = postId, + FileName = fileName, + FilePath = filePath, + ContentType = contentType, + FileSize = fileSize, + UploadedAt = DateTime.UtcNow + }; + + _context.DevicePostImages.Add(image); + await _context.SaveChangesAsync(cancellationToken); + + return image.Id; + } + + public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken) + { + var image = await _context.DevicePostImages + .FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken); + + if (image == null) + { + throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد"); + } + + _context.DevicePostImages.Remove(image); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task CanUserAccessDeviceAsync( + int userId, + int deviceId, + CancellationToken cancellationToken) + { + // Check if user is the device owner or has access through DeviceUsers + var hasAccess = await _context.Devices + .AnyAsync(d => d.Id == deviceId && d.UserId == userId, cancellationToken); + + if (!hasAccess) + { + hasAccess = await _context.DeviceUsers + .AnyAsync(du => du.DeviceId == deviceId && du.UserId == userId, cancellationToken); + } + + return hasAccess; + } +} + diff --git a/src/GreenHome.Infrastructure/GreenHomeDbContext.cs b/src/GreenHome.Infrastructure/GreenHomeDbContext.cs index 5fe4901..fc07242 100644 --- a/src/GreenHome.Infrastructure/GreenHomeDbContext.cs +++ b/src/GreenHome.Infrastructure/GreenHomeDbContext.cs @@ -15,8 +15,17 @@ public sealed class GreenHomeDbContext : DbContext public DbSet VerificationCodes => Set(); public DbSet DeviceUsers => Set(); public DbSet AlertNotifications => Set(); + public DbSet AlertLogs => Set(); public DbSet AIQueries => Set(); public DbSet DailyReports => Set(); + public DbSet UserDailyReports => Set(); + public DbSet ReportImages => Set(); + public DbSet Checklists => Set(); + public DbSet ChecklistItems => Set(); + public DbSet ChecklistCompletions => Set(); + public DbSet ChecklistItemCompletions => Set(); + public DbSet DevicePosts => Set(); + public DbSet DevicePostImages => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -58,6 +67,10 @@ public sealed class GreenHomeDbContext : DbContext b.Property(x => x.City).HasMaxLength(100); b.Property(x => x.Latitude).HasColumnType("decimal(9,6)"); b.Property(x => x.Longitude).HasColumnType("decimal(9,6)"); + b.Property(x => x.ProductType).HasMaxLength(100); + b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15); + b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60); + b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)"); b.HasOne(x => x.Device) .WithMany() .HasForeignKey(x => x.DeviceId) @@ -112,6 +125,7 @@ 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.HasOne(x => x.Device) .WithMany(d => d.DeviceUsers) .HasForeignKey(x => x.DeviceId) @@ -189,5 +203,169 @@ public sealed class GreenHomeDbContext : DbContext b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); b.HasIndex(x => x.CreatedAt); }); + + modelBuilder.Entity(b => + { + b.ToTable("AlertLogs"); + b.HasKey(x => x.Id); + b.Property(x => x.AlertType).IsRequired().HasConversion(); + b.Property(x => x.NotificationType).IsRequired().HasConversion(); + b.Property(x => x.Message).IsRequired().HasMaxLength(1000); + b.Property(x => x.Status).IsRequired().HasConversion(); + b.Property(x => x.ErrorMessage).HasMaxLength(2000); + b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20); + b.HasOne(x => x.Device) + .WithMany() + .HasForeignKey(x => x.DeviceId) + .OnDelete(DeleteBehavior.Restrict); + b.HasOne(x => x.User) + .WithMany() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.Restrict); + b.HasOne(x => x.AlertCondition) + .WithMany() + .HasForeignKey(x => x.AlertConditionId) + .OnDelete(DeleteBehavior.SetNull); + b.HasIndex(x => new { x.DeviceId, x.SentAt }); + b.HasIndex(x => new { x.UserId, x.SentAt }); + b.HasIndex(x => x.AlertType); + b.HasIndex(x => x.Status); + }); + + modelBuilder.Entity(b => + { + b.ToTable("UserDailyReports"); + b.HasKey(x => x.Id); + b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10); + b.Property(x => x.Title).IsRequired().HasMaxLength(200); + b.Property(x => x.Observations).IsRequired(); + b.Property(x => x.Operations).IsRequired(); + b.Property(x => x.Notes).HasMaxLength(2000); + b.HasOne(x => x.Device) + .WithMany() + .HasForeignKey(x => x.DeviceId) + .OnDelete(DeleteBehavior.Cascade); + b.HasOne(x => x.User) + .WithMany() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.Restrict); + b.HasIndex(x => new { x.DeviceId, x.PersianDate }); + b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); + b.HasIndex(x => x.UserId); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ReportImages"); + b.HasKey(x => x.Id); + b.Property(x => x.FileName).IsRequired().HasMaxLength(255); + b.Property(x => x.FilePath).IsRequired().HasMaxLength(500); + b.Property(x => x.ContentType).IsRequired().HasMaxLength(100); + b.Property(x => x.Description).HasMaxLength(500); + b.HasOne(x => x.UserDailyReport) + .WithMany(r => r.Images) + .HasForeignKey(x => x.UserDailyReportId) + .OnDelete(DeleteBehavior.Cascade); + b.HasIndex(x => x.UserDailyReportId); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Checklists"); + b.HasKey(x => x.Id); + b.Property(x => x.Title).IsRequired().HasMaxLength(200); + b.Property(x => x.Description).HasMaxLength(1000); + b.Property(x => x.IsActive).IsRequired(); + b.HasOne(x => x.Device) + .WithMany() + .HasForeignKey(x => x.DeviceId) + .OnDelete(DeleteBehavior.Cascade); + b.HasOne(x => x.CreatedByUser) + .WithMany() + .HasForeignKey(x => x.CreatedByUserId) + .OnDelete(DeleteBehavior.Restrict); + b.HasIndex(x => new { x.DeviceId, x.IsActive }); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ChecklistItems"); + b.HasKey(x => x.Id); + b.Property(x => x.Title).IsRequired().HasMaxLength(300); + b.Property(x => x.Description).HasMaxLength(1000); + b.Property(x => x.Order).IsRequired(); + b.Property(x => x.IsRequired).IsRequired(); + b.HasOne(x => x.Checklist) + .WithMany(c => c.Items) + .HasForeignKey(x => x.ChecklistId) + .OnDelete(DeleteBehavior.Cascade); + b.HasIndex(x => new { x.ChecklistId, x.Order }); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ChecklistCompletions"); + b.HasKey(x => x.Id); + b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10); + b.Property(x => x.Notes).HasMaxLength(2000); + b.HasOne(x => x.Checklist) + .WithMany(c => c.Completions) + .HasForeignKey(x => x.ChecklistId) + .OnDelete(DeleteBehavior.Cascade); + b.HasOne(x => x.CompletedByUser) + .WithMany() + .HasForeignKey(x => x.CompletedByUserId) + .OnDelete(DeleteBehavior.Restrict); + b.HasIndex(x => new { x.ChecklistId, x.PersianDate }); + b.HasIndex(x => x.CompletedByUserId); + }); + + modelBuilder.Entity(b => + { + b.ToTable("ChecklistItemCompletions"); + b.HasKey(x => x.Id); + b.Property(x => x.IsChecked).IsRequired(); + b.Property(x => x.Note).HasMaxLength(500); + b.HasOne(x => x.ChecklistCompletion) + .WithMany(cc => cc.ItemCompletions) + .HasForeignKey(x => x.ChecklistCompletionId) + .OnDelete(DeleteBehavior.Cascade); + b.HasOne(x => x.ChecklistItem) + .WithMany() + .HasForeignKey(x => x.ChecklistItemId) + .OnDelete(DeleteBehavior.Restrict); + b.HasIndex(x => x.ChecklistCompletionId); + }); + + modelBuilder.Entity(b => + { + b.ToTable("DevicePosts"); + b.HasKey(x => x.Id); + b.Property(x => x.Content).IsRequired().HasMaxLength(5000); + b.HasOne(x => x.Device) + .WithMany() + .HasForeignKey(x => x.DeviceId) + .OnDelete(DeleteBehavior.Cascade); + b.HasOne(x => x.AuthorUser) + .WithMany() + .HasForeignKey(x => x.AuthorUserId) + .OnDelete(DeleteBehavior.Restrict); + b.HasIndex(x => new { x.DeviceId, x.CreatedAt }); + b.HasIndex(x => x.AuthorUserId); + }); + + modelBuilder.Entity(b => + { + b.ToTable("DevicePostImages"); + b.HasKey(x => x.Id); + b.Property(x => x.FileName).IsRequired().HasMaxLength(255); + b.Property(x => x.FilePath).IsRequired().HasMaxLength(500); + b.Property(x => x.ContentType).IsRequired().HasMaxLength(100); + b.HasOne(x => x.DevicePost) + .WithMany(p => p.Images) + .HasForeignKey(x => x.DevicePostId) + .OnDelete(DeleteBehavior.Cascade); + b.HasIndex(x => x.DevicePostId); + }); } } diff --git a/src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs b/src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs new file mode 100644 index 0000000..d795020 --- /dev/null +++ b/src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs @@ -0,0 +1,1199 @@ +// +using System; +using GreenHome.Infrastructure; +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.Migrations +{ + [DbContext(typeof(GreenHomeDbContext))] + [Migration("20251216204600_AddAllNewFeatures")] + partial class AddAllNewFeatures + { + /// + 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("UpdatedAt") + .HasColumnType("datetime2"); + + 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.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("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("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceSettings", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("ReceiveAlerts") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.HasKey("DeviceId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("DeviceUsers", (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("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("ServerTimestampUtc") + .HasColumnType("datetime2"); + + b.Property("SoilPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("TemperatureC") + .HasColumnType("decimal(18,2)"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + 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.DeviceSettings", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.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("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Navigation("Images"); + }); + + 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/Migrations/20251216204600_AddAllNewFeatures.cs b/src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.cs new file mode 100644 index 0000000..1448676 --- /dev/null +++ b/src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.cs @@ -0,0 +1,479 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GreenHome.Infrastructure.Migrations +{ + /// + public partial class AddAllNewFeatures : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ReceiveAlerts", + table: "DeviceUsers", + type: "bit", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "AreaSquareMeters", + table: "DeviceSettings", + type: "decimal(18,2)", + nullable: true); + + migrationBuilder.AddColumn( + name: "MinimumCallIntervalMinutes", + table: "DeviceSettings", + type: "int", + nullable: false, + defaultValue: 60); + + migrationBuilder.AddColumn( + name: "MinimumSmsIntervalMinutes", + table: "DeviceSettings", + type: "int", + nullable: false, + defaultValue: 15); + + migrationBuilder.AddColumn( + name: "ProductType", + table: "DeviceSettings", + type: "nvarchar(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.AlterColumn( + name: "AlertConditionId", + table: "AlertNotifications", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.CreateTable( + name: "AlertLogs", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceId = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "int", nullable: false), + AlertConditionId = table.Column(type: "int", nullable: true), + AlertType = table.Column(type: "int", nullable: false), + NotificationType = table.Column(type: "int", nullable: false), + Message = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false), + Status = table.Column(type: "int", nullable: false), + ErrorMessage = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + PhoneNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + SentAt = table.Column(type: "datetime2", nullable: false), + ProcessingTimeMs = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AlertLogs", x => x.Id); + table.ForeignKey( + name: "FK_AlertLogs_AlertConditions_AlertConditionId", + column: x => x.AlertConditionId, + principalTable: "AlertConditions", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_AlertLogs_Devices_DeviceId", + column: x => x.DeviceId, + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_AlertLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Checklists", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceId = table.Column(type: "int", nullable: false), + Title = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + IsActive = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + CreatedByUserId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Checklists", x => x.Id); + table.ForeignKey( + name: "FK_Checklists_Devices_DeviceId", + column: x => x.DeviceId, + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Checklists_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "DevicePosts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceId = table.Column(type: "int", nullable: false), + AuthorUserId = table.Column(type: "int", nullable: false), + Content = table.Column(type: "nvarchar(max)", maxLength: 5000, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DevicePosts", x => x.Id); + table.ForeignKey( + name: "FK_DevicePosts_Devices_DeviceId", + column: x => x.DeviceId, + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DevicePosts_Users_AuthorUserId", + column: x => x.AuthorUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UserDailyReports", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceId = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "int", nullable: false), + PersianDate = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + PersianYear = table.Column(type: "int", nullable: false), + PersianMonth = table.Column(type: "int", nullable: false), + PersianDay = table.Column(type: "int", nullable: false), + Title = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Observations = table.Column(type: "nvarchar(max)", nullable: false), + Operations = table.Column(type: "nvarchar(max)", nullable: false), + Notes = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserDailyReports", x => x.Id); + table.ForeignKey( + name: "FK_UserDailyReports_Devices_DeviceId", + column: x => x.DeviceId, + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserDailyReports_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ChecklistCompletions", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ChecklistId = table.Column(type: "int", nullable: false), + CompletedByUserId = table.Column(type: "int", nullable: false), + PersianDate = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + Notes = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + CompletedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChecklistCompletions", x => x.Id); + table.ForeignKey( + name: "FK_ChecklistCompletions_Checklists_ChecklistId", + column: x => x.ChecklistId, + principalTable: "Checklists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ChecklistCompletions_Users_CompletedByUserId", + column: x => x.CompletedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ChecklistItems", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ChecklistId = table.Column(type: "int", nullable: false), + Title = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + Description = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + Order = table.Column(type: "int", nullable: false), + IsRequired = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChecklistItems", x => x.Id); + table.ForeignKey( + name: "FK_ChecklistItems_Checklists_ChecklistId", + column: x => x.ChecklistId, + principalTable: "Checklists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DevicePostImages", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DevicePostId = table.Column(type: "int", nullable: false), + FileName = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + FilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + ContentType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + FileSize = table.Column(type: "bigint", nullable: false), + UploadedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DevicePostImages", x => x.Id); + table.ForeignKey( + name: "FK_DevicePostImages_DevicePosts_DevicePostId", + column: x => x.DevicePostId, + principalTable: "DevicePosts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ReportImages", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserDailyReportId = table.Column(type: "int", nullable: false), + FileName = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + FilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + ContentType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + FileSize = table.Column(type: "bigint", nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + UploadedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ReportImages", x => x.Id); + table.ForeignKey( + name: "FK_ReportImages_UserDailyReports_UserDailyReportId", + column: x => x.UserDailyReportId, + principalTable: "UserDailyReports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ChecklistItemCompletions", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ChecklistCompletionId = table.Column(type: "int", nullable: false), + ChecklistItemId = table.Column(type: "int", nullable: false), + IsChecked = table.Column(type: "bit", nullable: false), + Note = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ChecklistItemCompletions", x => x.Id); + table.ForeignKey( + name: "FK_ChecklistItemCompletions_ChecklistCompletions_ChecklistCompletionId", + column: x => x.ChecklistCompletionId, + principalTable: "ChecklistCompletions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ChecklistItemCompletions_ChecklistItems_ChecklistItemId", + column: x => x.ChecklistItemId, + principalTable: "ChecklistItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_AlertLogs_AlertConditionId", + table: "AlertLogs", + column: "AlertConditionId"); + + migrationBuilder.CreateIndex( + name: "IX_AlertLogs_AlertType", + table: "AlertLogs", + column: "AlertType"); + + migrationBuilder.CreateIndex( + name: "IX_AlertLogs_DeviceId_SentAt", + table: "AlertLogs", + columns: new[] { "DeviceId", "SentAt" }); + + migrationBuilder.CreateIndex( + name: "IX_AlertLogs_Status", + table: "AlertLogs", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AlertLogs_UserId_SentAt", + table: "AlertLogs", + columns: new[] { "UserId", "SentAt" }); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistCompletions_ChecklistId_PersianDate", + table: "ChecklistCompletions", + columns: new[] { "ChecklistId", "PersianDate" }); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistCompletions_CompletedByUserId", + table: "ChecklistCompletions", + column: "CompletedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistItemCompletions_ChecklistCompletionId", + table: "ChecklistItemCompletions", + column: "ChecklistCompletionId"); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistItemCompletions_ChecklistItemId", + table: "ChecklistItemCompletions", + column: "ChecklistItemId"); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistItems_ChecklistId_Order", + table: "ChecklistItems", + columns: new[] { "ChecklistId", "Order" }); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_CreatedByUserId", + table: "Checklists", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_DeviceId_IsActive", + table: "Checklists", + columns: new[] { "DeviceId", "IsActive" }); + + migrationBuilder.CreateIndex( + name: "IX_DevicePostImages_DevicePostId", + table: "DevicePostImages", + column: "DevicePostId"); + + migrationBuilder.CreateIndex( + name: "IX_DevicePosts_AuthorUserId", + table: "DevicePosts", + column: "AuthorUserId"); + + migrationBuilder.CreateIndex( + name: "IX_DevicePosts_DeviceId_CreatedAt", + table: "DevicePosts", + columns: new[] { "DeviceId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_ReportImages_UserDailyReportId", + table: "ReportImages", + column: "UserDailyReportId"); + + migrationBuilder.CreateIndex( + name: "IX_UserDailyReports_DeviceId_PersianDate", + table: "UserDailyReports", + columns: new[] { "DeviceId", "PersianDate" }); + + migrationBuilder.CreateIndex( + name: "IX_UserDailyReports_DeviceId_PersianYear_PersianMonth", + table: "UserDailyReports", + columns: new[] { "DeviceId", "PersianYear", "PersianMonth" }); + + migrationBuilder.CreateIndex( + name: "IX_UserDailyReports_UserId", + table: "UserDailyReports", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AlertLogs"); + + migrationBuilder.DropTable( + name: "ChecklistItemCompletions"); + + migrationBuilder.DropTable( + name: "DevicePostImages"); + + migrationBuilder.DropTable( + name: "ReportImages"); + + migrationBuilder.DropTable( + name: "ChecklistCompletions"); + + migrationBuilder.DropTable( + name: "ChecklistItems"); + + migrationBuilder.DropTable( + name: "DevicePosts"); + + migrationBuilder.DropTable( + name: "UserDailyReports"); + + migrationBuilder.DropTable( + name: "Checklists"); + + migrationBuilder.DropColumn( + name: "ReceiveAlerts", + table: "DeviceUsers"); + + migrationBuilder.DropColumn( + name: "AreaSquareMeters", + table: "DeviceSettings"); + + migrationBuilder.DropColumn( + name: "MinimumCallIntervalMinutes", + table: "DeviceSettings"); + + migrationBuilder.DropColumn( + name: "MinimumSmsIntervalMinutes", + table: "DeviceSettings"); + + migrationBuilder.DropColumn( + name: "ProductType", + table: "DeviceSettings"); + + migrationBuilder.AlterColumn( + name: "AlertConditionId", + table: "AlertNotifications", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + } +} diff --git a/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs b/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs index 56b795a..c0f75ba 100644 --- a/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs +++ b/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs @@ -116,6 +116,67 @@ namespace GreenHome.Infrastructure.Migrations 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") @@ -124,7 +185,7 @@ namespace GreenHome.Infrastructure.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("AlertConditionId") + b.Property("AlertConditionId") .HasColumnType("int"); b.Property("DeviceId") @@ -199,6 +260,142 @@ namespace GreenHome.Infrastructure.Migrations 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") @@ -298,6 +495,79 @@ namespace GreenHome.Infrastructure.Migrations 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("UpdatedAt") + .HasColumnType("datetime2"); + + 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.DeviceSettings", b => { b.Property("Id") @@ -306,6 +576,9 @@ namespace GreenHome.Infrastructure.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AreaSquareMeters") + .HasColumnType("decimal(18,2)"); + b.Property("City") .IsRequired() .HasMaxLength(100) @@ -323,6 +596,21 @@ namespace GreenHome.Infrastructure.Migrations 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) @@ -347,6 +635,11 @@ namespace GreenHome.Infrastructure.Migrations b.Property("UserId") .HasColumnType("int"); + b.Property("ReceiveAlerts") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + b.HasKey("DeviceId", "UserId"); b.HasIndex("UserId"); @@ -354,6 +647,49 @@ namespace GreenHome.Infrastructure.Migrations b.ToTable("DeviceUsers", (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") @@ -448,6 +784,68 @@ namespace GreenHome.Infrastructure.Migrations 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") @@ -513,13 +911,38 @@ namespace GreenHome.Infrastructure.Migrations 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) - .IsRequired(); + .OnDelete(DeleteBehavior.Restrict); b.HasOne("GreenHome.Domain.Device", "Device") .WithMany() @@ -551,6 +974,74 @@ namespace GreenHome.Infrastructure.Migrations 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") @@ -573,6 +1064,36 @@ namespace GreenHome.Infrastructure.Migrations 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.DeviceSettings", b => { b.HasOne("GreenHome.Domain.Device", "Device") @@ -603,20 +1124,72 @@ namespace GreenHome.Infrastructure.Migrations b.Navigation("User"); }); + 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("DeviceUsers"); }); + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Navigation("Images"); + }); + 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/MonthlyReportService.cs b/src/GreenHome.Infrastructure/MonthlyReportService.cs new file mode 100644 index 0000000..fad1cee --- /dev/null +++ b/src/GreenHome.Infrastructure/MonthlyReportService.cs @@ -0,0 +1,185 @@ +using GreenHome.Application; +using Microsoft.EntityFrameworkCore; + +namespace GreenHome.Infrastructure; + +public sealed class MonthlyReportService : IMonthlyReportService +{ + private readonly GreenHomeDbContext _context; + + public MonthlyReportService(GreenHomeDbContext context) + { + _context = context; + } + + public async Task GetMonthlyReportAsync( + int deviceId, + int year, + int month, + CancellationToken cancellationToken) + { + var device = await _context.Devices + .FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken); + + if (device == null) + { + throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد"); + } + + // Get alert logs for the month + var alertLogs = await _context.AlertLogs + .Where(al => al.DeviceId == deviceId && + al.SentAt.Year == year && + al.SentAt.Month == month) + .ToListAsync(cancellationToken); + + var totalAlerts = alertLogs.Count; + var smsAlerts = alertLogs.Count(al => al.NotificationType == Domain.AlertNotificationType.SMS); + var callAlerts = alertLogs.Count(al => al.NotificationType == Domain.AlertNotificationType.Call); + var successfulAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Success); + var failedAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Failed); + var powerOutageAlerts = alertLogs.Count(al => al.AlertType == Domain.AlertType.PowerOutage); + + // Get telemetry statistics + var telemetryData = await _context.TelemetryRecords + .Where(t => t.DeviceId == deviceId && + t.PersianYear == year && + t.PersianMonth == month) + .ToListAsync(cancellationToken); + + var totalTelemetryRecords = telemetryData.Count; + var avgTemp = telemetryData.Any() ? telemetryData.Average(t => t.TemperatureC) : 0; + var minTemp = telemetryData.Any() ? telemetryData.Min(t => t.TemperatureC) : 0; + var maxTemp = telemetryData.Any() ? telemetryData.Max(t => t.TemperatureC) : 0; + var avgHumidity = telemetryData.Any() ? telemetryData.Average(t => t.HumidityPercent) : 0; + var minHumidity = telemetryData.Any() ? telemetryData.Min(t => t.HumidityPercent) : 0; + var maxHumidity = telemetryData.Any() ? telemetryData.Max(t => t.HumidityPercent) : 0; + var avgLux = telemetryData.Any() ? telemetryData.Average(t => t.Lux) : 0; + var avgGas = telemetryData.Any() ? (int)telemetryData.Average(t => t.GasPPM) : 0; + var maxGas = telemetryData.Any() ? telemetryData.Max(t => t.GasPPM) : 0; + + // Get user activity + var userReportsCount = await _context.UserDailyReports + .CountAsync(r => r.DeviceId == deviceId && + r.PersianYear == year && + r.PersianMonth == month, + cancellationToken); + + var checklistCompletionsCount = await _context.ChecklistCompletions + .Where(cc => cc.Checklist.DeviceId == deviceId) + .CountAsync(cc => cc.PersianDate.StartsWith($"{year}/{month:D2}"), cancellationToken); + + var dailyAnalysesCount = await _context.DailyReports + .CountAsync(dr => dr.DeviceId == deviceId && + dr.PersianYear == year && + dr.PersianMonth == month, + cancellationToken); + + // Generate performance summary + var performanceSummary = GeneratePerformanceSummary( + totalTelemetryRecords, + totalAlerts, + successfulAlerts, + failedAlerts, + avgTemp, + avgHumidity, + avgLux, + avgGas, + userReportsCount, + checklistCompletionsCount); + + return new MonthlyReportDto + { + DeviceId = deviceId, + DeviceName = device.DeviceName, + Year = year, + Month = month, + TotalAlerts = totalAlerts, + SmsAlerts = smsAlerts, + CallAlerts = callAlerts, + SuccessfulAlerts = successfulAlerts, + FailedAlerts = failedAlerts, + PowerOutageAlerts = powerOutageAlerts, + TotalTelemetryRecords = totalTelemetryRecords, + AverageTemperature = avgTemp, + MinTemperature = minTemp, + MaxTemperature = maxTemp, + AverageHumidity = avgHumidity, + MinHumidity = minHumidity, + MaxHumidity = maxHumidity, + AverageLux = avgLux, + AverageGasPPM = avgGas, + MaxGasPPM = maxGas, + UserDailyReportsCount = userReportsCount, + ChecklistCompletionsCount = checklistCompletionsCount, + DailyAnalysesCount = dailyAnalysesCount, + PerformanceSummary = performanceSummary, + GeneratedAt = DateTime.UtcNow + }; + } + + private string GeneratePerformanceSummary( + int totalRecords, + int totalAlerts, + int successfulAlerts, + int failedAlerts, + decimal avgTemp, + decimal avgHumidity, + decimal avgLux, + int avgGas, + int userReports, + int checklistCompletions) + { + var summary = new List(); + + summary.Add($"📊 آمار کلی:"); + summary.Add($" • تعداد رکوردهای ثبت شده: {totalRecords:N0}"); + summary.Add($" • میانگین دما: {avgTemp:F1}°C"); + summary.Add($" • میانگین رطوبت: {avgHumidity:F1}%"); + summary.Add($" • میانگین نور: {avgLux:F0} لوکس"); + if (avgGas > 0) + summary.Add($" • میانگین CO: {avgGas} PPM"); + + summary.Add(""); + summary.Add($"🚨 هشدارها:"); + summary.Add($" • تعداد کل: {totalAlerts}"); + summary.Add($" • موفق: {successfulAlerts}"); + if (failedAlerts > 0) + summary.Add($" • ناموفق: {failedAlerts} ⚠️"); + + if (userReports > 0 || checklistCompletions > 0) + { + summary.Add(""); + summary.Add($"📝 فعالیت کاربران:"); + if (userReports > 0) + summary.Add($" • گزارش‌های روزانه: {userReports}"); + if (checklistCompletions > 0) + summary.Add($" • تکمیل چک‌لیست: {checklistCompletions}"); + } + + // Performance rating + summary.Add(""); + var rating = CalculatePerformanceRating(totalRecords, failedAlerts, totalAlerts); + summary.Add($"⭐ ارزیابی کلی: {rating}"); + + return string.Join("\n", summary); + } + + private string CalculatePerformanceRating(int totalRecords, int failedAlerts, int totalAlerts) + { + if (totalRecords == 0) + return "بدون داده"; + + var failureRate = totalAlerts > 0 ? (double)failedAlerts / totalAlerts : 0; + + if (failureRate == 0 && totalRecords > 1000) + return "عالی ✅"; + else if (failureRate < 0.1 && totalRecords > 500) + return "خوب 👍"; + else if (failureRate < 0.3) + return "متوسط ⚠️"; + else + return "نیاز به بررسی 🔧"; + } +} + diff --git a/src/GreenHome.Infrastructure/UserDailyReportService.cs b/src/GreenHome.Infrastructure/UserDailyReportService.cs new file mode 100644 index 0000000..0ba1cb6 --- /dev/null +++ b/src/GreenHome.Infrastructure/UserDailyReportService.cs @@ -0,0 +1,225 @@ +using AutoMapper; +using GreenHome.Application; +using Microsoft.EntityFrameworkCore; + +namespace GreenHome.Infrastructure; + +public sealed class UserDailyReportService : IUserDailyReportService +{ + private readonly GreenHomeDbContext _context; + private readonly IMapper _mapper; + + public UserDailyReportService(GreenHomeDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> GetReportsAsync( + UserDailyReportFilter filter, + CancellationToken cancellationToken) + { + var query = _context.UserDailyReports + .Include(r => r.Device) + .Include(r => r.User) + .Include(r => r.Images) + .AsNoTracking() + .AsQueryable(); + + if (filter.DeviceId.HasValue) + { + query = query.Where(r => r.DeviceId == filter.DeviceId.Value); + } + + if (filter.UserId.HasValue) + { + query = query.Where(r => r.UserId == filter.UserId.Value); + } + + if (!string.IsNullOrWhiteSpace(filter.PersianDate)) + { + query = query.Where(r => r.PersianDate == filter.PersianDate); + } + + if (filter.Year.HasValue) + { + query = query.Where(r => r.PersianYear == filter.Year.Value); + } + + if (filter.Month.HasValue) + { + query = query.Where(r => r.PersianMonth == filter.Month.Value); + } + + var totalCount = await query.CountAsync(cancellationToken); + + var items = await query + .OrderByDescending(r => r.PersianDate) + .ThenByDescending(r => r.CreatedAt) + .Skip((filter.Page - 1) * filter.PageSize) + .Take(filter.PageSize) + .ToListAsync(cancellationToken); + + var dtos = _mapper.Map>(items); + + return new PagedResult + { + Items = dtos, + TotalCount = totalCount, + Page = filter.Page, + PageSize = filter.PageSize + }; + } + + public async Task GetReportByIdAsync(int id, CancellationToken cancellationToken) + { + var report = await _context.UserDailyReports + .Include(r => r.Device) + .Include(r => r.User) + .Include(r => r.Images) + .AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == id, cancellationToken); + + return report != null ? _mapper.Map(report) : null; + } + + public async Task CreateReportAsync( + CreateUserDailyReportRequest request, + CancellationToken cancellationToken) + { + // Validate date format + if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day)) + { + throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد"); + } + + var report = new Domain.UserDailyReport + { + DeviceId = request.DeviceId, + UserId = request.UserId, + PersianDate = request.PersianDate, + PersianYear = year, + PersianMonth = month, + PersianDay = day, + Title = request.Title, + Observations = request.Observations, + Operations = request.Operations, + Notes = request.Notes, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _context.UserDailyReports.Add(report); + await _context.SaveChangesAsync(cancellationToken); + + return report.Id; + } + + public async Task UpdateReportAsync( + UpdateUserDailyReportRequest request, + CancellationToken cancellationToken) + { + var report = await _context.UserDailyReports + .FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken); + + if (report == null) + { + throw new InvalidOperationException($"گزارش با شناسه {request.Id} یافت نشد"); + } + + report.Title = request.Title; + report.Observations = request.Observations; + report.Operations = request.Operations; + report.Notes = request.Notes; + report.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteReportAsync(int id, CancellationToken cancellationToken) + { + var report = await _context.UserDailyReports + .Include(r => r.Images) + .FirstOrDefaultAsync(r => r.Id == id, cancellationToken); + + if (report == null) + { + throw new InvalidOperationException($"گزارش با شناسه {id} یافت نشد"); + } + + _context.UserDailyReports.Remove(report); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task AddImageToReportAsync( + int reportId, + string fileName, + string filePath, + string contentType, + long fileSize, + string? description, + CancellationToken cancellationToken) + { + var report = await _context.UserDailyReports + .FirstOrDefaultAsync(r => r.Id == reportId, cancellationToken); + + if (report == null) + { + throw new InvalidOperationException($"گزارش با شناسه {reportId} یافت نشد"); + } + + var image = new Domain.ReportImage + { + UserDailyReportId = reportId, + FileName = fileName, + FilePath = filePath, + ContentType = contentType, + FileSize = fileSize, + Description = description, + UploadedAt = DateTime.UtcNow + }; + + _context.ReportImages.Add(image); + await _context.SaveChangesAsync(cancellationToken); + + return image.Id; + } + + public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken) + { + var image = await _context.ReportImages + .FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken); + + if (image == null) + { + throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد"); + } + + _context.ReportImages.Remove(image); + await _context.SaveChangesAsync(cancellationToken); + } + + private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day) + { + year = month = day = 0; + + if (string.IsNullOrWhiteSpace(persianDate)) + return false; + + var parts = persianDate.Split('/'); + if (parts.Length != 3) + return false; + + if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500) + return false; + + if (!int.TryParse(parts[1], out month) || month < 1 || month > 12) + return false; + + if (!int.TryParse(parts[2], out day) || day < 1 || day > 31) + return false; + + return true; + } +} +