feat: configurable field picker for work report CSV export

Users can now choose exactly which columns appear in the CSV:
- Title (always included)
- Calendar week grouping header
- Completed date, Scheduled date, Created date
- Project, List
- Priority (Eisenhower label or ABCDE grade)
- Link / URL
- Notes, Description
- Delegated to, Delegation note
- Start time, Kanban stage

UI additions in Settings > Data Export:
- 2-column checkbox grid with Select All / Default reset buttons
- Quick date presets: This week / This month / Last month / This year
- Download button shows the active column count

Default-on fields: week, completed date, scheduled date,
project, list, priority. All others opt-in.

API: fields passed as comma-separated ?fields= param; falls back to
defaults when omitted. Column headers are language-aware (de/en).

v1.92.0
This commit is contained in:
mARTin 2026-04-07 09:27:02 +02:00
parent f9b27fe56c
commit 51e2b13ec6
3 changed files with 250 additions and 127 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.91.0", "version": "1.92.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -3,48 +3,94 @@ import { getServerSession } from 'next-auth';
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
/** Return ISO week string like "2026-W14" */ // ── Field registry ─────────────────────────────────────────────────────────
// All optional columns the user can include. "key" is what the UI sends in
// the ?fields= query param. "always" fields are always present.
export type ExportFieldKey =
| 'completedDate'
| 'scheduledDate'
| 'createdDate'
| 'project'
| 'list'
| 'priority'
| 'url'
| 'notes'
| 'description'
| 'delegatedTo'
| 'delegationNote'
| 'startTime'
| 'kanbanStage'
| 'week';
export const EXPORT_FIELDS: {
key: ExportFieldKey;
labelDe: string;
labelEn: string;
defaultOn: boolean;
}[] = [
{ key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true },
{ key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true },
{ key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true },
{ key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false },
{ key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true },
{ key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true },
{ key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true },
{ key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false },
{ key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false },
{ key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false },
{ key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false },
{ key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false },
{ key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false },
{ key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false },
];
// ── Helpers ────────────────────────────────────────────────────────────────
function isoWeek(date: Date): string { function isoWeek(date: Date): string {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7; // Mon=1 … Sun=7 const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum); d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
} }
function weekLabel(date: Date): string { function weekLabel(date: Date, de: boolean): string {
// "KW 14 — 2026 (Mon 30 Mar Sun 5 Apr)"
const week = isoWeek(date); const week = isoWeek(date);
const [year, w] = week.split('-W'); const [year, w] = week.split('-W');
// Find Monday of that week
const d = new Date(Date.UTC(parseInt(year), 0, 1)); const d = new Date(Date.UTC(parseInt(year), 0, 1));
d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1); d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1);
const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6); const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6);
const fmt = (x: Date) => x.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: 'UTC' }); const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
return `KW ${w}${year} (${fmt(d)} ${fmt(sun)})`; return de
? `KW ${w}${year} (${fmt(d)} ${fmt(sun)})`
: `Week ${w}${year} (${fmt(d)} ${fmt(sun)})`;
} }
function fmtDate(d: Date | null | undefined): string { function fmtDate(d: Date | null | undefined, de: boolean): string {
if (!d) return ''; if (!d) return '';
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); return de
? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
: d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
} }
function priorityLabel(task: any): string { function priorityLabel(task: any, de: boolean): string {
if (task.urgency != null && task.importance != null) { if (task.urgency != null && task.importance != null) {
if (task.urgency && task.importance) return 'Sofort erledigen'; if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now';
if (!task.urgency && task.importance) return 'Planen'; if (!task.urgency && task.importance) return de ? 'Planen' : 'Schedule';
if (task.urgency && !task.importance) return 'Delegieren'; if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate';
return 'Eliminieren'; return de ? 'Eliminieren' : 'Eliminate';
} }
if (task.priority) return task.priority; if (task.priority) return task.priority; // A / B / C / D / E
return ''; return '';
} }
function csvCell(value: string): string { function csvCell(value: string): string {
return `"${(value || '').replace(/"/g, '""').replace(/\n/g, ' ')}"`; return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`;
} }
// ── Route ──────────────────────────────────────────────────────────────────
export async function GET(request: Request) { export async function GET(request: Request) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
@ -53,25 +99,21 @@ export async function GET(request: Request) {
const lang = searchParams.get('lang') || 'de'; const lang = searchParams.get('lang') || 'de';
const de = lang === 'de'; const de = lang === 'de';
// Parse selected fields; fall back to all defaultOn fields
const fieldsParam = searchParams.get('fields');
const selectedFields: Set<ExportFieldKey> = fieldsParam
? new Set(fieldsParam.split(',') as ExportFieldKey[])
: new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key));
if (!session || !session.user?.email) { if (!session || !session.user?.email) {
return new NextResponse('Unauthorized', { status: 401 }); return new NextResponse('Unauthorized', { status: 401 });
} }
try { try {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({ where: { email: session.user.email } });
where: { email: session.user.email }, if (!user) return new NextResponse('User not found', { status: 404 });
});
if (!user) {
return new NextResponse('User not found', { status: 404 });
}
const where: any = {
userId: user.id,
completed: true,
deletedAt: null,
};
const where: any = { userId: user.id, completed: true, deletedAt: null };
if (startDate || endDate) { if (startDate || endDate) {
where.updatedAt = {}; where.updatedAt = {};
if (startDate) where.updatedAt.gte = new Date(startDate); if (startDate) where.updatedAt.gte = new Date(startDate);
@ -86,11 +128,21 @@ export async function GET(request: Request) {
where, where,
orderBy: { updatedAt: 'desc' }, orderBy: { updatedAt: 'desc' },
include: { include: {
project: { select: { name: true, color: true } }, project: { select: { name: true } },
somedayList: { select: { title: true } }, somedayList: { select: { title: true } },
}, },
}); });
// Build header row — Title is always first
const headerRow: string[] = [de ? 'Aufgabe' : 'Task'];
for (const f of EXPORT_FIELDS) {
if (selectedFields.has(f.key) && f.key !== 'week') {
headerRow.push(de ? f.labelDe : f.labelEn);
}
}
const rows: string[][] = [headerRow];
// Group by ISO week (most recent first) // Group by ISO week (most recent first)
const byWeek = new Map<string, typeof tasks>(); const byWeek = new Map<string, typeof tasks>();
for (const task of tasks) { for (const task of tasks) {
@ -99,50 +151,58 @@ export async function GET(request: Request) {
byWeek.get(key)!.push(task); byWeek.get(key)!.push(task);
} }
const headers = de for (const [, weekTasks] of byWeek) {
? ['Woche', 'Erledigt am', 'Geplant für', 'Erstellt am', 'Aufgabe', 'Projekt', 'Liste', 'Priorität', 'Delegiert an', 'Notiz'] // Section header row when week column is selected
: ['Week', 'Completed', 'Scheduled', 'Created', 'Task', 'Project', 'List', 'Priority', 'Delegated to', 'Notes']; if (selectedFields.has('week')) {
const label = weekLabel(weekTasks[0].updatedAt, de);
const rows: string[][] = [headers]; // Fill entire row width with the label in first cell, rest blank
rows.push([label, ...Array(headerRow.length - 1).fill('')]);
for (const [weekKey, weekTasks] of byWeek) { }
// Section header row (empty for all columns except Week)
const weekDisplay = weekLabel(weekTasks[0].updatedAt);
rows.push([weekDisplay, '', '', '', '', '', '', '', '', '']);
for (const t of weekTasks) { for (const t of weekTasks) {
rows.push([ const row: string[] = [t.title || ''];
'', // no week label on task rows (grouped visually)
fmtDate(t.updatedAt), for (const f of EXPORT_FIELDS) {
fmtDate(t.scheduledDate ?? undefined), if (!selectedFields.has(f.key) || f.key === 'week') continue;
fmtDate(t.createdAt), switch (f.key) {
t.title || '', case 'completedDate': row.push(fmtDate(t.updatedAt, de)); break;
t.project?.name || '', case 'scheduledDate': row.push(fmtDate(t.scheduledDate ?? undefined, de)); break;
t.somedayList?.title || '', case 'createdDate': row.push(fmtDate(t.createdAt, de)); break;
priorityLabel(t), case 'project': row.push(t.project?.name || ''); break;
t.delegatedTo || '', case 'list': row.push(t.somedayList?.title || ''); break;
(t.markdownContent || t.description || '').slice(0, 500), case 'priority': row.push(priorityLabel(t, de)); break;
]); case 'url': row.push(t.url || ''); break;
case 'notes': row.push((t.markdownContent || '').slice(0, 1000)); break;
case 'description': row.push((t.description || '').slice(0, 500)); break;
case 'delegatedTo': row.push(t.delegatedTo || ''); break;
case 'delegationNote': row.push(t.delegationNote || ''); break;
case 'startTime': row.push(t.startTime || ''); break;
case 'kanbanStage': row.push(t.kanbanStage || ''); break;
}
}
rows.push(row);
} }
// Blank spacer between weeks // Blank spacer between weeks
rows.push(Array(headers.length).fill('')); rows.push(Array(headerRow.length).fill(''));
} }
const csvContent = rows const csvContent = rows.map(row => row.map(csvCell).join(',')).join('\r\n');
.map((row) => row.map(csvCell).join(','))
.join('\r\n');
// Prepend BOM so Excel opens UTF-8 correctly
const bom = '\uFEFF'; const bom = '\uFEFF';
const period = startDate && endDate const period = startDate && endDate
? `_${startDate}_bis_${endDate}` ? `_${startDate}_${endDate}`
: startDate ? `_ab_${startDate}` : endDate ? `_bis_${endDate}` : ''; : startDate ? `_ab_${startDate}` : endDate ? `_bis_${endDate}` : '';
const filename = de
? `arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.csv`
: `work-report${period}_${new Date().toISOString().split('T')[0]}.csv`;
return new NextResponse(bom + csvContent, { return new NextResponse(bom + csvContent, {
headers: { headers: {
'Content-Type': 'text/csv; charset=utf-8', 'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': `attachment; filename="arbeitsbericht${period}_${new Date().toISOString().split('T')[0]}.csv"`, 'Content-Disposition': `attachment; filename="${filename}"`,
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -10,6 +10,7 @@ import { ViewStyle, KanbanStage, Task } from "./WeeklyView";
import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants"; import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants";
import { translations } from "../lib/weeklyViewTranslations"; import { translations } from "../lib/weeklyViewTranslations";
import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants"; import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants";
import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route";
import { import {
ArrowLeftRight, ArrowLeftRight,
Calendar, Calendar,
@ -331,6 +332,9 @@ function SettingsSidebar({
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [exportStartDate, setExportStartDate] = useState(""); const [exportStartDate, setExportStartDate] = useState("");
const [exportEndDate, setExportEndDate] = useState(""); const [exportEndDate, setExportEndDate] = useState("");
const [exportFields, setExportFields] = useState<Set<ExportFieldKey>>(
() => new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key))
);
const [importMode, setImportMode] = useState<"merge" | "replace">("merge"); const [importMode, setImportMode] = useState<"merge" | "replace">("merge");
const [importFile, setImportFile] = useState<File | null>(null); const [importFile, setImportFile] = useState<File | null>(null);
const [importMsg, setImportMsg] = useState(""); const [importMsg, setImportMsg] = useState("");
@ -4236,94 +4240,153 @@ function SettingsSidebar({
> >
{profile.language === "de" ? "Datenexport" : "Data Export"} {profile.language === "de" ? "Datenexport" : "Data Export"}
</h3> </h3>
<p <p style={{ fontSize: "0.85rem", color: "var(--weekly-settings-label)", marginBottom: "14px" }}>
style={{
fontSize: "0.9rem",
color: "var(--weekly-settings-label)",
marginBottom: "15px",
}}
>
{profile.language === "de" {profile.language === "de"
? "CSV-Arbeitsbericht mit erledigten Aufgaben, Projekten und Prioritäten — nach Kalenderwochen gruppiert. Ideal für Freelancer und Mitarbeiter." ? "CSV-Arbeitsbericht erledigter Aufgaben — nach Kalenderwochen gruppiert. Wähle Felder, Zeitraum und lade die Datei herunter."
: "CSV work report of completed tasks with project, list, priority and notes — grouped by calendar week. Perfect for freelancers and employees."} : "CSV work report of completed tasks grouped by calendar week. Choose fields, date range and download."}
</p> </p>
<div
style={{ display: "flex", gap: "10px", marginBottom: "15px" }} {/* Date range */}
> <div style={{ display: "flex", gap: "10px", marginBottom: "14px" }}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<label <label style={{ display: "block", fontSize: "0.78rem", fontWeight: 600, marginBottom: "4px", color: "var(--weekly-settings-label)" }}>
style={{ {profile.language === "de" ? "Von" : "From"}
display: "block",
fontSize: "0.8rem",
fontWeight: 600,
marginBottom: "4px",
color: "var(--weekly-settings-label)",
}}
>
Start
</label> </label>
<input <input
type="date" type="date"
value={exportStartDate} value={exportStartDate}
onChange={(e) => setExportStartDate(e.target.value)} onChange={(e) => setExportStartDate(e.target.value)}
className="weekly-input" className="weekly-input"
style={{ style={{ width: "100%", padding: "6px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }}
width: "100%",
padding: "6px",
border: "1px solid var(--weekly-settings-input-border)",
borderRadius: "4px",
background: "var(--weekly-settings-input-bg)",
color: "var(--weekly-settings-text)",
}}
/> />
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<label <label style={{ display: "block", fontSize: "0.78rem", fontWeight: 600, marginBottom: "4px", color: "var(--weekly-settings-label)" }}>
style={{ {profile.language === "de" ? "Bis" : "To"}
display: "block",
fontSize: "0.8rem",
fontWeight: 600,
marginBottom: "4px",
color: "var(--weekly-settings-label)",
}}
>
End
</label> </label>
<input <input
type="date" type="date"
value={exportEndDate} value={exportEndDate}
onChange={(e) => setExportEndDate(e.target.value)} onChange={(e) => setExportEndDate(e.target.value)}
className="weekly-input" className="weekly-input"
style={{ style={{ width: "100%", padding: "6px", border: "1px solid var(--weekly-settings-input-border)", borderRadius: "4px", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-text)" }}
width: "100%",
padding: "6px",
border: "1px solid var(--weekly-settings-input-border)",
borderRadius: "4px",
background: "var(--weekly-settings-input-bg)",
color: "var(--weekly-settings-text)",
}}
/> />
</div> </div>
</div> </div>
{/* Quick date presets */}
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap", marginBottom: "16px" }}>
{[
{ label: profile.language === "de" ? "Diese Woche" : "This week", days: 7 },
{ label: profile.language === "de" ? "Dieser Monat" : "This month", days: 30 },
{ label: profile.language === "de" ? "Letzter Monat" : "Last month", days: 60, offset: 30 },
{ label: profile.language === "de" ? "Dieses Jahr" : "This year", days: 365 },
].map(({ label, days, offset }) => (
<button
key={label}
onClick={() => {
const end = new Date();
if (offset) end.setDate(end.getDate() - offset);
const start = new Date(end);
start.setDate(start.getDate() - days + (offset ? 0 : 0));
// Simpler: this month = first day of month
if (label.includes("Monat") || label.includes("month")) {
if (offset) {
// last month
const now = new Date();
const s = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const e = new Date(now.getFullYear(), now.getMonth(), 0);
setExportStartDate(s.toISOString().split("T")[0]);
setExportEndDate(e.toISOString().split("T")[0]);
return;
} else {
const now = new Date();
const s = new Date(now.getFullYear(), now.getMonth(), 1);
setExportStartDate(s.toISOString().split("T")[0]);
setExportEndDate(new Date().toISOString().split("T")[0]);
return;
}
}
if (label.includes("Jahr") || label.includes("year")) {
const now = new Date();
setExportStartDate(`${now.getFullYear()}-01-01`);
setExportEndDate(now.toISOString().split("T")[0]);
return;
}
// This week
const now = new Date();
const day = now.getDay() || 7;
const mon = new Date(now); mon.setDate(now.getDate() - day + 1);
setExportStartDate(mon.toISOString().split("T")[0]);
setExportEndDate(now.toISOString().split("T")[0]);
}}
style={{ fontSize: "0.75rem", padding: "3px 9px", borderRadius: "5px", border: "1px solid var(--weekly-settings-input-border)", background: "var(--weekly-settings-input-bg)", color: "var(--weekly-settings-label)", cursor: "pointer" }}
>
{label}
</button>
))}
</div>
{/* Field picker */}
<div style={{ marginBottom: "14px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Spalten auswählen:" : "Select columns:"}
</span>
<div style={{ display: "flex", gap: "6px" }}>
<button
onClick={() => setExportFields(new Set(EXPORT_FIELDS.map(f => f.key)))}
style={{ fontSize: "0.72rem", padding: "2px 7px", borderRadius: "4px", border: "1px solid var(--weekly-settings-input-border)", background: "transparent", color: "var(--weekly-settings-label)", cursor: "pointer" }}
>
{profile.language === "de" ? "Alle" : "All"}
</button>
<button
onClick={() => setExportFields(new Set(EXPORT_FIELDS.filter(f => f.defaultOn).map(f => f.key)))}
style={{ fontSize: "0.72rem", padding: "2px 7px", borderRadius: "4px", border: "1px solid var(--weekly-settings-input-border)", background: "transparent", color: "var(--weekly-settings-label)", cursor: "pointer" }}
>
{profile.language === "de" ? "Standard" : "Default"}
</button>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px 12px" }}>
{EXPORT_FIELDS.map(f => {
const checked = exportFields.has(f.key);
const label = profile.language === "de" ? f.labelDe : f.labelEn;
return (
<label
key={f.key}
style={{ display: "flex", alignItems: "center", gap: "7px", fontSize: "0.82rem", color: "var(--weekly-settings-text)", cursor: "pointer", padding: "3px 0" }}
>
<input
type="checkbox"
checked={checked}
onChange={() => {
setExportFields(prev => {
const next = new Set(prev);
if (next.has(f.key)) next.delete(f.key);
else next.add(f.key);
return next;
});
}}
style={{ accentColor: "#6366f1", width: "14px", height: "14px", flexShrink: 0 }}
/>
{label}
</label>
);
})}
</div>
</div>
{/* Download button */}
<a <a
href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}`} href={`/api/user/export?startDate=${exportStartDate}&endDate=${exportEndDate}&lang=${profile.language || 'de'}&fields=${Array.from(exportFields).join(',')}`}
target="_blank" target="_blank"
className="weekly-auth-button w-full justify-center" className="weekly-auth-button w-full justify-center"
style={{ style={{ display: "inline-flex", textDecoration: "none", background: "var(--weekly-settings-item-bg)", border: "1px solid var(--weekly-settings-input-border)", color: "var(--weekly-settings-text)", padding: "10px", borderRadius: "4px", fontWeight: 500, transition: "background-color 0.2s" }}
display: "inline-flex",
textDecoration: "none",
background: "var(--weekly-settings-item-bg)",
border: "1px solid var(--weekly-settings-input-border)",
color: "var(--weekly-settings-text)",
padding: "10px",
borderRadius: "4px",
fontWeight: 500,
transition: "background-color 0.2s",
}}
> >
{profile.language === "de" {profile.language === "de"
? "Arbeitsbericht exportieren (CSV)" ? `Arbeitsbericht exportieren (${exportFields.size} Spalten)`
: "Export Work Report (CSV)"} : `Export Work Report (${exportFields.size} columns)`}
</a> </a>
</div> </div>