Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
.idea/
log
.DS_Store
backend/tmp/

.vscode/
28 changes: 23 additions & 5 deletions backend/src/modules/location-address/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ export class LocationAddressService implements ILocationAddressService {
createLocationAddress = withServiceErrorHandling(
async (payload: CreateLocationAddressDTO, companyId: string): Promise<CreateLocationAddressResponse> => {
const fipsLocation = await this.locationMatcher.getLocationFips(payload);

if (fipsLocation === null) {
throw Boom.badRequest("Fips state and county code cannot be null");
if (
fipsLocation === null ||
!fipsLocation ||
fipsLocation.fipsStateCode === null ||
fipsLocation.fipsCountyCode === null
) {
throw Boom.badRequest(
`Please enter a valid address. Unable to validate address: ${payload.streetAddress}, ${payload.city}, ${payload.stateProvince}.`
);
}

// Add FIPS codes into payload to send to transaction layer
Expand Down Expand Up @@ -137,6 +143,11 @@ export class LocationAddressService implements ILocationAddressService {
const transformedPayload = await Promise.all(
payload.map(async (element) => {
const currFips = await this.locationMatcher.getLocationFips(element);
if (!currFips || currFips.fipsStateCode === null || currFips.fipsCountyCode === null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also add this to the createLocationAddress method in this file for single creation? That way we see a good error in the business profile page? You may also have to modify the createLocation method in frontend/api/location.ts

throw Boom.badRequest(
`Please enter a valid address. Unable to validate address: ${element.streetAddress}, ${element.city}, ${element.stateProvince}.`
);
}
return { ...element, ...currFips };
})
);
Expand Down Expand Up @@ -177,8 +188,15 @@ export class LocationAddressService implements ILocationAddressService {
// get the new fips codes if any of the address fields have changed
const fipsLocation = await this.locationMatcher.getLocationFips(locationForMatching);

if (fipsLocation === null) {
throw Boom.badRequest("Fips state and county code cannot be null");
if (
fipsLocation === null ||
!fipsLocation ||
fipsLocation.fipsStateCode === null ||
fipsLocation.fipsCountyCode === null
) {
throw Boom.badRequest(
`Please enter a valid address. Unable to validate address: ${payload.streetAddress}, ${payload.city}, ${payload.stateProvince}.`
);
}

const updatedLocationWithFips = {
Expand Down
10 changes: 5 additions & 5 deletions backend/src/types/Location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const LocationAddressSchema = z.object({
city: z.string(),
streetAddress: z.string(),
postalCode: z.string().nonempty().regex(/^\d+$/, {
message: "Must be a non-negative number string",
message: "Please enter a valid Postal Code. Must be a non-negative number.",
}),
county: z.string().optional(),
companyId: z.uuid(),
Expand All @@ -29,7 +29,7 @@ export const LocationAddressSchemaType = z.object({
city: z.string(),
streetAddress: z.string(),
postalCode: z.string().nonempty().regex(/^\d+$/, {
message: "Must be a non-negative number string",
message: "Please enter a valid Postal Code. Must be a non-negative number.",
}),
county: z.string().optional(),
companyId: z.uuid(),
Expand All @@ -45,7 +45,7 @@ export const CreateLocationAddressSchema = z.object({
city: z.string().nonempty(),
streetAddress: z.string().nonempty(),
postalCode: z.string().nonempty().regex(/^\d+$/, {
message: "Must be a non-negative number string",
message: "Please enter a valid Postal Code. Must be a non-negative number.",
}),
county: z.string().nonempty().optional(),
});
Expand All @@ -72,7 +72,7 @@ export const GetAllLocationAddressesSchema = z.array(
city: z.string().nonempty(),
streetAddress: z.string().nonempty(),
postalCode: z.string().nonempty().regex(/^\d+$/, {
message: "Must be a non-negative number string",
message: "Please enter a valid Postal Code. Must be a non-negative number.",
}),
county: z.string().nonempty().optional(),
companyId: z.uuid(),
Expand All @@ -93,7 +93,7 @@ export const UpdateLocationAddressDTOSchema = z.object({
postalCode: z
.string()
.regex(/^\d+$/, {
message: "Must be a non-negative number string",
message: "Please enter a valid Postal Code. Must be a non-negative number.",
})
.optional(),
county: z.string().optional().nullable(),
Expand Down
2 changes: 1 addition & 1 deletion backend/src/utilities/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const withServiceErrorHandling = <T extends any[], R>(handler: (...args:
case "23502":
throw Boom.badRequest("Missing required field");
default:
throw Boom.internal(error, { message: "An expected error occured" });
throw Boom.internal(error, { message: "An unexpected error occured" });
}
} else {
throw Boom.boomify(error);
Expand Down
16 changes: 14 additions & 2 deletions frontend/api/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ export const createLocation = async (payload: CreateLocationRequest): Promise<Lo
if (response.ok) {
return data!;
} else {
throw Error(error?.error);
const apiError = new Error(error?.error || "Failed to create locations - Unkown Error") as Error & {
status: number;
statusText: string;
};
apiError.status = response.status;
apiError.statusText = response.statusText;
throw apiError;
}
};
return authWrapper<Location>()(req);
Expand All @@ -36,7 +42,13 @@ export const createLocationBulk = async (payload: CreateLocationBulkRequest): Pr
if (response.ok) {
return data!;
} else {
throw Error(error?.error);
const apiError = new Error(error?.error || "Failed to create locations - Unkown Error") as Error & {
status: number;
statusText: string;
};
apiError.status = response.status;
apiError.statusText = response.statusText;
throw apiError;
}
};
return authWrapper<Location[]>()(req);
Expand Down
6 changes: 4 additions & 2 deletions frontend/app/business-profile/overview/InsuranceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export default function InsuranceCard() {
setEditingInsuranceIndex(null);
},
onError: (error: Error) => {
setSaveError("An error occurred while saving the location: " + error.message);
const errorMessage = error.message || "Error updating policy. Check required fields and try again";
setSaveError(errorMessage);
},
});

Expand All @@ -45,7 +46,8 @@ export default function InsuranceCard() {
setEditingInsuranceIndex(null);
},
onError: (error: Error) => {
setSaveError("An error occurred while saving the location: " + error.message);
const errorMessage = error.message || "Error creating policy. Check required fields and try again";
setSaveError(errorMessage);
},
});

Expand Down
9 changes: 6 additions & 3 deletions frontend/app/business-profile/overview/LocationsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default function LocationsCard() {
if (error.message.includes("postalCode")) {
setSaveError("Error updating location. Please check postal code details and try again.");
} else {
setSaveError("An error occurred while saving the location.");
const errorMessage = error.message || "Error updating location. Check required fields and try again";
setSaveError(errorMessage);
}
},
});
Expand All @@ -46,7 +47,8 @@ export default function LocationsCard() {
if (error.message.includes("postalCode")) {
setSaveError("Error creating location. Please check postal code details and try again.");
} else {
setSaveError("An error occurred while creating the location.");
const errorMessage = error.message || "Error creating location. Check required fields and try again";
setSaveError(errorMessage);
}
},
});
Expand All @@ -58,7 +60,8 @@ export default function LocationsCard() {
setEditingLocationIndex(null);
},
onError: (_error: Error) => {
setSaveError("An error occurred while deleting the location.");
const errorMessage = _error.message || "Error removing location. Check required fields and try again";
setSaveError(errorMessage);
},
});

Expand Down
3 changes: 2 additions & 1 deletion frontend/app/signup/company.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export default function Company({ handleNext: incrementNext }: CompanyInfoProps)
incrementNext();
},
onError: (_error: Error) => {
setLocError("Error creating locations. Check required fields and try again");
const errorMessage = _error.message || "Error creating locations. Check required fields and try again";
setLocError(errorMessage);
},
});

Expand Down
92 changes: 28 additions & 64 deletions frontend/components/table/CategorySelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export default function CategorySelector({
categories = defaultCategories,
}: CategorySelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null);
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
const portalDropdownRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -62,12 +61,9 @@ export default function CategorySelector({
};
}, [isOpen]);

