feat: display vehicle names instead of UUIDs in document views (refs #31)
- Created shared utility getVehicleLabel() for consistent vehicle display - Updated DocumentsPage to show vehicle names with clickable links - Added "Shared with X vehicles" indicator for multi-vehicle docs - Updated DocumentDetailPage with vehicle name and shared vehicle list - Updated DocumentsMobileScreen with vehicle names and "Shared" indicator - All vehicle names link to vehicle detail pages - Mobile-first with 44px touch targets on all links 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef, useMemo } from 'react';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -7,6 +7,8 @@ import { useDocumentsList } from '../hooks/useDocuments';
|
|||||||
import { useUploadWithProgress } from '../hooks/useUploadWithProgress';
|
import { useUploadWithProgress } from '../hooks/useUploadWithProgress';
|
||||||
import { Button } from '../../../shared-minimal/components/Button';
|
import { Button } from '../../../shared-minimal/components/Button';
|
||||||
import { AddDocumentDialog } from '../components/AddDocumentDialog';
|
import { AddDocumentDialog } from '../components/AddDocumentDialog';
|
||||||
|
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
||||||
|
import { getVehicleLabel } from '../utils/vehicleLabel';
|
||||||
|
|
||||||
export const DocumentsMobileScreen: React.FC = () => {
|
export const DocumentsMobileScreen: React.FC = () => {
|
||||||
console.log('[DocumentsMobileScreen] Component initializing');
|
console.log('[DocumentsMobileScreen] Component initializing');
|
||||||
@@ -17,12 +19,15 @@ export const DocumentsMobileScreen: React.FC = () => {
|
|||||||
|
|
||||||
// Data hooks (unconditional per React rules)
|
// Data hooks (unconditional per React rules)
|
||||||
const { data, isLoading, error } = useDocumentsList();
|
const { data, isLoading, error } = useDocumentsList();
|
||||||
|
const { data: vehicles } = useVehicles();
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [currentId, setCurrentId] = React.useState<string | null>(null);
|
const [currentId, setCurrentId] = React.useState<string | null>(null);
|
||||||
const upload = useUploadWithProgress(currentId || '');
|
const upload = useUploadWithProgress(currentId || '');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isAddOpen, setIsAddOpen] = React.useState(false);
|
const [isAddOpen, setIsAddOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const vehiclesMap = useMemo(() => new Map(vehicles?.map(v => [v.id, v]) || []), [vehicles]);
|
||||||
|
|
||||||
const triggerUpload = (docId: string) => {
|
const triggerUpload = (docId: string) => {
|
||||||
try {
|
try {
|
||||||
setCurrentId(docId);
|
setCurrentId(docId);
|
||||||
@@ -170,14 +175,25 @@ export const DocumentsMobileScreen: React.FC = () => {
|
|||||||
{!isLoading && !hasError && data && data.length > 0 && (
|
{!isLoading && !hasError && data && data.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.map((doc) => {
|
{data.map((doc) => {
|
||||||
const vehicleLabel = doc.vehicleId ? `${doc.vehicleId.slice(0, 8)}...` : '—';
|
const vehicle = vehiclesMap.get(doc.vehicleId);
|
||||||
|
const vehicleLabel = getVehicleLabel(vehicle);
|
||||||
|
const isShared = doc.sharedVehicleIds.length > 0;
|
||||||
return (
|
return (
|
||||||
<div key={doc.id} className="flex items-center justify-between border rounded-xl p-3">
|
<div key={doc.id} className="border rounded-xl p-3 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-slate-800 dark:text-avus">{doc.title}</div>
|
<div className="font-medium text-slate-800 dark:text-avus">{doc.title}</div>
|
||||||
<div className="text-xs text-slate-500 dark:text-titanio">{doc.documentType} • {vehicleLabel}</div>
|
<div className="text-xs text-slate-500 dark:text-titanio">
|
||||||
|
{doc.documentType}
|
||||||
|
{isShared && ' • Shared'}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/garage/vehicles/${doc.vehicleId}`)}
|
||||||
|
className="text-xs text-blue-600 hover:text-blue-800 underline min-h-[44px] inline-flex items-center"
|
||||||
|
>
|
||||||
|
{vehicleLabel}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
<Button onClick={() => navigate(`/garage/documents/${doc.id}`)}>Open</Button>
|
<Button onClick={() => navigate(`/garage/documents/${doc.id}`)}>Open</Button>
|
||||||
<Button onClick={() => triggerUpload(doc.id)}>Upload</Button>
|
<Button onClick={() => triggerUpload(doc.id)}>Upload</Button>
|
||||||
{upload.isPending && currentId === doc.id && (
|
{upload.isPending && currentId === doc.id && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
@@ -8,15 +8,21 @@ import { useDocument } from '../hooks/useDocuments';
|
|||||||
import { useUploadWithProgress } from '../hooks/useUploadWithProgress';
|
import { useUploadWithProgress } from '../hooks/useUploadWithProgress';
|
||||||
import { documentsApi } from '../api/documents.api';
|
import { documentsApi } from '../api/documents.api';
|
||||||
import { DocumentPreview } from '../components/DocumentPreview';
|
import { DocumentPreview } from '../components/DocumentPreview';
|
||||||
|
import { useVehicle, useVehicles } from '../../vehicles/hooks/useVehicles';
|
||||||
|
import { getVehicleLabel } from '../utils/vehicleLabel';
|
||||||
|
|
||||||
export const DocumentDetailPage: React.FC = () => {
|
export const DocumentDetailPage: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0();
|
const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0();
|
||||||
const { data: doc, isLoading, error } = useDocument(id);
|
const { data: doc, isLoading, error } = useDocument(id);
|
||||||
|
const { data: vehicle } = useVehicle(doc?.vehicleId || '');
|
||||||
|
const { data: vehicles } = useVehicles();
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const upload = useUploadWithProgress(id!);
|
const upload = useUploadWithProgress(id!);
|
||||||
|
|
||||||
|
const vehiclesMap = useMemo(() => new Map(vehicles?.map(v => [v.id, v]) || []), [vehicles]);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const blob = await documentsApi.download(id);
|
const blob = await documentsApi.download(id);
|
||||||
@@ -141,7 +147,35 @@ export const DocumentDetailPage: React.FC = () => {
|
|||||||
<div className="p-4 space-y-2">
|
<div className="p-4 space-y-2">
|
||||||
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
||||||
<div className="text-sm text-slate-500">Type: {doc.documentType}</div>
|
<div className="text-sm text-slate-500">Type: {doc.documentType}</div>
|
||||||
<div className="text-sm text-slate-500">Vehicle: {doc.vehicleId}</div>
|
<div className="text-sm">
|
||||||
|
<span className="text-slate-500">Vehicle: </span>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/garage/vehicles/${doc.vehicleId}`)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 underline min-h-[44px] inline-flex items-center"
|
||||||
|
>
|
||||||
|
{getVehicleLabel(vehicle)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{doc.sharedVehicleIds.length > 0 && (
|
||||||
|
<div className="text-sm text-slate-500 space-y-1">
|
||||||
|
<div className="font-medium">Shared with:</div>
|
||||||
|
<ul className="list-disc list-inside pl-2">
|
||||||
|
{doc.sharedVehicleIds.map((vehicleId) => {
|
||||||
|
const sharedVehicle = vehiclesMap.get(vehicleId);
|
||||||
|
return (
|
||||||
|
<li key={vehicleId}>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/garage/vehicles/${vehicleId}`)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 underline min-h-[44px] inline-flex items-center"
|
||||||
|
>
|
||||||
|
{getVehicleLabel(sharedVehicle)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<DocumentPreview doc={doc} />
|
<DocumentPreview doc={doc} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { useDocumentsList, useDeleteDocument } from '../hooks/useDocuments';
|
import { useDocumentsList, useDeleteDocument } from '../hooks/useDocuments';
|
||||||
import { Card } from '../../../shared-minimal/components/Card';
|
import { Card } from '../../../shared-minimal/components/Card';
|
||||||
import { Button } from '../../../shared-minimal/components/Button';
|
import { Button } from '../../../shared-minimal/components/Button';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { AddDocumentDialog } from '../components/AddDocumentDialog';
|
import { AddDocumentDialog } from '../components/AddDocumentDialog';
|
||||||
|
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
||||||
|
import { getVehicleLabel } from '../utils/vehicleLabel';
|
||||||
|
|
||||||
export const DocumentsPage: React.FC = () => {
|
export const DocumentsPage: React.FC = () => {
|
||||||
const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0();
|
const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0();
|
||||||
const { data, isLoading, error } = useDocumentsList();
|
const { data, isLoading, error } = useDocumentsList();
|
||||||
|
const { data: vehicles } = useVehicles();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const removeDoc = useDeleteDocument();
|
const removeDoc = useDeleteDocument();
|
||||||
const [isAddOpen, setIsAddOpen] = React.useState(false);
|
const [isAddOpen, setIsAddOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const vehiclesMap = useMemo(() => new Map(vehicles?.map(v => [v.id, v]) || []), [vehicles]);
|
||||||
|
|
||||||
// Show loading while auth is initializing
|
// Show loading while auth is initializing
|
||||||
if (authLoading) {
|
if (authLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -124,19 +129,36 @@ export const DocumentsPage: React.FC = () => {
|
|||||||
|
|
||||||
{!isLoading && !error && data && data.length > 0 && (
|
{!isLoading && !error && data && data.length > 0 && (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{data.map((doc) => (
|
{data.map((doc) => {
|
||||||
<Card key={doc.id}>
|
const vehicle = vehiclesMap.get(doc.vehicleId);
|
||||||
<div className="p-4 space-y-2">
|
const vehicleLabel = getVehicleLabel(vehicle);
|
||||||
<div className="font-medium">{doc.title}</div>
|
return (
|
||||||
<div className="text-sm text-slate-500">Type: {doc.documentType}</div>
|
<Card key={doc.id}>
|
||||||
<div className="text-sm text-slate-500">Vehicle: {doc.vehicleId}</div>
|
<div className="p-4 space-y-2">
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="font-medium">{doc.title}</div>
|
||||||
<Button onClick={() => navigate(`/garage/documents/${doc.id}`)}>Open</Button>
|
<div className="text-sm text-slate-500">Type: {doc.documentType}</div>
|
||||||
<Button variant="danger" onClick={() => removeDoc.mutate(doc.id)}>Delete</Button>
|
<div className="text-sm">
|
||||||
|
<span className="text-slate-500">Vehicle: </span>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/garage/vehicles/${doc.vehicleId}`)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 underline min-h-[44px] inline-flex items-center"
|
||||||
|
>
|
||||||
|
{vehicleLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{doc.sharedVehicleIds.length > 0 && (
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Shared with {doc.sharedVehicleIds.length} other vehicle{doc.sharedVehicleIds.length > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button onClick={() => navigate(`/garage/documents/${doc.id}`)}>Open</Button>
|
||||||
|
<Button variant="danger" onClick={() => removeDoc.mutate(doc.id)}>Delete</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
frontend/src/features/documents/utils/vehicleLabel.ts
Normal file
11
frontend/src/features/documents/utils/vehicleLabel.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Vehicle } from '../../vehicles/types/vehicles.types';
|
||||||
|
|
||||||
|
export const getVehicleLabel = (vehicle: Vehicle | undefined): string => {
|
||||||
|
if (!vehicle) return 'Unknown Vehicle';
|
||||||
|
if (vehicle.nickname?.trim()) return vehicle.nickname.trim();
|
||||||
|
const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean);
|
||||||
|
const primary = parts.join(' ').trim();
|
||||||
|
if (primary.length > 0) return primary;
|
||||||
|
if (vehicle.vin?.length > 0) return vehicle.vin;
|
||||||
|
return vehicle.id.slice(0, 8) + '...';
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user