diff --git a/CLAUDE.md b/CLAUDE.md index d2086efa..7b29d400 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,12 @@ Use the Task tool with these specialized subagents for appropriate tasks: - **ALWAYS use Playwright MCP tools** (`mcp__playwright__*`) for browser testing - Test against: `http://localhost:3000` (frontend), `http://localhost:8001` (API) +## Code Guidelines +- Always document non-obvious logic changes with comments + - Why it matters: Future maintainers (including future-you) need to understand intent behind non-obvious code + - What counts: Complex algorithms, surprising workarounds, non-standard patterns, subtle invariants + - What doesn't: Well-named functions/variables that speak for themselves + ## Stack - **Frontend**: Vue 3 + Composition API + Vite (port 3000) - **Backend**: Python FastAPI (port 8001) diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5..2618593c 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,42 +1,50 @@ @@ -317,55 +317,8 @@ export default { diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 00000000..b7871e45 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/client/src/views/Spending.vue b/client/src/views/Spending.vue index 17af4d10..b2dc9c53 100644 --- a/client/src/views/Spending.vue +++ b/client/src/views/Spending.vue @@ -9,21 +9,21 @@
{{ error }}
-
-
+
+
{{ t('finance.totalRevenue') }}
{{ formatCurrency(revenueMetrics.totalRevenue) }}
- + {{ t('finance.fromOrders', { count: revenueMetrics.orderCount }) }}
-
+
{{ t('finance.totalCosts') }}
{{ formatCurrency(totalCosts) }}
{{ t('finance.costBreakdown') }}
-
+
{{ t('finance.netProfit') }}
{{ formatCurrency(netProfit) }}
{{ profitMargin }}% {{ t('finance.margin') }}
@@ -38,7 +38,7 @@
-

{{ t('finance.revenueVsCosts.title') }}

+ {{ t('finance.revenueVsCosts.title') }}
{{ t('finance.revenueVsCosts.revenue') }} {{ t('finance.revenueVsCosts.costs') }} @@ -69,7 +69,7 @@
-

{{ t('finance.monthlyCostFlow.title') }}

+ {{ t('finance.monthlyCostFlow.title') }}
{{ t('finance.monthlyCostFlow.procurement') }} {{ t('finance.monthlyCostFlow.operational') }} @@ -102,11 +102,11 @@
-
+
-

{{ t('finance.categorySpending.title') }}

+ {{ t('finance.categorySpending.title') }}
@@ -128,12 +128,12 @@
-
+
-

{{ t('finance.transactions.title') }}

