Compare commits

..

5 Commits

Author SHA1 Message Date
0678fdcaa7 mit Auth, aber ohne Hash 2026-02-28 15:34:37 +00:00
36f352de58 Auto 2026-02-27 20:08:32 +00:00
734dbfe24b Autovervollständigung 2026-02-27 20:07:45 +00:00
ba7082897f Keine Linie über dem Footer 2026-02-27 18:38:16 +00:00
5981a7a6db V1.0.0 Es funktioniert soweit Alles 2026-02-27 16:41:03 +00:00
8 changed files with 145 additions and 22 deletions

View File

@@ -2,3 +2,13 @@ DB_HOST=localhost
DB_USER=root DB_USER=root
DB_PASSWORD=your_password DB_PASSWORD=your_password
DB_NAME=RXF DB_NAME=RXF
# Authentication Configuration
# Format: username:passwordHash,username2:passwordHash2 (max 5 users)
# Use 'node scripts/generate-password.js [password]' to generate hashes
# Leave empty to disable authentication
# Example hashes below (passwords: admin123, pass1):
AUTH_USERS=admin:$2b$10$DKLO7uQPmdAw9Z64NChro.8mOsnqZQaRZjctWDojIkK926ROBVyJW,user1:$2b$10$K613Z70Hodr6xyEh10Mw2uoRZMV3U4LIB09929JUWw2n/pXKoUqaW
# Secret key for JWT session encryption (change in production!)
AUTH_SECRET=your-super-secret-key-change-this-in-production

View File

@@ -10,13 +10,20 @@ export async function PUT(
try { try {
const { id } = await context.params; const { id } = await context.params;
const body = await request.json(); const body = await request.json();
const { Datum, Wo, Was, Wieviel, Wie, TYP, OK } = body; const { Datum, Wo, Was, Wieviel, Wie, TYP } = body;
if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) {
return NextResponse.json(
{ success: false, error: 'Missing required fields' },
{ status: 400 }
);
}
const pool = getDbPool(); const pool = getDbPool();
const query = ` const query = `
UPDATE Ausgaben UPDATE Ausgaben
SET Datum = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, TYP = ?, OK = ? SET Datum = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, TYP = ?
WHERE ID = ? WHERE ID = ?
`; `;
@@ -27,7 +34,6 @@ export async function PUT(
parseFloat(Wieviel), parseFloat(Wieviel),
Wie, Wie,
TYP, TYP,
OK || 0,
parseInt(id), parseInt(id),
]); ]);

View File

@@ -0,0 +1,51 @@
import { NextResponse } from 'next/server';
import { getDbPool } from '@/lib/db';
import { RowDataPacket } from 'mysql2';
// GET /api/ausgaben/autocomplete - Fetch unique Wo and Was values for autocomplete
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const typ = searchParams.get('typ');
const pool = getDbPool();
let query = 'SELECT DISTINCT Wo, Was FROM Ausgaben';
const params: any[] = [];
if (typ !== null && typ !== undefined) {
query += ' WHERE TYP = ?';
params.push(parseInt(typ));
}
query += ' ORDER BY Wo, Was';
const [rows] = await pool.query<RowDataPacket[]>(query, params);
// Extract unique Wo and Was values
const woSet = new Set<string>();
const wasSet = new Set<string>();
rows.forEach((row) => {
if (row.Wo) woSet.add(row.Wo);
if (row.Was) wasSet.add(row.Was);
});
const wo = Array.from(woSet).sort();
const was = Array.from(wasSet).sort();
return NextResponse.json({
success: true,
data: {
wo,
was,
},
});
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ success: false, error: 'Database error' },
{ status: 500 }
);
}
}

View File

