فهرست منبع

Primeiro commit com o programa funcional

Nathan Guedes de Lacerda 4 هفته پیش
کامیت
aa1f9e5f92

+ 482 - 0
.gitignore

@@ -0,0 +1,482 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp

+ 19 - 0
AGEHAB.RelatorioSeadIpasgo.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net8.0-windows</TargetFramework>
+    <UseWPF>true</UseWPF>
+    <AssemblyTitle>Gerador Relatório SEAD e Ipasgo</AssemblyTitle>
+    <Product>Gerador Relatório SEAD e Ipasgo</Product>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ClosedXML" Version="0.104.1" />
+    <PackageReference Include="Dapper" Version="2.1.66" />
+    <PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
+  </ItemGroup>
+
+</Project>

+ 34 - 0
AGEHAB.RelatorioSeadIpasgo.sln

@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGEHAB.RelatorioSeadIpasgo", "AGEHAB.RelatorioSeadIpasgo.csproj", "{047BB6B0-8510-4717-A87A-291A481FDB0B}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|Any CPU = Release|Any CPU
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Debug|x64.Build.0 = Debug|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Debug|x86.Build.0 = Debug|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Release|x64.ActiveCfg = Release|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Release|x64.Build.0 = Release|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Release|x86.ActiveCfg = Release|Any CPU
+		{047BB6B0-8510-4717-A87A-291A481FDB0B}.Release|x86.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

+ 3 - 0
AGEHAB.RelatorioSeadIpasgo.slnx

@@ -0,0 +1,3 @@
+<Solution>
+  <Project Path="AGEHAB.RelatorioSeadIpasgo.csproj" />
+</Solution>

+ 6 - 0
App.xaml

@@ -0,0 +1,6 @@
+<Application x:Class="AGEHAB.RelatorioSeadIpasgo.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Application.Resources>
+    </Application.Resources>
+</Application>

+ 34 - 0
App.xaml.cs

@@ -0,0 +1,34 @@
+using System.Windows;
+using AGEHAB.RelatorioSeadIpasgo.Application.UseCases;
+using AGEHAB.RelatorioSeadIpasgo.Domain.Services;
+using AGEHAB.RelatorioSeadIpasgo.Infrastructure.Database;
+using AGEHAB.RelatorioSeadIpasgo.Infrastructure.Excel;
+using AGEHAB.RelatorioSeadIpasgo.Infrastructure.Zip;
+using AGEHAB.RelatorioSeadIpasgo.Presentation;
+using AGEHAB.RelatorioSeadIpasgo.Presentation.Services;
+using AGEHAB.RelatorioSeadIpasgo.Presentation.ViewModels;
+
+namespace AGEHAB.RelatorioSeadIpasgo;
+
+public partial class App : System.Windows.Application
+{
+    protected override void OnStartup(StartupEventArgs e)
+    {
+        base.OnStartup(e);
+
+        const string connectionString = "Server=10.6.57.33; initial catalog=ERP_RH_AGEHAB; User Id=usr_pld; Password=Consulta01; MultipleActiveResultSets=true; TrustServerCertificate=True";
+
+        var repository = new RelatorioRepository(connectionString);
+        var filtroIpasgoService = new FiltroIpasgoService();
+        var excelWriter = new ExcelRelatorioWriter();
+        var zipWriter = new IpasgoZipWriter();
+
+        var carregarCompetenciasUseCase = new CarregarCompetenciasUseCase(repository);
+        var gerarRelatorioUseCase = new GerarRelatorioUseCase(repository, filtroIpasgoService, excelWriter, zipWriter);
+        var dialogService = new UserDialogService();
+        var viewModel = new MainViewModel(carregarCompetenciasUseCase, gerarRelatorioUseCase, dialogService);
+
+        var window = new MainWindow(viewModel);
+        window.Show();
+    }
+}

+ 6 - 0
Application/Abstractions/IExcelRelatorioWriter.cs

@@ -0,0 +1,6 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+
+public interface IExcelRelatorioWriter
+{
+    void Escrever(string tituloRelatorio, IReadOnlyList<dynamic> linhas, string caminhoArquivo, CancellationToken cancellationToken);
+}

+ 6 - 0
Application/Abstractions/IIpasgoZipWriter.cs

@@ -0,0 +1,6 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+
+public interface IIpasgoZipWriter
+{
+    void Escrever(string referencia, IReadOnlyList<dynamic> linhas, string caminhoArquivo, CancellationToken cancellationToken);
+}

+ 9 - 0
Application/Abstractions/IRelatorioRepository.cs

@@ -0,0 +1,9 @@
+using AGEHAB.RelatorioSeadIpasgo.Domain.Entities;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+
+public interface IRelatorioRepository
+{
+    Task<IReadOnlyList<Competencia>> ObterCompetenciasAsync(CancellationToken cancellationToken);
+    Task<IReadOnlyList<dynamic>> ObterRelatorioAsync(int competenciaHandle, string tiposCalculo, CancellationToken cancellationToken);
+}

+ 11 - 0
Application/DTOs/GerarRelatorioRequest.cs

@@ -0,0 +1,11 @@
+using AGEHAB.RelatorioSeadIpasgo.Domain.Entities;
+using AGEHAB.RelatorioSeadIpasgo.Domain.ValueObjects;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Application.DTOs;
+
+public sealed record GerarRelatorioRequest(
+    Competencia Competencia,
+    TipoRelatorioOpcao Relatorio,
+    TipoCalculo TipoCalculo,
+    string? ZipPath,
+    string ExcelPath);

+ 16 - 0
Application/UseCases/CarregarCompetenciasUseCase.cs

@@ -0,0 +1,16 @@
+using AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+using AGEHAB.RelatorioSeadIpasgo.Domain.Entities;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Application.UseCases;
+
+public class CarregarCompetenciasUseCase
+{
+    private readonly IRelatorioRepository _repository;
+
+    public CarregarCompetenciasUseCase(IRelatorioRepository repository) => _repository = repository;
+
+    public Task<IReadOnlyList<Competencia>> ExecutarAsync(CancellationToken cancellationToken)
+    {
+        return _repository.ObterCompetenciasAsync(cancellationToken);
+    }
+}

+ 71 - 0
Application/UseCases/GerarRelatorioUseCase.cs