+ {{ t('finance.transactions.title') }}
-
- +
+
@@ -151,10 +151,10 @@ @click="handleTransactionClick(transaction)" > - - - - + + + +
{{ t('finance.transactions.id') }}{{ transaction.id.toString().padStart(3, '0') }}{{ transaction.description }}{{ transaction.vendor }}{{ formatDateShort(transaction.date) }}{{ currencySymbol }}{{ transaction.amount.toLocaleString() }}{{ transaction.description }}{{ transaction.vendor }}{{ formatDateShort(transaction.date) }}{{ currencySymbol }}{{ transaction.amount.toLocaleString() }}
@@ -513,8 +513,15 @@ export default { font-size: 1rem; } +.stat-meta { + margin-top: 0.5rem; + font-size: 0.813rem; + color: var(--text-secondary); +} + +/* Chart cards */ .chart-card { - margin-bottom: 1.75rem; + margin-bottom: 1.5rem; } .chart-legend { @@ -527,47 +534,55 @@ export default { display: flex; align-items: center; gap: 0.5rem; - color: #64748b; + color: var(--text-secondary); } .legend-dot { width: 12px; height: 12px; border-radius: 3px; + flex-shrink: 0; } -.legend-dot.procurement { background: #3b82f6; } -.legend-dot.operational { background: #8b5cf6; } -.legend-dot.labor { background: #10b981; } -.legend-dot.overhead { background: #f59e0b; } +.legend-dot.procurement { background: #3b82f6; } +.legend-dot.operational { background: #8b5cf6; } +.legend-dot.labor { background: #10b981; } +.legend-dot.overhead { background: #f59e0b; } .legend-dot.revenue-color { background: #0f172a; } -.legend-dot.cost-color { background: #ef4444; } - -.stats-grid-finance { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} +.legend-dot.cost-color { background: #ef4444; } -.revenue-card { - border-left: 4px solid #0f172a; +/* Bar chart shared layout */ +.chart-container { + padding: 1rem 0; } -.cost-card { - border-left: 4px solid #ef4444; +.bar-chart { + display: flex; + gap: 1.5rem; + height: 320px; } -.profit-card { - border-left: 4px solid #3b82f6; +.y-axis { + display: flex; + flex-direction: column; + justify-content: space-between; + padding-right: 1rem; + font-size: 0.75rem; + color: var(--text-muted); + border-right: 1px solid var(--border); + min-width: 48px; + text-align: right; } -.stat-meta { - margin-top: 0.5rem; - font-size: 0.813rem; - color: #64748b; +.chart-area { + flex: 1; + display: flex; + align-items: flex-end; + justify-content: space-around; + gap: 0.5rem; } +/* Revenue vs Cost bars */ .bar-group-revenue { display: flex; flex-direction: column; @@ -587,56 +602,25 @@ export default { padding-bottom: 2rem; } -.revenue-bar, .cost-bar { +.revenue-bar, +.cost-bar { width: 50%; max-width: 30px; border-radius: 6px 6px 0 0; - transition: all 0.3s ease; + transition: opacity 0.2s ease; cursor: pointer; min-height: 4px; } -.revenue-bar { - background: #0f172a; -} - -.cost-bar { - background: #ef4444; -} - -.revenue-bar:hover, .cost-bar:hover { - opacity: 0.8; - transform: scaleY(1.05); -} - -.chart-container { - padding: 1.5rem 0; -} - -.bar-chart { - display: flex; - gap: 1.5rem; - height: 350px; -} - -.y-axis { - display: flex; - flex-direction: column; - justify-content: space-between; - padding-right: 1rem; - font-size: 0.75rem; - color: #94a3b8; - border-right: 1px solid #e2e8f0; -} +.revenue-bar { background: #0f172a; } +.cost-bar { background: #ef4444; } -.chart-area { - flex: 1; - display: flex; - align-items: flex-end; - justify-content: space-around; - gap: 0.5rem; +.revenue-bar:hover, +.cost-bar:hover { + opacity: 0.75; } +/* Stacked cost bars */ .bar-group { display: flex; flex-direction: column; @@ -663,51 +647,49 @@ export default { .bar-segment { width: 100%; - transition: all 0.3s ease; - cursor: pointer; + transition: opacity 0.2s ease; display: block; } -.bar-segment:first-child { - border-radius: 0 0 6px 6px; -} - -.bar-segment:last-child { - border-radius: 6px 6px 0 0; -} +.bar-segment:first-child { border-radius: 0 0 6px 6px; } +.bar-segment:last-child { border-radius: 6px 6px 0 0; } .bar-segment.procurement { background: #3b82f6; } .bar-segment.operational { background: #8b5cf6; } -.bar-segment.labor { background: #10b981; } -.bar-segment.overhead { background: #f59e0b; } - -.bar-segment:hover { - opacity: 0.8; -} +.bar-segment.labor { background: #10b981; } +.bar-segment.overhead { background: #f59e0b; } .bar-label { margin-top: 0.5rem; font-size: 0.75rem; font-weight: 600; - color: #64748b; + color: var(--text-secondary); } -.two-column-grid { +/* Two-column bottom grid */ +.charts-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); - gap: 1.75rem; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; } +@media (max-width: 900px) { + .charts-grid { + grid-template-columns: 1fr; + } +} + +/* Category breakdown */ .category-list { display: flex; flex-direction: column; - gap: 1.5rem; + gap: 1.25rem; } .category-item { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.375rem; } .category-info { @@ -718,135 +700,80 @@ export default { .category-name { font-weight: 600; - color: #0f172a; + color: var(--text-primary); + font-size: 0.875rem; } .category-amount { font-weight: 700; - color: #2563eb; - font-size: 1.125rem; + color: var(--accent); + font-size: 1rem; } .category-bar-container { width: 100%; - height: 8px; + height: 6px; background: #f1f5f9; - border-radius: 4px; + border-radius: 3px; overflow: hidden; } .category-bar { height: 100%; - background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%); - border-radius: 4px; - transition: width 0.6s ease; + background: linear-gradient(90deg, #3b82f6 0%, var(--accent) 100%); + border-radius: 3px; + transition: width 0.5s ease; } .category-meta { display: flex; justify-content: space-between; - font-size: 0.813rem; + font-size: 0.8125rem; } .percentage { - color: #64748b; + color: var(--text-secondary); } .change { font-weight: 600; } -.change.positive { - color: #059669; -} - -.change.negative { - color: #dc2626; -} +.change.positive { color: #059669; } +.change.negative { color: #dc2626; } -.transactions-card { - display: flex; - flex-direction: column; -} - -.transactions-table-container { +/* Transactions table */ +.transactions-scroll { + max-height: 380px; overflow-y: auto; - max-height: 400px; -} - -.transactions-table { - width: 100%; - border-collapse: collapse; -} - -.transactions-table thead { - position: sticky; - top: 0; - background: #f8fafc; - z-index: 1; -} - -.transactions-table th { - text-align: left; - padding: 0.625rem 0.75rem; - font-weight: 600; - color: #475569; - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid #e2e8f0; -} - -.transactions-table th.text-right { - text-align: right; -} - -.transactions-table td { - padding: 0.75rem 0.75rem; - border-bottom: 1px solid #f1f5f9; - font-size: 0.875rem; -} - -.transactions-table tbody tr { - cursor: pointer; - transition: background-color 0.15s ease; -} - -.transactions-table tbody tr:hover { - background: #f8fafc; -} - -.transactions-table tbody tr.clickable-row:hover { - background: #eff6ff; } .transaction-id { - color: #64748b; + color: var(--text-secondary); font-weight: 500; font-family: 'Monaco', 'Courier New', monospace; - font-size: 0.813rem; + font-size: 0.8125rem; } -.transaction-description { - color: #0f172a; - font-weight: 500; +.text-secondary { + color: var(--text-secondary); } -.transaction-vendor { - color: #64748b; +.text-right { + text-align: right; } -.transaction-date { - color: #64748b; - font-size: 0.813rem; +.amount-cell { + font-weight: 700; + color: var(--text-primary); + text-align: right; } -.transaction-amount { - font-weight: 700; - color: #0f172a; +.clickable-row { + cursor: pointer; } -.text-right { - text-align: right; +.clickable-row:hover { + background: #eff6ff !important; } diff --git a/server/main.py b/server/main.py index a0c2d8c5..a8c81f28 100644 --- a/server/main.py +++ b/server/main.py @@ -2,10 +2,14 @@ from fastapi.middleware.cors import CORSMiddleware from typing import List, Optional from pydantic import BaseModel +from datetime import datetime, timedelta from mock_data import inventory_items, orders, demand_forecasts, backlog_items, spending_summary, monthly_spending, category_spending, recent_transactions, purchase_orders app = FastAPI(title="Factory Inventory Management System") +# In-memory restocking orders storage +restocking_orders: list = [] + # Quarter mapping for date filtering QUARTER_MAP = { 'Q1-2025': ['2025-01', '2025-02', '2025-03'], @@ -120,6 +124,25 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingItem(BaseModel): + sku: str + item_name: str + quantity: int + unit_cost: float + +class RestockingOrder(BaseModel): + id: str + items: List[RestockingItem] + total_cost: float + status: str + created_date: str + expected_delivery_date: str + notes: Optional[str] = None + +class CreateRestockingOrderRequest(BaseModel): + items: List[RestockingItem] + notes: Optional[str] = None + # API endpoints @app.get("/") def root(): @@ -304,6 +327,27 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.get("/api/restocking-orders", response_model=List[RestockingOrder]) +def get_restocking_orders(): + """Get all restocking orders""" + return restocking_orders + +@app.post("/api/restocking-orders", response_model=RestockingOrder) +def create_restocking_order(request: CreateRestockingOrderRequest): + """Create a new restocking order""" + now = datetime.utcnow() + order = { + "id": str(len(restocking_orders) + 1), + "items": [item.dict() for item in request.items], + "total_cost": sum(item.quantity * item.unit_cost for item in request.items), + "status": "Processing", + "created_date": now.isoformat(), + "expected_delivery_date": (now + timedelta(days=14)).isoformat(), + "notes": request.notes, + } + restocking_orders.append(order) + return order + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)