Skip to main content

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 EndpointNew EndpointChange
GET /govintel/signal-poolGET /govintel/signal-poolResponse structure changed
POST /govintel/signal-pool/:idPOST /govintel/signals/:id/publishNew endpoint
DELETE /govintel/signal-pool/:idPOST /govintel/signals/:id/unpublishNew endpoint
PATCH /govintel/signal-pool/requests/:idSameRequest/response format unchanged

Status Values

Old: approvalStatus with values like "pending", "approved", "rejected"

NEW: Unified status with 9 values:

  • signal_unassigned
  • signal_assigned
  • signal_accepted
  • signal_declined
  • contact_request_pending
  • contact_request_approved
  • contact_request_rejected
  • signal_archived
  • signal_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.