All files / src/components/projects/tabs DetailsTab.tsx

92.1% Statements 35/38
90% Branches 27/30
78.57% Functions 11/14
91.17% Lines 31/34

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                                  6x             82x     82x       82x 34x 34x     34x         34x 34x       82x 7x 7x 5x             82x 53x 39x 14x       14x 8x 4x 8x     4x     18x     82x 34x 13x     82x 82x       82x         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>;
}
 
const SCOPE_LABELS: Record<string, string> = {
	design: "Design",
	material: "Material",
	execution: "Execution",
};
 
export function DetailsTab({ project, onSave }: DetailsTabProps) {
	const [editingSection, setEditingSection] = useState<
		"basic" | "scope" | null
	>(null);
	const [serviceCategories, setServiceCategories] = useState<ServiceCategory[]>(
		[],
	);
 
	useEffect(() => {
		let cancelled = false;
		taxonomyApi
			.getServiceCategories()
			.then((res) => {
				if (!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>
	);
}