diff --git a/src/components/status-updates/StatusUpdateCard.jsx b/src/components/status-updates/StatusUpdateCard.jsx index b5078d4..b608e6f 100644 --- a/src/components/status-updates/StatusUpdateCard.jsx +++ b/src/components/status-updates/StatusUpdateCard.jsx @@ -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
{update.associations &&

{update.associations.name}

} -

{update.title}

+
+

{update.title}

+ {canManage && update.hidden_from_board && ( + + Hidden from board + + )} +
{canManage && (
diff --git a/src/components/status-updates/StatusUpdateDialog.jsx b/src/components/status-updates/StatusUpdateDialog.jsx index dcac93c..7379230 100644 --- a/src/components/status-updates/StatusUpdateDialog.jsx +++ b/src/components/status-updates/StatusUpdateDialog.jsx @@ -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
-
+
+ setHiddenFromBoard(e.target.checked)} + className="mt-0.5 h-4 w-4 rounded border-border" + /> + +
+ diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index fe925f3..972d785 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -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 diff --git a/src/pages/StatusUpdatesPage.tsx b/src/pages/StatusUpdatesPage.tsx index a9b532f..f290924 100644 --- a/src/pages/StatusUpdatesPage.tsx +++ b/src/pages/StatusUpdatesPage.tsx @@ -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 || []); diff --git a/supabase/migrations/20260616200000_status_updates_hidden_from_board.sql b/supabase/migrations/20260616200000_status_updates_hidden_from_board.sql new file mode 100644 index 0000000..c989185 --- /dev/null +++ b/supabase/migrations/20260616200000_status_updates_hidden_from_board.sql @@ -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) +);