feat: Complete JTBD-aligned bakery dashboard redesign
Implements comprehensive dashboard redesign based on Jobs To Be Done methodology
focused on answering: "What requires my attention right now?"
## Backend Implementation
### Dashboard Service (NEW)
- Health status calculation (green/yellow/red traffic light)
- Action queue prioritization (critical/important/normal)
- Orchestration summary with narrative format
- Production timeline transformation
- Insights calculation and consequence prediction
### API Endpoints (NEW)
- GET /dashboard/health-status - Overall bakery health indicator
- GET /dashboard/orchestration-summary - What system did automatically
- GET /dashboard/action-queue - Prioritized tasks requiring attention
- GET /dashboard/production-timeline - Today's production schedule
- GET /dashboard/insights - Key metrics (savings, inventory, waste, deliveries)
### Enhanced Models
- PurchaseOrder: Added reasoning, consequence, reasoning_data fields
- ProductionBatch: Added reasoning, reasoning_data fields
- Enables transparency into automation decisions
## Frontend Implementation
### API Hooks (NEW)
- useBakeryHealthStatus() - Real-time health monitoring
- useOrchestrationSummary() - System transparency
- useActionQueue() - Prioritized action management
- useProductionTimeline() - Production tracking
- useInsights() - Glanceable metrics
### Dashboard Components (NEW)
- HealthStatusCard: Traffic light indicator with checklist
- ActionQueueCard: Prioritized actions with reasoning/consequences
- OrchestrationSummaryCard: Narrative of what system did
- ProductionTimelineCard: Chronological production view
- InsightsGrid: 2x2 grid of key metrics
### Main Dashboard Page (REPLACED)
- Complete rewrite with mobile-first design
- All sections integrated with error handling
- Real-time refresh and quick action links
- Old dashboard backed up as DashboardPage.legacy.tsx
## Key Features
### Automation-First
- Shows what orchestrator did overnight
- Builds trust through transparency
- Explains reasoning for all automated decisions
### Action-Oriented
- Prioritizes tasks over information display
- Clear consequences for each action
- Large touch-friendly buttons
### Progressive Disclosure
- Shows 20% of info that matters 80% of time
- Expandable details when needed
- No overwhelming metrics
### Mobile-First
- One-handed operation
- Large touch targets (min 44px)
- Responsive grid layouts
### Trust-Building
- Narrative format ("I planned your day")
- Reasoning inputs transparency
- Clear status indicators
## User Segments Supported
1. Solo Bakery Owner (Primary)
- Simple health indicator
- Action checklist (max 3-5 items)
- Mobile-optimized
2. Multi-Location Owner
- Multi-tenant support (existing)
- Comparison capabilities
- Delegation ready
3. Enterprise/Central Bakery (Future)
- Network topology support
- Advanced analytics ready
## JTBD Analysis Delivered
Main Job: "Help me quickly understand bakery status and know what needs my intervention"
Emotional Jobs Addressed:
- Feel in control despite automation
- Reduce daily anxiety
- Feel competent with technology
- Trust system as safety net
Social Jobs Addressed:
- Demonstrate professional management
- Avoid being bottleneck
- Show sustainability
## Technical Stack
Backend: Python, FastAPI, SQLAlchemy, PostgreSQL
Frontend: React, TypeScript, TanStack Query, Tailwind CSS
Architecture: Microservices with circuit breakers
## Breaking Changes
- Complete dashboard page rewrite (old version backed up)
- New API endpoints require orchestrator service deployment
- Database migrations needed for reasoning fields
## Migration Required
Run migrations to add new model fields:
- purchase_orders: reasoning, consequence, reasoning_data
- production_batches: reasoning, reasoning_data
## Documentation
See DASHBOARD_REDESIGN_SUMMARY.md for complete implementation details,
JTBD analysis, success metrics, and deployment guide.
BREAKING CHANGE: Dashboard page completely redesigned with new data structures
This commit is contained in:
249
frontend/src/components/dashboard/ActionQueueCard.tsx
Normal file
249
frontend/src/components/dashboard/ActionQueueCard.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
// ================================================================
|
||||
// frontend/src/components/dashboard/ActionQueueCard.tsx
|
||||
// ================================================================
|
||||
/**
|
||||
* Action Queue Card - What needs your attention right now
|
||||
*
|
||||
* Prioritized list of actions the user needs to take, with context
|
||||
* about why each action is needed and what happens if they don't do it.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FileText,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
Edit,
|
||||
Clock,
|
||||
Euro,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { ActionItem, ActionQueue } from '../../api/hooks/newDashboard';
|
||||
|
||||
interface ActionQueueCardProps {
|
||||
actionQueue: ActionQueue;
|
||||
loading?: boolean;
|
||||
onApprove?: (actionId: string) => void;
|
||||
onViewDetails?: (actionId: string) => void;
|
||||
onModify?: (actionId: string) => void;
|
||||
}
|
||||
|
||||
const urgencyConfig = {
|
||||
critical: {
|
||||
bg: 'bg-red-50',
|
||||
border: 'border-red-300',
|
||||
badge: 'bg-red-100 text-red-800',
|
||||
icon: AlertCircle,
|
||||
},
|
||||
important: {
|
||||
bg: 'bg-amber-50',
|
||||
border: 'border-amber-300',
|
||||
badge: 'bg-amber-100 text-amber-800',
|
||||
icon: AlertCircle,
|
||||
},
|
||||
normal: {
|
||||
bg: 'bg-blue-50',
|
||||
border: 'border-blue-300',
|
||||
badge: 'bg-blue-100 text-blue-800',
|
||||
icon: FileText,
|
||||
},
|
||||
};
|
||||
|
||||
function ActionItemCard({
|
||||
action,
|
||||
onApprove,
|
||||
onViewDetails,
|
||||
onModify,
|
||||
}: {
|
||||
action: ActionItem;
|
||||
onApprove?: (id: string) => void;
|
||||
onViewDetails?: (id: string) => void;
|
||||
onModify?: (id: string) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const config = urgencyConfig[action.urgency];
|
||||
const UrgencyIcon = config.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${config.bg} ${config.border} border-2 rounded-lg p-4 md:p-5 transition-all duration-200 hover:shadow-md`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<UrgencyIcon className={`w-6 h-6 flex-shrink-0 ${config.badge.split(' ')[1]}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 className="font-bold text-lg">{action.title}</h3>
|
||||
<span className={`${config.badge} px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0`}>
|
||||
{action.urgency}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{action.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount (for POs) */}
|
||||
{action.amount && (
|
||||
<div className="flex items-center gap-2 mb-3 text-lg font-bold">
|
||||
<Euro className="w-5 h-5" />
|
||||
<span>
|
||||
{action.amount.toFixed(2)} {action.currency}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reasoning (always visible) */}
|
||||
<div className="bg-white rounded-md p-3 mb-3">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">Why this is needed:</p>
|
||||
<p className="text-sm text-gray-600">{action.reasoning}</p>
|
||||
</div>
|
||||
|
||||
{/* Consequence (expandable) */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-3 w-full"
|
||||
>
|
||||
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
<span className="font-medium">What happens if I don't do this?</span>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 mb-3">
|
||||
<p className="text-sm text-amber-900">{action.consequence}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Time Estimate */}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 mb-4">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Estimated time: {action.estimatedTimeMinutes} min</span>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{action.actions.map((button, index) => {
|
||||
const buttonStyles = {
|
||||
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
|
||||
tertiary: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300',
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (button.action === 'approve' && onApprove) {
|
||||
onApprove(action.id);
|
||||
} else if (button.action === 'view_details' && onViewDetails) {
|
||||
onViewDetails(action.id);
|
||||
} else if (button.action === 'modify' && onModify) {
|
||||
onModify(action.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={handleClick}
|
||||
className={`${buttonStyles[button.type]} px-4 py-2 rounded-lg font-semibold text-sm transition-colors duration-200 flex items-center gap-2 min-h-[44px]`}
|
||||
>
|
||||
{button.action === 'approve' && <CheckCircle2 className="w-4 h-4" />}
|
||||
{button.action === 'view_details' && <Eye className="w-4 h-4" />}
|
||||
{button.action === 'modify' && <Edit className="w-4 h-4" />}
|
||||
{button.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionQueueCard({
|
||||
actionQueue,
|
||||
loading,
|
||||
onApprove,
|
||||
onViewDetails,
|
||||
onModify,
|
||||
}: ActionQueueCardProps) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const displayedActions = showAll ? actionQueue.actions : actionQueue.actions.slice(0, 3);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-32 bg-gray-200 rounded"></div>
|
||||
<div className="h-32 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (actionQueue.actions.length === 0) {
|
||||
return (
|
||||
<div className="bg-green-50 border-2 border-green-200 rounded-xl p-8 text-center">
|
||||
<CheckCircle2 className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-green-900 mb-2">All caught up!</h3>
|
||||
<p className="text-green-700">No actions requiring your attention right now.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">What Needs Your Attention</h2>
|
||||
{actionQueue.totalActions > 3 && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.totalActions} total
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary Badges */}
|
||||
{(actionQueue.criticalCount > 0 || actionQueue.importantCount > 0) && (
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{actionQueue.criticalCount > 0 && (
|
||||
<span className="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.criticalCount} critical
|
||||
</span>
|
||||
)}
|
||||
{actionQueue.importantCount > 0 && (
|
||||
<span className="bg-amber-100 text-amber-800 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
{actionQueue.importantCount} important
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Items */}
|
||||
<div className="space-y-4">
|
||||
{displayedActions.map((action) => (
|
||||
<ActionItemCard
|
||||
key={action.id}
|
||||
action={action}
|
||||
onApprove={onApprove}
|
||||
onViewDetails={onViewDetails}
|
||||
onModify={onModify}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Show More/Less */}
|
||||
{actionQueue.totalActions > 3 && (
|
||||
<button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
className="w-full mt-4 py-3 bg-gray-100 hover:bg-gray-200 rounded-lg font-semibold text-gray-700 transition-colors duration-200"
|
||||
>
|
||||
{showAll
|
||||
? 'Show Less'
|
||||
: `Show ${actionQueue.totalActions - 3} More Action${
|
||||
actionQueue.totalActions - 3 !== 1 ? 's' : ''
|
||||
}`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user