diff --git a/docs/development/devlog/2025-12-12-110000_communications_module.md b/docs/development/devlog/2025-12-12-110000_communications_module.md index c878ceb..a105cc5 100644 --- a/docs/development/devlog/2025-12-12-110000_communications_module.md +++ b/docs/development/devlog/2025-12-12-110000_communications_module.md @@ -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 diff --git a/src/backend/Zentral.API/Apps/Communications/Controllers/CommunicationsController.cs b/src/backend/Zentral.API/Apps/Communications/Controllers/CommunicationsController.cs new file mode 100644 index 0000000..e964f66 --- /dev/null +++ b/src/backend/Zentral.API/Apps/Communications/Controllers/CommunicationsController.cs @@ -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> 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 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 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>> 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 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"; + } +} diff --git a/src/backend/Zentral.API/Apps/Communications/Dtos/EmailLogDto.cs b/src/backend/Zentral.API/Apps/Communications/Dtos/EmailLogDto.cs new file mode 100644 index 0000000..14ff648 --- /dev/null +++ b/src/backend/Zentral.API/Apps/Communications/Dtos/EmailLogDto.cs @@ -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; } +} diff --git a/src/backend/Zentral.API/Apps/Communications/Dtos/SmtpConfigDto.cs b/src/backend/Zentral.API/Apps/Communications/Dtos/SmtpConfigDto.cs new file mode 100644 index 0000000..03441af --- /dev/null +++ b/src/backend/Zentral.API/Apps/Communications/Dtos/SmtpConfigDto.cs @@ -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; +} diff --git a/src/backend/Zentral.API/Apps/Communications/Dtos/TestEmailDto.cs b/src/backend/Zentral.API/Apps/Communications/Dtos/TestEmailDto.cs new file mode 100644 index 0000000..5089966 --- /dev/null +++ b/src/backend/Zentral.API/Apps/Communications/Dtos/TestEmailDto.cs @@ -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."; +} diff --git a/src/backend/Zentral.API/Program.cs b/src/backend/Zentral.API/Program.cs index b1c9245..81ebdf5 100644 --- a/src/backend/Zentral.API/Program.cs +++ b/src/backend/Zentral.API/Program.cs @@ -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(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +// Communications Module Services +builder.Services.AddTransient(); + // Warehouse Module Services builder.Services.AddScoped(); diff --git a/src/backend/Zentral.API/Services/AppService.cs b/src/backend/Zentral.API/Services/AppService.cs index cb498f1..3955d83 100644 --- a/src/backend/Zentral.API/Services/AppService.cs +++ b/src/backend/Zentral.API/Services/AppService.cs @@ -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 } }; diff --git a/src/backend/Zentral.Domain/Entities/Communications/EmailLog.cs b/src/backend/Zentral.Domain/Entities/Communications/EmailLog.cs new file mode 100644 index 0000000..bbfc66f --- /dev/null +++ b/src/backend/Zentral.Domain/Entities/Communications/EmailLog.cs @@ -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; } +} diff --git a/src/backend/Zentral.Domain/Interfaces/IEmailSender.cs b/src/backend/Zentral.Domain/Interfaces/IEmailSender.cs new file mode 100644 index 0000000..87eaa0c --- /dev/null +++ b/src/backend/Zentral.Domain/Interfaces/IEmailSender.cs @@ -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 attachments, bool isHtml = true); +} diff --git a/src/backend/Zentral.Infrastructure/Data/ZentralDbContext.cs b/src/backend/Zentral.Infrastructure/Data/ZentralDbContext.cs index 7b81d4b..822ea1a 100644 --- a/src/backend/Zentral.Infrastructure/Data/ZentralDbContext.cs +++ b/src/backend/Zentral.Infrastructure/Data/ZentralDbContext.cs @@ -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 Assenze => Set(); public DbSet Pagamenti => Set(); public DbSet Rimborsi => Set(); + + // Communications module entities + public DbSet EmailLogs => Set(); 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(entity => + { + entity.ToTable("EmailLogs"); + entity.HasIndex(e => e.SentDate); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.Recipient); + }); } } diff --git a/src/backend/Zentral.Infrastructure/Services/SmtpEmailSender.cs b/src/backend/Zentral.Infrastructure/Services/SmtpEmailSender.cs new file mode 100644 index 0000000..377fc92 --- /dev/null +++ b/src/backend/Zentral.Infrastructure/Services/SmtpEmailSender.cs @@ -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(), isHtml); + } + + public async Task SendEmailAsync(string to, string subject, string body, List attachments, bool isHtml = true) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + // 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 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(); + } +} diff --git a/src/backend/Zentral.Infrastructure/Zentral.Infrastructure.csproj b/src/backend/Zentral.Infrastructure/Zentral.Infrastructure.csproj index c75c72b..c690254 100644 --- a/src/backend/Zentral.Infrastructure/Zentral.Infrastructure.csproj +++ b/src/backend/Zentral.Infrastructure/Zentral.Infrastructure.csproj @@ -10,6 +10,7 @@ all + diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 1826749..d8dc754 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -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() { } /> + {/* Communications Module */} + + + + } + /> diff --git a/src/frontend/src/apps/communications/components/CommunicationsLayout.tsx b/src/frontend/src/apps/communications/components/CommunicationsLayout.tsx new file mode 100644 index 0000000..9eefb68 --- /dev/null +++ b/src/frontend/src/apps/communications/components/CommunicationsLayout.tsx @@ -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 ( + + + + + + + + + + + + ); +} diff --git a/src/frontend/src/apps/communications/pages/LogsPage.tsx b/src/frontend/src/apps/communications/pages/LogsPage.tsx new file mode 100644 index 0000000..31c51b4 --- /dev/null +++ b/src/frontend/src/apps/communications/pages/LogsPage.tsx @@ -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([]); + 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) => ( + + {params.value} + + ) + }, + { 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 ( + + + Email Logs + + + + ); +} diff --git a/src/frontend/src/apps/communications/pages/SettingsPage.tsx b/src/frontend/src/apps/communications/pages/SettingsPage.tsx new file mode 100644 index 0000000..e65bd3f --- /dev/null +++ b/src/frontend/src/apps/communications/pages/SettingsPage.tsx @@ -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(); + const [loading, setLoading] = useState(false); + const [testMode, setTestMode] = useState(false); + const [testData, setTestData] = useState({ 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 ( + + + Configurazione SMTP + + + +
+ + + } + /> + + + } + /> + + + + } + /> + + + } + /> + + + + ( + } + label="Enable SSL/TLS" + /> + )} + /> + + + + + Mittente Default + + + + } + /> + + + } + /> + + + + + + + +
+
+ + {testMode && ( + + Test Email + + + setTestData({ ...testData, to: e.target.value })} + /> + + + setTestData({ ...testData, subject: e.target.value })} + /> + + + + + + + )} + + setNotification(null)} + > + setNotification(null)}> + {notification?.message} + + +
+ ); +} diff --git a/src/frontend/src/apps/communications/routes.tsx b/src/frontend/src/apps/communications/routes.tsx new file mode 100644 index 0000000..0b703ef --- /dev/null +++ b/src/frontend/src/apps/communications/routes.tsx @@ -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 ( + + }> + } /> + } /> + } /> + + + ); +} diff --git a/src/frontend/src/apps/communications/services/communicationsService.ts b/src/frontend/src/apps/communications/services/communicationsService.ts new file mode 100644 index 0000000..59f0d61 --- /dev/null +++ b/src/frontend/src/apps/communications/services/communicationsService.ts @@ -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('/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('/communications/logs', { params: { limit } }); + return response.data; + } +}; diff --git a/src/frontend/src/apps/communications/types/index.ts b/src/frontend/src/apps/communications/types/index.ts new file mode 100644 index 0000000..17c0d46 --- /dev/null +++ b/src/frontend/src/apps/communications/types/index.ts @@ -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; +}