Compare commits

5 Commits

Author SHA1 Message Date
admin 7aa42c9952 Add phpMyAdmin service to local docker-compose
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:45:13 +02:00
admin e5868d904a Merge branch 'responsive-mobile-fixes'
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 07:53:59 +02:00
admin dff78027b6 V 2.3.1 Formular und Liste responsiv gestaltet
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 07:53:51 +02:00
admin cd96bdfc6a Formular und Liste responsiv gestaltet
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 18:41:32 +02:00
admin 4acb1a5eca Version auf 2.3.0 erhöht
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 13:59:30 +02:00
8 changed files with 190 additions and 132 deletions
+5 -2
View File
@@ -8,6 +8,7 @@ import TabLayout from '@/components/TabLayout';
import { AusgabenEntry } from '@/types/ausgaben'; import { AusgabenEntry } from '@/types/ausgaben';
const MAX_ENTRIES = 15; const MAX_ENTRIES = 15;
const MOBILE_ENTRIES = 5;
export default function Home() { export default function Home() {
const [activeTab, setActiveTab] = useState(0); // 0 = Haushalt, 1 = Privat const [activeTab, setActiveTab] = useState(0); // 0 = Haushalt, 1 = Privat
@@ -69,11 +70,13 @@ export default function Home() {
<MonatsStatistik typ={activeTab} refreshKey={statsRefreshKey} /> <MonatsStatistik typ={activeTab} refreshKey={statsRefreshKey} />
<div className="mt-6 bg-white border border-black rounded-lg shadow-md p-6"> <div className="mt-6 bg-white border border-black rounded-lg shadow-md p-6">
<h3 className="text-xl font-semibold mb-4">Letzte {MAX_ENTRIES} Einträge</h3> <h3 className="text-xl font-semibold mb-4">
Letzte <span className="md:hidden">{MOBILE_ENTRIES}</span><span className="hidden md:inline">{MAX_ENTRIES}</span> Einträge
</h3>
{isLoading ? ( {isLoading ? (
<div className="text-center py-4">Lade Daten...</div> <div className="text-center py-4">Lade Daten...</div>
) : ( ) : (
<AusgabenList entries={entries} onDelete={handleDelete} onEdit={handleEdit} /> <AusgabenList entries={entries} onDelete={handleDelete} onEdit={handleEdit} mobileLimit={MOBILE_ENTRIES} />
)} )}
</div> </div>
</div> </div>
+120 -118
View File
@@ -204,131 +204,133 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
setEditId(null); setEditId(null);
}; };
const fieldInputClass = "w-full min-w-0 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none";
return ( return (
<div className="bg-[#CCCCFF] border border-black p-6 rounded-lg mb-6"> <div className="bg-[#CCCCFF] border border-black p-3 sm:p-6 rounded-lg mb-6">
{editId && ( {editId && (
<div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800"> <div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800">
<strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag. <strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag.
</div> </div>
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<table className="w-full text-center"> {/* Wo/Was sind auf Mobil/Tablet volle Breite (mehr Platz zum Tippen), am Desktop feste Spaltenbreiten wie zuvor im Tabellen-Layout. */}
<thead> <div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-[110px_1fr_1fr_70px_100px_140px] gap-3 sm:gap-2">
<tr > <div className="flex flex-col gap-1">
<th className="p-2 w-32">Datum</th> <label className="text-xs font-medium text-center">Datum</label>
<th className="p-2">Wo</th> <input
<th className="p-2">Was</th> type="date"
<th className="p-2 w-12">Kat.</th> value={formData.Datum}
<th className="p-2 w-24">Wieviel</th> onChange={(e) => handleDateChange(e.target.value)}
<th className="p-2 w-4"></th> className={fieldInputClass}
<th className="p-2 w-38 text-left">Wie</th> required
</tr> />
</thead> </div>
<tbody>
<tr> <div className="flex flex-col gap-1 col-span-2 sm:col-span-2 md:col-span-1">
<td className="p-2 w-32"> <label className="text-xs font-medium text-center">Wo</label>
<input <input
type="date" type="text"
value={formData.Datum} value={formData.Wo}
onChange={(e) => handleDateChange(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={fieldInputClass}
required placeholder="Geschäft/Ort"
/> list="wo-suggestions"
</td> required
<td className="p-2"> />
<input </div>
type="text"
value={formData.Wo} <div className="flex flex-col gap-1 col-span-2 sm:col-span-2 md:col-span-1">
onChange={(e) => setFormData({ ...formData, Wo: e.target.value })} <label className="text-xs font-medium text-center">Was</label>
className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none" <input
placeholder="Geschäft/Ort" type="text"
list="wo-suggestions" value={formData.Was}
required onChange={(e) => setFormData({ ...formData, Was: e.target.value })}
/> className={fieldInputClass}
<datalist id="wo-suggestions"> placeholder="Beschreibung"
{autoCompleteWo.map((wo, index) => ( list="was-suggestions"
<option key={index} value={wo} /> required
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">Kat.</label>
<div ref={katDropdownRef} className="relative w-full">
<button
type="button"
onClick={() => setKatDropdownOpen((o) => !o)}
className={`${fieldInputClass} text-left`}
>
{formData.Kat}
</button>
{katDropdownOpen && (
<ul className="absolute z-50 left-0 mt-1 w-48 max-w-[calc(100vw-2rem)] bg-white border-2 border-gray-400 rounded shadow-lg max-h-60 overflow-y-auto text-left">
{categories.map((cat) => (
<li
key={cat.value}
className={`px-3 py-1 cursor-pointer hover:bg-blue-100 text-sm ${
formData.Kat === cat.value ? 'bg-blue-50 font-semibold' : ''
}`}
onMouseDown={() => {
setFormData({ ...formData, Kat: cat.value });
setKatDropdownOpen(false);
}}
>
{cat.value} - {cat.label}
</li>
))} ))}
</datalist> </ul>
</td> )}
<td className="p-2"> </div>
<input </div>
type="text"
value={formData.Was} <div className="flex flex-col gap-1">
onChange={(e) => setFormData({ ...formData, Was: e.target.value })} <label className="text-xs font-medium text-center">
className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none" Wieviel
placeholder="Beschreibung" <span className="text-[10px] font-normal"> ()</span>
list="was-suggestions" </label>
required <input
/> type="text"
<datalist id="was-suggestions"> inputMode="decimal"
{autoCompleteWas.map((was, index) => ( value={formData.Wieviel}
<option key={index} value={was} /> onChange={(e) => {
))} const val = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.');
</datalist> setFormData({ ...formData, Wieviel: val });
</td> }}
<td className="p-2 w-12"> className={fieldInputClass}
<div ref={katDropdownRef} className="relative w-full"> placeholder="0.00"
<button required
type="button" />
onClick={() => setKatDropdownOpen((o) => !o)} </div>
className="w-full px-2 py-1 text-base text-left rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
> <div className="flex flex-col gap-1 col-span-2 sm:col-span-2 md:col-span-1">
{formData.Kat} <label className="text-xs font-medium text-center">Wie</label>
</button> <select
{katDropdownOpen && ( value={formData.Wie}
<ul className="absolute z-50 left-0 mt-1 w-48 bg-white border-2 border-gray-400 rounded shadow-lg max-h-60 overflow-y-auto text-left"> onChange={(e) => setFormData({ ...formData, Wie: e.target.value })}
{categories.map((cat) => ( className={fieldInputClass}
<li required
key={cat.value} >
className={`px-3 py-1 cursor-pointer hover:bg-blue-100 text-sm ${ {zahlungsarten.map((za) => (
formData.Kat === cat.value ? 'bg-blue-50 font-semibold' : '' <option key={za.value} value={za.value}>
}`} {za.label}
onMouseDown={() => { </option>
setFormData({ ...formData, Kat: cat.value }); ))}
setKatDropdownOpen(false); </select>
}} </div>
> </div>
{cat.value} - {cat.label}
</li> <datalist id="wo-suggestions">
))} {autoCompleteWo.map((wo, index) => (
</ul> <option key={index} value={wo} />
)} ))}
</div> </datalist>
</td> <datalist id="was-suggestions">
<td className="p-2 w-24"> {autoCompleteWas.map((was, index) => (
<input <option key={index} value={was} />
type="text" ))}
inputMode="decimal" </datalist>
value={formData.Wieviel}
onChange={(e) => {
const val = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.');
setFormData({ ...formData, Wieviel: val });
}}
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="0.00"
required
/>
</td>
<td className="p-2 w-4 text-left"></td>
<td className="p-2 w-38">
<select
value={formData.Wie}
onChange={(e) => setFormData({ ...formData, Wie: 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"
required
>
{zahlungsarten.map((za) => (
<option key={za.value} value={za.value}>
{za.label}
</option>
))}
</select>
</td>
</tr>
</tbody>
</table>
{/* Wochentag */} {/* Wochentag */}
<div className="mt-3 text-left pl-3"> <div className="mt-3 text-left pl-3">
@@ -336,7 +338,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
</div> </div>
{/* Buttons */} {/* Buttons */}
<div className="mb-3 flex gap-10 justify-center"> <div className="mb-3 flex gap-4 sm:gap-10 justify-center">
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
+43 -3
View File
@@ -7,9 +7,10 @@ interface AusgabenListProps {
entries: AusgabenEntry[]; entries: AusgabenEntry[];
onDelete: (id: number) => void; onDelete: (id: number) => void;
onEdit: (entry: AusgabenEntry) => void; onEdit: (entry: AusgabenEntry) => void;
mobileLimit?: number;
} }
export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenListProps) { export default function AusgabenList({ entries, onDelete, onEdit, mobileLimit }: AusgabenListProps) {
const [confirmId, setConfirmId] = useState<number | null>(null); const [confirmId, setConfirmId] = useState<number | null>(null);
const handleDeleteConfirmed = async (id: number) => { const handleDeleteConfirmed = async (id: number) => {
@@ -39,7 +40,8 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
}; };
return ( return (
<div className="overflow-x-auto"> <div>
<div className="hidden md:block overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="bg-[#CCCCFF]"> <tr className="bg-[#CCCCFF]">
@@ -93,11 +95,49 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
)} )}
</tbody> </tbody>
</table> </table>
</div>
<div className="md:hidden flex flex-col gap-2">
{entries.length === 0 ? (
<div className="text-center py-4 text-gray-500">Keine Einträge vorhanden</div>
) : (
(mobileLimit ? entries.slice(0, mobileLimit) : entries).map((entry, index) => (
<div
key={entry.ID || `entry-idx-${index}`}
className={`border border-gray-300 rounded-lg p-2 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}
>
<div className="flex justify-between items-center text-sm font-medium">
<span>{formatDate(entry.Datum)} ({entry.WochTag.slice(0, 2)}) · {formatAmount(entry.Wieviel)}</span>
<div className="flex gap-2">
<button
onClick={() => onEdit(entry)}
className="text-blue-600 hover:text-blue-800 text-xs"
>
Editieren
</button>
<button
onClick={() => setConfirmId(entry.ID)}
className="text-red-600 hover:text-red-800 text-xs"
>
Löschen
</button>
</div>
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-gray-700 mt-1">
<span>Wo: {entry.Wo}</span>
<span>Was: {entry.Was}</span>
<span>Kat: {entry.Kat}</span>
<span>Wie: {entry.Wie}</span>
</div>
</div>
))
)}
</div>
{/* Bestätigungs-Modal */} {/* Bestätigungs-Modal */}
{confirmId !== null && ( {confirmId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white border-2 border-black rounded-lg shadow-xl p-6 w-80"> <div className="bg-white border-2 border-black rounded-lg shadow-xl p-6 w-80 max-w-[calc(100vw-2rem)] mx-4">
<p className="text-lg font-semibold mb-6 text-center">Eintrag wirklich löschen?</p> <p className="text-lg font-semibold mb-6 text-center">Eintrag wirklich löschen?</p>
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<button <button
+3 -3
View File
@@ -59,10 +59,10 @@ export default function MonatsStatistik({ typ, refreshKey }: MonatsStatistikProp
}; };
return ( return (
<div className="mt-4 bg-[#E0E0FF] border border-black rounded-lg shadow-md p-4"> <div className="mt-4 bg-[#E0E0FF] border border-black rounded-lg shadow-md p-3 sm:p-4">
{/* Zeile 1: Monat/Jahr + Gesamtsumme */} {/* Zeile 1: Monat/Jahr + Gesamtsumme */}
<div className="flex items-center justify-between flex-wrap gap-2"> <div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex gap-4 items-center"> <div className="flex flex-wrap gap-x-4 gap-y-2 items-center">
<label className="font-semibold">Monat:</label> <label className="font-semibold">Monat:</label>
<select <select
value={month} value={month}
@@ -88,7 +88,7 @@ export default function MonatsStatistik({ typ, refreshKey }: MonatsStatistikProp
type="number" type="number"
value={year} value={year}
onChange={(e) => setYear(e.target.value)} onChange={(e) => setYear(e.target.value)}
className="border border-gray-400 rounded px-3 py-1 w-24" className="border border-gray-400 rounded px-3 py-1 w-20"
min="2013" min="2013"
max="2099" max="2099"
/> />
+3 -3
View File
@@ -26,9 +26,9 @@ export default function TabLayout({ children, activeTab, onTabChange }: TabLayou
new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
return ( return (
<div className="min-h-screen py-8 px-4"> <div className="min-h-screen py-8 px-2 sm:px-4">
{/* Outer wrapper with border */} {/* Outer wrapper with border */}
<div className="max-w-316 mx-auto border-2 border-black rounded-xl bg-gray-200 p-6"> <div className="max-w-316 mx-auto border-2 border-black rounded-xl bg-gray-200 p-3 sm:p-6">
{/* Page title */} {/* Page title */}
<h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Ausgaben - Log</h1> <h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Ausgaben - Log</h1>
@@ -58,7 +58,7 @@ export default function TabLayout({ children, activeTab, onTabChange }: TabLayou
</div> </div>
{/* Content panel */} {/* Content panel */}
<main className="border-2 border-black rounded-b-lg rounded-tr-lg p-6 bg-[#FFFFDD]"> <main className="border-2 border-black rounded-b-lg rounded-tr-lg p-3 sm:p-6 bg-[#FFFFDD]">
{children} {children}
</main> </main>
+13
View File
@@ -15,3 +15,16 @@ services:
- DB_USER=${DB_USER} - DB_USER=${DB_USER}
- DB_PASS=${DB_PASS} - DB_PASS=${DB_PASS}
- DB_NAME=${DB_NAME} - DB_NAME=${DB_NAME}
phpmyadmin:
image: phpmyadmin:latest
container_name: ausgaben-next-phpmyadmin
restart: unless-stopped
ports:
- "0.0.0.0:3006:80"
environment:
- PMA_HOST=${DB_HOST}
- PMA_PORT=3306
- PMA_USER=${DB_USER}
- PMA_PASSWORD=${DB_PASS}
- UPLOAD_LIMIT=256M
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ausgaben_next", "name": "ausgaben_next",
"version": "2.2.1", "version": "2.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ausgaben_next", "name": "ausgaben_next",
"version": "2.2.1", "version": "2.3.1",
"dependencies": { "dependencies": {
"mysql2": "^3.17.4", "mysql2": "^3.17.4",
"next": "16.1.6", "next": "16.1.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ausgaben_next", "name": "ausgaben_next",
"version": "2.2.1", "version": "2.3.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3005", "dev": "next dev -p 3005",