Pages Update Guide for Unified Status System
Quick Reference: This guide shows exactly what to change in each page to use the new unified signal status system.
Admin Pages
1. Signal Pool Page (apps/admin/app/signal-pool/page.tsx)
Changes Needed:
1. Update Interface (lines 59-94):
// OLD - Remove SignalPoolEntry interface entirely
interface SignalPoolEntry {
id: string;
signal: { ... };
isActive: boolean;
status: string;
// ...
}
// NEW - Signals now have isPublic flag directly
interface PoolSignal {
id: string;
signalIdentifier: string;
governmentName: string;
contactName?: string;
painPoints: string;
improvement: string;
mqLScore: number;
solutionCategory: string[];
// New unified status fields
status: string; // signal_assigned, signal_accepted, etc.
isPublic: boolean;
isActive: boolean;
publishedAt: string;
expiresAt?: string;
visibilityLevel: string;
viewCount: number;
requestCount: number;
poolRequests: SignalPoolRequest[]; // Direct relation now
}
2. Update State (line 98):
// OLD
const [poolEntries, setPoolEntries] = useState<SignalPoolEntry[]>([]);
// NEW
const [poolSignals, setPoolSignals] = useState<PoolSignal[]>([]);
3. Update Fetch Function:
// OLD
const response = await apiClient.request("/govintel/signal-pool/admin/all");
setPoolEntries(response.data.data);
// NEW - Use new API method
const response = await apiClient.getPoolSignalsAdmin({ page, limit, status });
setPoolSignals(response.data.signals); // Note: signals, not data.data
4. Update Render Logic:
// OLD
{poolEntries.map((entry) => (
<TableRow key={entry.id}>
<TableCell>{entry.signal.signalIdentifier}</TableCell>
<TableCell>{entry.signal.governmentName}</TableCell>
<TableCell>{entry.status}</TableCell> {/* Old poolEntry status */}
// ...
</TableRow>
))}
// NEW - Signal is at top level now
{poolSignals.map((signal) => (
<TableRow key={signal.id}>
<TableCell>{signal.signalIdentifier}</TableCell>
<TableCell>{signal.governmentName}</TableCell>
<TableCell>
<SignalStatusBadge status={signal.status} isPublic={signal.isPublic} />
</TableCell>
// ...
</TableRow>
))}
5. Import Status Badge:
import { SignalStatusBadge } from "@/components/SignalStatusBadge";
2. Signals List Page (apps/admin/app/signals/page.tsx)
Changes Needed:
1. Add Status Filter:
import { STATUS_FILTER_OPTIONS } from "@/components/SignalStatusBadge";
const [statusFilter, setStatusFilter] = useState('');
// Add to UI
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
{STATUS_FILTER_OPTIONS.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
2. Update Fetch to Include Status Filter:
const response = await apiClient.getSignals({
page,
limit,
urgency,
status: statusFilter, // Add this
search,
});
3. Display Status Badge:
{signals.map((signal) => (
<TableRow key={signal.id}>
{/* ... other cells ... */}
<TableCell>
<SignalStatusBadge
status={signal.status}
isPublic={signal.isPublic}
showPublicIndicator={true}
/>
</TableCell>
</TableRow>
))}
4. Add isPublic Indicator (optional column):
<TableCell>
{signal.isPublic && (
<Badge variant="outline" className="bg-purple-100">
Public Pool
</Badge>
)}
</TableCell>
3. Signal Detail Page (apps/admin/app/signals/[id]/page.tsx)
Changes Needed:
1. Display Status:
import { SignalStatusBadge } from "@/components/SignalStatusBadge";
// In render
<div className="mb-4">
<Label>Status</Label>
<SignalStatusBadge
status={signal.status}
isPublic={signal.isPublic}
showPublicIndicator={true}
/>
</div>
2. Add Action Buttons Based on Status:
{/* Unassigned signals can be assigned or published */}
{signal.status === 'signal_unassigned' && (
<div className="flex gap-2">
<Button onClick={() => setShowAssignDialog(true)}>
Assign to Startups
</Button>
<Button onClick={() => handlePublishToPool()}>
Publish to Pool
</Button>
</div>
)}
{/* Public signals can be unpublished */}
{signal.isPublic && (
<Button variant="outline" onClick={() => handleUnpublish()}>
Remove from Pool
</Button>
)}
{/* All signals can be archived (if not already) */}
{!signal.isArchived && (
<Button variant="destructive" onClick={() => handleArchive()}>
Archive Signal
</Button>
)}
3. Implement Action Handlers:
const handlePublishToPool = async () => {
try {
await apiClient.publishToPool(signal.id, {
visibilityLevel: "all",
expiresAt: undefined, // or set a date
});
toast.success("Signal published to pool");
refetch(); // Refresh signal data
} catch (error) {
toast.error("Failed to publish signal");
}
};
const handleUnpublish = async () => {
try {
await apiClient.unpublishFromPool(signal.id);
toast.success("Signal removed from pool");
refetch();
} catch (error) {
toast.error("Failed to unpublish signal");
}
};
const handleArchive = async () => {
const notes = window.prompt("Reason for archiving (optional):");
try {
await apiClient.archiveSignal(signal.id, notes || undefined);
toast.success("Signal archived");
refetch();
} catch (error) {
toast.error("Failed to archive signal");
}
};
const handleAssign = async (startupIds: string[]) => {
try {
await apiClient.assignSignalToStartups(signal.id, {
startupIds,
assignmentType: "direct",
});
toast.success(`Signal assigned to ${startupIds.length} startup(s)`);
refetch();
} catch (error) {
toast.error("Failed to assign signal");
}
};
4. Show Status History (optional):
const [statusHistory, setStatusHistory] = useState([]);
useEffect(() => {
const fetchHistory = async () => {
const response = await apiClient.getSignalStatusHistory(signal.id);
setStatusHistory(response.data);
};
fetchHistory();
}, [signal.id]);
// In render
<Card>
<CardHeader>
<CardTitle>Status History</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{statusHistory.map((entry, i) => (
<div key={i} className="flex justify-between border-b pb-2">
<div>
<SignalStatusBadge status={entry.status} />
{entry.notes && <p className="text-sm text-gray-600">{entry.notes}</p>}
</div>
<div className="text-sm text-gray-500">
{new Date(entry.changedAt).toLocaleString()}
</div>
</div>
))}
</div>
</CardContent>
</Card>
4. Pool Requests Page
If you have a dedicated page for managing pool requests, update it:
Fetch Requests:
const response = await apiClient.getPoolRequests({
status: statusFilter,
page,
limit,
});
setRequests(response.data.requests);
Approve/Reject Actions:
const handleApprove = async (requestId: string) => {
try {
await apiClient.approvePoolRequest(requestId);
toast.success("Request approved");
refetch();
} catch (error) {
toast.error("Failed to approve request");
}
};
const handleReject = async (requestId: string) => {
const reason = window.prompt("Rejection reason:");
if (!reason) return;
try {
await apiClient.rejectPoolRequest(requestId, reason);
toast.success("Request rejected");
refetch();
} catch (error) {
toast.error("Failed to reject request");
}
};
Frontend (Startup) Pages
1. Inbox/Signals Page (apps/frontend/app/govintel/signals/page.tsx)
Changes Needed:
1. Import Status Badge:
import {
SignalStatusBadge,
canTakeAction,
getAvailableActions,
} from "@/components/SignalStatusBadge";
2. Display Status with Description:
{signals.map((signal) => (
<Card key={signal.id}>
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle>{signal.signalIdentifier}</CardTitle>
<SignalStatusBadge
status={signal.status}
showDescription={true} // Shows user-friendly description
/>
</div>
</CardHeader>
<CardContent>
{/* ... signal details ... */}
{/* Action buttons based on status */}
{canTakeAction(signal.status) && (
<div className="flex gap-2 mt-4">
{getAvailableActions(signal.status).includes('accept') && (
<Button onClick={() => handleAccept(signal.id)}>
Accept Signal
</Button>
)}
{getAvailableActions(signal.status).includes('decline') && (
<Button variant="outline" onClick={() => handleDecline(signal.id)}>
Decline
</Button>
)}
{getAvailableActions(signal.status).includes('request_contact') && (
<Button onClick={() => handleRequestContact(signal.id)}>
Request Contact Info
</Button>
)}
{getAvailableActions(signal.status).includes('view_contact') && (
<Button onClick={() => router.push(`/govintel/connections`)}>
View Connection
</Button>
)}
</div>
)}
</CardContent>
</Card>
))}
3. Implement Actions (using direct fetch since no API client):
import { apiUrl } from "@/lib/api";
const handleAccept = async (signalId: string) => {
const notes = window.prompt("Notes (optional):");
try {
const token = await getToken();
const response = await fetch(
apiUrl(`govintel/client/signals/${signalId}/accept`),
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ notes: notes || undefined }),
},
);
if (!response.ok) throw new Error("Failed to accept signal");
toast.success("Signal accepted!");
refetch();
} catch (error) {
toast.error("Failed to accept signal");
}
};
const handleDecline = async (signalId: string) => {
const reason = window.prompt("Reason for declining (optional):");
try {
const token = await getToken();
const response = await fetch(
apiUrl(`govintel/client/signals/${signalId}/decline`),
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ reason: reason || undefined }),
},
);
if (!response.ok) throw new Error("Failed to decline signal");
toast.success("Signal declined");
refetch();
} catch (error) {
toast.error("Failed to decline signal");
}
};
const handleRequestContact = async (signalId: string) => {
try {
const token = await getToken();
const response = await fetch(
apiUrl(`govintel/client/signals/${signalId}/request-contact`),
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ requestSource: "direct" }), // or 'pool'
},
);
if (!response.ok) throw new Error("Failed to request contact");
toast.success("Contact request submitted. Awaiting admin approval.");
refetch();
} catch (error) {
toast.error("Failed to request contact");
}
};
2. Pool Page (apps/frontend/app/govintel/pool/page.tsx)
Changes Needed:
1. Update Fetch (signals are now at top level, not wrapped in poolEntry):
// OLD
const response = await fetch(apiUrl('govintel/signal-pool'), {...});
const data = await response.json();
setPoolSignals(data.data.poolEntries.map(e => e.signal));
// NEW
const response = await fetch(apiUrl('govintel/signal-pool'), {...});
const data = await response.json();
setPoolSignals(data.data.signals); // Signals directly
2. Display Signals:
{poolSignals.map((signal) => (
<Card key={signal.id}>
<CardHeader>
<CardTitle>{signal.signalIdentifier}</CardTitle>
<SignalStatusBadge status={signal.status} isPublic={true} />
</CardHeader>
<CardContent>
{/* Signal details */}
<p>{signal.painPoints}</p>
<p>{signal.improvement}</p>
{/* Stats */}
<div className="flex gap-4 mt-4 text-sm text-gray-600">
<span>{signal.viewCount} views</span>
<span>{signal.requestCount} requests</span>
</div>
{/* Action button */}
<Button
onClick={() => handleRequestSignal(signal.id)}
className="mt-4"
>
Request This Signal
</Button>
</CardContent>
</Card>
))}
3. Track Views:
const handleViewSignal = async (signalId: string) => {
try {
const token = await getToken();
await fetch(apiUrl(`govintel/signal-pool/${signalId}/view`), {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
// Silent - just tracks the view
} catch (error) {
// Ignore errors
}
};
// Call when signal is opened/viewed
useEffect(() => {
if (selectedSignalId) {
handleViewSignal(selectedSignalId);
}
}, [selectedSignalId]);
4. Request Signal:
const handleRequestSignal = async (signalId: string) => {
const justification = window.prompt(
"Why is this a good fit for your company? (optional)",
);
try {
const token = await getToken();
const response = await fetch(
apiUrl(`govintel/signal-pool/${signalId}/request`),
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ justification: justification || undefined }),
},
);
if (!response.ok) throw new Error("Failed to request signal");
toast.success("Signal requested! Check your inbox.");
router.push("/govintel/signals"); // Go to inbox
} catch (error) {
toast.error("Failed to request signal");
}
};
Key Points
Data Structure Changes
OLD Structure:
{
poolEntry: {
id: "entry-123",
signal: { id: "sig-456", ... },
status: "available",
...
}
}
NEW Structure:
{
signal: {
id: "sig-456",
status: "signal_assigned",
isPublic: true,
...
}
}
API Endpoints Changed
| Old Endpoint | New Endpoint | Change |
|---|---|---|
GET /govintel/signal-pool | GET /govintel/signal-pool | Response structure changed |
POST /govintel/signal-pool/:id | POST /govintel/signals/:id/publish | New endpoint |
DELETE /govintel/signal-pool/:id | POST /govintel/signals/:id/unpublish | New endpoint |
PATCH /govintel/signal-pool/requests/:id | Same | Request/response format unchanged |
Status Values
Old: approvalStatus with values like "pending", "approved", "rejected"
NEW: Unified status with 9 values:
signal_unassignedsignal_assignedsignal_acceptedsignal_declinedcontact_request_pendingcontact_request_approvedcontact_request_rejectedsignal_archivedsignal_expired
Testing Checklist
After updates, test:
- Admin can view all signals with correct statuses
- Admin can publish signal to pool
- Admin can unpublish signal from pool
- Admin can assign signal to startups
- Admin can archive/unarchive signals
- Startup can view pool signals
- Startup can request pool signal
- Startup can accept/decline assigned signals
- Startup can request contact info
- Admin can approve/reject contact requests
- Status badges display correctly
- Action buttons show/hide based on status
Pro Tip: Update one page at a time, test it, then move to the next. Start with the admin signal pool page as it's the simplest.