This commit is contained in:
2026-02-27 16:14:40 +00:00
parent b44d5689bb
commit 14bb3fd2cd
5 changed files with 119 additions and 96 deletions

View File

@@ -9,29 +9,19 @@ Dies ist die modernisierte Version des alten PHP/jQuery-basierten Ausgaben-Progr
## Features ## Features
- **Zwei Tabs für verschiedene Ausgabenkategorien:** - **Zwei Tabs für verschiedene Ausgabenkategorien:**
- **Haushalt (TYP=0)**: Zahlungsarten EC-R, EC-B, bar-R, bar-B, Einnahme, Überweisung - **Haushalt (TYP=0)**: Zahlungsarten ECR, ECB, barR, barB, Ein(nahme), Uber(weisung)
- **Privat (TYP=1)**: Zahlungsarten bar, EC, VISA, Master, Einnahme, Überweisung - **Privat (TYP=1)**: Zahlungsarten bar, EC, VISA, MASTER, Einnahme, Uber(weisung)
- **Eingabe**: Erfassen von Ausgaben mit: - **Eingabeformular mit integrierten Features:**
- Datum (mit automatischem Wochentag) - Datum (mit automatischem Wochentag)
- Wo (Geschäft/Ort) - Wo (Geschäft/Ort)
- Was (Beschreibung) - Was (Beschreibung)
- Wieviel (Betrag in Euro) - Wieviel (Betrag in Euro)
- Wie (Zahlungsart - abhängig vom aktiven Tab) - Wie (Zahlungsart - abhängig vom aktiven Tab)
- Monatsstatistiken (TYP-spezifisch) - Monatliche Statistiken im Formular (Gesamtsumme, aufgeschlüsselt nach Zahlungsart)
- Letzte 10 Einträge des aktiven TYPs - Letzte 10 Einträge direkt unter dem Formular mit Bearbeiten/Löschen-Funktion
- Bearbeiten-Funktion: Klick auf Eintrag lädt ihn ins Formular
- **Listen-Ansicht**: Vollständige Auflistung aller Einträge mit: - Filterung nach aktivem TYP (Haushalt/Privat)
- Bearbeiten-Funktion
- Löschen-Funktion
- Sortierung nach Datum (absteigend)
- Filterung nach TYP (Haushalt/Privat)
- **Monatliche Statistiken**:
- Gesamtausgaben pro TYP
- Aufschlüsselung nach Zahlungsart
- Einnahmen
- Überweisungen
## Technologie-Stack ## Technologie-Stack

View File

@@ -13,6 +13,7 @@ export default function Home() {
const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null); const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null);
const version = packageJson.version; const version = packageJson.version;
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
useEffect(() => { useEffect(() => {
fetchRecentEntries(); fetchRecentEntries();
@@ -22,7 +23,7 @@ export default function Home() {
const fetchRecentEntries = async () => { const fetchRecentEntries = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`/api/ausgaben?limit=10&typ=${activeTab}`, { const response = await fetch(`/api/ausgaben?limit=20&typ=${activeTab}`, {
cache: 'no-store', cache: 'no-store',
headers: { headers: {
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
@@ -89,7 +90,7 @@ export default function Home() {
<AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} typ={activeTab} /> <AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} typ={activeTab} />
<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 10 Einträge</h3> <h3 className="text-xl font-semibold mb-4">Letzte 20 Einträge</h3>
{isLoading ? ( {isLoading ? (
<div className="text-center py-4">Lade Daten...</div> <div className="text-center py-4">Lade Daten...</div>
) : ( ) : (
@@ -106,7 +107,7 @@ export default function Home() {
</a> </a>
</div> </div>
<div className="text-right"> <div className="text-right">
Version {version} Version {version} - {buildDate}
</div> </div>
</footer> </footer>
</main> </main>

View File

@@ -264,17 +264,16 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
</select> </select>
</td> </td>
</tr> </tr>
<tr> </tbody>
<th className="p-2 w-32">{formData.WochTag}</th> </table>
<th className="p-2"></th>
<th className="p-2"></th> {/* Wochentag */}
<th className="p-2"></th> <div className="mt-3 text-left pl-3">
<th className="p-2"></th> <span className="font-semibold">{formData.WochTag}</span>
<th className="p-2"></th> </div>
</tr>
<tr> {/* Buttons */}
<td colSpan={6} className="p-3"> <div className="mb-3 flex gap-10 justify-center">
<div className="flex gap-3 justify-center">
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
@@ -290,11 +289,10 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
Löschen Löschen
</button> </button>
</div> </div>
</td>
</tr> {/* Monatsstatistiken */}
<tr> <div className="mt-6 pt-4 pb-6 -mb-6 border-t border-black -mx-6 px-6 bg-[#E0E0FF]">
<td colSpan={6} className="p-3 pt-6 border-t border-black"> <div className="flex items-center justify-between pt-1">
<div className="flex items-center justify-between">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<label className="font-semibold">Monat:</label> <label className="font-semibold">Monat:</label>
<select <select
@@ -337,10 +335,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
) : null} ) : null}
</div> </div>
</div> </div>
</td> </div>
</tr>
</tbody>
</table>
</form> </form>
</div> </div>
); );

View File

@@ -67,7 +67,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
</tr> </tr>
) : ( ) : (
entries.map((entry, index) => ( entries.map((entry, index) => (
<tr key={entry.ID} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-100'}> <tr key={entry.ID || `entry-idx-${index}`} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-100'}>
<td className="border-y border-black p-2 text-center"> <td className="border-y border-black p-2 text-center">
{formatDate(entry.Datum)} {formatDate(entry.Datum)}
</td> </td>

37
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,37 @@
# Docker Compose für Production Server mit Traefik
services:
ausgaben-app:
image: docker.citysensor.de/ausgaben-next:latest
container_name: ausgaben-next-app
restart: unless-stopped
expose:
- 3000
environment:
- NODE_ENV=production
- DB_HOST=${DB_HOST}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_NAME=${DB_NAME}
labels:
- traefik.enable=true
- traefik.http.routers.ausgaben.entrypoints=http
- traefik.http.routers.ausgaben.rule=Host(`ausgaben.fuerst-stuttgart.de`)
- traefik.http.middlewares.ausgaben-https-redirect.redirectscheme.scheme=https
- traefik.http.routers.ausgaben.middlewares=ausgaben-https-redirect
- traefik.http.routers.ausgaben-secure.entrypoints=https
- traefik.http.routers.ausgaben-secure.rule=Host(`ausgaben.fuerst-stuttgart.de`)
- traefik.http.routers.ausgaben-secure.tls=true
- traefik.http.routers.ausgaben-secure.tls.certresolver=letsencrypt
- traefik.http.routers.ausgaben-secure.service=ausgaben
- traefik.http.services.ausgaben.loadbalancer.server.port=3000
networks:
- proxy
- gitea-internal
networks:
proxy:
name: dockge_default
external: true
gitea-internal:
name: gitea_gitea-internal
external: true