BEGIN; ALTER TABLE decision ADD COLUMN IF NOT EXISTS voting_open BOOLEAN NOT NULL DEFAULT FALSE, ADD COLUMN IF NOT EXISTS voting_started_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS voting_started_by BIGINT REFERENCES app_user(id), ADD COLUMN IF NOT EXISTS voting_closed_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS voting_closed_by BIGINT REFERENCES app_user(id); CREATE TABLE IF NOT EXISTS decision_vote ( decision_id BIGINT NOT NULL REFERENCES decision(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, vote_value VARCHAR(20) NOT NULL CHECK (vote_value IN ('yes', 'no', 'abstain')), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (decision_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_decision_vote_decision_id ON decision_vote(decision_id); CREATE INDEX IF NOT EXISTS idx_decision_vote_user_id ON decision_vote(user_id); DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_decision_vote_updated_at') THEN CREATE TRIGGER trg_decision_vote_updated_at BEFORE UPDATE ON decision_vote FOR EACH ROW EXECUTE FUNCTION set_updated_at(); END IF; END $$; ALTER TABLE decision_vote ENABLE ROW LEVEL SECURITY; ALTER TABLE decision_vote FORCE ROW LEVEL SECURITY; -- Benutzer desselben Gremiums dürfen sich gegenseitig für Abstimmungslisten sehen. -- Admin/chair behalten den bisherigen erweiterten Zugriff; jeder Benutzer sieht sich weiterhin selbst. DROP POLICY IF EXISTS user_select ON app_user; CREATE POLICY user_select ON app_user FOR SELECT USING ( app_role() IN ('admin','chair') OR id = app_user_id() OR EXISTS ( SELECT 1 FROM committee_member mine JOIN committee_member other_member ON other_member.committee_id = mine.committee_id WHERE mine.user_id = app_user_id() AND other_member.user_id = app_user.id ) ); INSERT INTO app_permission(permission_key, label, category, description) VALUES ('decisions.vote.manage', 'Abstimmungen verwalten', 'Beschlüsse', 'Beschluss-Abstimmungen öffnen, schließen und auswerten') ON CONFLICT (permission_key) DO UPDATE SET label = EXCLUDED.label, category = EXCLUDED.category, description = EXCLUDED.description; INSERT INTO app_role_permission(role_profile_id, permission_key, granted) SELECT id, 'decisions.vote.manage', TRUE FROM app_role_profile WHERE code IN ('admin', 'chair', 'secretary') ON CONFLICT (role_profile_id, permission_key) DO UPDATE SET granted = TRUE; DROP POLICY IF EXISTS decision_modify_policy ON decision; CREATE POLICY decision_modify_policy ON decision FOR ALL USING ( app_has_permission('decisions.write') OR app_has_permission('decisions.vote.manage') OR responsible_user_id = app_user_id() OR EXISTS ( SELECT 1 FROM meeting m JOIN committee_member cm ON cm.committee_id = m.committee_id WHERE m.id = decision.meeting_id AND cm.user_id = app_user_id() ) ) WITH CHECK ( app_has_permission('decisions.write') OR app_has_permission('decisions.vote.manage') OR responsible_user_id = app_user_id() OR EXISTS ( SELECT 1 FROM meeting m JOIN committee_member cm ON cm.committee_id = m.committee_id WHERE m.id = decision.meeting_id AND cm.user_id = app_user_id() ) ); DROP POLICY IF EXISTS decision_vote_select_policy ON decision_vote; DROP POLICY IF EXISTS decision_vote_insert_policy ON decision_vote; DROP POLICY IF EXISTS decision_vote_update_policy ON decision_vote; DROP POLICY IF EXISTS decision_vote_delete_policy ON decision_vote; CREATE POLICY decision_vote_select_policy ON decision_vote FOR SELECT USING ( user_id = app_user_id() OR app_has_permission('decisions.vote.manage') OR EXISTS ( SELECT 1 FROM decision d JOIN meeting m ON m.id = d.meeting_id JOIN committee_member cm ON cm.committee_id = m.committee_id WHERE d.id = decision_vote.decision_id AND cm.user_id = app_user_id() ) ); CREATE POLICY decision_vote_insert_policy ON decision_vote FOR INSERT WITH CHECK ( user_id = app_user_id() AND app_role() <> 'advisor' AND EXISTS ( SELECT 1 FROM decision d JOIN meeting m ON m.id = d.meeting_id JOIN committee_member cm ON cm.committee_id = m.committee_id WHERE d.id = decision_vote.decision_id AND d.voting_open = TRUE AND m.status = 'gestartet' AND cm.user_id = app_user_id() ) ); CREATE POLICY decision_vote_update_policy ON decision_vote FOR UPDATE USING ( user_id = app_user_id() AND app_role() <> 'advisor' AND EXISTS ( SELECT 1 FROM decision d JOIN meeting m ON m.id = d.meeting_id JOIN committee_member cm ON cm.committee_id = m.committee_id WHERE d.id = decision_vote.decision_id AND d.voting_open = TRUE AND m.status = 'gestartet' AND cm.user_id = app_user_id() ) ) WITH CHECK ( user_id = app_user_id() AND app_role() <> 'advisor' ); CREATE POLICY decision_vote_delete_policy ON decision_vote FOR DELETE USING ( app_has_permission('decisions.vote.manage') ); CREATE OR REPLACE FUNCTION sync_decision_vote_counts(p_decision_id BIGINT) RETURNS TABLE(votes_yes INTEGER, votes_no INTEGER, votes_abstain INTEGER) AS $$ DECLARE yes_count INTEGER; no_count INTEGER; abstain_count INTEGER; BEGIN SELECT COUNT(*) FILTER (WHERE vote_value = 'yes')::int, COUNT(*) FILTER (WHERE vote_value = 'no')::int, COUNT(*) FILTER (WHERE vote_value = 'abstain')::int INTO yes_count, no_count, abstain_count FROM decision_vote WHERE decision_id = p_decision_id; UPDATE decision SET votes_yes = COALESCE(yes_count, 0), votes_no = COALESCE(no_count, 0), votes_abstain = COALESCE(abstain_count, 0), updated_at = NOW() WHERE id = p_decision_id; RETURN QUERY SELECT COALESCE(yes_count, 0), COALESCE(no_count, 0), COALESCE(abstain_count, 0); END; $$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; GRANT EXECUTE ON FUNCTION sync_decision_vote_counts(BIGINT) TO PUBLIC; COMMIT;