feat: implement communications module with SMTP settings, email logging, and frontend UI
This commit is contained in:
@@ -11,35 +11,35 @@ Sarà allineato alla visione del modulo "Comunicazioni" (Gestione invio mail, ch
|
||||
## Piano di Lavoro
|
||||
|
||||
### 1. Documentazione
|
||||
- [ ] Aggiornamento piano di lavoro (questo file).
|
||||
- [ ] Aggiornamento `ZENTRAL.md`.
|
||||
- [x] Aggiornamento piano di lavoro (questo file).
|
||||
- [x] Aggiornamento `ZENTRAL.md`.
|
||||
|
||||
### 2. Backend (.NET)
|
||||
#### Domain Layer (`Zentral.Domain`)
|
||||
- [ ] **Interfaccia `IEmailSender`**: Contratto standard per l'invio.
|
||||
- [ ] **Entities (Namespace `Communications`)**:
|
||||
- [x] **Interfaccia `IEmailSender`**: Contratto standard per l'invio.
|
||||
- [x] **Entities (Namespace `Communications`)**:
|
||||
- `EmailLog`: Storico invii (`Id`, `Data`, `Mittente`, `Destinatario`, `Oggetto`, `Stato`, `Errore`).
|
||||
- `EmailTemplate` (Opzionale Fase 1): Per standardizzare il layout delle mail.
|
||||
|
||||
#### Infrastructure Layer (`Zentral.Infrastructure`)
|
||||
- [ ] **Implementazione `SmtpEmailSender`**:
|
||||
- [x] **Implementazione `SmtpEmailSender`**:
|
||||
- Logica di invio tramite MailKit.
|
||||
- Integrazione con `Configurazione` per leggere le credenziali SMTP a runtime.
|
||||
- Salvataggio automatico del log in `EmailLog`.
|
||||
|
||||
#### API Layer (`Zentral.API`)
|
||||
- [ ] **Controller `CommunicationsController`**:
|
||||
- [x] **Controller `CommunicationsController`**:
|
||||
- Endpoint per test invio.
|
||||
- Endpoint per consultazione Logs.
|
||||
- Endpoint per salvataggio Configurazione SMTP.
|
||||
|
||||
### 3. Frontend (React)
|
||||
#### Modulo `communications` (`src/apps/communications`)
|
||||
- [ ] **Setup App**: Creazione struttura standard modulo.
|
||||
- [ ] **Settings Page**:
|
||||
- [x] **Setup App**: Creazione struttura standard modulo.
|
||||
- [x] **Settings Page**:
|
||||
- Form per configurazione SMTP (Host, Port, User, Pass, SSL).
|
||||
- Pulsante "Test Connessione".
|
||||
- [ ] **Logs Page**:
|
||||
- [x] **Logs Page**:
|
||||
- Tabella visualizzazione storico email inviate con stato (Successo/Errore).
|
||||
|
||||
## Integrazione
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Zentral.API.Apps.Communications.Dtos;
|
||||
using Zentral.Domain.Entities;
|
||||
using Zentral.Domain.Interfaces;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Zentral.API.Apps.Communications.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/communications")]
|
||||
public class CommunicationsController : ControllerBase
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public CommunicationsController(ZentralDbContext context, IEmailSender emailSender)
|
||||
{
|
||||
_context = context;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
[HttpGet("config")]
|
||||
public async Task<ActionResult<SmtpConfigDto>> GetConfig()
|
||||
{
|
||||
var configs = await _context.Configurazioni
|
||||
.Where(c => c.Chiave.StartsWith("SMTP_"))
|
||||
.ToDictionaryAsync(c => c.Chiave, c => c.Valore);
|
||||
|
||||
var dto = new SmtpConfigDto
|
||||
{
|
||||
Host = GetValue(configs, "SMTP_HOST"),
|
||||
Port = int.Parse(GetValue(configs, "SMTP_PORT", "587")),
|
||||
User = GetValue(configs, "SMTP_USER"),
|
||||
Password = GetValue(configs, "SMTP_PASS"),
|
||||
EnableSsl = bool.Parse(GetValue(configs, "SMTP_SSL", "false")),
|
||||
FromEmail = GetValue(configs, "SMTP_FROM_EMAIL"),
|
||||
FromName = GetValue(configs, "SMTP_FROM_NAME")
|
||||
};
|
||||
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost("config")]
|
||||
public async Task<ActionResult> SaveConfig(SmtpConfigDto dto)
|
||||
{
|
||||
await SetConfig("SMTP_HOST", dto.Host);
|
||||
await SetConfig("SMTP_PORT", dto.Port.ToString());
|
||||
await SetConfig("SMTP_USER", dto.User);
|
||||
await SetConfig("SMTP_PASS", dto.Password);
|
||||
await SetConfig("SMTP_SSL", dto.EnableSsl.ToString().ToLower());
|
||||
await SetConfig("SMTP_FROM_EMAIL", dto.FromEmail);
|
||||
await SetConfig("SMTP_FROM_NAME", dto.FromName);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("send-test")]
|
||||
public async Task<ActionResult> SendTestEmail(TestEmailDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _emailSender.SendEmailAsync(dto.To, dto.Subject, dto.Body);
|
||||
return Ok(new { message = "Email send process initiated. Check logs for status." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public async Task<ActionResult<List<EmailLogDto>>> GetLogs([FromQuery] int limit = 50)
|
||||
{
|
||||
var logs = await _context.EmailLogs
|
||||
.OrderByDescending(l => l.SentDate)
|
||||
.Take(limit)
|
||||
.Select(l => new EmailLogDto
|
||||
{
|
||||
Id = l.Id,
|
||||
SentDate = l.SentDate,
|
||||
Sender = l.Sender,
|
||||
Recipient = l.Recipient,
|
||||
Subject = l.Subject,
|
||||
Status = l.Status,
|
||||
ErrorMessage = l.ErrorMessage
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(logs);
|
||||
}
|
||||
|
||||
private string GetValue(Dictionary<string, string?> dict, string key, string def = "")
|
||||
{
|
||||
return dict.ContainsKey(key) && dict[key] != null ? dict[key]! : def;
|
||||
}
|
||||
|
||||
private async Task SetConfig(string key, string? value)
|
||||
{
|
||||
var config = await _context.Configurazioni.FirstOrDefaultAsync(c => c.Chiave == key);
|
||||
if (config == null)
|
||||
{
|
||||
config = new Configurazione { Chiave = key, CreatedAt = DateTime.UtcNow, CreatedBy = User.FindFirstValue(ClaimTypes.Name) ?? "System" };
|
||||
_context.Configurazioni.Add(config);
|
||||
}
|
||||
config.Valore = value;
|
||||
config.UpdatedAt = DateTime.UtcNow;
|
||||
config.UpdatedBy = User.FindFirstValue(ClaimTypes.Name) ?? "System";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Zentral.API.Apps.Communications.Dtos;
|
||||
|
||||
public class EmailLogDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime SentDate { get; set; }
|
||||
public string Sender { get; set; } = string.Empty;
|
||||
public string Recipient { get; set; } = string.Empty;
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Zentral.API.Apps.Communications.Dtos;
|
||||
|
||||
public class SmtpConfigDto
|
||||
{
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public int Port { get; set; } = 587;
|
||||
public string User { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public bool EnableSsl { get; set; } = false;
|
||||
public string FromEmail { get; set; } = string.Empty;
|
||||
public string FromName { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Zentral.API.Apps.Communications.Dtos;
|
||||
|
||||
public class TestEmailDto
|
||||
{
|
||||
public string To { get; set; } = string.Empty;
|
||||
public string Subject { get; set; } = "Test Email from Zentral";
|
||||
public string Body { get; set; } = "This is a test email sent from Zentral Communications Module.";
|
||||
}
|
||||
@@ -6,7 +6,10 @@ using Zentral.API.Apps.Warehouse.Services;
|
||||
using Zentral.API.Apps.Purchases.Services;
|
||||
using Zentral.API.Apps.Sales.Services;
|
||||
using Zentral.API.Apps.Production.Services;
|
||||
using Zentral.API.Apps.Production.Services;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Zentral.Infrastructure.Services;
|
||||
using Zentral.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -28,6 +31,9 @@ builder.Services.AddScoped<AutoCodeService>();
|
||||
builder.Services.AddScoped<CustomFieldService>();
|
||||
builder.Services.AddSingleton<DataNotificationService>();
|
||||
|
||||
// Communications Module Services
|
||||
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
|
||||
|
||||
// Warehouse Module Services
|
||||
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
|
||||
|
||||
|
||||
@@ -521,6 +521,20 @@ public class AppService
|
||||
RoutePath = "/report-designer",
|
||||
IsAvailable = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
},
|
||||
new App
|
||||
{
|
||||
Code = "communications",
|
||||
Name = "Comunicazioni",
|
||||
Description = "Gestione invio mail, chat interna e condivisione risorse",
|
||||
Icon = "Email",
|
||||
BasePrice = 1000m,
|
||||
MonthlyMultiplier = 1.2m,
|
||||
SortOrder = 90,
|
||||
IsCore = false,
|
||||
RoutePath = "/communications",
|
||||
IsAvailable = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using Zentral.Domain;
|
||||
|
||||
namespace Zentral.Domain.Entities.Communications;
|
||||
|
||||
public class EmailLog : BaseEntity
|
||||
{
|
||||
public DateTime SentDate { get; set; }
|
||||
public string Sender { get; set; } = string.Empty;
|
||||
public string Recipient { get; set; } = string.Empty;
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty; // "Success", "Failed"
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
7
src/backend/Zentral.Domain/Interfaces/IEmailSender.cs
Normal file
7
src/backend/Zentral.Domain/Interfaces/IEmailSender.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Zentral.Domain.Interfaces;
|
||||
|
||||
public interface IEmailSender
|
||||
{
|
||||
Task SendEmailAsync(string to, string subject, string body, bool isHtml = true);
|
||||
Task SendEmailAsync(string to, string subject, string body, List<string> attachments, bool isHtml = true);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Zentral.Domain.Entities.Purchases;
|
||||
using Zentral.Domain.Entities.Sales;
|
||||
using Zentral.Domain.Entities.Production;
|
||||
using Zentral.Domain.Entities.HR;
|
||||
using Zentral.Domain.Entities.Communications;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Zentral.Infrastructure.Data;
|
||||
@@ -94,6 +95,9 @@ public class ZentralDbContext : DbContext
|
||||
public DbSet<Assenza> Assenze => Set<Assenza>();
|
||||
public DbSet<Pagamento> Pagamenti => Set<Pagamento>();
|
||||
public DbSet<Rimborso> Rimborsi => Set<Rimborso>();
|
||||
|
||||
// Communications module entities
|
||||
public DbSet<EmailLog> EmailLogs => Set<EmailLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -989,5 +993,16 @@ public class ZentralDbContext : DbContext
|
||||
.HasForeignKey(e => e.ArticleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// ===============================================
|
||||
// COMMUNICATIONS MODULE ENTITIES
|
||||
// ===============================================
|
||||
modelBuilder.Entity<EmailLog>(entity =>
|
||||
{
|
||||
entity.ToTable("EmailLogs");
|
||||
entity.HasIndex(e => e.SentDate);
|
||||
entity.HasIndex(e => e.Status);
|
||||
entity.HasIndex(e => e.Recipient);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
133
src/backend/Zentral.Infrastructure/Services/SmtpEmailSender.cs
Normal file
133
src/backend/Zentral.Infrastructure/Services/SmtpEmailSender.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MimeKit;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Zentral.Domain.Entities.Communications;
|
||||
using Zentral.Domain.Interfaces;
|
||||
using Zentral.Infrastructure.Data;
|
||||
|
||||
namespace Zentral.Infrastructure.Services;
|
||||
|
||||
public class SmtpEmailSender : IEmailSender
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public SmtpEmailSender(IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true)
|
||||
{
|
||||
await SendEmailAsync(to, subject, body, new List<string>(), isHtml);
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string to, string subject, string body, List<string> attachments, bool isHtml = true)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<ZentralDbContext>();
|
||||
|
||||
// 1. Get Configuration
|
||||
var configs = await context.Configurazioni
|
||||
.Where(c => c.Chiave.StartsWith("SMTP_"))
|
||||
.ToDictionaryAsync(c => c.Chiave, c => c.Valore);
|
||||
|
||||
var host = GetConfig(configs, "SMTP_HOST");
|
||||
var portStr = GetConfig(configs, "SMTP_PORT", "587");
|
||||
var user = GetConfig(configs, "SMTP_USER");
|
||||
var pass = GetConfig(configs, "SMTP_PASS");
|
||||
var sslStr = GetConfig(configs, "SMTP_SSL", "false"); // StartTls is usually implied by port 587 but simpler handling here
|
||||
var fromEmail = GetConfig(configs, "SMTP_FROM_EMAIL", user);
|
||||
var fromName = GetConfig(configs, "SMTP_FROM_NAME", "Zentral");
|
||||
|
||||
if (string.IsNullOrEmpty(host))
|
||||
{
|
||||
await LogResultAsync(context, fromEmail, to, subject, "Failed", "SMTP Host not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
int.TryParse(portStr, out int port);
|
||||
bool.TryParse(sslStr, out bool useSsl);
|
||||
|
||||
// 2. Prepare Message
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(fromName, fromEmail));
|
||||
message.To.Add(MailboxAddress.Parse(to));
|
||||
message.Subject = subject;
|
||||
|
||||
var builder = new BodyBuilder();
|
||||
if (isHtml)
|
||||
builder.HtmlBody = body;
|
||||
else
|
||||
builder.TextBody = body;
|
||||
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
if (System.IO.File.Exists(attachment))
|
||||
{
|
||||
builder.Attachments.Add(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
message.Body = builder.ToMessageBody();
|
||||
|
||||
// 3. Send
|
||||
try
|
||||
{
|
||||
using var client = new SmtpClient();
|
||||
// Use SecureSocketOptions.Auto for flexibility or StartTls based on config
|
||||
// For now, simple logic:
|
||||
if (port == 465)
|
||||
await client.ConnectAsync(host, port, SecureSocketOptions.SslOnConnect);
|
||||
else if (port == 587)
|
||||
await client.ConnectAsync(host, port, SecureSocketOptions.StartTls);
|
||||
else
|
||||
await client.ConnectAsync(host, port, SecureSocketOptions.Auto);
|
||||
|
||||
if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(pass))
|
||||
{
|
||||
await client.AuthenticateAsync(user, pass);
|
||||
}
|
||||
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
|
||||
await LogResultAsync(context, fromEmail, to, subject, "Success", null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await LogResultAsync(context, fromEmail, to, subject, "Failed", ex.Message);
|
||||
// We do not rethrow the exception to avoid breaking the application flow,
|
||||
// but in some cases it might be useful to return a result.
|
||||
// For now, logging to DB is the requirement.
|
||||
}
|
||||
}
|
||||
|
||||
private string GetConfig(Dictionary<string, string?> configs, string key, string defaultValue = "")
|
||||
{
|
||||
return configs.ContainsKey(key) && !string.IsNullOrEmpty(configs[key]) ? configs[key]! : defaultValue;
|
||||
}
|
||||
|
||||
private async Task LogResultAsync(ZentralDbContext context, string from, string to, string subject, string status, string? error)
|
||||
{
|
||||
var log = new EmailLog
|
||||
{
|
||||
SentDate = DateTime.UtcNow,
|
||||
Sender = from,
|
||||
Recipient = to,
|
||||
Subject = subject,
|
||||
Status = status,
|
||||
ErrorMessage = error,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = "System"
|
||||
};
|
||||
|
||||
context.EmailLogs.Add(log);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -19,6 +19,7 @@ import SalesRoutes from "./apps/sales/routes";
|
||||
import ProductionRoutes from "./apps/production/routes";
|
||||
import EventsRoutes from "./apps/events/routes";
|
||||
import HRRoutes from "./apps/hr/routes";
|
||||
import CommunicationsRoutes from "./apps/communications/routes";
|
||||
import { AppGuard } from "./components/AppGuard";
|
||||
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
||||
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
||||
@@ -135,6 +136,15 @@ function App() {
|
||||
</AppGuard>
|
||||
}
|
||||
/>
|
||||
{/* Communications Module */}
|
||||
<Route
|
||||
path="communications/*"
|
||||
element={
|
||||
<AppGuard appCode="communications">
|
||||
<CommunicationsRoutes />
|
||||
</AppGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</TabProvider>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Box, Paper, Tab, Tabs } from "@mui/material";
|
||||
|
||||
export default function CommunicationsLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const getActiveTab = () => {
|
||||
const path = location.pathname;
|
||||
if (path.includes("/communications/logs")) return "/communications/logs";
|
||||
return "/communications/settings";
|
||||
};
|
||||
|
||||
const handleChange = (_event: React.SyntheticEvent, newValue: string) => {
|
||||
navigate(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<Paper sx={{ mb: 2 }}>
|
||||
<Tabs
|
||||
value={getActiveTab()}
|
||||
onChange={handleChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
>
|
||||
<Tab label="Configurazione" value="/communications/settings" />
|
||||
<Tab label="Logs" value="/communications/logs" />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Box sx={{ flex: 1, overflow: "auto" }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
69
src/frontend/src/apps/communications/pages/LogsPage.tsx
Normal file
69
src/frontend/src/apps/communications/pages/LogsPage.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { History } from '@mui/icons-material';
|
||||
import { communicationsService } from '../services/communicationsService';
|
||||
import { EmailLog } from '../types';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default function LogsPage() {
|
||||
const [logs, setLogs] = useState<EmailLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs();
|
||||
}, []);
|
||||
|
||||
const loadLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await communicationsService.getLogs(100);
|
||||
setLogs(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', width: 70 },
|
||||
{
|
||||
field: 'sentDate', headerName: 'Data', width: 180,
|
||||
valueFormatter: (params) => dayjs(params.value).format('DD/MM/YYYY HH:mm')
|
||||
},
|
||||
{
|
||||
field: 'status', headerName: 'Stato', width: 120,
|
||||
renderCell: (params) => (
|
||||
<span style={{
|
||||
color: params.value === 'Success' ? 'green' : 'red',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{params.value}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{ field: 'sender', headerName: 'Mittente', width: 200 },
|
||||
{ field: 'recipient', headerName: 'Destinatario', width: 200 },
|
||||
{ field: 'subject', headerName: 'Oggetto', flex: 1 },
|
||||
{ field: 'errorMessage', headerName: 'Errore', width: 200 },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box p={3} sx={{ height: '80vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h4"><History /> Email Logs</Typography>
|
||||
</Box>
|
||||
<DataGrid
|
||||
rows={logs}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
204
src/frontend/src/apps/communications/pages/SettingsPage.tsx
Normal file
204
src/frontend/src/apps/communications/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
Box, Paper, Typography, TextField, Button, Grid,
|
||||
Switch, FormControlLabel, Divider, Alert, Snackbar
|
||||
} from '@mui/material';
|
||||
import { Save, Send, Email } from '@mui/icons-material';
|
||||
import { communicationsService } from '../services/communicationsService';
|
||||
import { SmtpConfig, TestEmail } from '../types';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { control, handleSubmit, reset } = useForm<SmtpConfig>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testMode, setTestMode] = useState(false);
|
||||
const [testData, setTestData] = useState<TestEmail>({ to: '', subject: 'Test Email', body: 'Test content' });
|
||||
const [notification, setNotification] = useState<{ type: 'success' | 'error', message: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const config = await communicationsService.getConfig();
|
||||
reset(config);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNotification({ type: 'error', message: 'Failed to load configuration' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: SmtpConfig) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await communicationsService.saveConfig(data);
|
||||
setNotification({ type: 'success', message: 'Configuration saved successfully' });
|
||||
} catch (error) {
|
||||
setNotification({ type: 'error', message: 'Failed to save configuration' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sendTest = async () => {
|
||||
if (!testData.to) {
|
||||
setNotification({ type: 'error', message: 'Recipient email is required for test' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
await communicationsService.sendTestEmail(testData);
|
||||
setNotification({ type: 'success', message: 'Test email queued successfully' });
|
||||
setTestMode(false);
|
||||
} catch (error: any) {
|
||||
setNotification({ type: 'error', message: error.response?.data?.message || 'Failed to send test email' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Typography variant="h4" gutterBottom display="flex" alignItems="center" gap={2}>
|
||||
<Email fontSize="large" color="primary" /> Configurazione SMTP
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Controller
|
||||
name="host"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => <TextField {...field} label="SMTP Host" fullWidth required />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Controller
|
||||
name="port"
|
||||
control={control}
|
||||
defaultValue={587}
|
||||
render={({ field }) => <TextField {...field} label="Port" type="number" fullWidth required />}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Controller
|
||||
name="user"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => <TextField {...field} label="Username" fullWidth />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => <TextField {...field} label="Password" type="password" fullWidth />}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Controller
|
||||
name="enableSsl"
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControlLabel
|
||||
control={<Switch checked={value} onChange={onChange} />}
|
||||
label="Enable SSL/TLS"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="h6">Mittente Default</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Controller
|
||||
name="fromEmail"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => <TextField {...field} label="From Email" fullWidth required />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Controller
|
||||
name="fromName"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => <TextField {...field} label="From Name" fullWidth />}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Send />}
|
||||
onClick={() => setTestMode(!testMode)}
|
||||
>
|
||||
Test Connessione
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
startIcon={<Save />}
|
||||
disabled={loading}
|
||||
>
|
||||
Salva Configurazione
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
{testMode && (
|
||||
<Paper sx={{ p: 3, bgcolor: '#f5f5f5' }}>
|
||||
<Typography variant="h6" gutterBottom>Test Email</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
label="Destinatario"
|
||||
fullWidth
|
||||
value={testData.to}
|
||||
onChange={(e) => setTestData({ ...testData, to: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
label="Oggetto"
|
||||
fullWidth
|
||||
value={testData.subject}
|
||||
onChange={(e) => setTestData({ ...testData, subject: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button variant="contained" color="secondary" onClick={sendTest} disabled={loading}>
|
||||
Invia Test
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Snackbar
|
||||
open={!!notification}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setNotification(null)}
|
||||
>
|
||||
<Alert severity={notification?.type || 'info'} onClose={() => setNotification(null)}>
|
||||
{notification?.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
16
src/frontend/src/apps/communications/routes.tsx
Normal file
16
src/frontend/src/apps/communications/routes.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import LogsPage from "./pages/LogsPage";
|
||||
import CommunicationsLayout from "./components/CommunicationsLayout";
|
||||
|
||||
export default function CommunicationsRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<CommunicationsLayout />}>
|
||||
<Route index element={<Navigate to="settings" replace />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="logs" element={<LogsPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import api from '../../../services/api';
|
||||
import { SmtpConfig, TestEmail, EmailLog } from '../types';
|
||||
|
||||
export const communicationsService = {
|
||||
getConfig: async () => {
|
||||
const response = await api.get<SmtpConfig>('/communications/config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
saveConfig: async (config: SmtpConfig) => {
|
||||
await api.post('/communications/config', config);
|
||||
},
|
||||
|
||||
sendTestEmail: async (data: TestEmail) => {
|
||||
await api.post('/communications/send-test', data);
|
||||
},
|
||||
|
||||
getLogs: async (limit: number = 50) => {
|
||||
const response = await api.get<EmailLog[]>('/communications/logs', { params: { limit } });
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
25
src/frontend/src/apps/communications/types/index.ts
Normal file
25
src/frontend/src/apps/communications/types/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface SmtpConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password?: string;
|
||||
enableSsl: boolean;
|
||||
fromEmail: string;
|
||||
fromName: string;
|
||||
}
|
||||
|
||||
export interface TestEmail {
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface EmailLog {
|
||||
id: number;
|
||||
sentDate: string;
|
||||
sender: string;
|
||||
recipient: string;
|
||||
subject: string;
|
||||
status: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user