PSA/docs/ui/avatar_system.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

562 lines
17 KiB
Markdown

# Avatar System Documentation
## Overview
The Avatar System is a comprehensive solution for managing and displaying user, contact, and client profile images throughout the application. It provides a consistent way to upload, store, retrieve, and display images with appropriate fallbacks when images are not available.
The system follows a component-based architecture with a central base component (`EntityAvatar`) that is extended by entity-specific components (`UserAvatar`, `ContactAvatar`, `ClientAvatar`). This approach ensures consistent styling and behavior while allowing for entity-specific customizations.
## Architecture
The Avatar System is built on a layered architecture:
```mermaid
graph TD
A[UI Components] --> B[Service Layer]
B --> C[Storage Layer]
A --> D[Utility Functions]
D --> B
subgraph "UI Components"
A1[EntityAvatar] --> A2[UserAvatar]
A1 --> A3[ContactAvatar]
A1 --> A4[ClientAvatar]
A5[EntityImageUpload]
end
subgraph "Service Layer"
B1[EntityImageService]
end
subgraph "Utility Functions"
D1[avatarUtils]
end
```
### Key Components
1. **Base Components**:
- `EntityAvatar`: The core component that handles image display, fallbacks, and loading states
- `EntityImageUpload`: Handles image upload, preview, and deletion for all entity types
2. **Entity-Specific Components**:
- `UserAvatar`: Specialized for user avatars
- `ContactAvatar`: Specialized for contact avatars
- `ClientAvatar`: Specialized for client logos
3. **Service Layer**:
- `EntityImageService`: Provides methods for uploading and deleting entity images
4. **Utilities**:
- `avatarUtils`: Helper functions for retrieving image URLs and other common operations
### Data Flow
```mermaid
sequenceDiagram
participant User
participant Component as UI Component
participant Service as EntityImageService
participant Storage as Storage Service
participant DB as Database
User->>Component: Upload image
Component->>Service: uploadEntityImage()
Service->>Storage: Upload file
Storage-->>Service: Return file ID
Service->>DB: Create document record
Service->>DB: Create/update association
Service-->>Component: Return image URL
Component-->>User: Display updated image
```
## Components
### EntityAvatar
The base component that renders an avatar with fallback to initials when no image is available.
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `entityId` | `string \| number` | Unique identifier for the entity |
| `entityName` | `string` | Name of the entity (used for generating initials and colors) |
| `imageUrl` | `string \| null` | URL to the entity's image |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| number` | Size of the avatar (predefined or custom pixel value) |
| `className` | `string` | Optional additional CSS classes |
| `getInitials` | `(name: string) => string` | Optional custom function to generate initials |
| `altText` | `string` | Optional alt text for the image |
#### Usage Example
```tsx
import { EntityAvatar } from '@alga-psa/ui';
<EntityAvatar
entityId="123"
entityName="John Doe"
imageUrl="https://example.com/avatar.jpg"
size="md"
/>
```
### UserAvatar
Specialized component for displaying user avatars.
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `userId` | `string` | User's unique identifier |
| `userName` | `string` | User's name |
| `avatarUrl` | `string \| null` | URL to the user's avatar |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| number` | Size of the avatar |
| `className` | `string` | Optional additional CSS classes |
#### Usage Example
```tsx
import { UserAvatar } from '@alga-psa/ui';
<UserAvatar
userId="user-123"
userName="John Doe"
avatarUrl={userAvatarUrl}
size="md"
/>
```
### ContactAvatar
Specialized component for displaying contact avatars.
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `contactId` | `string` | Contact's unique identifier |
| `contactName` | `string` | Contact's name |
| `avatarUrl` | `string \| null` | URL to the contact's avatar |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| number` | Size of the avatar |
| `className` | `string` | Optional additional CSS classes |
#### Usage Example
```tsx
import { ContactAvatar } from '@alga-psa/ui';
<ContactAvatar
contactId="contact-123"
contactName="Jane Smith"
avatarUrl={contactAvatarUrl}
size="md"
/>
```
### ClientAvatar
Specialized component for displaying client logos.
**Location**: `packages/ui/src/components/ClientAvatar.tsx`
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `clientId` | `string \| number` | Client's unique identifier |
| `clientName` | `string` | Client's name |
| `logoUrl` | `string \| null` | URL to the client's logo |
| `size` | `EntityAvatarProps['size']` | Size of the avatar |
| `className` | `string` | Optional additional CSS classes |
#### Usage Example
```tsx
import { ClientAvatar } from '@alga-psa/ui';
<ClientAvatar
clientId="client-123"
clientName="Acme Inc."
logoUrl={clientLogoUrl}
size="md"
/>
```
### EntityImageUpload
Component for uploading, previewing, and deleting entity images.
#### Props
| Prop | Type | Description |
|------|------|-------------|
| `entityType` | `'user' \| 'contact' \| 'client' \| 'tenant'` | Type of entity |
| `entityId` | `string` | Entity's unique identifier |
| `entityName` | `string` | Entity's name |
| `imageUrl` | `string \| null` | Current image URL |
| `onImageChange` | `(newImageUrl: string \| null) => void` | Callback when image changes |
| `uploadAction` | `(entityId: string, formData: FormData) => Promise<{...}>` | Function to handle upload |
| `deleteAction` | `(entityId: string) => Promise<{...}>` | Function to handle deletion |
| `linkDocumentAsAvatar` | `(args: { entityType: EntityType; entityId: string; documentId: string }) => Promise<LinkDocumentAsAvatarResult>` | Links an existing document as the entity's avatar |
| `renderDocumentSelector` | `(args: { isOpen: boolean; onClose: () => void; onSelectDocumentId: (documentId: string) => void; entityType: EntityType; entityId: string }) => React.ReactNode` | Custom render function for document selector modal |
| `userType` | `string` | Optional user type context |
| `userEntityId` | `string` | Optional user entity ID context |
| `canModify` | `boolean` | Whether the current user can modify the avatar |
| `className` | `string` | Optional additional CSS classes |
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | Size of the avatar preview |
#### Usage Example
```tsx
import { EntityImageUpload } from '@alga-psa/ui';
import { uploadUserAvatar, deleteUserAvatar } from '@alga-psa/users/actions';
<EntityImageUpload
entityType="user"
entityId={userId}
entityName={userName}
imageUrl={avatarUrl}
onImageChange={(newUrl) => setAvatarUrl(newUrl)}
uploadAction={uploadUserAvatar}
deleteAction={deleteUserAvatar}
size="lg"
/>
```
## Services
### EntityImageService
Service for managing entity images, handling upload and deletion operations.
#### Methods
##### `uploadEntityImage`
Uploads an image for an entity and associates it with the entity.
```typescript
async function uploadEntityImage(
entityType: EntityType,
entityId: string,
file: File,
userId: string,
tenant: string,
contextName?: string
): Promise<UploadResult>
```
**Parameters:**
- `entityType`: Type of entity ('user', 'contact', 'client', or 'tenant')
- `entityId`: Entity's unique identifier
- `file`: File object to upload
- `userId`: ID of the user performing the upload
- `tenant`: Tenant context
- `contextName`: Optional context name override
**Returns:**
- Promise resolving to an object with:
- `success`: Boolean indicating success
- `message`: Optional message
- `imageUrl`: Optional new image URL
##### `deleteEntityImage`
Deletes an image associated with an entity.
```typescript
async function deleteEntityImage(
entityType: EntityType,
entityId: string,
userId: string,
tenant: string
): Promise<{ success: boolean; message?: string }>
```
**Parameters:**
- `entityType`: Type of entity ('user', 'contact', 'client', or 'tenant')
- `entityId`: Entity's unique identifier
- `userId`: ID of the user performing the deletion
- `tenant`: Tenant context
**Returns:**
- Promise resolving to an object with:
- `success`: Boolean indicating success
- `message`: Optional message
## Utilities
### avatarUtils
Utility functions for working with entity images.
#### Functions
##### `getEntityImageUrl`
Retrieves the image URL for an entity.
```typescript
async function getEntityImageUrl(
entityType: EntityType,
entityId: string,
tenant: string
): Promise<string | null>
```
**Parameters:**
- `entityType`: Type of entity ('user', 'contact', 'client', or 'tenant')
- `entityId`: Entity's unique identifier
- `tenant`: Tenant context
**Returns:**
- Promise resolving to the image URL or null if not found
##### Convenience Functions
```typescript
// Get a user's avatar URL
async function getUserAvatarUrl(userId: string, tenant: string): Promise<string | null>
// Get a contact's avatar URL
async function getContactAvatarUrl(contactId: string, tenant: string): Promise<string | null>
// Get a client's logo URL
async function getClientLogoUrl(clientId: string, tenant: string): Promise<string | null>
```
##### Batch Functions
For efficient retrieval of multiple avatar URLs in a single query:
```typescript
// Get logo URLs for multiple clients
async function getClientLogoUrlsBatch(clientIds: string[], tenant: string): Promise<Map<string, string | null>>
// Get avatar URLs for multiple users
async function getUserAvatarUrlsBatch(userIds: string[], tenant: string): Promise<Map<string, string | null>>
// Get avatar URLs for multiple contacts
async function getContactAvatarUrlsBatch(contactIds: string[], tenant: string): Promise<Map<string, string | null>>
// Generic batch function for any entity type
async function getEntityImageUrlsBatch(entityType: EntityType, entityIds: string[], tenant: string): Promise<Map<string, string | null>>
```
## Integration Examples
### User Profile Page
```tsx
import { useState, useEffect } from 'react';
import { UserAvatar, EntityImageUpload } from '@alga-psa/ui';
import { uploadUserAvatar, deleteUserAvatar } from '@alga-psa/users/actions';
import { getUserAvatarUrl } from 'server/src/lib/utils/avatarUtils';
const UserProfilePage = ({ userId, userName, tenant }) => {
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
useEffect(() => {
const fetchAvatarUrl = async () => {
const url = await getUserAvatarUrl(userId, tenant);
setAvatarUrl(url);
};
fetchAvatarUrl();
}, [userId, tenant]);
return (
<div className="profile-page">
<h1>User Profile</h1>
<div className="avatar-section">
<EntityImageUpload
entityType="user"
entityId={userId}
entityName={userName}
imageUrl={avatarUrl}
onImageChange={(newUrl) => setAvatarUrl(newUrl)}
uploadAction={uploadUserAvatar}
deleteAction={deleteUserAvatar}
size="lg"
/>
</div>
{/* Rest of profile content */}
</div>
);
};
```
### Client Details Page
```tsx
import { useState, useEffect } from 'react';
import { ClientAvatar, EntityImageUpload } from '@alga-psa/ui';
import { uploadClientLogo, deleteClientLogo } from '@alga-psa/clients/actions';
import { getClientLogoUrl } from 'server/src/lib/utils/avatarUtils';
const ClientDetailsPage = ({ clientId, clientName, tenant }) => {
const [logoUrl, setLogoUrl] = useState<string | null>(null);
useEffect(() => {
const fetchLogoUrl = async () => {
const url = await getClientLogoUrl(clientId, tenant);
setLogoUrl(url);
};
fetchLogoUrl();
}, [clientId, tenant]);
return (
<div className="client-details">
<div className="client-header">
<EntityImageUpload
entityType="client"
entityId={clientId}
entityName={clientName}
imageUrl={logoUrl}
onImageChange={(newUrl) => setLogoUrl(newUrl)}
uploadAction={uploadClientLogo}
deleteAction={deleteClientLogo}
size="xl"
/>
<h1>{clientName}</h1>
</div>
{/* Rest of client details */}
</div>
);
};
```
### Ticket Conversation with User Avatars
```tsx
import { UserAvatar, ContactAvatar } from '@alga-psa/ui';
const TicketComment = ({ comment, author, isContact }) => {
return (
<div className="comment">
<div className="comment-avatar">
{isContact ? (
<ContactAvatar
contactId={author.id}
contactName={author.name}
avatarUrl={author.avatarUrl}
size="md"
/>
) : (
<UserAvatar
userId={author.id}
userName={author.name}
avatarUrl={author.avatarUrl}
size="md"
/>
)}
</div>
<div className="comment-content">
<div className="comment-header">
<span className="author-name">{author.name}</span>
<span className="comment-time">{comment.timestamp}</span>
</div>
<div className="comment-body">
{comment.content}
</div>
</div>
</div>
);
};
```
## Best Practices
### When to Use Each Component
- **EntityAvatar**: Use directly only when creating a new entity-specific avatar component. For most cases, use one of the specialized components.
- **UserAvatar**: Use for displaying user avatars throughout the application.
- **ContactAvatar**: Use for displaying contact avatars throughout the application.
- **ClientAvatar**: Use for displaying client logos throughout the application.
- **EntityImageUpload**: Use on profile/details pages where users should be able to upload or change images.
### Performance Considerations
1. **Image Optimization**:
- The system automatically processes uploaded images to optimize them for display.
- Images are stored efficiently in the storage system.
2. **Loading States**:
- The avatar components include built-in loading states to provide visual feedback during image loading.
- Use the `key` prop with a timestamp when updating images to force re-rendering.
3. **Caching**:
- Image URLs include a timestamp query parameter to prevent browser caching issues.
- Consider implementing a client-side cache for frequently accessed avatar URLs.
### Error Handling
1. **Upload Errors**:
- The `EntityImageUpload` component handles upload errors and displays appropriate toast messages.
- Server-side validation ensures only valid images are accepted.
2. **Display Fallbacks**:
- All avatar components automatically fall back to displaying initials when no image is available.
- The fallback colors are consistently generated based on the entity name.
## Troubleshooting
### Common Issues
#### Images Not Updating Immediately
**Symptoms**: After uploading a new image, the old image still appears in some places.
**Solutions**:
- Ensure the `onImageChange` callback is properly updating state in parent components.
- Add a timestamp query parameter to the image URL to bypass browser caching.
- Use the `key` prop with a timestamp to force React to re-render the component.
#### Missing Avatars
**Symptoms**: Avatars not appearing in certain parts of the application.
**Solutions**:
- Check if the component is correctly retrieving the avatar URL.
- Verify that the entity ID and tenant are correctly passed to the utility functions.
- Ensure the document association exists in the database.
#### Upload Failures
**Symptoms**: Image uploads fail with error messages.
**Solutions**:
- Check file size (must be under 2MB).
- Ensure the file is a valid image format (PNG, JPG, GIF).
- Verify that the user has permission to upload images for the entity.
- Check server logs for detailed error information.
## Integration Points
The Avatar System integrates with several other system components:
1. **Storage System**: For storing and retrieving image files.
2. **Document System**: For managing document metadata and associations.
3. **User Management**: For user avatars and permissions.
4. **Client Management**: For client logos.
5. **Contact Management**: For contact avatars.
## Future Enhancements
Potential improvements to consider for the Avatar System:
1. **Image Cropping**: Add support for cropping images during upload.
2. **Multiple Image Sizes**: Generate and store multiple sizes of each image for different display contexts.
3. **Animated Avatars**: Support for animated GIFs or short video avatars.
4. **Default Avatars**: Provide a selection of default avatars instead of just initials.
5. **Group Avatars**: Support for displaying multiple avatars in a group (e.g., for teams).