@@ -0,0 +1,71 @@
+using System.IO;
+using AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+using AGEHAB.RelatorioSeadIpasgo.Application.DTOs;
+using AGEHAB.RelatorioSeadIpasgo.Domain.Services;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Application.UseCases;
+
+public class GerarRelatorioUseCase
+{
+    private readonly IRelatorioRepository _repository;
+    private readonly IIpasgoFiltroService _filtroIpasgoService;
+    private readonly IExcelRelatorioWriter _excelWriter;
+    private readonly IIpasgoZipWriter _zipWriter;
+
+    public GerarRelatorioUseCase(
+        IRelatorioRepository repository,
+        IIpasgoFiltroService filtroIpasgoService,
+        IExcelRelatorioWriter excelWriter,
+        IIpasgoZipWriter zipWriter)
+    {
+        _repository = repository;
+        _filtroIpasgoService = filtroIpasgoService;
+        _excelWriter = excelWriter;
+        _zipWriter = zipWriter;
+    }
+
+    public async Task ExecutarAsync(GerarRelatorioRequest request, CancellationToken cancellationToken)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        GarantirDiretorio(request.ExcelPath);
+
+        var linhas = await _repository.ObterRelatorioAsync(request.Competencia.Handle, request.TipoCalculo.Valor, cancellationToken);
+        cancellationToken.ThrowIfCancellationRequested();
+
+        if (request.Relatorio.EhIpasgo)
+        {
+            if (string.IsNullOrWhiteSpace(request.ZipPath))
+            {
+                throw new InvalidOperationException("Escolha onde salvar o ZIP do IPASGO.");
+            }
+
+            GarantirDiretorio(request.ZipPath);
+            linhas = _filtroIpasgoService.Filtrar(linhas);
+            cancellationToken.ThrowIfCancellationRequested();
+            SubstituirArquivo(request.ZipPath);
+            _zipWriter.Escrever(request.Competencia.Referencia, linhas, request.ZipPath, cancellationToken);
+        }
+
+        cancellationToken.ThrowIfCancellationRequested();
+        SubstituirArquivo(request.ExcelPath);
+        _excelWriter.Escrever(request.Relatorio.TituloExcel, linhas, request.ExcelPath, cancellationToken);
+    }
+
+    private static void GarantirDiretorio(string filePath)
+    {
+        var diretorio = Path.GetDirectoryName(filePath);
+
+        if (!string.IsNullOrWhiteSpace(diretorio) && !Directory.Exists(diretorio))
+        {
+            Directory.CreateDirectory(diretorio);
+        }
+    }
+
+    private static void SubstituirArquivo(string filePath)
+    {
+        if (File.Exists(filePath))
+        {
+            File.Delete(filePath);
+        }
+    }
+}

+ 9 - 0
Domain/Entities/Competencia.cs

@@ -0,0 +1,9 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Domain.Entities;
+
+public class Competencia
+{
+    public int Handle { get; set; }
+    public DateTime CompetenciaData { get; set; }
+    public string Referencia => CompetenciaData.ToString("yyyyMM");
+    public string Descricao => CompetenciaData.ToString("MM/yyyy");
+}

+ 7 - 0
Domain/Enums/TipoRelatorio.cs

@@ -0,0 +1,7 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Domain.Enums;
+
+public enum TipoRelatorio
+{
+    Sead,
+    Ipasgo
+}

+ 22 - 0
Domain/Services/FiltroIpasgoService.cs

@@ -0,0 +1,22 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Domain.Services;
+
+public class FiltroIpasgoService : IIpasgoFiltroService
+{
+    public IReadOnlyList<dynamic> Filtrar(IReadOnlyList<dynamic> linhas)
+    {
+        return linhas.Where(LinhaEhIpasgo).ToList();
+    }
+
+    private static bool LinhaEhIpasgo(dynamic linha)
+    {
+        var dict = (IDictionary<string, object>)linha;
+
+        return ValorNaoNulo(dict, "D1700_DescMensIpasgoBasico") ||
+               ValorNaoNulo(dict, "D1702_DescMensIpasgoEspecial");
+    }
+
+    private static bool ValorNaoNulo(IDictionary<string, object> dict, string coluna)
+    {
+        return dict.TryGetValue(coluna, out var valor) && valor != null && valor != DBNull.Value;
+    }
+}

+ 6 - 0
Domain/Services/IIpasgoFiltroService.cs

@@ -0,0 +1,6 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Domain.Services;
+
+public interface IIpasgoFiltroService
+{
+    IReadOnlyList<dynamic> Filtrar(IReadOnlyList<dynamic> linhas);
+}

+ 11 - 0
Domain/ValueObjects/TipoCalculo.cs

@@ -0,0 +1,11 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Domain.ValueObjects;
+
+public sealed record TipoCalculo(string Descricao, string Valor, string NomeArquivo)
+{
+    public static IReadOnlyList<TipoCalculo> Opcoes { get; } =
+    [
+        new("Normal", "1", "Normal"),
+        new("Complementar", "2", "Complementar"),
+        new("Normal + Complementar", "1,2,3", "NormalComplementar")
+    ];
+}

+ 16 - 0
Domain/ValueObjects/TipoRelatorioOpcao.cs

@@ -0,0 +1,16 @@
+using AGEHAB.RelatorioSeadIpasgo.Domain.Enums;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Domain.ValueObjects;
+
+public sealed record TipoRelatorioOpcao(string Descricao, TipoRelatorio Tipo)
+{
+    public bool EhIpasgo => Tipo == TipoRelatorio.Ipasgo;
+    public string TituloExcel => EhIpasgo ? "Relatório IPASGO" : "Relatório SEAD";
+    public string PrefixoArquivo => EhIpasgo ? "IPASGO" : "SEAD";
+
+    public static IReadOnlyList<TipoRelatorioOpcao> Opcoes { get; } =
+    [
+        new("SEAD", TipoRelatorio.Sead),
+        new("IPASGO", TipoRelatorio.Ipasgo)
+    ];
+}

+ 41 - 0
Infrastructure/Database/RelatorioRepository.cs

