Skip to content

Commit a11b6b5

Browse files
Add Engineer Inspection section and blockchain hash demo
1 parent c1d700f commit a11b6b5

File tree

5 files changed

+307
-1
lines changed

5 files changed

+307
-1
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Deploy to Vercel
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
7+
jobs:
8+
build_and_deploy:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v4
13+
14+
- name: Use Node.js
15+
uses: actions/setup-node@v4
16+
with:
17+
node-version: 18
18+
19+
- name: Install dependencies
20+
run: npm ci
21+
22+
- name: Build
23+
run: npm run build
24+
25+
- name: Deploy to Vercel
26+
uses: amondnet/vercel-action@v20
27+
with:
28+
vercel-token: ${{ secrets.VERCEL_TOKEN }}
29+
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
30+
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
31+
vercel-args: '--prod'

App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Dashboard from './pages/Dashboard';
66
import CreateInvoice from './pages/CreateInvoice';
77
import Marketplace from './pages/Marketplace';
88
import InvoiceDetail from './pages/InvoiceDetail';
9+
import EngineerCheck from './pages/EngineerCheck';
910
import { WalletProvider } from './contexts/WalletContext';
1011
import { RainbowKitModal } from './components/RainbowKit';
1112

@@ -19,6 +20,9 @@ export default function App() {
1920
<Route path="dashboard" element={<Dashboard />} />
2021
<Route path="invoices/new" element={<CreateInvoice />} />
2122
<Route path="invoices/:id" element={<InvoiceDetail />} />
23+
<Route path="engineer" element={<EngineerCheck />} />
24+
25+
<Route path="inspection/new" element={<EngineerCheck />} />
2226
<Route path="marketplace" element={<Marketplace />} />
2327
{/* Fallback routes */}
2428
<Route path="invoices" element={<Dashboard />} />

DEPLOY_VERCEL.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Deploying invoicechain to Vercel
2+
3+
This document shows two ways to deploy the `invoicechain` Vite app to Vercel:
4+
5+
- Manual (recommended quick start)
6+
- Automated (GitHub Actions)
7+
8+
Prerequisites
9+
- Node.js (>=16)
10+
- npm
11+
- A Vercel account and the project created in your Vercel dashboard (optional if using the GitHub integration)
12+
13+
Manual deploy (quick)
14+
1. Build the app locally:
15+
16+
```bash
17+
npm ci
18+
npm run build
19+
```
20+
21+
2. From the Vercel dashboard, import the repository (or drag & drop the project folder) and set the build settings:
22+
23+
- Build command: `npm run build`
24+
- Output directory: `dist`
25+
26+
3. Deploy — Vercel will run the build and publish the site.
27+
28+
Automated deploy with GitHub Actions
29+
30+
1. Add the following repository secrets in GitHub (Repository → Settings → Secrets & variables → Actions):
31+
32+
- `VERCEL_TOKEN` — a personal token from Vercel (Account Settings → Tokens)
33+
- `VERCEL_ORG_ID` — your Vercel organization ID (found in project settings or Vercel dashboard)
34+
- `VERCEL_PROJECT_ID` — your Vercel project ID (found in project settings)
35+
36+
2. The provided GitHub Action workflow (`.github/workflows/deploy-vercel.yml`) will:
37+
38+
- checkout the repo
39+
- install dependencies (`npm ci`)
40+
- run `npm run build`
41+
- call Vercel to deploy to production
42+
43+
Notes
44+
- Your project already uses Vite. The default build output directory is `dist`, which Vercel detects automatically when you set the build command as above.
45+
- If your app uses any runtime secrets, configure them in the Vercel dashboard or set them as GitHub Secrets for the action.
46+
47+
Hindi (हिंदी में संक्षेप)
48+
49+
तेज़ शुरुआत के लिए:
50+
1. लोकल बिल्ड करें: `npm ci` और `npm run build`
51+
2. Vercel डैशबोर्ड पर अपना रिपॉज़िटरी इम्पोर्ट करें।
52+
3. Build command: `npm run build`, Output directory: `dist` — फिर Deploy बटन दबाएँ।
53+
54+
ऑटो-डिप्लॉय (GitHub Actions) के लिए GitHub में तीन Secrets जोड़ें: `VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID`
55+
56+
---
57+
If you want, I can also add a Netlify `netlify.toml` or a GitHub Action that deploys to GitHub Pages instead — tell me which provider you prefer next.

components/Layout.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
Store,
1616
LogOut,
1717
ChevronDown,
18-
UserCircle
18+
UserCircle,
19+
ShieldCheck
1920
} from 'lucide-react';
2021
import { Button } from './ui/LayoutPrimitives';
2122
import { CURRENT_USER } from '../constants';
@@ -114,6 +115,16 @@ export default function Layout() {
114115
Finance
115116
</div>
116117
<SidebarItem to="/wallet" icon={Wallet} label="Wallet & Settlements" />
118+
<div className="mt-8 mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-text-tertiary">
119+
Engineer
120+
</div>
121+
122+
<SidebarItem
123+
to="/engineer"
124+
icon={ShieldCheck}
125+
label="Engineer Inspection"
126+
/>
127+
117128

118129
<div className="mt-8 mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-text-tertiary">
119130
Funder Mode (Demo)

pages/EngineerCheck.tsx

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import React, { useState } from 'react';
2+
3+
export default function EngineerCheck() {
4+
const [engineerId, setEngineerId] = useState('');
5+
const [warehouseId, setWarehouseId] = useState('');
6+
const [location, setLocation] = useState('');
7+
const [rawMaterial, setRawMaterial] = useState('');
8+
const [quantity, setQuantity] = useState<number | ''>('');
9+
const [quality, setQuality] = useState('Good');
10+
const [vendorName, setVendorName] = useState('');
11+
const [vendorContact, setVendorContact] = useState('');
12+
const [temperature, setTemperature] = useState<number | ''>('');
13+
const [humidity, setHumidity] = useState<number | ''>('');
14+
const [weight, setWeight] = useState<number | ''>('');
15+
const [notes, setNotes] = useState('');
16+
const [photos, setPhotos] = useState<FileList | null>(null);
17+
const [videos, setVideos] = useState<FileList | null>(null);
18+
const [status, setStatus] = useState<string | null>(null);
19+
const [blockchainHash, setBlockchainHash] = useState<string | null>(null);
20+
const inputStyle = {
21+
color: '#000000e8',
22+
backgroundColor: '#d13c3c'
23+
};
24+
25+
async function filesToDataURLs(files: FileList | null) {
26+
if (!files) return [];
27+
const arr = Array.from(files);
28+
const results = await Promise.all(
29+
arr.map(
30+
(f) =>
31+
new Promise((res) => {
32+
const reader = new FileReader();
33+
reader.onload = () => res({ name: f.name, type: f.type, data: reader.result });
34+
reader.onerror = () => res(null);
35+
reader.readAsDataURL(f);
36+
})
37+
)
38+
);
39+
return results.filter(Boolean);
40+
}
41+
async function generateHash(data: any) {
42+
const encoder = new TextEncoder();
43+
const encoded = encoder.encode(JSON.stringify(data));
44+
const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
45+
const hashArray = Array.from(new Uint8Array(hashBuffer));
46+
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
47+
}
48+
49+
async function handleSubmit(e: React.FormEvent) {
50+
e.preventDefault();
51+
setStatus('Uploading...');
52+
53+
const photoData = await filesToDataURLs(photos);
54+
const videoData = await filesToDataURLs(videos);
55+
56+
const payload = {
57+
engineerId,
58+
warehouseId,
59+
location,
60+
sensorData: {
61+
temperature: temperature === '' ? null : Number(temperature),
62+
humidity: humidity === '' ? null : Number(humidity),
63+
weight: weight === '' ? null : Number(weight)
64+
},
65+
result: {
66+
rawMaterial,
67+
quantity: quantity === '' ? null : Number(quantity),
68+
quality,
69+
vendor: { name: vendorName, contact: vendorContact },
70+
notes,
71+
media: { photos: photoData, videos: videoData }
72+
}
73+
};
74+
const hash = await generateHash(payload);
75+
setBlockchainHash(hash);
76+
77+
try {
78+
const res = await fetch('/api/inspection/create', {
79+
method: 'POST',
80+
headers: { 'Content-Type': 'application/json' },
81+
body: JSON.stringify(payload)
82+
});
83+
84+
const json = await res.json();
85+
if (!res.ok) throw new Error(json.error || 'Upload failed');
86+
setStatus('Submitted ✓');
87+
} catch (err: any) {
88+
setStatus('Error: ' + (err.message || err));
89+
}
90+
}
91+
92+
return (
93+
<div style={{ padding: 24, maxWidth: 900, margin: '0 auto' }}>
94+
<h1>Engineer Inspection — Submit Check</h1>
95+
<form onSubmit={handleSubmit}>
96+
<label>Engineer ID<br />
97+
<input value={engineerId} onChange={(e) => setEngineerId(e.target.value)} required />
98+
</label>
99+
100+
<br />
101+
<label>Warehouse ID<br />
102+
<input value={warehouseId} onChange={(e) => setWarehouseId(e.target.value)} required />
103+
</label>
104+
105+
<br />
106+
<label>Location<br />
107+
<input value={location} onChange={(e) => setLocation(e.target.value)} required />
108+
</label>
109+
110+
<br />
111+
<label>Raw material type<br />
112+
<input value={rawMaterial} onChange={(e) => setRawMaterial(e.target.value)} />
113+
</label>
114+
115+
<br />
116+
<label>Quantity<br />
117+
<input type="number" value={quantity as any} onChange={(e) => setQuantity(e.target.value === '' ? '' : Number(e.target.value))} />
118+
</label>
119+
120+
<br />
121+
<label>Quality<br />
122+
<select value={quality} onChange={(e) => setQuality(e.target.value)}>
123+
<option>Good</option>
124+
<option>Acceptable</option>
125+
<option>Poor</option>
126+
</select>
127+
</label>
128+
129+
<br />
130+
<fieldset style={{ marginTop: 8 }}>
131+
<legend>Vendor</legend>
132+
<label>Name<br />
133+
<input value={vendorName} onChange={(e) => setVendorName(e.target.value)} />
134+
</label>
135+
<br />
136+
<label>Contact<br />
137+
<input value={vendorContact} onChange={(e) => setVendorContact(e.target.value)} />
138+
</label>
139+
</fieldset>
140+
141+
<br />
142+
<fieldset>
143+
<legend>Sensor data (truck / storage)</legend>
144+
<label>Temperature (°C)<br />
145+
<input type="number" value={temperature as any} onChange={(e) => setTemperature(e.target.value === '' ? '' : Number(e.target.value))} />
146+
</label>
147+
<br />
148+
<label>Humidity (%)<br />
149+
<input type="number" value={humidity as any} onChange={(e) => setHumidity(e.target.value === '' ? '' : Number(e.target.value))} />
150+
</label>
151+
<br />
152+
<label>Weight (kg)<br />
153+
<input type="number" value={weight as any} onChange={(e) => setWeight(e.target.value === '' ? '' : Number(e.target.value))} />
154+
</label>
155+
</fieldset>
156+
157+
<br />
158+
<label>Notes<br />
159+
<textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={4} />
160+
</label>
161+
162+
<br />
163+
<label>Photos (multiple)<br />
164+
<input type="file" accept="image/*" multiple onChange={(e) => setPhotos(e.target.files)} />
165+
</label>
166+
167+
<br />
168+
<label>Videos (optional)<br />
169+
<input type="file" accept="video/*" multiple onChange={(e) => setVideos(e.target.files)} />
170+
</label>
171+
172+
<br />
173+
<button type="submit">Submit Inspection</button>
174+
</form>
175+
176+
{status && <p style={{ marginTop: 12 }}>{status}</p>}
177+
178+
179+
{blockchainHash && (
180+
<div
181+
style={{
182+
marginTop: 16,
183+
padding: 12,
184+
borderRadius: 8,
185+
border: '1px solid #22c55e',
186+
background: 'rgba(23, 211, 92, 0.1)'
187+
}}
188+
>
189+
<p style={{ fontWeight: 600, color: '#22c55e' }}>
190+
✅ Inspection Stored on Blockchain (Demo)
191+
</p>
192+
<p style={{ fontSize: 12, wordBreak: 'break-all' }}>
193+
Hash: {blockchainHash}
194+
</p>
195+
<p style={{ fontSize: 12 }}>
196+
Network: Polygon (Demo)
197+
</p>
198+
</div>
199+
)}
200+
201+
</div>
202+
);
203+
}

0 commit comments

Comments
 (0)