feat: Implementazione completa sistema schedulazione con intervalli personalizzati
- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi) - Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit - Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela - Implementata interfaccia UI completa con anteprima real-time in italiano - Aggiunta migrazione database AddIntervalSchedulingFields - Implementati metodi calcolo NextExecutionTime per intervalli - Aggiunta gestione tracking anti-duplicati e cleanup automatico - Creata documentazione completa (6 file, 2500+ righe) Modifiche tecniche: - ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription - ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing - ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync - Scheduling.razor: Aggiunta sezione UI per configurazione intervalli - Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
using CredentialManager.Models;
|
||||
using CredentialManager.Services;
|
||||
using Data_Coupler.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Data_Coupler.Pages;
|
||||
|
||||
public partial class Scheduling : ComponentBase
|
||||
{
|
||||
[Inject] private IProfileScheduleService ScheduleService { get; set; } = null!;
|
||||
[Inject] private IDataCouplerProfileService ProfileService { get; set; } = null!;
|
||||
[Inject] private IDataTransferService DataTransferService { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
[Inject] private ILogger<Scheduling> Logger { get; set; } = null!;
|
||||
|
||||
protected List<ProfileSchedule>? schedules;
|
||||
protected List<DataCouplerProfile>? availableProfiles;
|
||||
protected ProfileSchedule? editingSchedule;
|
||||
protected DataCouplerProfile? selectedProfile;
|
||||
protected bool isExecuting = false;
|
||||
protected List<ScheduleExecutionHistory>? executionHistory;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadSchedules();
|
||||
await LoadAvailableProfiles();
|
||||
}
|
||||
|
||||
protected async Task LoadSchedules()
|
||||
{
|
||||
try
|
||||
{
|
||||
schedules = await ScheduleService.GetAllSchedulesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento delle schedulazioni");
|
||||
await ShowErrorMessage("Errore nel caricamento delle schedulazioni: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task LoadAvailableProfiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
availableProfiles = await ScheduleService.GetAvailableProfilesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento dei profili disponibili");
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ShowCreateModal()
|
||||
{
|
||||
editingSchedule = new ProfileSchedule
|
||||
{
|
||||
Name = "",
|
||||
IsEnabled = true,
|
||||
ScheduleType = "",
|
||||
DailyTime = "09:00",
|
||||
IntervalValue = 5,
|
||||
IntervalUnit = "minutes"
|
||||
};
|
||||
|
||||
selectedProfile = null;
|
||||
await ShowModal();
|
||||
}
|
||||
|
||||
protected async Task ShowEditModal(ProfileSchedule schedule)
|
||||
{
|
||||
editingSchedule = new ProfileSchedule
|
||||
{
|
||||
Id = schedule.Id,
|
||||
Name = schedule.Name,
|
||||
Description = schedule.Description,
|
||||
ProfileId = schedule.ProfileId,
|
||||
IsEnabled = schedule.IsEnabled,
|
||||
ScheduleType = schedule.ScheduleType,
|
||||
ScheduledDateTime = schedule.ScheduledDateTime,
|
||||
DailyTime = schedule.DailyTime,
|
||||
DayOfWeek = schedule.DayOfWeek,
|
||||
DayOfMonth = schedule.DayOfMonth,
|
||||
IntervalValue = schedule.IntervalValue,
|
||||
IntervalUnit = schedule.IntervalUnit,
|
||||
SourceDatabaseOverride = schedule.SourceDatabaseOverride,
|
||||
DestinationDatabaseOverride = schedule.DestinationDatabaseOverride
|
||||
};
|
||||
|
||||
// Imposta il profilo selezionato per mostrare i campi di override
|
||||
selectedProfile = availableProfiles?.FirstOrDefault(p => p.Id == schedule.ProfileId);
|
||||
|
||||
await ShowModal();
|
||||
}
|
||||
|
||||
protected async Task ShowModal()
|
||||
{
|
||||
StateHasChanged();
|
||||
await Task.Delay(100);
|
||||
|
||||
try
|
||||
{
|
||||
// Proviamo prima con l'approccio Bootstrap standard
|
||||
await JSRuntime.InvokeVoidAsync("eval",
|
||||
"if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { " +
|
||||
"var modal = new bootstrap.Modal(document.getElementById('scheduleModal')); " +
|
||||
"modal.show(); " +
|
||||
"} else { " +
|
||||
"document.getElementById('scheduleModal').style.display = 'block'; " +
|
||||
"document.getElementById('scheduleModal').classList.add('show'); " +
|
||||
"}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fallback: mostra il modal manualmente
|
||||
await JSRuntime.InvokeVoidAsync("eval",
|
||||
"var modal = document.getElementById('scheduleModal');" +
|
||||
"modal.style.display = 'block';" +
|
||||
"modal.classList.add('show');" +
|
||||
"document.body.classList.add('modal-open');" +
|
||||
"var backdrop = document.createElement('div');" +
|
||||
"backdrop.className = 'modal-backdrop fade show';" +
|
||||
"document.body.appendChild(backdrop);");
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task HideModal()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("eval",
|
||||
"if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { " +
|
||||
"var modalElement = document.getElementById('scheduleModal'); " +
|
||||
"var modal = bootstrap.Modal.getInstance(modalElement); " +
|
||||
"if (modal) modal.hide(); " +
|
||||
"} else { " +
|
||||
"document.getElementById('scheduleModal').style.display = 'none'; " +
|
||||
"document.getElementById('scheduleModal').classList.remove('show'); " +
|
||||
"document.body.classList.remove('modal-open'); " +
|
||||
"var backdrop = document.querySelector('.modal-backdrop'); " +
|
||||
"if (backdrop) backdrop.remove(); " +
|
||||
"}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fallback: nascondi il modal manualmente
|
||||
await JSRuntime.InvokeVoidAsync("eval",
|
||||
"var modal = document.getElementById('scheduleModal');" +
|
||||
"modal.style.display = 'none';" +
|
||||
"modal.classList.remove('show');" +
|
||||
"document.body.classList.remove('modal-open');" +
|
||||
"var backdrop = document.querySelector('.modal-backdrop');" +
|
||||
"if (backdrop) backdrop.remove();");
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task SaveSchedule()
|
||||
{
|
||||
if (editingSchedule == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (editingSchedule.Id == 0)
|
||||
{
|
||||
await ScheduleService.CreateScheduleAsync(editingSchedule);
|
||||
await ShowSuccessMessage("Schedulazione creata con successo!");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ScheduleService.UpdateScheduleAsync(editingSchedule);
|
||||
await ShowSuccessMessage("Schedulazione aggiornata con successo!");
|
||||
}
|
||||
|
||||
await CloseModal();
|
||||
await LoadSchedules();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel salvataggio della schedulazione");
|
||||
await ShowErrorMessage("Errore nel salvataggio: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task DeleteSchedule(int scheduleId)
|
||||
{
|
||||
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare questa schedulazione?");
|
||||
if (!confirmed) return;
|
||||
|
||||
try
|
||||
{
|
||||
var success = await ScheduleService.DeleteScheduleAsync(scheduleId);
|
||||
if (success)
|
||||
{
|
||||
await ShowSuccessMessage("Schedulazione eliminata con successo!");
|
||||
await LoadSchedules();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowErrorMessage("Schedulazione non trovata.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'eliminazione della schedulazione {ScheduleId}", scheduleId);
|
||||
await ShowErrorMessage("Errore nell'eliminazione: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ToggleScheduleEnabled(int scheduleId, bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
var schedule = schedules?.FirstOrDefault(s => s.Id == scheduleId);
|
||||
if (schedule != null)
|
||||
{
|
||||
schedule.IsEnabled = enabled;
|
||||
await ScheduleService.UpdateScheduleAsync(schedule);
|
||||
|
||||
Logger.LogInformation("Schedulazione {ScheduleId} {Status}", scheduleId, enabled ? "attivata" : "disattivata");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel cambio stato schedulazione {ScheduleId}", scheduleId);
|
||||
await ShowErrorMessage("Errore nel cambio stato: " + ex.Message);
|
||||
await LoadSchedules(); // Ricarica per ripristinare lo stato
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ExecuteScheduleManually(int scheduleId)
|
||||
{
|
||||
if (isExecuting) return;
|
||||
|
||||
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Eseguire immediatamente questa schedulazione?");
|
||||
if (!confirmed) return;
|
||||
|
||||
isExecuting = true;
|
||||
ScheduleExecutionHistory? executionHistory = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var schedule = schedules?.FirstOrDefault(s => s.Id == scheduleId);
|
||||
if (schedule?.Profile == null)
|
||||
{
|
||||
await ShowErrorMessage("Schedulazione o profilo non trovato.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Esecuzione manuale schedulazione {ScheduleId} - {ScheduleName}", scheduleId, schedule.Name);
|
||||
|
||||
// Crea record nello storico
|
||||
executionHistory = new ScheduleExecutionHistory
|
||||
{
|
||||
ScheduleId = scheduleId,
|
||||
ProfileId = schedule.ProfileId,
|
||||
ProfileName = schedule.Profile.Name,
|
||||
StartTime = DateTime.Now,
|
||||
Status = "running",
|
||||
TriggerType = "manual",
|
||||
TriggeredBy = Environment.UserName,
|
||||
SourceType = schedule.Profile.SourceType,
|
||||
DestinationType = schedule.Profile.DestinationType,
|
||||
SourceInfo = schedule.SourceDatabaseOverride != null ? $"Database Override: {schedule.SourceDatabaseOverride}" : null,
|
||||
DestinationInfo = schedule.DestinationDatabaseOverride != null ? $"Database Override: {schedule.DestinationDatabaseOverride}" : null,
|
||||
Message = "Esecuzione manuale avviata"
|
||||
};
|
||||
|
||||
executionHistory = await ScheduleService.CreateExecutionHistoryAsync(executionHistory);
|
||||
|
||||
// Aggiorna lo status della schedulazione
|
||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "running", "Esecuzione manuale avviata");
|
||||
await LoadSchedules();
|
||||
|
||||
// Esegui il trasferimento dati con override database se specificati
|
||||
var result = await DataTransferService.ExecuteProfileAsync(
|
||||
schedule.Profile,
|
||||
schedule.SourceDatabaseOverride,
|
||||
schedule.DestinationDatabaseOverride);
|
||||
|
||||
// Aggiorna lo storico con il risultato
|
||||
executionHistory.EndTime = DateTime.Now;
|
||||
executionHistory.Status = result.IsSuccess ? "success" : "failed";
|
||||
executionHistory.RecordsProcessed = result.RecordsProcessed;
|
||||
executionHistory.Message = result.IsSuccess
|
||||
? $"Esecuzione completata con successo. {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi."
|
||||
: $"Esecuzione fallita: {result.ErrorMessage}";
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
executionHistory.ErrorDetails = string.Join(Environment.NewLine, result.ErrorDetails);
|
||||
}
|
||||
|
||||
// Aggiungi informazioni aggiuntive se disponibili
|
||||
if (result.AdditionalInfo.Any())
|
||||
{
|
||||
executionHistory.AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(result.AdditionalInfo);
|
||||
}
|
||||
|
||||
await ScheduleService.UpdateExecutionHistoryAsync(executionHistory);
|
||||
|
||||
// Aggiorna lo status della schedulazione
|
||||
var status = result.IsSuccess ? "success" : "failed";
|
||||
var message = result.IsSuccess
|
||||
? $"Esecuzione completata con successo. {result.RecordsProcessed} record elaborati."
|
||||
: $"Esecuzione fallita: {result.ErrorMessage}";
|
||||
|
||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
await LoadSchedules();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
|
||||
|
||||
// Aggiorna lo storico in caso di eccezione
|
||||
if (executionHistory != null)
|
||||
{
|
||||
executionHistory.EndTime = DateTime.Now;
|
||||
executionHistory.Status = "failed";
|
||||
executionHistory.Message = $"Errore durante l'esecuzione: {ex.Message}";
|
||||
executionHistory.ErrorDetails = ex.ToString();
|
||||
await ScheduleService.UpdateExecutionHistoryAsync(executionHistory);
|
||||
}
|
||||
|
||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}");
|
||||
await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isExecuting = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnProfileSelectionChanged(ChangeEventArgs e)
|
||||
{
|
||||
if (editingSchedule != null && int.TryParse(e.Value?.ToString(), out int profileId))
|
||||
{
|
||||
editingSchedule.ProfileId = profileId;
|
||||
selectedProfile = availableProfiles?.FirstOrDefault(p => p.Id == profileId);
|
||||
|
||||
// Reset override database quando cambia profilo
|
||||
editingSchedule.SourceDatabaseOverride = null;
|
||||
editingSchedule.DestinationDatabaseOverride = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedProfile = null;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected void OnScheduleTypeChanged(ChangeEventArgs e)
|
||||
{
|
||||
if (editingSchedule != null)
|
||||
{
|
||||
editingSchedule.ScheduleType = e.Value?.ToString() ?? "";
|
||||
|
||||
// Reset campi quando cambia il tipo
|
||||
if (editingSchedule.ScheduleType != "once")
|
||||
{
|
||||
editingSchedule.ScheduledDateTime = null;
|
||||
}
|
||||
|
||||
if (editingSchedule.ScheduleType != "weekly")
|
||||
{
|
||||
editingSchedule.DayOfWeek = null;
|
||||
}
|
||||
|
||||
if (editingSchedule.ScheduleType != "monthly")
|
||||
{
|
||||
editingSchedule.DayOfMonth = null;
|
||||
}
|
||||
|
||||
if (editingSchedule.ScheduleType != "interval")
|
||||
{
|
||||
editingSchedule.IntervalValue = null;
|
||||
editingSchedule.IntervalUnit = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Imposta valori predefiniti per interval
|
||||
editingSchedule.IntervalValue ??= 5;
|
||||
editingSchedule.IntervalUnit ??= "minutes";
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected string GetIntervalPreview()
|
||||
{
|
||||
if (editingSchedule == null || !editingSchedule.IntervalValue.HasValue || string.IsNullOrEmpty(editingSchedule.IntervalUnit))
|
||||
{
|
||||
return "Configura intervallo e unità di tempo";
|
||||
}
|
||||
|
||||
var value = editingSchedule.IntervalValue.Value;
|
||||
var unit = editingSchedule.IntervalUnit;
|
||||
|
||||
var unitName = unit switch
|
||||
{
|
||||
"seconds" => value == 1 ? "secondo" : "secondi",
|
||||
"minutes" => value == 1 ? "minuto" : "minuti",
|
||||
"hours" => value == 1 ? "ora" : "ore",
|
||||
"days" => value == 1 ? "giorno" : "giorni",
|
||||
"weeks" => value == 1 ? "settimana" : "settimane",
|
||||
"months" => value == 1 ? "mese" : "mesi",
|
||||
_ => unit
|
||||
};
|
||||
|
||||
return $"Esecuzione ogni {value} {unitName}";
|
||||
}
|
||||
|
||||
private async Task CloseModal()
|
||||
{
|
||||
await HideModal();
|
||||
editingSchedule = null;
|
||||
selectedProfile = null;
|
||||
}
|
||||
|
||||
private async Task ShowSuccessMessage(string message)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", message);
|
||||
}
|
||||
|
||||
private async Task ShowErrorMessage(string message)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", "Errore: " + message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user