Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Factory Inventory Management — Architecture

## System Overview

A full-stack demo application for factory inventory tracking. No database — all data lives in JSON files loaded into memory at server startup.

```
┌─────────────────┐ HTTP/JSON ┌─────────────────┐ import ┌─────────────┐
│ Vue 3 SPA │ ─────────────────> │ FastAPI │ ──────────────> │ JSON files │
│ (port 3000) │ <───────────────── │ (port 8001) │ at startup │ server/data│
└─────────────────┘ └─────────────────┘ └─────────────┘
│ │
│ │
Vite dev server In-memory lists
Vue Router (7 views) Pydantic validation
Axios client List-comprehension filters
```

## Tech Stack

| Layer | Technology | Purpose |
|------------|-------------------------------------|--------------------------------------|
| Frontend | Vue 3 (Composition API) | Reactive UI |
| Router | Vue Router 4 | Client-side navigation |
| HTTP | Axios | API calls |
| Build | Vite 5 | Dev server + bundler |
| Backend | FastAPI | REST API |
| Validation | Pydantic 2 | Response model enforcement |
| Server | Uvicorn | ASGI runtime |
| Data | JSON files (no DB) | Loaded once at import time |
| Testing | pytest + httpx + FastAPI TestClient | Backend only |

## Frontend Structure

```
client/src/
├── views/ 7 route-level pages
│ ├── Dashboard.vue KPI summary + charts
│ ├── Inventory.vue Stock levels by SKU
│ ├── Orders.vue Order list (250 records)
│ ├── Demand.vue Forecasts
│ ├── Backlog.vue Pending items
│ ├── Spending.vue Cost breakdown
│ └── Reports.vue Quarterly + monthly trends
├── components/ 8 modals + FilterBar + ProfileMenu + LanguageSwitcher
├── composables/
│ ├── useFilters.js Shared filter state (warehouse, category, status, month)
│ ├── useAuth.js Auth state
│ └── useI18n.js en/ja locale switching
├── api.js Axios wrapper — builds query strings from filter state
└── App.vue Shell + global styles
```

## Backend Structure

```
server/
├── main.py FastAPI app, 14 routes, Pydantic models, filter helpers
├── mock_data.py Loads all JSON → module-level lists at import time
├── generate_data.py Seed-data generator (dev utility)
└── data/
├── inventory.json 32 items
├── orders.json 250 records
├── demand_forecasts.json 9 records
├── backlog_items.json 4 records
├── spending.json summary + monthly + category
├── transactions.json 56 records
└── purchase_orders.json empty
```

## API Surface

| Endpoint | Filters | Returns |
|--------------------------------|-------------------------------------|-------------------|
| `GET /api/inventory` | warehouse, category | InventoryItem[] |
| `GET /api/inventory/{id}` | — | InventoryItem |
| `GET /api/orders` | warehouse, category, status, month | Order[] |
| `GET /api/orders/{id}` | — | Order |
| `GET /api/demand` | — | DemandForecast[] |
| `GET /api/backlog` | — | BacklogItem[] |
| `GET /api/dashboard/summary` | warehouse, category, status, month | aggregated dict |
| `GET /api/spending/summary` | — | dict |
| `GET /api/spending/monthly` | — | list |
| `GET /api/spending/categories` | — | list |
| `GET /api/spending/transactions`| — | list |
| `GET /api/reports/quarterly` | — | dict |
| `GET /api/reports/monthly-trends`| — | dict |

## Data Flow — Filter Pipeline

This is the core pattern. A single filter change propagates end-to-end:

```
1. User selects "Warehouse B" in FilterBar.vue
2. useFilters.js composable updates reactive ref
3. View component's watch() fires → calls api.getOrders(filters)
4. api.js builds query string: GET /api/orders?warehouse=Warehouse+B
5. main.py apply_filters() → [o for o in orders if o['warehouse'] == 'Warehouse B']
6. Pydantic validates each item against Order model
7. JSON response → Axios → ref update → Vue re-renders
```

**Key insight:** filters are stateless on the server — every request re-filters the full in-memory list. No caching, no pagination. Fine for 250 records; would not scale.

## Filtering Internals (`main.py`)

- `apply_filters(items, warehouse, category, status)` — chained list comprehensions, case-insensitive on category/status
- `filter_by_month(items, month)` — supports quarter tokens (`Q1-2025` → expands to 3 month prefixes) via `QUARTER_MAP`, matched by substring against `order_date`
- `'all'` sentinel = no filter (checked both client and server side)

## Data Loading

`mock_data.py` runs at **import time** — every JSON file is read once when the server process starts. The data lives as module-level Python lists for the lifetime of the process. Editing a JSON file requires a server restart to take effect.
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ npm install && npm run dev

