Compare commits
5 Commits
bb35aa28c9
...
phpmyadmin
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aa42c9952 | |||
| e5868d904a | |||
| dff78027b6 | |||
| cd96bdfc6a | |||
| 4acb1a5eca |
+5
-2
@@ -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>
|
||||||
|
|||||||
+51
-49
@@ -204,8 +204,10 @@ 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.
|
||||||
@@ -213,72 +215,57 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<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>
|
|
||||||
<th className="p-2">Was</th>
|
|
||||||
<th className="p-2 w-12">Kat.</th>
|
|
||||||
<th className="p-2 w-24">Wieviel</th>
|
|
||||||
<th className="p-2 w-4"></th>
|
|
||||||
<th className="p-2 w-38 text-left">Wie</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td className="p-2 w-32">
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.Datum}
|
value={formData.Datum}
|
||||||
onChange={(e) => handleDateChange(e.target.value)}
|
onChange={(e) => handleDateChange(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
|
required
|
||||||
/>
|
/>
|
||||||
</td>
|
</div>
|
||||||
<td className="p-2">
|
|
||||||
|
<div className="flex flex-col gap-1 col-span-2 sm:col-span-2 md:col-span-1">
|
||||||
|
<label className="text-xs font-medium text-center">Wo</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.Wo}
|
value={formData.Wo}
|
||||||
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={fieldInputClass}
|
||||||
placeholder="Geschäft/Ort"
|
placeholder="Geschäft/Ort"
|
||||||
list="wo-suggestions"
|
list="wo-suggestions"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<datalist id="wo-suggestions">
|
</div>
|
||||||
{autoCompleteWo.map((wo, index) => (
|
|
||||||
<option key={index} value={wo} />
|
<div className="flex flex-col gap-1 col-span-2 sm:col-span-2 md:col-span-1">
|
||||||
))}
|
<label className="text-xs font-medium text-center">Was</label>
|
||||||
</datalist>
|
|
||||||
</td>
|
|
||||||
<td className="p-2">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.Was}
|
value={formData.Was}
|
||||||
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={fieldInputClass}
|
||||||
placeholder="Beschreibung"
|
placeholder="Beschreibung"
|
||||||
list="was-suggestions"
|
list="was-suggestions"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<datalist id="was-suggestions">
|
</div>
|
||||||
{autoCompleteWas.map((was, index) => (
|
|
||||||
<option key={index} value={was} />
|
<div className="flex flex-col gap-1">
|
||||||
))}
|
<label className="text-xs font-medium text-center">Kat.</label>
|
||||||
</datalist>
|
|
||||||
</td>
|
|
||||||
<td className="p-2 w-12">
|
|
||||||
<div ref={katDropdownRef} className="relative w-full">
|
<div ref={katDropdownRef} className="relative w-full">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setKatDropdownOpen((o) => !o)}
|
onClick={() => setKatDropdownOpen((o) => !o)}
|
||||||
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"
|
className={`${fieldInputClass} text-left`}
|
||||||
>
|
>
|
||||||
{formData.Kat}
|
{formData.Kat}
|
||||||
</button>
|
</button>
|
||||||
{katDropdownOpen && (
|
{katDropdownOpen && (
|
||||||
<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">
|
<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) => (
|
{categories.map((cat) => (
|
||||||
<li
|
<li
|
||||||
key={cat.value}
|
key={cat.value}
|
||||||
@@ -296,8 +283,13 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td className="p-2 w-24">
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs font-medium text-center">
|
||||||
|
Wieviel
|
||||||
|
<span className="text-[10px] font-normal"> (€)</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
@@ -306,17 +298,18 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
const val = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.');
|
const val = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.');
|
||||||
setFormData({ ...formData, Wieviel: val });
|
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"
|
className={fieldInputClass}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</td>
|
</div>
|
||||||
<td className="p-2 w-4 text-left">€</td>
|
|
||||||
<td className="p-2 w-38">
|
<div className="flex flex-col gap-1 col-span-2 sm:col-span-2 md:col-span-1">
|
||||||
|
<label className="text-xs font-medium text-center">Wie</label>
|
||||||
<select
|
<select
|
||||||
value={formData.Wie}
|
value={formData.Wie}
|
||||||
onChange={(e) => setFormData({ ...formData, Wie: e.target.value })}
|
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"
|
className={fieldInputClass}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
{zahlungsarten.map((za) => (
|
{zahlungsarten.map((za) => (
|
||||||
@@ -325,10 +318,19 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
<datalist id="wo-suggestions">
|
||||||
|
{autoCompleteWo.map((wo, index) => (
|
||||||
|
<option key={index} value={wo} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
<datalist id="was-suggestions">
|
||||||
|
{autoCompleteWas.map((was, index) => (
|
||||||
|
<option key={index} value={was} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
|
||||||
{/* 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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user