Compare commits
5 Commits
14bb3fd2cd
...
auth
| Author | SHA1 | Date | |
|---|---|---|---|
| 0678fdcaa7 | |||
| 36f352de58 | |||
| 734dbfe24b | |||
| ba7082897f | |||
| 5981a7a6db |
10
.env.example
10
.env.example
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
51
app/api/ausgaben/autocomplete/route.ts
Normal file
51
app/api/ausgaben/autocomplete/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user