Move fis widgets SQL into scalar functions
Playwright Tests / test (push) Waiting to run

Add fis_wdg_* table functions for dashboard widgets and wire them into
FuchsDatabase.sqlproj; update the invoice list procs accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Stefan
2026-10-16 11:35:28 +02:00
co-authored by Copilot
parent a39ccb8bbc
commit 330c33e65d
20 changed files with 307 additions and 13 deletions
@@ -0,0 +1,11 @@
-- =============================================
-- Description: Dashboard widget 'wdg_balance_net_thismonth' (Umsatz diesen Monat).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_balance_net_thismonth](@authuser)
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_balance_net_thismonth](@authuser varchar(100))
RETURNS TABLE
AS
RETURN
(
SELECT [label] = FORMAT(GETDATE(), 'MMMM yy', 'de'), [value] = [dbo].[fds__r_getBalanceThisMonth](@authuser)
);
@@ -0,0 +1,16 @@
-- =============================================
-- Description: Dashboard widget 'wdg_bankingupload' (Bank-Buchungen).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_bankingupload]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_bankingupload]()
RETURNS TABLE
AS
RETURN
(
SELECT TOP (1) [label] = N'Letzte Übertragung der Bankdaten am ' + FORMAT(CAST([value] as date), 'dd.MM.', 'de')
, [value] = FORMAT(DATEDIFF(DAY, CAST([value] as date), CAST(GETDATE() as date)), '0', 'de') + ' d'
, [status] = IIF(DATEDIFF(DAY, CAST([value] as date), CAST(GETDATE() as date)) > 6, 'neg', 'pos')
FROM [dbo].[fds__admin_settings]
WHERE [type] = 'log' AND [key] = 'bankingtransaction_merge'
ORDER BY TRY_CAST([value] as date) DESC
);
@@ -0,0 +1,14 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_overdue' (Unbezahlte Rechungen Überfällig).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_overdue]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_overdue]()
RETURNS TABLE
AS
RETURN
(
SELECT [label] = N'überfällige Rechnungen (10wd)', [value] = COUNT(*)
FROM [dbo].[fds__fn_unpaidInvoices]()
WHERE [dbo].[fds__fn_invoiceDate]([DateFinalized], NULL) >= DATEFROMPARTS(2026, 1, 1)
AND [dbo].[date_addcustom]([dbo].[fds__fn_invoiceDate]([DateFinalized], NULL), '10wd') < CAST(GETDATE() as date)
);
@@ -0,0 +1,15 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_overdue_sum' (Summe überfällige Rechnungen).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_overdue_sum]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_overdue_sum]()
RETURNS TABLE
AS
RETURN
(
SELECT [label] = N'Summe überfällige Rechnungen (10wd, Teilzahlungen ab ' + FORMAT((SELECT MIN([valuedate]) FROM [dbo].[fds__bankingtransactions]), 'dd.MM.yy', 'de') + ' berücksichtigt) '
, [value] = FORMAT(0.001 * SUM([OpenAmount]), '#0', 'de') + N'k€'
FROM [dbo].[fds__fn_unpaidInvoices]()
WHERE [dbo].[fds__fn_invoiceDate]([DateFinalized], NULL) >= DATEFROMPARTS(2026, 1, 1)
AND [dbo].[date_addcustom]([dbo].[fds__fn_invoiceDate]([DateFinalized], NULL), '10wd') < CAST(GETDATE() as date)
);
@@ -0,0 +1,18 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_reminded_fds' (Überfällige, angemahnte Rechnungen (Intranet)).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_reminded_fds]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_reminded_fds]()
RETURNS TABLE
AS
RETURN
(
WITH rem AS (SELECT [invid], [rem#] = COUNT(*) FROM [dbo].[fds__reminder] AS r GROUP BY [invid])
SELECT [label] = N'angemahnte, überfällige Rechnungen (10wd, nur Intranet)', [value] = COUNT(*)
FROM [dbo].[fds__fn_unpaidInvoices]() AS i
LEFT JOIN rem ON i.[id] = rem.[InvId]
WHERE i.[fds] = 1
AND [dbo].[fds__fn_invoiceDate](i.[DateFinalized], NULL) >= DATEFROMPARTS(2026, 1, 1)
AND [dbo].[date_addcustom]([dbo].[fds__fn_invoiceDate](i.[DateFinalized], NULL), '10wd') < CAST(GETDATE() as date)
AND ISNULL(rem.[rem#], 0) > 0
);
@@ -0,0 +1,12 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_today' (Rechnungen Heute).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_today]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_today]()
RETURNS TABLE
AS
RETURN
(
SELECT [label] = N'Rechnungen heute erstellt'
, [value] = FORMAT((SELECT COUNT(*) FROM [dbo].[fds__invoices] WHERE CAST([DateFinalized] as date) = CAST(GETDATE() as date)), '#0')
);
@@ -0,0 +1,12 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_unpaid' (Unbezahlte Rechnungen).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_unpaid]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_unpaid]()
RETURNS TABLE
AS
RETURN
(
SELECT [label] = N'unbez. Rechnungen', [value] = COUNT(*)
FROM [dbo].[fds__fn_unpaidInvoices]()
);
@@ -0,0 +1,18 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_unreminded_fds' (Überfällige, ungemahnte Rechnungen (Intranet)).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_unreminded_fds]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_unreminded_fds]()
RETURNS TABLE
AS
RETURN
(
WITH rem AS (SELECT [invid], [rem#] = COUNT(*) FROM [dbo].[fds__reminder] AS r GROUP BY [invid])
SELECT [label] = N'UNgemahnte, überfällige Rechnungen (10wd, nur Intranet)', [value] = COUNT(*)
FROM [dbo].[fds__fn_unpaidInvoices]() AS i
LEFT JOIN rem ON i.[id] = rem.[InvId]
WHERE i.[fds] = 1
AND [dbo].[fds__fn_invoiceDate](i.[DateFinalized], NULL) >= DATEFROMPARTS(2026, 1, 1)
AND [dbo].[date_addcustom]([dbo].[fds__fn_invoiceDate](i.[DateFinalized], NULL), '10wd') < CAST(GETDATE() as date)
AND ISNULL(rem.[rem#], 0) = 0
);
@@ -0,0 +1,12 @@
-- =============================================
-- Description: Dashboard widget 'wdg_invoices_yesterday' (Rechnung des letzten Werktags).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_invoices_yesterday]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_invoices_yesterday]()
RETURNS TABLE
AS
RETURN
(
SELECT [label] = N'Rechnungen erstellt am ' + FORMAT(CAST([dbo].[date_add](GETDATE(), '-1wd', 0) as date), 'dd.MM.yy', 'de')
, [value] = FORMAT((SELECT COUNT(*) FROM [dbo].[fds__invoices] WHERE CAST([DateFinalized] as date) = CAST([dbo].[date_add](GETDATE(), '-1wd', 0) as date)), '#0')
);
@@ -0,0 +1,18 @@
-- =============================================
-- Description: Dashboard widget 'wdg_reminder_open10wd' (Offene Mahnungen, älter 10wd).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_reminder_open10wd]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_reminder_open10wd]()
RETURNS TABLE
AS
RETURN
(
WITH rem AS (SELECT [invid], [rem#] = COUNT(*), [last] = MAX([DateFinalized]) FROM [dbo].[fds__reminder] AS r GROUP BY [invid])
SELECT [label] = N'ALTE offene Mahnungen (10wd, nur Intranet)', [value] = COUNT(*)
FROM [dbo].[fds__fn_unpaidInvoices]() AS i
LEFT JOIN rem ON i.[id] = rem.[InvId]
WHERE i.[fds] = 1
AND [dbo].[fds__fn_invoiceDate](i.[DateFinalized], NULL) >= DATEFROMPARTS(2026, 1, 1)
AND [dbo].[date_addcustom]([dbo].[fds__fn_invoiceDate](rem.[last], NULL), '10wd') < CAST(GETDATE() as date)
AND ISNULL(rem.[rem#], 0) > 0
);
@@ -0,0 +1,20 @@
-- =============================================
-- Description: Dashboard widget 'wdg_requests_today' (Aufträge terminiert für heute).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_requests_today]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_requests_today]()
RETURNS TABLE
AS
RETURN
(
WITH osrq AS (
SELECT s.[id], s.[name], s.[externalid], s.[customerId]
, [last_appointment] = MAX(CAST(a.[startdatetime] as date)), [#a] = COUNT(*)
FROM [dbo].[mfr__servicerequests] AS s
JOIN [dbo].[mfr__appointments] AS a ON s.[id] = a.[ServiceRequestId]
WHERE s.[state] IN ('Scheduled', 'Released', 'InProgress')
GROUP BY s.[id], s.[name], s.[externalid], s.[customerId])
SELECT [Nummer] = [externalid], [Auftrag] = [name], [Kunde] = [dbo].[fds__getCompanyName]([customerId])
FROM osrq
WHERE [last_appointment] = CAST(GETDATE() as date)
);
@@ -0,0 +1,18 @@
-- =============================================
-- Description: Dashboard widget 'wdg_requestsforinvoice' (Aufträge zur Rechnungsstellung).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_requestsforinvoice]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_requestsforinvoice]()
RETURNS TABLE
AS
RETURN
(
SELECT rfi.[Id]
, [Nummer] = rfi.[ExternalId]
, [Auftrag] = rfi.[Name]
, [Arbeit erledigt am] = FORMAT(rfi.[workdoneat], 'dd.MM.', 'de')
, [Freigegeben am] = FORMAT(rfi.[ReleasedAt], 'dd.MM.', 'de')
, [Kunde] = c.[name]
FROM [dbo].[fds__fn_requestsforinvoice]() AS rfi
LEFT JOIN [dbo].[mfr__companies] AS c ON c.[id] = rfi.[CustomerId]
);
@@ -0,0 +1,20 @@
-- =============================================
-- Description: Dashboard widget 'wdg_unbez_akonto' (Unbezahlte Akontos älter als 5 Arbeitstage (ohne MFR !)).
-- Called from [dbo].[fis_widgets].[sql] as: SELECT * FROM [dbo].[fis_wdg_unbez_akonto]()
-- =============================================
CREATE FUNCTION [dbo].[fis_wdg_unbez_akonto]()
RETURNS TABLE
AS
RETURN
(
SELECT [Rechnung] = i.[invoiceId]
, [Name] = i.[InvoiceTitle] + CHAR(10) + ' (' + (SELECT TOP (1) c.[name] FROM [dbo].[mfr__companies] AS c WHERE i.[customerid] = c.[id]) + ')'
, [Betrag (net)] = FORMAT(i.[invoicebalance], N'#0.00€', 'de')
, [Datum] = FORMAT(i.[DateFinalized], 'dd.MM.yy', 'de')
, [Zahlungen] = FORMAT(ISNULL(pay.[Amount], 0.0), N'#0.00€', 'de')
, [order] = ROW_NUMBER() OVER (ORDER BY i.[DateFinalized], i.[DateSent])
FROM [dbo].[fds__invoices] AS i
LEFT JOIN [dbo].[fds__fn_InvoicePaymentAmounts]() AS pay ON pay.[InvId] = i.[id]
WHERE i.[InvoiceType] = 'i' AND i.[IsPayed] = 0 AND i.[iscanceled] = 0
AND i.[DateFinalized] < [dbo].[date_add](GETDATE(), '-5wd', 0)
);
@@ -1,12 +1,25 @@
CREATE FUNCTION [dbo].[strings_encodeHTML](@inputText NVARCHAR(MAX))
CREATE FUNCTION [dbo].[strings_encodeHTML](@inputText NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
AS
BEGIN
DECLARE @encodedText NVARCHAR(MAX);
DECLARE @encodedText NVARCHAR(MAX) = N'';
-- Use FOR XML PATH to escape special characters
SET @encodedText = CAST((SELECT @inputText AS [p] FOR XML PATH(''), TYPE) as nvarchar(max));
IF DATALENGTH(@inputText) > 0
BEGIN
/* 1) Escape reserved HTML characters (& < >); FOR XML also entitizes CR as &#x0D; */
SET @encodedText = CAST((SELECT @inputText AS [p] FOR XML PATH(''), TYPE) AS NVARCHAR(MAX));
RETURN IIF(LEN(@encodedText) < 7, '', dbo.strings_replaceSpecialCharsInHTML(SUBSTRING(@encodedText, 4, LEN(@encodedText) -7)));
END;
/* 2) Remove the leading <p> and trailing </p> that FOR XML adds */
SET @encodedText = SUBSTRING(@encodedText, 4, LEN(@encodedText) - 7);
/* 3) Line breaks (CRLF, LF, lone CR) -> <br>; NBSP -> &nbsp; */
SET @encodedText = REPLACE(@encodedText, N'&#x0D;' + NCHAR(10), N'<br>');
SET @encodedText = REPLACE(@encodedText, NCHAR(13) + NCHAR(10), N'<br>');
SET @encodedText = REPLACE(@encodedText, NCHAR(10), N'<br>');
SET @encodedText = REPLACE(@encodedText, N'&#x0D;', N'<br>');
SET @encodedText = REPLACE(@encodedText, NCHAR(13), N'<br>');
SET @encodedText = REPLACE(@encodedText, NCHAR(160), N'&nbsp;');
END
RETURN @encodedText;
END;