## Key Patterns

**Comments**: Always document non-obvious logic changes with comments

**Filter System**: 4 filters (Time Period, Warehouse, Category, Order Status) apply to all data via query params
**Data Flow**: Vue filters → `client/src/api.js` → FastAPI → In-memory filtering → Pydantic validation → Computed properties
**Reactivity**: Raw data in refs (`allOrders`, `inventoryItems`), derived data in computed properties
Expand Down
3 changes: 3 additions & 0 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
<router-link to="/orders" :class="{ active: $route.path === '/orders' }">
{{ t('nav.orders') }}
</router-link>
<router-link to="/restocking" :class="{ active: $route.path === '/restocking' }">
{{ t('nav.restocking') }}
</router-link>
<router-link to="/spending" :class="{ active: $route.path === '/spending' }">
{{ t('nav.finance') }}
</router-link>
Expand Down
15 changes: 15 additions & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,20 @@ export const api = {
async getPurchaseOrderByBacklogItem(backlogItemId) {
const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`)
return response.data
},

async getRestockingRecommendations(budget) {
const response = await axios.get(`${API_BASE_URL}/restocking/recommendations`, { params: { budget } })
return response.data
},

async placeRestockingOrder(budget, items) {
const response = await axios.post(`${API_BASE_URL}/restocking/orders`, { budget, items })
return response.data
},

async getSubmittedRestockingOrders() {
const response = await axios.get(`${API_BASE_URL}/restocking/orders`)
return response.data
}
}
1 change: 1 addition & 0 deletions client/src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
orders: 'Orders',
finance: 'Finance',
demandForecast: 'Demand Forecast',
restocking: 'Restocking',
companyName: 'Catalyst Components',
subtitle: 'Inventory Management System'
},
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
orders: '注文',
finance: '財務',
demandForecast: '需要予測',
restocking: '補充発注',
companyName: '触媒コンポーネンツ',
subtitle: '在庫管理システム'
},
Expand Down
4 changes: 3 additions & 1 deletion client/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Orders from './views/Orders.vue'
import Demand from './views/Demand.vue'
import Spending from './views/Spending.vue'
import Reports from './views/Reports.vue'
import Restocking from './views/Restocking.vue'

const router = createRouter({
history: createWebHistory(),
Expand All @@ -16,7 +17,8 @@ const router = createRouter({
{ path: '/orders', component: Orders },
{ path: '/demand', component: Demand },
{ path: '/spending', component: Spending },
{ path: '/reports', component: Reports }
{ path: '/reports', component: Reports },
{ path: '/restocking', component: Restocking }
]
})

Expand Down
49 changes: 48 additions & 1 deletion client/src/views/Orders.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@
</div>
</div>

<div v-if="submittedOrders.length > 0" class="card">
<div class="card-header">
<h3 class="card-title">Submitted Restocking Orders ({{ submittedOrders.length }})</h3>
</div>
<div class="table-container">
<table class="orders-table">
<thead>
<tr>
<th>Order #</th>
<th>Items</th>
<th>Total Value</th>
<th>Lead Time</th>
<th>Expected Delivery</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="order in submittedOrders" :key="order.id">
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.items.length }} item(s)</td>
<td><strong>{{ currencySymbol }}{{ order.total_value.toLocaleString() }}</strong></td>
<td>{{ order.lead_time_days }} days</td>
<td>{{ formatDate(order.expected_delivery) }}</td>
<td>
<span class="badge info">{{ order.status }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>

<div class="card">
<div class="card-header">
<h3 class="card-title">{{ t('orders.allOrders') }} ({{ orders.length }})</h3>
Expand Down Expand Up @@ -95,6 +127,16 @@ export default {
const loading = ref(true)
const error = ref(null)
const orders = ref([])
const submittedOrders = ref([])

const loadSubmittedOrders = async () => {
try {
submittedOrders.value = await api.getSubmittedRestockingOrders()
} catch (err) {
// Restocking orders are supplementary; don't block the main view on failure
submittedOrders.value = []
}
}

// Use shared filters
const {
Expand Down Expand Up @@ -127,6 +169,7 @@ export default {
// Watch for filter changes and reload data
watch([selectedPeriod, selectedLocation, selectedCategory, selectedStatus], () => {
loadOrders()
loadSubmittedOrders()
})

const getOrdersByStatus = (status) => {
Expand All @@ -153,13 +196,17 @@ export default {
})
}

onMounted(loadOrders)
onMounted(() => {
loadOrders()
loadSubmittedOrders()
})

return {
t,
loading,
error,
orders,
submittedOrders,
getOrdersByStatus,
getOrderStatusClass,
formatDate,
Expand Down
Loading