fix: Normalize optional user fields (department, designation, and supplier) in user modals and update AuthenticatedImage formatting.

This commit is contained in:
Yashwin 2026-03-17 10:56:28 +05:30
parent d242bbf708
commit c9503c78be
3 changed files with 140 additions and 100 deletions

View File

@ -1,9 +1,17 @@
import { useState, useEffect, type ImgHTMLAttributes, type ReactElement } from 'react'; import {
import { fileService } from '@/services/file-service'; useState,
import apiClient from '@/services/api-client'; useEffect,
import { Loader2, ImageIcon } from 'lucide-react'; type ImgHTMLAttributes,
type ReactElement,
} from "react";
import { fileService } from "@/services/file-service";
import apiClient from "@/services/api-client";
import { Loader2, ImageIcon } from "lucide-react";
interface AuthenticatedImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> { interface AuthenticatedImageProps extends Omit<
ImgHTMLAttributes<HTMLImageElement>,
"src"
> {
fileId?: string | null; fileId?: string | null;
src?: string | null; src?: string | null;
fallback?: ReactElement; fallback?: ReactElement;
@ -14,7 +22,7 @@ export const AuthenticatedImage = ({
src, src,
fallback, fallback,
className, className,
alt = 'Image', alt = "Image",
...props ...props
}: AuthenticatedImageProps): ReactElement => { }: AuthenticatedImageProps): ReactElement => {
const [blobUrl, setBlobUrl] = useState<string | null>(null); const [blobUrl, setBlobUrl] = useState<string | null>(null);
@ -23,13 +31,15 @@ export const AuthenticatedImage = ({
useEffect(() => { useEffect(() => {
// If it's already a blob URL (local preview) or a data URL, use it directly // If it's already a blob URL (local preview) or a data URL, use it directly
if (src && (src.startsWith('blob:') || src.startsWith('data:'))) { if (src && (src.startsWith("blob:") || src.startsWith("data:"))) {
setBlobUrl(src); setBlobUrl(src);
return; return;
} }
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; const baseUrl =
const isBackendUrl = src && src.includes(`${baseUrl}/files/`) && src.includes('/preview'); import.meta.env.VITE_API_BASE_URL || "http://localhost:3000";
const isBackendUrl =
src && src.includes(`${baseUrl}/files/`) && src.includes("/preview");
// If we have a fileId or a backend URL, fetch it via authenticated request // If we have a fileId or a backend URL, fetch it via authenticated request
if (fileId || isBackendUrl) { if (fileId || isBackendUrl) {
@ -42,7 +52,7 @@ export const AuthenticatedImage = ({
if (fileId) { if (fileId) {
url = await fileService.getPreview(fileId); url = await fileService.getPreview(fileId);
} else if (src) { } else if (src) {
const response = await apiClient.get(src, { responseType: 'blob' }); const response = await apiClient.get(src, { responseType: "blob" });
url = URL.createObjectURL(response.data); url = URL.createObjectURL(response.data);
} else { } else {
return; return;
@ -52,7 +62,7 @@ export const AuthenticatedImage = ({
setBlobUrl(url); setBlobUrl(url);
} }
} catch (err) { } catch (err) {
console.error('Failed to fetch authenticated image:', err); console.error("Failed to fetch authenticated image:", err);
if (isMounted) { if (isMounted) {
setError(true); setError(true);
} }
@ -67,7 +77,7 @@ export const AuthenticatedImage = ({
return () => { return () => {
isMounted = false; isMounted = false;
if (blobUrl && blobUrl.startsWith('blob:')) { if (blobUrl && blobUrl.startsWith("blob:")) {
URL.revokeObjectURL(blobUrl); URL.revokeObjectURL(blobUrl);
} }
}; };
@ -79,23 +89,29 @@ export const AuthenticatedImage = ({
if (isLoading) { if (isLoading) {
return ( return (
<div className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}> <div
className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}
>
<Loader2 className="w-5 h-5 text-[#6b7280] animate-spin" /> <Loader2 className="w-5 h-5 text-[#6b7280] animate-spin" />
</div> </div>
); );
} }
if (error || (!blobUrl && !src)) { if (error || (!blobUrl && !src)) {
return fallback || ( return (
<div className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}> fallback || (
<div
className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}
>
<ImageIcon className="w-5 h-5 text-[#9ca3af]" /> <ImageIcon className="w-5 h-5 text-[#9ca3af]" />
</div> </div>
)
); );
} }
return ( return (
<img <img
src={blobUrl || src || ''} src={blobUrl || src || ""}
className={className} className={className}
alt={alt} alt={alt}
{...props} {...props}

View File

@ -345,6 +345,17 @@ export const EditUserModal = ({
if (defaultTenantId) { if (defaultTenantId) {
data.tenant_id = defaultTenantId; data.tenant_id = defaultTenantId;
} }
// Normalize empty strings to null for optional UUID fields
if (!data.department_id) data.department_id = undefined;
if (!data.designation_id) data.designation_id = undefined;
// If category is tenant_user, supplier_id should always be null
if (data.category === "tenant_user") {
data.supplier_id = null;
} else if (!data.supplier_id) {
data.supplier_id = null;
}
await onSubmit(userId, data); await onSubmit(userId, data);
} catch (error: any) { } catch (error: any) {
if ( if (
@ -491,13 +502,13 @@ export const EditUserModal = ({
{ value: "supplier_user", label: "Supplier User" }, { value: "supplier_user", label: "Supplier User" },
]} ]}
value={categoryValue || "tenant_user"} value={categoryValue || "tenant_user"}
onValueChange={(value) => onValueChange={(value) => {
setValue( const category = value as "tenant_user" | "supplier_user";
"category", setValue("category", category, { shouldValidate: true });
value as "tenant_user" | "supplier_user", if (category === "tenant_user") {
{ shouldValidate: true }, setValue("supplier_id", null);
)
} }
}}
error={errors.category?.message} error={errors.category?.message}
/> />

View File

@ -216,6 +216,17 @@ export const NewUserModal = ({
clearErrors(); clearErrors();
try { try {
const { confirmPassword, ...submitData } = data; const { confirmPassword, ...submitData } = data;
// Normalize empty strings to null for optional UUID fields
if (!submitData.department_id) submitData.department_id = undefined;
if (!submitData.designation_id) submitData.designation_id = undefined;
// If category is tenant_user, supplier_id should always be null
if (submitData.category === "tenant_user") {
submitData.supplier_id = null;
} else if (!submitData.supplier_id) {
submitData.supplier_id = null;
}
await onSubmit(submitData); await onSubmit(submitData);
} catch (error: any) { } catch (error: any) {
// Handle validation errors from API // Handle validation errors from API
@ -387,11 +398,13 @@ export const NewUserModal = ({
{ value: "supplier_user", label: "Supplier User" }, { value: "supplier_user", label: "Supplier User" },
]} ]}
value={categoryValue || "tenant_user"} value={categoryValue || "tenant_user"}
onValueChange={(value) => onValueChange={(value) => {
setValue("category", value as "tenant_user" | "supplier_user", { const category = value as "tenant_user" | "supplier_user";
shouldValidate: true, setValue("category", category, { shouldValidate: true });
}) if (category === "tenant_user") {
setValue("supplier_id", null);
} }
}}
error={errors.category?.message} error={errors.category?.message}
/> />