const filteredCategories = categories.filter((cat) => cat.toLowerCase().includes(searchQuery.toLowerCase()));

const handleCategorySelect = (category: string) => {
onCategoryChange(category);
setIsOpen(false);
setSearchQuery("");
};

const handleRemoveCategory = (e: React.MouseEvent) => {
Expand Down Expand Up @@ -107,78 +103,46 @@ export default function CategorySelector({
createPortal(
<div
ref={portalDropdownRef}
className=" top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 z-[999999] max-h-[400px] overflow-hidden"
className="top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 z-[999999] max-h-[400px] overflow-hidden"
style={{
position: "absolute",
top: coords.top,
left: coords.left,
width: coords.width,
}}
>
{/* Search Input */}
<div className="p-4 border-b border-gray-200">
<input
type="text"
placeholder="Select an option or create one"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm"
autoFocus
/>
</div>

{/* Category List */}
<div className="overflow-y-auto max-h-[300px]">
{filteredCategories.length > 0 ? (
filteredCategories.map((category) => (
<div
key={category}
onClick={() => handleCategorySelect(category)}
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors"
>
{/* Drag Handle */}
<div className="flex flex-col gap-[2px]">
<div className="flex gap-[2px]">
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>
<div className="flex gap-[2px]">
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>
<div className="flex gap-[2px]">
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>
{categories.map((category) => (
<div
key={category}
onClick={() => handleCategorySelect(category)}
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors"
>
{/* Drag Handle */}
<div className="flex flex-col gap-[2px]">
<div className="flex gap-[2px]">
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>

{/* Category */}
<span
className={`px-3 py-1 rounded-md text-sm font-medium ${defaultCategoryColors[category] || "bg-gray-100 text-gray-700"}`}
>
{category}
</span>
</div>
))
) : (
<div className="px-4 py-8 text-center text-gray-500 text-sm">
{searchQuery ? (
<div>
<p>No matching categories</p>
<button
onClick={() => {
handleCategorySelect(searchQuery);
}}
className="mt-2 text-purple-600 hover:text-purple-700 font-medium"
>
Create &quot;{searchQuery}&quot;
</button>
<div className="flex gap-[2px]">
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>
<div className="flex gap-[2px]">
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>
) : (
"No categories available"
)}
</div>

{/* Category */}
<span
className={`px-3 py-1 rounded-md text-sm font-medium ${defaultCategoryColors[category] || "bg-gray-100 text-gray-700"}`}
>
{category}
</span>
</div>
)}
))}
</div>
</div>,
portalRoot
Expand Down
Loading