version 3

This commit is contained in:
2025-12-17 00:34:41 +03:30
parent 139924db94
commit 74e8480a68
38 changed files with 5399 additions and 70 deletions

View 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);
}
}

View 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 });
}
}
}

View File

@@ -73,5 +73,71 @@ public class DailyReportController : ControllerBase
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 = "خطا در دریافت تحلیل ماهانه" });
}
}
}

View 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 });
}
}

View 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 = "خطا در ایجاد گزارش ماهانه" });
}
}
}

View 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 = "خطا در ارسال هشدار قطع برق" });
}
}
}

View 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 });
}
}
}

View File

@@ -13,7 +13,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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>

View File

@@ -5,12 +5,13 @@ using GreenHome.Infrastructure;
using GreenHome.Sms.Ippanel;
using GreenHome.VoiceCall.Avanak;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddOpenApi();
// Application/Infrastructure DI
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
@@ -60,6 +61,11 @@ builder.Services.AddScoped<GreenHome.Application.IAlertConditionService, GreenHo
builder.Services.AddScoped<GreenHome.Application.ISunCalculatorService, GreenHome.Infrastructure.SunCalculatorService>();
builder.Services.AddScoped<GreenHome.Application.IAIQueryService, GreenHome.Infrastructure.AIQueryService>();
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
builder.Services.AddIppanelSms(builder.Configuration);
@@ -89,11 +95,8 @@ using (var scope = app.Services.CreateScope())
}
// Configure the HTTP request pipeline.
//if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapOpenApi();
app.MapScalarApiReference();
// HTTPS Redirection فقط در Production
if (!app.Environment.IsDevelopment())

View 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; }
}

View 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;
}

View File

@@ -123,6 +123,11 @@ public sealed class DeviceSettingsDto
public decimal? Latitude { get; set; }
public decimal? Longitude { get; set; }
public string ProductType { get; set; } = string.Empty;
public int MinimumSmsIntervalMinutes { get; set; } = 15;
public int MinimumCallIntervalMinutes { get; set; } = 60;
public decimal? AreaSquareMeters { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
@@ -203,3 +208,108 @@ public sealed class DailyReportResponse
public DateTime CreatedAt { get; set; }
public bool FromCache { get; set; }
}
public sealed class WeeklyAnalysisRequest
{
public required int DeviceId { get; set; }
public required string StartDate { get; set; } // yyyy/MM/dd
public required string EndDate { get; set; } // yyyy/MM/dd
}
public sealed class MonthlyAnalysisRequest
{
public required int DeviceId { get; set; }
public required int Year { get; set; }
public required int Month { get; set; }
}
public sealed class AlertLogDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public int? AlertConditionId { get; set; }
public Domain.AlertType AlertType { get; set; }
public Domain.AlertNotificationType NotificationType { get; set; }
public string Message { get; set; } = string.Empty;
public Domain.AlertStatus Status { get; set; }
public string? ErrorMessage { get; set; }
public string PhoneNumber { get; set; } = string.Empty;
public DateTime SentAt { get; set; }
public long ProcessingTimeMs { get; set; }
}
public sealed class AlertLogFilter
{
public int? DeviceId { get; set; }
public int? UserId { get; set; }
public Domain.AlertType? AlertType { get; set; }
public Domain.AlertStatus? Status { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class UserDailyReportDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserFamily { get; set; } = string.Empty;
public string PersianDate { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Observations { get; set; } = string.Empty;
public string Operations { get; set; } = string.Empty;
public string? Notes { get; set; }
public List<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;
}

View 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);
}

View File

@@ -3,5 +3,6 @@ namespace GreenHome.Application;
public interface IAlertService
{
Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken);
}

View 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);
}

View File

@@ -9,5 +9,9 @@ public interface IDailyReportService
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Daily report with AI analysis</returns>
Task<DailyReportResponse> GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken);
Task<DailyReportResponse> GetWeeklyAnalysisAsync(WeeklyAnalysisRequest request, CancellationToken cancellationToken);
Task<DailyReportResponse> GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken);
}

View 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);
}

View 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; }
}

View 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);
}

View File

@@ -32,5 +32,40 @@ public sealed class MappingProfile : Profile
CreateMap<CreateAlertRuleRequest, Domain.AlertRule>();
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>();
}
}

View 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
}

View File

@@ -7,8 +7,8 @@ public sealed class AlertNotification
public Device Device { get; set; } = null!;
public int UserId { get; set; }
public User User { get; set; } = null!;
public int AlertConditionId { get; set; }
public AlertCondition AlertCondition { get; set; } = null!;
public int? AlertConditionId { get; set; }
public AlertCondition? AlertCondition { get; set; }
public AlertNotificationType NotificationType { get; set; } // Call or SMS
public string Message { get; set; } = string.Empty;
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS)

