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:
330
frontend/src/api/hooks/newDashboard.ts
Normal file
330
frontend/src/api/hooks/newDashboard.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/hooks/newDashboard.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* API Hooks for JTBD-Aligned Dashboard
|
||||
*
|
||||
* Provides data fetching for the redesigned bakery dashboard with focus on:
|
||||
* - Health status
|
||||
* - Action queue
|
||||
* - Orchestration summary
|
||||
* - Production timeline
|
||||
* - Key insights
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../client';
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
export interface HealthChecklistItem {
|
||||
icon: 'check' | 'warning' | 'alert';
|
||||
text: string;
|
||||
actionRequired: boolean;
|
||||
}
|
||||
|
||||
export interface BakeryHealthStatus {
|
||||
status: 'green' | 'yellow' | 'red';
|
||||
headline: string;
|
||||
lastOrchestrationRun: string | null;
|
||||
nextScheduledRun: string;
|
||||
checklistItems: HealthChecklistItem[];
|
||||
criticalIssues: number;
|
||||
pendingActions: number;
|
||||
}
|
||||
|
||||
export interface ReasoningInputs {
|
||||
customerOrders: number;
|
||||
historicalDemand: boolean;
|
||||
inventoryLevels: boolean;
|
||||
aiInsights: boolean;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderSummary {
|
||||
supplierName: string;
|
||||
itemCategories: string[];
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export interface ProductionBatchSummary {
|
||||
productName: string;
|
||||
quantity: number;
|
||||
readyByTime: string;
|
||||
}
|
||||
|
||||
export interface OrchestrationSummary {
|
||||
runTimestamp: string | null;
|
||||
runNumber: number | null;
|
||||
status: string;
|
||||
purchaseOrdersCreated: number;
|
||||
purchaseOrdersSummary: PurchaseOrderSummary[];
|
||||
productionBatchesCreated: number;
|
||||
productionBatchesSummary: ProductionBatchSummary[];
|
||||
reasoningInputs: ReasoningInputs;
|
||||
userActionsRequired: number;
|
||||
durationSeconds: number | null;
|
||||
aiAssisted: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ActionButton {
|
||||
label: string;
|
||||
type: 'primary' | 'secondary' | 'tertiary';
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface ActionItem {
|
||||
id: string;
|
||||
type: string;
|
||||
urgency: 'critical' | 'important' | 'normal';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
reasoning: string;
|
||||
consequence: string;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
actions: ActionButton[];
|
||||
estimatedTimeMinutes: number;
|
||||
}
|
||||
|
||||
export interface ActionQueue {
|
||||
actions: ActionItem[];
|
||||
totalActions: number;
|
||||
criticalCount: number;
|
||||
importantCount: number;
|
||||
}
|
||||
|
||||
export interface ProductionTimelineItem {
|
||||
id: string;
|
||||
batchNumber: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
plannedStartTime: string | null;
|
||||
plannedEndTime: string | null;
|
||||
actualStartTime: string | null;
|
||||
status: string;
|
||||
statusIcon: string;
|
||||
statusText: string;
|
||||
progress: number;
|
||||
readyBy: string | null;
|
||||
priority: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export interface ProductionTimeline {
|
||||
timeline: ProductionTimelineItem[];
|
||||
totalBatches: number;
|
||||
completedBatches: number;
|
||||
inProgressBatches: number;
|
||||
pendingBatches: number;
|
||||
}
|
||||
|
||||
export interface InsightCard {
|
||||
label: string;
|
||||
value: string;
|
||||
detail: string;
|
||||
color: 'green' | 'amber' | 'red';
|
||||
}
|
||||
|
||||
export interface Insights {
|
||||
savings: InsightCard;
|
||||
inventory: InsightCard;
|
||||
waste: InsightCard;
|
||||
deliveries: InsightCard;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Hooks
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get bakery health status
|
||||
*
|
||||
* This is the top-level health indicator showing if the bakery is running smoothly.
|
||||
* Updates every 30 seconds to keep status fresh.
|
||||
*/
|
||||
export function useBakeryHealthStatus(tenantId: string) {
|
||||
return useQuery<BakeryHealthStatus>({
|
||||
queryKey: ['bakery-health-status', tenantId],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get(
|
||||
`/orchestrator/tenants/${tenantId}/dashboard/health-status`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
staleTime: 20000, // Consider stale after 20 seconds
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orchestration summary
|
||||
*
|
||||
* Shows what the automated system did (transparency for trust building).
|
||||
*/
|
||||
export function useOrchestrationSummary(tenantId: string, runId?: string) {
|
||||
return useQuery<OrchestrationSummary>({
|
||||
queryKey: ['orchestration-summary', tenantId, runId],
|
||||
queryFn: async () => {
|
||||
const params = runId ? { run_id: runId } : {};
|
||||
const response = await apiClient.get(
|
||||
`/orchestrator/tenants/${tenantId}/dashboard/orchestration-summary`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 60000, // Summary doesn't change often
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action queue
|
||||
*
|
||||
* Prioritized list of what requires user attention right now.
|
||||
* This is the core JTBD dashboard feature.
|
||||
*/
|
||||
export function useActionQueue(tenantId: string) {
|
||||
return useQuery<ActionQueue>({
|
||||
queryKey: ['action-queue', tenantId],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get(
|
||||
`/orchestrator/tenants/${tenantId}/dashboard/action-queue`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
staleTime: 30000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get production timeline
|
||||
*
|
||||
* Shows today's production schedule in chronological order.
|
||||
*/
|
||||
export function useProductionTimeline(tenantId: string) {
|
||||
return useQuery<ProductionTimeline>({
|
||||
queryKey: ['production-timeline', tenantId],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get(
|
||||
`/orchestrator/tenants/${tenantId}/dashboard/production-timeline`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
staleTime: 30000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key insights
|
||||
*
|
||||
* Glanceable metrics on savings, inventory, waste, and deliveries.
|
||||
*/
|
||||
export function useInsights(tenantId: string) {
|
||||
return useQuery<Insights>({
|
||||
queryKey: ['dashboard-insights', tenantId],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get(
|
||||
`/orchestrator/tenants/${tenantId}/dashboard/insights`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 120000, // Refresh every 2 minutes
|
||||
staleTime: 60000,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Action Mutations
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Approve a purchase order from the action queue
|
||||
*/
|
||||
export function useApprovePurchaseOrder() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, poId }: { tenantId: string; poId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/procurement/tenants/${tenantId}/purchase-orders/${poId}/approve`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: ['action-queue', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['orchestration-summary', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an alert from the action queue
|
||||
*/
|
||||
export function useDismissAlert() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, alertId }: { tenantId: string; alertId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/alert-processor/tenants/${tenantId}/alerts/${alertId}/dismiss`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['action-queue', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a production batch
|
||||
*/
|
||||
export function useStartProductionBatch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, batchId }: { tenantId: string; batchId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/production/tenants/${tenantId}/production-batches/${batchId}/start`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['production-timeline', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a production batch
|
||||
*/
|
||||
export function usePauseProductionBatch() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ tenantId, batchId }: { tenantId: string; batchId: string }) => {
|
||||
const response = await apiClient.post(
|
||||
`/production/tenants/${tenantId}/production-batches/${batchId}/pause`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['production-timeline', variables.tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['bakery-health-status', variables.tenantId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -742,4 +742,29 @@ export {
|
||||
useRunDailyWorkflow,
|
||||
} from './hooks/orchestrator';
|
||||
|
||||
// Hooks - New Dashboard (JTBD-aligned)
|
||||
export {
|
||||
useBakeryHealthStatus,
|
||||
useOrchestrationSummary,
|
||||
useActionQueue,
|
||||
useProductionTimeline,
|
||||
useInsights,
|
||||
useApprovePurchaseOrder as useApprovePurchaseOrderDashboard,
|
||||
useDismissAlert as useDismissAlertDashboard,
|
||||
useStartProductionBatch,
|
||||
usePauseProductionBatch,
|
||||
} from './hooks/newDashboard';
|
||||
|
||||
export type {
|
||||
BakeryHealthStatus,
|
||||
HealthChecklistItem,
|
||||
OrchestrationSummary,
|
||||
ActionQueue,
|
||||
ActionItem,
|
||||
ProductionTimeline,
|
||||
ProductionTimelineItem,
|
||||
Insights,
|
||||
InsightCard,
|
||||
} from './hooks/newDashboard';
|
||||
|
||||
// Note: All query key factories are already exported in their respective hook sections above
|
||||
|
||||
Reference in New Issue
Block a user