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 = "خطای سرور در پردازش درخواست" }); 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> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Scalar.AspNetCore" Version="2.11.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

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

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 public interface IAlertService
{ {
Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken); Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken);
} }

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> /// <param name="cancellationToken">Cancellation token</param>
/// <returns>Daily report with AI analysis</returns> /// <returns>Daily report with AI analysis</returns>
Task<DailyReportResponse> GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken); Task<DailyReportResponse> GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken);
Task<DailyReportResponse> GetWeeklyAnalysisAsync(WeeklyAnalysisRequest request, CancellationToken cancellationToken);
Task<DailyReportResponse> GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken);
} }

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<CreateAlertRuleRequest, Domain.AlertRule>();
CreateMap<Domain.User, UserDto>().ReverseMap(); CreateMap<Domain.User, UserDto>().ReverseMap();
CreateMap<Domain.AlertLog, AlertLogDto>()
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name))
.ForMember(dest => dest.UserMobile, opt => opt.MapFrom(src => src.User.Mobile))
.ReverseMap()
.ForMember(dest => dest.Device, opt => opt.Ignore())
.ForMember(dest => dest.User, opt => opt.Ignore())
.ForMember(dest => dest.AlertCondition, opt => opt.Ignore());
CreateMap<Domain.UserDailyReport, UserDailyReportDto>()
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name))
.ForMember(dest => dest.UserFamily, opt => opt.MapFrom(src => src.User.Family));
CreateMap<Domain.ReportImage, ReportImageDto>();
CreateMap<Domain.Checklist, ChecklistDto>()
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
.ForMember(dest => dest.CreatedByUserName, opt => opt.MapFrom(src => src.CreatedByUser.Name + " " + src.CreatedByUser.Family));
CreateMap<Domain.ChecklistItem, ChecklistItemDto>();
CreateMap<Domain.ChecklistCompletion, ChecklistCompletionDto>()
.ForMember(dest => dest.ChecklistTitle, opt => opt.MapFrom(src => src.Checklist.Title))
.ForMember(dest => dest.CompletedByUserName, opt => opt.MapFrom(src => src.CompletedByUser.Name + " " + src.CompletedByUser.Family));
CreateMap<Domain.ChecklistItemCompletion, ChecklistItemCompletionDto>()
.ForMember(dest => dest.ItemTitle, opt => opt.MapFrom(src => src.ChecklistItem.Title));
CreateMap<Domain.DevicePost, DevicePostDto>()
.ForMember(dest => dest.AuthorName, opt => opt.MapFrom(src => src.AuthorUser.Name))
.ForMember(dest => dest.AuthorFamily, opt => opt.MapFrom(src => src.AuthorUser.Family));
CreateMap<Domain.DevicePostImage, DevicePostImageDto>();
} }
} }

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 Device Device { get; set; } = null!;
public int UserId { get; set; } public int UserId { get; set; }
public User User { get; set; } = null!; public User User { get; set; } = null!;
public int AlertConditionId { get; set; } public int? AlertConditionId { get; set; }
public AlertCondition AlertCondition { get; set; } = null!; public AlertCondition? AlertCondition { get; set; }
public AlertNotificationType NotificationType { get; set; } // Call or SMS public AlertNotificationType NotificationType { get; set; } // Call or SMS
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS) public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS)

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> /// </summary>
public decimal? Longitude { get; set; } public decimal? Longitude { get; set; }
/// <summary>
/// نوع محصول
/// </summary>
public string ProductType { get; set; } = string.Empty;
/// <summary>
/// حداقل فاصله زمانی ارسال پیامک (به دقیقه)
/// </summary>
public int MinimumSmsIntervalMinutes { get; set; } = 15;
/// <summary>
/// حداقل فاصله زمانی تماس (به دقیقه)
/// </summary>
public int MinimumCallIntervalMinutes { get; set; } = 60;
/// <summary>
/// مساحت گلخانه (متر مربع)
/// </summary>
public decimal? AreaSquareMeters { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; } public DateTime UpdatedAt { get; set; }
} }

