version 3
This commit is contained in:
100
src/GreenHome.Infrastructure/AlertLogService.cs
Normal file
100
src/GreenHome.Infrastructure/AlertLogService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class AlertLogService : IAlertLogService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public AlertLogService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<AlertLogDto>> GetAlertLogsAsync(
|
||||
AlertLogFilter filter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.AlertLogs
|
||||
.Include(x => x.Device)
|
||||
.Include(x => x.User)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
if (filter.DeviceId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.DeviceId == filter.DeviceId.Value);
|
||||
}
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.UserId == filter.UserId.Value);
|
||||
}
|
||||
|
||||
if (filter.AlertType.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.AlertType == filter.AlertType.Value);
|
||||
}
|
||||
|
||||
if (filter.Status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == filter.Status.Value);
|
||||
}
|
||||
|
||||
if (filter.StartDate.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.SentAt >= filter.StartDate.Value);
|
||||
}
|
||||
|
||||
if (filter.EndDate.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.SentAt <= filter.EndDate.Value);
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
// Apply pagination and ordering
|
||||
var items = await query
|
||||
.OrderByDescending(x => x.SentAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var dtos = _mapper.Map<List<AlertLogDto>>(items);
|
||||
|
||||
return new PagedResult<AlertLogDto>
|
||||
{
|
||||
Items = dtos,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var log = await _context.AlertLogs
|
||||
.Include(x => x.Device)
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.AlertCondition)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
return log != null ? _mapper.Map<AlertLogDto>(log) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = _mapper.Map<Domain.AlertLog>(dto);
|
||||
_context.AlertLogs.Add(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +35,28 @@ public sealed class AlertService : IAlertService
|
||||
|
||||
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device with settings and user
|
||||
// Get device with all users who should receive alerts
|
||||
var device = await dbContext.Devices
|
||||
.Include(d => d.User)
|
||||
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||
.ThenInclude(du => du.User)
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null || device.User == null)
|
||||
if (device == null)
|
||||
{
|
||||
logger.LogWarning("Device or user not found: DeviceId={DeviceId}", deviceId);
|
||||
logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all users who should receive alerts
|
||||
var usersToAlert = device.DeviceUsers
|
||||
.Where(du => du.ReceiveAlerts)
|
||||
.Select(du => du.User)
|
||||
.ToList();
|
||||
|
||||
if (usersToAlert.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +101,7 @@ public sealed class AlertService : IAlertService
|
||||
if (allRulesMatch && condition.Rules.Any())
|
||||
{
|
||||
// All rules passed, send alert if cooldown period has passed
|
||||
await SendAlertForConditionAsync(condition, device, telemetry, cancellationToken);
|
||||
await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +133,7 @@ public sealed class AlertService : IAlertService
|
||||
private async Task SendAlertForConditionAsync(
|
||||
Domain.AlertCondition condition,
|
||||
Domain.Device device,
|
||||
List<Domain.User> usersToAlert,
|
||||
TelemetryDto telemetry,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -127,68 +142,95 @@ public sealed class AlertService : IAlertService
|
||||
? condition.CallCooldownMinutes
|
||||
: condition.SmsCooldownMinutes;
|
||||
|
||||
// Check if alert was sent recently
|
||||
var cooldownTime = DateTime.UtcNow.AddMinutes(-cooldownMinutes);
|
||||
var recentAlert = await dbContext.AlertNotifications
|
||||
.Where(a => a.DeviceId == device.Id &&
|
||||
a.UserId == device.User.Id &&
|
||||
a.AlertConditionId == condition.Id &&
|
||||
a.SentAt >= cooldownTime)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (recentAlert != null)
|
||||
{
|
||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
|
||||
device.Id, condition.Id, condition.NotificationType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build alert message
|
||||
// Build alert message once
|
||||
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
|
||||
var sentAt = DateTime.UtcNow;
|
||||
|
||||
// Send notification
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
// Send alert to each user
|
||||
foreach (var user in usersToAlert)
|
||||
{
|
||||
// Check if alert was sent recently to this user
|
||||
var cooldownTime = sentAt.AddMinutes(-cooldownMinutes);
|
||||
var recentAlert = await dbContext.AlertNotifications
|
||||
.Where(a => a.DeviceId == device.Id &&
|
||||
a.UserId == user.Id &&
|
||||
a.AlertConditionId == condition.Id &&
|
||||
a.SentAt >= cooldownTime)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
||||
if (recentAlert != null)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
|
||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
|
||||
device.Id, user.Id, condition.Id);
|
||||
continue;
|
||||
}
|
||||
else // Call
|
||||
|
||||
// Send notification
|
||||
var startTime = DateTime.UtcNow;
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
|
||||
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
else // Call
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
|
||||
device.Id, user.Id, condition.Id);
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
|
||||
device.Id, condition.Id, condition.NotificationType);
|
||||
|
||||
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Save notification to database (old table for backwards compatibility)
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = sentAt,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
|
||||
// Log the alert
|
||||
var alertLog = new Domain.AlertLog
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
AlertType = Domain.AlertType.Condition,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
|
||||
ErrorMessage = errorMessage,
|
||||
PhoneNumber = user.Mobile,
|
||||
SentAt = sentAt,
|
||||
ProcessingTimeMs = processingTime
|
||||
};
|
||||
|
||||
dbContext.AlertLogs.Add(alertLog);
|
||||
}
|
||||
|
||||
// Save notification to database
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = device.User.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = DateTime.UtcNow,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -323,5 +365,131 @@ public sealed class AlertService : IAlertService
|
||||
return (false, null, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device with all users who should receive alerts
|
||||
var device = await dbContext.Devices
|
||||
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||
.ThenInclude(du => du.User)
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId);
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get all users who should receive alerts
|
||||
var usersToAlert = device.DeviceUsers
|
||||
.Where(du => du.ReceiveAlerts)
|
||||
.Select(du => du.User)
|
||||
.ToList();
|
||||
|
||||
if (usersToAlert.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
var message = $"⚠️ هشدار قطع برق! دستگاه {device.DeviceName} از برق قطع شده است.";
|
||||
var sentAt = DateTime.UtcNow;
|
||||
|
||||
// Send to all users (both SMS and Call for power outage - it's critical!)
|
||||
foreach (var user in usersToAlert)
|
||||
{
|
||||
// Send SMS
|
||||
await SendPowerOutageNotificationAsync(
|
||||
device, user, message, sentAt,
|
||||
Domain.AlertNotificationType.SMS,
|
||||
cancellationToken);
|
||||
|
||||
// Send Call (important alert)
|
||||
await SendPowerOutageNotificationAsync(
|
||||
device, user, message, sentAt,
|
||||
Domain.AlertNotificationType.Call,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}",
|
||||
usersToAlert.Count, deviceId);
|
||||
}
|
||||
|
||||
private async Task SendPowerOutageNotificationAsync(
|
||||
Domain.Device device,
|
||||
Domain.User user,
|
||||
string message,
|
||||
DateTime sentAt,
|
||||
Domain.AlertNotificationType notificationType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var startTime = DateTime.UtcNow;
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (notificationType == Domain.AlertNotificationType.SMS)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(
|
||||
user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(
|
||||
user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}",
|
||||
device.Id, user.Id, notificationType);
|
||||
}
|
||||
|
||||
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Save notification (old table)
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = null,
|
||||
NotificationType = notificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = sentAt,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
|
||||
// Log the alert
|
||||
var alertLog = new Domain.AlertLog
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = null,
|
||||
AlertType = Domain.AlertType.PowerOutage,
|
||||
NotificationType = notificationType,
|
||||
Message = message,
|
||||
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
|
||||
ErrorMessage = errorMessage,
|
||||
PhoneNumber = user.Mobile,
|
||||
SentAt = sentAt,
|
||||
ProcessingTimeMs = processingTime
|
||||
};
|
||||
|
||||
dbContext.AlertLogs.Add(alertLog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
src/GreenHome.Infrastructure/ChecklistService.cs
Normal file
152
src/GreenHome.Infrastructure/ChecklistService.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class ChecklistService : IChecklistService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ChecklistService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<ChecklistDto?> GetActiveChecklistByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklist = await _context.Checklists
|
||||
.Include(c => c.Device)
|
||||
.Include(c => c.CreatedByUser)
|
||||
.Include(c => c.Items.OrderBy(i => i.Order))
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.DeviceId == deviceId && c.IsActive, cancellationToken);
|
||||
|
||||
return checklist != null ? _mapper.Map<ChecklistDto>(checklist) : null;
|
||||
}
|
||||
|
||||
public async Task<List<ChecklistDto>> GetChecklistsByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklists = await _context.Checklists
|
||||
.Include(c => c.Device)
|
||||
.Include(c => c.CreatedByUser)
|
||||
.Include(c => c.Items.OrderBy(i => i.Order))
|
||||
.AsNoTracking()
|
||||
.Where(c => c.DeviceId == deviceId)
|
||||
.OrderByDescending(c => c.IsActive)
|
||||
.ThenByDescending(c => c.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return _mapper.Map<List<ChecklistDto>>(checklists);
|
||||
}
|
||||
|
||||
public async Task<ChecklistDto?> GetChecklistByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklist = await _context.Checklists
|
||||
.Include(c => c.Device)
|
||||
.Include(c => c.CreatedByUser)
|
||||
.Include(c => c.Items.OrderBy(i => i.Order))
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
|
||||
|
||||
return checklist != null ? _mapper.Map<ChecklistDto>(checklist) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateChecklistAsync(CreateChecklistRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Deactivate existing active checklist for this device
|
||||
var existingActiveChecklist = await _context.Checklists
|
||||
.FirstOrDefaultAsync(c => c.DeviceId == request.DeviceId && c.IsActive, cancellationToken);
|
||||
|
||||
if (existingActiveChecklist != null)
|
||||
{
|
||||
existingActiveChecklist.IsActive = false;
|
||||
}
|
||||
|
||||
// Create new checklist
|
||||
var checklist = new Domain.Checklist
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
CreatedByUserId = request.CreatedByUserId,
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Add items
|
||||
foreach (var itemRequest in request.Items)
|
||||
{
|
||||
var item = new Domain.ChecklistItem
|
||||
{
|
||||
Title = itemRequest.Title,
|
||||
Description = itemRequest.Description,
|
||||
Order = itemRequest.Order,
|
||||
IsRequired = itemRequest.IsRequired
|
||||
};
|
||||
checklist.Items.Add(item);
|
||||
}
|
||||
|
||||
_context.Checklists.Add(checklist);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return checklist.Id;
|
||||
}
|
||||
|
||||
public async Task<List<ChecklistCompletionDto>> GetCompletionsByChecklistIdAsync(
|
||||
int checklistId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var completions = await _context.ChecklistCompletions
|
||||
.Include(cc => cc.Checklist)
|
||||
.Include(cc => cc.CompletedByUser)
|
||||
.Include(cc => cc.ItemCompletions)
|
||||
.ThenInclude(ic => ic.ChecklistItem)
|
||||
.AsNoTracking()
|
||||
.Where(cc => cc.ChecklistId == checklistId)
|
||||
.OrderByDescending(cc => cc.CompletedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return _mapper.Map<List<ChecklistCompletionDto>>(completions);
|
||||
}
|
||||
|
||||
public async Task<int> CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklist = await _context.Checklists
|
||||
.Include(c => c.Items)
|
||||
.FirstOrDefaultAsync(c => c.Id == request.ChecklistId, cancellationToken);
|
||||
|
||||
if (checklist == null)
|
||||
{
|
||||
throw new InvalidOperationException($"چکلیست با شناسه {request.ChecklistId} یافت نشد");
|
||||
}
|
||||
|
||||
var completion = new Domain.ChecklistCompletion
|
||||
{
|
||||
ChecklistId = request.ChecklistId,
|
||||
CompletedByUserId = request.CompletedByUserId,
|
||||
PersianDate = request.PersianDate,
|
||||
Notes = request.Notes,
|
||||
CompletedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
foreach (var itemCompletion in request.ItemCompletions)
|
||||
{
|
||||
var item = new Domain.ChecklistItemCompletion
|
||||
{
|
||||
ChecklistItemId = itemCompletion.ChecklistItemId,
|
||||
IsChecked = itemCompletion.IsChecked,
|
||||
Note = itemCompletion.Note
|
||||
};
|
||||
completion.ItemCompletions.Add(item);
|
||||
}
|
||||
|
||||
_context.ChecklistCompletions.Add(completion);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return completion.Id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ public class DailyReportService : IDailyReportService
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get device settings (including ProductType if available)
|
||||
var deviceSettings = await _context.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||||
|
||||
// Query telemetry data for the specified date
|
||||
var telemetryRecords = await _context.TelemetryRecords
|
||||
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
|
||||
@@ -115,7 +119,15 @@ public class DailyReportService : IDailyReportService
|
||||
}
|
||||
|
||||
// Prepare the question for AI
|
||||
var question = $@"این دادههای تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند:
|
||||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||||
: string.Empty;
|
||||
|
||||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||||
: string.Empty;
|
||||
|
||||
var question = $@"این دادههای تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||||
|
||||
{dataBuilder}
|
||||
|
||||
@@ -123,7 +135,7 @@ public class DailyReportService : IDailyReportService
|
||||
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
||||
2. روندهای مشاهده شده در طول روز
|
||||
3. هر گونه نکته یا هشدار مهم
|
||||
4. پیشنهادات برای بهبود شرایط گلخانه
|
||||
4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)}
|
||||
|
||||
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
||||
|
||||
@@ -133,12 +145,16 @@ public class DailyReportService : IDailyReportService
|
||||
|
||||
try
|
||||
{
|
||||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||||
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||||
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی." },
|
||||
new() { Role = "system", Content = systemMessage },
|
||||
new() { Role = "user", Content = question }
|
||||
},
|
||||
Temperature = 0.7
|
||||
@@ -225,5 +241,239 @@ public class DailyReportService : IDailyReportService
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<DailyReportResponse> GetWeeklyAnalysisAsync(
|
||||
WeeklyAnalysisRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device info
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get device settings
|
||||
var deviceSettings = await _context.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||||
|
||||
// Query telemetry data for the week
|
||||
var telemetryRecords = await _context.TelemetryRecords
|
||||
.Where(t => t.DeviceId == request.DeviceId &&
|
||||
string.Compare(t.PersianDate, request.StartDate) >= 0 &&
|
||||
string.Compare(t.PersianDate, request.EndDate) <= 0)
|
||||
.OrderBy(t => t.TimestampUtc)
|
||||
.Select(t => new
|
||||
{
|
||||
t.TimestampUtc,
|
||||
t.TemperatureC,
|
||||
t.HumidityPercent,
|
||||
t.Lux,
|
||||
t.GasPPM,
|
||||
t.PersianDate
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (telemetryRecords.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"هیچ رکوردی برای دستگاه {request.DeviceId} در بازه {request.StartDate} تا {request.EndDate} یافت نشد");
|
||||
}
|
||||
|
||||
// Sample 1 per 100 records
|
||||
var sampledRecords = telemetryRecords
|
||||
.Select((record, index) => new { record, index })
|
||||
.Where(x => x.index % 100 == 0)
|
||||
.Select(x => x.record)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation(
|
||||
"تعداد {TotalCount} رکورد یافت شد. نمونهبرداری هفتگی: {SampledCount} رکورد",
|
||||
telemetryRecords.Count, sampledRecords.Count);
|
||||
|
||||
// Build the data string
|
||||
var dataBuilder = new StringBuilder();
|
||||
dataBuilder.AppendLine("تاریخ | زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
|
||||
dataBuilder.AppendLine("---------|----------|----------|-----------|-----------|----------");
|
||||
|
||||
foreach (var record in sampledRecords)
|
||||
{
|
||||
var localTime = record.TimestampUtc.AddHours(3.5);
|
||||
dataBuilder.AppendLine(
|
||||
$"{record.PersianDate} | {localTime:HH:mm} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
|
||||
}
|
||||
|
||||
// Prepare AI prompt
|
||||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||||
: string.Empty;
|
||||
|
||||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||||
: string.Empty;
|
||||
|
||||
var question = $@"این دادههای تلمتری یک هفته ({request.StartDate} تا {request.EndDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||||
|
||||
{dataBuilder}
|
||||
|
||||
لطفاً یک تحلیل جامع هفتگی بده که شامل:
|
||||
1. خلاصه روند هفتگی دما، رطوبت، نور و کیفیت هوا
|
||||
2. مقایسه شرایط در روزهای مختلف هفته
|
||||
3. نکات و هشدارهای مهم
|
||||
4. توصیهها برای هفته آینده
|
||||
|
||||
خلاصه و کاربردی باش (حداکثر 500 کلمه).";
|
||||
|
||||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||||
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||||
|
||||
// Send to DeepSeek
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = systemMessage },
|
||||
new() { Role = "user", Content = question }
|
||||
},
|
||||
Temperature = 0.7
|
||||
};
|
||||
|
||||
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||||
stopwatch.Stop();
|
||||
|
||||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||||
{
|
||||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||||
}
|
||||
|
||||
return new DailyReportResponse
|
||||
{
|
||||
Id = 0,
|
||||
DeviceId = request.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
PersianDate = $"{request.StartDate} تا {request.EndDate}",
|
||||
Analysis = aiResponse.Choices[0].Message!.Content,
|
||||
RecordCount = telemetryRecords.Count,
|
||||
SampledRecordCount = sampledRecords.Count,
|
||||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
FromCache = false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DailyReportResponse> GetMonthlyAnalysisAsync(
|
||||
MonthlyAnalysisRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device info
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get device settings
|
||||
var deviceSettings = await _context.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||||
|
||||
// Get all daily reports for this month
|
||||
var dailyReports = await _context.DailyReports
|
||||
.Where(dr => dr.DeviceId == request.DeviceId &&
|
||||
dr.PersianYear == request.Year &&
|
||||
dr.PersianMonth == request.Month)
|
||||
.OrderBy(dr => dr.PersianDay)
|
||||
.Select(dr => new { dr.PersianDate, dr.Analysis })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (dailyReports.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"هیچ تحلیل روزانهای برای دستگاه {request.DeviceId} در ماه {request.Month} سال {request.Year} یافت نشد");
|
||||
}
|
||||
|
||||
// Build summary of daily analyses
|
||||
var summaryBuilder = new StringBuilder();
|
||||
summaryBuilder.AppendLine($"تحلیلهای روزانه ماه {request.Month} سال {request.Year}:");
|
||||
summaryBuilder.AppendLine();
|
||||
|
||||
foreach (var report in dailyReports)
|
||||
{
|
||||
summaryBuilder.AppendLine($"📅 {report.PersianDate}:");
|
||||
summaryBuilder.AppendLine(report.Analysis);
|
||||
summaryBuilder.AppendLine();
|
||||
summaryBuilder.AppendLine("---");
|
||||
summaryBuilder.AppendLine();
|
||||
}
|
||||
|
||||
// Prepare AI prompt
|
||||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||||
: string.Empty;
|
||||
|
||||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||||
: string.Empty;
|
||||
|
||||
var question = $@"این تحلیلهای روزانه یک ماه ({request.Month}/{request.Year}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||||
|
||||
{summaryBuilder}
|
||||
|
||||
لطفاً یک تحلیل جامع ماهانه بده که شامل:
|
||||
1. خلاصه کلی عملکرد ماه
|
||||
2. روندهای اصلی و تغییرات مهم
|
||||
3. نقاط قوت و ضعف
|
||||
4. توصیههای کلیدی برای ماه آینده
|
||||
5. نکات مهم برای بهبود بهرهوری
|
||||
|
||||
جامع و کاربردی باش (حداکثر 800 کلمه).";
|
||||
|
||||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری. تحلیلهای روزانه رو بررسی کن و یک جمعبندی ماهانه جامع ارائه بده."
|
||||
: "تو یک متخصص کشاورزی و گلخانه هستی. تحلیلهای روزانه رو بررسی کن و یک جمعبندی ماهانه جامع ارائه بده.";
|
||||
|
||||
// Send to DeepSeek
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = systemMessage },
|
||||
new() { Role = "user", Content = question }
|
||||
},
|
||||
Temperature = 0.7
|
||||
};
|
||||
|
||||
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||||
stopwatch.Stop();
|
||||
|
||||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||||
{
|
||||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||||
}
|
||||
|
||||
return new DailyReportResponse
|
||||
{
|
||||
Id = 0,
|
||||
DeviceId = request.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
PersianDate = $"ماه {request.Month} سال {request.Year}",
|
||||
Analysis = aiResponse.Choices[0].Message!.Content,
|
||||
RecordCount = dailyReports.Count,
|
||||
SampledRecordCount = dailyReports.Count,
|
||||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
FromCache = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
185
src/GreenHome.Infrastructure/DevicePostService.cs
Normal file
185
src/GreenHome.Infrastructure/DevicePostService.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class DevicePostService : IDevicePostService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public DevicePostService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<DevicePostDto>> GetPostsAsync(
|
||||
DevicePostFilter filter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.DevicePosts
|
||||
.Include(p => p.AuthorUser)
|
||||
.Include(p => p.Images)
|
||||
.AsNoTracking()
|
||||
.Where(p => p.DeviceId == filter.DeviceId);
|
||||
|
||||
if (filter.AuthorUserId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.AuthorUserId == filter.AuthorUserId.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var posts = await query
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var dtos = _mapper.Map<List<DevicePostDto>>(posts);
|
||||
|
||||
return new PagedResult<DevicePostDto>
|
||||
{
|
||||
Items = dtos,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DevicePostDto?> GetPostByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.Include(p => p.AuthorUser)
|
||||
.Include(p => p.Images)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
|
||||
return post != null ? _mapper.Map<DevicePostDto>(post) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreatePostAsync(
|
||||
CreateDevicePostRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Verify user has access to device
|
||||
var hasAccess = await CanUserAccessDeviceAsync(request.AuthorUserId, request.DeviceId, cancellationToken);
|
||||
if (!hasAccess)
|
||||
{
|
||||
throw new UnauthorizedAccessException("کاربر به این دستگاه دسترسی ندارد");
|
||||
}
|
||||
|
||||
var post = new Domain.DevicePost
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
AuthorUserId = request.AuthorUserId,
|
||||
Content = request.Content,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DevicePosts.Add(post);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return post.Id;
|
||||
}
|
||||
|
||||
public async Task UpdatePostAsync(
|
||||
UpdateDevicePostRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken);
|
||||
|
||||
if (post == null)
|
||||
{
|
||||
throw new InvalidOperationException($"پست با شناسه {request.Id} یافت نشد");
|
||||
}
|
||||
|
||||
post.Content = request.Content;
|
||||
post.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePostAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.Include(p => p.Images)
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
|
||||
if (post == null)
|
||||
{
|
||||
throw new InvalidOperationException($"پست با شناسه {id} یافت نشد");
|
||||
}
|
||||
|
||||
_context.DevicePosts.Remove(post);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> AddImageToPostAsync(
|
||||
int postId,
|
||||
string fileName,
|
||||
string filePath,
|
||||
string contentType,
|
||||
long fileSize,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.FirstOrDefaultAsync(p => p.Id == postId, cancellationToken);
|
||||
|
||||
if (post == null)
|
||||
{
|
||||
throw new InvalidOperationException($"پست با شناسه {postId} یافت نشد");
|
||||
}
|
||||
|
||||
var image = new Domain.DevicePostImage
|
||||
{
|
||||
DevicePostId = postId,
|
||||
FileName = fileName,
|
||||
FilePath = filePath,
|
||||
ContentType = contentType,
|
||||
FileSize = fileSize,
|
||||
UploadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DevicePostImages.Add(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return image.Id;
|
||||
}
|
||||
|
||||
public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken)
|
||||
{
|
||||
var image = await _context.DevicePostImages
|
||||
.FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken);
|
||||
|
||||
if (image == null)
|
||||
{
|
||||
throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد");
|
||||
}
|
||||
|
||||
_context.DevicePostImages.Remove(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> CanUserAccessDeviceAsync(
|
||||
int userId,
|
||||
int deviceId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if user is the device owner or has access through DeviceUsers
|
||||
var hasAccess = await _context.Devices
|
||||
.AnyAsync(d => d.Id == deviceId && d.UserId == userId, cancellationToken);
|
||||
|
||||
if (!hasAccess)
|
||||
{
|
||||
hasAccess = await _context.DeviceUsers
|
||||
.AnyAsync(du => du.DeviceId == deviceId && du.UserId == userId, cancellationToken);
|
||||
}
|
||||
|
||||
return hasAccess;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,17 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
||||
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
||||
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
||||
public DbSet<Domain.AlertLog> AlertLogs => Set<Domain.AlertLog>();
|
||||
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
|
||||
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
|
||||
public DbSet<Domain.UserDailyReport> UserDailyReports => Set<Domain.UserDailyReport>();
|
||||
public DbSet<Domain.ReportImage> ReportImages => Set<Domain.ReportImage>();
|
||||
public DbSet<Domain.Checklist> Checklists => Set<Domain.Checklist>();
|
||||
public DbSet<Domain.ChecklistItem> ChecklistItems => Set<Domain.ChecklistItem>();
|
||||
public DbSet<Domain.ChecklistCompletion> ChecklistCompletions => Set<Domain.ChecklistCompletion>();
|
||||
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
|
||||
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
|
||||
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -58,6 +67,10 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
b.Property(x => x.City).HasMaxLength(100);
|
||||
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
|
||||
b.Property(x => x.Longitude).HasColumnType("decimal(9,6)");
|
||||
b.Property(x => x.ProductType).HasMaxLength(100);
|
||||
b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15);
|
||||
b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60);
|
||||
b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)");
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
@@ -112,6 +125,7 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
{
|
||||
b.ToTable("DeviceUsers");
|
||||
b.HasKey(x => new { x.DeviceId, x.UserId });
|
||||
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany(d => d.DeviceUsers)
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
@@ -189,5 +203,169 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
|
||||
b.HasIndex(x => x.CreatedAt);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.AlertLog>(b =>
|
||||
{
|
||||
b.ToTable("AlertLogs");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.AlertType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.Message).IsRequired().HasMaxLength(1000);
|
||||
b.Property(x => x.Status).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasOne(x => x.AlertCondition)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.AlertConditionId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
b.HasIndex(x => new { x.DeviceId, x.SentAt });
|
||||
b.HasIndex(x => new { x.UserId, x.SentAt });
|
||||
b.HasIndex(x => x.AlertType);
|
||||
b.HasIndex(x => x.Status);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.UserDailyReport>(b =>
|
||||
{
|
||||
b.ToTable("UserDailyReports");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
b.Property(x => x.Observations).IsRequired();
|
||||
b.Property(x => x.Operations).IsRequired();
|
||||
b.Property(x => x.Notes).HasMaxLength(2000);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianDate });
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
|
||||
b.HasIndex(x => x.UserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ReportImage>(b =>
|
||||
{
|
||||
b.ToTable("ReportImages");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
|
||||
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
|
||||
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
|
||||
b.Property(x => x.Description).HasMaxLength(500);
|
||||
b.HasOne(x => x.UserDailyReport)
|
||||
.WithMany(r => r.Images)
|
||||
.HasForeignKey(x => x.UserDailyReportId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.UserDailyReportId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.Checklist>(b =>
|
||||
{
|
||||
b.ToTable("Checklists");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
b.Property(x => x.Description).HasMaxLength(1000);
|
||||
b.Property(x => x.IsActive).IsRequired();
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.CreatedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CreatedByUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.IsActive });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ChecklistItem>(b =>
|
||||
{
|
||||
b.ToTable("ChecklistItems");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(300);
|
||||
b.Property(x => x.Description).HasMaxLength(1000);
|
||||
b.Property(x => x.Order).IsRequired();
|
||||
b.Property(x => x.IsRequired).IsRequired();
|
||||
b.HasOne(x => x.Checklist)
|
||||
.WithMany(c => c.Items)
|
||||
.HasForeignKey(x => x.ChecklistId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => new { x.ChecklistId, x.Order });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ChecklistCompletion>(b =>
|
||||
{
|
||||
b.ToTable("ChecklistCompletions");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
|
||||
b.Property(x => x.Notes).HasMaxLength(2000);
|
||||
b.HasOne(x => x.Checklist)
|
||||
.WithMany(c => c.Completions)
|
||||
.HasForeignKey(x => x.ChecklistId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.CompletedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CompletedByUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.ChecklistId, x.PersianDate });
|
||||
b.HasIndex(x => x.CompletedByUserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ChecklistItemCompletion>(b =>
|
||||
{
|
||||
b.ToTable("ChecklistItemCompletions");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.IsChecked).IsRequired();
|
||||
b.Property(x => x.Note).HasMaxLength(500);
|
||||
b.HasOne(x => x.ChecklistCompletion)
|
||||
.WithMany(cc => cc.ItemCompletions)
|
||||
.HasForeignKey(x => x.ChecklistCompletionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.ChecklistItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ChecklistItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => x.ChecklistCompletionId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.DevicePost>(b =>
|
||||
{
|
||||
b.ToTable("DevicePosts");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Content).IsRequired().HasMaxLength(5000);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.AuthorUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.AuthorUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.CreatedAt });
|
||||
b.HasIndex(x => x.AuthorUserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.DevicePostImage>(b =>
|
||||
{
|
||||
b.ToTable("DevicePostImages");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
|
||||
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
|
||||
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
|
||||
b.HasOne(x => x.DevicePost)
|
||||
.WithMany(p => p.Images)
|
||||
.HasForeignKey(x => x.DevicePostId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.DevicePostId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
1199
src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs
generated
Normal file
1199
src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAllNewFeatures : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ReceiveAlerts",
|
||||
table: "DeviceUsers",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "AreaSquareMeters",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MinimumCallIntervalMinutes",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 60);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MinimumSmsIntervalMinutes",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 15);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ProductType",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
type: "int",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AlertLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
AlertConditionId = table.Column<int>(type: "int", nullable: true),
|
||||
AlertType = table.Column<int>(type: "int", nullable: false),
|
||||
NotificationType = table.Column<int>(type: "int", nullable: false),
|
||||
Message = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
ErrorMessage = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
SentAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ProcessingTimeMs = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AlertLogs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertLogs_AlertConditions_AlertConditionId",
|
||||
column: x => x.AlertConditionId,
|
||||
principalTable: "AlertConditions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertLogs_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertLogs_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Checklists",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedByUserId = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Checklists", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Checklists_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Checklists_Users_CreatedByUserId",
|
||||
column: x => x.CreatedByUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DevicePosts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
AuthorUserId = table.Column<int>(type: "int", nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 5000, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DevicePosts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DevicePosts_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DevicePosts_Users_AuthorUserId",
|
||||
column: x => x.AuthorUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDailyReports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
PersianYear = table.Column<int>(type: "int", nullable: false),
|
||||
PersianMonth = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDay = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Observations = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Operations = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDailyReports", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDailyReports_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDailyReports_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChecklistCompletions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ChecklistId = table.Column<int>(type: "int", nullable: false),
|
||||
CompletedByUserId = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChecklistCompletions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistCompletions_Checklists_ChecklistId",
|
||||
column: x => x.ChecklistId,
|
||||
principalTable: "Checklists",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistCompletions_Users_CompletedByUserId",
|
||||
column: x => x.CompletedByUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChecklistItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ChecklistId = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
Order = table.Column<int>(type: "int", nullable: false),
|
||||
IsRequired = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChecklistItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistItems_Checklists_ChecklistId",
|
||||
column: x => x.ChecklistId,
|
||||
principalTable: "Checklists",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DevicePostImages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DevicePostId = table.Column<int>(type: "int", nullable: false),
|
||||
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||
FilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||
UploadedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DevicePostImages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DevicePostImages_DevicePosts_DevicePostId",
|
||||
column: x => x.DevicePostId,
|
||||
principalTable: "DevicePosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReportImages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserDailyReportId = table.Column<int>(type: "int", nullable: false),
|
||||
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||
FilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
UploadedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ReportImages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ReportImages_UserDailyReports_UserDailyReportId",
|
||||
column: x => x.UserDailyReportId,
|
||||
principalTable: "UserDailyReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChecklistItemCompletions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ChecklistCompletionId = table.Column<int>(type: "int", nullable: false),
|
||||
ChecklistItemId = table.Column<int>(type: "int", nullable: false),
|
||||
IsChecked = table.Column<bool>(type: "bit", nullable: false),
|
||||
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChecklistItemCompletions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistItemCompletions_ChecklistCompletions_ChecklistCompletionId",
|
||||
column: x => x.ChecklistCompletionId,
|
||||
principalTable: "ChecklistCompletions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistItemCompletions_ChecklistItems_ChecklistItemId",
|
||||
column: x => x.ChecklistItemId,
|
||||
principalTable: "ChecklistItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_AlertConditionId",
|
||||
table: "AlertLogs",
|
||||
column: "AlertConditionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_AlertType",
|
||||
table: "AlertLogs",
|
||||
column: "AlertType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_DeviceId_SentAt",
|
||||
table: "AlertLogs",
|
||||
columns: new[] { "DeviceId", "SentAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_Status",
|
||||
table: "AlertLogs",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_UserId_SentAt",
|
||||
table: "AlertLogs",
|
||||
columns: new[] { "UserId", "SentAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistCompletions_ChecklistId_PersianDate",
|
||||
table: "ChecklistCompletions",
|
||||
columns: new[] { "ChecklistId", "PersianDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistCompletions_CompletedByUserId",
|
||||
table: "ChecklistCompletions",
|
||||
column: "CompletedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistItemCompletions_ChecklistCompletionId",
|
||||
table: "ChecklistItemCompletions",
|
||||
column: "ChecklistCompletionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistItemCompletions_ChecklistItemId",
|
||||
table: "ChecklistItemCompletions",
|
||||
column: "ChecklistItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistItems_ChecklistId_Order",
|
||||
table: "ChecklistItems",
|
||||
columns: new[] { "ChecklistId", "Order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Checklists_CreatedByUserId",
|
||||
table: "Checklists",
|
||||
column: "CreatedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Checklists_DeviceId_IsActive",
|
||||
table: "Checklists",
|
||||
columns: new[] { "DeviceId", "IsActive" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DevicePostImages_DevicePostId",
|
||||
table: "DevicePostImages",
|
||||
column: "DevicePostId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DevicePosts_AuthorUserId",
|
||||
table: "DevicePosts",
|
||||
column: "AuthorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DevicePosts_DeviceId_CreatedAt",
|
||||
table: "DevicePosts",
|
||||
columns: new[] { "DeviceId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReportImages_UserDailyReportId",
|
||||
table: "ReportImages",
|
||||
column: "UserDailyReportId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserDailyReports_DeviceId_PersianDate",
|
||||
table: "UserDailyReports",
|
||||
columns: new[] { "DeviceId", "PersianDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserDailyReports_DeviceId_PersianYear_PersianMonth",
|
||||
table: "UserDailyReports",
|
||||
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserDailyReports_UserId",
|
||||
table: "UserDailyReports",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AlertLogs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChecklistItemCompletions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DevicePostImages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReportImages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChecklistCompletions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChecklistItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DevicePosts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDailyReports");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Checklists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ReceiveAlerts",
|
||||
table: "DeviceUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AreaSquareMeters",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinimumCallIntervalMinutes",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinimumSmsIntervalMinutes",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProductType",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,67 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.ToTable("AlertConditions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("AlertConditionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AlertType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<int>("NotificationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<long>("ProcessingTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlertConditionId");
|
||||
|
||||
b.HasIndex("AlertType");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("DeviceId", "SentAt");
|
||||
|
||||
b.HasIndex("UserId", "SentAt");
|
||||
|
||||
b.ToTable("AlertLogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -124,7 +185,7 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlertConditionId")
|
||||
b.Property<int?>("AlertConditionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
@@ -199,6 +260,142 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.ToTable("AlertRules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("CreatedByUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedByUserId");
|
||||
|
||||
b.HasIndex("DeviceId", "IsActive");
|
||||
|
||||
b.ToTable("Checklists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ChecklistId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("CompletedByUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompletedByUserId");
|
||||
|
||||
b.HasIndex("ChecklistId", "PersianDate");
|
||||
|
||||
b.ToTable("ChecklistCompletions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ChecklistId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<bool>("IsRequired")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChecklistId", "Order");
|
||||
|
||||
b.ToTable("ChecklistItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ChecklistCompletionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ChecklistItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsChecked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChecklistCompletionId");
|
||||
|
||||
b.HasIndex("ChecklistItemId");
|
||||
|
||||
b.ToTable("ChecklistItemCompletions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -298,6 +495,79 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.ToTable("Devices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AuthorUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(5000)
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorUserId");
|
||||
|
||||
b.HasIndex("DeviceId", "CreatedAt");
|
||||
|
||||
b.ToTable("DevicePosts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("DevicePostId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.Property<string>("FilePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("UploadedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DevicePostId");
|
||||
|
||||
b.ToTable("DevicePostImages", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -306,6 +576,9 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal?>("AreaSquareMeters")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
@@ -323,6 +596,21 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.Property<decimal?>("Longitude")
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<int>("MinimumCallIntervalMinutes")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(60);
|
||||
|
||||
b.Property<int>("MinimumSmsIntervalMinutes")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(15);
|
||||
|
||||
b.Property<string>("ProductType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Province")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
@@ -347,6 +635,11 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("ReceiveAlerts")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.HasKey("DeviceId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
@@ -354,6 +647,49 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.ToTable("DeviceUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.Property<string>("FilePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("UploadedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("UserDailyReportId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserDailyReportId");
|
||||
|
||||
b.ToTable("ReportImages", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -448,6 +784,68 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("Observations")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Operations")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("PersianDay")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianDate");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||
|
||||
b.ToTable("UserDailyReports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -513,13 +911,38 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
|
||||
.WithMany()
|
||||
.HasForeignKey("AlertConditionId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AlertCondition");
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
|
||||
.WithMany()
|
||||
.HasForeignKey("AlertConditionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
@@ -551,6 +974,74 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.Navigation("AlertCondition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.User", "CreatedByUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedByUserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CreatedByUser");
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Checklist", "Checklist")
|
||||
.WithMany("Completions")
|
||||
.HasForeignKey("ChecklistId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "CompletedByUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("CompletedByUserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Checklist");
|
||||
|
||||
b.Navigation("CompletedByUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Checklist", "Checklist")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ChecklistId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Checklist");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion")
|
||||
.WithMany("ItemCompletions")
|
||||
.HasForeignKey("ChecklistCompletionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChecklistItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ChecklistCompletion");
|
||||
|
||||
b.Navigation("ChecklistItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
@@ -573,6 +1064,36 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.User", "AuthorUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorUserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AuthorUser");
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.DevicePost", "DevicePost")
|
||||
.WithMany("Images")
|
||||
.HasForeignKey("DevicePostId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DevicePost");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
@@ -603,20 +1124,72 @@ namespace GreenHome.Infrastructure.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport")
|
||||
.WithMany("Images")
|
||||
.HasForeignKey("UserDailyReportId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("UserDailyReport");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
|
||||
{
|
||||
b.Navigation("Rules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
|
||||
{
|
||||
b.Navigation("Completions");
|
||||
|
||||
b.Navigation("Items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
|
||||
{
|
||||
b.Navigation("ItemCompletions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
|
||||
{
|
||||
b.Navigation("Images");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
|
||||
{
|
||||
b.Navigation("Images");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
185
src/GreenHome.Infrastructure/MonthlyReportService.cs
Normal file
185
src/GreenHome.Infrastructure/MonthlyReportService.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class MonthlyReportService : IMonthlyReportService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
|
||||
public MonthlyReportService(GreenHomeDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<MonthlyReportDto> GetMonthlyReportAsync(
|
||||
int deviceId,
|
||||
int year,
|
||||
int month,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get alert logs for the month
|
||||
var alertLogs = await _context.AlertLogs
|
||||
.Where(al => al.DeviceId == deviceId &&
|
||||
al.SentAt.Year == year &&
|
||||
al.SentAt.Month == month)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalAlerts = alertLogs.Count;
|
||||
var smsAlerts = alertLogs.Count(al => al.NotificationType == Domain.AlertNotificationType.SMS);
|
||||
var callAlerts = alertLogs.Count(al => al.NotificationType == Domain.AlertNotificationType.Call);
|
||||
var successfulAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Success);
|
||||
var failedAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Failed);
|
||||
var powerOutageAlerts = alertLogs.Count(al => al.AlertType == Domain.AlertType.PowerOutage);
|
||||
|
||||
// Get telemetry statistics
|
||||
var telemetryData = await _context.TelemetryRecords
|
||||
.Where(t => t.DeviceId == deviceId &&
|
||||
t.PersianYear == year &&
|
||||
t.PersianMonth == month)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalTelemetryRecords = telemetryData.Count;
|
||||
var avgTemp = telemetryData.Any() ? telemetryData.Average(t => t.TemperatureC) : 0;
|
||||
var minTemp = telemetryData.Any() ? telemetryData.Min(t => t.TemperatureC) : 0;
|
||||
var maxTemp = telemetryData.Any() ? telemetryData.Max(t => t.TemperatureC) : 0;
|
||||
var avgHumidity = telemetryData.Any() ? telemetryData.Average(t => t.HumidityPercent) : 0;
|
||||
var minHumidity = telemetryData.Any() ? telemetryData.Min(t => t.HumidityPercent) : 0;
|
||||
var maxHumidity = telemetryData.Any() ? telemetryData.Max(t => t.HumidityPercent) : 0;
|
||||
var avgLux = telemetryData.Any() ? telemetryData.Average(t => t.Lux) : 0;
|
||||
var avgGas = telemetryData.Any() ? (int)telemetryData.Average(t => t.GasPPM) : 0;
|
||||
var maxGas = telemetryData.Any() ? telemetryData.Max(t => t.GasPPM) : 0;
|
||||
|
||||
// Get user activity
|
||||
var userReportsCount = await _context.UserDailyReports
|
||||
.CountAsync(r => r.DeviceId == deviceId &&
|
||||
r.PersianYear == year &&
|
||||
r.PersianMonth == month,
|
||||
cancellationToken);
|
||||
|
||||
var checklistCompletionsCount = await _context.ChecklistCompletions
|
||||
.Where(cc => cc.Checklist.DeviceId == deviceId)
|
||||
.CountAsync(cc => cc.PersianDate.StartsWith($"{year}/{month:D2}"), cancellationToken);
|
||||
|
||||
var dailyAnalysesCount = await _context.DailyReports
|
||||
.CountAsync(dr => dr.DeviceId == deviceId &&
|
||||
dr.PersianYear == year &&
|
||||
dr.PersianMonth == month,
|
||||
cancellationToken);
|
||||
|
||||
// Generate performance summary
|
||||
var performanceSummary = GeneratePerformanceSummary(
|
||||
totalTelemetryRecords,
|
||||
totalAlerts,
|
||||
successfulAlerts,
|
||||
failedAlerts,
|
||||
avgTemp,
|
||||
avgHumidity,
|
||||
avgLux,
|
||||
avgGas,
|
||||
userReportsCount,
|
||||
checklistCompletionsCount);
|
||||
|
||||
return new MonthlyReportDto
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
Year = year,
|
||||
Month = month,
|
||||
TotalAlerts = totalAlerts,
|
||||
SmsAlerts = smsAlerts,
|
||||
CallAlerts = callAlerts,
|
||||
SuccessfulAlerts = successfulAlerts,
|
||||
FailedAlerts = failedAlerts,
|
||||
PowerOutageAlerts = powerOutageAlerts,
|
||||
TotalTelemetryRecords = totalTelemetryRecords,
|
||||
AverageTemperature = avgTemp,
|
||||
MinTemperature = minTemp,
|
||||
MaxTemperature = maxTemp,
|
||||
AverageHumidity = avgHumidity,
|
||||
MinHumidity = minHumidity,
|
||||
MaxHumidity = maxHumidity,
|
||||
AverageLux = avgLux,
|
||||
AverageGasPPM = avgGas,
|
||||
MaxGasPPM = maxGas,
|
||||
UserDailyReportsCount = userReportsCount,
|
||||
ChecklistCompletionsCount = checklistCompletionsCount,
|
||||
DailyAnalysesCount = dailyAnalysesCount,
|
||||
PerformanceSummary = performanceSummary,
|
||||
GeneratedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private string GeneratePerformanceSummary(
|
||||
int totalRecords,
|
||||
int totalAlerts,
|
||||
int successfulAlerts,
|
||||
int failedAlerts,
|
||||
decimal avgTemp,
|
||||
decimal avgHumidity,
|
||||
decimal avgLux,
|
||||
int avgGas,
|
||||
int userReports,
|
||||
int checklistCompletions)
|
||||
{
|
||||
var summary = new List<string>();
|
||||
|
||||
summary.Add($"📊 آمار کلی:");
|
||||
summary.Add($" • تعداد رکوردهای ثبت شده: {totalRecords:N0}");
|
||||
summary.Add($" • میانگین دما: {avgTemp:F1}°C");
|
||||
summary.Add($" • میانگین رطوبت: {avgHumidity:F1}%");
|
||||
summary.Add($" • میانگین نور: {avgLux:F0} لوکس");
|
||||
if (avgGas > 0)
|
||||
summary.Add($" • میانگین CO: {avgGas} PPM");
|
||||
|
||||
summary.Add("");
|
||||
summary.Add($"🚨 هشدارها:");
|
||||
summary.Add($" • تعداد کل: {totalAlerts}");
|
||||
summary.Add($" • موفق: {successfulAlerts}");
|
||||
if (failedAlerts > 0)
|
||||
summary.Add($" • ناموفق: {failedAlerts} ⚠️");
|
||||
|
||||
if (userReports > 0 || checklistCompletions > 0)
|
||||
{
|
||||
summary.Add("");
|
||||
summary.Add($"📝 فعالیت کاربران:");
|
||||
if (userReports > 0)
|
||||
summary.Add($" • گزارشهای روزانه: {userReports}");
|
||||
if (checklistCompletions > 0)
|
||||
summary.Add($" • تکمیل چکلیست: {checklistCompletions}");
|
||||
}
|
||||
|
||||
// Performance rating
|
||||
summary.Add("");
|
||||
var rating = CalculatePerformanceRating(totalRecords, failedAlerts, totalAlerts);
|
||||
summary.Add($"⭐ ارزیابی کلی: {rating}");
|
||||
|
||||
return string.Join("\n", summary);
|
||||
}
|
||||
|
||||
private string CalculatePerformanceRating(int totalRecords, int failedAlerts, int totalAlerts)
|
||||
{
|
||||
if (totalRecords == 0)
|
||||
return "بدون داده";
|
||||
|
||||
var failureRate = totalAlerts > 0 ? (double)failedAlerts / totalAlerts : 0;
|
||||
|
||||
if (failureRate == 0 && totalRecords > 1000)
|
||||
return "عالی ✅";
|
||||
else if (failureRate < 0.1 && totalRecords > 500)
|
||||
return "خوب 👍";
|
||||
else if (failureRate < 0.3)
|
||||
return "متوسط ⚠️";
|
||||
else
|
||||
return "نیاز به بررسی 🔧";
|
||||
}
|
||||
}
|
||||
|
||||
225
src/GreenHome.Infrastructure/UserDailyReportService.cs
Normal file
225
src/GreenHome.Infrastructure/UserDailyReportService.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class UserDailyReportService : IUserDailyReportService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UserDailyReportService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<UserDailyReportDto>> GetReportsAsync(
|
||||
UserDailyReportFilter filter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.UserDailyReports
|
||||
.Include(r => r.Device)
|
||||
.Include(r => r.User)
|
||||
.Include(r => r.Images)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (filter.DeviceId.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.DeviceId == filter.DeviceId.Value);
|
||||
}
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.UserId == filter.UserId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PersianDate))
|
||||
{
|
||||
query = query.Where(r => r.PersianDate == filter.PersianDate);
|
||||
}
|
||||
|
||||
if (filter.Year.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.PersianYear == filter.Year.Value);
|
||||
}
|
||||
|
||||
if (filter.Month.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.PersianMonth == filter.Month.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(r => r.PersianDate)
|
||||
.ThenByDescending(r => r.CreatedAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var dtos = _mapper.Map<List<UserDailyReportDto>>(items);
|
||||
|
||||
return new PagedResult<UserDailyReportDto>
|
||||
{
|
||||
Items = dtos,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<UserDailyReportDto?> GetReportByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.Include(r => r.Device)
|
||||
.Include(r => r.User)
|
||||
.Include(r => r.Images)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
|
||||
|
||||
return report != null ? _mapper.Map<UserDailyReportDto>(report) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateReportAsync(
|
||||
CreateUserDailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Validate date format
|
||||
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day))
|
||||
{
|
||||
throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد");
|
||||
}
|
||||
|
||||
var report = new Domain.UserDailyReport
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = request.UserId,
|
||||
PersianDate = request.PersianDate,
|
||||
PersianYear = year,
|
||||
PersianMonth = month,
|
||||
PersianDay = day,
|
||||
Title = request.Title,
|
||||
Observations = request.Observations,
|
||||
Operations = request.Operations,
|
||||
Notes = request.Notes,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.UserDailyReports.Add(report);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return report.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateReportAsync(
|
||||
UpdateUserDailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new InvalidOperationException($"گزارش با شناسه {request.Id} یافت نشد");
|
||||
}
|
||||
|
||||
report.Title = request.Title;
|
||||
report.Observations = request.Observations;
|
||||
report.Operations = request.Operations;
|
||||
report.Notes = request.Notes;
|
||||
report.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteReportAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.Include(r => r.Images)
|
||||
.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new InvalidOperationException($"گزارش با شناسه {id} یافت نشد");
|
||||
}
|
||||
|
||||
_context.UserDailyReports.Remove(report);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> AddImageToReportAsync(
|
||||
int reportId,
|
||||
string fileName,
|
||||
string filePath,
|
||||
string contentType,
|
||||
long fileSize,
|
||||
string? description,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.FirstOrDefaultAsync(r => r.Id == reportId, cancellationToken);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new InvalidOperationException($"گزارش با شناسه {reportId} یافت نشد");
|
||||
}
|
||||
|
||||
var image = new Domain.ReportImage
|
||||
{
|
||||
UserDailyReportId = reportId,
|
||||
FileName = fileName,
|
||||
FilePath = filePath,
|
||||
ContentType = contentType,
|
||||
FileSize = fileSize,
|
||||
Description = description,
|
||||
UploadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.ReportImages.Add(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return image.Id;
|
||||
}
|
||||
|
||||
public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken)
|
||||
{
|
||||
var image = await _context.ReportImages
|
||||
.FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken);
|
||||
|
||||
if (image == null)
|
||||
{
|
||||
throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد");
|
||||
}
|
||||
|
||||
_context.ReportImages.Remove(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day)
|
||||
{
|
||||
year = month = day = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(persianDate))
|
||||
return false;
|
||||
|
||||
var parts = persianDate.Split('/');
|
||||
if (parts.Length != 3)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[1], out month) || month < 1 || month > 12)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[2], out day) || day < 1 || day > 31)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user