Skip to content

Commit 44ce549

Browse files
kanekoshoyuclaude
andcommitted
feat: add Actions section to Orbit Signal dashboard
- Add action_status field to HiringIntent type (pending, added_to_actions, ignored) - Implement updateHiringIntentActionStatus API function - Add "Add to Actions" and "Ignore" buttons to signal cards - Create dedicated Actions section for signals marked as actions - Filter out ignored signals from UI automatically - Add count badges for both Actions and Orbit Signals sections - Implement real-time state updates for action changes Database schema update required: Add action_status field to hiring_intent collection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 0b3e40b commit 44ce549

File tree

3 files changed

+317
-71
lines changed

3 files changed

+317
-71
lines changed

TODO.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,3 +723,109 @@ Missing fields for better matching:
723723
**Last Updated**: 2025-11-27
724724
**Reviewer**: Claude Code
725725
**Priority**: P0 items are critical for bidirectional functionality
726+
727+
---
728+
729+
## Orbit Signal - Actions Feature
730+
731+
### Overview
732+
Add action tracking to Orbit Signal dashboard, allowing users to mark signals as "add to actions" or "ignore", with automatic filtering and organization.
733+
734+
### Implementation Status: ✅ COMPLETED (2025-12-22)
735+
736+
### Features Implemented
737+
- ✅ Action status tracking (`pending`, `added_to_actions`, `ignored`)
738+
-"Add to Actions" and "Ignore" buttons on each signal card
739+
- ✅ Dedicated "Actions" section displaying signals marked for action
740+
- ✅ Automatic removal of ignored signals from UI
741+
-Real-time state updates when action status changes
742+
- ✅ Count badges for both Actions and Orbit Signals sections
743+
- ✅ Visual distinction between pending and actioned signals
744+
745+
### Frontend Changes
746+
- **Type Updates**: `src/lib/utils.ts:1486`
747+
- Added `action_status?: 'pending' | 'added_to_actions' | 'ignored'` to `HiringIntent` type
748+
749+
- **API Functions**: `src/lib/utils.ts:1540-1586`
750+
- Added `updateHiringIntentActionStatus()` for updating signal status
751+
- Updated `getHiringIntentsBySpace()` to fetch `action_status` field
752+
753+
- **Component Updates**: `src/components/interactive/HiringIntentDashboard.tsx`
754+
- Added action buttons (Add to Actions, Ignore) with icons
755+
- Implemented filtering logic for pending vs actioned signals
756+
- Created separate sections: "Actions" and "Orbit Signals"
757+
- Added real-time state management for action updates
758+
- Implemented conditional rendering based on action status
759+
760+
### Database Schema Required ⚠️
761+
762+
**CRITICAL**: Add `action_status` field to `hiring_intent` collection in Directus
763+
764+
```sql
765+
-- Add action_status field to hiring_intent table
766+
ALTER TABLE hiring_intent
767+
ADD COLUMN action_status VARCHAR(20) DEFAULT 'pending'
768+
CHECK (action_status IN ('pending', 'added_to_actions', 'ignored'));
769+
770+
-- Create index for faster filtering
771+
CREATE INDEX idx_hiring_intent_action_status
772+
ON hiring_intent(action_status);
773+
```
774+
775+
**Directus Configuration**:
776+
- [ ] Add `action_status` field to `hiring_intent` collection
777+
- Type: `string` or `dropdown`
778+
- Default: `pending`
779+
- Options: `pending`, `added_to_actions`, `ignored`
780+
- Interface: Dropdown or Radio Buttons
781+
- Required: NO (defaults to 'pending')
782+
783+
### UI Design
784+
785+
#### Actions Section (New)
786+
- Appears at the top when signals are added to actions
787+
- Green badge with count
788+
- Cards without action buttons (already actioned)
789+
- Visually separated from pending signals
790+
791+
#### Orbit Signals Section
792+
- Shows pending/new signals only
793+
- Each card has "Add to Actions" (green) and "Ignore" (red) buttons
794+
- Badges show count of pending signals
795+
- Ignored signals are completely removed from view
796+
797+
### Testing Checklist
798+
- [ ] Verify database field `action_status` exists in `hiring_intent`
799+
- [ ] Test "Add to Actions" button functionality
800+
- [ ] Test "Ignore" button functionality
801+
- [ ] Verify ignored signals disappear from UI
802+
- [ ] Verify actioned signals appear in Actions section
803+
- [ ] Test with multiple spaces
804+
- [ ] Test with space filter (All vs specific space)
805+
- [ ] Verify count badges update correctly
806+
- [ ] Test on mobile responsive layout
807+
808+
### Future Enhancements (Optional)
809+
- [ ] Add "Undo" functionality for ignored signals
810+
- [ ] Add bulk actions (select multiple signals)
811+
- [ ] Add export functionality for actioned signals
812+
- [ ] Add filtering by action status
813+
- [ ] Add notes/comments on actioned signals
814+
- [ ] Add notification when new signals arrive
815+
- [ ] Add CRM integration for actioned signals
816+
817+
### Files Modified
818+
- `src/lib/utils.ts` - Type and API updates
819+
- `src/components/interactive/HiringIntentDashboard.tsx` - UI and logic implementation
820+
821+
### Dependencies
822+
- Lucide React icons: `CheckCircle2`, `XCircle` (already imported)
823+
- Button component from ShadCN (already available)
824+
- Badge component from ShadCN (already available)
825+
826+
---
827+
828+
**Feature Status**: ✅ Completed - Pending Database Schema Update
829+
**Implemented**: 2025-12-22
830+
**Developer**: Claude Code
831+
**Priority**: P1 - Requires database field addition to function

