version 3
This commit is contained in:
62
src/GreenHome.Api/Controllers/AlertLogsController.cs
Normal file
62
src/GreenHome.Api/Controllers/AlertLogsController.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت لیست لاگهای هشدار با فیلتر و صفحهبندی
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<PagedResult<AlertLogDto>>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت جزئیات کامل یک لاگ هشدار
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<ActionResult<AlertLogDto>> GetAlertLogById(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _alertLogService.GetAlertLogByIdAsync(id, cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"لاگ هشدار با شناسه {id} یافت نشد" });
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
98
src/GreenHome.Api/Controllers/ChecklistsController.cs
Normal file
98
src/GreenHome.Api/Controllers/ChecklistsController.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت چکلیست فعال یک دستگاه
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("active/{deviceId}")]
|
||||||
|
public async Task<ActionResult<ChecklistDto>> GetActiveChecklist(int deviceId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _checklistService.GetActiveChecklistByDeviceIdAsync(deviceId, cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = "چکلیست فعالی برای این دستگاه یافت نشد" });
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت تمام چکلیستهای یک دستگاه
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("device/{deviceId}")]
|
||||||
|
public async Task<ActionResult<List<ChecklistDto>>> GetChecklists(int deviceId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _checklistService.GetChecklistsByDeviceIdAsync(deviceId, cancellationToken);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت جزئیات یک چکلیست
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<ActionResult<ChecklistDto>> GetChecklistById(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _checklistService.GetChecklistByIdAsync(id, cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"چکلیست با شناسه {id} یافت نشد" });
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد چکلیست جدید (چکلیست قبلی غیرفعال میشود)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<int>> CreateChecklist(
|
||||||
|
CreateChecklistRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var id = await _checklistService.CreateChecklistAsync(request, cancellationToken);
|
||||||
|
return Ok(new { id, message = "چکلیست با موفقیت ایجاد شد و چکلیست قبلی غیرفعال شد" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت سابقه تکمیلهای یک چکلیست
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{checklistId}/completions")]
|
||||||
|
public async Task<ActionResult<List<ChecklistCompletionDto>>> GetCompletions(
|
||||||
|
int checklistId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _checklistService.GetCompletionsByChecklistIdAsync(checklistId, cancellationToken);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ثبت تکمیل چکلیست
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("complete")]
|
||||||
|
public async Task<ActionResult<int>> 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -73,5 +73,71 @@ public class DailyReportController : ControllerBase
|
|||||||
return StatusCode(500, new { error = "خطای سرور در پردازش درخواست" });
|
return StatusCode(500, new { error = "خطای سرور در پردازش درخواست" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت تحلیل هفتگی
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("weekly")]
|
||||||
|
public async Task<ActionResult<DailyReportResponse>> 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 = "خطا در دریافت تحلیل هفتگی" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت تحلیل ماهانه
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("monthly")]
|
||||||
|
public async Task<ActionResult<DailyReportResponse>> 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 = "خطا در دریافت تحلیل ماهانه" });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
212
src/GreenHome.Api/Controllers/DevicePostsController.cs
Normal file
212
src/GreenHome.Api/Controllers/DevicePostsController.cs
Normal file
@@ -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<DevicePostsController> _logger;
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
|
public DevicePostsController(
|
||||||
|
IDevicePostService postService,
|
||||||
|
ILogger<DevicePostsController> logger,
|
||||||
|
IWebHostEnvironment environment)
|
||||||
|
{
|
||||||
|
_postService = postService;
|
||||||
|
_logger = logger;
|
||||||
|
_environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت پستهای گروه مجازی دستگاه (تایملاین)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<PagedResult<DevicePostDto>>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت جزئیات یک پست
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<ActionResult<DevicePostDto>> GetPostById(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _postService.GetPostByIdAsync(id, cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"پست با شناسه {id} یافت نشد" });
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد پست جدید در گروه مجازی
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<int>> 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ویرایش پست
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<ActionResult> UpdatePost(
|
||||||
|
UpdateDevicePostRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _postService.UpdatePostAsync(request, cancellationToken);
|
||||||
|
return Ok(new { message = "پست با موفقیت ویرایش شد" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف پست
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<ActionResult> DeletePost(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _postService.DeletePostAsync(id, cancellationToken);
|
||||||
|
return Ok(new { message = "پست با موفقیت حذف شد" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آپلود تصویر برای پست
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("{postId}/images")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
public async Task<ActionResult<int>> 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 = "خطا در آپلود تصویر" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف تصویر از پست
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("images/{imageId}")]
|
||||||
|
public async Task<ActionResult> DeleteImage(int imageId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _postService.DeleteImageAsync(imageId, cancellationToken);
|
||||||
|
return Ok(new { message = "تصویر با موفقیت حذف شد" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی دسترسی کاربر به دستگاه
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("access/{userId}/{deviceId}")]
|
||||||
|
public async Task<ActionResult<bool>> CheckAccess(int userId, int deviceId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var hasAccess = await _postService.CanUserAccessDeviceAsync(userId, deviceId, cancellationToken);
|
||||||
|
return Ok(new { hasAccess });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
62
src/GreenHome.Api/Controllers/MonthlyReportController.cs
Normal file
62
src/GreenHome.Api/Controllers/MonthlyReportController.cs
Normal file
@@ -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<MonthlyReportController> _logger;
|
||||||
|
|
||||||
|
public MonthlyReportController(
|
||||||
|
IMonthlyReportService monthlyReportService,
|
||||||
|
ILogger<MonthlyReportController> logger)
|
||||||
|
{
|
||||||
|
_monthlyReportService = monthlyReportService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت گزارش آماری ماهانه
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<MonthlyReportDto>> 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 = "خطا در ایجاد گزارش ماهانه" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
60
src/GreenHome.Api/Controllers/PowerOutageController.cs
Normal file
60
src/GreenHome.Api/Controllers/PowerOutageController.cs
Normal file
@@ -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<PowerOutageController> _logger;
|
||||||
|
|
||||||
|
public PowerOutageController(
|
||||||
|
IAlertService alertService,
|
||||||
|
ILogger<PowerOutageController> logger)
|
||||||
|
{
|
||||||
|
_alertService = alertService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ارسال هشدار قطع برق برای یک دستگاه
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceId">شناسه دستگاه</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>نتیجه عملیات</returns>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult> 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 = "خطا در ارسال هشدار قطع برق" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
210
src/GreenHome.Api/Controllers/UserDailyReportsController.cs
Normal file
210
src/GreenHome.Api/Controllers/UserDailyReportsController.cs
Normal file
@@ -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<UserDailyReportsController> _logger;
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
|
public UserDailyReportsController(
|
||||||
|
IUserDailyReportService reportService,
|
||||||
|
ILogger<UserDailyReportsController> logger,
|
||||||
|
IWebHostEnvironment environment)
|
||||||
|
{
|
||||||
|
_reportService = reportService;
|
||||||
|
_logger = logger;
|
||||||
|
_environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت لیست گزارشهای روزانه کاربران با فیلتر
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<PagedResult<UserDailyReportDto>>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت جزئیات یک گزارش روزانه
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<ActionResult<UserDailyReportDto>> GetReportById(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _reportService.GetReportByIdAsync(id, cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"گزارش با شناسه {id} یافت نشد" });
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد گزارش روزانه جدید
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<int>> 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ویرایش گزارش روزانه
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<ActionResult> UpdateReport(
|
||||||
|
UpdateUserDailyReportRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _reportService.UpdateReportAsync(request, cancellationToken);
|
||||||
|
return Ok(new { message = "گزارش با موفقیت ویرایش شد" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف گزارش روزانه
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<ActionResult> DeleteReport(int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _reportService.DeleteReportAsync(id, cancellationToken);
|
||||||
|
return Ok(new { message = "گزارش با موفقیت حذف شد" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آپلود تصویر برای گزارش روزانه
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("{reportId}/images")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
public async Task<ActionResult<int>> 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 = "خطا در آپلود تصویر" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حذف تصویر از گزارش
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("images/{imageId}")]
|
||||||
|
public async Task<ActionResult> DeleteImage(int imageId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _reportService.DeleteImageAsync(imageId, cancellationToken);
|
||||||
|
return Ok(new { message = "تصویر با موفقیت حذف شد" });
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.11.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ using GreenHome.Infrastructure;
|
|||||||
using GreenHome.Sms.Ippanel;
|
using GreenHome.Sms.Ippanel;
|
||||||
using GreenHome.VoiceCall.Avanak;
|
using GreenHome.VoiceCall.Avanak;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Scalar.AspNetCore;
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
// Application/Infrastructure DI
|
// Application/Infrastructure DI
|
||||||
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
|
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
|
||||||
@@ -60,6 +61,11 @@ builder.Services.AddScoped<GreenHome.Application.IAlertConditionService, GreenHo
|
|||||||
builder.Services.AddScoped<GreenHome.Application.ISunCalculatorService, GreenHome.Infrastructure.SunCalculatorService>();
|
builder.Services.AddScoped<GreenHome.Application.ISunCalculatorService, GreenHome.Infrastructure.SunCalculatorService>();
|
||||||
builder.Services.AddScoped<GreenHome.Application.IAIQueryService, GreenHome.Infrastructure.AIQueryService>();
|
builder.Services.AddScoped<GreenHome.Application.IAIQueryService, GreenHome.Infrastructure.AIQueryService>();
|
||||||
builder.Services.AddScoped<GreenHome.Application.IDailyReportService, GreenHome.Infrastructure.DailyReportService>();
|
builder.Services.AddScoped<GreenHome.Application.IDailyReportService, GreenHome.Infrastructure.DailyReportService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IAlertLogService, GreenHome.Infrastructure.AlertLogService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IUserDailyReportService, GreenHome.Infrastructure.UserDailyReportService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IChecklistService, GreenHome.Infrastructure.ChecklistService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IMonthlyReportService, GreenHome.Infrastructure.MonthlyReportService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IDevicePostService, GreenHome.Infrastructure.DevicePostService>();
|
||||||
|
|
||||||
// SMS Service Configuration
|
// SMS Service Configuration
|
||||||
builder.Services.AddIppanelSms(builder.Configuration);
|
builder.Services.AddIppanelSms(builder.Configuration);
|
||||||
@@ -89,11 +95,8 @@ using (var scope = app.Services.CreateScope())
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
//if (app.Environment.IsDevelopment())
|
app.MapOpenApi();
|
||||||
{
|
app.MapScalarApiReference();
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPS Redirection فقط در Production
|
// HTTPS Redirection فقط در Production
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
|
|||||||
80
src/GreenHome.Application/ChecklistDtos.cs
Normal file
80
src/GreenHome.Application/ChecklistDtos.cs
Normal file
@@ -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<ChecklistItemDto> 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<CreateChecklistItemRequest> 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<ChecklistItemCompletionDto> 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<CompleteChecklistItemRequest> 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; }
|
||||||
|
}
|
||||||
|
|
||||||
46
src/GreenHome.Application/DevicePostDtos.cs
Normal file
46
src/GreenHome.Application/DevicePostDtos.cs
Normal file
@@ -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<DevicePostImageDto> 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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -123,6 +123,11 @@ public sealed class DeviceSettingsDto
|
|||||||
public decimal? Latitude { get; set; }
|
public decimal? Latitude { get; set; }
|
||||||
public decimal? Longitude { 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 CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
@@ -203,3 +208,108 @@ public sealed class DailyReportResponse
|
|||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public bool FromCache { 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<ReportImageDto> 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;
|
||||||
|
}
|
||||||
9
src/GreenHome.Application/IAlertLogService.cs
Normal file
9
src/GreenHome.Application/IAlertLogService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IAlertLogService
|
||||||
|
{
|
||||||
|
Task<PagedResult<AlertLogDto>> GetAlertLogsAsync(AlertLogFilter filter, CancellationToken cancellationToken);
|
||||||
|
Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,5 +3,6 @@ namespace GreenHome.Application;
|
|||||||
public interface IAlertService
|
public interface IAlertService
|
||||||
{
|
{
|
||||||
Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
|
Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
|
||||||
|
Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/GreenHome.Application/IChecklistService.cs
Normal file
12
src/GreenHome.Application/IChecklistService.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IChecklistService
|
||||||
|
{
|
||||||
|
Task<ChecklistDto?> GetActiveChecklistByDeviceIdAsync(int deviceId, CancellationToken cancellationToken);
|
||||||
|
Task<List<ChecklistDto>> GetChecklistsByDeviceIdAsync(int deviceId, CancellationToken cancellationToken);
|
||||||
|
Task<ChecklistDto?> GetChecklistByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<int> CreateChecklistAsync(CreateChecklistRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<List<ChecklistCompletionDto>> GetCompletionsByChecklistIdAsync(int checklistId, CancellationToken cancellationToken);
|
||||||
|
Task<int> CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,5 +9,9 @@ public interface IDailyReportService
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Daily report with AI analysis</returns>
|
/// <returns>Daily report with AI analysis</returns>
|
||||||
Task<DailyReportResponse> GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken);
|
Task<DailyReportResponse> GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<DailyReportResponse> GetWeeklyAnalysisAsync(WeeklyAnalysisRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<DailyReportResponse> GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
src/GreenHome.Application/IDevicePostService.cs
Normal file
14
src/GreenHome.Application/IDevicePostService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IDevicePostService
|
||||||
|
{
|
||||||
|
Task<PagedResult<DevicePostDto>> GetPostsAsync(DevicePostFilter filter, CancellationToken cancellationToken);
|
||||||
|
Task<DevicePostDto?> GetPostByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<int> CreatePostAsync(CreateDevicePostRequest request, CancellationToken cancellationToken);
|
||||||
|
Task UpdatePostAsync(UpdateDevicePostRequest request, CancellationToken cancellationToken);
|
||||||
|
Task DeletePostAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<int> AddImageToPostAsync(int postId, string fileName, string filePath, string contentType, long fileSize, CancellationToken cancellationToken);
|
||||||
|
Task DeleteImageAsync(int imageId, CancellationToken cancellationToken);
|
||||||
|
Task<bool> CanUserAccessDeviceAsync(int userId, int deviceId, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
45
src/GreenHome.Application/IMonthlyReportService.cs
Normal file
45
src/GreenHome.Application/IMonthlyReportService.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IMonthlyReportService
|
||||||
|
{
|
||||||
|
Task<MonthlyReportDto> 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; }
|
||||||
|
}
|
||||||
|
|
||||||
13
src/GreenHome.Application/IUserDailyReportService.cs
Normal file
13
src/GreenHome.Application/IUserDailyReportService.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IUserDailyReportService
|
||||||
|
{
|
||||||
|
Task<PagedResult<UserDailyReportDto>> GetReportsAsync(UserDailyReportFilter filter, CancellationToken cancellationToken);
|
||||||
|
Task<UserDailyReportDto?> GetReportByIdAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<int> CreateReportAsync(CreateUserDailyReportRequest request, CancellationToken cancellationToken);
|
||||||
|
Task UpdateReportAsync(UpdateUserDailyReportRequest request, CancellationToken cancellationToken);
|
||||||
|
Task DeleteReportAsync(int id, CancellationToken cancellationToken);
|
||||||
|
Task<int> AddImageToReportAsync(int reportId, string fileName, string filePath, string contentType, long fileSize, string? description, CancellationToken cancellationToken);
|
||||||
|
Task DeleteImageAsync(int imageId, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -32,5 +32,40 @@ public sealed class MappingProfile : Profile
|
|||||||
CreateMap<CreateAlertRuleRequest, Domain.AlertRule>();
|
CreateMap<CreateAlertRuleRequest, Domain.AlertRule>();
|
||||||
|
|
||||||
CreateMap<Domain.User, UserDto>().ReverseMap();
|
CreateMap<Domain.User, UserDto>().ReverseMap();
|
||||||
|
|
||||||
|
CreateMap<Domain.AlertLog, AlertLogDto>()
|
||||||
|
.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<Domain.UserDailyReport, UserDailyReportDto>()
|
||||||
|
.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<Domain.ReportImage, ReportImageDto>();
|
||||||
|
|
||||||
|
CreateMap<Domain.Checklist, ChecklistDto>()
|
||||||
|
.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<Domain.ChecklistItem, ChecklistItemDto>();
|
||||||
|
|
||||||
|
CreateMap<Domain.ChecklistCompletion, ChecklistCompletionDto>()
|
||||||
|
.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<Domain.ChecklistItemCompletion, ChecklistItemCompletionDto>()
|
||||||
|
.ForMember(dest => dest.ItemTitle, opt => opt.MapFrom(src => src.ChecklistItem.Title));
|
||||||
|
|
||||||
|
CreateMap<Domain.DevicePost, DevicePostDto>()
|
||||||
|
.ForMember(dest => dest.AuthorName, opt => opt.MapFrom(src => src.AuthorUser.Name))
|
||||||
|
.ForMember(dest => dest.AuthorFamily, opt => opt.MapFrom(src => src.AuthorUser.Family));
|
||||||
|
|
||||||
|
CreateMap<Domain.DevicePostImage, DevicePostImageDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/GreenHome.Domain/AlertLog.cs
Normal file
110
src/GreenHome.Domain/AlertLog.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// لاگ هشدارهای ارسال شده
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AlertLog
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه دستگاه
|
||||||
|
/// </summary>
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public Device Device { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربری که هشدار به او ارسال شده
|
||||||
|
/// </summary>
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه شرط هشدار (اگر مربوط به شرط خاصی بود)
|
||||||
|
/// </summary>
|
||||||
|
public int? AlertConditionId { get; set; }
|
||||||
|
public AlertCondition? AlertCondition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع هشدار (SMS, Call, PowerOutage)
|
||||||
|
/// </summary>
|
||||||
|
public AlertType AlertType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع اعلان (SMS یا Call)
|
||||||
|
/// </summary>
|
||||||
|
public AlertNotificationType NotificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام هشدار
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت ارسال
|
||||||
|
/// </summary>
|
||||||
|
public AlertStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام خطا (در صورت شکست)
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره تماس یا پیامک
|
||||||
|
/// </summary>
|
||||||
|
public string PhoneNumber { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان ارسال
|
||||||
|
/// </summary>
|
||||||
|
public DateTime SentAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مدت زمان پردازش (میلیثانیه)
|
||||||
|
/// </summary>
|
||||||
|
public long ProcessingTimeMs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع هشدار
|
||||||
|
/// </summary>
|
||||||
|
public enum AlertType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// هشدار بر اساس شرط
|
||||||
|
/// </summary>
|
||||||
|
Condition = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// هشدار قطع برق
|
||||||
|
/// </summary>
|
||||||
|
PowerOutage = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// هشدار دستی
|
||||||
|
/// </summary>
|
||||||
|
Manual = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت ارسال هشدار
|
||||||
|
/// </summary>
|
||||||
|
public enum AlertStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// با موفقیت ارسال شد
|
||||||
|
/// </summary>
|
||||||
|
Success = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// با خطا مواجه شد
|
||||||
|
/// </summary>
|
||||||
|
Failed = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// در صف ارسال
|
||||||
|
/// </summary>
|
||||||
|
Pending = 3
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ public sealed class AlertNotification
|
|||||||
public Device Device { get; set; } = null!;
|
public Device Device { get; set; } = null!;
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public User User { get; set; } = null!;
|
public User User { get; set; } = null!;
|
||||||
public int AlertConditionId { get; set; }
|
public int? AlertConditionId { get; set; }
|
||||||
public AlertCondition AlertCondition { get; set; } = null!;
|
public AlertCondition? AlertCondition { get; set; }
|
||||||
public AlertNotificationType NotificationType { get; set; } // Call or SMS
|
public AlertNotificationType NotificationType { get; set; } // Call or SMS
|
||||||
public string Message { get; set; } = string.Empty;
|
public string Message { get; set; } = string.Empty;
|
||||||
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS)
|
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS)
|
||||||
|
|||||||
156
src/GreenHome.Domain/Checklist.cs
Normal file
156
src/GreenHome.Domain/Checklist.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// چکلیست دستگاه
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Checklist
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه دستگاه
|
||||||
|
/// </summary>
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public Device Device { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// عنوان چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا این چکلیست فعال است؟ (فقط یک چکلیست فعال برای هر دستگاه)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیتمهای چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<ChecklistItem> Items { get; set; } = new List<ChecklistItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سابقه تکمیلهای چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<ChecklistCompletion> Completions { get; set; } = new List<ChecklistCompletion>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان ایجاد
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر ایجاد کننده
|
||||||
|
/// </summary>
|
||||||
|
public int CreatedByUserId { get; set; }
|
||||||
|
public User CreatedByUser { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیتم چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChecklistItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public int ChecklistId { get; set; }
|
||||||
|
public Checklist Checklist { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// عنوان آیتم
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ترتیب نمایش
|
||||||
|
/// </summary>
|
||||||
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا اجباری است؟
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سابقه تکمیل چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChecklistCompletion
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public int ChecklistId { get; set; }
|
||||||
|
public Checklist Checklist { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر انجام دهنده
|
||||||
|
/// </summary>
|
||||||
|
public int CompletedByUserId { get; set; }
|
||||||
|
public User CompletedByUser { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ شمسی تکمیل
|
||||||
|
/// </summary>
|
||||||
|
public string PersianDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیتمهای چک شده
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<ChecklistItemCompletion> ItemCompletions { get; set; } = new List<ChecklistItemCompletion>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// یادداشتهای اضافی
|
||||||
|
/// </summary>
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان تکمیل
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CompletedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تکمیل آیتم چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChecklistItemCompletion
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه تکمیل چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public int ChecklistCompletionId { get; set; }
|
||||||
|
public ChecklistCompletion ChecklistCompletion { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه آیتم چکلیست
|
||||||
|
/// </summary>
|
||||||
|
public int ChecklistItemId { get; set; }
|
||||||
|
public ChecklistItem ChecklistItem { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا چک شده؟
|
||||||
|
/// </summary>
|
||||||
|
public bool IsChecked { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// یادداشت برای این آیتم
|
||||||
|
/// </summary>
|
||||||
|
public string? Note { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
81
src/GreenHome.Domain/DevicePost.cs
Normal file
81
src/GreenHome.Domain/DevicePost.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پستهای گروه مجازی دستگاه (تایملاین مشترک)
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevicePost
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه دستگاه
|
||||||
|
/// </summary>
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public Device Device { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر نویسنده پست
|
||||||
|
/// </summary>
|
||||||
|
public int AuthorUserId { get; set; }
|
||||||
|
public User AuthorUser { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// متن پست
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تصاویر پیوست
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<DevicePostImage> Images { get; set; } = new List<DevicePostImage>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان ایجاد
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان آخرین ویرایش
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تصاویر پیوست پست
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevicePostImage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه پست
|
||||||
|
/// </summary>
|
||||||
|
public int DevicePostId { get; set; }
|
||||||
|
public DevicePost DevicePost { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام فایل
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مسیر ذخیره فایل
|
||||||
|
/// </summary>
|
||||||
|
public string FilePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع فایل (MIME type)
|
||||||
|
/// </summary>
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حجم فایل (بایت)
|
||||||
|
/// </summary>
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان آپلود
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UploadedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -26,6 +26,26 @@ public sealed class DeviceSettings
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? Longitude { get; set; }
|
public decimal? Longitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع محصول
|
||||||
|
/// </summary>
|
||||||
|
public string ProductType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداقل فاصله زمانی ارسال پیامک (به دقیقه)
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumSmsIntervalMinutes { get; set; } = 15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداقل فاصله زمانی تماس (به دقیقه)
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumCallIntervalMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مساحت گلخانه (متر مربع)
|
||||||
|
/// </summary>
|
||||||
|
public decimal? AreaSquareMeters { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,10 @@ public sealed class DeviceUser
|
|||||||
public Device Device { get; set; } = null!;
|
public Device Device { get; set; } = null!;
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public User User { get; set; } = null!;
|
public User User { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا این کاربر باید هشدارهای این دستگاه را دریافت کند؟
|
||||||
|
/// </summary>
|
||||||
|
public bool ReceiveAlerts { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
src/GreenHome.Domain/UserDailyReport.cs
Normal file
121
src/GreenHome.Domain/UserDailyReport.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// گزارش روزانه کاربر (مشاهدات و عملیات انجام شده)
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserDailyReport
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه دستگاه
|
||||||
|
/// </summary>
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public Device Device { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر گزارشدهنده
|
||||||
|
/// </summary>
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تاریخ شمسی (yyyy/MM/dd)
|
||||||
|
/// </summary>
|
||||||
|
public string PersianDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سال شمسی
|
||||||
|
/// </summary>
|
||||||
|
public int PersianYear { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ماه شمسی
|
||||||
|
/// </summary>
|
||||||
|
public int PersianMonth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// روز شمسی
|
||||||
|
/// </summary>
|
||||||
|
public int PersianDay { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// عنوان گزارش
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شرح مشاهدات
|
||||||
|
/// </summary>
|
||||||
|
public string Observations { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// عملیات انجام شده
|
||||||
|
/// </summary>
|
||||||
|
public string Operations { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// یادداشتهای اضافی
|
||||||
|
/// </summary>
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تصاویر پیوست
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<ReportImage> Images { get; set; } = new List<ReportImage>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان ایجاد
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان آخرین ویرایش
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تصاویر پیوست گزارش روزانه
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReportImage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه گزارش
|
||||||
|
/// </summary>
|
||||||
|
public int UserDailyReportId { get; set; }
|
||||||
|
public UserDailyReport UserDailyReport { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام فایل
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مسیر ذخیره فایل
|
||||||
|
/// </summary>
|
||||||
|
public string FilePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نوع فایل (MIME type)
|
||||||
|
/// </summary>
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حجم فایل (بایت)
|
||||||
|
/// </summary>
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات تصویر
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// زمان آپلود
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UploadedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
100
src/GreenHome.Infrastructure/AlertLogService.cs
Normal file
100
src/GreenHome.Infrastructure/AlertLogService.cs
Normal file
@@ -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<PagedResult<AlertLogDto>> 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<List<AlertLogDto>>(items);
|
||||||
|
|
||||||
|
return new PagedResult<AlertLogDto>
|
||||||
|
{
|
||||||
|
Items = dtos,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Page = filter.Page,
|
||||||
|
PageSize = filter.PageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AlertLogDto?> 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<AlertLogDto>(log) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = _mapper.Map<Domain.AlertLog>(dto);
|
||||||
|
_context.AlertLogs.Add(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -35,14 +35,28 @@ public sealed class AlertService : IAlertService
|
|||||||
|
|
||||||
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
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
|
var device = await dbContext.Devices
|
||||||
.Include(d => d.User)
|
.Include(d => d.User)
|
||||||
|
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||||
|
.ThenInclude(du => du.User)
|
||||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +101,7 @@ public sealed class AlertService : IAlertService
|
|||||||
if (allRulesMatch && condition.Rules.Any())
|
if (allRulesMatch && condition.Rules.Any())
|
||||||
{
|
{
|
||||||
// All rules passed, send alert if cooldown period has passed
|
// 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(
|
private async Task SendAlertForConditionAsync(
|
||||||
Domain.AlertCondition condition,
|
Domain.AlertCondition condition,
|
||||||
Domain.Device device,
|
Domain.Device device,
|
||||||
|
List<Domain.User> usersToAlert,
|
||||||
TelemetryDto telemetry,
|
TelemetryDto telemetry,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -127,68 +142,95 @@ public sealed class AlertService : IAlertService
|
|||||||
? condition.CallCooldownMinutes
|
? condition.CallCooldownMinutes
|
||||||
: condition.SmsCooldownMinutes;
|
: condition.SmsCooldownMinutes;
|
||||||
|
|
||||||
// Check if alert was sent recently
|
// Build alert message once
|
||||||
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
|
|
||||||
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
|
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
|
||||||
|
var sentAt = DateTime.UtcNow;
|
||||||
|
|
||||||
// Send notification
|
// Send alert to each user
|
||||||
string? messageOutboxIds = null;
|
foreach (var user in usersToAlert)
|
||||||
string? errorMessage = null;
|
{
|
||||||
bool isSent = false;
|
// 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 (recentAlert != null)
|
||||||
{
|
|
||||||
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
|
||||||
{
|
{
|
||||||
(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)
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Exception: {ex.Message}";
|
|
||||||
if (ex.InnerException != null)
|
|
||||||
{
|
{
|
||||||
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}",
|
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||||
device.Id, condition.Id, condition.NotificationType);
|
|
||||||
|
// 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);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,5 +365,131 @@ public sealed class AlertService : IAlertService
|
|||||||
return (false, null, errorMsg);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
152
src/GreenHome.Infrastructure/ChecklistService.cs
Normal file
152
src/GreenHome.Infrastructure/ChecklistService.cs
Normal file
@@ -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<ChecklistDto?> 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<ChecklistDto>(checklist) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ChecklistDto>> 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<List<ChecklistDto>>(checklists);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChecklistDto?> 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<ChecklistDto>(checklist) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> 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<List<ChecklistCompletionDto>> 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<List<ChecklistCompletionDto>>(completions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -70,6 +70,10 @@ public class DailyReportService : IDailyReportService
|
|||||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
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
|
// Query telemetry data for the specified date
|
||||||
var telemetryRecords = await _context.TelemetryRecords
|
var telemetryRecords = await _context.TelemetryRecords
|
||||||
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
|
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
|
||||||
@@ -115,7 +119,15 @@ public class DailyReportService : IDailyReportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the question for AI
|
// 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}
|
{dataBuilder}
|
||||||
|
|
||||||
@@ -123,7 +135,7 @@ public class DailyReportService : IDailyReportService
|
|||||||
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
||||||
2. روندهای مشاهده شده در طول روز
|
2. روندهای مشاهده شده در طول روز
|
||||||
3. هر گونه نکته یا هشدار مهم
|
3. هر گونه نکته یا هشدار مهم
|
||||||
4. پیشنهادات برای بهبود شرایط گلخانه
|
4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)}
|
||||||
|
|
||||||
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
||||||
|
|
||||||
@@ -133,12 +145,16 @@ public class DailyReportService : IDailyReportService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||||
|
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||||||
|
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||||||
|
|
||||||
var chatRequest = new ChatRequest
|
var chatRequest = new ChatRequest
|
||||||
{
|
{
|
||||||
Model = "deepseek-chat",
|
Model = "deepseek-chat",
|
||||||
Messages = new List<ChatMessage>
|
Messages = new List<ChatMessage>
|
||||||
{
|
{
|
||||||
new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی." },
|
new() { Role = "system", Content = systemMessage },
|
||||||
new() { Role = "user", Content = question }
|
new() { Role = "user", Content = question }
|
||||||
},
|
},
|
||||||
Temperature = 0.7
|
Temperature = 0.7
|
||||||
@@ -225,5 +241,239 @@ public class DailyReportService : IDailyReportService
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DailyReportResponse> 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<ChatMessage>
|
||||||
|
{
|
||||||
|
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<DailyReportResponse> 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<ChatMessage>
|
||||||
|
{
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
185
src/GreenHome.Infrastructure/DevicePostService.cs
Normal file
185
src/GreenHome.Infrastructure/DevicePostService.cs
Normal file
@@ -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<PagedResult<DevicePostDto>> 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<List<DevicePostDto>>(posts);
|
||||||
|
|
||||||
|
return new PagedResult<DevicePostDto>
|
||||||
|
{
|
||||||
|
Items = dtos,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Page = filter.Page,
|
||||||
|
PageSize = filter.PageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DevicePostDto?> 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<DevicePostDto>(post) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> 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<int> 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<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,8 +15,17 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
||||||
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
||||||
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
||||||
|
public DbSet<Domain.AlertLog> AlertLogs => Set<Domain.AlertLog>();
|
||||||
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
|
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
|
||||||
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
|
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
|
||||||
|
public DbSet<Domain.UserDailyReport> UserDailyReports => Set<Domain.UserDailyReport>();
|
||||||
|
public DbSet<Domain.ReportImage> ReportImages => Set<Domain.ReportImage>();
|
||||||
|
public DbSet<Domain.Checklist> Checklists => Set<Domain.Checklist>();
|
||||||
|
public DbSet<Domain.ChecklistItem> ChecklistItems => Set<Domain.ChecklistItem>();
|
||||||
|
public DbSet<Domain.ChecklistCompletion> ChecklistCompletions => Set<Domain.ChecklistCompletion>();
|
||||||
|
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
|
||||||
|
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
|
||||||
|
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
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.City).HasMaxLength(100);
|
||||||
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
|
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
|
||||||
b.Property(x => x.Longitude).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)
|
b.HasOne(x => x.Device)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(x => x.DeviceId)
|
.HasForeignKey(x => x.DeviceId)
|
||||||
@@ -112,6 +125,7 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
{
|
{
|
||||||
b.ToTable("DeviceUsers");
|
b.ToTable("DeviceUsers");
|
||||||
b.HasKey(x => new { x.DeviceId, x.UserId });
|
b.HasKey(x => new { x.DeviceId, x.UserId });
|
||||||
|
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
|
||||||
b.HasOne(x => x.Device)
|
b.HasOne(x => x.Device)
|
||||||
.WithMany(d => d.DeviceUsers)
|
.WithMany(d => d.DeviceUsers)
|
||||||
.HasForeignKey(x => x.DeviceId)
|
.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 => new { x.DeviceId, x.PersianYear, x.PersianMonth });
|
||||||
b.HasIndex(x => x.CreatedAt);
|
b.HasIndex(x => x.CreatedAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Domain.AlertLog>(b =>
|
||||||
|
{
|
||||||
|
b.ToTable("AlertLogs");
|
||||||
|
b.HasKey(x => x.Id);
|
||||||
|
b.Property(x => x.AlertType).IsRequired().HasConversion<int>();
|
||||||
|
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
|
||||||
|
b.Property(x => x.Message).IsRequired().HasMaxLength(1000);
|
||||||
|
b.Property(x => x.Status).IsRequired().HasConversion<int>();
|
||||||
|
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<Domain.UserDailyReport>(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<Domain.ReportImage>(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<Domain.Checklist>(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<Domain.ChecklistItem>(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<Domain.ChecklistCompletion>(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<Domain.ChecklistItemCompletion>(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<Domain.DevicePost>(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<Domain.DevicePostImage>(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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1199
src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs
generated
Normal file
1199
src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAllNewFeatures : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ReceiveAlerts",
|
||||||
|
table: "DeviceUsers",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "AreaSquareMeters",
|
||||||
|
table: "DeviceSettings",
|
||||||
|
type: "decimal(18,2)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MinimumCallIntervalMinutes",
|
||||||
|
table: "DeviceSettings",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 60);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MinimumSmsIntervalMinutes",
|
||||||
|
table: "DeviceSettings",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 15);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ProductType",
|
||||||
|
table: "DeviceSettings",
|
||||||
|
type: "nvarchar(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "AlertConditionId",
|
||||||
|
table: "AlertNotifications",
|
||||||
|
type: "int",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "int");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AlertLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AlertConditionId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
AlertType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
NotificationType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Message = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ErrorMessage = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||||
|
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||||
|
SentAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
ProcessingTimeMs = table.Column<long>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
CreatedByUserId = table.Column<int>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AuthorUserId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 5000, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||||
|
PersianYear = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PersianMonth = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PersianDay = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||||
|
Observations = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Operations = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
ChecklistId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CompletedByUserId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||||
|
CompletedAt = table.Column<DateTime>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
ChecklistId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
Order = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsRequired = table.Column<bool>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DevicePostId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||||
|
FilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
|
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
UploadedAt = table.Column<DateTime>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
UserDailyReportId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||||
|
FilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
|
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
UploadedAt = table.Column<DateTime>(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<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
ChecklistCompletionId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ChecklistItemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsChecked = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Note = table.Column<string>(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<int>(
|
||||||
|
name: "AlertConditionId",
|
||||||
|
table: "AlertNotifications",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "int",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,6 +116,67 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("AlertConditions", (string)null);
|
b.ToTable("AlertConditions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("AlertConditionId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("AlertType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<int>("NotificationType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<long>("ProcessingTimeMs")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("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 =>
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -124,7 +185,7 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
b.Property<int>("AlertConditionId")
|
b.Property<int?>("AlertConditionId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("DeviceId")
|
b.Property<int>("DeviceId")
|
||||||
@@ -199,6 +260,142 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("AlertRules", (string)null);
|
b.ToTable("AlertRules", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("CreatedByUserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("ChecklistId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CompletedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("CompletedByUserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("ChecklistId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRequired")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("ChecklistCompletionId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("ChecklistItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsChecked")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("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 =>
|
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -298,6 +495,79 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("Devices", (string)null);
|
b.ToTable("Devices", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AuthorUserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(5000)
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("DevicePostId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UploadedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DevicePostId");
|
||||||
|
|
||||||
|
b.ToTable("DevicePostImages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -306,6 +576,9 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal?>("AreaSquareMeters")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
b.Property<string>("City")
|
b.Property<string>("City")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
@@ -323,6 +596,21 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Property<decimal?>("Longitude")
|
b.Property<decimal?>("Longitude")
|
||||||
.HasColumnType("decimal(9,6)");
|
.HasColumnType("decimal(9,6)");
|
||||||
|
|
||||||
|
b.Property<int>("MinimumCallIntervalMinutes")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasDefaultValue(60);
|
||||||
|
|
||||||
|
b.Property<int>("MinimumSmsIntervalMinutes")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasDefaultValue(15);
|
||||||
|
|
||||||
|
b.Property<string>("ProductType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
b.Property<string>("Province")
|
b.Property<string>("Province")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
@@ -347,6 +635,11 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Property<int>("UserId")
|
b.Property<int>("UserId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("ReceiveAlerts")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.HasKey("DeviceId", "UserId");
|
b.HasKey("DeviceId", "UserId");
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
@@ -354,6 +647,49 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("DeviceUsers", (string)null);
|
b.ToTable("DeviceUsers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UploadedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("UserDailyReportId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserDailyReportId");
|
||||||
|
|
||||||
|
b.ToTable("ReportImages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -448,6 +784,68 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("Users", (string)null);
|
b.ToTable("Users", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("Observations")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Operations")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PersianDate")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<int>("PersianDay")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PersianMonth")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PersianYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("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 =>
|
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -513,13 +911,38 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Navigation("Device");
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
|
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("AlertConditionId")
|
.HasForeignKey("AlertConditionId")
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
@@ -551,6 +974,74 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Navigation("AlertCondition");
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
@@ -573,6 +1064,36 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
@@ -603,20 +1124,72 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Rules");
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("DeviceUsers");
|
b.Navigation("DeviceUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Images");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("DeviceUsers");
|
b.Navigation("DeviceUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Images");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
185
src/GreenHome.Infrastructure/MonthlyReportService.cs
Normal file
185
src/GreenHome.Infrastructure/MonthlyReportService.cs
Normal file
@@ -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<MonthlyReportDto> 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<string>();
|
||||||
|
|
||||||
|
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 "نیاز به بررسی 🔧";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
225
src/GreenHome.Infrastructure/UserDailyReportService.cs
Normal file
225
src/GreenHome.Infrastructure/UserDailyReportService.cs
Normal file
@@ -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<PagedResult<UserDailyReportDto>> 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<List<UserDailyReportDto>>(items);
|
||||||
|
|
||||||
|
return new PagedResult<UserDailyReportDto>
|
||||||
|
{
|
||||||
|
Items = dtos,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Page = filter.Page,
|
||||||
|
PageSize = filter.PageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDailyReportDto?> 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<UserDailyReportDto>(report) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> 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<int> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user