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 React, { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { formatDateTimeShortEST } from '@/lib/timezoneUtils';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
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 flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{update.associations && <p className="text-sm font-semibold text-primary">{update.associations.name}</p>}
|
{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>
|
</div>
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<div className="flex items-center space-x-2 flex-shrink-0 self-start">
|
<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 [associationId, setAssociationId] = useState('');
|
||||||
const [requestedAction, setRequestedAction] = useState('');
|
const [requestedAction, setRequestedAction] = useState('');
|
||||||
const [imageUrls, setImageUrls] = useState([]);
|
const [imageUrls, setImageUrls] = useState([]);
|
||||||
|
const [hiddenFromBoard, setHiddenFromBoard] = useState(false);
|
||||||
const [associations, setAssociations] = useState([]);
|
const [associations, setAssociations] = useState([]);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [createdAt, setCreatedAt] = useState('');
|
const [createdAt, setCreatedAt] = useState('');
|
||||||
@@ -30,9 +31,11 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
|
|||||||
setAssociationId(update.association_id);
|
setAssociationId(update.association_id);
|
||||||
setRequestedAction(update.requested_action || '');
|
setRequestedAction(update.requested_action || '');
|
||||||
setImageUrls(update.image_urls || []);
|
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));
|
setCreatedAt(update.created_at ? new Date(update.created_at).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16));
|
||||||
} else {
|
} else {
|
||||||
setTitle(''); setContent(''); setAssociationId(''); setRequestedAction(''); setImageUrls([]);
|
setTitle(''); setContent(''); setAssociationId(''); setRequestedAction(''); setImageUrls([]);
|
||||||
|
setHiddenFromBoard(false);
|
||||||
setCreatedAt(new Date().toISOString().slice(0, 16));
|
setCreatedAt(new Date().toISOString().slice(0, 16));
|
||||||
}
|
}
|
||||||
}, [update, open]);
|
}, [update, open]);
|
||||||
@@ -61,6 +64,7 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
|
|||||||
created_by: currentUserId,
|
created_by: currentUserId,
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
image_urls: imageUrls || [],
|
image_urls: imageUrls || [],
|
||||||
|
hidden_from_board: hiddenFromBoard,
|
||||||
created_at: new Date(createdAt).toISOString(),
|
created_at: new Date(createdAt).toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,14 +181,30 @@ function StatusUpdateDialog({ open, onOpenChange, update, onSuccess, currentUser
|
|||||||
|
|
||||||
<div className="pt-8">
|
<div className="pt-8">
|
||||||
<label className="block text-sm font-medium mb-2">Images</label>
|
<label className="block text-sm font-medium mb-2">Images</label>
|
||||||
<ImageUploadField
|
<ImageUploadField
|
||||||
images={imageUrls || []}
|
images={imageUrls || []}
|
||||||
onChange={setImageUrls}
|
onChange={setImageUrls}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
bucket="status-update-images"
|
bucket="status-update-images"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
|||||||
@@ -8404,6 +8404,7 @@ export type Database = {
|
|||||||
content: string | null
|
content: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
created_by: string | null
|
created_by: string | null
|
||||||
|
hidden_from_board: boolean
|
||||||
id: string
|
id: string
|
||||||
image_urls: Json | null
|
image_urls: Json | null
|
||||||
requested_action: string | null
|
requested_action: string | null
|
||||||
@@ -8417,6 +8418,7 @@ export type Database = {
|
|||||||
content?: string | null
|
content?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
created_by?: string | null
|
created_by?: string | null
|
||||||
|
hidden_from_board?: boolean
|
||||||
id?: string
|
id?: string
|
||||||
image_urls?: Json | null
|
image_urls?: Json | null
|
||||||
requested_action?: string | null
|
requested_action?: string | null
|
||||||
@@ -8430,6 +8432,7 @@ export type Database = {
|
|||||||
content?: string | null
|
content?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
created_by?: string | null
|
created_by?: string | null
|
||||||
|
hidden_from_board?: boolean
|
||||||
id?: string
|
id?: string
|
||||||
image_urls?: Json | null
|
image_urls?: Json | null
|
||||||
requested_action?: string | null
|
requested_action?: string | null
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function StatusUpdatesPage({ boardAssociationIds }: { boardAssoci
|
|||||||
.from("status_updates")
|
.from("status_updates")
|
||||||
.select("*, associations(name), status_update_votes(*), profiles:created_by(full_name)")
|
.select("*, associations(name), status_update_votes(*), profiles:created_by(full_name)")
|
||||||
.order("created_at", { ascending: false });
|
.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);
|
if (!isBoardView && filterAssocId !== "all") query = query.eq("association_id", filterAssocId);
|
||||||
const { data } = await query;
|
const { data } = await query;
|
||||||
setUpdates(data || []);
|
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