View 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; }
}

View 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; }
}

View File

@@ -26,6 +26,26 @@ public sealed class DeviceSettings
/// </summary>
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 UpdatedAt { get; set; }
}

View File

@@ -6,5 +6,10 @@ public sealed class DeviceUser
public Device Device { get; set; } = null!;
public int UserId { get; set; }
public User User { get; set; } = null!;
/// <summary>
/// آیا این کاربر باید هشدارهای این دستگاه را دریافت کند؟
/// </summary>
public bool ReceiveAlerts { get; set; } = true;
}

View 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; }
}

View 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;
}
}

View File

@@ -35,14 +35,28 @@ public sealed class AlertService : IAlertService
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
{
// Get device with settings and user
// Get device with all users who should receive alerts
var device = await dbContext.Devices
.Include(d => d.User)
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
.ThenInclude(du => du.User)
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
if (device == null || device.User == null)
if (device == null)
{
logger.LogWarning("Device or user not found: DeviceId={DeviceId}", deviceId);
logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId);
return;
}
// Get all users who should receive alerts
var usersToAlert = device.DeviceUsers
.Where(du => du.ReceiveAlerts)
.Select(du => du.User)
.ToList();
if (usersToAlert.Count == 0)
{
logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId);
return;
}
@@ -87,7 +101,7 @@ public sealed class AlertService : IAlertService
if (allRulesMatch && condition.Rules.Any())
{
// All rules passed, send alert if cooldown period has passed
await SendAlertForConditionAsync(condition, device, telemetry, cancellationToken);
await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken);
}
}
}
@@ -119,6 +133,7 @@ public sealed class AlertService : IAlertService
private async Task SendAlertForConditionAsync(
Domain.AlertCondition condition,
Domain.Device device,
List<Domain.User> usersToAlert,
TelemetryDto telemetry,
CancellationToken cancellationToken)
{
@@ -127,68 +142,95 @@ public sealed class AlertService : IAlertService
? condition.CallCooldownMinutes
: condition.SmsCooldownMinutes;
// Check if alert was sent recently
var cooldownTime = DateTime.UtcNow.AddMinutes(-cooldownMinutes);
var recentAlert = await dbContext.AlertNotifications
.Where(a => a.DeviceId == device.Id &&
a.UserId == device.User.Id &&
a.AlertConditionId == condition.Id &&
a.SentAt >= cooldownTime)
.FirstOrDefaultAsync(cancellationToken);
if (recentAlert != null)
{
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
device.Id, condition.Id, condition.NotificationType);
return;
}
// Build alert message
// Build alert message once
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
var sentAt = DateTime.UtcNow;
// Send notification
string? messageOutboxIds = null;
string? errorMessage = null;
bool isSent = false;
// Send alert to each user
foreach (var user in usersToAlert)
{
// Check if alert was sent recently to this user
var cooldownTime = sentAt.AddMinutes(-cooldownMinutes);
var recentAlert = await dbContext.AlertNotifications
.Where(a => a.DeviceId == device.Id &&
a.UserId == user.Id &&
a.AlertConditionId == condition.Id &&
a.SentAt >= cooldownTime)
.FirstOrDefaultAsync(cancellationToken);
try
{
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
if (recentAlert != null)
{
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
device.Id, user.Id, condition.Id);
continue;
}
else // Call
// Send notification
var startTime = DateTime.UtcNow;
string? messageOutboxIds = null;
string? errorMessage = null;
bool isSent = false;
try
{
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
{
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
}
else // Call
{
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
}
}
}
catch (Exception ex)
{
errorMessage = $"Exception: {ex.Message}";
if (ex.InnerException != null)
catch (Exception ex)
{
errorMessage += $" | InnerException: {ex.InnerException.Message}";
errorMessage = $"Exception: {ex.Message}";
if (ex.InnerException != null)
{
errorMessage += $" | InnerException: {ex.InnerException.Message}";
}
isSent = false;
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
device.Id, user.Id, condition.Id);
}
isSent = false;
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
device.Id, condition.Id, condition.NotificationType);
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
// Save notification to database (old table for backwards compatibility)
var notification = new Domain.AlertNotification
{
DeviceId = device.Id,
UserId = user.Id,
AlertConditionId = condition.Id,
NotificationType = condition.NotificationType,
Message = message,
MessageOutboxIds = messageOutboxIds,
ErrorMessage = errorMessage,
SentAt = sentAt,
IsSent = isSent
};
dbContext.AlertNotifications.Add(notification);
// Log the alert
var alertLog = new Domain.AlertLog
{
DeviceId = device.Id,
UserId = user.Id,
AlertConditionId = condition.Id,
AlertType = Domain.AlertType.Condition,
NotificationType = condition.NotificationType,
Message = message,
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
ErrorMessage = errorMessage,
PhoneNumber = user.Mobile,
SentAt = sentAt,
ProcessingTimeMs = processingTime
};
dbContext.AlertLogs.Add(alertLog);
}
// Save notification to database
var notification = new Domain.AlertNotification
{
DeviceId = device.Id,
UserId = device.User.Id,
AlertConditionId = condition.Id,
NotificationType = condition.NotificationType,
Message = message,
MessageOutboxIds = messageOutboxIds,
ErrorMessage = errorMessage,
SentAt = DateTime.UtcNow,
IsSent = isSent
};
dbContext.AlertNotifications.Add(notification);
await dbContext.SaveChangesAsync(cancellationToken);
}
@@ -323,5 +365,131 @@ public sealed class AlertService : IAlertService
return (false, null, errorMsg);
}
}
public async Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken)
{
// Get device with all users who should receive alerts
var device = await dbContext.Devices
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
.ThenInclude(du => du.User)
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
if (device == null)
{
logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId);
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
}
// Get all users who should receive alerts
var usersToAlert = device.DeviceUsers
.Where(du => du.ReceiveAlerts)
.Select(du => du.User)
.ToList();
if (usersToAlert.Count == 0)
{
logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId);
return;
}
var message = $"⚠️ هشدار قطع برق! دستگاه {device.DeviceName} از برق قطع شده است.";
var sentAt = DateTime.UtcNow;
// Send to all users (both SMS and Call for power outage - it's critical!)
foreach (var user in usersToAlert)
{
// Send SMS
await SendPowerOutageNotificationAsync(
device, user, message, sentAt,
Domain.AlertNotificationType.SMS,
cancellationToken);
// Send Call (important alert)
await SendPowerOutageNotificationAsync(
device, user, message, sentAt,
Domain.AlertNotificationType.Call,
cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}",
usersToAlert.Count, deviceId);
}
private async Task SendPowerOutageNotificationAsync(
Domain.Device device,
Domain.User user,
string message,
DateTime sentAt,
Domain.AlertNotificationType notificationType,
CancellationToken cancellationToken)
{
var startTime = DateTime.UtcNow;
string? messageOutboxIds = null;
string? errorMessage = null;
bool isSent = false;
try
{
if (notificationType == Domain.AlertNotificationType.SMS)
{
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(
user.Mobile, device.DeviceName, message, cancellationToken);
}
else
{
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(
user.Mobile, device.DeviceName, message, cancellationToken);
}
}
catch (Exception ex)
{
errorMessage = $"Exception: {ex.Message}";
if (ex.InnerException != null)
{
errorMessage += $" | InnerException: {ex.InnerException.Message}";
}
isSent = false;
logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}",
device.Id, user.Id, notificationType);
}
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
// Save notification (old table)
var notification = new Domain.AlertNotification
{
DeviceId = device.Id,
UserId = user.Id,
AlertConditionId = null,
NotificationType = notificationType,
Message = message,
MessageOutboxIds = messageOutboxIds,
ErrorMessage = errorMessage,
SentAt = sentAt,
IsSent = isSent
};
dbContext.AlertNotifications.Add(notification);
// Log the alert
var alertLog = new Domain.AlertLog
{
DeviceId = device.Id,
UserId = user.Id,
AlertConditionId = null,
AlertType = Domain.AlertType.PowerOutage,
NotificationType = notificationType,
Message = message,
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
ErrorMessage = errorMessage,
PhoneNumber = user.Mobile,
SentAt = sentAt,
ProcessingTimeMs = processingTime
};
dbContext.AlertLogs.Add(alertLog);
}
}

