v1.3: Serverseitige Datenbank (PostgreSQL + Node-API) ersetzt localStorage
- server/: Express-API (Auth, Bootstrap, Tickets, Threads, Benutzerverwaltung, Codes) mit pg-Schema (11 Tabellen), idempotem Seed (50 Wohnungen, 552 Buchungen) - mp-db (PostgreSQL 16, Volume mp_pgdata) + mp-api im Compose-Stack, nginx proxiet /api/ an mp-api; deploy.sh baut API-Image auf dem Server - Frontend: Store laedt /api/bootstrap, Schreibzugriff async, kein localStorage mehr (ausser Theme), Ladebildschirm beim Session-Check, vite-Proxy fuer Dev - Rollenrechte serverseitig: Mieter nur eigene Wohnung (getestet, 403-Verweis) - Mail-Benachrichtigungen komplett serverseitig (server/src/mail.js) - beispieldaten.py schreibt server/seed/generated.json; Verwaltungs-Mail = axel@hocks.eu - E2E live verifiziert: Login, Scoping, Ticket + Zustellung (status=sent), Registrierung, Codes; Testdaten bereinigt; Doku nachgefuehrt
This commit is contained in:
+16
-43
@@ -13,12 +13,6 @@ import {
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
sendeBenachrichtigung,
|
||||
verwaltungsAdressen,
|
||||
nachrichtenText,
|
||||
einheitLabel,
|
||||
} from '../lib/mail'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -52,45 +46,26 @@ const categories = [
|
||||
]
|
||||
|
||||
function NewTicketDialog() {
|
||||
const { dispatch, role, me, meineEinheit, benutzer, units } = useStore()
|
||||
const { addTicket, role, meineEinheit } = useStore()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
const [category, setCategory] = useState('Sanitär')
|
||||
const [priority, setPriority] = useState<TicketPriority>('mittel')
|
||||
const [description, setDescription] = useState('')
|
||||
const [sende, setSende] = useState(false)
|
||||
|
||||
const submit = () => {
|
||||
if (!title.trim()) return
|
||||
const unitId = role === 'tenant' ? meineEinheit.id : 'hs-1'
|
||||
const einheit = units.find((u) => u.id === unitId) ?? meineEinheit
|
||||
const mieterName = role === 'tenant' ? (me?.name ?? 'Mieter') : 'Hinterstein Immobilien (intern)'
|
||||
dispatch({
|
||||
type: 'addTicket',
|
||||
ticket: {
|
||||
id: `t-${Date.now()}`,
|
||||
unitId,
|
||||
tenant: mieterName,
|
||||
title: title.trim(),
|
||||
description: description.trim() || '–',
|
||||
category,
|
||||
priority,
|
||||
status: 'offen',
|
||||
createdAt: new Date().toLocaleDateString('de-DE'),
|
||||
},
|
||||
const submit = async () => {
|
||||
if (!title.trim() || sende) return
|
||||
setSende(true)
|
||||
const err = await addTicket({
|
||||
title: title.trim(),
|
||||
description: description.trim() || '–',
|
||||
category,
|
||||
priority,
|
||||
unitId: role === 'tenant' ? meineEinheit.id : 'hs-1',
|
||||
})
|
||||
// Verwaltung per E-Mail benachrichtigen (fire-and-forget)
|
||||
sendeBenachrichtigung(
|
||||
verwaltungsAdressen(benutzer),
|
||||
`Neue Schadensmeldung: ${title.trim()} (${einheitLabel(einheit)})`,
|
||||
nachrichtenText(
|
||||
`Im Mieterportal wurde eine neue Schadensmeldung erstellt.\n\n` +
|
||||
`Wohnung: ${einheitLabel(einheit)}\n` +
|
||||
`Meldung von: ${mieterName}\n` +
|
||||
`Kategorie: ${category}\n` +
|
||||
`Dringlichkeit: ${priority}\n\n` +
|
||||
`${title.trim()}\n${description.trim() || '–'}`,
|
||||
),
|
||||
)
|
||||
setSende(false)
|
||||
if (err) return
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
setOpen(false)
|
||||
@@ -158,7 +133,7 @@ function NewTicketDialog() {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
|
||||
<Button onClick={submit} disabled={!title.trim()}>Meldung absenden</Button>
|
||||
<Button onClick={submit} disabled={!title.trim() || sende}>{sende ? 'Wird gesendet …' : 'Meldung absenden'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -166,7 +141,7 @@ function NewTicketDialog() {
|
||||
}
|
||||
|
||||
export default function Tickets() {
|
||||
const { tickets, units, role, dispatch, meineEinheit } = useStore()
|
||||
const { tickets, units, role, setTicketStatus, meineEinheit } = useStore()
|
||||
const [filter, setFilter] = useState<'alle' | TicketStatus>('alle')
|
||||
|
||||
const visible = useMemo(
|
||||
@@ -247,9 +222,7 @@ export default function Tickets() {
|
||||
{role === 'admin' ? (
|
||||
<Select
|
||||
value={t.status}
|
||||
onValueChange={(v) =>
|
||||
dispatch({ type: 'setTicketStatus', id: t.id, status: v as TicketStatus })
|
||||
}
|
||||
onValueChange={(v) => setTicketStatus(t.id, v as TicketStatus)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-40">
|
||||
<SelectValue />
|
||||
|
||||
Reference in New Issue
Block a user