Hocks Mieterportal: 5 Module (Dashboard, Mietkonto, Schadensmeldungen, Dokumente, Nachrichten), Docker-Setup, eigenes Branding

This commit is contained in:
2026-10-02 10:02:19 +02:00
commit 38d5099a2c
91 changed files with 17062 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
import {
createContext,
useContext,
useEffect,
useMemo,
useReducer,
type ReactNode,
} from 'react'
import type { Role, Ticket, Transaction, DocumentItem, Thread, Message } from '../types'
import {
units,
transactions,
tickets,
documents,
threads,
announcements,
DEMO_TENANT,
DEMO_UNIT_ID,
} from '../data/seed'
interface State {
role: Role
tickets: Ticket[]
transactions: Transaction[]
documents: DocumentItem[]
threads: Thread[]
}
type Action =
| { type: 'setRole'; role: Role }
| { type: 'addTicket'; ticket: Ticket }
| { type: 'setTicketStatus'; id: string; status: Ticket['status'] }
| {
type: 'addMessage'
threadId: string
message: Message
}
| { type: 'newThread'; thread: Thread }
| { type: 'markThreadRead'; threadId: string; role: Role }
const initialState: State = {
role: 'tenant',
tickets,
transactions,
documents,
threads,
}
const STORAGE_KEY = 'hocks-mieterportal-v1'
function loadState(): State {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) return { ...initialState, ...JSON.parse(raw) }
} catch {
/* ignore */
}
return initialState
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'setRole':
return { ...state, role: action.role }
case 'addTicket':
return { ...state, tickets: [action.ticket, ...state.tickets] }
case 'setTicketStatus':
return {
...state,
tickets: state.tickets.map((t) =>
t.id === action.id ? { ...t, status: action.status } : t,
),
}
case 'addMessage':
return {
...state,
threads: state.threads.map((th) =>
th.id === action.threadId
? {
...th,
unreadByAdmin: action.message.from === 'tenant',
unreadByTenant: action.message.from === 'admin',
messages: [...th.messages, action.message],
}
: th,
),
}
case 'newThread':
return { ...state, threads: [action.thread, ...state.threads] }
case 'markThreadRead':
return {
...state,
threads: state.threads.map((th) =>
th.id === action.threadId
? action.role === 'tenant'
? { ...th, unreadByTenant: false }
: { ...th, unreadByAdmin: false }
: th,
),
}
default:
return state
}
}
interface Store extends State {
dispatch: React.Dispatch<Action>
demoUnit: typeof units[number]
units: typeof units
announcements: typeof announcements
}
const StoreContext = createContext<Store | null>(null)
export function StoreProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, undefined, loadState)
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
}, [state])
const value = useMemo<Store>(
() => ({
...state,
dispatch,
demoUnit: units.find((u) => u.id === DEMO_UNIT_ID)!,
units,
announcements,
}),
[state],
)
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>
}
export function useStore(): Store {
const ctx = useContext(StoreContext)
if (!ctx) throw new Error('useStore außerhalb von StoreProvider')
return ctx
}
export const fmtEUR = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
})
export { DEMO_TENANT, DEMO_UNIT_ID }