feat: Stunden-Auszahlung Feature (/hr/payouts)

- Backend: Model HoursPayout, Schema, Router GET/POST/DELETE
- GET /hr/payouts: HR/Admin sehen alle, Employee/Manager nur eigene
- POST /hr/payouts: reduziert OvertimeBalance.taken_hours sofort
- DELETE /hr/payouts/{id}: storniert und bucht Stunden zurück
- AuditLog-Einträge bei Anlegen und Stornieren
- Migration 0030: hours_payouts Tabelle
- Frontend: /hr/payouts Seite (lila, 💸) mit Filter, Tabelle, Modal
- Modal zeigt verfügbares Überstundenguthaben + Warnung bei Überziehung
- Navigation: Stunden-Auszahlung (HR/COMPANY_ADMIN/SUPER_ADMIN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 22:17:52 +02:00
parent e83a3fbbdd
commit a63b0e835f
11 changed files with 956 additions and 0 deletions
@@ -0,0 +1,36 @@
"""hours_payouts table
Revision ID: 0030
Revises: 0029
Create Date: 2026-05-25
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision = '0030'
down_revision = '0029'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'hours_payouts',
sa.Column('id', UUID(as_uuid=True), primary_key=True),
sa.Column('company_id', UUID(as_uuid=True), sa.ForeignKey('companies.id', ondelete='CASCADE'), nullable=False),
sa.Column('user_id', UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
sa.Column('hours', sa.Numeric(6, 2), nullable=False),
sa.Column('period_year', sa.Integer(), nullable=True),
sa.Column('period_month', sa.Integer(), nullable=True),
sa.Column('note', sa.Text(), nullable=True),
sa.Column('created_by', UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
)
op.create_index('ix_hours_payouts_company_id', 'hours_payouts', ['company_id'])
op.create_index('ix_hours_payouts_user_id', 'hours_payouts', ['user_id'])
op.create_index('ix_hours_payouts_created_at', 'hours_payouts', ['created_at'])
def downgrade():
op.drop_table('hours_payouts')