feat: dashboard overhaul, chargeability reports, dispo import enhancements, UI polish
Dashboard: expanded chargeability widget, resource/project table widgets with sorting and filters, stat cards with formatMoney integration. Chargeability: new report client with filtering, chargeability-bookings use case, updated dashboard overview logic. Dispo import: TBD project handling, parse-dispo-matrix improvements, stage-dispo-projects resource value scores, new tests. Estimates: CommercialTermsEditor component, commercial-terms engine module, expanded estimate schemas and types. UI: AppShell navigation updates, timeline filter/toolbar enhancements, role management improvements, signin page redesign, Tailwind/globals polish, SystemSettings SMTP section, anonymization support. Tests: new router tests (anonymization, chargeability, effort-rule, entitlement, estimate, experience-multiplier, notification, resource, staffing, vacation). Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -6,8 +6,16 @@ import { trpc } from "~/lib/trpc/client.js";
|
||||
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
|
||||
|
||||
const PRESET_COLORS = [
|
||||
"#6366f1", "#8b5cf6", "#ec4899", "#ef4444", "#f97316",
|
||||
"#eab308", "#22c55e", "#14b8a6", "#06b6d4", "#3b82f6",
|
||||
"#6366f1",
|
||||
"#8b5cf6",
|
||||
"#ec4899",
|
||||
"#ef4444",
|
||||
"#f97316",
|
||||
"#eab308",
|
||||
"#22c55e",
|
||||
"#14b8a6",
|
||||
"#06b6d4",
|
||||
"#3b82f6",
|
||||
];
|
||||
|
||||
interface RoleModalProps {
|
||||
@@ -69,33 +77,48 @@ export function RoleModal({ role, onClose, onSuccess }: RoleModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
|
||||
const labelClass = "block text-sm font-medium text-gray-700 mb-1";
|
||||
const inputClass = "app-input";
|
||||
const labelClass = "app-label";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center overflow-y-auto py-8"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/55 py-8 backdrop-blur-sm"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="bg-white rounded-xl shadow-2xl w-full max-w-md mx-4"
|
||||
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
|
||||
className="mx-4 w-full max-w-md rounded-3xl border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{isEditing ? "Edit Role" : "New Role"}
|
||||
</h2>
|
||||
<button type="button" onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-xl leading-none text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-200"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="px-6 py-5 space-y-4">
|
||||
<div>
|
||||
<label className={labelClass}>Name <span className="text-red-500">*</span></label>
|
||||
<label className={labelClass}>
|
||||
Name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); setServerError(null); }}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setServerError(null);
|
||||
}}
|
||||
placeholder="e.g. 3D Artist"
|
||||
className={inputClass}
|
||||
maxLength={100}
|
||||
@@ -117,8 +140,16 @@ export function RoleModal({ role, onClose, onSuccess }: RoleModalProps) {
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Color</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-gray-200 flex-shrink-0" style={{ backgroundColor: color }} />
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/70">
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div
|
||||
className="h-8 w-8 flex-shrink-0 rounded-full border-2 border-gray-200 dark:border-gray-600"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Pick a color that stays readable in timelines and chips.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 flex-1">
|
||||
{PRESET_COLORS.map((c) => (
|
||||
<button
|
||||
@@ -137,23 +168,32 @@ export function RoleModal({ role, onClose, onSuccess }: RoleModalProps) {
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="w-8 h-8 rounded cursor-pointer border border-gray-300"
|
||||
className="mt-3 h-8 w-10 cursor-pointer rounded border border-gray-300 bg-transparent dark:border-gray-600"
|
||||
title="Custom color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{serverError && (
|
||||
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
||||
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-300">
|
||||
{serverError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button type="button" onClick={onClose} disabled={isPending} className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 disabled:opacity-50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isPending}
|
||||
className="rounded-xl px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-100 hover:text-gray-900 disabled:opacity-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" disabled={isPending} className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user