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,104 +1,120 @@
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<
fileId?: string | null; ImgHTMLAttributes<HTMLImageElement>,
src?: string | null; "src"
fallback?: ReactElement; > {
fileId?: string | null;
src?: string | null;
fallback?: ReactElement;
} }
export const AuthenticatedImage = ({ export const AuthenticatedImage = ({
fileId, fileId,
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);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
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;
}
const baseUrl =
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 (fileId || isBackendUrl) {
let isMounted = true;
const fetchImage = async () => {
setIsLoading(true);
setError(false);
try {
let url: string;
if (fileId) {
url = await fileService.getPreview(fileId);
} else if (src) {
const response = await apiClient.get(src, { responseType: "blob" });
url = URL.createObjectURL(response.data);
} else {
return; return;
}
if (isMounted) {
setBlobUrl(url);
}
} catch (err) {
console.error("Failed to fetch authenticated image:", err);
if (isMounted) {
setError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
} }
};
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; fetchImage();
const isBackendUrl = src && src.includes(`${baseUrl}/files/`) && src.includes('/preview');
// If we have a fileId or a backend URL, fetch it via authenticated request return () => {
if (fileId || isBackendUrl) { isMounted = false;
let isMounted = true; if (blobUrl && blobUrl.startsWith("blob:")) {
const fetchImage = async () => { URL.revokeObjectURL(blobUrl);
setIsLoading(true);
setError(false);
try {
let url: string;
if (fileId) {
url = await fileService.getPreview(fileId);
} else if (src) {
const response = await apiClient.get(src, { responseType: 'blob' });
url = URL.createObjectURL(response.data);
} else {
return;
}
if (isMounted) {
setBlobUrl(url);
}
} catch (err) {
console.error('Failed to fetch authenticated image:', err);
if (isMounted) {
setError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
fetchImage();
return () => {
isMounted = false;
if (blobUrl && blobUrl.startsWith('blob:')) {
URL.revokeObjectURL(blobUrl);
}
};
} else if (src) {
// For other external URLs, use them directly
setBlobUrl(src);
} }
}, [fileId, src]); };
} else if (src) {
if (isLoading) { // For other external URLs, use them directly
return ( setBlobUrl(src);
<div className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}>
<Loader2 className="w-5 h-5 text-[#6b7280] animate-spin" />
</div>
);
}
if (error || (!blobUrl && !src)) {
return fallback || (
<div className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}>
<ImageIcon className="w-5 h-5 text-[#9ca3af]" />
</div>
);
} }
}, [fileId, src]);
if (isLoading) {
return ( return (
<img <div
src={blobUrl || src || ''} className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}
className={className} >
alt={alt} <Loader2 className="w-5 h-5 text-[#6b7280] animate-spin" />
{...props} </div>
/>
); );
}
if (error || (!blobUrl && !src)) {
return (
fallback || (
<div
className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}
>
<ImageIcon className="w-5 h-5 text-[#9ca3af]" />
</div>
)
);
}
return (
<img
src={blobUrl || src || ""}
className={className}
alt={alt}
{...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}
/> />