Skip to content

Commit 7b31808

Browse files
committed
Merge branch 'feature/hierarchical-identifiers'
2 parents 467a401 + 541bb32 commit 7b31808

File tree

57 files changed

+6120
-405
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+6120
-405
lines changed

app/src/app/establishments/[id]/general-info/general-info-form.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { FormField } from "@/components/form/form-field";
33
import { useBaseData } from "@/atoms/base-data";
44
import { EditableField } from "@/components/form/editable-field";
5+
import { EditableHierarchicalField } from "@/components/form/editable-hierarchical-field";
56
import { useActionState, useEffect, useState } from "react";
67
import { updateExternalIdent } from "@/app/legal-units/[id]/update-external-ident-server-action";
78
import { SubmissionFeedbackDebugInfo } from "@/components/form/submission-feedback-debug-info";
@@ -94,6 +95,22 @@ export default function GeneralInfoForm({ id }: { readonly id: string }) {
9495
<div className="grid lg:grid-cols-2 gap-4 p-3">
9596
{externalIdentTypes.map((type) => {
9697
const value = establishment?.external_idents[type.code];
98+
99+
// Use hierarchical field component for hierarchical identifier types
100+
if (type.shape === "hierarchical" && type.labels) {
101+
return (
102+
<EditableHierarchicalField
103+
key={type.code}
104+
fieldId={`${type.code}`}
105+
label={type.name ?? type.code!}
106+
value={value}
107+
identType={type}
108+
response={externalIdentState}
109+
formAction={externalIdentFormAction}
110+
/>
111+
);
112+
}
113+
97114
return (
98115
<EditableField
99116
key={type.code}
Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,55 @@
11
"use client";
2-
import { useEstablishment } from "@/components/statistical-unit-details/use-unit-details";
2+
import { useEstablishment, useStatisticalUnitDetails } from "@/components/statistical-unit-details/use-unit-details";
33
import DataDump from "@/components/data-dump";
44
import UnitNotFound from "@/components/statistical-unit-details/unit-not-found";
5+
import useSWR from "swr";
6+
import { getBrowserRestClient } from "@/context/RestClientStore";
7+
8+
function useExternalIdents(establishmentId: string) {
9+
const { data, isLoading, error } = useSWR(
10+
["external_idents_establishment", establishmentId],
11+
async () => {
12+
const client = await getBrowserRestClient();
13+
const { data, error } = await client
14+
.from("external_ident")
15+
.select("*, external_ident_type:type_id(code, name, shape, labels)")
16+
.eq("establishment_id", parseInt(establishmentId, 10));
17+
if (error) throw error;
18+
return data;
19+
},
20+
{ revalidateOnFocus: false }
21+
);
22+
return { externalIdents: data, isLoading, error };
23+
}
524

625
export default function InspectDump({ id }: { readonly id: string }) {
7-
const { establishment, error } = useEstablishment(id);
26+
const { establishment, error: establishmentError } = useEstablishment(id);
27+
const { data: details, error: detailsError } = useStatisticalUnitDetails(id, "establishment");
28+
const { externalIdents, error: externalIdentsError } = useExternalIdents(id);
829

9-
if (error || !establishment) {
30+
if (establishmentError || !establishment) {
1031
return <UnitNotFound />;
1132
}
1233

13-
return <DataDump data={establishment} />;
34+
const establishmentDetails = details?.establishment?.[0];
35+
36+
return (
37+
<div className="space-y-6">
38+
<DataDump data={establishment} title="establishment (base table)" />
39+
40+
{externalIdents && externalIdents.length > 0 && (
41+
<DataDump
42+
data={externalIdents}
43+
title="external_ident (related records)"
44+
/>
45+
)}
46+
47+
{establishmentDetails && (
48+
<DataDump
49+
data={establishmentDetails}
50+
title="statistical_unit_details (computed view)"
51+
/>
52+
)}
53+
</div>
54+
);
1455
}

app/src/app/legal-units/[id]/general-info/general-info-form.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useBaseData } from "@/atoms/base-data";
1313
import { updateExternalIdent } from "@/app/legal-units/[id]/update-external-ident-server-action";
1414
import { Tables } from "@/lib/database.types";
1515
import { EditableField } from "@/components/form/editable-field";
16+
import { EditableHierarchicalField } from "@/components/form/editable-hierarchical-field";
1617
import { SubmissionFeedbackDebugInfo } from "@/components/form/submission-feedback-debug-info";
1718
import { useStatisticalUnitDetails } from "@/components/statistical-unit-details/use-unit-details";
1819
import Loading from "@/components/statistical-unit-details/loading";
@@ -143,6 +144,22 @@ export default function GeneralInfoForm({ id }: { readonly id: string }) {
143144
{externalIdentTypes.map(
144145
(type: Tables<"external_ident_type_active">) => {
145146
const value = legalUnit?.external_idents[type.code];
147+
148+
// Use hierarchical field component for hierarchical identifier types
149+
if (type.shape === "hierarchical" && type.labels) {
150+
return (
151+
<EditableHierarchicalField
152+
key={type.code}
153+
fieldId={`${type.code}`}
154+
label={type.name ?? type.code!}
155+
value={value}
156+
identType={type}
157+
response={externalIdentState}
158+
formAction={externalIdentFormAction}
159+
/>
160+
);
161+
}
162+
146163
return (
147164
<EditableField
148165
key={type.code}
Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,55 @@
11
"use client";
2-
import { useLegalUnit } from "@/components/statistical-unit-details/use-unit-details";
2+
import { useLegalUnit, useStatisticalUnitDetails } from "@/components/statistical-unit-details/use-unit-details";
33
import DataDump from "@/components/data-dump";
44
import UnitNotFound from "@/components/statistical-unit-details/unit-not-found";
5+
import useSWR from "swr";
6+
import { getBrowserRestClient } from "@/context/RestClientStore";
7+
8+
function useExternalIdents(legalUnitId: string) {
9+
const { data, isLoading, error } = useSWR(
10+
["external_idents", legalUnitId],
11+
async () => {
12+
const client = await getBrowserRestClient();
13+
const { data, error } = await client
14+
.from("external_ident")
15+
.select("*, external_ident_type:type_id(code, name, shape, labels)")
16+
.eq("legal_unit_id", parseInt(legalUnitId, 10));
17+
if (error) throw error;
18+
return data;
19+
},
20+
{ revalidateOnFocus: false }
21+
);
22+
return { externalIdents: data, isLoading, error };
23+
}
524

625
export default function InspectDump({ id }: { readonly id: string }) {
7-
const { legalUnit, error } = useLegalUnit(id);
26+
const { legalUnit, error: legalUnitError } = useLegalUnit(id);
27+
const { data: details, error: detailsError } = useStatisticalUnitDetails(id, "legal_unit");
28+
const { externalIdents, error: externalIdentsError } = useExternalIdents(id);
829

9-
if (error || !legalUnit) {
30+
if (legalUnitError || !legalUnit) {
1031
return <UnitNotFound />;
1132
}
1233

13-
return <DataDump data={legalUnit} />;
34+
const legalUnitDetails = details?.legal_unit?.[0];
35+
36+
return (
37+
<div className="space-y-6">
38+
<DataDump data={legalUnit} title="legal_unit (base table)" />
39+
40+
{externalIdents && externalIdents.length > 0 && (
41+
<DataDump
42+
data={externalIdents}
43+
title="external_ident (related records)"
44+
/>
45+
)}
46+
47+
{legalUnitDetails && (
48+
<DataDump
49+
data={legalUnitDetails}
50+
title="statistical_unit_details (computed view)"
51+
/>
52+
)}
53+
</div>
54+
);
1455
}

app/src/app/legal-units/[id]/update-external-ident-server-action.ts

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,34 @@ const externalIdentsSchema = zfd.formData(
1919
)
2020
);
2121

22+
/**
23+
* Constructs a hierarchical identifier path from form data.
24+
* Form fields are named like: census_ident_census, census_ident_region, census_ident_surveyor, census_ident_unit_no
25+
* The labels (like "census.region.surveyor.unit_no") tell us the level names.
26+
*/
27+
function constructHierarchicalPath(
28+
identTypeCode: string,
29+
labels: string,
30+
formData: Record<string, string | undefined>
31+
): string | null {
32+
const levelNames = labels.split(".");
33+
const parts: string[] = [];
34+
35+
for (const levelName of levelNames) {
36+
const fieldKey = `${identTypeCode}_${levelName}`;
37+
const value = formData[fieldKey];
38+
if (value) {
39+
parts.push(value);
40+
} else {
41+
// If any level is empty, stop constructing the path
42+
break;
43+
}
44+
}
45+
46+
// Return null if no parts, otherwise join with dots
47+
return parts.length > 0 ? parts.join(".") : null;
48+
}
49+
2250
export async function updateExternalIdent(
2351
id: string,
2452
unitType: "establishment" | "legal_unit",
@@ -54,18 +82,50 @@ export async function updateExternalIdent(
5482

5583
const unitIdField = `${unitType}_id`;
5684

57-
const [[identTypeCode, newIdentValue]] = Object.entries(validatedFields.data);
58-
85+
// Get first key to determine which identifier type we're editing
86+
const firstKey = Object.keys(validatedFields.data)[0];
87+
88+
// Detect if this is a hierarchical identifier by checking for underscore pattern
89+
// e.g., "census_ident_census" -> "census_ident"
90+
const identTypeCode = firstKey.includes("_") &&
91+
externalIdentTypes.some(t => t.shape === "hierarchical" && firstKey.startsWith(t.code + "_"))
92+
? firstKey.substring(0, firstKey.lastIndexOf("_"))
93+
: firstKey;
94+
95+
// For hierarchical types, find the actual type code by checking prefixes
5996
const identType = externalIdentTypes.find(
60-
(type) => type.code === identTypeCode
97+
(type) => {
98+
if (type.shape === "hierarchical" && type.labels) {
99+
// Check if any form key starts with this type's code followed by underscore
100+
return Object.keys(validatedFields.data).some(key =>
101+
key.startsWith(type.code + "_")
102+
);
103+
}
104+
return type.code === firstKey;
105+
}
61106
);
107+
62108
if (!identType) {
63109
return {
64110
status: "error",
65-
message: `Invalid external identifier type: ${identTypeCode}`,
111+
message: `Invalid external identifier type: ${firstKey}`,
66112
};
67113
}
68114
const identTypeId = identType.id;
115+
const isHierarchical = identType.shape === "hierarchical";
116+
117+
// Construct the value based on identifier type
118+
let newIdentValue: string | null = null;
119+
if (isHierarchical && identType.labels) {
120+
newIdentValue = constructHierarchicalPath(
121+
identType.code!,
122+
identType.labels,
123+
validatedFields.data
124+
);
125+
} else {
126+
newIdentValue = validatedFields.data[firstKey] || null;
127+
}
128+
69129
try {
70130
const { data: exisitingIdent, error } = await client
71131
.from("external_ident")
@@ -85,7 +145,7 @@ export async function updateExternalIdent(
85145
if (count === 1) {
86146
return {
87147
status: "error",
88-
message: `Cannot delete ${identTypeCode}. Unit must have at least one external identifier.`,
148+
message: `Cannot delete ${identType.code}. Unit must have at least one external identifier.`,
89149
};
90150
}
91151
response = await client
@@ -94,40 +154,59 @@ export async function updateExternalIdent(
94154
.eq("type_id", identTypeId!)
95155
.eq(unitIdField, parseInt(id));
96156
} else if (!exisitingIdent || exisitingIdent.length === 0) {
97-
response = await client.from("external_ident").insert({
98-
ident: newIdentValue,
157+
// Note: 'shape' and 'labels' are derived by trigger from type_id
158+
// but TypeScript requires them. The trigger will override our value.
159+
const insertData: Record<string, unknown> = {
99160
[unitIdField]: parseInt(id),
100161
type_id: identTypeId!,
101162
edit_by_user_id: userId,
102-
});
163+
shape: identType.shape!, // Will be overwritten by trigger
164+
};
165+
166+
if (isHierarchical) {
167+
insertData.idents = newIdentValue;
168+
} else {
169+
insertData.ident = newIdentValue;
170+
}
171+
172+
response = await client.from("external_ident").insert(insertData);
103173
} else {
174+
const updateData: Record<string, unknown> = {
175+
edit_by_user_id: userId,
176+
edit_at: new Date().toISOString(),
177+
};
178+
179+
if (isHierarchical) {
180+
updateData.idents = newIdentValue;
181+
updateData.ident = null; // Clear the regular ident field
182+
} else {
183+
updateData.ident = newIdentValue;
184+
updateData.idents = null; // Clear the hierarchical idents field
185+
}
186+
104187
response = await client
105188
.from("external_ident")
106-
.update({
107-
ident: newIdentValue,
108-
edit_by_user_id: userId,
109-
edit_at: new Date().toISOString(),
110-
})
189+
.update(updateData)
111190
.eq("type_id", identTypeId!)
112191
.eq(unitIdField, parseInt(id));
113192
}
114193
if (response?.error) {
115-
logger.error(response.error, `failed to update ${identTypeCode}`);
194+
logger.error(response.error, `failed to update ${identType.code}`);
116195
return {
117196
status: "error",
118-
message: `failed to update ${identTypeCode}: ${response.error.message}`,
197+
message: `failed to update ${identType.code}: ${response.error.message}`,
119198
};
120199
}
121200

122201
revalidatePath(`/${unitType}s/${id}`);
123202
return {
124203
status: "success",
125-
message: `${identTypeCode} successfully updated`,
204+
message: `${identType.code} successfully updated`,
126205
};
127206
} catch (error) {
128207
return {
129208
status: "error",
130-
message: `failed to update ${identTypeCode}`,
209+
message: `failed to update ${identType.code}`,
131210
};
132211
}
133212
}

0 commit comments

Comments
 (0)