Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | 6x 92x 92x 56x 92x 92x 28x 28x 28x 28x 28x 92x 7x 7x 5x 92x 53x 39x 14x 14x 8x 4x 8x 4x 18x 92x 28x 13x 92x 92x 92x 7x 2x | import { FileText, Layers } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import {
type Project,
type ServiceCategory,
taxonomyApi,
} from "../../../lib/api";
import { ReadOnlyField } from "../../profile/ReadOnlyField";
import { SectionCard } from "../../profile/SectionCard";
import { BasicInfoModal } from "../modals/BasicInfoModal";
import { ScopeAreasModal } from "../modals/ScopeAreasModal";
interface DetailsTabProps {
project: Project;
onSave: (data: Partial<Project>) => void | Promise<void>;
/**
* Opens one of the read-only cards' edit modals from outside the tab — the
* "Make it rank better" panel's `Add ▸` links point at the card that fixes
* the item, rather than making the pro find it.
*/
openSection?: string | null;
/** Called once an externally-requested section has been opened. */
onSectionHandled?: () => void;
}
const SCOPE_LABELS: Record<string, string> = {
design: "Design",
material: "Material",
execution: "Execution",
};
export function DetailsTab({
project,
onSave,
openSection,
onSectionHandled,
}: DetailsTabProps) {
const [editingSection, setEditingSection] = useState<
"basic" | "scope" | null
>(null);
useEffect(() => {
Eif (openSection !== "basic" && openSection !== "scope") return;
setEditingSection(openSection);
onSectionHandled?.();
}, [openSection, onSectionHandled]);
const [serviceCategories, setServiceCategories] = useState<ServiceCategory[]>(
[],
);
useEffect(() => {
let cancelled = false;
taxonomyApi
.getServiceCategories()
.then((res) => {
Eif (!cancelled) setServiceCategories(res.data || []);
})
.catch((err) => {
console.error("Failed to load service categories:", err);
});
return () => {
cancelled = true;
};
}, []);
const handleSave = async (data: Partial<Project>) => {
try {
await onSave(data);
setEditingSection(null);
} catch {
// Error is handled by the parent component
}
};
// Resolve worked area IDs to names
const workedAreaNames = useMemo(() => {
if (!project.workedAreaIds || project.workedAreaIds.length === 0)
return null;
const idToName: Record<string, string> = {
full_home: "Full Home",
full_office: "Full Office",
};
for (const cat of serviceCategories) {
if (cat.children && cat.children.length > 0) {
for (const child of cat.children) {
idToName[child.id] = child.name;
}
} else {
idToName[cat.id] = cat.name;
}
}
return project.workedAreaIds.map((id) => idToName[id] || id);
}, [project.workedAreaIds, serviceCategories]);
const scopeLabels = useMemo(() => {
if (!project.scope || project.scope.length === 0) return null;
return (project.scope as string[]).map((s) => SCOPE_LABELS[s] || s);
}, [project.scope]);
const isBasicEmpty = !project.title && !project.description;
const isScopeEmpty =
(!project.scope || project.scope.length === 0) &&
(!project.workedAreaIds || project.workedAreaIds.length === 0);
return (
<div className="space-y-6">
<SectionCard
title="Basic Info"
icon={<FileText className="h-4 w-4" />}
onEdit={() => setEditingSection("basic")}
isEmpty={isBasicEmpty}
>
<dl className="grid grid-cols-1 gap-4">
<ReadOnlyField label="Title" value={project.title} />
<ReadOnlyField label="Description" value={project.description} />
</dl>
</SectionCard>
<SectionCard
title="Scope & Areas"
icon={<Layers className="h-4 w-4" />}
onEdit={() => setEditingSection("scope")}
isEmpty={isScopeEmpty}
>
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ReadOnlyField label="Scope of Work" value={scopeLabels} />
<ReadOnlyField label="Worked Areas" value={workedAreaNames} />
</dl>
</SectionCard>
{editingSection === "basic" && (
<BasicInfoModal
project={project}
onSave={handleSave}
onClose={() => setEditingSection(null)}
/>
)}
{editingSection === "scope" && (
<ScopeAreasModal
project={project}
onSave={handleSave}
onClose={() => setEditingSection(null)}
/>
)}
</div>
);
}
|