@@ -0,0 +1,41 @@
+using System.Data;
+using AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+using AGEHAB.RelatorioSeadIpasgo.Domain.Entities;
+using Dapper;
+using Microsoft.Data.SqlClient;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Infrastructure.Database;
+
+public class RelatorioRepository : IRelatorioRepository
+{
+    private readonly string _connectionString;
+
+    public RelatorioRepository(string connectionString) => _connectionString = connectionString;
+
+    public async Task<IReadOnlyList<Competencia>> ObterCompetenciasAsync(CancellationToken cancellationToken)
+    {
+        using IDbConnection db = new SqlConnection(_connectionString);
+        var command = new CommandDefinition("""
+            SELECT TOP 12
+                HANDLE AS Handle,
+                COMPETENCIA AS CompetenciaData
+            FROM FP_COMPETENCIAS
+            ORDER BY COMPETENCIA DESC
+            """, cancellationToken: cancellationToken);
+        var resultado = await db.QueryAsync<Competencia>(command);
+
+        return resultado.ToList();
+    }
+
+    public async Task<IReadOnlyList<dynamic>> ObterRelatorioAsync(int competenciaHandle, string tiposCalculo, CancellationToken cancellationToken)
+    {
+        using IDbConnection db = new SqlConnection(_connectionString);
+        var parametros = new DynamicParameters();
+        parametros.Add("@CompetenciaHandle", competenciaHandle, DbType.Int32);
+        parametros.Add("@TiposCalculo", tiposCalculo, DbType.String);
+
+        var command = new CommandDefinition("sp_AgehabRelatorioFinal", parametros, commandType: CommandType.StoredProcedure, cancellationToken: cancellationToken);
+        var resultado = await db.QueryAsync(command);
+        return resultado.ToList();
+    }
+}

+ 57 - 0
Infrastructure/Excel/ExcelRelatorioWriter.cs

@@ -0,0 +1,57 @@
+using AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+using ClosedXML.Excel;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Infrastructure.Excel;
+
+public class ExcelRelatorioWriter : IExcelRelatorioWriter
+{
+    public void Escrever(string tituloRelatorio, IReadOnlyList<dynamic> linhas, string caminhoArquivo, CancellationToken cancellationToken)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        using var workbook = new XLWorkbook();
+        var worksheet = workbook.Worksheets.Add(tituloRelatorio);
+
+        var primeiraLinha = linhas.FirstOrDefault();
+        if (primeiraLinha != null)
+        {
+            var dict = (IDictionary<string, object>)primeiraLinha;
+            var colunas = dict.Keys.ToList();
+
+            for (int i = 0; i < colunas.Count; i++)
+            {
+                worksheet.Cell(1, i + 1).Value = colunas[i];
+                worksheet.Cell(1, i + 1).Style.Font.Bold = true;
+                worksheet.Cell(1, i + 1).Style.Fill.BackgroundColor = XLColor.LightGray;
+            }
+
+            int row = 2;
+            foreach (var linha in linhas)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                var linhaDict = (IDictionary<string, object>)linha;
+                for (int i = 0; i < colunas.Count; i++)
+                {
+                    var valor = linhaDict[colunas[i]];
+                    if (valor is decimal decimalValue)
+                    {
+                        worksheet.Cell(row, i + 1).Value = decimalValue;
+                        worksheet.Cell(row, i + 1).Style.NumberFormat.Format = "#,##0.00";
+                    }
+                    else if (valor is int intValue)
+                    {
+                        worksheet.Cell(row, i + 1).Value = intValue;
+                    }
+                    else
+                    {
+                        worksheet.Cell(row, i + 1).Value = valor?.ToString() ?? "";
+                    }
+                }
+
+                row++;
+            }
+        }
+
+        worksheet.Columns().AdjustToContents();
+        workbook.SaveAs(caminhoArquivo);
+    }
+}

+ 145 - 0
Infrastructure/Zip/IpasgoZipWriter.cs