@@ -14,7 +14,8 @@ export async function GET(request: Request) {
const pool = getDbPool(); const pool = getDbPool();
let query = `SELECT *, let query = `SELECT
ID, Datum, Wo, Was, Wieviel, Wie, TYP,
CASE DAYOFWEEK(Datum) CASE DAYOFWEEK(Datum)
WHEN 1 THEN 'Sonntag' WHEN 1 THEN 'Sonntag'
WHEN 2 THEN 'Montag' WHEN 2 THEN 'Montag'
@@ -67,7 +68,7 @@ export async function GET(request: Request) {
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); const body = await request.json();
const { Datum, Wo, Was, Wieviel, Wie, TYP, OK } = body; const { Datum, Wo, Was, Wieviel, Wie, TYP } = body;
if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) { if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) {
return NextResponse.json( return NextResponse.json(
@@ -79,8 +80,8 @@ export async function POST(request: Request) {
const pool = getDbPool(); const pool = getDbPool();
const query = ` const query = `
INSERT INTO Ausgaben (Datum, Wo, Was, Wieviel, Wie, TYP, OK) INSERT INTO Ausgaben (Datum, Wo, Was, Wieviel, Wie, TYP)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
`; `;
const [result] = await pool.query<ResultSetHeader>(query, [ const [result] = await pool.query<ResultSetHeader>(query, [
@@ -90,7 +91,6 @@ export async function POST(request: Request) {
parseFloat(Wieviel), parseFloat(Wieviel),
Wie, Wie,
TYP, TYP,
OK || 0,
]); ]);
return NextResponse.json({ return NextResponse.json({

View File

@@ -100,7 +100,7 @@ export default function Home() {
</div> </div>
{/* Footer */} {/* Footer */}
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4 border-t-2 border-black pt-4"> <footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4 ">
<div> <div>
<a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline"> <a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline">
mailto:rxf@gmx.de mailto:rxf@gmx.de

View File

@@ -21,7 +21,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
Wieviel: '', Wieviel: '',
Wie: defaultZahlungsart, Wie: defaultZahlungsart,
TYP: typ, TYP: typ,
OK: 0,
}); });
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -32,6 +31,10 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
const [month, setMonth] = useState(''); const [month, setMonth] = useState('');
const [year, setYear] = useState(''); const [year, setYear] = useState('');
const [isLoadingStats, setIsLoadingStats] = useState(false); const [isLoadingStats, setIsLoadingStats] = useState(false);
// Autocomplete data
const [autoCompleteWo, setAutoCompleteWo] = useState<string[]>([]);
const [autoCompleteWas, setAutoCompleteWas] = useState<string[]>([]);
const fetchStats = useCallback(async (y: string, m: string) => { const fetchStats = useCallback(async (y: string, m: string) => {
if (!y || !m) return; if (!y || !m) return;
@@ -50,6 +53,19 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
} }
}, [typ]); }, [typ]);
const fetchAutoComplete = useCallback(async () => {
try {
const response = await fetch(`/api/ausgaben/autocomplete?typ=${typ}`);
const data = await response.json();
if (data.success) {
setAutoCompleteWo(data.data.wo);
setAutoCompleteWas(data.data.was);
}
} catch (error) {
console.error('Error fetching autocomplete data:', error);
}
}, [typ]);
// Initialize month/year on first load // Initialize month/year on first load
useEffect(() => { useEffect(() => {
const now = new Date(); const now = new Date();
@@ -66,6 +82,11 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
} }
}, [month, year, typ, fetchStats]); }, [month, year, typ, fetchStats]);
// Fetch autocomplete data when typ changes
useEffect(() => {
fetchAutoComplete();
}, [typ, fetchAutoComplete]);
const handleMonthChange = (newMonth: string) => { const handleMonthChange = (newMonth: string) => {
setMonth(newMonth); setMonth(newMonth);
}; };
@@ -95,26 +116,41 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
Wieviel: selectedEntry.Wieviel.toString(), Wieviel: selectedEntry.Wieviel.toString(),
Wie: selectedEntry.Wie, Wie: selectedEntry.Wie,
TYP: selectedEntry.TYP, TYP: selectedEntry.TYP,
OK: selectedEntry.OK || 0,
}); });
setEditId(selectedEntry.ID); // Handle both uppercase and lowercase ID field names
const entryId = (selectedEntry as any).id || selectedEntry.ID;
setEditId(entryId);
} else { } else {
// Initialize with current date for new entry // Reset form for new entry
const now = new Date(); const now = new Date();
const dateStr = now.toISOString().split('T')[0]; const dateStr = now.toISOString().split('T')[0];
const weekday = getWeekday(now); const weekday = getWeekday(now);
setFormData(prev => ({ setFormData({
...prev,
Datum: dateStr, Datum: dateStr,
WochTag: weekday, WochTag: weekday,
Wo: '',
Was: '',
Wieviel: '',
Wie: defaultZahlungsart,
TYP: typ, TYP: typ,
})); });
setEditId(null); setEditId(null);
} }
}, [selectedEntry, typ]); }, [selectedEntry]);
// Update TYP when tab changes
useEffect(() => {
if (!selectedEntry) {
setFormData(prev => ({
...prev,
TYP: typ,
Wie: defaultZahlungsart,
}));
}
}, [typ]);
const getWeekday = (date: Date): string => { const getWeekday = (date: Date): string => {
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
@@ -143,12 +179,22 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
const url = editId ? `/api/ausgaben/${editId}` : '/api/ausgaben'; const url = editId ? `/api/ausgaben/${editId}` : '/api/ausgaben';
const method = editId ? 'PUT' : 'POST'; const method = editId ? 'PUT' : 'POST';
// Send only the fields we need, excluding any extra fields
const dataToSend = {
Datum: formData.Datum,
Wo: formData.Wo,
Was: formData.Was,
Wieviel: formData.Wieviel,
Wie: formData.Wie,
TYP: formData.TYP,
};
const response = await fetch(url, { const response = await fetch(url, {
method: method, method: method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(formData), body: JSON.stringify(dataToSend),
}); });
if (response.ok) { if (response.ok) {
@@ -180,7 +226,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
Wieviel: '', Wieviel: '',
Wie: defaultZahlungsart, Wie: defaultZahlungsart,
TYP: typ, TYP: typ,
OK: 0,
}); });
setEditId(null); setEditId(null);
@@ -224,8 +270,14 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
onChange={(e) => setFormData({ ...formData, Wo: e.target.value })} onChange={(e) => setFormData({ ...formData, Wo: e.target.value })}
className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none" className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
placeholder="Geschäft/Ort" placeholder="Geschäft/Ort"
list="wo-suggestions"
required required
/> />
<datalist id="wo-suggestions">
{autoCompleteWo.map((wo, index) => (
<option key={index} value={wo} />
))}
</datalist>
</td> </td>
<td className="p-2"> <td className="p-2">
<input <input
@@ -234,8 +286,14 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
onChange={(e) => setFormData({ ...formData, Was: e.target.value })} onChange={(e) => setFormData({ ...formData, Was: e.target.value })}
className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none" className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
placeholder="Beschreibung" placeholder="Beschreibung"
list="was-suggestions"
required required
/> />
<datalist id="was-suggestions">
{autoCompleteWas.map((was, index) => (
<option key={index} value={was} />
))}
</datalist>
</td> </td>
<td className="p-2 w-24"> <td className="p-2 w-24">
<input <input

View File

@@ -1,6 +1,6 @@
{ {
"name": "ausgaben_next", "name": "ausgaben_next",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3005", "dev": "next dev -p 3005",

View File

@@ -9,7 +9,6 @@ export interface AusgabenEntry {
Wieviel: number; Wieviel: number;
Wie: string; Wie: string;
TYP: number; TYP: number;
OK?: number;
} }
export interface CreateAusgabenEntry { export interface CreateAusgabenEntry {
@@ -20,7 +19,6 @@ export interface CreateAusgabenEntry {
Wieviel: string | number; Wieviel: string | number;
Wie: string; Wie: string;
TYP: number; TYP: number;
OK?: number;
} }
export interface MonthlyStats { export interface MonthlyStats {