feat: agent-11 PR3 – Scheduler + Erinnerungs-Mails + Notification-Prefs
Geplante Erinnerungen (Feature-Parität mit Urlaubsverwaltung): - APScheduler (AsyncIOScheduler) in der FastAPI-Lifespan; tägliche Jobs ab settings.reminder_hour. Redis-Tageslock gegen Doppelversand bei mehreren Prozessen; jeder Job mit eigener Session + RLS-Bypass. - Drei Jobs (auch einzeln aufrufbar): offene Anträge an Genehmiger, Resturlaub-Verfall-Vorwarnung an Mitarbeiter, fehlende AU an HR. - Pro-User notification_prefs (JSONB, opt-out); GET/PATCH /users/me/notification-prefs + ProfilePage-UI; Vertreter-Mail respektiert die Prefs. - Manueller Trigger POST /companies/me/run-reminders (Admin) – gleiche Logik, firmen-scoped (testbar ohne Warten). - Bugfix: GET-/PATCH-Urlaubskonto (update_balance) nutzte nicht existente Felder (base_days/carried_over_days/ip_address) → korrigiert auf entitled_days/carried_over/ip + company_id; available_days ergänzt. Migration 0037 (users.notification_prefs). 188/188 Tests grün. Deployed 137 + 164. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,8 @@ export function ProfilePage() {
|
||||
const [pinSuccess, setPinSuccess] = useState(false)
|
||||
const [pinError, setPinError] = useState<string | null>(null)
|
||||
|
||||
const [notifPrefs, setNotifPrefs] = useState<{ key: string; label: string; enabled: boolean }[]>([])
|
||||
|
||||
const loadMe = () => {
|
||||
api.get<UserOut>('/auth/me').then(u => {
|
||||
setMe(u)
|
||||
@@ -192,7 +194,21 @@ export function ProfilePage() {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => { loadMe() }, [])
|
||||
const loadNotifPrefs = () => {
|
||||
api.get<{ key: string; label: string; enabled: boolean }[]>('/users/me/notification-prefs')
|
||||
.then(setNotifPrefs).catch(() => {})
|
||||
}
|
||||
|
||||
async function toggleNotif(key: string, enabled: boolean) {
|
||||
setNotifPrefs(prev => prev.map(p => p.key === key ? { ...p, enabled } : p))
|
||||
try {
|
||||
const updated = await api.patch<{ key: string; label: string; enabled: boolean }[]>(
|
||||
'/users/me/notification-prefs', { prefs: { [key]: enabled } })
|
||||
setNotifPrefs(updated)
|
||||
} catch { loadNotifPrefs() }
|
||||
}
|
||||
|
||||
useEffect(() => { loadMe(); loadNotifPrefs() }, [])
|
||||
|
||||
async function changePassword(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
@@ -328,6 +344,24 @@ export function ProfilePage() {
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Benachrichtigungen */}
|
||||
{notifPrefs.length > 0 && (
|
||||
<div className='bg-white rounded-xl shadow-sm border border-gray-200 p-6'>
|
||||
<h2 className='font-semibold text-gray-700 mb-1'>E-Mail-Benachrichtigungen</h2>
|
||||
<p className='text-xs text-gray-400 mb-4'>Lege fest, worüber du per E-Mail informiert werden möchtest.</p>
|
||||
<div className='space-y-3'>
|
||||
{notifPrefs.map(p => (
|
||||
<label key={p.key} className='flex items-center justify-between gap-3 cursor-pointer'>
|
||||
<span className='text-sm text-gray-700'>{p.label}</span>
|
||||
<input type='checkbox' checked={p.enabled}
|
||||
onChange={e => toggleNotif(p.key, e.target.checked)}
|
||||
className='h-4 w-4 accent-blue-600' />
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user