3v324v23 commited on
Commit
e3fd5ec
·
1 Parent(s): 8b8b6f5

feat: initial MERN inventory app (backend + frontend)

Browse files
.gitignore CHANGED
@@ -1,7 +1,41 @@
1
- node_modules
2
- dist
3
- .vercel
4
- # envs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  .env
 
 
6
  backend/.env
7
  frontend/.env
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+ pnpm-debug.log*
7
+
8
+ # Builds
9
+ dist/
10
+ build/
11
+ frontend/dist/
12
+ backend/dist/
13
+
14
+ # Logs
15
+ logs/
16
+ *.log
17
+ *.log.*
18
+ pids/
19
+ *.pid
20
+ *.seed
21
+ *.pid.lock
22
+
23
+ # Env (keep examples only)
24
  .env
25
+ .env.*
26
+ !.env.example
27
  backend/.env
28
  frontend/.env
29
+
30
+ # CSV scratch
31
+ products.csv
32
+ exported.csv
33
+
34
+ # OS/editor
35
+ .DS_Store
36
+ Thumbs.db
37
+ .vscode/
38
+ .idea/
39
+
40
+ # Coverage
41
+ coverage/
README.md CHANGED
@@ -1 +1,330 @@
1
- "# Inventory Pro"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Here you go — a complete **`README.md`** you can copy-paste as-is.
2
+
3
+ ---
4
+
5
+ # Inventory Pro — MERN (Assignment)
6
+
7
+ A full-stack inventory app that hits the assignment spec end-to-end:
8
+
9
+ * 🔎 Search & filter, 📥 Import / 📤 Export CSV, ✏️ inline row editing, 🧾 inventory history
10
+ * 💎 Clean, responsive UI (table on desktop, cards on mobile)
11
+ * 🧰 Robust CSV parser (BOM, comma/semicolon/tab), optimistic updates
12
+ * 🔐 Production-friendly CORS, Express **v5** compatible preflight
13
+ * ☁️ Ready to deploy: Backend → Render, Frontend → Netlify
14
+
15
+ ---
16
+
17
+ ## Tech Stack
18
+
19
+ * **Backend:** Node.js, Express **v5**, Mongoose, Multer, csv-parse, json2csv, express-validator, Helmet, Morgan, CORS
20
+ * **Frontend:** React + TypeScript + Vite, Tailwind CSS, @tanstack/react-query v5, axios, lucide-react
21
+ * **Database:** MongoDB (Atlas or local)
22
+
23
+ ---
24
+
25
+ ## Project Structure
26
+
27
+ ```
28
+ inventory-pro/
29
+ backend/
30
+ src/
31
+ config/
32
+ controllers/
33
+ middleware/
34
+ models/
35
+ routes/
36
+ utils/
37
+ server.js
38
+ .env
39
+ package.json
40
+ frontend/
41
+ src/
42
+ components/
43
+ lib/
44
+ App.tsx
45
+ main.tsx
46
+ index.css
47
+ .env
48
+ index.html
49
+ netlify.toml # (optional, for Netlify)
50
+ package.json
51
+ README.md
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Prerequisites
57
+
58
+ * Node.js 18+ (recommended 20+)
59
+ * MongoDB: **Atlas** connection string OR local `mongod`
60
+
61
+ ---
62
+
63
+ ## 1) Run Locally
64
+
65
+ ### A) Backend
66
+
67
+ 1. Create `backend/.env`:
68
+
69
+ ```env
70
+ PORT=4000
71
+ # Choose ONE:
72
+ # MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.no4gl81.mongodb.net/inventorydb?retryWrites=true&w=majority&appName=Cluster0
73
+ MONGODB_URI=mongodb://127.0.0.1:27017/inventorydb
74
+
75
+ # Allow both common dev origins
76
+ CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
77
+ ```
78
+
79
+ > If your password has special characters (`@`, `#`, `!`, etc.), URL-encode them (`@` → `%40`).
80
+
81
+ 2. Install & run:
82
+
83
+ ```bash
84
+ cd backend
85
+ npm i
86
+ npm run dev
87
+ ```
88
+
89
+ Expected log:
90
+
91
+ ```
92
+ ✅ MongoDB connected
93
+ 🚀 API running on http://localhost:4000
94
+ ```
95
+
96
+ Health checks in browser:
97
+
98
+ * [http://localhost:4000/health](http://localhost:4000/health) → `{"ok":true,"db":"connected"}`
99
+ * [http://localhost:4000/api/products](http://localhost:4000/api/products) → JSON list
100
+
101
+ > **Express v5 note:** Preflight is handled safely (no wildcard `*` route needed).
102
+
103
+ ---
104
+
105
+ ### B) Frontend
106
+
107
+ 1. Create `frontend/.env`:
108
+
109
+ ```env
110
+ VITE_API_BASE=http://localhost:4000
111
+ ```
112
+
113
+ 2. Install & run:
114
+
115
+ ```bash
116
+ cd frontend
117
+ npm i
118
+ npm run dev
119
+ ```
120
+
121
+ Open the app (Vite prints the URL, usually [http://localhost:5173](http://localhost:5173)).
122
+
123
+ * Top-right **API / DB** pills should be green ✅ when backend is running.
124
+ * Hover them to see the API base URL in use.
125
+
126
+ ---
127
+
128
+ ## 2) CSV Format (Import/Export)
129
+
130
+ **Headers (required):**
131
+ `name,unit,category,brand,stock,status,image`
132
+
133
+ * `status` is optional; if omitted, it’s derived from `stock` (0 → OUT\_OF\_STOCK, >0 → IN\_STOCK)
134
+ * Parser accepts **comma**, **semicolon**, or **tab** delimited CSV
135
+ * BOM/Excel CSVs are handled
136
+
137
+ **Example:**
138
+
139
+ ```csv
140
+ name,unit,category,brand,stock,status,image
141
+ Coffee Beans,kg,Beverages,Acme,50,IN_STOCK,https://picsum.photos/seed/coffee/200
142
+ Tea Bags,box,Beverages,Acme,0,OUT_OF_STOCK,https://picsum.photos/seed/tea/200
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 3) Quick Tests (curl / PowerShell)
148
+
149
+ **Create a CSV quickly (PowerShell):**
150
+
151
+ ```powershell
152
+ @'
153
+ name,unit,category,brand,stock,status,image
154
+ Coffee Beans,kg,Beverages,Acme,50,IN_STOCK,https://picsum.photos/seed/coffee/200
155
+ Tea Bags,box,Beverages,Acme,0,OUT_OF_STOCK,https://picsum.photos/seed/tea/200
156
+ Sugar,kg,Grocery,SweetCo,100,IN_STOCK,https://picsum.photos/seed/sugar/200
157
+ Milk,litre,Dairy,DairyBest,20,IN_STOCK,https://picsum.photos/seed/milk/200
158
+ '@ | Set-Content -Encoding UTF8 ".\products.csv"
159
+ ```
160
+
161
+ **Import:**
162
+
163
+ ```bash
164
+ curl.exe -s -X POST "http://localhost:4000/api/products/import" -F "file=@products.csv"
165
+ # → {"addedCount":4,"skipped":[...if duplicates...]}
166
+ ```
167
+
168
+ **List:**
169
+
170
+ ```bash
171
+ curl.exe -s "http://localhost:4000/api/products?page=1&limit=10"
172
+ ```
173
+
174
+ **Search (assignment alias):**
175
+
176
+ ```bash
177
+ curl.exe -s "http://localhost:4000/api/products/search?name=Tea"
178
+ ```
179
+
180
+ **Update stock (PUT):**
181
+
182
+ ```powershell
183
+ # get first id
184
+ $resp = Invoke-RestMethod -Uri "http://localhost:4000/api/products?limit=1" -Method GET
185
+ $id = $resp.data[0]._id
186
+ Invoke-RestMethod -Uri "http://localhost:4000/api/products/$id" -Method PUT -ContentType "application/json" -Body '{"stock":75}'
187
+ ```
188
+
189
+ **History:**
190
+
191
+ ```bash
192
+ curl.exe -s "http://localhost:4000/api/products/<id>/history"
193
+ ```
194
+
195
+ **Export:**
196
+
197
+ ```bash
198
+ curl.exe -L "http://localhost:4000/api/products/export" -o exported.csv
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 4) API Reference
204
+
205
+ Base URL (dev): `http://localhost:4000/api`
206
+
207
+ | Method | Path | Description |
208
+ | -----: | ----------------------- | ------------------------------------------------------ |
209
+ | GET | `/products` | List (pagination, sorting; filters `name`, `category`) |
210
+ | GET | `/products/search` | Alias of list supporting `?name=` |
211
+ | POST | `/products/import` | Import CSV (multipart: `file`) |
212
+ | GET | `/products/export` | Export all products as CSV |
213
+ | PUT | `/products/:id` | Update editable fields (validates) |
214
+ | GET | `/products/:id/history` | Inventory change logs (sorted desc) |
215
+
216
+ ---
217
+
218
+ ## 5) Assignment Mapping (Scoring)
219
+
220
+ **Frontend (50 pts)**
221
+
222
+ * Search bar (`/api/products/search?name=` alias) + category filter ✅
223
+ * “Add New Product” button (CSV one-row import) ✅
224
+ * Import/Export buttons aligned right (Export downloads; Import uploads & refreshes) ✅
225
+ * Products table columns: Image, Name, Unit, Category, Brand, Stock, Status, Actions ✅
226
+ * Stock labels: green “In Stock” when `>0`, red “Out of Stock” when `0` ✅
227
+ * Actions: Edit/Delete; inline editing with Save → PUT `/api/products/:id` ✅
228
+ * Table updates without full page refresh (optimistic) ✅
229
+ * Inventory history sidebar (fetch logs desc by date) ✅
230
+ * Responsive/mobile layout ✅
231
+
232
+ **Backend (50 pts)**
233
+
234
+ * Import CSV: accept file, parse columns, insert only new, return `addedCount` + `skipped` (duplicate info for editing) ✅
235
+ * Export CSV: returns all products ✅
236
+ * Products list: JSON with pagination & sorting ✅
237
+ * Update product: validation (unique name, numeric stock, status enum), updates only editable fields ✅
238
+ * Inventory history: logs stock changes; fetch API returns desc ✅
239
+
240
+ ---
241
+
242
+ ## 6) Deployment
243
+
244
+ ### A) Backend → Render
245
+
246
+ 1. Push repo to GitHub.
247
+ 2. Render: **New → Web Service** → connect `backend/`
248
+ 3. **Build Command:** `npm install`
249
+ **Start Command:** `node src/server.js`
250
+ 4. **Environment variables:**
251
+
252
+ * `MONGODB_URI` → your Atlas URI (e.g., `mongodb+srv://.../inventorydb?...`)
253
+ * `CORS_ORIGIN` → `https://<your-netlify>.netlify.app`
254
+
255
+ * (Optionally add dev: `http://localhost:5173,http://127.0.0.1:5173`)
256
+ 5. Deploy. Verify: `https://<render-app>.onrender.com/health` → `{ "ok": true, "db": "connected" }`.
257
+
258
+ > Express reads `process.env.PORT` automatically on Render.
259
+
260
+ ### B) Frontend → Netlify
261
+
262
+ 1. In `frontend/.env` set:
263
+
264
+ ```env
265
+ VITE_API_BASE=https://<render-app>.onrender.com
266
+ ```
267
+
268
+ 2. (Optional) Add `frontend/netlify.toml`:
269
+
270
+ ```toml
271
+ [build]
272
+ command = "npm run build"
273
+ publish = "dist"
274
+
275
+ [[redirects]]
276
+ from = "/*"
277
+ to = "/index.html"
278
+ status = 200
279
+ ```
280
+
281
+ 3. Netlify → **New site from Git**
282
+
283
+ * Base directory: `frontend`
284
+ * Build command: `npm run build`
285
+ * Publish directory: `dist`
286
+ * Env var: `VITE_API_BASE=https://<render-app>.onrender.com`
287
+
288
+ ---
289
+
290
+ ## 7) Troubleshooting
291
+
292
+ * **Frontend “failed to fetch” / red API pill**
293
+
294
+ * Backend not running or URL mismatch. Check `frontend/.env` and restart `npm run dev`.
295
+ * CORS: ensure your frontend origin is in `CORS_ORIGIN`.
296
+
297
+ * **Import says “No valid rows found in CSV”**
298
+
299
+ * Headers must match exactly: `name,unit,category,brand,stock,status,image`.
300
+ * At least `name`, `unit`, `category` must be non-empty for a row to import.
301
+ * Excel CSVs with semicolons are supported.
302
+
303
+ * **History not updating**
304
+
305
+ * Only changes to `stock` create history entries.
306
+
307
+ ---
308
+
309
+ ## 8) Scripts
310
+
311
+ **Backend**
312
+
313
+ ```bash
314
+ npm run dev # nodemon dev server
315
+ npm start # node src/server.js
316
+ ```
317
+
318
+ **Frontend**
319
+
320
+ ```bash
321
+ npm run dev # Vite dev server
322
+ npm run build # production build
323
+ npm run preview # serve built app locally
324
+ ```
325
+
326
+ ---
327
+
328
+ ## License
329
+
330
+ MIT
backend/.env.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ PORT=4000
2
+ # Use ONE of the following:
3
+ # MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.no4gl81.mongodb.net/inventorydb?retryWrites=true&w=majority&appName=Cluster0
4
+ MONGODB_URI=mongodb://127.0.0.1:27017/inventorydb
5
+ CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
backend/src/app.js DELETED
@@ -1,61 +0,0 @@
1
- import express from "express";
2
- import cors from "cors";
3
- import helmet from "helmet";
4
- import morgan from "morgan";
5
- import mongoose from "mongoose";
6
- import productsRouter from "./routes/products.routes.js";
7
- import { connectDB } from "./db.js";
8
-
9
- const app = express();
10
-
11
- /* ---------- CORS ---------- */
12
- const allowed = (process.env.CORS_ORIGIN || "http://localhost:5173,http://127.0.0.1:5173")
13
- .split(",")
14
- .map((s) => s.trim())
15
- .filter(Boolean);
16
-
17
- const corsMiddleware = cors({
18
- origin: allowed.includes("*")
19
- ? true
20
- : (origin, cb) => {
21
- // allow server-to-server/curl (no Origin header)
22
- const ok = !origin || allowed.includes(origin);
23
- cb(null, ok);
24
- },
25
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
26
- allowedHeaders: ["Content-Type", "Authorization"],
27
- credentials: false,
28
- });
29
-
30
- app.use(corsMiddleware);
31
-
32
- /* ✅ Express 5-safe generic preflight */
33
- app.use((req, res, next) => {
34
- if (req.method === "OPTIONS") return res.sendStatus(204);
35
- next();
36
- });
37
-
38
- /* ---------- Security & parsing ---------- */
39
- app.use(helmet());
40
- app.use(morgan("dev"));
41
- app.use(express.json({ limit: "1mb" }));
42
-
43
- /* ---------- Ensure DB connection on first request (serverless-safe) ---------- */
44
- app.use(async (_req, _res, next) => {
45
- if (mongoose.connection.readyState !== 1) {
46
- await connectDB();
47
- }
48
- next();
49
- });
50
-
51
- /* ---------- Health ---------- */
52
- app.get("/health", (_req, res) => {
53
- const states = ["disconnected", "connected", "connecting", "disconnecting"];
54
- const db = states[mongoose.connection.readyState] || "unknown";
55
- res.json({ ok: true, db });
56
- });
57
-
58
- /* ---------- API routes ---------- */
59
- app.use("/api/products", productsRouter);
60
-
61
- export default app;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/src/db.js DELETED
@@ -1,17 +0,0 @@
1
- import mongoose from "mongoose";
2
-
3
- const MONGODB_URI = process.env.MONGODB_URI;
4
- if (!MONGODB_URI) {
5
- console.error("❌ Missing MONGODB_URI env");
6
- }
7
-
8
- let cached = global.__mongoose_conn;
9
-
10
- /** Serverless-safe shared connection */
11
- export async function connectDB() {
12
- if (!cached) {
13
- cached = mongoose.connect(MONGODB_URI);
14
- global.__mongoose_conn = cached;
15
- }
16
- return cached;
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Backend base URL
2
+ VITE_API_BASE=http://localhost:4000