mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
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:
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -185,6 +189,22 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
Reference in New Issue
Block a user