Status updates: add "hidden from board" flag

Lets management post internal status updates that don't appear in the
board portal. Adds status_updates.hidden_from_board and re-creates the
association-scoped RLS SELECT policy so board members can't read hidden
rows (staff still see all). Dialog gains a "Hide from board" toggle, the
board view filters hidden updates, and management cards show a badge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 20:28:09 -04:00
parent 03cd7127a2
commit a866160482
5 changed files with 54 additions and 5 deletions
@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { formatDateTimeShortEST } from '@/lib/timezoneUtils';
import { Edit, Trash2, User, Calendar, MessageSquare, Send, AlertCircle, ThumbsUp, ThumbsDown, Vote, Loader2 } from 'lucide-react';
import { Edit, Trash2, User, Calendar, MessageSquare, Send, AlertCircle, ThumbsUp, ThumbsDown, Vote, Loader2, EyeOff } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
@@ -159,7 +159,14 @@ function StatusUpdateCard({ update, canManage, onEdit, onDelete, index, onRefres
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
<div className="flex-1 min-w-0">
{update.associations && <p className="text-sm font-semibold text-primary">{update.associations.name}</p>}
<h3 className="text-xl font-bold text-foreground mt-1 break-words">{update.title}</h3>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<h3 className="text-xl font-bold text-foreground break-words">{update.title}</h3>
{canManage && update.hidden_from_board && (
<Badge variant="secondary" className="gap-1 shrink-0">
<EyeOff className="w-3 h-3" /> Hidden from board
</Badge>
)}
</div>
</div>
{canManage && (
<div className="flex items-center space-x-2 flex-shrink-0 self-start">
@@ -19,6 +19,7 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
const [associationId, setAssociationId] = useState('');
const [requestedAction, setRequestedAction] = useState('');
const [imageUrls, setImageUrls] = useState([]);
const [hiddenFromBoard, setHiddenFromBoard] = useState(false);
const [associations, setAssociations] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [createdAt, setCreatedAt] = useState('');
@@ -30,9 +31,11 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
setAssociationId(update.association_id);
setRequestedAction(update.requested_action || '');
setImageUrls(update.image_urls || []);
setHiddenFromBoard(!!update.hidden_from_board);
setCreatedAt(update.created_at ? new Date(update.created_at).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16));
} else {
setTitle(''); setContent(''); setAssociationId(''); setRequestedAction(''); setImageUrls([]);
setHiddenFromBoard(false);
setCreatedAt(new Date().toISOString().slice(0, 16));
}
}, [update, open]);
@@ -61,6 +64,7 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
created_by: currentUserId,
updated_at: new Date().toISOString(),
image_urls: imageUrls || [],
hidden_from_board: hiddenFromBoard,
created_at: new Date(createdAt).toISOString(),
};
@@ -177,14 +181,30 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
<div className="pt-8">
<label className="block text-sm font-medium mb-2">Images</label>
<ImageUploadField
images={imageUrls || []}
<ImageUploadField
images={imageUrls || []}
onChange={setImageUrls}
disabled={isSubmitting}
bucket="status-update-images"
/>
</div>
<div className="flex items-start gap-3 rounded-md border border-border bg-muted/30 p-3">
<input
id="hidden_from_board"
type="checkbox"
checked={hiddenFromBoard}
onChange={(e) => setHiddenFromBoard(e.target.checked)}
className="mt-0.5 h-4 w-4 rounded border-border"
/>
<label htmlFor="hidden_from_board" className="text-sm cursor-pointer">
<span className="font-medium">Hide from board</span>
<span className="block text-xs text-muted-foreground">
Internal only this update won't appear in the board portal. Staff can still see it.
</span>
</label>
</div>
</form>
</ScrollArea>
+3
View File
@@ -8404,6 +8404,7 @@ export type Database = {
content: string | null
created_at: string
created_by: string | null
hidden_from_board: boolean
id: string
image_urls: Json | null
requested_action: string | null
@@ -8417,6 +8418,7 @@ export type Database = {
content?: string | null
created_at?: string
created_by?: string | null
hidden_from_board?: boolean
id?: string
image_urls?: Json | null
requested_action?: string | null
@@ -8430,6 +8432,7 @@ export type Database = {
content?: string | null
created_at?: string
created_by?: string | null
hidden_from_board?: boolean
id?: string
image_urls?: Json | null
requested_action?: string | null
+1 -1
View File
@@ -46,7 +46,7 @@ export default function StatusUpdatesPage({ boardAssociationIds }: { boardAssoci
.from("status_updates")
.select("*, associations(name), status_update_votes(*), profiles:created_by(full_name)")
.order("created_at", { ascending: false });
if (isBoardView) query = query.in("association_id", boardAssociationIds!);
if (isBoardView) query = query.in("association_id", boardAssociationIds!).eq("hidden_from_board", false);
if (!isBoardView && filterAssocId !== "all") query = query.eq("association_id", filterAssocId);
const { data } = await query;
setUpdates(data || []);
@@ -0,0 +1,19 @@
-- Allow management to post status updates that are hidden from the board portal.
alter table public.status_updates
add column if not exists hidden_from_board boolean not null default false;
-- Board members can only read status_updates via association membership. Re-create
-- that SELECT policy so association-only readers (board members) do NOT see updates
-- flagged hidden_from_board. Staff roles (admin/manager/employee) and admins still
-- see everything (the separate "Admins can view all status_updates" policy is unchanged).
drop policy if exists "Authenticated users can read status updates for their associati" on public.status_updates;
create policy "Authenticated users can read status updates for their associati"
on public.status_updates
for select
to authenticated
using (
((association_id in (select get_user_association_ids())) and hidden_from_board = false)
or has_role(auth.uid(), 'admin'::app_role)
or has_role(auth.uid(), 'manager'::app_role)
or has_role(auth.uid(), 'employee'::app_role)
);