View 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;
}
}

View File

@@ -70,6 +70,10 @@ public class DailyReportService : IDailyReportService
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
}
// Get device settings (including ProductType if available)
var deviceSettings = await _context.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Query telemetry data for the specified date
var telemetryRecords = await _context.TelemetryRecords
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
@@ -115,7 +119,15 @@ public class DailyReportService : IDailyReportService
}
// Prepare the question for AI
var question = $@"این داده‌های تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند:
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
var areaInfo = deviceSettings?.AreaSquareMeters != null
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
: string.Empty;
var question = $@"این داده‌های تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
{dataBuilder}
@@ -123,7 +135,7 @@ public class DailyReportService : IDailyReportService
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
2. روندهای مشاهده شده در طول روز
3. هر گونه نکته یا هشدار مهم
4. پیشنهادات برای بهبود شرایط گلخانه
4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)}
خلاصه و مفید باش (حداکثر 300 کلمه).";
@@ -133,12 +145,16 @@ public class DailyReportService : IDailyReportService
try
{
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی."
: "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی.";
var chatRequest = new ChatRequest
{
Model = "deepseek-chat",
Messages = new List<ChatMessage>
{
new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی." },
new() { Role = "system", Content = systemMessage },
new() { Role = "user", Content = question }
},
Temperature = 0.7
@@ -225,5 +241,239 @@ public class DailyReportService : IDailyReportService
return true;
}
public async Task<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
};
}
}

