-
setIssueDate(e.target.value)} />
-
setDueDate(e.target.value)} />
+
+
setScheduledSendAt(e.target.value)} />
diff --git a/frontend/src/pages/admin/bills/BillsListPage.tsx b/frontend/src/pages/admin/bills/BillsListPage.tsx
index cba0e2e5..a4c413f8 100644
--- a/frontend/src/pages/admin/bills/BillsListPage.tsx
+++ b/frontend/src/pages/admin/bills/BillsListPage.tsx
@@ -8,7 +8,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Plus, Search, Upload, X } from 'lucide-react';
import { billsService, type InvoiceStatus, type InvoiceSort } from '../../../services/bills.service';
-import { Button, Card, Input, Loading } from '../../../components/common';
+import { Button, Card, Input, Loading, LocalizedDateInput } from '../../../components/common';
import { formatMoney } from '../../../components/admin/LineItemsTable';
import { customerAdminService } from '../../../services/customerAdmin.service';
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
@@ -329,12 +329,12 @@ const ImportHistoricalInvoiceModal: React.FC = ({ onClose }) =
value={currency}
maxLength={3}
onChange={(e) => setCurrency(e.target.value.toUpperCase())} />
-
- = ({ onClose }) =
);
};
-
-/**
- * Date field that displays + accepts values in the admin-configured
- * format from Settings → General (`general_date_format`). Stores
- * + emits ISO (YYYY-MM-DD) so the rest of the form / API surface
- * keeps the canonical shape.
- *
- * Native `
` always renders in the browser's
- * locale (en-US users see MM/DD/YYYY), which mismatched what
- * customers + the rest of the app see elsewhere. This component
- * uses a plain text input + parses on blur, with the configured
- * format shown as both placeholder and helper text. A small
- * shadow native date input next to the field gives the click-to-
- * open calendar without affecting the displayed format.
- */
-interface LocalizedDateFieldProps {
- label: string;
- value: string;
- onChange: (iso: string) => void;
-}
-const LocalizedDateField: React.FC
= ({ label, value, onChange }) => {
- const { dateFormat } = useLocalizedDate();
- // Normalise the configured format down to the four shapes our
- // parser understands. Defaults to DD.MM.YYYY (the maintainer's
- // primary locale) when unknown.
- const normalisedFormat = ((): 'DD.MM.YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY-MM-DD' => {
- const f = String(dateFormat || 'dd.MM.yyyy').toLowerCase();
- if (f.startsWith('mm/dd')) return 'MM/DD/YYYY';
- if (f.startsWith('yyyy')) return 'YYYY-MM-DD';
- if (f.includes('/')) return 'DD/MM/YYYY';
- return 'DD.MM.YYYY';
- })();
- const placeholder = normalisedFormat.toLowerCase();
-
- // ISO → display
- const toDisplay = (iso: string): string => {
- if (!iso) return '';
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
- if (!m) return iso;
- const [, y, mo, d] = m;
- switch (normalisedFormat) {
- case 'MM/DD/YYYY': return `${mo}/${d}/${y}`;
- case 'YYYY-MM-DD': return `${y}-${mo}-${d}`;
- case 'DD/MM/YYYY': return `${d}/${mo}/${y}`;
- case 'DD.MM.YYYY':
- default: return `${d}.${mo}.${y}`;
- }
- };
-
- // display → ISO (accepts variant separators leniently)
- const toIso = (raw: string): string => {
- const s = raw.trim();
- if (!s) return '';
- // Already ISO?
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
- // Split on . / -
- const parts = s.split(/[./-]/);
- if (parts.length !== 3) return '';
- let [a, b, c] = parts;
- let y: string, mo: string, d: string;
- if (normalisedFormat === 'YYYY-MM-DD' || a.length === 4) {
- [y, mo, d] = [a, b, c];
- } else if (normalisedFormat === 'MM/DD/YYYY') {
- [mo, d, y] = [a, b, c];
- } else {
- // DD.MM.YYYY or DD/MM/YYYY
- [d, mo, y] = [a, b, c];
- }
- if (!/^\d{1,2}$/.test(d) || !/^\d{1,2}$/.test(mo) || !/^\d{4}$/.test(y)) return '';
- return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
- };
-
- const [text, setText] = React.useState(toDisplay(value));
- React.useEffect(() => { setText(toDisplay(value)); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [value]);
-
- return (
-
-
-
- setText(e.target.value)}
- onBlur={() => {
- const iso = toIso(text);
- if (iso) {
- onChange(iso);
- setText(toDisplay(iso));
- } else if (!text.trim()) {
- onChange('');
- }
- }}
- />
- {/* Tiny native date picker shortcut — gives the calendar
- without polluting the visible text input. Hidden value
- stays in ISO so it's always parseable. */}
- onChange(e.target.value)}
- aria-label={label}
- className="text-sm px-2 rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800"
- style={{ width: 36 }}
- />
-
-
{placeholder}
-
- );
-};
-
diff --git a/frontend/src/pages/admin/clients/TaxReportPage.tsx b/frontend/src/pages/admin/clients/TaxReportPage.tsx
index 958ef04c..d60c3ade 100644
--- a/frontend/src/pages/admin/clients/TaxReportPage.tsx
+++ b/frontend/src/pages/admin/clients/TaxReportPage.tsx
@@ -18,7 +18,7 @@ import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Calculator, Download, FileDown, AlertCircle } from 'lucide-react';
-import { Button, Card, Loading, Input } from '../../../components/common';
+import { Button, Card, Loading, LocalizedDateInput } from '../../../components/common';
// Lightweight native select styled to match Input — the common barrel
// doesn't export a Select component, and the form pieces here are
@@ -89,7 +89,7 @@ function triggerBrowserDownload(url: string, filename: string) {
export const TaxReportPage: React.FC = () => {
const { t, i18n } = useTranslation();
- const { format: fmtDate, dateInputLang } = useLocalizedDate();
+ const { format: fmtDate } = useLocalizedDate();
const [preset, setPreset] = useState('thisYear');
const initialPeriod = useMemo(() => periodForPreset('thisYear'), []);
const [from, setFrom] = useState(initialPeriod.from);
@@ -196,27 +196,21 @@ export const TaxReportPage: React.FC = () => {