feat: implement communications module with SMTP settings, email logging, and frontend UI

This commit is contained in:
2025-12-12 11:19:25 +01:00
parent dedd4f4e69
commit 9174e75be0
19 changed files with 727 additions and 9 deletions

View File

@@ -11,35 +11,35 @@ Sarà allineato alla visione del modulo "Comunicazioni" (Gestione invio mail, ch
## Piano di Lavoro ## Piano di Lavoro
### 1. Documentazione ### 1. Documentazione
- [ ] Aggiornamento piano di lavoro (questo file). - [x] Aggiornamento piano di lavoro (questo file).
- [ ] Aggiornamento `ZENTRAL.md`. - [x] Aggiornamento `ZENTRAL.md`.
### 2. Backend (.NET) ### 2. Backend (.NET)
#### Domain Layer (`Zentral.Domain`) #### Domain Layer (`Zentral.Domain`)
- [ ] **Interfaccia `IEmailSender`**: Contratto standard per l'invio. - [x] **Interfaccia `IEmailSender`**: Contratto standard per l'invio.
- [ ] **Entities (Namespace `Communications`)**: - [x] **Entities (Namespace `Communications`)**:
- `EmailLog`: Storico invii (`Id`, `Data`, `Mittente`, `Destinatario`, `Oggetto`, `Stato`, `Errore`). - `EmailLog`: Storico invii (`Id`, `Data`, `Mittente`, `Destinatario`, `Oggetto`, `Stato`, `Errore`).
- `EmailTemplate` (Opzionale Fase 1): Per standardizzare il layout delle mail. - `EmailTemplate` (Opzionale Fase 1): Per standardizzare il layout delle mail.
#### Infrastructure Layer (`Zentral.Infrastructure`) #### Infrastructure Layer (`Zentral.Infrastructure`)
- [ ] **Implementazione `SmtpEmailSender`**: - [x] **Implementazione `SmtpEmailSender`**:
- Logica di invio tramite MailKit. - Logica di invio tramite MailKit.
- Integrazione con `Configurazione` per leggere le credenziali SMTP a runtime. - Integrazione con `Configurazione` per leggere le credenziali SMTP a runtime.
- Salvataggio automatico del log in `EmailLog`. - Salvataggio automatico del log in `EmailLog`.
#### API Layer (`Zentral.API`) #### API Layer (`Zentral.API`)
- [ ] **Controller `CommunicationsController`**: - [x] **Controller `CommunicationsController`**:
- Endpoint per test invio. - Endpoint per test invio.
- Endpoint per consultazione Logs. - Endpoint per consultazione Logs.
- Endpoint per salvataggio Configurazione SMTP. - Endpoint per salvataggio Configurazione SMTP.
### 3. Frontend (React) ### 3. Frontend (React)
#### Modulo `communications` (`src/apps/communications`) #### Modulo `communications` (`src/apps/communications`)
- [ ] **Setup App**: Creazione struttura standard modulo. - [x] **Setup App**: Creazione struttura standard modulo.
- [ ] **Settings Page**: - [x] **Settings Page**:
- Form per configurazione SMTP (Host, Port, User, Pass, SSL). - Form per configurazione SMTP (Host, Port, User, Pass, SSL).
- Pulsante "Test Connessione". - Pulsante "Test Connessione".
- [ ] **Logs Page**: - [x] **Logs Page**:
- Tabella visualizzazione storico email inviate con stato (Successo/Errore). - Tabella visualizzazione storico email inviate con stato (Successo/Errore).
## Integrazione ## Integrazione

View File

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

View File

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

View File

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

View File

@@ -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.";
}

View File

@@ -6,7 +6,10 @@ using Zentral.API.Apps.Warehouse.Services;
using Zentral.API.Apps.Purchases.Services; using Zentral.API.Apps.Purchases.Services;
using Zentral.API.Apps.Sales.Services; using Zentral.API.Apps.Sales.Services;
using Zentral.API.Apps.Production.Services; using Zentral.API.Apps.Production.Services;
using Zentral.API.Apps.Production.Services;
using Zentral.Infrastructure.Data; using Zentral.Infrastructure.Data;
using Zentral.Infrastructure.Services;
using Zentral.Domain.Interfaces;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -28,6 +31,9 @@ builder.Services.AddScoped<AutoCodeService>();
builder.Services.AddScoped<CustomFieldService>(); builder.Services.AddScoped<CustomFieldService>();
builder.Services.AddSingleton<DataNotificationService>(); builder.Services.AddSingleton<DataNotificationService>();
// Communications Module Services
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
// Warehouse Module Services // Warehouse Module Services
builder.Services.AddScoped<IWarehouseService, WarehouseService>(); builder.Services.AddScoped<IWarehouseService, WarehouseService>();

View File

@@ -521,6 +521,20 @@ public class AppService
RoutePath = "/report-designer", RoutePath = "/report-designer",
IsAvailable = true, IsAvailable = true,
CreatedAt = DateTime.UtcNow 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
} }
}; };

View File

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

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

View File

@@ -4,6 +4,7 @@ using Zentral.Domain.Entities.Purchases;
using Zentral.Domain.Entities.Sales; using Zentral.Domain.Entities.Sales;
using Zentral.Domain.Entities.Production; using Zentral.Domain.Entities.Production;
using Zentral.Domain.Entities.HR; using Zentral.Domain.Entities.HR;
using Zentral.Domain.Entities.Communications;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Zentral.Infrastructure.Data; namespace Zentral.Infrastructure.Data;
@@ -94,6 +95,9 @@ public class ZentralDbContext : DbContext
public DbSet<Assenza> Assenze => Set<Assenza>(); public DbSet<Assenza> Assenze => Set<Assenza>();
public DbSet<Pagamento> Pagamenti => Set<Pagamento>(); public DbSet<Pagamento> Pagamenti => Set<Pagamento>();
public DbSet<Rimborso> Rimborsi => Set<Rimborso>(); public DbSet<Rimborso> Rimborsi => Set<Rimborso>();
// Communications module entities
public DbSet<EmailLog> EmailLogs => Set<EmailLog>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -989,5 +993,16 @@ public class ZentralDbContext : DbContext
.HasForeignKey(e => e.ArticleId) .HasForeignKey(e => e.ArticleId)
.OnDelete(DeleteBehavior.Cascade); .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);
});
} }
} }

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

View File

@@ -10,6 +10,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="MailKit" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -19,6 +19,7 @@ import SalesRoutes from "./apps/sales/routes";
import ProductionRoutes from "./apps/production/routes"; import ProductionRoutes from "./apps/production/routes";
import EventsRoutes from "./apps/events/routes"; import EventsRoutes from "./apps/events/routes";
import HRRoutes from "./apps/hr/routes"; import HRRoutes from "./apps/hr/routes";
import CommunicationsRoutes from "./apps/communications/routes";
import { AppGuard } from "./components/AppGuard"; import { AppGuard } from "./components/AppGuard";
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates"; import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
import { CollaborationProvider } from "./contexts/CollaborationContext"; import { CollaborationProvider } from "./contexts/CollaborationContext";
@@ -135,6 +136,15 @@ function App() {
</AppGuard> </AppGuard>
} }
/> />
{/* Communications Module */}
<Route
path="communications/*"
element={
<AppGuard appCode="communications">
<CommunicationsRoutes />
</AppGuard>
}
/>
</Route> </Route>
</Routes> </Routes>
</TabProvider> </TabProvider>

View File

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

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

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

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

View File

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

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