src/components/interactive/HiringIntentDashboard.tsx

Lines changed: 160 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import { useState, useEffect } from "react";
44
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
55
import { Badge } from "@/components/ui/badge";
6+
import { Button } from "@/components/ui/button";
67
import SpaceSelector from "@/components/interactive/SpaceSelector";
7-
import { getHiringIntentsBySpace, type HiringIntent } from "@/lib/utils";
8+
import { getHiringIntentsBySpace, updateHiringIntentActionStatus, type HiringIntent } from "@/lib/utils";
89
import { EXTERNAL } from "@/constant";
10+
import { CheckCircle2, XCircle } from "lucide-react";
911

1012
export default function HiringIntentDashboard() {
1113
const [hiringIntents, setHiringIntents] = useState<HiringIntent[]>([]);
@@ -72,8 +74,138 @@ export default function HiringIntentDashboard() {
7274
setSelectedSpaceId(spaceId);
7375
};
7476

77+
const handleActionStatusUpdate = async (
78+
hiringIntentId: number,
79+
actionStatus: 'added_to_actions' | 'ignored'
80+
) => {
81+
try {
82+
const result = await updateHiringIntentActionStatus(
83+
hiringIntentId,
84+
actionStatus,
85+
EXTERNAL.directus_url
86+
);
87+
88+
if (result.success) {
89+
// Update local state to reflect the change
90+
setHiringIntents(prevIntents =>
91+
prevIntents.map(intent =>
92+
intent.id === hiringIntentId
93+
? { ...intent, action_status: actionStatus }
94+
: intent
95+
)
96+
);
97+
} else {
98+
console.error('Failed to update action status:', result.error);
99+
setError(result.error || 'Failed to update action status');
100+
}
101+
} catch (err) {
102+
console.error('Error updating action status:', err);
103+
setError('An error occurred while updating action status');
104+
}
105+
};
106+
107+
// Filter intents into different categories
108+
const pendingIntents = hiringIntents.filter(
109+
intent => !intent.action_status || intent.action_status === 'pending'
110+
);
111+
const actionIntents = hiringIntents.filter(
112+
intent => intent.action_status === 'added_to_actions'
113+
);
114+
// Ignored intents are not displayed
115+
116+
const renderIntentCard = (intent: HiringIntent, showActions: boolean = true) => (
117+
<Card key={intent.id} className="hover:shadow-lg transition-shadow">
118+
<CardHeader>
119+
<div className="flex items-start justify-between">
120+
<CardTitle className="text-lg">
121+
{intent.company_profile?.name || "Unknown Company"}
122+
</CardTitle>
123+
{intent.category && (
124+
<Badge className={getCategoryColor(intent.category)}>
125+
{intent.category}
126+
</Badge>
127+
)}
128+
</div>
129+
</CardHeader>
130+
<CardContent className="space-y-3">
131+
{/* Reason */}
132+
{intent.reason && (
133+
<div>
134+
<p className="text-xs font-medium text-gray-500 mb-1">Reason</p>
135+
<p className="text-sm text-gray-700 line-clamp-3">{intent.reason}</p>
136+
</div>
137+
)}
138+
139+
{/* Potential Role */}
140+
{intent.potential_role && (
141+
<div>
142+
<p className="text-xs font-medium text-gray-500 mb-1">Potential Role</p>
143+
<p className="text-sm text-gray-700">
144+
{typeof intent.potential_role === "string"
145+
? intent.potential_role
146+
: JSON.stringify(intent.potential_role)}
147+
</p>
148+
</div>
149+
)}
150+
151+
{/* Skills */}
152+
{intent.skill && (
153+
<div>
154+
<p className="text-xs font-medium text-gray-500 mb-1">Skills</p>
155+
<p className="text-sm text-gray-700">
156+
{typeof intent.skill === "string"
157+
? intent.skill
158+
: JSON.stringify(intent.skill)}
159+
</p>
160+
</div>
161+
)}
162+
163+
{/* Confidence Score */}
164+
{intent.confidence !== undefined && intent.confidence !== null && (
165+
<div className="flex items-center justify-between">
166+
<p className="text-xs font-medium text-gray-500">Confidence</p>
167+
<Badge className={getConfidenceColor(intent.confidence)}>
168+
{intent.confidence}%
169+
</Badge>
170+
</div>
171+
)}
172+
173+
{/* Action Buttons */}
174+
{showActions && (!intent.action_status || intent.action_status === 'pending') && (
175+
<div className="pt-3 border-t border-gray-100 flex gap-2">
176+
<Button
177+
size="sm"
178+
variant="default"
179+
className="flex-1 bg-green-600 hover:bg-green-700"
180+
onClick={() => handleActionStatusUpdate(intent.id, 'added_to_actions')}
181+
>
182+
<CheckCircle2 className="w-4 h-4 mr-1" />
183+
Add to Actions
184+
</Button>
185+
<Button
186+
size="sm"
187+
variant="outline"
188+
className="flex-1 border-red-300 text-red-600 hover:bg-red-50"
189+
onClick={() => handleActionStatusUpdate(intent.id, 'ignored')}
190+
>
191+
<XCircle className="w-4 h-4 mr-1" />
192+
Ignore
193+
</Button>
194+
</div>
195+
)}
196+
197+
{/* Date Created */}
198+
<div className="pt-2 border-t border-gray-100">
199+
<p className="text-xs text-gray-400">
200+
Created {formatDate(intent.date_created)}
201+
</p>
202+
</div>
203+
</CardContent>
204+
</Card>
205+
);
206+
75207
return (
76-
<div className="space-y-6">
208+
<div className="space-y-8">
77209
{/* Space Selector */}
78210
<div className="flex items-center justify-between">
79211
<div className="flex items-center gap-3">
@@ -122,75 +254,33 @@ export default function HiringIntentDashboard() {
122254
</Card>
123255
)}
124256

257+
{/* Actions Section */}
258+
{!isLoading && !error && actionIntents.length > 0 && (
259+
<div className="space-y-4">
260+
<div className="flex items-center gap-2">
261+
<h2 className="text-xl font-semibold text-gray-900">Actions</h2>
262+
<Badge variant="secondary" className="bg-green-100 text-green-800">
263+
{actionIntents.length}
264+
</Badge>
265+
</div>
266+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
267+
{actionIntents.map((intent) => renderIntentCard(intent, false))}
268+
</div>
269+
</div>
270+
)}
271+
125272
{/* Orbit Signals Grid */}
126-
{!isLoading && !error && hiringIntents.length > 0 && (
127-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
128-
{hiringIntents.map((intent) => (
129-
<Card key={intent.id} className="hover:shadow-lg transition-shadow">
130-
<CardHeader>
131-
<div className="flex items-start justify-between">
132-
<CardTitle className="text-lg">
133-
{intent.company_profile?.name || "Unknown Company"}
134-
</CardTitle>
135-
{intent.category && (
136-
<Badge className={getCategoryColor(intent.category)}>
137-
{intent.category}
138-
</Badge>
139-
)}
140-
</div>
141-
</CardHeader>
142-
<CardContent className="space-y-3">
143-
{/* Reason */}
144-
{intent.reason && (
145-
<div>
146-
<p className="text-xs font-medium text-gray-500 mb-1">Reason</p>
147-
<p className="text-sm text-gray-700 line-clamp-3">{intent.reason}</p>
148-
</div>
149-
)}
150-
151-
{/* Potential Role */}
152-
{intent.potential_role && (
153-
<div>
154-
<p className="text-xs font-medium text-gray-500 mb-1">Potential Role</p>
155-
<p className="text-sm text-gray-700">
156-
{typeof intent.potential_role === "string"
157-
? intent.potential_role
158-
: JSON.stringify(intent.potential_role)}
159-
</p>
160-
</div>
161-
)}
162-
163-
{/* Skills */}
164-
{intent.skill && (
165-
<div>
166-
<p className="text-xs font-medium text-gray-500 mb-1">Skills</p>
167-
<p className="text-sm text-gray-700">
168-
{typeof intent.skill === "string"
169-
? intent.skill
170-
: JSON.stringify(intent.skill)}
171-
</p>
172-
</div>
173-
)}
174-
175-
{/* Confidence Score */}
176-
{intent.confidence !== undefined && intent.confidence !== null && (
177-
<div className="flex items-center justify-between">
178-
<p className="text-xs font-medium text-gray-500">Confidence</p>
179-
<Badge className={getConfidenceColor(intent.confidence)}>
180-
{intent.confidence}%
181-
</Badge>
182-
</div>
183-
)}
184-
185-
{/* Date Created */}
186-
<div className="pt-2 border-t border-gray-100">
187-
<p className="text-xs text-gray-400">
188-
Created {formatDate(intent.date_created)}
189-
</p>
190-
</div>
191-
</CardContent>
192-
</Card>
193-
))}
273+
{!isLoading && !error && pendingIntents.length > 0 && (
274+
<div className="space-y-4">
275+
<div className="flex items-center gap-2">
276+
<h2 className="text-xl font-semibold text-gray-900">Orbit Signals</h2>
277+
<Badge variant="secondary">
278+
{pendingIntents.length}
279+
</Badge>
280+
</div>
281+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
282+
{pendingIntents.map((intent) => renderIntentCard(intent, true))}
283+
</div>
194284
</div>
195285
)}
196286
</div>

0 commit comments

Comments
 (0)