Files
Fuchs_Intranet/Fuchs/Views/Shared/_Layout.cshtml
T
StefanandClaude Sonnet 5 ecc74cf3c6
Playwright Tests / test (push) Canceled after 0s
Add user-defined invoice set structure dialog (ADR 0016)
Lets a back-office user correct/define which invoice line belongs to
which "set" and how deep it is nested, instead of relying only on the
implicit "everything after a Set header until the next Set" grouping
fds__prepInvoice derives. Sets may contain sets; every change applies
immediately to the cached draft session and is reflected in the editor
and PDF (ADR 0006/0008).

Backend:
- InvoiceSetStructure: pure parent-link forest (assign, change level,
  create, place/move-sibling, depth-first linearise + level recompute).
- InvoiceDraftSetStructureDeltas: new inv/dpatch targets set.assign,
  set.level, set.create, set.move.
- InvoiceSetPricing.Build is now order-preserving and nesting-aware
  (governed by the nearest converted ancestor); the set-item switch
  sums a header's transitive descendants so nested conversions never
  double-count.
- FuchsPdf indents nested set members per level, mirroring the editor.

Frontend:
- New fis.inv_sets.js: "Set-Struktur bearbeiten" dialog — per-block
  table of all lines with drag & drop reorder (before/after/into),
  ▲▼ order and ◀▶ level buttons, fold/unfold per set (view-only), and
  a create bar for new Set/Text lines. Posts one delta per action; the
  dialog is a pure view of the server state (ADR 0006).
- _Layout.cshtml / fis_main.js: cache-bust the dynamically imported
  per-module bundles (web/fis.<mdl>.<locale>.js/.css) with an asset
  version derived from the newest bundle write time, so a gulp rebuild
  is picked up without a stale cached module.

Docs: ADR 0016, live-draft-editing concept, INVOICE_SET_PRICING.md,
USER_GUIDE.md, ARCHITECTURE.md updated. Tests: InvoiceSetStructureTests
(new) + additions to InvoiceDraftServiceTests/InvoiceSetPricingTests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 08:55:05 +02:00

149 lines
5.9 KiB
Plaintext

@using System.Security.Claims
@using Microsoft.Data.SqlClient
@using Newtonsoft.Json
@inject IConfiguration Configuration
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment HostEnvironment
@{
bool isAuth = User.Identity?.IsAuthenticated ?? false;
var authinfo = new Dictionary<string, object>();
if (isAuth)
{
authinfo["useraccount_id"] = User.FindFirstValue("fuchs:useraccount_id") ?? "";
authinfo["email"] = User.FindFirstValue("fuchs:email") ?? "";
authinfo["authorization"] = User.FindFirstValue("fuchs:authorization") ?? "0";
}
authinfo["requestedaccount"] = "";
authinfo["accountrequired"] = false;
// Cache-buster for the per-module bundles ($ocms.getScript loads web/fis.<mdl>.<locale>.js and
// web/fis.<mdl>.css dynamically, so asp-append-version cannot cover them): the newest write
// time of any module bundle, appended as ?v=… by fis_main.js, so a gulp rebuild is picked up
// on the next page load instead of a stale cached module.
try
{
var webDir = new System.IO.DirectoryInfo(System.IO.Path.Combine(HostEnvironment.WebRootPath, "web"));
authinfo["assetversion"] = webDir.Exists
? webDir.GetFiles("fis.*").Select(f => f.LastWriteTimeUtc.Ticks).DefaultIfEmpty(0L).Max().ToString()
: "0";
}
catch { authinfo["assetversion"] = "0"; }
string appName = ViewData["AppName"] as string ?? "Fuchs Intranet";
string fullName = ViewData["FullName"] as string ?? "";
string pageTitle = ViewData["Title"] as string ?? "Intranet";
// When the deployment is flagged as a test/staging instance (Fuchs:IsTestDeployment = true),
// a distinctive green background is injected below so a test instance is never mistaken for
// production. Defaults to false (production) in appsettings.json.
bool isTestDeployment = Configuration.GetValue<bool>("Fuchs:IsTestDeployment");
string? debugDbTarget = null;
if (HostEnvironment.IsDevelopment())
{
var connectionString = Configuration.GetConnectionString("fuchs_fds_ConnectionString");
if (!string.IsNullOrWhiteSpace(connectionString))
{
var builder = new SqlConnectionStringBuilder(connectionString);
debugDbTarget = $"{builder.DataSource} / {builder.InitialCatalog}";
}
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@if (!string.IsNullOrWhiteSpace(debugDbTarget))
{
<meta name="fuchs-debug-database" content="@debugDbTarget" />
}
<title>@pageTitle</title>
<script src="~/web/tools.js" asp-append-version="true"></script>
@if (isAuth)
{
<script src="~/lib/tinymce/tinymce.min.js"></script>
<link rel="stylesheet" href="~/web/fis.min.css" asp-append-version="true" />
<script src="~/web/fis.min.js" asp-append-version="true"></script>
}
else
{
<link rel="stylesheet" href="~/web/fisb.min.css" asp-append-version="true" />
<script src="~/web/fisb.min.js" asp-append-version="true"></script>
}
@await RenderSectionAsync("CustomHeader", required: false)
@if (isTestDeployment)
{
<style>
body, html, body > header > nav, body > main > #topbar > nav {
background-color: #13a143 !important;
}
</style>
}
<script type="text/javascript">
$ocms.auth = @Html.Raw(JsonConvert.SerializeObject(authinfo));
</script>
</head>
<body class="ldng">
<div id="bgimg"></div>
@if (isAuth)
{
<div class="pgb inactive">
<div class="pgb-progress">
<div class="inner"></div>
</div>
</div>
<header>
<nav id="mainmenu">
<div class="nav-header">
<div id="logo">&nbsp;</div>
<button id="mmmb" class="nav-btn" data-toggle="vis" data-target="#mainmenu">
<span class="glyphicon glyphicon-menu-hamburger"></span>
</button>
<div id="brand" class="brand">@appName</div>
<div id="activemodule" class="activemodule">Übersicht</div>
</div>
<ul class="nav-right">
<li class="dropdown">
<a class="dds dropdown-toggle" role="button" aria-expanded="false">
<span class="glyphicon glyphicon-cog" aria-hidden="true"></span>
<span class="caret dd"></span>
</a>
<ul id="vm_menu_auth" class="dropdown-menu right" role="menu">
<li class="dropdown submenu">
<a class="dds dropdown-toggle" role="button" aria-expanded="false">
@fullName<span class="caret dd"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li><a onclick="$ocms.logout.call(this);">abmelden</a></li>
<li><a onclick="$fis.resetPass.call(this);">Passwort ändern</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
@await RenderSectionAsync("BodyHeader", required: false)
</header>
<main>
<div id="topbar"></div>
<div id="listframe"></div>
<div id="contentframe">
@RenderBody()
</div>
</main>
<footer>
<div id="notification_frame"></div>
@await RenderSectionAsync("BodyFooter", required: false)
</footer>
}
else
{
@await Html.PartialAsync("~/Views/Partials/vpart__ocms_login.cshtml")
@RenderBody()
}
<div class="timer"></div>
</body>
</html>