View File

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

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

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} یافت نشد"); throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
} }
// Get device settings (including ProductType if available)
var deviceSettings = await _context.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Query telemetry data for the specified date // Query telemetry data for the specified date
var telemetryRecords = await _context.TelemetryRecords var telemetryRecords = await _context.TelemetryRecords
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate) .Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
@@ -115,7 +119,15 @@ public class DailyReportService : IDailyReportService
} }
// Prepare the question for AI // Prepare the question for AI
var question = $@"این داده‌های تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند: var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
var areaInfo = deviceSettings?.AreaSquareMeters != null
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
: string.Empty;
var question = $@"این داده‌های تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
{dataBuilder} {dataBuilder}
@@ -123,7 +135,7 @@ public class DailyReportService : IDailyReportService
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا 1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
2. روندهای مشاهده شده در طول روز 2. روندهای مشاهده شده در طول روز
3. هر گونه نکته یا هشدار مهم 3. هر گونه نکته یا هشدار مهم
4. پیشنهادات برای بهبود شرایط گلخانه 4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)}
خلاصه و مفید باش (حداکثر 300 کلمه)."; خلاصه و مفید باش (حداکثر 300 کلمه).";
@@ -133,12 +145,16 @@ public class DailyReportService : IDailyReportService
try try
{ {
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی."
: "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی.";
var chatRequest = new ChatRequest var chatRequest = new ChatRequest
{ {
Model = "deepseek-chat", Model = "deepseek-chat",
Messages = new List<ChatMessage> Messages = new List<ChatMessage>
{ {
new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی." }, new() { Role = "system", Content = systemMessage },
new() { Role = "user", Content = question } new() { Role = "user", Content = question }
}, },
Temperature = 0.7 Temperature = 0.7
@@ -225,5 +241,239 @@ public class DailyReportService : IDailyReportService
return true; return true;
} }
public async Task<DailyReportResponse> GetWeeklyAnalysisAsync(
WeeklyAnalysisRequest request,
CancellationToken cancellationToken)
{
// Get device info
var device = await _context.Devices
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
if (device == null)
{
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
}
// Get device settings
var deviceSettings = await _context.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Query telemetry data for the week
var telemetryRecords = await _context.TelemetryRecords
.Where(t => t.DeviceId == request.DeviceId &&
string.Compare(t.PersianDate, request.StartDate) >= 0 &&
string.Compare(t.PersianDate, request.EndDate) <= 0)
.OrderBy(t => t.TimestampUtc)
.Select(t => new
{
t.TimestampUtc,
t.TemperatureC,
t.HumidityPercent,
t.Lux,
t.GasPPM,
t.PersianDate
})
.ToListAsync(cancellationToken);
if (telemetryRecords.Count == 0)
{
throw new InvalidOperationException(
$"هیچ رکوردی برای دستگاه {request.DeviceId} در بازه {request.StartDate} تا {request.EndDate} یافت نشد");
}
// Sample 1 per 100 records
var sampledRecords = telemetryRecords
.Select((record, index) => new { record, index })
.Where(x => x.index % 100 == 0)
.Select(x => x.record)
.ToList();
_logger.LogInformation(
"تعداد {TotalCount} رکورد یافت شد. نمونه‌برداری هفتگی: {SampledCount} رکورد",
telemetryRecords.Count, sampledRecords.Count);
// Build the data string
var dataBuilder = new StringBuilder();
dataBuilder.AppendLine("تاریخ | زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
dataBuilder.AppendLine("---------|----------|----------|-----------|-----------|----------");
foreach (var record in sampledRecords)
{
var localTime = record.TimestampUtc.AddHours(3.5);
dataBuilder.AppendLine(
$"{record.PersianDate} | {localTime:HH:mm} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
}
// Prepare AI prompt
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
var areaInfo = deviceSettings?.AreaSquareMeters != null
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
: string.Empty;
var question = $@"این داده‌های تلمتری یک هفته ({request.StartDate} تا {request.EndDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
{dataBuilder}
لطفاً یک تحلیل جامع هفتگی بده که شامل:
1. خلاصه روند هفتگی دما، رطوبت، نور و کیفیت هوا
2. مقایسه شرایط در روزهای مختلف هفته
3. نکات و هشدارهای مهم
4. توصیه‌ها برای هفته آینده
خلاصه و کاربردی باش (حداکثر 500 کلمه).";
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی."
: "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی.";
// Send to DeepSeek
var stopwatch = Stopwatch.StartNew();
var chatRequest = new ChatRequest
{
Model = "deepseek-chat",
Messages = new List<ChatMessage>
{
new() { Role = "system", Content = systemMessage },
new() { Role = "user", Content = question }
},
Temperature = 0.7
};
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
stopwatch.Stop();
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
{
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
}
return new DailyReportResponse
{
Id = 0,
DeviceId = request.DeviceId,
DeviceName = device.DeviceName,
PersianDate = $"{request.StartDate} تا {request.EndDate}",
Analysis = aiResponse.Choices[0].Message!.Content,
RecordCount = telemetryRecords.Count,
SampledRecordCount = sampledRecords.Count,
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
CreatedAt = DateTime.UtcNow,
FromCache = false
};
}
public async Task<DailyReportResponse> GetMonthlyAnalysisAsync(
MonthlyAnalysisRequest request,
CancellationToken cancellationToken)
{
// Get device info
var device = await _context.Devices
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
if (device == null)
{
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
}
// Get device settings
var deviceSettings = await _context.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Get all daily reports for this month
var dailyReports = await _context.DailyReports
.Where(dr => dr.DeviceId == request.DeviceId &&
dr.PersianYear == request.Year &&
dr.PersianMonth == request.Month)
.OrderBy(dr => dr.PersianDay)
.Select(dr => new { dr.PersianDate, dr.Analysis })
.ToListAsync(cancellationToken);
if (dailyReports.Count == 0)
{
throw new InvalidOperationException(
$"هیچ تحلیل روزانه‌ای برای دستگاه {request.DeviceId} در ماه {request.Month} سال {request.Year} یافت نشد");
}
// Build summary of daily analyses
var summaryBuilder = new StringBuilder();
summaryBuilder.AppendLine($"تحلیل‌های روزانه ماه {request.Month} سال {request.Year}:");
summaryBuilder.AppendLine();
foreach (var report in dailyReports)
{
summaryBuilder.AppendLine($"📅 {report.PersianDate}:");
summaryBuilder.AppendLine(report.Analysis);
summaryBuilder.AppendLine();
summaryBuilder.AppendLine("---");
summaryBuilder.AppendLine();
}
// Prepare AI prompt
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
var areaInfo = deviceSettings?.AreaSquareMeters != null
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
: string.Empty;
var question = $@"این تحلیل‌های روزانه یک ماه ({request.Month}/{request.Year}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
{summaryBuilder}
لطفاً یک تحلیل جامع ماهانه بده که شامل:
1. خلاصه کلی عملکرد ماه
2. روندهای اصلی و تغییرات مهم
3. نقاط قوت و ضعف
4. توصیه‌های کلیدی برای ماه آینده
5. نکات مهم برای بهبود بهره‌وری
جامع و کاربردی باش (حداکثر 800 کلمه).";
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری. تحلیل‌های روزانه رو بررسی کن و یک جمع‌بندی ماهانه جامع ارائه بده."
: "تو یک متخصص کشاورزی و گلخانه هستی. تحلیل‌های روزانه رو بررسی کن و یک جمع‌بندی ماهانه جامع ارائه بده.";
// Send to DeepSeek
var stopwatch = Stopwatch.StartNew();
var chatRequest = new ChatRequest
{
Model = "deepseek-chat",
Messages = new List<ChatMessage>
{
new() { Role = "system", Content = systemMessage },
new() { Role = "user", Content = question }
},
Temperature = 0.7
};
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
stopwatch.Stop();
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
{
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
}
return new DailyReportResponse
{
Id = 0,
DeviceId = request.DeviceId,
DeviceName = device.DeviceName,
PersianDate = $"ماه {request.Month} سال {request.Year}",
Analysis = aiResponse.Choices[0].Message!.Content,
RecordCount = dailyReports.Count,
SampledRecordCount = dailyReports.Count,
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
CreatedAt = DateTime.UtcNow,
FromCache = false
};
}
} }

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.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>(); public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>(); public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
public DbSet<Domain.AlertLog> AlertLogs => Set<Domain.AlertLog>();
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>(); public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>(); public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
public DbSet<Domain.UserDailyReport> UserDailyReports => Set<Domain.UserDailyReport>();
public DbSet<Domain.ReportImage> ReportImages => Set<Domain.ReportImage>();
public DbSet<Domain.Checklist> Checklists => Set<Domain.Checklist>();
public DbSet<Domain.ChecklistItem> ChecklistItems => Set<Domain.ChecklistItem>();
public DbSet<Domain.ChecklistCompletion> ChecklistCompletions => Set<Domain.ChecklistCompletion>();
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -58,6 +67,10 @@ public sealed class GreenHomeDbContext : DbContext
b.Property(x => x.City).HasMaxLength(100); b.Property(x => x.City).HasMaxLength(100);
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)"); b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
b.Property(x => x.Longitude).HasColumnType("decimal(9,6)"); b.Property(x => x.Longitude).HasColumnType("decimal(9,6)");
b.Property(x => x.ProductType).HasMaxLength(100);
b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15);
b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60);
b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)");
b.HasOne(x => x.Device) b.HasOne(x => x.Device)
.WithMany() .WithMany()
.HasForeignKey(x => x.DeviceId) .HasForeignKey(x => x.DeviceId)
@@ -112,6 +125,7 @@ public sealed class GreenHomeDbContext : DbContext
{ {
b.ToTable("DeviceUsers"); b.ToTable("DeviceUsers");
b.HasKey(x => new { x.DeviceId, x.UserId }); b.HasKey(x => new { x.DeviceId, x.UserId });
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
b.HasOne(x => x.Device) b.HasOne(x => x.Device)
.WithMany(d => d.DeviceUsers) .WithMany(d => d.DeviceUsers)
.HasForeignKey(x => x.DeviceId) .HasForeignKey(x => x.DeviceId)
@@ -189,5 +203,169 @@ public sealed class GreenHomeDbContext : DbContext
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth }); b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.CreatedAt); b.HasIndex(x => x.CreatedAt);
}); });
modelBuilder.Entity<Domain.AlertLog>(b =>
{
b.ToTable("AlertLogs");
b.HasKey(x => x.Id);
b.Property(x => x.AlertType).IsRequired().HasConversion<int>();
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
b.Property(x => x.Message).IsRequired().HasMaxLength(1000);
b.Property(x => x.Status).IsRequired().HasConversion<int>();
b.Property(x => x.ErrorMessage).HasMaxLength(2000);
b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.AlertCondition)
.WithMany()
.HasForeignKey(x => x.AlertConditionId)
.OnDelete(DeleteBehavior.SetNull);
b.HasIndex(x => new { x.DeviceId, x.SentAt });
b.HasIndex(x => new { x.UserId, x.SentAt });
b.HasIndex(x => x.AlertType);
b.HasIndex(x => x.Status);
});
modelBuilder.Entity<Domain.UserDailyReport>(b =>
{
b.ToTable("UserDailyReports");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
b.Property(x => x.Observations).IsRequired();
b.Property(x => x.Operations).IsRequired();
b.Property(x => x.Notes).HasMaxLength(2000);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.PersianDate });
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.UserId);
});
modelBuilder.Entity<Domain.ReportImage>(b =>
{
b.ToTable("ReportImages");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
b.Property(x => x.Description).HasMaxLength(500);
b.HasOne(x => x.UserDailyReport)
.WithMany(r => r.Images)
.HasForeignKey(x => x.UserDailyReportId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.UserDailyReportId);
});
modelBuilder.Entity<Domain.Checklist>(b =>
{
b.ToTable("Checklists");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
b.Property(x => x.Description).HasMaxLength(1000);
b.Property(x => x.IsActive).IsRequired();
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.CreatedByUser)
.WithMany()
.HasForeignKey(x => x.CreatedByUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.IsActive });
});
modelBuilder.Entity<Domain.ChecklistItem>(b =>
{
b.ToTable("ChecklistItems");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired().HasMaxLength(300);
b.Property(x => x.Description).HasMaxLength(1000);
b.Property(x => x.Order).IsRequired();
b.Property(x => x.IsRequired).IsRequired();
b.HasOne(x => x.Checklist)
.WithMany(c => c.Items)
.HasForeignKey(x => x.ChecklistId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.ChecklistId, x.Order });
});
modelBuilder.Entity<Domain.ChecklistCompletion>(b =>
{
b.ToTable("ChecklistCompletions");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Notes).HasMaxLength(2000);
b.HasOne(x => x.Checklist)
.WithMany(c => c.Completions)
.HasForeignKey(x => x.ChecklistId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.CompletedByUser)
.WithMany()
.HasForeignKey(x => x.CompletedByUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.ChecklistId, x.PersianDate });
b.HasIndex(x => x.CompletedByUserId);
});
modelBuilder.Entity<Domain.ChecklistItemCompletion>(b =>
{
b.ToTable("ChecklistItemCompletions");
b.HasKey(x => x.Id);
b.Property(x => x.IsChecked).IsRequired();
b.Property(x => x.Note).HasMaxLength(500);
b.HasOne(x => x.ChecklistCompletion)
.WithMany(cc => cc.ItemCompletions)
.HasForeignKey(x => x.ChecklistCompletionId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.ChecklistItem)
.WithMany()
.HasForeignKey(x => x.ChecklistItemId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => x.ChecklistCompletionId);
});
modelBuilder.Entity<Domain.DevicePost>(b =>
{
b.ToTable("DevicePosts");
b.HasKey(x => x.Id);
b.Property(x => x.Content).IsRequired().HasMaxLength(5000);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.AuthorUser)
.WithMany()
.HasForeignKey(x => x.AuthorUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.CreatedAt });
b.HasIndex(x => x.AuthorUserId);
});
modelBuilder.Entity<Domain.DevicePostImage>(b =>
{
b.ToTable("DevicePostImages");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
b.HasOne(x => x.DevicePost)
.WithMany(p => p.Images)
.HasForeignKey(x => x.DevicePostId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.DevicePostId);
});
} }
} }

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); b.ToTable("AlertConditions", (string)null);
}); });
modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("AlertConditionId")
.HasColumnType("int");
b.Property<int>("AlertType")
.HasColumnType("int");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("ErrorMessage")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int>("NotificationType")
.HasColumnType("int");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<long>("ProcessingTimeMs")
.HasColumnType("bigint");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime2");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AlertConditionId");
b.HasIndex("AlertType");
b.HasIndex("Status");
b.HasIndex("DeviceId", "SentAt");
b.HasIndex("UserId", "SentAt");
b.ToTable("AlertLogs", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -124,7 +185,7 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AlertConditionId") b.Property<int?>("AlertConditionId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("DeviceId") b.Property<int>("DeviceId")
@@ -199,6 +260,142 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("AlertRules", (string)null); b.ToTable("AlertRules", (string)null);
}); });
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("CreatedByUserId")
.HasColumnType("int");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("CreatedByUserId");
b.HasIndex("DeviceId", "IsActive");
b.ToTable("Checklists", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChecklistId")
.HasColumnType("int");
b.Property<DateTime>("CompletedAt")
.HasColumnType("datetime2");
b.Property<int>("CompletedByUserId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.HasKey("Id");
b.HasIndex("CompletedByUserId");
b.HasIndex("ChecklistId", "PersianDate");
b.ToTable("ChecklistCompletions", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChecklistId")
.HasColumnType("int");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.HasKey("Id");
b.HasIndex("ChecklistId", "Order");
b.ToTable("ChecklistItems", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("ChecklistCompletionId")
.HasColumnType("int");
b.Property<int>("ChecklistItemId")
.HasColumnType("int");
b.Property<bool>("IsChecked")
.HasColumnType("bit");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("ChecklistCompletionId");
b.HasIndex("ChecklistItemId");
b.ToTable("ChecklistItemCompletions", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b => modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -298,6 +495,79 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("Devices", (string)null); b.ToTable("Devices", (string)null);
}); });
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AuthorUserId")
.HasColumnType("int");
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(5000)
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("AuthorUserId");
b.HasIndex("DeviceId", "CreatedAt");
b.ToTable("DevicePosts", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("DevicePostId")
.HasColumnType("int");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("FilePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<DateTime>("UploadedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DevicePostId");
b.ToTable("DevicePostImages", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -306,6 +576,9 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal?>("AreaSquareMeters")
.HasColumnType("decimal(18,2)");
b.Property<string>("City") b.Property<string>("City")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
@@ -323,6 +596,21 @@ namespace GreenHome.Infrastructure.Migrations
b.Property<decimal?>("Longitude") b.Property<decimal?>("Longitude")
.HasColumnType("decimal(9,6)"); .HasColumnType("decimal(9,6)");
b.Property<int>("MinimumCallIntervalMinutes")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(60);
b.Property<int>("MinimumSmsIntervalMinutes")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(15);
b.Property<string>("ProductType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Province") b.Property<string>("Province")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
@@ -347,6 +635,11 @@ namespace GreenHome.Infrastructure.Migrations
b.Property<int>("UserId") b.Property<int>("UserId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<bool>("ReceiveAlerts")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.HasKey("DeviceId", "UserId"); b.HasKey("DeviceId", "UserId");
b.HasIndex("UserId"); b.HasIndex("UserId");
@@ -354,6 +647,49 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("DeviceUsers", (string)null); b.ToTable("DeviceUsers", (string)null);
}); });
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("FilePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<DateTime>("UploadedAt")
.HasColumnType("datetime2");
b.Property<int>("UserDailyReportId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserDailyReportId");
b.ToTable("ReportImages", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b => modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -448,6 +784,68 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("Users", (string)null); b.ToTable("Users", (string)null);
}); });
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("Observations")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Operations")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianDay")
.HasColumnType("int");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("DeviceId", "PersianDate");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("UserDailyReports", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b => modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -513,13 +911,38 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("Device"); b.Navigation("Device");
}); });
modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany()
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("AlertCondition");
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{ {
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany() .WithMany()
.HasForeignKey("AlertConditionId") .HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict);
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device") b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany() .WithMany()
@@ -551,6 +974,74 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("AlertCondition"); b.Navigation("AlertCondition");
}); });
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
{
b.HasOne("GreenHome.Domain.User", "CreatedByUser")
.WithMany()
.HasForeignKey("CreatedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedByUser");
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
{
b.HasOne("GreenHome.Domain.Checklist", "Checklist")
.WithMany("Completions")
.HasForeignKey("ChecklistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "CompletedByUser")
.WithMany()
.HasForeignKey("CompletedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Checklist");
b.Navigation("CompletedByUser");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
{
b.HasOne("GreenHome.Domain.Checklist", "Checklist")
.WithMany("Items")
.HasForeignKey("ChecklistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Checklist");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
{
b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion")
.WithMany("ItemCompletions")
.HasForeignKey("ChecklistCompletionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem")
.WithMany()
.HasForeignKey("ChecklistItemId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ChecklistCompletion");
b.Navigation("ChecklistItem");
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b => modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{ {
b.HasOne("GreenHome.Domain.Device", "Device") b.HasOne("GreenHome.Domain.Device", "Device")
@@ -573,6 +1064,36 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
{
b.HasOne("GreenHome.Domain.User", "AuthorUser")
.WithMany()
.HasForeignKey("AuthorUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AuthorUser");
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
{
b.HasOne("GreenHome.Domain.DevicePost", "DevicePost")
.WithMany("Images")
.HasForeignKey("DevicePostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DevicePost");
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{ {
b.HasOne("GreenHome.Domain.Device", "Device") b.HasOne("GreenHome.Domain.Device", "Device")
@@ -603,20 +1124,72 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
{
b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport")
.WithMany("Images")
.HasForeignKey("UserDailyReportId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UserDailyReport");
});
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{ {
b.Navigation("Rules"); b.Navigation("Rules");
}); });
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
{
b.Navigation("Completions");
b.Navigation("Items");
});
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
{
b.Navigation("ItemCompletions");
});
modelBuilder.Entity("GreenHome.Domain.Device", b => modelBuilder.Entity("GreenHome.Domain.Device", b =>
{ {
b.Navigation("DeviceUsers"); b.Navigation("DeviceUsers");
}); });
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
{
b.Navigation("Images");
});
modelBuilder.Entity("GreenHome.Domain.User", b => modelBuilder.Entity("GreenHome.Domain.User", b =>
{ {
b.Navigation("DeviceUsers"); b.Navigation("DeviceUsers");
}); });
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
{
b.Navigation("Images");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

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