@@ -0,0 +1,145 @@
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+using AGEHAB.RelatorioSeadIpasgo.Application.Abstractions;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Infrastructure.Zip;
+
+public class IpasgoZipWriter : IIpasgoZipWriter
+{
+    private static readonly Dictionary<string, string> DePara = new()
+    {
+        ["1"] = "01",
+        ["5026"] = "02",
+        ["5007"] = "04",
+        ["5050"] = "04",
+        ["5046"] = "04",
+        ["102"] = "06",
+        ["104"] = "06",
+        ["5934"] = "10",
+        ["5"] = "13",
+        ["200"] = "13",
+        ["5030"] = "16",
+        ["5101"] = "16",
+        ["51"] = "18",
+        ["52"] = "18",
+        ["53"] = "18",
+        ["41"] = "18",
+        ["100"] = "20",
+        ["101"] = "21",
+        ["20"] = "22",
+        ["110"] = "31",
+        ["5067"] = "31",
+        ["5009"] = "31",
+        ["12"] = "50",
+        ["90"] = "51",
+        ["22"] = "53",
+        ["23"] = "53"
+    };
+
+    public void Escrever(string referencia, IReadOnlyList<dynamic> linhas, string caminhoArquivo, CancellationToken cancellationToken)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        var remessa = MontarRemessa(linhas, cancellationToken);
+        var nomeBase = $"REM{referencia}";
+        var pastaTemporaria = Path.Combine(Path.GetTempPath(), "Ipasgo", Guid.NewGuid().ToString("N"));
+
+        Directory.CreateDirectory(pastaTemporaria);
+
+        try
+        {
+            var txtPath = Path.Combine(pastaTemporaria, $"{nomeBase}.txt");
+            var zipInternoPath = Path.Combine(pastaTemporaria, Path.GetFileName(caminhoArquivo));
+
+            cancellationToken.ThrowIfCancellationRequested();
+            File.WriteAllText(txtPath, remessa, Encoding.UTF8);
+
+            using (var zip = ZipFile.Open(zipInternoPath, ZipArchiveMode.Create))
+                zip.CreateEntryFromFile(txtPath, Path.GetFileName(txtPath), CompressionLevel.Optimal);
+
+            cancellationToken.ThrowIfCancellationRequested();
+            using (var outerZip = ZipFile.Open(caminhoArquivo, ZipArchiveMode.Create))
+                outerZip.CreateEntryFromFile(zipInternoPath, Path.GetFileName(zipInternoPath), CompressionLevel.Optimal);
+        }
+        finally
+        {
+            if (Directory.Exists(pastaTemporaria))
+            {
+                Directory.Delete(pastaTemporaria, true);
+            }
+        }
+    }
+
+    private static string MontarRemessa(IReadOnlyList<dynamic> linhas, CancellationToken cancellationToken)
+    {
+        var salarios = ObterSalarios(linhas, cancellationToken);
+        var sb = new StringBuilder();
+        var sequencial = 1;
+
+        foreach (var item in salarios)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+            sb.Append("10");
+            sb.Append("8792");
+            sb.Append(item.Cpf.PadLeft(11, '0'));
+            sb.Append(new string(' ', 30));
+            sb.Append("0001160");
+            sb.Append(item.Sufixo.PadLeft(2, '0'));
+            sb.Append("000");
+            sb.Append(new string(' ', 50));
+            sb.Append("00");
+            sb.Append(new string(' ', 255));
+            sb.Append(item.Valor.ToString("0000000000.00").Replace('.', ','));
+            sb.Append(sequencial.ToString().PadLeft(6, '0'));
+            sb.AppendLine();
+            sequencial++;
+        }
+
+        sb.Append("99");
+        sb.Append(new string(' ', 380));
+        sb.Append(sequencial.ToString().PadLeft(6, '0'));
+
+        return sb.ToString();
+    }
+
+    private static List<(string Cpf, string Sufixo, decimal Valor)> ObterSalarios(IReadOnlyList<dynamic> linhas, CancellationToken cancellationToken)
+    {
+        var resultado = new List<(string Cpf, string Sufixo, decimal Valor)>();
+
+        foreach (var linha in linhas)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+            var dict = (IDictionary<string, object>)linha;
+
+            if (!dict.ContainsKey("CPF"))
+                continue;
+
+            var cpf = dict["CPF"]?.ToString();
+
+            if (string.IsNullOrWhiteSpace(cpf))
+                continue;
+
+            foreach (var kv in dict)
+            {
+                if (kv.Key.StartsWith("R") || kv.Key.StartsWith("D"))
+                {
+                    if (kv.Value is decimal valor && valor != 0)
+                    {
+                        var match = System.Text.RegularExpressions.Regex.Match(kv.Key, @"\d+");
+                        if (match.Success)
+                        {
+                            var codigo = match.Value.TrimStart('0');
+
+                            if (DePara.TryGetValue(codigo, out var sufixo))
+                            {
+                                resultado.Add((cpf, sufixo, valor));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return resultado;
+    }
+}

+ 49 - 0
Presentation/Commands/AsyncRelayCommand.cs

@@ -0,0 +1,49 @@
+using System.Windows.Input;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation.Commands;
+
+public class AsyncRelayCommand : ICommand
+{
+    private readonly Func<Task> _execute;
+    private readonly Func<bool>? _canExecute;
+    private bool _isExecuting;
+
+    public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
+    {
+        _execute = execute;
+        _canExecute = canExecute;
+    }
+
+    public event EventHandler? CanExecuteChanged;
+
+    public bool CanExecute(object? parameter)
+    {
+        return !_isExecuting && (_canExecute?.Invoke() ?? true);
+    }
+
+    public async void Execute(object? parameter)
+    {
+        if (!CanExecute(parameter))
+        {
+            return;
+        }
+
+        _isExecuting = true;
+        RaiseCanExecuteChanged();
+
+        try
+        {
+            await _execute();
+        }
+        finally
+        {
+            _isExecuting = false;
+            RaiseCanExecuteChanged();
+        }
+    }
+
+    public void RaiseCanExecuteChanged()
+    {
+        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+    }
+}

+ 25 - 0
Presentation/Commands/RelayCommand.cs

@@ -0,0 +1,25 @@
+using System.Windows.Input;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation.Commands;
+
+public class RelayCommand : ICommand
+{
+    private readonly Action _execute;
+    private readonly Func<bool>? _canExecute;
+
+    public RelayCommand(Action execute, Func<bool>? canExecute = null)
+    {
+        _execute = execute;
+        _canExecute = canExecute;
+    }
+
+    public event EventHandler? CanExecuteChanged;
+
+    public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
+    public void Execute(object? parameter) => _execute();
+
+    public void RaiseCanExecuteChanged()
+    {
+        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+    }
+}

+ 196 - 0
Presentation/MainWindow.xaml

@@ -0,0 +1,196 @@
+<Window x:Class="AGEHAB.RelatorioSeadIpasgo.Presentation.MainWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        Title="Gerador Relatório SEAD e Ipasgo"
+        Width="980"
+        MinWidth="940"
+        MinHeight="500"
+        SizeToContent="Height"
+        ResizeMode="NoResize"
+        Cursor="{Binding CursorAtual}"
+        WindowStartupLocation="CenterScreen">
+    <Window.Resources>
+        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
+        <Style TargetType="Button">
+            <Setter Property="Height" Value="38" />
+            <Setter Property="Padding" Value="16,0" />
+            <Setter Property="VerticalContentAlignment" Value="Center" />
+            <Setter Property="HorizontalContentAlignment" Value="Center" />
+            <Setter Property="FontWeight" Value="SemiBold" />
+            <Setter Property="Cursor" Value="Hand" />
+        </Style>
+        <Style TargetType="ComboBox">
+            <Setter Property="Height" Value="36" />
+            <Setter Property="VerticalContentAlignment" Value="Center" />
+        </Style>
+        <Style TargetType="TextBox">
+            <Setter Property="Height" Value="36" />
+            <Setter Property="Padding" Value="10,5" />
+            <Setter Property="VerticalContentAlignment" Value="Center" />
+        </Style>
+    </Window.Resources>
+
+    <Border Padding="28" Background="#EEF3F8">
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="20" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="14" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="14" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="20" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
+
+            <Border Grid.Row="0"
+                    Padding="22"
+                    Background="#1D3557"
+                    CornerRadius="14">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="*" />
+                        <ColumnDefinition Width="Auto" />
+                    </Grid.ColumnDefinitions>
+
+                    <StackPanel Grid.Column="0">
+                        <TextBlock Text="Gerador Relatório SEAD e Ipasgo"
+                                   FontSize="25"
+                                   FontWeight="SemiBold"
+                                   Foreground="White" />
+                        <TextBlock Text="Escolha o relatório, a competência, o tipo de cálculo e uma pasta única de destino."
+                                   Margin="0,7,0,0"
+                                   Foreground="#D8E4F0" />
+                    </StackPanel>
+
+                    <TextBlock Grid.Column="1"
+                               VerticalAlignment="Top"
+                               Foreground="#D8E4F0"
+                               FontWeight="SemiBold"
+                               Text="Versão: 1.0" />
+                </Grid>
+            </Border>
+
+            <Border Grid.Row="2" Padding="18" Background="White" CornerRadius="12">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="80" />
+                        <ColumnDefinition Width="150" />
+                        <ColumnDefinition Width="18" />
+                        <ColumnDefinition Width="100" />
+                        <ColumnDefinition Width="150" />
+                        <ColumnDefinition Width="18" />
+                        <ColumnDefinition Width="100" />
+                        <ColumnDefinition Width="220" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
+
+                    <TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Relatório" Foreground="#374151" />
+                    <ComboBox Grid.Column="1"
+                              ItemsSource="{Binding Relatorios}"
+                              SelectedItem="{Binding RelatorioSelecionado}"
+                              DisplayMemberPath="Descricao"
+                              IsEnabled="{Binding CamposHabilitados}" />
+
+                    <TextBlock Grid.Column="3" VerticalAlignment="Center" Text="Competência" Foreground="#374151" />
+                    <ComboBox Grid.Column="4"
+                              ItemsSource="{Binding Competencias}"
+                              SelectedItem="{Binding CompetenciaSelecionada}"
+                              DisplayMemberPath="Descricao"
+                              IsEnabled="{Binding CamposHabilitados}" />
+
+                    <TextBlock Grid.Column="6" VerticalAlignment="Center" Text="Tipo cálculo" Foreground="#374151" />
+                    <ComboBox Grid.Column="7"
+                              ItemsSource="{Binding TiposCalculo}"
+                              SelectedItem="{Binding TipoCalculoSelecionado}"
+                              DisplayMemberPath="Descricao"
+                              IsEnabled="{Binding CamposHabilitados}" />
+                </Grid>
+            </Border>
+
+            <Border Grid.Row="4" Padding="18" Background="White" CornerRadius="12">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="130" />
+                        <ColumnDefinition Width="*" />
+                        <ColumnDefinition Width="14" />
+                        <ColumnDefinition Width="150" />
+                    </Grid.ColumnDefinitions>
+
+                    <TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Pasta destino" Foreground="#374151" />
+                    <TextBox Grid.Column="1"
+                             Text="{Binding PastaDestino, UpdateSourceTrigger=PropertyChanged}"
+                             IsReadOnly="True"
+                             IsEnabled="{Binding CamposHabilitados}" />
+                    <Button Grid.Column="3"
+                            Content="Selecionar pasta"
+                            Command="{Binding EscolherPastaCommand}" />
+                </Grid>
+            </Border>
+
+            <Border Grid.Row="6"
+                    Padding="18"
+                    Background="White"
+                    CornerRadius="12"
+                    Visibility="{Binding IpasgoSelecionado, Converter={StaticResource BooleanToVisibilityConverter}}">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="130" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
+
+                    <TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Arquivo ZIP" Foreground="#374151" />
+                    <TextBox Grid.Column="1"
+                             Text="{Binding ZipPath, UpdateSourceTrigger=PropertyChanged}"
+                             IsReadOnly="True" />
+                </Grid>
+            </Border>
+
+            <Border Grid.Row="7"
+                    Height="14"
+                    Visibility="{Binding IpasgoSelecionado, Converter={StaticResource BooleanToVisibilityConverter}}" />
+
+            <Border Grid.Row="8" Padding="18" Background="White" CornerRadius="12">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="130" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
+
+                    <TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Arquivo Excel" Foreground="#374151" />
+                    <TextBox Grid.Column="1"
+                             Text="{Binding ExcelPath, UpdateSourceTrigger=PropertyChanged}"
+                             IsReadOnly="True" />
+                </Grid>
+            </Border>
+
+            <DockPanel Grid.Row="10" LastChildFill="False">
+                <TextBlock DockPanel.Dock="Left"
+                           VerticalAlignment="Center"
+                           Foreground="#4B5563"
+                           Text="{Binding Status}" />
+
+                <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
+                    <Button Width="130"
+                            Margin="0,0,12,0"
+                            Content="Abrir pasta"
+                            Command="{Binding AbrirPastaCommand}" />
+                    <Button Width="120"
+                            Margin="0,0,12,0"
+                            Background="#DC2626"
+                            Foreground="White"
+                            Content="Cancelar"
+                            Command="{Binding CancelarCommand}" />
+                    <Button Width="160"
+                            Background="#1D4ED8"
+                            Foreground="White"
+                            Content="Gerar arquivos"
+                            Command="{Binding GerarArquivosCommand}" />
+                </StackPanel>
+            </DockPanel>
+        </Grid>
+    </Border>
+</Window>

+ 14 - 0
Presentation/MainWindow.xaml.cs

@@ -0,0 +1,14 @@
+using System.Windows;
+using AGEHAB.RelatorioSeadIpasgo.Presentation.ViewModels;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation;
+
+public partial class MainWindow : Window
+{
+    public MainWindow(MainViewModel viewModel)
+    {
+        InitializeComponent();
+        DataContext = viewModel;
+        Loaded += (_, _) => viewModel.CarregarCommand.Execute(null);
+    }
+}

+ 11 - 0
Presentation/Services/IUserDialogService.cs

@@ -0,0 +1,11 @@
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation.Services;
+
+public interface IUserDialogService
+{
+    string? EscolherArquivoParaSalvar(string titulo, string filtro, string extensaoPadrao, string caminhoAtual);
+    string? EscolherPasta(string titulo, string pastaAtual);
+    bool ConfirmarSubstituicao(string caminhoArquivo);
+    void Informar(string mensagem, string titulo);
+    void Alertar(string mensagem, string titulo);
+    void ExibirErro(string mensagem);
+}

+ 81 - 0
Presentation/Services/UserDialogService.cs

@@ -0,0 +1,81 @@
+using Microsoft.Win32;
+using System.IO;
+using System.Windows;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation.Services;
+
+public class UserDialogService : IUserDialogService
+{
+    public string? EscolherArquivoParaSalvar(string titulo, string filtro, string extensaoPadrao, string caminhoAtual)
+    {
+        var dialog = new SaveFileDialog
+        {
+            Title = titulo,
+            Filter = filtro,
+            DefaultExt = extensaoPadrao,
+            AddExtension = true,
+            OverwritePrompt = false,
+            FileName = Path.GetFileName(caminhoAtual),
+            InitialDirectory = ObterDiretorioInicial(caminhoAtual)
+        };
+
+        return dialog.ShowDialog(ObterJanelaPrincipal()) == true ? dialog.FileName : null;
+    }
+
+    public string? EscolherPasta(string titulo, string pastaAtual)
+    {
+        var dialog = new OpenFolderDialog
+        {
+            Title = titulo,
+            InitialDirectory = Directory.Exists(pastaAtual)
+                ? pastaAtual
+                : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)
+        };
+
+        return dialog.ShowDialog(ObterJanelaPrincipal()) == true ? dialog.FolderName : null;
+    }
+
+    public bool ConfirmarSubstituicao(string caminhoArquivo)
+    {
+        var resultado = MessageBox.Show(
+            ObterJanelaPrincipal(),
+            $"O arquivo já existe:\n\n{caminhoArquivo}\n\nDeseja substituir?",
+            "Substituir arquivo",
+            MessageBoxButton.YesNo,
+            MessageBoxImage.Question);
+
+        return resultado == MessageBoxResult.Yes;
+    }
+
+    public void Informar(string mensagem, string titulo)
+    {
+        MessageBox.Show(ObterJanelaPrincipal(), mensagem, titulo, MessageBoxButton.OK, MessageBoxImage.Information);
+    }
+
+    public void Alertar(string mensagem, string titulo)
+    {
+        MessageBox.Show(ObterJanelaPrincipal(), mensagem, titulo, MessageBoxButton.OK, MessageBoxImage.Warning);
+    }
+
+    public void ExibirErro(string mensagem)
+    {
+        MessageBox.Show(ObterJanelaPrincipal(), mensagem, "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
+    }
+
+    private static Window ObterJanelaPrincipal()
+    {
+        return System.Windows.Application.Current.MainWindow;
+    }
+
+    private static string ObterDiretorioInicial(string caminhoAtual)
+    {
+        var diretorio = Path.GetDirectoryName(caminhoAtual);
+
+        if (!string.IsNullOrWhiteSpace(diretorio) && Directory.Exists(diretorio))
+        {
+            return diretorio;
+        }
+
+        return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+    }
+}

+ 436 - 0
Presentation/ViewModels/MainViewModel.cs

@@ -0,0 +1,436 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Windows.Input;
+using AGEHAB.RelatorioSeadIpasgo.Application.DTOs;
+using AGEHAB.RelatorioSeadIpasgo.Application.UseCases;
+using AGEHAB.RelatorioSeadIpasgo.Domain.Entities;
+using AGEHAB.RelatorioSeadIpasgo.Domain.ValueObjects;
+using AGEHAB.RelatorioSeadIpasgo.Presentation.Commands;
+using AGEHAB.RelatorioSeadIpasgo.Presentation.Services;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation.ViewModels;
+
+public class MainViewModel : ViewModelBase
+{
+    private readonly CarregarCompetenciasUseCase _carregarCompetenciasUseCase;
+    private readonly GerarRelatorioUseCase _gerarRelatorioUseCase;
+    private readonly IUserDialogService _dialogService;
+    private readonly RelayCommand _escolherZipCommand;
+    private readonly RelayCommand _escolherExcelCommand;
+    private readonly RelayCommand _escolherPastaCommand;
+    private readonly RelayCommand _abrirPastaCommand;
+    private readonly RelayCommand _cancelarCommand;
+    private readonly AsyncRelayCommand _gerarArquivosCommand;
+    private CancellationTokenSource? _cancellationTokenSource;
+
+    private TipoRelatorioOpcao? _relatorioSelecionado;
+    private Competencia? _competenciaSelecionada;
+    private TipoCalculo? _tipoCalculoSelecionado;
+    private string _zipPath;
+    private string _excelPath;
+    private string _pastaDestino;
+    private string _status = "Pronto para gerar.";
+    private bool _estaOcupado;
+
+    public MainViewModel(
+        CarregarCompetenciasUseCase carregarCompetenciasUseCase,
+        GerarRelatorioUseCase gerarRelatorioUseCase,
+        IUserDialogService dialogService)
+    {
+        _carregarCompetenciasUseCase = carregarCompetenciasUseCase;
+        _gerarRelatorioUseCase = gerarRelatorioUseCase;
+        _dialogService = dialogService;
+
+        var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+        _pastaDestino = desktop;
+        _zipPath = Path.Combine(desktop, "REM202606.zip");
+        _excelPath = Path.Combine(desktop, "Relatorio_SEAD_202606.xlsx");
+
+        Relatorios = new ObservableCollection<TipoRelatorioOpcao>(TipoRelatorioOpcao.Opcoes);
+        TiposCalculo = new ObservableCollection<TipoCalculo>(TipoCalculo.Opcoes);
+
+        CarregarCommand = new AsyncRelayCommand(CarregarAsync);
+        _gerarArquivosCommand = new AsyncRelayCommand(GerarArquivosAsync, PodeGerar);
+        GerarArquivosCommand = _gerarArquivosCommand;
+        _escolherZipCommand = new RelayCommand(EscolherZip, () => !EstaOcupado && ZipHabilitado);
+        EscolherZipCommand = _escolherZipCommand;
+        _escolherExcelCommand = new RelayCommand(EscolherExcel, () => !EstaOcupado);
+        EscolherExcelCommand = _escolherExcelCommand;
+        _escolherPastaCommand = new RelayCommand(EscolherPasta, () => !EstaOcupado);
+        EscolherPastaCommand = _escolherPastaCommand;
+        _abrirPastaCommand = new RelayCommand(AbrirPastaArquivos, () => !EstaOcupado);
+        AbrirPastaCommand = _abrirPastaCommand;
+        _cancelarCommand = new RelayCommand(CancelarOperacao, () => EstaOcupado);
+        CancelarCommand = _cancelarCommand;
+
+        RelatorioSelecionado = Relatorios.FirstOrDefault();
+        TipoCalculoSelecionado = TiposCalculo.FirstOrDefault(tipoCalculo => tipoCalculo.Valor == "1,2,3");
+    }
+
+    public ObservableCollection<TipoRelatorioOpcao> Relatorios { get; }
+    public ObservableCollection<Competencia> Competencias { get; } = [];
+    public ObservableCollection<TipoCalculo> TiposCalculo { get; }
+
+    public TipoRelatorioOpcao? RelatorioSelecionado
+    {
+        get => _relatorioSelecionado;
+        set
+        {
+            if (SetProperty(ref _relatorioSelecionado, value))
+            {
+                OnPropertyChanged(nameof(ZipHabilitado));
+                OnPropertyChanged(nameof(IpasgoSelecionado));
+                OnPropertyChanged(nameof(AlturaJanela));
+                AtualizarNomeArquivos();
+                AtualizarComandos();
+            }
+        }
+    }
+
+    public Competencia? CompetenciaSelecionada
+    {
+        get => _competenciaSelecionada;
+        set
+        {
+            if (SetProperty(ref _competenciaSelecionada, value))
+            {
+                AtualizarNomeArquivos();
+                AtualizarComandos();
+            }
+        }
+    }
+
+    public TipoCalculo? TipoCalculoSelecionado
+    {
+        get => _tipoCalculoSelecionado;
+        set
+        {
+            if (SetProperty(ref _tipoCalculoSelecionado, value))
+            {
+                AtualizarNomeArquivos();
+                AtualizarComandos();
+            }
+        }
+    }
+
+    public string ZipPath
+    {
+        get => _zipPath;
+        set => SetProperty(ref _zipPath, value);
+    }
+
+    public string ExcelPath
+    {
+        get => _excelPath;
+        set => SetProperty(ref _excelPath, value);
+    }
+
+    public string PastaDestino
+    {
+        get => _pastaDestino;
+        set
+        {
+            if (SetProperty(ref _pastaDestino, value))
+            {
+                AtualizarNomeArquivos();
+            }
+        }
+    }
+
+    public string Status
+    {
+        get => _status;
+        set => SetProperty(ref _status, value);
+    }
+
+    public bool EstaOcupado
+    {
+        get => _estaOcupado;
+        set
+        {
+            if (SetProperty(ref _estaOcupado, value))
+            {
+                OnPropertyChanged(nameof(ZipHabilitado));
+                OnPropertyChanged(nameof(CursorAtual));
+                AtualizarComandos();
+            }
+        }
+    }
+
+    public bool ZipHabilitado => !EstaOcupado && RelatorioSelecionado?.EhIpasgo == true;
+    public bool CamposHabilitados => !EstaOcupado;
+    public bool CancelamentoHabilitado => EstaOcupado;
+    public bool IpasgoSelecionado => RelatorioSelecionado?.EhIpasgo == true;
+    public double AlturaJanela => IpasgoSelecionado ? 640 : 560;
+    public Cursor CursorAtual => EstaOcupado ? Cursors.Wait : Cursors.Arrow;
+
+    public ICommand CarregarCommand { get; }
+    public ICommand GerarArquivosCommand { get; }
+    public ICommand EscolherZipCommand { get; }
+    public ICommand EscolherExcelCommand { get; }
+    public ICommand EscolherPastaCommand { get; }
+    public ICommand AbrirPastaCommand { get; }
+    public ICommand CancelarCommand { get; }
+
+    private async Task CarregarAsync()
+    {
+        _cancellationTokenSource?.Dispose();
+        _cancellationTokenSource = new CancellationTokenSource();
+        EstaOcupado = true;
+        Status = "Carregando competências...";
+
+        try
+        {
+            var competencias = await _carregarCompetenciasUseCase.ExecutarAsync(_cancellationTokenSource.Token);
+            Competencias.Clear();
+
+            foreach (var competencia in competencias)
+            {
+                Competencias.Add(competencia);
+            }
+
+            CompetenciaSelecionada = Competencias.FirstOrDefault();
+            Status = Competencias.Count > 0 ? "Pronto para gerar." : "Nenhuma competência encontrada.";
+
+            if (Competencias.Count == 0)
+            {
+                _dialogService.Alertar("Nenhuma competência foi encontrada em FP_COMPETENCIAS.", "Competências");
+            }
+        }
+        catch (OperationCanceledException)
+        {
+            Status = "Operação cancelada pelo usuário.";
+        }
+        catch (Exception ex)
+        {
+            Status = "Erro ao carregar competências.";
+            _dialogService.ExibirErro(ex.Message);
+        }
+        finally
+        {
+            _cancellationTokenSource?.Dispose();
+            _cancellationTokenSource = null;
+            EstaOcupado = false;
+        }
+    }
+
+    private async Task GerarArquivosAsync()
+    {
+        if (!ValidarCampos())
+        {
+            return;
+        }
+
+        if (!ConfirmarSubstituicoes())
+        {
+            return;
+        }
+
+        _cancellationTokenSource?.Dispose();
+        _cancellationTokenSource = new CancellationTokenSource();
+        EstaOcupado = true;
+        Status = "Gerando arquivos...";
+
+        try
+        {
+            var request = new GerarRelatorioRequest(
+                CompetenciaSelecionada!,
+                RelatorioSelecionado!,
+                TipoCalculoSelecionado!,
+                ZipPath,
+                ExcelPath);
+
+            await _gerarRelatorioUseCase.ExecutarAsync(request, _cancellationTokenSource.Token);
+            Status = "Arquivos gerados com sucesso.";
+            _dialogService.Informar("Arquivos gerados com sucesso.", "Concluído");
+        }
+        catch (OperationCanceledException)
+        {
+            Status = "Operação cancelada pelo usuário.";
+        }
+        catch (Exception ex)
+        {
+            if (EhCancelamentoSolicitado(ex))
+            {
+                Status = "Operação cancelada pelo usuário.";
+                return;
+            }
+
+            Status = "Erro ao gerar arquivos.";
+            _dialogService.ExibirErro(ex.Message);
+        }
+        finally
+        {
+            _cancellationTokenSource?.Dispose();
+            _cancellationTokenSource = null;
+            EstaOcupado = false;
+        }
+    }
+
+    private void CancelarOperacao()
+    {
+        Status = "Cancelando...";
+        _cancellationTokenSource?.Cancel();
+    }
+
+    private void EscolherZip()
+    {
+        var fileName = _dialogService.EscolherArquivoParaSalvar("Salvar arquivo ZIP", "Arquivo ZIP (*.zip)|*.zip", ".zip", ZipPath);
+
+        if (fileName != null)
+        {
+            ZipPath = fileName;
+        }
+    }
+
+    private void EscolherExcel()
+    {
+        var fileName = _dialogService.EscolherArquivoParaSalvar("Salvar arquivo Excel", "Arquivo Excel (*.xlsx)|*.xlsx", ".xlsx", ExcelPath);
+
+        if (fileName != null)
+        {
+            ExcelPath = fileName;
+        }
+    }
+
+    private void EscolherPasta()
+    {
+        var pasta = _dialogService.EscolherPasta("Selecionar pasta de destino", PastaDestino);
+
+        if (pasta != null)
+        {
+            PastaDestino = pasta;
+        }
+    }
+
+    private void AbrirPastaArquivos()
+    {
+        if (!Directory.Exists(PastaDestino))
+        {
+            _dialogService.Alertar("Nenhuma pasta foi encontrada para abrir.", "Pastas");
+            return;
+        }
+
+        AbrirCaminho(PastaDestino);
+    }
+
+    private bool ValidarCampos()
+    {
+        if (RelatorioSelecionado == null)
+        {
+            _dialogService.Alertar("Selecione o relatório.", "Validação");
+            return false;
+        }
+
+        if (CompetenciaSelecionada == null)
+        {
+            _dialogService.Alertar("Selecione uma competência.", "Validação");
+            return false;
+        }
+
+        if (TipoCalculoSelecionado == null)
+        {
+            _dialogService.Alertar("Selecione um tipo de cálculo.", "Validação");
+            return false;
+        }
+
+        if (RelatorioSelecionado.EhIpasgo && string.IsNullOrWhiteSpace(ZipPath))
+        {
+            _dialogService.Alertar("Escolha onde salvar o ZIP do IPASGO.", "Validação");
+            return false;
+        }
+
+        if (string.IsNullOrWhiteSpace(ExcelPath))
+        {
+            _dialogService.Alertar("Escolha onde salvar o Excel.", "Validação");
+            return false;
+        }
+
+        return true;
+    }
+
+    private bool ConfirmarSubstituicoes()
+    {
+        if (RelatorioSelecionado?.EhIpasgo == true && File.Exists(ZipPath) && !_dialogService.ConfirmarSubstituicao(ZipPath))
+        {
+            return false;
+        }
+
+        if (File.Exists(ExcelPath) && !_dialogService.ConfirmarSubstituicao(ExcelPath))
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    private bool PodeGerar()
+    {
+        return !EstaOcupado && RelatorioSelecionado != null && CompetenciaSelecionada != null && TipoCalculoSelecionado != null;
+    }
+
+    private static bool EhCancelamentoSolicitado(Exception ex)
+    {
+        return ex.Message.Contains("Operação cancelada pelo usuário", StringComparison.OrdinalIgnoreCase) ||
+               ex.Message.Contains("Operation cancelled", StringComparison.OrdinalIgnoreCase) ||
+               ex.Message.Contains("Operation canceled", StringComparison.OrdinalIgnoreCase);
+    }
+
+    private void AtualizarNomeArquivos()
+    {
+        if (CompetenciaSelecionada == null || RelatorioSelecionado == null || TipoCalculoSelecionado == null)
+        {
+            return;
+        }
+
+        ZipPath = Path.Combine(PastaDestino, $"REM{CompetenciaSelecionada.Referencia}.zip");
+        ExcelPath = Path.Combine(PastaDestino, $"Relatorio_{RelatorioSelecionado.PrefixoArquivo}_{CompetenciaSelecionada.Referencia}_{TipoCalculoSelecionado.NomeArquivo}.xlsx");
+    }
+
+    private void AtualizarComandos()
+    {
+        _gerarArquivosCommand.RaiseCanExecuteChanged();
+        _escolherZipCommand.RaiseCanExecuteChanged();
+        _escolherExcelCommand.RaiseCanExecuteChanged();
+        _escolherPastaCommand.RaiseCanExecuteChanged();
+        _abrirPastaCommand.RaiseCanExecuteChanged();
+        _cancelarCommand.RaiseCanExecuteChanged();
+        OnPropertyChanged(nameof(ZipHabilitado));
+        OnPropertyChanged(nameof(CamposHabilitados));
+        OnPropertyChanged(nameof(CancelamentoHabilitado));
+        OnPropertyChanged(nameof(CursorAtual));
+        OnPropertyChanged(nameof(IpasgoSelecionado));
+        OnPropertyChanged(nameof(AlturaJanela));
+    }
+
+    private static void AbrirCaminho(string caminho)
+    {
+        Process.Start(new ProcessStartInfo(caminho)
+        {
+            UseShellExecute = true
+        });
+    }
+
+    private static void AdicionarPastaSeExistir(HashSet<string> pastas, string caminhoArquivo)
+    {
+        var pasta = Path.GetDirectoryName(caminhoArquivo);
+
+        if (!string.IsNullOrWhiteSpace(pasta) && Directory.Exists(pasta))
+        {
+            pastas.Add(pasta);
+        }
+    }
+
+    private static string ObterDiretorioInicial(string caminhoAtual)
+    {
+        var diretorio = Path.GetDirectoryName(caminhoAtual);
+
+        if (!string.IsNullOrWhiteSpace(diretorio) && Directory.Exists(diretorio))
+        {
+            return diretorio;
+        }
+
+        return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+    }
+}

+ 26 - 0
Presentation/ViewModels/ViewModelBase.cs

@@ -0,0 +1,26 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace AGEHAB.RelatorioSeadIpasgo.Presentation.ViewModels;
+
+public abstract class ViewModelBase : INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
+    {
+        if (EqualityComparer<T>.Default.Equals(field, value))
+        {
+            return false;
+        }
+
+        field = value;
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        return true;
+    }
+
+    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+}