SGA CARS PRODUCTIVITY
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 5.2k
- Forks
- 1.8k
- Avg merge
- 3d 12h
- Merged PRs (30d)
- 6
Description
// vercel.json configuration for deployment
// Place this file in the project root when deploying to Vercel
// Build command: npm run build
// Output directory: dist
{
"version": 2,
"builds": [
{ "src": "package.json", "use": "@vercel/static-build" }
],
"routes": [
{ "src": "/(.*)", "dest": "/index.html" }
]
}
/*
SGA Productivity - Web Admin Dashboard
Single-file React component (default export) ready for a Vite / Create React App project.
How to deploy directly online (Vercel):
1️⃣ Create a GitHub repository and push your project.
2️⃣ Go to https://vercel.com → Import Project → Select your repo.
3️⃣ Use these settings:
• Framework: React
• Build command: npm run build
• Output directory: dist
4️⃣ Once deployed, Vercel will generate a public URL like https://sga-productivity.vercel.app.
*/
import React, { useEffect, useState, useRef } from 'react';
import axios from 'axios';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
});
const API_BASE = process.env.REACT_APP_API_BASE || 'https://your-backend-url/api';
const STATUS_OPTIONS = ['assigned','in_progress','road_test','final_inspection','qc_pending','completed'];
export default function App() {
const [token, setToken] = useState(() => localStorage.getItem('sga_token') || '');
const [user, setUser] = useState(() => {
try { return JSON.parse(localStorage.getItem('sga_user') || 'null'); } catch { return null; }
});
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [jobs, setJobs] = useState([]);
const [locations, setLocations] = useState([]);
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showCreate, setShowCreate] = useState(false);
const [newJob, setNewJob] = useState({ title:'', vehicle_reg:'', description:'', assigned_to:'' });
const refreshIntervalRef = useRef(null);
useEffect(() => {
if (token) {
axios.defaults.headers.common['Authorization'] = Bearer ${token};
fetchAll();
refreshIntervalRef.current = setInterval(() => { fetchLocations(); fetchJobs(); }, 15000);
} else {
delete axios.defaults.headers.common['Authorization'];
}
return () => {
if (refreshIntervalRef.current) {
clearInterval(refreshIntervalRef.current);
refreshIntervalRef.current = null;
}
};
}, [token]);
async function fetchJobs(){
try{ const r = await axios.get(${API_BASE}/jobs); setJobs(Array.isArray(r.data)?r.data:[]); }
catch(err){ setError('Failed to load jobs'); }
}
async function fetchLocations(){
try{ const r = await axios.get(${API_BASE}/locations/latest); setLocations(Array.isArray(r.data)?r.data:[]); }
catch(err){ setError('Failed to load locations'); }
}
async function fetchUsers(){
try{ const r = await axios.get(${API_BASE}/users); setUsers(Array.isArray(r.data)?r.data:[]); }
catch(err){ console.warn('fetchUsers warning', err); }
}
async function fetchAll(){ setLoading(true); await Promise.all([fetchJobs(),fetchLocations(),fetchUsers()]); setLoading(false); }
async function handleLogin(e){
e.preventDefault(); setError('');
try{
const r = await axios.post(${API_BASE}/login,{email,password});
if(r.data.token){ setToken(r.data.token); setUser(r.data.user||null); localStorage.setItem('sga_token',r.data.token); localStorage.setItem('sga_user',JSON.stringify(r.data.user||null)); }
else setError('Invalid login');
}catch{ setError('Login failed'); }
}
function handleLogout(){ setToken(''); setUser(null); localStorage.clear(); }
async function handleCreateJob(e){ e.preventDefault(); try{ await axios.post(${API_BASE}/jobs,{...newJob,priority:3}); setShowCreate(false); fetchJobs(); }catch{ setError('Failed to create job'); } }
async function updateJobStatus(id,s){ try{ await axios.patch(${API_BASE}/jobs/${id}/status,{status:s}); fetchJobs(); }catch{ setError('Failed to update job'); } }
const mapCenter=[12.9716,77.5946];
return (
SGA Productivity — Admin Dashboard
{user?Logout:null}
<main className="max-w-7xl mx-auto p-6 grid grid-cols-12 gap-6">
{!token?(
<section className="col-span-12 bg-white p-6 rounded shadow">
<h2 className="text-lg font-semibold mb-3">Login</h2>
<form onSubmit={handleLogin} className="grid grid-cols-2 gap-4">
<input className="col-span-2 p-3 border rounded" placeholder="Email" value={email} onChange={e=>setEmail(e.target.value)} />
<input className="col-span-2 p-3 border rounded" placeholder="Password" type="password" value={password} onChange={e=>setPassword(e.target.value)} />
<button className="col-span-2 p-3 bg-blue-600 text-white rounded" type="submit">Login</button>
</form>
{error&&<div className="mt-3 text-red-600">{error}</div>}
</section>
):(
<React.Fragment>
<section className="col-span-8 bg-white p-4 rounded shadow">
<h2 className="text-lg font-semibold mb-3">Live Technician Map</h2>
<div style={{height:'60vh'}} className="rounded overflow-hidden border">
<MapContainer center={mapCenter} zoom={12} style={{height:'100%',width:'100%'}}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{locations.map(loc=>(
<Marker key={String(loc.user_id)} position={[parseFloat(loc.latitude),parseFloat(loc.longitude)]}>
<Popup>
<div className="text-sm"><div className="font-semibold">{loc.name}</div><div className="text-xs">{loc.recorded_at?new Date(loc.recorded_at).toLocaleString():''}</div></div>
</Popup>
</Marker>
))}
</MapContainer>
</div>
</section>
<section className="col-span-4 space-y-4">
<div className="bg-white p-4 rounded shadow">
<h3 className="font-semibold text-md mb-2">Jobs</h3>
<div className="space-y-2 max-h-64 overflow-auto">
{jobs.map(j=>(
<div key={String(j.id)} className="p-2 border rounded flex justify-between">
<div><div className="font-medium">{j.title}</div><div className="text-xs">{j.vehicle_reg}</div></div>
<div><div className="text-xs bg-gray-100 px-2 py-1 rounded">{j.status}</div>{STATUS_OPTIONS.map(s=>(<button key={s} onClick={()=>updateJobStatus(j.id,s)} className="text-xs px-2 py-1 bg-gray-100 rounded">{s}</button>))}</div>
</div>
))}
</div>
</div>
</section>
</React.Fragment>
)}
</main>
<footer className="text-center p-4 text-sm text-gray-500">SGA Productivity • Admin Dashboard</footer>
</div>
);
}
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The issue contains a vercel.json deployment snippet and a single-file React admin dashboard, but it does not state what should change. First clarify the intended feature, target file or project location, and acceptance criteria; only then can the relevant entry point, tests, and definition of done be identified.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react
- Domain
- cloud, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 10/100