diff --git a/.github/workflows/debug-pre-release.yml b/.github/workflows/debug-pre-release.yml index 358d0b1c..1dec9b07 100644 --- a/.github/workflows/debug-pre-release.yml +++ b/.github/workflows/debug-pre-release.yml @@ -168,17 +168,27 @@ jobs: # No CodesignKey/EnableCodeSigning -> the build applies only an ad-hoc signature # (all an internal test build needs; testers still have to clear the Gatekeeper # quarantine flag — see the release notes text in publish-release). - # Passing BOTH RIDs in a single RuntimeIdentifier value is how the .NET MAUI Mac - # Catalyst targets emit a universal (lipo'd x64 + arm64) binary. Release builds do - # this by default; a Debug build has to state it explicitly. See - # https://learn.microsoft.com/dotnet/maui/mac-catalyst/deployment/publish-unsigned + # PublishMacUniversal=true flips a conditional PropertyGroup in StageFright.App.csproj + # that sets the *plural* maccatalyst-x64;maccatalyst-arm64, + # which is what makes MAUI's Mac Catalyst targets emit a universal (lipo'd x64 + arm64) + # .app. It has to be set inside the .csproj, not passed here as -p:RuntimeIdentifier(s): + # * the singular RuntimeIdentifier is validated by the .NET SDK as ONE RID and rejects + # a `;`-list with "NETSDK1083: ... 'maccatalyst-x64;maccatalyst-arm64' is not + # recognized"; + # * passing -p:RuntimeIdentifiers on the CLI makes it a global property that leaks the + # `;`-list verbatim into every ProjectReference (the plain non-maccatalyst class + # libraries), which then fail the same NETSDK1083. + # As a project property it is evaluated locally and MAUI fans out per-RID inner builds + # that hand a single concrete RID to the referenced libraries. Release builds get this + # universal RID set by MAUI automatically; a Debug build has to opt in. See the + # dotnet/macios .NET 8 release notes and dotnet/maui's Controls.TestCases.HostApp.csproj. - name: Publish unsigned universal .app run: > dotnet publish ${{ env.APP_PROJECT }} -f ${{ env.TFM }} -c Debug -p:CreatePackage=false - -p:RuntimeIdentifier="maccatalyst-x64;maccatalyst-arm64" + -p:PublishMacUniversal=true # `ditto` is Apple's supported way to archive an .app bundle for transport — it # preserves the symlinks, permissions, and resource forks that a plain `zip` diff --git a/.specify/feature.json b/.specify/feature.json index dc7b0b52..7d6ca32f 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1 @@ -{ - "feature_directory": "specs/026-past-agm-committee-report" -} +{"feature_directory":"specs\\029-first-run-language-seed"} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 64340824..4a3debb8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,13 +83,13 @@ During development the SQLite database is written to `FileSystem.AppDataDirector ### Navigation -Blazor Router owns **all** navigation. Every screen has a `@page` directive. `NavigationManager.NavigateTo` is the only way to transition between pages. MAUI Shell routing is disabled — MAUI is a platform-only container. First-run detection redirects to `/setup` before the dashboard loads. +Blazor Router owns **all** navigation. Every screen has a `@page` directive. `NavigationManager.NavigateTo` is the only way to transition between pages. MAUI Shell routing is disabled — MAUI is a platform-only container. First-run detection redirects to `/language-select` (spec 029's pre-wizard display-language screen) when setup is incomplete and no language preference has been recorded yet, otherwise straight to `/setup`, before the dashboard loads. ### Module structure inside `StageFright.Core` Application logic lives in `StageFright.Core/Modules//`. Each module slice contains its services, request/response models, and menu/tile providers. Repositories are *not* module-owned; they live centrally in `StageFright.Data/Repositories/` (this is a spec-mandated deviation from pure vertical-slice, required by FR-042). -Current modules: `Agm`, `AuditTrail`, `Dashboard`, `Events`, `Finance`, `Members`, `Rehearsals`, `Settings`. +Current modules: `Agm`, `AuditTrail`, `Dashboard`, `Events`, `Finance`, `Localization`, `Members`, `Rehearsals`, `Settings`. ### Extension points (plugin contracts) @@ -105,15 +105,15 @@ MVP providers register in `MauiProgram.RegisterCoreServices`. External plugins a ### Finance / GL integrity -Every fee or payment write wraps fee creation + paired GL debit/credit + balance assertion in one `DbContext` ACID transaction. A `GLBalanceException` is thrown and the transaction rolled back if the sum of debits ≠ sum of credits. GL is the authoritative source for member balances: `outstanding = Σ(debits) − Σ(credits)` per member. Financial records (`Fee`, `Payment`, `Transaction`) are **immutable and never deleted** — corrections use GL reversing pairs. +Every fee or payment write wraps fee creation + paired GL debit/credit + balance assertion in one `DbContext` ACID transaction. A `GLBalanceException` is thrown and the transaction rolled back if the sum of debits ≠ sum of credits. GL is the authoritative source for member balances: `outstanding = Σ(debits) − Σ(credits)` per member. Financial records (`Fee`, `Payment`, `Transaction`) are **immutable and never deleted** — corrections use GL reversing pairs. The two seeded sales-tax clearing accounts are classified per their economic nature: `2310` "Tax Collected" (owed to the authority) is an `AccountType.Liability`; since spec 028 #355, `2320` "Tax Receivable" (input tax recoverable from the authority) is an `AccountType.Asset` — it keeps its 2000s number as a documented exception (renumbering would desync the denormalised `Transaction.GLAccount` snapshot on historical rows), and `SystemAccounts.TaxPaid*` keeps its C# name. `BalanceSheetReportProvider` / `TrialBalanceReportProvider` section by `AccountType` (so recoverable tax shows under Assets with no provider change); `TaxSummaryReportProvider`'s net calc reads directional GL movements, not classification, so its sign convention is unaffected. ### Reports pipeline -`IReportProvider` → `ReportData` (rows/columns/sections/subtotals) → `ReportViewer.razor` (modal "Generating…", synchronous) → `PdfReportRenderer` (QuestPDF) or `CsvReportExporter` (CsvHelper). Cancel appears after 5 s. All ten reports (`IncomeStatement`, `TrialBalance`, `AccountRegister`, `MemberAccountSummary`, `MemberList`, `Committee`, `BalanceSheet`, `BankReconciliation`, `TaxSummary`, `GeneralLedger`) follow this single pipeline. In QuestPDF-rendered checkbox-style cells (e.g. `AttendanceRollPdfRenderer`, `EventAttendanceSheetPdfRenderer`, `AgmAttendanceSheetPdfRenderer`), a checked box is a bordered `Container` with a centered "✓" glyph, never a solid filled box. `EventAttendanceSheetPdfRenderer` and `AgmAttendanceSheetPdfRenderer` are read-only, print-only sheets (spec 018) built outside the `IReportProvider` pipeline — like `AttendanceRollPdfRenderer`, their multi-column checkbox layout doesn't fit `ReportData`'s flat single-table model — and share their two-column page-composition mechanics via the internal `CheckboxSheetPdfBuilder` helper in `StageFright.Reports/Rendering/`. `AgmResultsPdfRenderer` (spec 026) is a further AGM-detail-scoped, print-only sheet built outside the `IReportProvider` pipeline — a plain position list rather than a checkbox roll, so it builds its QuestPDF document directly (following `PdfReportRenderer`'s page setup) instead of using `CheckboxSheetPdfBuilder`. +`IReportProvider` → `ReportData` (rows/columns/sections/subtotals) → `ReportViewer.razor` (modal "Generating…", synchronous) → `PdfReportRenderer` (QuestPDF) or `CsvReportExporter` (CsvHelper). Cancel appears after 5 s. All ten reports (`IncomeStatement`, `TrialBalance`, `AccountRegister`, `MemberAccountSummary`, `MemberList`, `Committee`, `BalanceSheet`, `BankReconciliation`, `TaxSummary`, `GeneralLedger`) follow this single pipeline. In QuestPDF-rendered checkbox-style cells (e.g. `AttendanceRollPdfRenderer`, `EventAttendanceSheetPdfRenderer`, `AgmAttendanceSheetPdfRenderer`), a checked box is a bordered `Container` with a centered "✓" glyph, never a solid filled box. `EventAttendanceSheetPdfRenderer` and `AgmAttendanceSheetPdfRenderer` are read-only, print-only sheets (spec 018) built outside the `IReportProvider` pipeline — like `AttendanceRollPdfRenderer`, their multi-column checkbox layout doesn't fit `ReportData`'s flat single-table model — and share their two-column page-composition mechanics via the internal `CheckboxSheetPdfBuilder` helper in `StageFright.Reports/Rendering/`. `AgmResultsPdfRenderer` (spec 026) is a further AGM-detail-scoped, print-only sheet built outside the `IReportProvider` pipeline — a plain position list rather than a checkbox roll, so it builds its QuestPDF document directly (following `PdfReportRenderer`'s page setup) instead of using `CheckboxSheetPdfBuilder`. Every money value a provider, renderer or exporter emits goes through `MoneyFormatter.Format` / `FormatWithCode` (never `ToString("C")` / `"{0:C}"` / `ToString("F2")` / `"N2"`), so the figure carries the organisation's configured currency symbol, ISO code and minor-unit precision — see Localization; the `Reports_Common_BasisOfAccounting` line and the Balance Sheet / Trial Balance integrity checks (spec 028) also live in the providers, and `tests/StageFright.Localization.Tests/CurrencySymbolGuardTests.cs` guards the no-hard-coded-`$`/`AUD` rule. ### Data grid standards -All tabular data uses `RadzenDataGrid`, never plain `` markup or a `table-responsive` wrapper div. Every grid instance follows the Members grid (`src/StageFright.UI/Pages/Members/MemberList.razor`) as the reference: `AllowSorting="true" AllowPaging="true" PageSize="15" class="rz-shadow-0"`. Grids needing a "select all" checkbox in a column header use a `HeaderTemplate` rather than a separate control outside the grid. `ReportViewer.razor` is the one exception — its dynamic columns, section headers, and subtotal/grand-total rows don't fit RadzenDataGrid's typed-column model, so it keeps hand-rolled paging (also fixed at a page size of 15) instead. A handful of grids use a smaller `PageSize` than 15 when the surrounding layout is space-constrained — `CommitteeSettingsTab`, one grid in `EventTypesTab`, and `MemberDetail.razor`'s Fee Payment History grid (spec 025, issue #305) — this is a deliberate, per-screen exception, not a new default. +All tabular data uses `RadzenDataGrid`, never plain `
` markup or a `table-responsive` wrapper div. Every grid instance follows the Members grid (`src/StageFright.UI/Pages/Members/MemberList.razor`) as the reference: `AllowSorting="true" AllowPaging="true" PageSize="15" class="rz-shadow-0"`. Grids needing a "select all" checkbox in a column header use a `HeaderTemplate` rather than a separate control outside the grid. `ReportViewer.razor` is the one exception — its dynamic columns, section headers, and subtotal/grand-total rows don't fit RadzenDataGrid's typed-column model, so it keeps hand-rolled paging (also fixed at a page size of 15) instead. A handful of grids use a smaller `PageSize` than 15 when the surrounding layout is space-constrained — `CommitteeSettingsTab`, one grid in `EventTypesTab`, and `MemberDetail.razor`'s Fee Payment History grid (spec 025, issue #305) — this is a deliberate, per-screen exception, not a new default. Money columns use a `