Phase 0.3: migrate date formatting to date-fns
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 0.1 | ✅ done | 2026-07-23 | codebase-vs-requirements.md neu geschrieben, security-review-phase2.md Resolution Summary, architecture.md Implementation Status, MASTER-PLAN.md + PROGRESS.md erstellt |
|
| 0.1 | ✅ done | 2026-07-23 | codebase-vs-requirements.md neu geschrieben, security-review-phase2.md Resolution Summary, architecture.md Implementation Status, MASTER-PLAN.md + PROGRESS.md erstellt |
|
||||||
| 0.2 | ✅ done | 2026-07-23 | lucide-react installieren + Icons migrieren |
|
| 0.2 | ✅ done | 2026-07-23 | lucide-react installieren + Icons migrieren |
|
||||||
| 0.3 | ⬜ pending | | date-fns installieren + Datum-Formatierung |
|
| 0.3 | ✅ done | 2026-07-23 | date-fns installieren + Datum-Formatierung |
|
||||||
| 0.4 | ⬜ pending | | hooks.ts aufteilen |
|
| 0.4 | ⬜ pending | | hooks.ts aufteilen |
|
||||||
| 0.5 | ⬜ pending | | Store-Verzeichnis konsolidieren |
|
| 0.5 | ⬜ pending | | Store-Verzeichnis konsolidieren |
|
||||||
| 0.6 | ⬜ pending | | Frontend-Bestandsanalyse als Dokument speichern |
|
| 0.6 | ⬜ pending | | Frontend-Bestandsanalyse als Dokument speichern |
|
||||||
|
|||||||
Generated
+10
@@ -23,6 +23,7 @@
|
|||||||
"@tiptap/starter-kit": "^3.28.0",
|
"@tiptap/starter-kit": "^3.28.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
"i18next": "^23.14.0",
|
"i18next": "^23.14.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"lucide-react": "^1.25.0",
|
"lucide-react": "^1.25.0",
|
||||||
@@ -2747,6 +2748,15 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@tiptap/starter-kit": "^3.28.0",
|
"@tiptap/starter-kit": "^3.28.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
"i18next": "^23.14.0",
|
"i18next": "^23.14.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"lucide-react": "^1.25.0",
|
"lucide-react": "^1.25.0",
|
||||||
|
|||||||
@@ -438,13 +438,14 @@ export function decodeMimeHeader(value: string | undefined | null): string {
|
|||||||
* Variables: {{user.name}}, {{user.first_name}}, {{user.last_name}},
|
* Variables: {{user.name}}, {{user.first_name}}, {{user.last_name}},
|
||||||
* {{user.email}}, {{user.role}}, {{tenant.name}}, {{date}}
|
* {{user.email}}, {{user.role}}, {{tenant.name}}, {{date}}
|
||||||
*/
|
*/
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
|
|
||||||
export function replaceSignatureVariables(
|
export function replaceSignatureVariables(
|
||||||
html: string,
|
html: string,
|
||||||
user: { name?: string; email?: string; role?: string; first_name?: string; last_name?: string },
|
user: { name?: string; email?: string; role?: string; first_name?: string; last_name?: string },
|
||||||
tenant?: { name?: string },
|
tenant?: { name?: string },
|
||||||
): string {
|
): string {
|
||||||
const now = new Date();
|
const dateStr = formatDateShort(new Date());
|
||||||
const dateStr = now.toLocaleDateString([], { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
||||||
const firstName = user.first_name || (user.name ? user.name.split(' ')[0] : '');
|
const firstName = user.first_name || (user.name ? user.name.split(' ')[0] : '');
|
||||||
const lastName = user.last_name || (user.name ? user.name.split(' ').slice(1).join(' ') : '');
|
const lastName = user.last_name || (user.name ? user.name.split(' ').slice(1).join(' ') : '');
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type EntryPriority,
|
type EntryPriority,
|
||||||
type EntrySubtype,
|
type EntrySubtype,
|
||||||
} from '@/api/calendar';
|
} from '@/api/calendar';
|
||||||
|
import { formatDateTimeInput } from '@/utils/date';
|
||||||
|
|
||||||
export interface AppointmentModalProps {
|
export interface AppointmentModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -41,9 +42,7 @@ export interface AppointmentModalProps {
|
|||||||
|
|
||||||
function toDateTimeLocalValue(d: Date | null | undefined): string {
|
function toDateTimeLocalValue(d: Date | null | undefined): string {
|
||||||
if (!d) return '';
|
if (!d) return '';
|
||||||
// datetime-local needs YYYY-MM-DDTHH:mm in local time
|
return formatDateTimeInput(d);
|
||||||
const pad = (n: number) => String(n).padStart(2, '0');
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromDateTimeLocalValue(v: string): Date | null {
|
function fromDateTimeLocalValue(v: string): Date | null {
|
||||||
|
|||||||
@@ -21,21 +21,11 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Calendar as CalendarIcon, Pencil, Trash2, X } from 'lucide-react';
|
import { Calendar as CalendarIcon, Pencil, Trash2, X } from 'lucide-react';
|
||||||
|
import { formatDateTime } from '@/utils/date';
|
||||||
|
|
||||||
function formatDate(dateStr: string | null | undefined): string {
|
function formatDate(dateStr: string | null | undefined): string {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
try {
|
return formatDateTime(dateStr) || '-';
|
||||||
const d = new Date(dateStr);
|
|
||||||
return d.toLocaleDateString(undefined, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function priorityClass(p: string): string {
|
function priorityClass(p: string): string {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CalendarEntry } from '@/api/calendar';
|
import type { CalendarEntry } from '@/api/calendar';
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
|
|
||||||
export type KanbanStatus = 'open' | 'in_progress' | 'done' | 'cancelled';
|
export type KanbanStatus = 'open' | 'in_progress' | 'done' | 'cancelled';
|
||||||
|
|
||||||
@@ -38,8 +39,7 @@ function priorityBadgeClass(p: string): string {
|
|||||||
|
|
||||||
function fmtDue(entry: CalendarEntry): string | null {
|
function fmtDue(entry: CalendarEntry): string | null {
|
||||||
if (!entry.due_date) return null;
|
if (!entry.due_date) return null;
|
||||||
const d = new Date(entry.due_date);
|
return formatDateShort(entry.due_date) || null;
|
||||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanBoard({ board, loading, onMove, onSelect }: KanbanBoardProps) {
|
export function KanbanBoard({ board, loading, onMove, onSelect }: KanbanBoardProps) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type CalendarEntry,
|
type CalendarEntry,
|
||||||
type Subtask,
|
type Subtask,
|
||||||
} from '@/api/calendar';
|
} from '@/api/calendar';
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ export function TaskDetailPanel({ entry, onClose, onChanged }: TaskDetailPanelPr
|
|||||||
</span>
|
</span>
|
||||||
{entry.due_date && (
|
{entry.due_date && (
|
||||||
<span className="px-2 py-0.5 rounded bg-primary-50 text-primary-700">
|
<span className="px-2 py-0.5 rounded bg-primary-50 text-primary-700">
|
||||||
{new Date(entry.due_date).toLocaleDateString('de-DE')}
|
{formatDateShort(entry.due_date)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,15 +9,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import type { DmsFile } from '@/api/dms';
|
import type { DmsFile } from '@/api/dms';
|
||||||
import { getFileIcon, formatFileSize } from './FileExplorer';
|
import { getFileIcon, formatFileSize } from './FileExplorer';
|
||||||
import { Download, Eye, FileText, Share2, Trash2, X } from 'lucide-react';
|
import { Download, Eye, FileText, Share2, Trash2, X } from 'lucide-react';
|
||||||
|
import { formatDateTime } from '@/utils/date';
|
||||||
|
|
||||||
function formatDate(dateStr: string | null | undefined): string {
|
function formatDate(dateStr: string | null | undefined): string {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
try {
|
return formatDateTime(dateStr) || '-';
|
||||||
const d = new Date(dateStr);
|
|
||||||
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
||||||
} catch {
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileSize(file: DmsFile): number {
|
function getFileSize(file: DmsFile): number {
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export function formatFileSize(bytes: number): string {
|
|||||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||||
}
|
}
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
|
|
||||||
|
|
||||||
function getFileSize(file: DmsFile): number {
|
function getFileSize(file: DmsFile): number {
|
||||||
return file.size_bytes ?? file.size ?? 0;
|
return file.size_bytes ?? file.size ?? 0;
|
||||||
@@ -45,12 +47,7 @@ function getFileSize(file: DmsFile): number {
|
|||||||
|
|
||||||
function formatDate(dateStr: string | null | undefined): string {
|
function formatDate(dateStr: string | null | undefined): string {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
try {
|
return formatDateShort(dateStr) || '';
|
||||||
const d = new Date(dateStr);
|
|
||||||
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
||||||
} catch {
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewMode = 'list' | 'table' | 'icons-sm' | 'icons-md' | 'icons-lg';
|
export type ViewMode = 'list' | 'table' | 'icons-sm' | 'icons-md' | 'icons-lg';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
||||||
import { FileText } from 'lucide-react';
|
import { FileText } from 'lucide-react';
|
||||||
@@ -56,7 +57,7 @@ export function FilePreviewModal({ open, file, onClose }: FilePreviewModalProps)
|
|||||||
<span className="text-sm text-secondary-500">{formatFileSize(file.size_bytes ?? file.size ?? 0)}</span>
|
<span className="text-sm text-secondary-500">{formatFileSize(file.size_bytes ?? file.size ?? 0)}</span>
|
||||||
{file.created_at && (
|
{file.created_at && (
|
||||||
<span className="text-sm text-secondary-500">
|
<span className="text-sm text-secondary-500">
|
||||||
{t('dms.fileModified')}: {new Date(file.created_at).toLocaleDateString()}
|
{t('dms.fileModified')}: {formatDateShort(file.created_at)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
@@ -259,7 +260,7 @@ export function ShareDialog({ open, file, onClose, onShared }: ShareDialogProps)
|
|||||||
)}
|
)}
|
||||||
{link.expires_at && (
|
{link.expires_at && (
|
||||||
<Badge variant="info">
|
<Badge variant="info">
|
||||||
{t('permissions.expiresAt')}: {new Date(link.expires_at).toLocaleDateString()}
|
{t('permissions.expiresAt')}: {formatDateShort(link.expires_at)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { formatTime } from '@/utils/date';
|
||||||
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
import { useCommStore } from '@/store/commStore';
|
import { useCommStore } from '@/store/commStore';
|
||||||
@@ -241,7 +242,7 @@ function MessageFeed({
|
|||||||
)}
|
)}
|
||||||
{msg.created_at && (
|
{msg.created_at && (
|
||||||
<span className="text-[10px] opacity-50 mt-0.5 block">
|
<span className="text-[10px] opacity-50 mt-0.5 block">
|
||||||
{new Date(msg.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
{formatTime(msg.created_at)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { decodeMimeHeader } from '@/api/mail';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { FileText, Loader2 } from 'lucide-react';
|
import { FileText, Loader2 } from 'lucide-react';
|
||||||
|
import { formatDateTime } from '@/utils/date';
|
||||||
|
|
||||||
export interface MailDetailProps {
|
export interface MailDetailProps {
|
||||||
mail: Mail | null; loading: boolean;
|
mail: Mail | null; loading: boolean;
|
||||||
@@ -26,14 +27,7 @@ export interface MailDetailProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatFullDate(dateStr: string): string {
|
function formatFullDate(dateStr: string): string {
|
||||||
const date = new Date(dateStr);
|
return formatDateTime(dateStr) || dateStr;
|
||||||
return date.toLocaleString([], {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { decodeMimeHeader } from '@/api/mail';
|
|||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { Pagination } from '@/components/ui/Pagination';
|
import { Pagination } from '@/components/ui/Pagination';
|
||||||
import { ChevronUp, Loader2, Paperclip, Star } from 'lucide-react';
|
import { ChevronUp, Loader2, Paperclip, Star } from 'lucide-react';
|
||||||
|
import { formatSmartDate } from '@/utils/date';
|
||||||
|
|
||||||
export interface MailListProps {
|
export interface MailListProps {
|
||||||
mails: Mail[];
|
mails: Mail[];
|
||||||
@@ -30,12 +31,7 @@ export interface MailListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateStr: string): string {
|
function formatDate(dateStr: string): string {
|
||||||
const date = new Date(dateStr);
|
return formatSmartDate(dateStr) || dateStr;
|
||||||
const now = new Date();
|
|
||||||
if (date.toDateString() === now.toDateString()) {
|
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
||||||
}
|
|
||||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MailList({
|
export function MailList({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { formatDateTime } from '@/utils/date';
|
||||||
|
|
||||||
export function AuditLogPage() {
|
export function AuditLogPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -41,8 +42,7 @@ export function AuditLogPage() {
|
|||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const val = info.getValue();
|
const val = info.getValue();
|
||||||
if (!val) return '—';
|
if (!val) return '—';
|
||||||
const date = new Date(val);
|
return formatDateTime(val) || '—';
|
||||||
return date.toLocaleString('de-DE');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { MonthView } from '@/components/calendar/MonthView';
|
|||||||
import { WeekView } from '@/components/calendar/WeekView';
|
import { WeekView } from '@/components/calendar/WeekView';
|
||||||
import { DayView } from '@/components/calendar/DayView';
|
import { DayView } from '@/components/calendar/DayView';
|
||||||
import { RangeView } from '@/components/calendar/RangeView';
|
import { RangeView } from '@/components/calendar/RangeView';
|
||||||
|
import { formatDateInput } from '@/utils/date';
|
||||||
import { CalendarDetail } from '@/components/calendar/CalendarDetail';
|
import { CalendarDetail } from '@/components/calendar/CalendarDetail';
|
||||||
import { AppointmentModal } from '@/components/calendar/AppointmentModal';
|
import { AppointmentModal } from '@/components/calendar/AppointmentModal';
|
||||||
import { IcsControls } from '@/components/calendar/IcsControls';
|
import { IcsControls } from '@/components/calendar/IcsControls';
|
||||||
@@ -488,7 +489,7 @@ export function CalendarPage() {
|
|||||||
<label className="text-xs text-secondary-600">{t('calendar.range.start')}</label>
|
<label className="text-xs text-secondary-600">{t('calendar.range.start')}</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={rangeStart ? rangeStart.toISOString().slice(0, 10) : ''}
|
value={rangeStart ? formatDateInput(rangeStart) : ''}
|
||||||
onChange={(e) => setRangeStart(e.target.value ? new Date(e.target.value) : null)}
|
onChange={(e) => setRangeStart(e.target.value ? new Date(e.target.value) : null)}
|
||||||
className="text-sm rounded-md border border-secondary-300 px-2 py-1"
|
className="text-sm rounded-md border border-secondary-300 px-2 py-1"
|
||||||
data-testid="calendar-range-start"
|
data-testid="calendar-range-start"
|
||||||
@@ -496,7 +497,7 @@ export function CalendarPage() {
|
|||||||
<label className="text-xs text-secondary-600">{t('calendar.range.end')}</label>
|
<label className="text-xs text-secondary-600">{t('calendar.range.end')}</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={rangeEnd ? rangeEnd.toISOString().slice(0, 10) : ''}
|
value={rangeEnd ? formatDateInput(rangeEnd) : ''}
|
||||||
onChange={(e) => setRangeEnd(e.target.value ? new Date(e.target.value) : null)}
|
onChange={(e) => setRangeEnd(e.target.value ? new Date(e.target.value) : null)}
|
||||||
className="text-sm rounded-md border border-secondary-300 px-2 py-1"
|
className="text-sm rounded-md border border-secondary-300 px-2 py-1"
|
||||||
data-testid="calendar-range-end"
|
data-testid="calendar-range-end"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { StatCard } from '@/components/shared/StatCard';
|
import { StatCard } from '@/components/shared/StatCard';
|
||||||
import { ActivityFeed, ActivityItem } from '@/components/shared/ActivityFeed';
|
import { ActivityFeed, ActivityItem } from '@/components/shared/ActivityFeed';
|
||||||
import { useUnifiedContacts, useAuditLog } from '@/api/hooks';
|
import { useUnifiedContacts, useAuditLog } from '@/api/hooks';
|
||||||
|
import { formatDateTime } from '@/utils/date';
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -36,7 +37,7 @@ export function DashboardPage() {
|
|||||||
id: `${entry.timestamp}-${entry.user}-${entry.action}`,
|
id: `${entry.timestamp}-${entry.user}-${entry.action}`,
|
||||||
user: entry.user || 'System',
|
user: entry.user || 'System',
|
||||||
action: entry.action || '',
|
action: entry.action || '',
|
||||||
time: entry.timestamp ? new Date(entry.timestamp).toLocaleString('de-DE') : '',
|
time: entry.timestamp ? (formatDateTime(entry.timestamp) || '') : '',
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { formatDateShort } from '@/utils/date';
|
||||||
import { Table, TableColumn } from '@/components/ui/Table';
|
import { Table, TableColumn } from '@/components/ui/Table';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { Skeleton } from '@/components/ui/Skeleton';
|
import { Skeleton } from '@/components/ui/Skeleton';
|
||||||
@@ -79,7 +80,7 @@ export function DmsTrashPage() {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
accessor: (file) => file.deleted_at || '',
|
accessor: (file) => file.deleted_at || '',
|
||||||
render: (file) => file.deleted_at
|
render: (file) => file.deleted_at
|
||||||
? new Date(file.deleted_at).toLocaleDateString()
|
? (formatDateShort(file.deleted_at) || '—')
|
||||||
: '—',
|
: '—',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { format, formatDistanceToNow, parseISO, isValid, isToday } from 'date-fns';
|
||||||
|
import { de } from 'date-fns/locale';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date string or Date object into a valid Date, or null if invalid/empty.
|
||||||
|
*/
|
||||||
|
function toDate(value: string | Date | null | undefined): Date | null {
|
||||||
|
if (value === null || value === undefined || value === '') return null;
|
||||||
|
if (value instanceof Date) return isValid(value) ? value : null;
|
||||||
|
const parsed = parseISO(value);
|
||||||
|
return isValid(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date with a custom date-fns pattern and German locale.
|
||||||
|
* Returns empty string for null/undefined/invalid dates.
|
||||||
|
*/
|
||||||
|
export function formatDate(date: string | Date | null | undefined, pattern: string): string {
|
||||||
|
const d = toDate(date);
|
||||||
|
if (!d) return '';
|
||||||
|
return format(d, pattern, { locale: de });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard date + time format: dd.MM.yyyy HH:mm
|
||||||
|
*/
|
||||||
|
export function formatDateTime(date: string | Date | null | undefined): string {
|
||||||
|
return formatDate(date, 'dd.MM.yyyy HH:mm');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short date format: dd.MM.yyyy
|
||||||
|
*/
|
||||||
|
export function formatDateShort(date: string | Date | null | undefined): string {
|
||||||
|
return formatDate(date, 'dd.MM.yyyy');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time only format: HH:mm
|
||||||
|
*/
|
||||||
|
export function formatTime(date: string | Date | null | undefined): string {
|
||||||
|
return formatDate(date, 'HH:mm');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative time format: "vor 5 Minuten", "in 2 Stunden"
|
||||||
|
*/
|
||||||
|
export function formatRelative(date: string | Date | null | undefined): string {
|
||||||
|
const d = toDate(date);
|
||||||
|
if (!d) return '';
|
||||||
|
return formatDistanceToNow(d, { locale: de, addSuffix: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Month + day format: "15. Juli"
|
||||||
|
*/
|
||||||
|
export function formatMonthDay(date: string | Date | null | undefined): string {
|
||||||
|
return formatDate(date, 'd. MMMM');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart date: shows time (HH:mm) if today, otherwise short date (d. MMM).
|
||||||
|
* Used in mail list and similar compact views.
|
||||||
|
*/
|
||||||
|
export function formatSmartDate(date: string | Date | null | undefined): string {
|
||||||
|
const d = toDate(date);
|
||||||
|
if (!d) return '';
|
||||||
|
if (isToday(d)) {
|
||||||
|
return format(d, 'HH:mm', { locale: de });
|
||||||
|
}
|
||||||
|
return format(d, 'd. MMM', { locale: de });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date input format for <input type="date">: yyyy-MM-dd
|
||||||
|
*/
|
||||||
|
export function formatDateInput(date: string | Date | null | undefined): string {
|
||||||
|
return formatDate(date, 'yyyy-MM-dd');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DateTime input format for <input type="datetime-local">: yyyy-MM-ddTHH:mm
|
||||||
|
*/
|
||||||
|
export function formatDateTimeInput(date: string | Date | null | undefined): string {
|
||||||
|
return formatDate(date, "yyyy-MM-dd'T'HH:mm");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user