fix(types): resolve the TypeScript build:check backlog

74 errors -> 1. No suppressions: zero `any`, `as unknown as`, `@ts-ignore` or
non-null `!` added, and tsconfig is untouched. Each error was triaged as
"the type is wrong" vs "the code is wrong" and fixed on that side.

Live bugs the checker was pointing at:

- admin.service.ts TS1117 duplicate key: admin_password_reset was defined
  twice and the later one won at runtime. Removed it so the earlier entry
  wins, which matches the actual emitter in userManagementService.js and
  carries the email fallback.
- PhotoGridWithLayouts dropped allowReactions from its prop type, so the
  Premium layout's reactions never activated even though GalleryView passes
  it and GalleryPremiumLayout reads it.
- SlideshowPage's poll never copied `order` into next/prev, so live
  play-order changes never reached a running kiosk.
- CustomerLayout compared branding_force_color_mode against 'auto', which is
  never persisted (only 'dark'|'light'|null), so the customer portal always
  picked the light logo even in OS dark mode.
- EmailConfigPage rendered lang.flag, but SUPPORTED_LANGUAGES exposes Flag, a
  component -- so nothing rendered. And editing a language with no translation
  yet spread undefined, storing a partial object missing required fields.
- publicQuotes.js projected only 6 line-item fields, omitting
  parentLineItemId/parentPosition/detailsText, so the migration-119 sub-item
  hierarchy and details text could never render on the customer-facing quote
  page -- the frontend code for it was unreachable. It reads from the same
  quoteService.getQuoteById the admin route uses, where those fields are
  present; adminQuotes.js projects all three. Fixed the projection rather
  than adding fields to the frontend type, which would have compiled while
  leaving the feature broken.
- DuplicateEventDialog's helper text was silently dropped: LocalizedDateInput
  had no helperText prop. Added, mirroring Input.tsx incl. aria-describedby.
- ThemeEditorModal/EventThemeSection still passed isPreviewMode, a prop
  822be9a9 deliberately removed but missed at these two call sites.
- GalleryPage's hero-photo injection was dead: /gallery/:slug/info does not
  return hero_photo_id (only /photos does) and GalleryView already does it
  correctly. Removed the dead block rather than adding a field the API
  never sends.

Stale types corrected against the backend route that produces each payload:
GalleryInfo (allow_downloads, allow_user_uploads), GalleryData.event
(download_zip_ready), UpdateEventData (client_access_enabled, client_password,
regenerate_client_token), InvoiceSummary (replacesInvoiceId), ExportOptions
(mark_source, plus a snake_case ExportFilter matching the actual wire format),
customer.service contracts, AdminUser timestamps widened to string|null,
formatMoney currency widened to match its own (currency || 'CHF') guard,
faceCropStyle dimensions widened to match its !photoWidth guard, DEFAULT_FLAGS
faces, logo_position 'sidepanel', and the hand-rolled t() props replaced with
i18next's TFunction in four files.

Unused symbols were checked before deletion; UpdateInstructionsDialog's
targetVersion prop was completed rather than deleted (declared and passed but
never rendered -- now the fallback before the query resolves).

Left unfixed, deliberately: GalleryStoryLayout's handleOpenFeedback (TS6133).
It is the only caller of setSelectedPhotoForFeedback and is itself never
called, so StoryFeedbackSheet can never open on the Story theme. Wiring it
needs a new affordance on StoryPhotoCard (no sibling layout exposes one to
copy) and deleting it would orphan the sheet -- a product decision, not a
type fix. Note PhotoLightbox on the same layout already handles feedback,
so the sheet may simply be superseded.

Refs testplan REPORT.md #22 (Part 1.3.04).
This commit is contained in:
Paul Nothaft
2026-09-01 17:09:24 +02:00
parent 5dbb43549c
commit 6e5755de02
45 changed files with 153 additions and 87 deletions
@@ -27,7 +27,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '../common';
import { customerAdminService } from '../../services/customerAdmin.service';
import { eventsService } from '../../services/events.service';
import type { Event as AdminEvent } from '../../services/events.service';
import type { Event as AdminEvent } from '../../types';
interface SelectedEvent {
id: number;
@@ -26,7 +26,11 @@ import { useMutationWithToast } from '../../hooks';
const ACCOUNT_TYPES: AccountType[] = ['asset', 'liability', 'equity', 'revenue', 'expense'];
const labelCls = 'block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1';
const selectCls = 'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm';
const SETTING_ACCOUNT_KEYS: (keyof LedgerSettings)[] = [
// Narrowed to the `ledger_account_*` keys so `patch[k] = settings[k]` below
// typechecks: they all share the value type `string | undefined`, whereas
// `keyof LedgerSettings` also spans the Record-valued VAT maps.
type LedgerAccountSettingKey = Extract<keyof LedgerSettings, `ledger_account_${string}`>;
const SETTING_ACCOUNT_KEYS: LedgerAccountSettingKey[] = [
'ledger_account_debitoren', 'ledger_account_kreditoren', 'ledger_account_bank', 'ledger_account_cash',
'ledger_account_default_revenue', 'ledger_account_default_expense',
'ledger_account_mileage', 'ledger_account_per_diem', 'ledger_account_rebilled_revenue',
@@ -14,7 +14,7 @@
import React from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Briefcase, UserCog, FileText, Receipt, Wrench, Calculator, Clock, ScrollText, Calendar, FolderKanban } from 'lucide-react';
import { Briefcase, UserCog, FileText, Receipt, Wrench, Clock, ScrollText, Calendar, FolderKanban } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
@@ -1,5 +1,5 @@
import React, { useState, useCallback } from 'react';
import { useEditor, EditorContent, type Editor } from '@tiptap/react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import HardBreak from '@tiptap/extension-hard-break';
@@ -10,7 +10,10 @@ interface GalleryPreviewBranding {
logo_url?: string;
logo_url_dark?: string;
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
logo_position?: 'left' | 'center' | 'right';
// Mirrors BrandingSettings.logo_position. 'sidepanel' has no distinct
// rendering in this small preview — it falls through to the left-aligned
// branch below.
logo_position?: 'left' | 'center' | 'right' | 'sidepanel';
}
interface GalleryPreviewProps {
@@ -6,11 +6,9 @@ import { useTranslation } from 'react-i18next';
import { Button, Input, Card } from '../common';
import { adminService } from '../../services/admin.service';
import { useAdminAuth } from '../../contexts';
export const MandatoryPasswordChangeModal: React.FC = () => {
const { t } = useTranslation();
const { updatePasswordChanged } = useAdminAuth();
const [formData, setFormData] = useState({
currentPassword: '',
newPassword: '',
@@ -145,7 +145,6 @@ export const ThemeEditorModal: React.FC<ThemeEditorModalProps> = ({
onChange={handleThemeChange}
presetName={presetName}
onPresetChange={handlePresetChange}
isPreviewMode={true}
showGalleryLayouts={true}
hideActions={true}
cssTemplates={cssTemplates}
@@ -139,9 +139,11 @@ export const UpdateInstructionsDialog: React.FC<UpdateInstructionsDialogProps> =
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('admin.updates.updateDialog.title', 'Update PicPeak')}
{data?.targetVersion && (
{/* The server response is authoritative; the prop covers the
window before the query resolves. */}
{(data?.targetVersion || targetVersion) && (
<span className="ml-2 text-blue-600 dark:text-blue-400">
v{data.targetVersion}
v{data?.targetVersion || targetVersion}
</span>
)}
</h3>