?
This commit is contained in:
24
README.md
24
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -264,83 +264,78 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th className="p-2 w-32">{formData.WochTag}</th>
|
|
||||||
<th className="p-2"></th>
|
|
||||||
<th className="p-2"></th>
|
|
||||||
<th className="p-2"></th>
|
|
||||||
<th className="p-2"></th>
|
|
||||||
<th className="p-2"></th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="p-3">
|
|
||||||
<div className="flex gap-3 justify-center">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Speichere...' : editId ? 'Aktualisieren' : 'Speichern'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleReset}
|
|
||||||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="p-3 pt-6 border-t border-black">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<label className="font-semibold">Monat:</label>
|
|
||||||
<select
|
|
||||||
value={month}
|
|
||||||
onChange={(e) => handleMonthChange(e.target.value)}
|
|
||||||
className="border border-gray-400 rounded px-3 py-1"
|
|
||||||
>
|
|
||||||
<option value="01">Januar</option>
|
|
||||||
<option value="02">Februar</option>
|
|
||||||
<option value="03">März</option>
|
|
||||||
<option value="04">April</option>
|
|
||||||
<option value="05">Mai</option>
|
|
||||||
<option value="06">Juni</option>
|
|
||||||
<option value="07">Juli</option>
|
|
||||||
<option value="08">August</option>
|
|
||||||
<option value="09">September</option>
|
|
||||||
<option value="10">Oktober</option>
|
|
||||||
<option value="11">November</option>
|
|
||||||
<option value="12">Dezember</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label className="font-semibold">Jahr:</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={year}
|
|
||||||
onChange={(e) => handleYearChange(e.target.value)}
|
|
||||||
className="border border-gray-400 rounded px-3 py-1 w-24"
|
|
||||||
min="2013"
|
|
||||||
max="2099"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{isLoadingStats ? (
|
|
||||||
<span>Lade...</span>
|
|
||||||
) : stats ? (
|
|
||||||
<span className="font-bold text-lg">
|
|
||||||
Summe: {formatAmount(stats.totalAusgaben)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{/* Wochentag */}
|
||||||
|
<div className="mt-3 text-left pl-3">
|
||||||
|
<span className="font-semibold">{formData.WochTag}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="mb-3 flex gap-10 justify-center">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Speichere...' : editId ? 'Aktualisieren' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Monatsstatistiken */}
|
||||||
|
<div className="mt-6 pt-4 pb-6 -mb-6 border-t border-black -mx-6 px-6 bg-[#E0E0FF]">
|
||||||
|
<div className="flex items-center justify-between pt-1">
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<label className="font-semibold">Monat:</label>
|
||||||
|
<select
|
||||||
|
value={month}
|
||||||
|
onChange={(e) => handleMonthChange(e.target.value)}
|
||||||
|
className="border border-gray-400 rounded px-3 py-1"
|
||||||
|
>
|
||||||
|
<option value="01">Januar</option>
|
||||||
|
<option value="02">Februar</option>
|
||||||
|
<option value="03">März</option>
|
||||||
|
<option value="04">April</option>
|
||||||
|
<option value="05">Mai</option>
|
||||||
|
<option value="06">Juni</option>
|
||||||
|
<option value="07">Juli</option>
|
||||||
|
<option value="08">August</option>
|
||||||
|
<option value="09">September</option>
|
||||||
|
<option value="10">Oktober</option>
|
||||||
|
<option value="11">November</option>
|
||||||
|
<option value="12">Dezember</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label className="font-semibold">Jahr:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={year}
|
||||||
|
onChange={(e) => handleYearChange(e.target.value)}
|
||||||
|
className="border border-gray-400 rounded px-3 py-1 w-24"
|
||||||
|
min="2013"
|
||||||
|
max="2099"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{isLoadingStats ? (
|
||||||
|
<span>Lade...</span>
|
||||||
|
) : stats ? (
|
||||||
|
<span className="font-bold text-lg">
|
||||||
|
Summe: {formatAmount(stats.totalAusgaben)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
37
docker-compose.prod.yml
Normal 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
|
||||||
Reference in New Issue
Block a user