diff --git a/TrafagSalesExporter/Components/Layout/MainLayout.razor b/TrafagSalesExporter/Components/Layout/MainLayout.razor index 5a34d7e..b6b9845 100644 --- a/TrafagSalesExporter/Components/Layout/MainLayout.razor +++ b/TrafagSalesExporter/Components/Layout/MainLayout.razor @@ -12,18 +12,30 @@ - @T("Trafag Finanze/Sales Management Cockpit", "Trafag Finance/Sales Management Cockpit") + @T("Trafag Finance/Sales Management Cockpit", "Trafag Finance/Sales Management Cockpit") - - DE - EN - + + + + @LanguageLabel + + + + Deutsch + English + Español + Italiano + हिन्दी + + @ShortName(authState.User) @@ -71,6 +83,8 @@ InvokeAsync(StateHasChanged); } + private string LanguageLabel => UiText.CurrentLanguage.ToUpperInvariant(); + private string T(string german, string english) => UiText.Text(german, english); private static string ShortName(ClaimsPrincipal user) diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor index 9a1d2fd..d94ccf0 100644 --- a/TrafagSalesExporter/Components/Layout/NavMenu.razor +++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor @@ -1,4 +1,5 @@ @using TrafagSalesExporter.Security +@implements IDisposable @inject TrafagSalesExporter.Services.IUiTextService UiText @inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess @inject IConfiguration Configuration @@ -58,11 +59,26 @@ @code { private bool ShowFinanceComparison => Configuration.GetValue("Navigation:ShowFinanceComparison", true); + protected override void OnInitialized() + { + UiText.Changed += HandleLanguageChanged; + } + private void LockFinanceCockpit() { FinanceAccess.Lock(); Navigation.NavigateTo("/"); } + private void HandleLanguageChanged() + { + InvokeAsync(StateHasChanged); + } + private string T(string german, string english) => UiText.Text(german, english); + + public void Dispose() + { + UiText.Changed -= HandleLanguageChanged; + } } diff --git a/TrafagSalesExporter/Services/ConsolidatedExportService.cs b/TrafagSalesExporter/Services/ConsolidatedExportService.cs index 9be8915..f089e4b 100644 --- a/TrafagSalesExporter/Services/ConsolidatedExportService.cs +++ b/TrafagSalesExporter/Services/ConsolidatedExportService.cs @@ -57,7 +57,8 @@ public class ConsolidatedExportService : IConsolidatedExportService await _sharePointService.UploadAsync( spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, - spConfig.SiteUrl, sharePointFolder, landSubfolder, consolidatedPath); + spConfig.SiteUrl, sharePointFolder, landSubfolder, consolidatedPath, + uploadTimestampedCopyIfLocked: true); } return consolidatedPath; diff --git a/TrafagSalesExporter/Services/ExcelExportService.cs b/TrafagSalesExporter/Services/ExcelExportService.cs index c4da3d0..09c93cb 100644 --- a/TrafagSalesExporter/Services/ExcelExportService.cs +++ b/TrafagSalesExporter/Services/ExcelExportService.cs @@ -184,6 +184,7 @@ public class ExcelExportService : IExcelExportService if (includeFinanceHelpSheet) { AddFinanceSummarySheet(workbook, records, financeRules); + AddFinanceDetailsSheet(workbook, records, financeRules); AddFinanceHelpSheet(workbook); } @@ -266,6 +267,107 @@ public class ExcelExportService : IExcelExportService ws.Columns().AdjustToContents(); } + private static void AddFinanceDetailsSheet(XLWorkbook workbook, List records, IReadOnlyList financeRules) + { + var ws = workbook.Worksheets.Add("Finance Details"); + var financeRuleEngine = new FinanceRuleEngine(financeRules); + ws.Position = 2; + + ws.Cell(1, 1).Value = "Finance Details"; + ws.Cell(1, 1).Style.Font.Bold = true; + ws.Cell(1, 1).Style.Font.FontSize = 14; + ws.Cell(2, 1).Value = "Diese Zeilen fuehren zur Summe im Blatt Finance Summary. Summe ueber Net Sales Actual bilden."; + + var headers = new[] + { + "Year", + "Country Key", + "Currency", + "Finance Date", + "Net Sales Actual", + "Source Value Field", + "TSC", + "Land", + "Document Type", + "Invoice Number", + "Position on invoice", + "Document Entry", + "Material", + "Name", + "Quantity", + "Customer number", + "Customer name", + "Customer country", + "Supplier number", + "Supplier name", + "Supplier country", + "posting date", + "invoice date", + "Sales Price/Value", + "Sales Currency", + "Document Currency", + "Document Total FC", + "Document Total LC", + "Company Currency" + }; + + for (var i = 0; i < headers.Length; i++) + { + ws.Cell(4, i + 1).Value = headers[i]; + ws.Cell(4, i + 1).Style.Font.Bold = true; + } + + var rowIndex = 5; + foreach (var record in records) + { + var countryKey = ResolveFinanceCountryKey(record.Land, record.Tsc); + var financeDate = financeRuleEngine.ResolveFinanceDate(record, countryKey); + var rawInclude = financeRuleEngine.ShouldInclude(record, countryKey); + var netSalesActual = financeRuleEngine.ResolveNetSalesActual(record, countryKey, rawInclude); + var include = rawInclude && netSalesActual != 0m; + + if (!include) + continue; + + ws.Cell(rowIndex, 1).Value = financeDate.Year; + ws.Cell(rowIndex, 2).Value = countryKey; + ws.Cell(rowIndex, 3).Value = ResolveFinanceCurrency(record); + ws.Cell(rowIndex, 4).Value = financeDate.ToString("dd.MM.yyyy"); + ws.Cell(rowIndex, 5).Value = netSalesActual; + ws.Cell(rowIndex, 6).Value = "Sales Price/Value"; + ws.Cell(rowIndex, 7).Value = record.Tsc; + ws.Cell(rowIndex, 8).Value = record.Land; + ws.Cell(rowIndex, 9).Value = record.DocumentType; + ws.Cell(rowIndex, 10).Value = record.InvoiceNumber; + ws.Cell(rowIndex, 11).Value = record.PositionOnInvoice; + ws.Cell(rowIndex, 12).Value = record.DocumentEntry; + ws.Cell(rowIndex, 13).Value = record.Material; + ws.Cell(rowIndex, 14).Value = record.Name; + ws.Cell(rowIndex, 15).Value = record.Quantity; + ws.Cell(rowIndex, 16).Value = record.CustomerNumber; + ws.Cell(rowIndex, 17).Value = record.CustomerName; + ws.Cell(rowIndex, 18).Value = record.CustomerCountry; + ws.Cell(rowIndex, 19).Value = record.SupplierNumber; + ws.Cell(rowIndex, 20).Value = record.SupplierName; + ws.Cell(rowIndex, 21).Value = record.SupplierCountry; + ws.Cell(rowIndex, 22).Value = record.PostingDate?.ToString("dd.MM.yyyy") ?? string.Empty; + ws.Cell(rowIndex, 23).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty; + ws.Cell(rowIndex, 24).Value = record.SalesPriceValue; + ws.Cell(rowIndex, 25).Value = record.SalesCurrency; + ws.Cell(rowIndex, 26).Value = record.DocumentCurrency; + ws.Cell(rowIndex, 27).Value = record.DocumentTotalForeignCurrency; + ws.Cell(rowIndex, 28).Value = record.DocumentTotalLocalCurrency; + ws.Cell(rowIndex, 29).Value = record.CompanyCurrency; + rowIndex++; + } + + ws.Column(5).Style.NumberFormat.Format = "#,##0.00"; + ws.Column(24).Style.NumberFormat.Format = "#,##0.00"; + ws.Column(27).Style.NumberFormat.Format = "#,##0.00"; + ws.Column(28).Style.NumberFormat.Format = "#,##0.00"; + ws.Columns().AdjustToContents(); + } + private static string BuildFinanceSummaryHint(string countryKey) => countryKey.ToUpperInvariant() switch { @@ -290,6 +392,7 @@ public class ExcelExportService : IExcelExportService ("2. Land filtern", "Finance | Country Key = CH, AT, DE, ES, FR, IN, IT, UK oder US"), ("3. Gueltige Zeilen filtern", "Finance | Include = TRUE"), ("4. Summe bilden", "Finance | Net Sales Actual summieren"), + ("Detailblatt", "Finance Details enthaelt nur die Zeilen, die zur Summe im Blatt Finance Summary fuehren."), ("Waehrung", "Finance | Currency zeigt die fuer den Finance-Abgleich fuehrende Hauswaehrung."), ("Datum", "Finance | Date verwendet PostingDate, danach InvoiceDate, danach ExtractionDate. DE Alphaplan wird als Jahresfile 2025 behandelt."), ("Wertquelle", "Finance | Source Value Field zeigt, aus welchem Rohfeld der Finance-Wert kommt."), diff --git a/TrafagSalesExporter/Services/ISharePointUploadService.cs b/TrafagSalesExporter/Services/ISharePointUploadService.cs index 363f3c9..51b1c50 100644 --- a/TrafagSalesExporter/Services/ISharePointUploadService.cs +++ b/TrafagSalesExporter/Services/ISharePointUploadService.cs @@ -2,7 +2,7 @@ namespace TrafagSalesExporter.Services; public interface ISharePointUploadService { - Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath); + Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath, bool uploadTimestampedCopyIfLocked = false); Task DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference); Task ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null); Task> ResolveManualImportFilesInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null); diff --git a/TrafagSalesExporter/Services/SharePointUploadService.cs b/TrafagSalesExporter/Services/SharePointUploadService.cs index 8e4fa30..e4bb80d 100644 --- a/TrafagSalesExporter/Services/SharePointUploadService.cs +++ b/TrafagSalesExporter/Services/SharePointUploadService.cs @@ -10,7 +10,7 @@ namespace TrafagSalesExporter.Services; public class SharePointUploadService : ISharePointUploadService { public async Task UploadAsync(string tenantId, string clientId, string clientSecret, - string siteUrl, string exportFolder, string land, string localFilePath) + string siteUrl, string exportFolder, string land, string localFilePath, bool uploadTimestampedCopyIfLocked = false) { var normalizedTenantId = Normalize(tenantId); var normalizedClientId = Normalize(clientId); @@ -33,17 +33,16 @@ public class SharePointUploadService : ISharePointUploadService if (drive?.Id is null) throw new InvalidOperationException("SharePoint Dokumentenbibliothek konnte nicht gefunden werden."); - var fileName = Path.GetFileName(localFilePath); - var remotePath = string.Join("/", - new[] - { - normalizedExportFolder.Trim('/').Trim(), - normalizedLand.Trim('/').Trim(), - fileName - }.Where(segment => !string.IsNullOrWhiteSpace(segment))); - - await using var stream = File.OpenRead(localFilePath); - await graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.PutAsync(stream); + var remotePath = BuildUploadPath(normalizedExportFolder, normalizedLand, Path.GetFileName(localFilePath)); + try + { + await UploadWithLockRetryAsync(graphClient, drive.Id, remotePath, localFilePath); + } + catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (uploadTimestampedCopyIfLocked && IsLockedSharePointResource(ex)) + { + var timestampedPath = BuildUploadPath(normalizedExportFolder, normalizedLand, BuildTimestampedFileName(localFilePath)); + await UploadWithLockRetryAsync(graphClient, drive.Id, timestampedPath, localFilePath); + } } public async Task DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference) @@ -242,6 +241,45 @@ public class SharePointUploadService : ISharePointUploadService private static string Normalize(string value) => value?.Trim() ?? string.Empty; + private static async Task UploadWithLockRetryAsync(GraphServiceClient graphClient, string driveId, string remotePath, string localFilePath) + { + const int attempts = 4; + for (var attempt = 1; attempt <= attempts; attempt++) + { + try + { + await using var stream = File.OpenRead(localFilePath); + await graphClient.Drives[driveId].Root.ItemWithPath(remotePath).Content.PutAsync(stream); + return; + } + catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (attempt < attempts && IsLockedSharePointResource(ex)) + { + await Task.Delay(TimeSpan.FromSeconds(5 * attempt)); + } + } + } + + private static string BuildUploadPath(string exportFolder, string land, string fileName) + => string.Join("/", + new[] + { + exportFolder.Trim('/').Trim(), + land.Trim('/').Trim(), + fileName + }.Where(segment => !string.IsNullOrWhiteSpace(segment))); + + private static string BuildTimestampedFileName(string localFilePath) + { + var name = Path.GetFileNameWithoutExtension(localFilePath); + var extension = Path.GetExtension(localFilePath); + var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); + return $"{name}_{timestamp}{extension}"; + } + + private static bool IsLockedSharePointResource(Exception ex) + => ex.Message.Contains("locked", StringComparison.OrdinalIgnoreCase) || + ex.ToString().Contains("locked", StringComparison.OrdinalIgnoreCase); + private static string ResolveRemotePath(string fileReference, Uri siteUri) { if (Uri.TryCreate(fileReference, UriKind.Absolute, out var fileUri)) diff --git a/TrafagSalesExporter/Services/UiTextService.cs b/TrafagSalesExporter/Services/UiTextService.cs index 4615253..664821f 100644 --- a/TrafagSalesExporter/Services/UiTextService.cs +++ b/TrafagSalesExporter/Services/UiTextService.cs @@ -11,6 +11,622 @@ public interface IUiTextService public sealed class UiTextService : IUiTextService { private string _currentLanguage = "de"; + private static readonly HashSet SupportedLanguages = new(StringComparer.OrdinalIgnoreCase) + { + "de", + "en", + "es", + "it", + "hi" + }; + + private static readonly IReadOnlyDictionary> Translations = + new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["es"] = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Trafag Finance/Sales Management Cockpit"] = "Trafag Cockpit de finanzas y ventas", + ["Finance Cockpit"] = "Cockpit financiero", + ["Finance Cockpit ist geschuetzt. Bitte separat anmelden."] = "El cockpit financiero está protegido. Inicie sesión por separado.", + ["Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren."] = "El acceso al cockpit financiero aún no está configurado. Configure Username y PasswordHash en FinanceCockpitAccess.", + ["Export Dashboard"] = "Panel de exportación", + ["Management Analyse"] = "Análisis de gestión", + ["Soll/Ist Vergleich"] = "Comparación real/referencia", + ["Manuelle Importe"] = "Importaciones manuales", + ["Admin"] = "Administración", + ["Standorte"] = "Sitios", + ["Transformationen"] = "Transformaciones", + ["Finance Regeln"] = "Reglas financieras", + ["Settings"] = "Configuración", + ["Logs"] = "Registros", + ["Finance sperren"] = "Bloquear finanzas", + ["HR KPI (Login)"] = "KPI RR. HH. (login)", + ["HR KPI enthaelt sensible Personaldaten. Bitte separat anmelden."] = "HR KPI contiene datos personales sensibles. Inicie sesión por separado.", + ["HR-KPI-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in HrKpiAccess konfigurieren."] = "El acceso a HR KPI aún no está configurado. Configure Username y PasswordHash en HrKpiAccess.", + ["HR KPI entsperren"] = "Desbloquear HR KPI", + ["HR-KPI-Anmeldung fehlgeschlagen."] = "Error al iniciar sesión en HR KPI.", + ["Name"] = "Nombre", + ["Passwort"] = "Contraseña", + ["Finance Cockpit entsperren"] = "Desbloquear cockpit financiero", + ["Finance-Jahr"] = "Año financiero", + ["Finance Summary laden"] = "Cargar resumen financiero", + ["Finance Summary"] = "Resumen financiero", + ["Net Sales Actual"] = "Ventas netas reales", + ["gefiltertes Endergebnis"] = "resultado final filtrado", + ["Enthaltene Zeilen"] = "Filas incluidas", + ["Finance Include = TRUE"] = "Finance Include = TRUE", + ["Finance-Regeln"] = "Reglas financieras", + ["Laender / Waehrungen"] = "Países / monedas", + ["Summen wie im Excel-Blatt Finance Summary"] = "Totales como en la hoja Excel Finance Summary", + ["Keine Finance-Summary-Daten fuer diese Filter."] = "No hay datos de resumen financiero para estos filtros.", + ["Jahresvergleich mit aktuellem Filter"] = "Comparación anual con el filtro actual", + ["Rohdaten Diagnose"] = "Diagnóstico de datos brutos", + ["Diese Analyse ist eine Plausibilitaets- und Rohdatensicht. Fuer den verbindlichen Finance-Abgleich bitte `Soll/Ist Vergleich` oder im Endexcel die `Finance | ...`-Spalten verwenden."] = "Este análisis es una vista de plausibilidad y datos brutos. Para la reconciliación financiera vinculante, use `Comparación real/referencia` o las columnas `Finance | ...` en el Excel final.", + ["Diese Regeln wirken nur auf die Finance-Sicht im zentralen Excel und im Abgleich. Rohdaten und Spaltenmapping bleiben unveraendert."] = "Estas reglas solo afectan la vista financiera en el Excel consolidado y en la reconciliación. Los datos brutos y el mapeo de columnas no cambian.", + ["Finance-Cockpit-Anmeldung fehlgeschlagen."] = "Error al iniciar sesión en el cockpit financiero.", + ["Alle exportieren"] = "Exportar todo", + ["Zentrale Datei neu erzeugen"] = "Reconstruir archivo consolidado", + ["Aktualisieren"] = "Actualizar", + ["Lade..."] = "Cargando...", + ["Speichern"] = "Guardar", + ["Schliessen"] = "Cerrar", + ["Regel hinzufuegen"] = "Añadir regla", + ["Alle speichern"] = "Guardar todo", + ["Code anzeigen"] = "Mostrar código", + ["Land"] = "País", + ["Laender"] = "Países", + ["Aktiv"] = "Activo", + ["Aktionen"] = "Acciones", + ["Status"] = "Estado", + ["Basis"] = "Base", + ["Quelle"] = "Fuente", + ["Schema"] = "Esquema", + ["Server"] = "Servidor", + ["Zeilen"] = "Filas", + ["Rechnungen"] = "Facturas", + ["Waehrung"] = "Moneda", + ["Differenz"] = "Diferencia", + ["Berechnung"] = "Cálculo", + ["Varianten"] = "Variantes", + ["Referenz"] = "Referencia", + ["Ist 2025"] = "Real 2025", + ["Ampel"] = "Estado", + ["Ohne Ist"] = "Sin valores reales vacíos", + ["Varianten anzeigen"] = "mostrar variantes", + ["Abgrenzung"] = "Alcance", + ["Wert"] = "Valor", + ["Jahr"] = "Año", + ["Monat"] = "Mes", + ["Tag"] = "Día", + ["Hinweise"] = "Notas", + ["Enthalten"] = "Incluido", + ["Ausgeschlossen"] = "Excluido", + ["Gefiltert"] = "Filtrado", + ["Global"] = "Global", + ["Top Kunden"] = "Clientes principales", + ["Top Produktgruppen"] = "Grupos de productos principales", + ["Datenqualitaet"] = "Calidad de datos", + ["Management Aussagen"] = "Declaraciones de gestión", + ["Nicht umgerechnet"] = "No convertido", + ["Jahreswerte"] = "Valores anuales", + ["Monatswerte"] = "Valores mensuales", + ["Werte nach Quelle"] = "Valores por fuente", + ["Werte nach Land"] = "Valores por país", + ["Rohdaten Diagnose"] = "Diagnóstico de datos brutos", + ["Vorhandene Excel-Datei"] = "Archivo Excel disponible", + ["Summenfeld"] = "Campo de valor", + ["Anzeige-Waehrung"] = "Moneda de visualización", + ["Dateien laden"] = "Cargar archivos", + ["Cockpit erzeugen"] = "Crear cockpit", + ["Zentrale Auswertung laden"] = "Cargar análisis central", + ["Datei / SharePoint-Ordner"] = "Archivo / carpeta SharePoint", + ["Letzter Upload"] = "Última carga", + ["Pfad pruefen"] = "Comprobar ruta", + ["Importdateien"] = "Archivos de importación", + ["Anleitung"] = "Guía", + ["Excel bereitstellen"] = "Preparar Excel", + ["Speichern und aktivieren"] = "Guardar y activar", + ["Standort exportieren"] = "Exportar sitio", + ["Zentrale Excel erzeugen"] = "Crear Excel consolidado", + ["Finance pruefen"] = "Comprobar finanzas", + ["Datei hochladen oder SharePoint-/UNC-Pfad eintragen."] = "Cargue un archivo o introduzca una ruta de SharePoint/UNC.", + ["Pfad pruefen, Standort aktiv setzen und speichern."] = "Compruebe la ruta, active el sitio y guarde.", + ["Im Export Dashboard den Standort starten. Die Daten landen in CentralSalesRecords."] = "Inicie el sitio en el panel de exportación. Los datos se escriben en CentralSalesRecords.", + ["Danach `Zentrale Datei neu erzeugen` ausfuehren."] = "Después ejecute `Reconstruir archivo consolidado`.", + ["Im Endexcel `Finance | ...` oder im Reiter `Soll/Ist Vergleich` kontrollieren."] = "Compruebe las columnas `Finance | ...` en el Excel final o la pestaña `Comparación real/referencia`.", + ["Richtige Reihenfolge"] = "Orden correcto", + ["Ein Standortexport aktualisiert die Datenbasis. Die zentrale Excel muss danach neu erzeugt werden."] = "Una exportación de sitio actualiza la base de datos. Después debe reconstruirse el Excel consolidado.", + ["DE bleibt fachlich offen"] = "DE sigue pendiente a nivel funcional", + ["Alphaplan ist technisch importierbar. Kundenlaender und Filter fuer den offiziellen DE-Istwert muessen noch bestaetigt werden."] = "Alphaplan puede importarse técnicamente. Los países de cliente y filtros para el valor real oficial de DE aún deben confirmarse.", + ["Server-Hinweis"] = "Nota del servidor", + ["Der Server braucht kein Microsoft Excel. XLSX/CSV wird direkt von der Anwendung gelesen."] = "El servidor no necesita Microsoft Excel. La aplicación lee XLSX/CSV directamente.", + ["Datei hochgeladen."] = "Archivo cargado.", + ["Upload fehlgeschlagen"] = "Error de carga", + ["Speichern fehlgeschlagen"] = "Error al guardar", + ["Ueberblick"] = "Resumen", + ["Fluktuation"] = "Rotación", + ["Austritte"] = "Salidas", + ["Austritte nach Austrittsart"] = "Salidas por tipo", + ["Austritte nach Organisation"] = "Salidas por organización", + ["Absenzen"] = "Ausencias", + ["Absenzen nach Organisation"] = "Ausencias por organización", + ["Krankheitstage"] = "Días de enfermedad", + ["Absenzen je Mitarbeiter"] = "Ausencias por empleado", + ["Personalnr."] = "N.º personal", + ["Organisation"] = "Organización", + ["Kurz"] = "Corto", + ["Lang"] = "Largo", + ["Gesamt"] = "Total", + ["Quote"] = "Tasa", + ["Zeit / Ferien"] = "Tiempo / vacaciones", + ["Kritische Restferien"] = "Vacaciones restantes críticas", + ["Rest"] = "Restante", + ["Ausstehend"] = "Pendiente", + ["Mitarbeitende"] = "Empleados", + ["Datenstatus"] = "Estado de datos", + ["Headcount nach Organisation"] = "Plantilla por organización", + ["HR-Ampel"] = "Estado RR. HH.", + ["Schwere"] = "Gravedad", + ["Bereich"] = "Área", + ["Pruefpunkt"] = "Comprobación", + ["Anzahl"] = "Cantidad", + ["Hinweis"] = "Nota", + ["Gruppe"] = "Grupo", + ["Hoechste Absenzen"] = "Ausencias más altas", + ["Kritische GLZ-Saldi"] = "Saldos horarios críticos", + ["Saldo"] = "Saldo", + ["Austritt"] = "Salida", + ["Austrittsart"] = "Tipo de salida", + ["Ausschlussgruende"] = "Motivos de exclusión", + ["Grund"] = "Motivo", + ["Kostenstelle"] = "Centro de coste", + ["Alter"] = "Edad", + ["Dienstjahre"] = "Años de servicio", + ["Typ"] = "Tipo", + ["Dateistatus"] = "Estado de archivos", + ["Stand"] = "Modificado", + ["Rexx exportieren"] = "Exportar desde Rexx", + ["Die benoetigten Rexx-Abfragen manuell herunterladen. Excel/XLSX verwenden, nicht PDF."] = "Descargue manualmente las consultas Rexx necesarias. Use Excel/XLSX, no PDF.", + ["Dateien ablegen"] = "Guardar archivos", + ["Downloads in den Datenordner kopieren und exakt wie unten benennen."] = "Copie las descargas en la carpeta de datos y nómbrelas exactamente como se indica abajo.", + ["Cockpit laden"] = "Cargar cockpit", + ["Im HR-KPI-Cockpit den Datenordner kontrollieren und Laden klicken."] = "En el cockpit HR KPI, compruebe la carpeta de datos y haga clic en Cargar.", + ["Datenstatus pruefen"] = "Comprobar estado de datos", + ["Im Reiter Datenstatus muessen die erwarteten Dateien gruen erscheinen."] = "En la pestaña Estado de datos, los archivos esperados deben aparecer en verde.", + ["Datenordner"] = "Carpeta de datos", + ["Der Standardordner ist konfigurierbar. Fuer einen anderen Ordner oben im HR-KPI-Filter den Datenordner anpassen und neu laden."] = "La carpeta predeterminada es configurable. Para usar otra carpeta, cambie la carpeta de datos en el filtro HR KPI superior y vuelva a cargar.", + ["HR-Dateien enthalten Personendaten. Nicht per E-Mail weiterleiten und keine Kopien in ungeschuetzten Ordnern liegen lassen."] = "Los archivos de HR contienen datos personales. No los reenvíe por correo electrónico ni deje copias en carpetas no protegidas.", + ["Neue Auswertungen im Cockpit"] = "Nuevas vistas en el cockpit", + ["Managementsicht anonymisiert Personendaten fuer Fuehrungsberichte."] = "La vista de gestión anonimiza los datos personales para informes directivos.", + ["Dateistatus zeigt Pfad, Zeilen, Aenderungsdatum, Alter und Frische."] = "El estado de archivos muestra ruta, filas, fecha de modificación, antigüedad y frescura.", + ["HR-Ampel fasst Fluktuation, Krankheit, GLZ, Restferien und Datenqualitaet zusammen."] = "El estado HR resume rotación, enfermedad, saldo horario, vacaciones restantes y calidad de datos.", + ["GLZ- und Restferien-Ampeln koennen gefiltert werden."] = "Los estados de saldo horario y vacaciones restantes pueden filtrarse.", + ["Periodenvergleich zeigt die wichtigsten Vorjahreswerte, soweit Daten vorhanden sind."] = "La comparación de periodos muestra los valores clave del año anterior cuando hay datos disponibles.", + ["Datenqualitaet markiert fehlende Dateien, alte Dateien und auffaellige Werte."] = "La calidad de datos marca archivos faltantes, archivos antiguos y valores llamativos.", + ["Austritte werden nach Austrittsart und Organisation gruppiert."] = "Las salidas se agrupan por tipo de salida y organización.", + ["Absenzen werden nach Organisation ausgewertet."] = "Las ausencias se evalúan por organización.", + ["Top-Absenzen und kritische Detailtabellen helfen bei der operativen Pruefung."] = "Las ausencias principales y tablas críticas ayudan en la revisión operativa.", + ["Drucken/PDF erzeugt eine weitergebbare Ansicht aus dem Browser."] = "Imprimir/PDF crea una vista compartible desde el navegador.", + ["Erwartete Dateien"] = "Archivos esperados", + ["Inhalt"] = "Contenido", + ["Datei/Pfad"] = "Archivo/ruta", + ["gefunden"] = "encontrado", + ["fehlt"] = "falta", + ["Source Viewer"] = "Visor de código", + ["Zurueck zur Transformation"] = "Volver a transformaciones", + ["Datei:"] = "Archivo:", + ["Klasse:"] = "Clase:", + ["bei Zeile"] = "en línea", + ["Kein Dateipfad angegeben."] = "No se indicó ninguna ruta de archivo.", + ["Ungueltiger Dateipfad."] = "Ruta de archivo no válida.", + ["Transformer Ansicht"] = "Vista de transformaciones", + ["Transformationscode"] = "Código de transformación", + ["Keine Beschreibung."] = "Sin descripción.", + ["Optionales Argument."] = "Argumento opcional." + }, + ["it"] = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Trafag Finance/Sales Management Cockpit"] = "Cockpit Trafag finanza e vendite", + ["Finance Cockpit"] = "Cockpit finance", + ["Finance Cockpit ist geschuetzt. Bitte separat anmelden."] = "Il cockpit finance è protetto. Effettuare un accesso separato.", + ["Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren."] = "L'accesso al cockpit finance non è ancora configurato. Configurare Username e PasswordHash in FinanceCockpitAccess.", + ["Export Dashboard"] = "Dashboard esportazioni", + ["Management Analyse"] = "Analisi di gestione", + ["Soll/Ist Vergleich"] = "Confronto consuntivo/riferimento", + ["Manuelle Importe"] = "Import manuali", + ["Admin"] = "Amministrazione", + ["Standorte"] = "Sedi", + ["Transformationen"] = "Trasformazioni", + ["Finance Regeln"] = "Regole finance", + ["Settings"] = "Impostazioni", + ["Logs"] = "Log", + ["Finance sperren"] = "Blocca finance", + ["HR KPI (Login)"] = "KPI HR (login)", + ["HR KPI enthaelt sensible Personaldaten. Bitte separat anmelden."] = "HR KPI contiene dati personali sensibili. Effettuare un accesso separato.", + ["HR-KPI-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in HrKpiAccess konfigurieren."] = "L'accesso a HR KPI non è ancora configurato. Configurare Username e PasswordHash in HrKpiAccess.", + ["HR KPI entsperren"] = "Sblocca HR KPI", + ["HR-KPI-Anmeldung fehlgeschlagen."] = "Accesso a HR KPI non riuscito.", + ["Name"] = "Nome", + ["Passwort"] = "Password", + ["Finance Cockpit entsperren"] = "Sblocca cockpit finance", + ["Finance-Jahr"] = "Anno finance", + ["Finance Summary laden"] = "Carica riepilogo finance", + ["Finance Summary"] = "Riepilogo finance", + ["Net Sales Actual"] = "Vendite nette consuntive", + ["gefiltertes Endergebnis"] = "risultato finale filtrato", + ["Enthaltene Zeilen"] = "Righe incluse", + ["Finance Include = TRUE"] = "Finance Include = TRUE", + ["Finance-Regeln"] = "Regole finance", + ["Laender / Waehrungen"] = "Paesi / valute", + ["Summen wie im Excel-Blatt Finance Summary"] = "Totali come nel foglio Excel Finance Summary", + ["Keine Finance-Summary-Daten fuer diese Filter."] = "Nessun dato di riepilogo finance per questi filtri.", + ["Jahresvergleich mit aktuellem Filter"] = "Confronto annuale con filtro attuale", + ["Rohdaten Diagnose"] = "Diagnosi dati grezzi", + ["Diese Analyse ist eine Plausibilitaets- und Rohdatensicht. Fuer den verbindlichen Finance-Abgleich bitte `Soll/Ist Vergleich` oder im Endexcel die `Finance | ...`-Spalten verwenden."] = "Questa analisi è una vista di plausibilità e dati grezzi. Per la riconciliazione finance vincolante usare `Confronto consuntivo/riferimento` o le colonne `Finance | ...` nell'Excel finale.", + ["Diese Regeln wirken nur auf die Finance-Sicht im zentralen Excel und im Abgleich. Rohdaten und Spaltenmapping bleiben unveraendert."] = "Queste regole agiscono solo sulla vista finance nell'Excel consolidato e nella riconciliazione. Dati grezzi e mapping colonne restano invariati.", + ["Finance-Cockpit-Anmeldung fehlgeschlagen."] = "Accesso al cockpit finance non riuscito.", + ["Alle exportieren"] = "Esporta tutto", + ["Zentrale Datei neu erzeugen"] = "Ricrea file consolidato", + ["Aktualisieren"] = "Aggiorna", + ["Lade..."] = "Caricamento...", + ["Speichern"] = "Salva", + ["Schliessen"] = "Chiudi", + ["Regel hinzufuegen"] = "Aggiungi regola", + ["Alle speichern"] = "Salva tutto", + ["Code anzeigen"] = "Mostra codice", + ["Land"] = "Paese", + ["Laender"] = "Paesi", + ["Aktiv"] = "Attivo", + ["Aktionen"] = "Azioni", + ["Status"] = "Stato", + ["Basis"] = "Base", + ["Quelle"] = "Fonte", + ["Schema"] = "Schema", + ["Server"] = "Server", + ["Zeilen"] = "Righe", + ["Rechnungen"] = "Fatture", + ["Waehrung"] = "Valuta", + ["Differenz"] = "Differenza", + ["Berechnung"] = "Calcolo", + ["Varianten"] = "Varianti", + ["Referenz"] = "Riferimento", + ["Ist 2025"] = "Consuntivo 2025", + ["Ampel"] = "Stato", + ["Ohne Ist"] = "Senza consuntivi vuoti", + ["Varianten anzeigen"] = "mostra varianti", + ["Abgrenzung"] = "Ambito", + ["Wert"] = "Valore", + ["Jahr"] = "Anno", + ["Monat"] = "Mese", + ["Tag"] = "Giorno", + ["Hinweise"] = "Note", + ["Enthalten"] = "Incluso", + ["Ausgeschlossen"] = "Escluso", + ["Gefiltert"] = "Filtrato", + ["Global"] = "Globale", + ["Top Kunden"] = "Clienti principali", + ["Top Produktgruppen"] = "Gruppi prodotto principali", + ["Datenqualitaet"] = "Qualità dati", + ["Management Aussagen"] = "Indicazioni management", + ["Nicht umgerechnet"] = "Non convertito", + ["Jahreswerte"] = "Valori annuali", + ["Monatswerte"] = "Valori mensili", + ["Werte nach Quelle"] = "Valori per fonte", + ["Werte nach Land"] = "Valori per paese", + ["Rohdaten Diagnose"] = "Diagnosi dati grezzi", + ["Vorhandene Excel-Datei"] = "File Excel disponibile", + ["Summenfeld"] = "Campo valore", + ["Anzeige-Waehrung"] = "Valuta visualizzata", + ["Dateien laden"] = "Carica file", + ["Cockpit erzeugen"] = "Crea cockpit", + ["Zentrale Auswertung laden"] = "Carica analisi centrale", + ["Datei / SharePoint-Ordner"] = "File / cartella SharePoint", + ["Letzter Upload"] = "Ultimo upload", + ["Pfad pruefen"] = "Controlla percorso", + ["Importdateien"] = "File di importazione", + ["Anleitung"] = "Guida", + ["Excel bereitstellen"] = "Prepara Excel", + ["Speichern und aktivieren"] = "Salva e attiva", + ["Standort exportieren"] = "Esporta sede", + ["Zentrale Excel erzeugen"] = "Crea Excel consolidato", + ["Finance pruefen"] = "Controlla finance", + ["Datei hochladen oder SharePoint-/UNC-Pfad eintragen."] = "Caricare un file o inserire un percorso SharePoint/UNC.", + ["Pfad pruefen, Standort aktiv setzen und speichern."] = "Controllare il percorso, attivare la sede e salvare.", + ["Im Export Dashboard den Standort starten. Die Daten landen in CentralSalesRecords."] = "Avviare la sede nel dashboard esportazioni. I dati vengono scritti in CentralSalesRecords.", + ["Danach `Zentrale Datei neu erzeugen` ausfuehren."] = "Poi eseguire `Ricrea file consolidato`.", + ["Im Endexcel `Finance | ...` oder im Reiter `Soll/Ist Vergleich` kontrollieren."] = "Controllare le colonne `Finance | ...` nell'Excel finale o la scheda `Confronto consuntivo/riferimento`.", + ["Richtige Reihenfolge"] = "Sequenza corretta", + ["Ein Standortexport aktualisiert die Datenbasis. Die zentrale Excel muss danach neu erzeugt werden."] = "L'esportazione di una sede aggiorna la base dati. L'Excel consolidato deve poi essere ricreato.", + ["DE bleibt fachlich offen"] = "DE resta aperta sul piano funzionale", + ["Alphaplan ist technisch importierbar. Kundenlaender und Filter fuer den offiziellen DE-Istwert muessen noch bestaetigt werden."] = "Alphaplan è tecnicamente importabile. Paesi cliente e filtri per il valore consuntivo ufficiale DE devono ancora essere confermati.", + ["Server-Hinweis"] = "Nota server", + ["Der Server braucht kein Microsoft Excel. XLSX/CSV wird direkt von der Anwendung gelesen."] = "Il server non richiede Microsoft Excel. XLSX/CSV viene letto direttamente dall'applicazione.", + ["Datei hochgeladen."] = "File caricato.", + ["Upload fehlgeschlagen"] = "Upload non riuscito", + ["Speichern fehlgeschlagen"] = "Salvataggio non riuscito", + ["Ueberblick"] = "Panoramica", + ["Fluktuation"] = "Turnover", + ["Austritte"] = "Uscite", + ["Austritte nach Austrittsart"] = "Uscite per tipo", + ["Austritte nach Organisation"] = "Uscite per organizzazione", + ["Absenzen"] = "Assenze", + ["Absenzen nach Organisation"] = "Assenze per organizzazione", + ["Krankheitstage"] = "Giorni di malattia", + ["Absenzen je Mitarbeiter"] = "Assenze per collaboratore", + ["Personalnr."] = "N. personale", + ["Organisation"] = "Organizzazione", + ["Kurz"] = "Breve", + ["Lang"] = "Lungo", + ["Gesamt"] = "Totale", + ["Quote"] = "Tasso", + ["Zeit / Ferien"] = "Tempo / ferie", + ["Kritische Restferien"] = "Ferie residue critiche", + ["Rest"] = "Residuo", + ["Ausstehend"] = "Aperto", + ["Mitarbeitende"] = "Collaboratori", + ["Datenstatus"] = "Stato dati", + ["Headcount nach Organisation"] = "Organico per organizzazione", + ["HR-Ampel"] = "Stato HR", + ["Schwere"] = "Gravità", + ["Bereich"] = "Area", + ["Pruefpunkt"] = "Controllo", + ["Anzahl"] = "Numero", + ["Hinweis"] = "Nota", + ["Gruppe"] = "Gruppo", + ["Hoechste Absenzen"] = "Assenze più alte", + ["Kritische GLZ-Saldi"] = "Saldi orari critici", + ["Saldo"] = "Saldo", + ["Austritt"] = "Uscita", + ["Austrittsart"] = "Tipo di uscita", + ["Ausschlussgruende"] = "Motivi di esclusione", + ["Grund"] = "Motivo", + ["Kostenstelle"] = "Centro di costo", + ["Alter"] = "Età", + ["Dienstjahre"] = "Anni di servizio", + ["Typ"] = "Tipo", + ["Dateistatus"] = "Stato file", + ["Stand"] = "Modificato", + ["Rexx exportieren"] = "Esporta da Rexx", + ["Die benoetigten Rexx-Abfragen manuell herunterladen. Excel/XLSX verwenden, nicht PDF."] = "Scaricare manualmente le query Rexx necessarie. Usare Excel/XLSX, non PDF.", + ["Dateien ablegen"] = "Archivia file", + ["Downloads in den Datenordner kopieren und exakt wie unten benennen."] = "Copiare i download nella cartella dati e denominarli esattamente come indicato sotto.", + ["Cockpit laden"] = "Carica cockpit", + ["Im HR-KPI-Cockpit den Datenordner kontrollieren und Laden klicken."] = "Nel cockpit HR KPI controllare la cartella dati e fare clic su Carica.", + ["Datenstatus pruefen"] = "Controlla stato dati", + ["Im Reiter Datenstatus muessen die erwarteten Dateien gruen erscheinen."] = "Nella scheda Stato dati i file attesi devono apparire in verde.", + ["Datenordner"] = "Cartella dati", + ["Der Standardordner ist konfigurierbar. Fuer einen anderen Ordner oben im HR-KPI-Filter den Datenordner anpassen und neu laden."] = "La cartella predefinita è configurabile. Per usare un'altra cartella, modificare la cartella dati nel filtro HR KPI in alto e ricaricare.", + ["HR-Dateien enthalten Personendaten. Nicht per E-Mail weiterleiten und keine Kopien in ungeschuetzten Ordnern liegen lassen."] = "I file HR contengono dati personali. Non inoltrarli via e-mail e non lasciare copie in cartelle non protette.", + ["Neue Auswertungen im Cockpit"] = "Nuove viste nel cockpit", + ["Managementsicht anonymisiert Personendaten fuer Fuehrungsberichte."] = "La vista management anonimizza i dati personali per i report direzionali.", + ["Dateistatus zeigt Pfad, Zeilen, Aenderungsdatum, Alter und Frische."] = "Lo stato file mostra percorso, righe, data modifica, età e freschezza.", + ["HR-Ampel fasst Fluktuation, Krankheit, GLZ, Restferien und Datenqualitaet zusammen."] = "Lo stato HR riassume turnover, malattia, saldo orario, ferie residue e qualità dati.", + ["GLZ- und Restferien-Ampeln koennen gefiltert werden."] = "Gli stati di saldo orario e ferie residue possono essere filtrati.", + ["Periodenvergleich zeigt die wichtigsten Vorjahreswerte, soweit Daten vorhanden sind."] = "Il confronto periodi mostra i principali valori dell'anno precedente quando disponibili.", + ["Datenqualitaet markiert fehlende Dateien, alte Dateien und auffaellige Werte."] = "La qualità dati segnala file mancanti, file vecchi e valori anomali.", + ["Austritte werden nach Austrittsart und Organisation gruppiert."] = "Le uscite sono raggruppate per tipo di uscita e organizzazione.", + ["Absenzen werden nach Organisation ausgewertet."] = "Le assenze sono analizzate per organizzazione.", + ["Top-Absenzen und kritische Detailtabellen helfen bei der operativen Pruefung."] = "Le assenze principali e le tabelle critiche supportano il controllo operativo.", + ["Drucken/PDF erzeugt eine weitergebbare Ansicht aus dem Browser."] = "Stampa/PDF crea dal browser una vista condivisibile.", + ["Erwartete Dateien"] = "File attesi", + ["Inhalt"] = "Contenuto", + ["Datei/Pfad"] = "File/percorso", + ["gefunden"] = "trovato", + ["fehlt"] = "manca", + ["Source Viewer"] = "Visualizzatore sorgente", + ["Zurueck zur Transformation"] = "Torna alle trasformazioni", + ["Datei:"] = "File:", + ["Klasse:"] = "Classe:", + ["bei Zeile"] = "alla riga", + ["Kein Dateipfad angegeben."] = "Nessun percorso file indicato.", + ["Ungueltiger Dateipfad."] = "Percorso file non valido.", + ["Transformer Ansicht"] = "Vista trasformazioni", + ["Transformationscode"] = "Codice trasformazione", + ["Keine Beschreibung."] = "Nessuna descrizione.", + ["Optionales Argument."] = "Argomento opzionale." + }, + ["hi"] = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Trafag Finance/Sales Management Cockpit"] = "Trafag वित्त और बिक्री प्रबंधन कॉकपिट", + ["Finance Cockpit"] = "वित्त कॉकपिट", + ["Finance Cockpit ist geschuetzt. Bitte separat anmelden."] = "वित्त कॉकपिट सुरक्षित है. कृपया अलग से साइन इन करें.", + ["Finance-Cockpit-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in FinanceCockpitAccess konfigurieren."] = "वित्त कॉकपिट एक्सेस अभी कॉन्फ़िगर नहीं है. कृपया FinanceCockpitAccess में Username और PasswordHash सेट करें.", + ["Export Dashboard"] = "निर्यात डैशबोर्ड", + ["Management Analyse"] = "प्रबंधन विश्लेषण", + ["Soll/Ist Vergleich"] = "वास्तविक/संदर्भ तुलना", + ["Manuelle Importe"] = "मैनुअल आयात", + ["Admin"] = "प्रशासन", + ["Standorte"] = "साइटें", + ["Transformationen"] = "रूपांतरण", + ["Finance Regeln"] = "वित्त नियम", + ["Settings"] = "सेटिंग्स", + ["Logs"] = "लॉग", + ["Finance sperren"] = "वित्त लॉक करें", + ["HR KPI (Login)"] = "HR KPI (लॉगिन)", + ["HR KPI enthaelt sensible Personaldaten. Bitte separat anmelden."] = "HR KPI में संवेदनशील कर्मचारी डेटा है. कृपया अलग से साइन इन करें.", + ["HR-KPI-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in HrKpiAccess konfigurieren."] = "HR KPI एक्सेस अभी कॉन्फ़िगर नहीं है. कृपया HrKpiAccess में Username और PasswordHash सेट करें.", + ["HR KPI entsperren"] = "HR KPI अनलॉक करें", + ["HR-KPI-Anmeldung fehlgeschlagen."] = "HR KPI साइन-इन विफल.", + ["Name"] = "नाम", + ["Passwort"] = "पासवर्ड", + ["Finance Cockpit entsperren"] = "वित्त कॉकपिट अनलॉक करें", + ["Finance-Jahr"] = "वित्त वर्ष", + ["Finance Summary laden"] = "वित्त सारांश लोड करें", + ["Finance Summary"] = "वित्त सारांश", + ["Net Sales Actual"] = "वास्तविक शुद्ध बिक्री", + ["gefiltertes Endergebnis"] = "फ़िल्टर किया अंतिम परिणाम", + ["Enthaltene Zeilen"] = "शामिल पंक्तियां", + ["Finance Include = TRUE"] = "Finance Include = TRUE", + ["Finance-Regeln"] = "वित्त नियम", + ["Laender / Waehrungen"] = "देश / मुद्राएं", + ["Summen wie im Excel-Blatt Finance Summary"] = "Excel शीट Finance Summary जैसे कुल", + ["Keine Finance-Summary-Daten fuer diese Filter."] = "इन फ़िल्टरों के लिए कोई वित्त सारांश डेटा नहीं है.", + ["Jahresvergleich mit aktuellem Filter"] = "वर्तमान फ़िल्टर के साथ वार्षिक तुलना", + ["Rohdaten Diagnose"] = "कच्चे डेटा का निदान", + ["Diese Analyse ist eine Plausibilitaets- und Rohdatensicht. Fuer den verbindlichen Finance-Abgleich bitte `Soll/Ist Vergleich` oder im Endexcel die `Finance | ...`-Spalten verwenden."] = "यह विश्लेषण विश्वसनीयता और कच्चे डेटा की दृश्य है. बाध्यकारी वित्त मिलान के लिए `वास्तविक/संदर्भ तुलना` या अंतिम Excel में `Finance | ...` कॉलम उपयोग करें.", + ["Diese Regeln wirken nur auf die Finance-Sicht im zentralen Excel und im Abgleich. Rohdaten und Spaltenmapping bleiben unveraendert."] = "ये नियम केवल केंद्रीय Excel और मिलान की वित्त दृश्य पर लागू होते हैं. कच्चा डेटा और कॉलम मैपिंग अपरिवर्तित रहते हैं.", + ["Finance-Cockpit-Anmeldung fehlgeschlagen."] = "वित्त कॉकपिट साइन-इन विफल.", + ["Alle exportieren"] = "सभी निर्यात करें", + ["Zentrale Datei neu erzeugen"] = "केंद्रीय फ़ाइल फिर बनाएं", + ["Aktualisieren"] = "ताज़ा करें", + ["Lade..."] = "लोड हो रहा है...", + ["Speichern"] = "सहेजें", + ["Schliessen"] = "बंद करें", + ["Regel hinzufuegen"] = "नियम जोड़ें", + ["Alle speichern"] = "सभी सहेजें", + ["Code anzeigen"] = "कोड दिखाएं", + ["Land"] = "देश", + ["Laender"] = "देश", + ["Aktiv"] = "सक्रिय", + ["Aktionen"] = "कार्रवाई", + ["Status"] = "स्थिति", + ["Basis"] = "आधार", + ["Quelle"] = "स्रोत", + ["Schema"] = "स्कीमा", + ["Server"] = "सर्वर", + ["Zeilen"] = "पंक्तियां", + ["Rechnungen"] = "चालान", + ["Waehrung"] = "मुद्रा", + ["Differenz"] = "अंतर", + ["Berechnung"] = "गणना", + ["Varianten"] = "वेरिएंट", + ["Referenz"] = "संदर्भ", + ["Ist 2025"] = "वास्तविक 2025", + ["Ampel"] = "स्थिति", + ["Ohne Ist"] = "खाली वास्तविक मानों के बिना", + ["Varianten anzeigen"] = "वेरिएंट दिखाएं", + ["Abgrenzung"] = "सीमा", + ["Wert"] = "मूल्य", + ["Jahr"] = "वर्ष", + ["Monat"] = "महीना", + ["Tag"] = "दिन", + ["Hinweise"] = "नोट्स", + ["Enthalten"] = "शामिल", + ["Ausgeschlossen"] = "बहिष्कृत", + ["Gefiltert"] = "फ़िल्टर किया गया", + ["Global"] = "वैश्विक", + ["Top Kunden"] = "शीर्ष ग्राहक", + ["Top Produktgruppen"] = "शीर्ष उत्पाद समूह", + ["Datenqualitaet"] = "डेटा गुणवत्ता", + ["Management Aussagen"] = "प्रबंधन कथन", + ["Nicht umgerechnet"] = "परिवर्तित नहीं", + ["Jahreswerte"] = "वार्षिक मान", + ["Monatswerte"] = "मासिक मान", + ["Werte nach Quelle"] = "स्रोत के अनुसार मान", + ["Werte nach Land"] = "देश के अनुसार मान", + ["Rohdaten Diagnose"] = "कच्चे डेटा का निदान", + ["Vorhandene Excel-Datei"] = "उपलब्ध Excel फ़ाइल", + ["Summenfeld"] = "मान फ़ील्ड", + ["Anzeige-Waehrung"] = "प्रदर्शन मुद्रा", + ["Dateien laden"] = "फ़ाइलें लोड करें", + ["Cockpit erzeugen"] = "कॉकपिट बनाएं", + ["Zentrale Auswertung laden"] = "केंद्रीय विश्लेषण लोड करें", + ["Datei / SharePoint-Ordner"] = "फ़ाइल / SharePoint फ़ोल्डर", + ["Letzter Upload"] = "अंतिम अपलोड", + ["Pfad pruefen"] = "पथ जांचें", + ["Importdateien"] = "आयात फ़ाइलें", + ["Anleitung"] = "गाइड", + ["Excel bereitstellen"] = "Excel तैयार करें", + ["Speichern und aktivieren"] = "सहेजें और सक्रिय करें", + ["Standort exportieren"] = "साइट निर्यात करें", + ["Zentrale Excel erzeugen"] = "केंद्रीय Excel बनाएं", + ["Finance pruefen"] = "वित्त जांचें", + ["Datei hochladen oder SharePoint-/UNC-Pfad eintragen."] = "फ़ाइल अपलोड करें या SharePoint/UNC पथ दर्ज करें.", + ["Pfad pruefen, Standort aktiv setzen und speichern."] = "पथ जांचें, साइट सक्रिय करें और सहेजें.", + ["Im Export Dashboard den Standort starten. Die Daten landen in CentralSalesRecords."] = "निर्यात डैशबोर्ड में साइट शुरू करें. डेटा CentralSalesRecords में लिखा जाएगा.", + ["Danach `Zentrale Datei neu erzeugen` ausfuehren."] = "इसके बाद `केंद्रीय फ़ाइल फिर बनाएं` चलाएं.", + ["Im Endexcel `Finance | ...` oder im Reiter `Soll/Ist Vergleich` kontrollieren."] = "अंतिम Excel में `Finance | ...` कॉलम या `वास्तविक/संदर्भ तुलना` टैब जांचें.", + ["Richtige Reihenfolge"] = "सही क्रम", + ["Ein Standortexport aktualisiert die Datenbasis. Die zentrale Excel muss danach neu erzeugt werden."] = "साइट निर्यात डेटा आधार अपडेट करता है. इसके बाद केंद्रीय Excel फिर बनाना होगा.", + ["DE bleibt fachlich offen"] = "DE कार्यात्मक रूप से अभी खुला है", + ["Alphaplan ist technisch importierbar. Kundenlaender und Filter fuer den offiziellen DE-Istwert muessen noch bestaetigt werden."] = "Alphaplan तकनीकी रूप से आयात योग्य है. आधिकारिक DE वास्तविक मान के लिए ग्राहक देश और फ़िल्टर अभी पुष्टि करने हैं.", + ["Server-Hinweis"] = "सर्वर नोट", + ["Der Server braucht kein Microsoft Excel. XLSX/CSV wird direkt von der Anwendung gelesen."] = "सर्वर को Microsoft Excel की आवश्यकता नहीं है. एप्लिकेशन XLSX/CSV सीधे पढ़ता है.", + ["Datei hochgeladen."] = "फ़ाइल अपलोड हुई.", + ["Upload fehlgeschlagen"] = "अपलोड विफल", + ["Speichern fehlgeschlagen"] = "सहेजना विफल", + ["Ueberblick"] = "अवलोकन", + ["Fluktuation"] = "टर्नओवर", + ["Austritte"] = "निकास", + ["Austritte nach Austrittsart"] = "निकास प्रकार के अनुसार", + ["Austritte nach Organisation"] = "संगठन के अनुसार निकास", + ["Absenzen"] = "अनुपस्थिति", + ["Absenzen nach Organisation"] = "संगठन के अनुसार अनुपस्थिति", + ["Krankheitstage"] = "बीमारी के दिन", + ["Absenzen je Mitarbeiter"] = "कर्मचारी के अनुसार अनुपस्थिति", + ["Personalnr."] = "कर्मचारी संख्या", + ["Organisation"] = "संगठन", + ["Kurz"] = "छोटा", + ["Lang"] = "लंबा", + ["Gesamt"] = "कुल", + ["Quote"] = "दर", + ["Zeit / Ferien"] = "समय / छुट्टी", + ["Kritische Restferien"] = "महत्वपूर्ण बची छुट्टी", + ["Rest"] = "शेष", + ["Ausstehend"] = "बकाया", + ["Mitarbeitende"] = "कर्मचारी", + ["Datenstatus"] = "डेटा स्थिति", + ["Headcount nach Organisation"] = "संगठन के अनुसार हेडकाउंट", + ["HR-Ampel"] = "HR स्थिति", + ["Schwere"] = "गंभीरता", + ["Bereich"] = "क्षेत्र", + ["Pruefpunkt"] = "जांच बिंदु", + ["Anzahl"] = "संख्या", + ["Hinweis"] = "नोट", + ["Gruppe"] = "समूह", + ["Hoechste Absenzen"] = "सबसे अधिक अनुपस्थिति", + ["Kritische GLZ-Saldi"] = "महत्वपूर्ण समय शेष", + ["Saldo"] = "शेष", + ["Austritt"] = "निकास", + ["Austrittsart"] = "निकास प्रकार", + ["Ausschlussgruende"] = "बहिष्करण कारण", + ["Grund"] = "कारण", + ["Kostenstelle"] = "लागत केंद्र", + ["Alter"] = "आयु", + ["Dienstjahre"] = "सेवा वर्ष", + ["Typ"] = "प्रकार", + ["Dateistatus"] = "फ़ाइल स्थिति", + ["Stand"] = "संशोधित", + ["Rexx exportieren"] = "Rexx से निर्यात करें", + ["Die benoetigten Rexx-Abfragen manuell herunterladen. Excel/XLSX verwenden, nicht PDF."] = "ज़रूरी Rexx क्वेरी मैन्युअल रूप से डाउनलोड करें. Excel/XLSX इस्तेमाल करें, PDF नहीं.", + ["Dateien ablegen"] = "फ़ाइलें रखें", + ["Downloads in den Datenordner kopieren und exakt wie unten benennen."] = "डाउनलोड को डेटा फ़ोल्डर में कॉपी करें और नीचे दिए गए नामों से ही सहेजें.", + ["Cockpit laden"] = "कॉकपिट लोड करें", + ["Im HR-KPI-Cockpit den Datenordner kontrollieren und Laden klicken."] = "HR-KPI कॉकपिट में डेटा फ़ोल्डर जांचें और लोड पर क्लिक करें.", + ["Datenstatus pruefen"] = "डेटा स्थिति जांचें", + ["Im Reiter Datenstatus muessen die erwarteten Dateien gruen erscheinen."] = "डेटा स्थिति टैब में अपेक्षित फ़ाइलें हरी दिखनी चाहिए.", + ["Datenordner"] = "डेटा फ़ोल्डर", + ["Der Standardordner ist konfigurierbar. Fuer einen anderen Ordner oben im HR-KPI-Filter den Datenordner anpassen und neu laden."] = "डिफ़ॉल्ट फ़ोल्डर कॉन्फ़िगर किया जा सकता है. दूसरा फ़ोल्डर उपयोग करने के लिए ऊपर HR KPI फ़िल्टर में डेटा फ़ोल्डर बदलें और फिर लोड करें.", + ["HR-Dateien enthalten Personendaten. Nicht per E-Mail weiterleiten und keine Kopien in ungeschuetzten Ordnern liegen lassen."] = "HR फ़ाइलों में व्यक्तिगत डेटा होता है. इन्हें ई-मेल से आगे न भेजें और असुरक्षित फ़ोल्डरों में कॉपी न छोड़ें.", + ["Neue Auswertungen im Cockpit"] = "कॉकपिट में नई दृश्यावलियां", + ["Managementsicht anonymisiert Personendaten fuer Fuehrungsberichte."] = "प्रबंधन दृश्य रिपोर्टों के लिए व्यक्तिगत डेटा को अनाम करता है.", + ["Dateistatus zeigt Pfad, Zeilen, Aenderungsdatum, Alter und Frische."] = "फ़ाइल स्थिति पथ, पंक्तियां, संशोधन तिथि, आयु और ताजगी दिखाती है.", + ["HR-Ampel fasst Fluktuation, Krankheit, GLZ, Restferien und Datenqualitaet zusammen."] = "HR स्थिति टर्नओवर, बीमारी, समय शेष, छुट्टी शेष और डेटा गुणवत्ता को सारांशित करती है.", + ["GLZ- und Restferien-Ampeln koennen gefiltert werden."] = "समय शेष और छुट्टी शेष स्थितियों को फ़िल्टर किया जा सकता है.", + ["Periodenvergleich zeigt die wichtigsten Vorjahreswerte, soweit Daten vorhanden sind."] = "अवधि तुलना उपलब्ध डेटा के अनुसार पिछले वर्ष के मुख्य मान दिखाती है.", + ["Datenqualitaet markiert fehlende Dateien, alte Dateien und auffaellige Werte."] = "डेटा गुणवत्ता गुम फ़ाइलें, पुरानी फ़ाइलें और असामान्य मान चिह्नित करती है.", + ["Austritte werden nach Austrittsart und Organisation gruppiert."] = "निकासों को निकास प्रकार और संगठन के अनुसार समूहित किया जाता है.", + ["Absenzen werden nach Organisation ausgewertet."] = "अनुपस्थितियों का मूल्यांकन संगठन के अनुसार किया जाता है.", + ["Top-Absenzen und kritische Detailtabellen helfen bei der operativen Pruefung."] = "शीर्ष अनुपस्थितियां और महत्वपूर्ण विवरण तालिकाएं संचालन जांच में मदद करती हैं.", + ["Drucken/PDF erzeugt eine weitergebbare Ansicht aus dem Browser."] = "प्रिंट/PDF ब्राउज़र से साझा करने योग्य दृश्य बनाता है.", + ["Erwartete Dateien"] = "अपेक्षित फ़ाइलें", + ["Inhalt"] = "सामग्री", + ["Datei/Pfad"] = "फ़ाइल/पथ", + ["gefunden"] = "मिला", + ["fehlt"] = "गुम", + ["Source Viewer"] = "स्रोत दर्शक", + ["Zurueck zur Transformation"] = "रूपांतरण पर वापस", + ["Datei:"] = "फ़ाइल:", + ["Klasse:"] = "क्लास:", + ["bei Zeile"] = "पंक्ति पर", + ["Kein Dateipfad angegeben."] = "कोई फ़ाइल पथ नहीं दिया गया.", + ["Ungueltiger Dateipfad."] = "अमान्य फ़ाइल पथ.", + ["Transformer Ansicht"] = "रूपांतरण दृश्य", + ["Transformationscode"] = "रूपांतरण कोड", + ["Keine Beschreibung."] = "कोई विवरण नहीं.", + ["Optionales Argument."] = "वैकल्पिक तर्क." + } + }; public string CurrentLanguage => _currentLanguage; @@ -18,7 +634,7 @@ public sealed class UiTextService : IUiTextService public void SetLanguage(string language) { - var normalized = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) ? "en" : "de"; + var normalized = NormalizeLanguage(language); if (string.Equals(_currentLanguage, normalized, StringComparison.OrdinalIgnoreCase)) return; @@ -27,5 +643,23 @@ public sealed class UiTextService : IUiTextService } public string Text(string german, string english) - => string.Equals(_currentLanguage, "en", StringComparison.OrdinalIgnoreCase) ? english : german; + { + if (string.Equals(_currentLanguage, "de", StringComparison.OrdinalIgnoreCase)) + return german; + if (string.Equals(_currentLanguage, "en", StringComparison.OrdinalIgnoreCase)) + return english; + + return Translations.TryGetValue(_currentLanguage, out var languageTranslations) && + languageTranslations.TryGetValue(german, out var translated) + ? translated + : english; + } + + private static string NormalizeLanguage(string language) + { + var normalized = (language ?? string.Empty).Trim().ToLowerInvariant(); + if (normalized is "in" or "ind" or "india" or "hindi") + normalized = "hi"; + return SupportedLanguages.Contains(normalized) ? normalized : "de"; + } } diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/ExcelExportServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/ExcelExportServiceTests.cs index e8a767c..62c2657 100644 --- a/TrafagSalesExporter/TrafagSalesExporter.Tests/ExcelExportServiceTests.cs +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/ExcelExportServiceTests.cs @@ -44,6 +44,17 @@ public class ExcelExportServiceTests Assert.Equal(2, includedGermanyRows.Count); Assert.Equal(80m, includedGermanyRows.Sum(row => row.Cell(39).GetValue())); + + var details = workbook.Worksheet("Finance Details"); + var includedGermanyDetailRows = details.RowsUsed() + .Where(row => row.RowNumber() > 4) + .Where(row => row.Cell(1).GetValue() == 2025) + .Where(row => row.Cell(2).GetString() == "DE") + .ToList(); + + Assert.Equal(2, includedGermanyDetailRows.Count); + Assert.Equal(80m, includedGermanyDetailRows.Sum(row => row.Cell(5).GetValue())); + Assert.All(includedGermanyDetailRows, row => Assert.Equal("Sales Price/Value", row.Cell(6).GetString())); } finally { diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelDataSourceAdapterTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelDataSourceAdapterTests.cs index ebd4567..4a3fcdc 100644 --- a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelDataSourceAdapterTests.cs +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManualExcelDataSourceAdapterTests.cs @@ -129,7 +129,7 @@ public class ManualExcelDataSourceAdapterTests public string LastResolvedTsc { get; private set; } = string.Empty; - public Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath) + public Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath, bool uploadTimestampedCopyIfLocked = false) => Task.CompletedTask; public Task DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference) diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/UiTextServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/UiTextServiceTests.cs new file mode 100644 index 0000000..d3c011e --- /dev/null +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/UiTextServiceTests.cs @@ -0,0 +1,27 @@ +using TrafagSalesExporter.Services; + +namespace TrafagSalesExporter.Tests; + +public class UiTextServiceTests +{ + [Fact] + public void Text_Returns_Selected_Language_Or_English_Fallback() + { + var service = new UiTextService(); + + Assert.Equal("Standorte", service.Text("Standorte", "Sites")); + + service.SetLanguage("en"); + Assert.Equal("Sites", service.Text("Standorte", "Sites")); + + service.SetLanguage("es"); + Assert.Equal("Sitios", service.Text("Standorte", "Sites")); + + service.SetLanguage("it"); + Assert.Equal("Sedi", service.Text("Standorte", "Sites")); + + service.SetLanguage("hi"); + Assert.Equal("साइटें", service.Text("Standorte", "Sites")); + Assert.Equal("Untranslated English", service.Text("Nicht uebersetzt", "Untranslated English")); + } +} diff --git a/TrafagSalesExporter/wwwroot/css/app.css b/TrafagSalesExporter/wwwroot/css/app.css index e0dca7f..2bf79fd 100644 --- a/TrafagSalesExporter/wwwroot/css/app.css +++ b/TrafagSalesExporter/wwwroot/css/app.css @@ -12,3 +12,21 @@ html, body { width: auto; object-fit: contain; } + +.language-menu { + color: #fff; +} + +.language-button { + min-width: 84px; + height: 34px; + border-color: rgba(255, 255, 255, 0.65) !important; + color: #fff !important; + text-transform: none; + font-weight: 700; +} + +.language-button .mud-button-icon-start, +.language-button .mud-button-icon-end { + color: #fff; +}