File size: 8,463 Bytes
db2ad38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import React, { useState, useEffect } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';

const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';

const STORES = [
  { id: 'store_001', name: 'Patia Dark Store' },
  { id: 'store_002', name: 'Infocity Hub' },
  { id: 'store_003', name: 'Saheed Nagar Node' },
];

export default function DarkStoreIntel() {
  const [forecast, setForecast] = useState(null);
  const [storeHealth, setStoreHealth] = useState(null);
  const [selectedStore, setSelectedStore] = useState('store_001');
  const [loading, setLoading] = useState(true);

  const fetchData = async (storeId) => {
    setLoading(true);
    try {
      const [fcRes, healthRes] = await Promise.all([
        fetch(`${API_BASE}/api/ml/demand-forecast?store_id=${storeId}`),
        fetch(`${API_BASE}/api/ml/store-health`),
      ]);
      setForecast(await fcRes.json());
      setStoreHealth(await healthRes.json());
    } catch {
      // Backend not running — show placeholder
    }
    setLoading(false);
  };

  useEffect(() => { fetchData(selectedStore); }, [selectedStore]);

  const currentStore = storeHealth?.stores?.find(s => s.id === selectedStore);
  const chartData = forecast?.forecast?.map(f => ({
    name: f.label,
    demand: f.predicted_units,
    lower: f.lower_ci,
    upper: f.upper_ci,
    isPeak: f.is_peak,
  })) || [];

  return (
    <div className="page">
      {/* Header */}
      <div className="page-header">
        <div>
          <div className="page-title">Dark Store Intel</div>
          <div className="page-subtitle">
            {forecast?.model || 'Heteroscedastic Tobit Regression (Type I Right-Censored)'} · Live demand forecasting
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          {STORES.map(s => (
            <button
              key={s.id}
              className={`btn ${selectedStore === s.id ? 'btn-primary' : 'btn-ghost'}`}
              onClick={() => setSelectedStore(s.id)}
              style={{ fontSize: 12, padding: '7px 14px' }}
            >
              {s.name}
            </button>
          ))}
        </div>
      </div>

      {/* KPI row */}
      <div className="grid-4">
        <div className="kpi-card">
          <div className="kpi-label">Health Score</div>
          <div className="kpi-value" style={{ color: 'var(--accent)' }}>
            {loading ? '—' : currentStore?.health_score?.toFixed(1) ?? '—'}
          </div>
          <div className="kpi-change">/ 100 composite</div>
        </div>
        <div className="kpi-card">
          <div className="kpi-label">Active Orders</div>
          <div className="kpi-value">{loading ? '—' : currentStore?.active_orders ?? '—'}</div>
          <div className="kpi-change">live queue</div>
        </div>
        <div className="kpi-card">
          <div className="kpi-label">Avg Fill Time</div>
          <div className="kpi-value">{loading ? '—' : `${currentStore?.avg_fill_time_min ?? '—'}m`}</div>
          <div className="kpi-change">pick + pack</div>
        </div>
        <div className="kpi-card">
          <div className="kpi-label">Model R²</div>
          <div className="kpi-value" style={{ color: 'var(--primary)' }}>
            {forecast?.model_rsq ?? '—'}
          </div>
          <div className="kpi-change">Tobit fit quality</div>
        </div>
      </div>

      {/* Stock health bars */}
      {currentStore && (
        <div className="glass" style={{ padding: 20 }}>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--on-surface-variant)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 14 }}>
            Stock Distribution
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <StockBar label="In Stock" pct={currentStore.in_stock_pct} color="var(--accent)" />
            <StockBar label="Low Stock" pct={currentStore.low_stock_pct} color="var(--warning)" />
            <StockBar label="Out of Stock" pct={currentStore.out_stock_pct} color="var(--danger)" />
          </div>
        </div>
      )}

      {/* Demand forecast chart */}
      <div className="glass" style={{ padding: 20, flex: 1, minHeight: 260 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
          <div>
            <div style={{ fontSize: 13, fontWeight: 600 }}>24-Hour Demand Forecast</div>
            <div style={{ fontSize: 11, color: 'var(--on-surface-variant)', marginTop: 2 }}>
              Predicted units with 95% confidence interval · Peak hours highlighted
            </div>
          </div>
          <span className="badge badge-green">Model live</span>
        </div>
        {loading ? (
          <div style={{ height: 220, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-surface-variant)', fontSize: 13 }}>
            Loading forecast...
          </div>
        ) : (
          <ResponsiveContainer width="100%" height={220}>
            <AreaChart data={chartData} margin={{ top: 5, right: 10, left: -10, bottom: 0 }}>
              <defs>
                <linearGradient id="demandGrad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="#FF0077" stopOpacity={0.3} />
                  <stop offset="95%" stopColor="#FF0077" stopOpacity={0} />
                </linearGradient>
                <linearGradient id="upperGrad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="#00E475" stopOpacity={0.1} />
                  <stop offset="95%" stopColor="#00E475" stopOpacity={0} />
                </linearGradient>
              </defs>
              <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
              <XAxis dataKey="name" tick={{ fontSize: 10, fill: '#6b7280' }} interval={3} />
              <YAxis tick={{ fontSize: 10, fill: '#6b7280' }} />
              <Tooltip
                contentStyle={{ background: '#131316', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, fontSize: 12 }}
                labelStyle={{ color: '#e5e1e6' }}
              />
              <Area type="monotone" dataKey="upper" stroke="transparent" fill="url(#upperGrad)" />
              <Area type="monotone" dataKey="demand" stroke="#FF0077" strokeWidth={2} fill="url(#demandGrad)" dot={false} />
              <Area type="monotone" dataKey="lower" stroke="transparent" fill="transparent" />
            </AreaChart>
          </ResponsiveContainer>
        )}
      </div>

      {/* All stores overview */}
      {storeHealth && (
        <div className="glass" style={{ padding: 20 }}>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--on-surface-variant)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 12 }}>
            All Stores
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {storeHealth.stores.map(s => (
              <div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 14px', background: 'rgba(255,255,255,0.02)', borderRadius: 10, border: '1px solid var(--border-glass)' }}>
                <span className="status-dot green" />
                <span style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>{s.name}</span>
                <span style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--accent)' }}>{s.health_score}</span>
                <span className="badge badge-gray">{s.active_orders} orders</span>
                <span className="badge badge-green">{s.in_stock_pct}% in stock</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function StockBar({ label, pct, color }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
      <div style={{ width: 100, fontSize: 12, color: 'var(--on-surface-variant)' }}>{label}</div>
      <div style={{ flex: 1, height: 8, background: 'rgba(255,255,255,0.06)', borderRadius: 4, overflow: 'hidden' }}>
        <div style={{ width: `${pct}%`, height: '100%', background: color, borderRadius: 4, transition: 'width 0.8s ease' }} />
      </div>
      <div style={{ width: 40, fontSize: 12, fontFamily: 'var(--font-mono)', color, textAlign: 'right' }}>{pct}%</div>
    </div>
  );
}