View 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;
}
}

View File

@@ -15,8 +15,17 @@ public sealed class GreenHomeDbContext : DbContext
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
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.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)
{
@@ -58,6 +67,10 @@ public sealed class GreenHomeDbContext : DbContext
b.Property(x => x.City).HasMaxLength(100);
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
b.Property(x => x.Longitude).HasColumnType("decimal(9,6)");
b.Property(x => x.ProductType).HasMaxLength(100);
b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15);
b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60);
b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)");
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
@@ -112,6 +125,7 @@ public sealed class GreenHomeDbContext : DbContext
{
b.ToTable("DeviceUsers");
b.HasKey(x => new { x.DeviceId, x.UserId });
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
b.HasOne(x => x.Device)
.WithMany(d => d.DeviceUsers)
.HasForeignKey(x => x.DeviceId)
@@ -189,5 +203,169 @@ public sealed class GreenHomeDbContext : DbContext
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.CreatedAt);
});
modelBuilder.Entity<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);
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}
}

View File

@@ -116,6 +116,67 @@ namespace GreenHome.Infrastructure.Migrations
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 =>
{
b.Property<int>("Id")
@@ -124,7 +185,7 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AlertConditionId")
b.Property<int?>("AlertConditionId")
.HasColumnType("int");
b.Property<int>("DeviceId")
@@ -199,6 +260,142 @@ namespace GreenHome.Infrastructure.Migrations
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 =>
{
b.Property<int>("Id")
@@ -298,6 +495,79 @@ namespace GreenHome.Infrastructure.Migrations
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 =>
{
b.Property<int>("Id")
@@ -306,6 +576,9 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal?>("AreaSquareMeters")
.HasColumnType("decimal(18,2)");
b.Property<string>("City")
.IsRequired()
.HasMaxLength(100)
@@ -323,6 +596,21 @@ namespace GreenHome.Infrastructure.Migrations
b.Property<decimal?>("Longitude")
.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")
.IsRequired()
.HasMaxLength(100)
@@ -347,6 +635,11 @@ namespace GreenHome.Infrastructure.Migrations
b.Property<int>("UserId")
.HasColumnType("int");
b.Property<bool>("ReceiveAlerts")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.HasKey("DeviceId", "UserId");
b.HasIndex("UserId");
@@ -354,6 +647,49 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("DeviceUsers", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
{
b.Property<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 =>
{
b.Property<int>("Id")
@@ -448,6 +784,68 @@ namespace GreenHome.Infrastructure.Migrations
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 =>
{
b.Property<int>("Id")
@@ -513,13 +911,38 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany()
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("AlertCondition");
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany()
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
@@ -551,6 +974,74 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("AlertCondition");
});
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
{
b.HasOne("GreenHome.Domain.User", "CreatedByUser")
.WithMany()
.HasForeignKey("CreatedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedByUser");
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
{
b.HasOne("GreenHome.Domain.Checklist", "Checklist")
.WithMany("Completions")
.HasForeignKey("ChecklistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "CompletedByUser")
.WithMany()
.HasForeignKey("CompletedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Checklist");
b.Navigation("CompletedByUser");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
{
b.HasOne("GreenHome.Domain.Checklist", "Checklist")
.WithMany("Items")
.HasForeignKey("ChecklistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Checklist");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
{
b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion")
.WithMany("ItemCompletions")
.HasForeignKey("ChecklistCompletionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem")
.WithMany()
.HasForeignKey("ChecklistItemId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ChecklistCompletion");
b.Navigation("ChecklistItem");
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
@@ -573,6 +1064,36 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
{
b.HasOne("GreenHome.Domain.User", "AuthorUser")
.WithMany()
.HasForeignKey("AuthorUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AuthorUser");
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
{
b.HasOne("GreenHome.Domain.DevicePost", "DevicePost")
.WithMany("Images")
.HasForeignKey("DevicePostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DevicePost");
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
@@ -603,20 +1124,72 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
{
b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport")
.WithMany("Images")
.HasForeignKey("UserDailyReportId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UserDailyReport");
});
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.Navigation("Rules");
});
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
{
b.Navigation("Completions");
b.Navigation("Items");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
{
b.Navigation("ItemCompletions");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Navigation("DeviceUsers");
});
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
{
b.Navigation("Images");
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Navigation("DeviceUsers");
});
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
{
b.Navigation("Images");
});
#pragma warning restore 612, 618
}
}

View 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 "نیاز به بررسی 🔧";
}
}

View 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;
}
}