Dockerized web app (FastAPI + React + PostgreSQL) for scoping client ratecards against the GMAL master asset database. Features: - GMAL data ingestion from Excel (390 assets, 120 roles, 5 model types) - AI-powered document parsing and asset extraction (Claude Opus 4.6) - AI matching engine with parallel batching, confidence scoring, caveats - Ratecard builder with hours x volume calculation - Excel and PDF export - GMAL browser and inline editor - AI cost tracking per project (persisted to DB) - Debug panel for AI call inspection - Dark theme UI with gold (#FFC407) accent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import api from '../api/client';
|
|
import { MODEL_TYPE_LABELS } from '../types';
|
|
import './NewProject.css';
|
|
|
|
export default function NewProject() {
|
|
const navigate = useNavigate();
|
|
const [name, setName] = useState('');
|
|
const [clientName, setClientName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [modelType, setModelType] = useState('current_oplus');
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!name.trim()) return;
|
|
|
|
setCreating(true);
|
|
try {
|
|
const res = await api.post('/projects', {
|
|
name: name.trim(),
|
|
client_name: clientName.trim() || null,
|
|
description: description.trim() || null,
|
|
model_type: modelType,
|
|
});
|
|
navigate(`/projects/${res.data.id}`);
|
|
} catch (err: any) {
|
|
alert(`Failed to create project: ${err.response?.data?.detail || err.message}`);
|
|
setCreating(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="new-project">
|
|
<h1 className="page-title">New Project</h1>
|
|
<form onSubmit={handleSubmit} className="form-card">
|
|
<div className="field">
|
|
<label className="label">Project Name <span className="required">*</span></label>
|
|
<input
|
|
className="input"
|
|
value={name}
|
|
onChange={e => setName(e.target.value)}
|
|
placeholder="e.g. Acme Corp Q2 2025 Scope"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label className="label">Client Name</label>
|
|
<input
|
|
className="input"
|
|
value={clientName}
|
|
onChange={e => setClientName(e.target.value)}
|
|
placeholder="e.g. Acme Corp"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label className="label">Description</label>
|
|
<textarea
|
|
className="input textarea"
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
placeholder="Brief description of this scope..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label className="label">Model Type</label>
|
|
<select className="input" value={modelType} onChange={e => setModelType(e.target.value)}>
|
|
{Object.entries(MODEL_TYPE_LABELS).map(([value, label]) => (
|
|
<option key={value} value={value}>{label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="form-actions">
|
|
<button type="button" onClick={() => navigate('/')} className="btn btn-secondary">Cancel</button>
|
|
<button type="submit" disabled={creating || !name.trim()} className="btn btn-primary">
|
|
{creating ? 'Creating...' : 'Create Project'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|