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:
parent
f9b27fe56c
commit
51e2b13ec6
@ -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": {
|
||||||
|
|||||||
@ -3,75 +3,117 @@ 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);
|
||||||
const startDate = searchParams.get('startDate');
|
const startDate = searchParams.get('startDate');
|
||||||
const endDate = searchParams.get('endDate');
|
const endDate = searchParams.get('endDate');
|
||||||
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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user