?
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
|
||||
|
||||
- **Zwei Tabs für verschiedene Ausgabenkategorien:**
|
||||
- **Haushalt (TYP=0)**: Zahlungsarten EC-R, EC-B, bar-R, bar-B, Einnahme, Überweisung
|
||||
- **Privat (TYP=1)**: Zahlungsarten bar, EC, VISA, Master, Einnahme, Überweisung
|
||||
- **Haushalt (TYP=0)**: Zahlungsarten ECR, ECB, barR, barB, Ein(nahme), Uber(weisung)
|
||||
- **Privat (TYP=1)**: Zahlungsarten bar, EC, VISA, MASTER, Einnahme, Uber(weisung)
|
||||
|
||||
- **Eingabe**: Erfassen von Ausgaben mit:
|
||||
- **Eingabeformular mit integrierten Features:**
|
||||
- Datum (mit automatischem Wochentag)
|
||||
- Wo (Geschäft/Ort)
|
||||
- Was (Beschreibung)
|
||||
- Wieviel (Betrag in Euro)
|
||||
- Wie (Zahlungsart - abhängig vom aktiven Tab)
|
||||
- Monatsstatistiken (TYP-spezifisch)
|
||||
- Letzte 10 Einträge des aktiven TYPs
|
||||
|
||||
- **Listen-Ansicht**: Vollständige Auflistung aller Einträge mit:
|
||||
- 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
|
||||
- Monatliche Statistiken im Formular (Gesamtsumme, aufgeschlüsselt nach Zahlungsart)
|
||||
- Letzte 10 Einträge direkt unter dem Formular mit Bearbeiten/Löschen-Funktion
|
||||
- Bearbeiten-Funktion: Klick auf Eintrag lädt ihn ins Formular
|
||||
- Filterung nach aktivem TYP (Haushalt/Privat)
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function Home() {
|
||||
const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null);
|
||||
|
||||
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(() => {
|
||||
fetchRecentEntries();
|
||||
@@ -22,7 +23,7 @@ export default function Home() {
|
||||
const fetchRecentEntries = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/ausgaben?limit=10&typ=${activeTab}`, {
|
||||
const response = await fetch(`/api/ausgaben?limit=20&typ=${activeTab}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
@@ -89,7 +90,7 @@ export default function Home() {
|
||||
<AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} typ={activeTab} />
|
||||
|
||||
<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 ? (
|
||||
<div className="text-center py-4">Lade Daten...</div>
|
||||
) : (
|
||||
@@ -106,7 +107,7 @@ export default function Home() {
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
Version {version}
|
||||
Version {version} - {buildDate}
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
@@ -264,83 +264,78 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
</select>
|
||||
</td>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
</tr>
|
||||
) : (
|
||||
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">
|
||||
{formatDate(entry.Datum)}
|
||||
</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