feat: add Apple Liquid Glass templates, image security settings, and automated releases

## New Features
- Apple Liquid Glass CSS template with iOS 26-inspired design
- Liquid Glass Dark theme with neon accents
- Image Security settings tab with per-event protection levels
- Release Please automation for versioning and changelog

## Improvements
- Update CSS template migration with final working templates
- Add search placeholder visibility fix for glass themes
- Update README roadmap (Download Protection, Gallery Templates, Filtering & Export now implemented)

## Infrastructure
- Add release-please.yml workflow for automated releases
- Add release-please-config.json and manifest
- Update docker-build.yml with Release Please integration comments
- Add comprehensive CHANGELOG.md

## Cleanup
- Add working/planning docs to .gitignore (CLAUDE.md, test-*.md, feature-*.md, etc.)
- Remove internal planning documents from git tracking (kept locally)

## Files Added
- .github/workflows/release-please.yml
- .release-please-manifest.json
- release-please-config.json
- CHANGELOG.md
- frontend/src/features/settings/tabs/ImageSecurityTab.tsx
This commit is contained in:
Paul Nothaft
2026-01-03 23:35:23 +01:00
parent f3c2cee362
commit 6033461be1
44 changed files with 1978 additions and 9443 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-492
View File
@@ -1,492 +0,0 @@
# Feature: Event Rename
## Overview
This feature allows administrators to rename gallery events from the admin panel. When an event is renamed, all associated resources (database records, file system folders, photo paths, and URLs) are updated to reflect the new name.
## User Story
As an administrator, I want to rename a gallery event so that I can correct typos, update event names after changes, or better organize my galleries while maintaining data integrity.
---
## Feature Requirements
### 1. UI Components
#### 1.1 Rename Button
- **Location**: Event detail page (next to "Edit" button)
- **Label**: "Rename Event" or icon with tooltip
- **Visibility**: Always visible for active (non-archived) events
- **Disabled state**: During rename operation or for archived events
#### 1.2 Rename Dialog/Modal
- **Trigger**: Click on "Rename Event" button
- **Components**:
- Header: "Rename Event"
- Current name display (read-only, for reference)
- Text input field for new event name
- Pre-filled with current name
- Validation: Required, min 3 characters, max 100 characters
- Real-time preview of new slug
- Checkbox: "Resend invitation email with new gallery link"
- Default: Unchecked
- Helper text: "Send updated gallery access email to {customer_email}"
- Cancel button
- Confirm button ("Rename Event")
#### 1.3 Progress/Status Indicator
- **Display**: Replace dialog content during operation
- **States**:
1. "Validating new name..."
2. "Renaming files..."
3. "Updating database..."
4. "Updating photo records..."
5. "Sending email..." (if checkbox selected)
6. "Complete!"
- **Error state**: Show error message with retry option
#### 1.4 Post-Rename Redirect
- After successful rename, redirect to: `/admin/events/{eventId}`
- Show success toast notification: "Event renamed successfully"
- If email sent: "Event renamed and invitation email sent"
---
## Technical Specification
### 2. Database Changes
#### 2.1 Events Table Updates
```sql
UPDATE events SET
event_name = :newEventName,
slug = :newSlug,
share_link = :newShareLink,
updated_at = NOW()
WHERE id = :eventId;
```
#### 2.2 Photos Table Updates
```sql
UPDATE photos SET
path = REPLACE(path, :oldSlug, :newSlug),
thumbnail_path = REPLACE(thumbnail_path, :oldSlug, :newSlug),
updated_at = NOW()
WHERE event_id = :eventId;
```
#### 2.3 New Table: Slug Redirects (Optional - for backward compatibility)
```sql
CREATE TABLE slug_redirects (
id SERIAL PRIMARY KEY,
old_slug VARCHAR(255) NOT NULL UNIQUE,
new_slug VARCHAR(255) NOT NULL,
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_slug_redirects_old_slug ON slug_redirects(old_slug);
```
#### 2.4 Activity Log Entry
```sql
INSERT INTO activity_logs (
event_id, action, details, created_at
) VALUES (
:eventId,
'event_renamed',
'{"old_name": "...", "new_name": "...", "old_slug": "...", "new_slug": "...", "email_sent": true/false}',
NOW()
);
```
---
### 3. File System Changes
#### 3.1 Folder Rename
```
Source: /storage/events/active/{oldSlug}/
Target: /storage/events/active/{newSlug}/
```
#### 3.2 Photo File Rename (within folder)
```
Source: {OldEventName}_individual_0001.jpg
Target: {NewEventName}_individual_0001.jpg
```
#### 3.3 Thumbnail Updates
Thumbnails are stored alongside photos - paths updated via database
#### 3.4 Rollback Strategy
1. Create backup of folder structure before rename
2. If any step fails, restore from backup
3. Use atomic operations where possible
---
### 4. API Endpoints
#### 4.1 Rename Event
```
POST /api/admin/events/:eventId/rename
```
**Request Body:**
```json
{
"newEventName": "New Wedding Gallery Name",
"resendEmail": true
}
```
**Response (Success - 200):**
```json
{
"success": true,
"message": "Event renamed successfully",
"data": {
"eventId": 1,
"oldName": "Test Wedding Gallery",
"newName": "New Wedding Gallery Name",
"oldSlug": "wedding-test-wedding-gallery-2026-01-01",
"newSlug": "wedding-new-wedding-gallery-name-2026-01-01",
"newShareLink": "/gallery/wedding-new-wedding-gallery-name-2026-01-01/abc123...",
"emailSent": true,
"filesRenamed": 3
}
}
```
**Response (Error - 400/500):**
```json
{
"success": false,
"error": "Event name already exists for this date",
"code": "DUPLICATE_SLUG"
}
```
#### 4.2 Validate New Name (Optional - for real-time validation)
```
POST /api/admin/events/:eventId/validate-rename
```
**Request Body:**
```json
{
"newEventName": "New Wedding Gallery Name"
}
```
**Response:**
```json
{
"valid": true,
"newSlug": "wedding-new-wedding-gallery-name-2026-01-01",
"conflicts": []
}
```
---
### 5. Backend Implementation
#### 5.1 New Route File
**Location**: `/backend/src/routes/adminEventRename.js`
#### 5.2 New Service File
**Location**: `/backend/src/services/eventRenameService.js`
**Service Methods:**
```javascript
class EventRenameService {
// Validate new name and check for conflicts
async validateRename(eventId, newEventName)
// Generate new slug from name
generateSlug(eventType, eventName, eventDate)
// Rename event folder on filesystem
async renameEventFolder(oldSlug, newSlug)
// Rename individual photo files
async renamePhotoFiles(eventId, oldName, newName)
// Update database records (events + photos)
async updateDatabaseRecords(eventId, oldSlug, newSlug, newName)
// Store old slug redirect
async createSlugRedirect(eventId, oldSlug, newSlug)
// Main orchestration method
async renameEvent(eventId, newEventName, resendEmail)
// Rollback on failure
async rollbackRename(eventId, backupData)
}
```
#### 5.3 Transaction Handling
```javascript
async renameEvent(eventId, newEventName, resendEmail) {
const trx = await db.transaction();
const backupData = {};
try {
// 1. Validate
const validation = await this.validateRename(eventId, newEventName);
if (!validation.valid) throw new Error(validation.error);
// 2. Get current event data
const event = await trx('events').where({ id: eventId }).first();
backupData.event = event;
// 3. Generate new slug
const newSlug = this.generateSlug(event.event_type, newEventName, event.event_date);
// 4. Rename folder (filesystem)
await this.renameEventFolder(event.slug, newSlug);
backupData.folderRenamed = true;
// 5. Rename photo files
const renamedFiles = await this.renamePhotoFiles(eventId, event.event_name, newEventName);
backupData.renamedFiles = renamedFiles;
// 6. Update database
await this.updateDatabaseRecords(trx, eventId, event.slug, newSlug, newEventName);
// 7. Create redirect entry
await this.createSlugRedirect(trx, eventId, event.slug, newSlug);
// 8. Commit transaction
await trx.commit();
// 9. Send email (after commit, non-critical)
if (resendEmail && event.customer_email) {
await this.sendRenamedEventEmail(eventId);
}
return { success: true, newSlug, ... };
} catch (error) {
await trx.rollback();
await this.rollbackRename(backupData);
throw error;
}
}
```
---
### 6. Frontend Implementation
#### 6.1 New Components
**Location**: `/frontend/src/components/admin/`
- `EventRenameButton.tsx` - Button component
- `EventRenameDialog.tsx` - Modal dialog with form
- `RenameProgressIndicator.tsx` - Status display during operation
#### 6.2 New Service Method
**Location**: `/frontend/src/services/eventService.ts`
```typescript
interface RenameEventRequest {
newEventName: string;
resendEmail: boolean;
}
interface RenameEventResponse {
success: boolean;
message: string;
data: {
eventId: number;
oldName: string;
newName: string;
oldSlug: string;
newSlug: string;
newShareLink: string;
emailSent: boolean;
filesRenamed: number;
};
}
export const renameEvent = async (
eventId: number,
request: RenameEventRequest
): Promise<RenameEventResponse> => {
const response = await api.post(`/admin/events/${eventId}/rename`, request);
return response.data;
};
```
#### 6.3 State Management
- Use React Query mutation for rename operation
- Invalidate event queries on success
- Handle optimistic updates if needed
---
### 7. Email Template Updates
#### 7.1 New/Updated Template
**Template**: Gallery link updated notification
**Subject**: "Your gallery link has been updated - {event_name}"
**Content**:
```
Hello {customer_name},
The gallery for {event_name} has been updated with a new link.
Your new gallery access link:
{new_gallery_url}
Password: (unchanged)
The previous link will automatically redirect to the new location.
Best regards,
{admin_name}
```
---
### 8. Slug Redirect Handling
#### 8.1 Gallery Route Update
**Location**: `/backend/src/routes/gallery.js`
```javascript
// Check for slug redirect before 404
router.get('/:slugOrToken/:token?', async (req, res) => {
let event = await findEventBySlug(req.params.slugOrToken);
if (!event) {
// Check redirect table
const redirect = await db('slug_redirects')
.where({ old_slug: req.params.slugOrToken })
.first();
if (redirect) {
// Redirect to new slug
const newUrl = `/gallery/${redirect.new_slug}/${req.params.token || ''}`;
return res.redirect(301, newUrl);
}
}
// Continue with normal flow...
});
```
---
## Implementation Checklist
### Phase 1: Backend Foundation
- [ ] Create database migration for `slug_redirects` table
- [ ] Create `eventRenameService.js` with core logic
- [ ] Create `adminEventRename.js` route file
- [ ] Add rename endpoint to admin routes
- [ ] Implement file system rename operations
- [ ] Add rollback mechanism
- [ ] Write unit tests for rename service
### Phase 2: Frontend UI
- [ ] Create `EventRenameButton.tsx` component
- [ ] Create `EventRenameDialog.tsx` modal component
- [ ] Create `RenameProgressIndicator.tsx` component
- [ ] Add rename service method to `eventService.ts`
- [ ] Integrate rename button into event detail page
- [ ] Add form validation
- [ ] Handle loading/error states
- [ ] Implement redirect after success
### Phase 3: Email Integration
- [ ] Create/update email template for renamed events
- [ ] Add email sending logic to rename service
- [ ] Test email delivery
### Phase 4: Redirect Support
- [ ] Update gallery route to check slug_redirects
- [ ] Add redirect logging for analytics
- [ ] Test old URLs redirect correctly
### Phase 5: Testing & Polish
- [ ] End-to-end testing with Chrome DevTools
- [ ] Test error scenarios and rollback
- [ ] Test with events containing many photos
- [ ] Performance testing
- [ ] Update API documentation
---
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| File rename fails mid-operation | Medium | High | Transaction + rollback mechanism |
| Duplicate slug collision | Low | Medium | Pre-validation + counter suffix |
| Old bookmarked URLs break | High | Medium | Slug redirect table |
| Email send fails | Low | Low | Non-blocking, log error, notify admin |
| Large events slow to rename | Medium | Low | Progress indicator, async processing |
---
## Security Considerations
1. **Authorization**: Only admin users can rename events
2. **Input validation**: Sanitize new event name, prevent path traversal
3. **Rate limiting**: Prevent abuse of rename endpoint
4. **Audit logging**: Log all rename operations with before/after state
---
## Future Enhancements
1. **Bulk rename**: Rename multiple events at once
2. **Rename history**: View previous names of an event
3. **Undo rename**: Revert to previous name within time window
4. **Scheduled rename**: Set future date for rename to take effect
---
## Related Files
### Backend
- `/backend/src/routes/adminEvents.js` - Existing event routes
- `/backend/src/routes/gallery.js` - Gallery public routes
- `/backend/src/services/photoProcessor.js` - Photo handling
- `/backend/src/utils/filenameSanitizer.js` - Name sanitization
- `/backend/src/utils/shareLinkUtils.js` - Share link generation
### Frontend
- `/frontend/src/components/admin/EventDetails.tsx` - Event detail page
- `/frontend/src/services/eventService.ts` - Event API service
- `/frontend/src/components/admin/EventForm.tsx` - Event form components
### Database
- `/backend/migrations/` - Migration files
- `/backend/src/database/db.js` - Database schema
---
## Acceptance Criteria
1. Admin can click "Rename Event" button on event detail page
2. Rename dialog shows current name and input for new name
3. New slug preview is shown as user types
4. Checkbox option to resend invitation email
5. Progress indicator shows rename steps
6. All files are renamed on filesystem
7. All database records are updated
8. Old gallery URLs redirect to new URLs (301)
9. Admin is redirected to event page after success
10. Success/error notifications are displayed
11. If email checkbox selected, customer receives email with new link
12. Operation can be cancelled before confirmation
13. Archived events cannot be renamed
-564
View File
@@ -1,564 +0,0 @@
# Feature: Optional Event Contact Fields
## Overview
This feature adds a settings option to configure whether client/customer contact information (name, email) and admin email are required fields when creating new events. This addresses the needs of users who don't use the email/SMTP functionality and find these mandatory fields unnecessary.
**GitHub Issue:** [#60](https://github.com/the-luap/picpeak/issues/60)
---
## Problem Statement
Currently, when creating a new event, the following fields are **mandatory**:
- Customer Name
- Customer Email
- Admin Email
For users who:
- Don't use SMTP/email functionality
- Share galleries via manual link distribution
- Self-host for personal use without client management
These required fields create unnecessary friction and force users to enter placeholder data.
---
## User Story
As an administrator, I want to configure whether client contact information is required when creating events, so that I can streamline event creation when I don't need email functionality.
---
## Feature Requirements
### 1. New Settings Options
Add three new settings to the Admin Settings page under a new section "Event Creation":
| Setting | Label | Type | Default | Description |
|---------|-------|------|---------|-------------|
| `event_require_customer_name` | Require customer name | Toggle | `true` | When enabled, customer name is required for new events |
| `event_require_customer_email` | Require customer email | Toggle | `true` | When enabled, customer email is required for new events |
| `event_require_admin_email` | Require admin email | Toggle | `true` | When enabled, admin email is required for new events |
### 2. Settings UI Location
**Path:** Admin Panel → Settings → Event Creation (new section)
```
┌─────────────────────────────────────────────────────────────┐
│ Settings │
├─────────────────────────────────────────────────────────────┤
│ ▼ General │
│ ▼ Gallery Display │
│ ▼ Storage │
│ ▼ Event Creation ← NEW SECTION │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Required Fields │ │
│ │ │ │
│ │ Configure which contact fields are required when │ │
│ │ creating new events. │ │
│ │ │ │
│ │ [✓] Require customer name │ │
│ │ Customer name must be provided for new events │ │
│ │ │ │
│ │ [✓] Require customer email │ │
│ │ Customer email must be provided for new events │ │
│ │ ⚠️ Required for sending gallery invitations │ │
│ │ │ │
│ │ [✓] Require admin email │ │
│ │ Admin email must be provided for new events │ │
│ │ ⚠️ Required for receiving event notifications │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▼ Security │
│ ▼ Email/SMTP │
└─────────────────────────────────────────────────────────────┘
```
### 3. Event Creation Form Updates
The event creation form (`/admin/events/new`) should:
1. **Fetch settings** on load to determine field requirements
2. **Conditionally apply validation** based on settings
3. **Update field labels** to show optional indicator when not required
4. **Remove asterisk (*)** from non-required fields
#### Field Display Logic
| Setting Value | Field Label | Validation | Placeholder |
|---------------|-------------|------------|-------------|
| `true` (required) | "Customer Name *" | Required | "Enter customer name" |
| `false` (optional) | "Customer Name (optional)" | Optional | "Enter customer name (optional)" |
### 4. Warning Messages
When disabling email-related required fields, show informational warnings:
**When disabling "Require customer email":**
> Note: Without a customer email, you won't be able to send gallery invitation emails automatically. You'll need to share gallery links manually.
**When disabling "Require admin email":**
> Note: Without an admin email, you won't receive event notifications such as expiration reminders or guest feedback alerts.
### 5. Backward Compatibility
- Existing events with empty contact fields remain valid
- Default values maintain current behavior (`true` = required)
- Settings changes only affect new event creation
---
## Technical Specification
### 6. Database Changes
#### 6.1 Settings Table Entries
Add new rows to `app_settings` table:
```sql
INSERT INTO app_settings (key, value, category, description) VALUES
('event_require_customer_name', 'true', 'events', 'Require customer name when creating events'),
('event_require_customer_email', 'true', 'events', 'Require customer email when creating events'),
('event_require_admin_email', 'true', 'events', 'Require admin email when creating events');
```
#### 6.2 Migration File
**File:** `/backend/migrations/core/YYYYMMDDHHMMSS_add_optional_event_fields_settings.js`
```javascript
exports.up = function(knex) {
return knex('app_settings').insert([
{
key: 'event_require_customer_name',
value: 'true',
category: 'events',
description: 'Require customer name when creating events'
},
{
key: 'event_require_customer_email',
value: 'true',
category: 'events',
description: 'Require customer email when creating events'
},
{
key: 'event_require_admin_email',
value: 'true',
category: 'events',
description: 'Require admin email when creating events'
}
]);
};
exports.down = function(knex) {
return knex('app_settings')
.whereIn('key', [
'event_require_customer_name',
'event_require_customer_email',
'event_require_admin_email'
])
.del();
};
```
---
### 7. API Changes
#### 7.1 Settings Endpoint (Existing)
The existing settings endpoints should already handle these new settings:
**GET `/api/admin/settings`** - Returns all settings including new ones
**PUT `/api/admin/settings`** - Updates settings including new ones
#### 7.2 Public Settings Endpoint
**GET `/api/public/settings`** - Should include event field requirements for frontend validation
Add to public settings response:
```json
{
"event_require_customer_name": true,
"event_require_customer_email": true,
"event_require_admin_email": true
}
```
#### 7.3 Event Creation Validation Update
**File:** `/backend/src/routes/adminEvents.js`
Update the event creation endpoint to conditionally validate fields:
```javascript
// Before (current)
if (!customer_name) {
return res.status(400).json({ error: 'Customer name is required' });
}
// After (with settings check)
const settings = await getSettings();
if (settings.event_require_customer_name && !customer_name) {
return res.status(400).json({ error: 'Customer name is required' });
}
```
---
### 8. Backend Implementation
#### 8.1 Update Event Validation
**File:** `/backend/src/routes/adminEvents.js`
```javascript
// POST /api/admin/events
router.post('/', adminAuth, async (req, res) => {
try {
const {
event_name,
event_type,
event_date,
customer_name,
customer_email,
admin_email,
// ... other fields
} = req.body;
// Get settings for validation
const settings = await db('app_settings')
.whereIn('key', [
'event_require_customer_name',
'event_require_customer_email',
'event_require_admin_email'
])
.select('key', 'value');
const settingsMap = settings.reduce((acc, s) => {
acc[s.key] = s.value === 'true';
return acc;
}, {});
// Conditional validation
const errors = [];
if (!event_name) {
errors.push('Event name is required');
}
if (settingsMap.event_require_customer_name && !customer_name) {
errors.push('Customer name is required');
}
if (settingsMap.event_require_customer_email && !customer_email) {
errors.push('Customer email is required');
}
if (settingsMap.event_require_admin_email && !admin_email) {
errors.push('Admin email is required');
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// Continue with event creation...
} catch (error) {
// Error handling...
}
});
```
#### 8.2 Add Settings to Public Endpoint
**File:** `/backend/src/routes/publicSettings.js`
```javascript
// Add to public settings response
const eventFieldSettings = await db('app_settings')
.whereIn('key', [
'event_require_customer_name',
'event_require_customer_email',
'event_require_admin_email'
])
.select('key', 'value');
// Include in response
response.eventFieldRequirements = eventFieldSettings.reduce((acc, s) => {
acc[s.key] = s.value === 'true';
return acc;
}, {});
```
---
### 9. Frontend Implementation
#### 9.1 Settings Page Update
**File:** `/frontend/src/components/admin/Settings.tsx` (or similar)
Add new section for Event Creation settings:
```tsx
// New section component
const EventCreationSettings: React.FC = () => {
const { settings, updateSetting } = useSettings();
return (
<SettingsSection title="Event Creation" icon={<CalendarIcon />}>
<SettingsGroup title="Required Fields">
<p className="text-sm text-gray-500 mb-4">
Configure which contact fields are required when creating new events.
</p>
<ToggleSetting
label="Require customer name"
description="Customer name must be provided for new events"
checked={settings.event_require_customer_name}
onChange={(value) => updateSetting('event_require_customer_name', value)}
/>
<ToggleSetting
label="Require customer email"
description="Customer email must be provided for new events"
checked={settings.event_require_customer_email}
onChange={(value) => updateSetting('event_require_customer_email', value)}
warning="Required for sending gallery invitations"
/>
<ToggleSetting
label="Require admin email"
description="Admin email must be provided for new events"
checked={settings.event_require_admin_email}
onChange={(value) => updateSetting('event_require_admin_email', value)}
warning="Required for receiving event notifications"
/>
</SettingsGroup>
</SettingsSection>
);
};
```
#### 9.2 Event Creation Form Update
**File:** `/frontend/src/components/admin/EventForm.tsx` (or similar)
```tsx
interface EventFormProps {
// ... existing props
}
const EventForm: React.FC<EventFormProps> = () => {
const { settings } = usePublicSettings();
const isCustomerNameRequired = settings?.event_require_customer_name ?? true;
const isCustomerEmailRequired = settings?.event_require_customer_email ?? true;
const isAdminEmailRequired = settings?.event_require_admin_email ?? true;
// Form validation schema (using Zod or Yup)
const validationSchema = useMemo(() => {
return z.object({
event_name: z.string().min(1, 'Event name is required'),
customer_name: isCustomerNameRequired
? z.string().min(1, 'Customer name is required')
: z.string().optional(),
customer_email: isCustomerEmailRequired
? z.string().email('Valid email required')
: z.string().email().optional().or(z.literal('')),
admin_email: isAdminEmailRequired
? z.string().email('Valid email required')
: z.string().email().optional().or(z.literal('')),
// ... other fields
});
}, [isCustomerNameRequired, isCustomerEmailRequired, isAdminEmailRequired]);
return (
<form>
{/* Customer Name Field */}
<FormField
label={isCustomerNameRequired ? 'Customer Name' : 'Customer Name (optional)'}
required={isCustomerNameRequired}
>
<Input
name="customer_name"
placeholder={isCustomerNameRequired
? 'Enter customer name'
: 'Enter customer name (optional)'
}
/>
</FormField>
{/* Customer Email Field */}
<FormField
label={isCustomerEmailRequired ? 'Customer Email' : 'Customer Email (optional)'}
required={isCustomerEmailRequired}
>
<Input
name="customer_email"
type="email"
placeholder={isCustomerEmailRequired
? 'Enter customer email'
: 'Enter customer email (optional)'
}
/>
</FormField>
{/* Admin Email Field */}
<FormField
label={isAdminEmailRequired ? 'Admin Email' : 'Admin Email (optional)'}
required={isAdminEmailRequired}
>
<Input
name="admin_email"
type="email"
placeholder={isAdminEmailRequired
? 'Enter admin email'
: 'Enter admin email (optional)'
}
/>
</FormField>
{/* ... rest of form */}
</form>
);
};
```
#### 9.3 Update Types
**File:** `/frontend/src/types/settings.ts`
```typescript
interface PublicSettings {
// ... existing settings
event_require_customer_name: boolean;
event_require_customer_email: boolean;
event_require_admin_email: boolean;
}
```
---
### 10. Email Functionality Impact
When contact fields are optional and left empty:
| Field Empty | Impact |
|-------------|--------|
| Customer Email | Cannot send invitation email, "Send Email" button disabled |
| Admin Email | No notifications sent, warning shown in event details |
| Customer Name | Email salutation uses generic greeting |
#### 10.1 UI Adjustments for Empty Fields
**Event Details Page:**
- If no customer email: Hide/disable "Send Invitation Email" button
- If no admin email: Show info message "No admin email configured for notifications"
**Event Creation Success:**
- If customer email provided: "Event created. Send invitation email?"
- If no customer email: "Event created. Share the gallery link manually."
---
## Implementation Checklist
### Phase 1: Backend
- [ ] Create database migration for new settings
- [ ] Run migration to add default settings
- [ ] Update event creation endpoint with conditional validation
- [ ] Add new settings to public settings endpoint
- [ ] Update event update endpoint with same validation logic
- [ ] Write unit tests for conditional validation
### Phase 2: Frontend - Settings
- [ ] Add "Event Creation" section to Settings page
- [ ] Create toggle components for each setting
- [ ] Add warning messages for email-related settings
- [ ] Test settings save/load functionality
### Phase 3: Frontend - Event Form
- [ ] Update EventForm to fetch settings
- [ ] Implement conditional validation schema
- [ ] Update field labels based on requirements
- [ ] Update placeholder text
- [ ] Test form validation with various settings combinations
### Phase 4: UI Polish
- [ ] Update event details page for empty contact fields
- [ ] Disable email buttons when no email provided
- [ ] Add informational messages where appropriate
- [ ] Update any tooltips or help text
### Phase 5: Testing
- [ ] Test with all settings enabled (default behavior)
- [ ] Test with all settings disabled
- [ ] Test with mixed settings
- [ ] Test event editing with changed settings
- [ ] End-to-end testing
---
## Test Scenarios
| # | Settings | Action | Expected Result |
|---|----------|--------|-----------------|
| 1 | All required (default) | Create event without customer name | Validation error |
| 2 | All required | Create event with all fields | Success |
| 3 | Name optional | Create event without customer name | Success |
| 4 | Email optional | Create event without customer email | Success, email button disabled |
| 5 | All optional | Create event with only event name/date | Success |
| 6 | Mixed | Edit settings, create event | Respects new settings |
---
## Acceptance Criteria
1. ✅ Three new toggle settings available in Admin Settings
2. ✅ Settings persist after save and page refresh
3. ✅ Event creation form respects settings for field requirements
4. ✅ Field labels update to show "(optional)" when not required
5. ✅ Backend validates according to settings
6. ✅ Warning messages shown when disabling email-related fields
7. ✅ Email functionality gracefully handles empty contact fields
8. ✅ Default behavior unchanged (all fields required)
9. ✅ Existing events unaffected by settings changes
---
## Security Considerations
1. Settings changes require admin authentication
2. Public settings endpoint only exposes necessary field requirements
3. No sensitive data exposed through settings
---
## Related Files
### Backend
- `/backend/src/routes/adminEvents.js` - Event creation/update
- `/backend/src/routes/adminSettings.js` - Settings management
- `/backend/src/routes/publicSettings.js` - Public settings API
- `/backend/migrations/core/` - Database migrations
### Frontend
- `/frontend/src/components/admin/Settings.tsx` - Settings page
- `/frontend/src/components/admin/EventForm.tsx` - Event creation form
- `/frontend/src/components/admin/EventDetails.tsx` - Event detail page
- `/frontend/src/services/settingsService.ts` - Settings API service
- `/frontend/src/hooks/useSettings.ts` - Settings hook
---
## Future Enhancements
1. **Field-level defaults**: Set default values for optional fields
2. **Conditional fields**: Show/hide fields based on other selections
3. **Custom required fields**: Allow adding custom required fields
4. **Per-event-type settings**: Different requirements for wedding vs corporate
-798
View File
@@ -1,798 +0,0 @@
# Feature: Photo Filtering & Export for Professional Workflows
## Overview
This feature enables administrators to filter photos by guest feedback (rated, liked, favorited) and export filtered selections in formats compatible with professional photo editing tools like Adobe Lightroom, Capture One, Photo Mechanic, and Affinity Photo.
**Priority:** Low
**Status:** Open
---
## Problem Statement
Currently, PicPeak collects valuable guest feedback (ratings, likes, favorites) but administrators cannot easily:
1. Filter photos in the admin panel based on this feedback
2. Export filtered selections for use in professional photo editing workflows
3. Identify "client picks" to prioritize in post-processing
Professional photographers need to:
- Quickly identify which photos guests loved most
- Export these selections to Lightroom/Capture One for final editing
- Create client-approved photo sets without manual searching
---
## User Stories
1. **As an administrator**, I want to filter photos by rating, likes, or favorites, so that I can quickly see which photos guests preferred.
2. **As an administrator**, I want to export a filtered selection of photos, so that I can import them into Lightroom or Capture One for final processing.
3. **As an administrator**, I want multiple export format options, so that I can use whichever format works best with my photo editing software.
---
## Research: Professional Photo Tool Import Formats
### Industry Standards
Based on research into professional photo editing workflows:
| Format | Description | Compatibility |
|--------|-------------|---------------|
| **XMP Sidecar** | XML-based metadata files with ratings/labels | Lightroom, Bridge, Capture One, ACR |
| **CSV/Text List** | Simple filename list for manual selection | All tools (via search/filter) |
| **ZIP Archive** | Direct photo export with folder structure | All tools (file import) |
| **JSON Metadata** | Structured data for programmatic workflows | Scripts, automation |
### Sources Referenced
- [Adobe XMP Metadata Documentation](https://helpx.adobe.com/lightroom-classic/help/metadata-basics-actions.html)
- [Capture One EIP Format](https://support.captureone.com/hc/en-us/articles/360002478617-Enhanced-Image-Package-EIP-overview)
- [IPTC Photo Metadata Standard](https://iptc.org/standards/photo-metadata/iptc-standard/)
- [ExifTool Documentation](https://exiftool.org/)
- [Lightroom Filename Search Method](https://community.adobe.com/t5/lightroom-classic-discussions/how-to-make-a-selection-of-images-in-lightroom-classic-from-a-list-of-filenames/td-p/12002275)
---
## Current PicPeak Feedback System
### Existing Features (from codebase analysis)
| Feedback Type | Database Column | Scale | Guest Toggle |
|---------------|-----------------|-------|--------------|
| **Ratings** | `average_rating`, `feedback_count` | 1-5 stars | Update/Remove |
| **Likes** | `like_count` | Count | Toggle on/off |
| **Favorites** | `favorite_count` | Count | Toggle on/off |
| **Comments** | `photo_feedback` table | Text | No |
### Database Schema (photos table)
```sql
average_rating DECIMAL(3,2) -- 0.00 to 5.00
feedback_count INTEGER -- Total rating count
like_count INTEGER -- Total likes
favorite_count INTEGER -- Total favorites
```
### Existing Export (feedback only)
- Endpoint: `GET /admin/feedback/events/:eventId/feedback/export`
- Formats: JSON, CSV
- Exports feedback records, not filtered photo lists
---
## Feature Requirements
### 1. Admin Photo Filter UI
#### 1.1 Filter Panel Location
**Path:** Admin Panel → Event → Photos Tab
Add filter controls above the photo grid:
```
┌─────────────────────────────────────────────────────────────────────┐
│ Photos [Upload] │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─ Filters ──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Feedback: [All ▼] [Rated ▼] [≥ 3 stars ▼] │ │
│ │ │ │
│ │ ☑ Has likes (15) ☑ Has favorites (8) ☐ Has comments (3) │ │
│ │ │ │
│ │ [Clear Filters] Showing: 23 of 150 photos │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ [Select All Filtered] [Export Selection ▼] │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ ☑ │ │ ☑ │ │ ☑ │ │ ☐ │ │ ☐ │ │ ☐ │ ... │
│ │ IMG │ │ IMG │ │ IMG │ │ IMG │ │ IMG │ │ IMG │ │
│ │ ⭐⭐⭐│ │ ⭐⭐⭐│ │ ⭐⭐⭐│ │ │ │ │ │ │ │
│ │ ❤ 5 │ │ ❤ 3 │ │ ❤ 8 │ │ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
#### 1.2 Filter Options
| Filter | Type | Options |
|--------|------|---------|
| **Rating** | Dropdown | All, Any Rating, ≥1 star, ≥2 stars, ≥3 stars, ≥4 stars, 5 stars only |
| **Likes** | Checkbox | Has likes (show count) |
| **Favorites** | Checkbox | Has favorites (show count) |
| **Comments** | Checkbox | Has comments (show count) |
| **Combine Logic** | Toggle | AND / OR (default: AND) |
#### 1.3 Quick Filter Presets
| Preset | Description | Filter Applied |
|--------|-------------|----------------|
| "Guest Picks" | Photos guests loved | Rating ≥4 OR Likes ≥3 OR Favorites ≥1 |
| "Top Rated" | Highest rated photos | Rating ≥4 stars |
| "Most Popular" | Most liked photos | Sorted by like_count DESC |
| "Client Favorites" | Favorited by guests | Has favorites |
---
### 2. Photo Selection & Batch Actions
#### 2.1 Selection Controls
- Individual photo checkbox selection
- "Select All Filtered" button (selects all photos matching current filter)
- "Select All" button (selects all photos regardless of filter)
- "Deselect All" button
- Selection counter: "23 photos selected"
#### 2.2 Selection Persistence
- Selection persists when changing filters
- Selection cleared on page navigation (with confirmation if >0 selected)
---
### 3. Export Functionality
#### 3.1 Export Button/Menu
```
┌──────────────────────────────┐
│ Export Selection ▼ │
├──────────────────────────────┤
│ 📋 Filename List (TXT) │ ← Simple text list
│ 📊 Filename List (CSV) │ ← Spreadsheet compatible
│ 📦 XMP Sidecar Files (ZIP) │ ← Lightroom/Bridge/C1
│ 🗂️ Original Photos (ZIP) │ ← Direct file export
│ 📄 Metadata (JSON) │ ← Programmatic use
│ ──────────────────────────── │
│ ⚙️ Export Settings... │ ← Configure defaults
└──────────────────────────────┘
```
#### 3.2 Export Formats
##### Format 1: Filename List (TXT)
**Use Case:** Paste into Lightroom's Library Filter to select matching photos
**Output:**
```
IMG_0001.jpg
IMG_0015.jpg
IMG_0023.jpg
IMG_0089.jpg
```
**Lightroom Workflow:**
1. Export TXT from PicPeak
2. In Lightroom: Library → Filter → Text → Filename → Contains
3. Paste comma-separated list: `IMG_0001, IMG_0015, IMG_0023, IMG_0089`
4. Select filtered photos → Add to Collection
##### Format 2: Filename List (CSV)
**Use Case:** Spreadsheet analysis, Photo Mechanic code replacement
**Output:**
```csv
filename,original_filename,rating,likes,favorites,category
Test_Wedding_individual_0001.jpg,IMG_0001.jpg,4.5,12,3,Ceremony
Test_Wedding_individual_0015.jpg,IMG_0015.jpg,5.0,8,5,Reception
Test_Wedding_individual_0023.jpg,IMG_0023.jpg,4.0,15,2,Portraits
```
##### Format 3: XMP Sidecar Files (ZIP)
**Use Case:** Import ratings/labels directly into Lightroom, Bridge, Capture One
**Output Structure:**
```
export_wedding_picks_2026-01-01.zip
├── IMG_0001.xmp
├── IMG_0015.xmp
├── IMG_0023.xmp
└── IMG_0089.xmp
```
**XMP Content Example:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
xmp:Rating="4"
xmp:Label="Yellow"
photoshop:Instructions="Guest Rating: 4.5 stars, 12 likes, 3 favorites">
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">PicPeak Export - Guest Picks</rdf:li>
</rdf:Alt>
</dc:description>
<Iptc4xmpCore:SubjectCode>
<rdf:Bag>
<rdf:li>guest-pick</rdf:li>
<rdf:li>rating-4</rdf:li>
</rdf:Bag>
</Iptc4xmpCore:SubjectCode>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
```
**XMP Rating Mapping:**
| PicPeak Rating | XMP Rating | XMP Label |
|----------------|------------|-----------|
| 4.5 - 5.0 | 5 | Red |
| 3.5 - 4.4 | 4 | Yellow |
| 2.5 - 3.4 | 3 | Green |
| 1.5 - 2.4 | 2 | Blue |
| 0.5 - 1.4 | 1 | Purple |
| No rating | 0 | None |
**Lightroom Import Workflow:**
1. Export XMP ZIP from PicPeak
2. Extract to folder containing original RAW/JPEG files
3. In Lightroom: Select photos → Metadata → Read Metadata from Files
4. Ratings and labels appear on photos
##### Format 4: Original Photos (ZIP)
**Use Case:** Direct photo export for clients or backup
**Output Structure:**
```
export_wedding_picks_2026-01-01.zip
├── Ceremony/
│ ├── IMG_0001.jpg
│ └── IMG_0002.jpg
├── Reception/
│ ├── IMG_0015.jpg
│ └── IMG_0016.jpg
└── export_manifest.json
```
**Options:**
- Include/exclude categories subfolder organization
- Include/exclude thumbnails
- Include/exclude metadata JSON
- Quality: Original / High (2048px) / Medium (1024px)
##### Format 5: Metadata JSON
**Use Case:** Programmatic workflows, custom integrations
**Output:**
```json
{
"export_info": {
"event_name": "Test Wedding Gallery",
"event_date": "2026-01-01",
"exported_at": "2026-01-02T10:30:00Z",
"filter_applied": {
"min_rating": 4,
"has_likes": true,
"has_favorites": null
},
"total_photos": 23
},
"photos": [
{
"filename": "Test_Wedding_individual_0001.jpg",
"original_filename": "IMG_0001.jpg",
"path": "wedding-test-2026-01-01/individual/Test_Wedding_individual_0001.jpg",
"category": "Ceremony",
"rating": {
"average": 4.5,
"count": 8
},
"likes": 12,
"favorites": 3,
"comments": 2,
"dimensions": {
"width": 5472,
"height": 3648
},
"size_bytes": 4521984,
"uploaded_at": "2026-01-01T14:30:00Z"
}
]
}
```
---
### 4. Export Settings Dialog
```
┌─────────────────────────────────────────────────────────────────┐
│ Export Settings [X] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ XMP Sidecar Options │
│ ─────────────────────────────────────────────────────────────── │
│ Filename matching: ○ PicPeak filename (Test_Wedding_0001.jpg) │
│ ● Original filename (IMG_0001.jpg) │
│ │
│ Include in XMP: ☑ Rating (as XMP Rating 1-5) │
│ ☑ Color label (based on rating) │
│ ☑ Description (feedback summary) │
│ ☑ Keywords (guest-pick, category) │
│ │
│ ─────────────────────────────────────────────────────────────── │
│ Photo Export Options │
│ ─────────────────────────────────────────────────────────────── │
│ Quality: ○ Original ● High (2048px) ○ Medium │
│ Organization: ☑ Group by category │
│ Include: ☑ Metadata JSON ☐ Thumbnails │
│ │
│ ─────────────────────────────────────────────────────────────── │
│ CSV/TXT Options │
│ ─────────────────────────────────────────────────────────────── │
│ Filename format: ○ PicPeak filename ● Original filename │
│ Separator: ○ Newline ● Comma ○ Semicolon │
│ │
│ [Cancel] [Save as Default] [OK] │
└─────────────────────────────────────────────────────────────────┘
```
---
## Technical Specification
### 5. API Endpoints
#### 5.1 Get Filtered Photos
```
GET /api/admin/photos/:eventId/filtered
```
**Query Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `min_rating` | number | Minimum average rating (0-5) |
| `max_rating` | number | Maximum average rating (0-5) |
| `has_likes` | boolean | Filter photos with likes |
| `min_likes` | number | Minimum like count |
| `has_favorites` | boolean | Filter photos with favorites |
| `min_favorites` | number | Minimum favorite count |
| `has_comments` | boolean | Filter photos with comments |
| `category_id` | number | Filter by category |
| `logic` | string | 'AND' or 'OR' (default: AND) |
| `sort` | string | 'rating', 'likes', 'favorites', 'date' |
| `order` | string | 'asc' or 'desc' |
| `page` | number | Page number |
| `limit` | number | Photos per page |
**Response:**
```json
{
"success": true,
"data": {
"photos": [...],
"pagination": {
"total": 150,
"filtered": 23,
"page": 1,
"limit": 50,
"pages": 1
},
"summary": {
"with_ratings": 45,
"with_likes": 78,
"with_favorites": 23,
"with_comments": 12
}
}
}
```
#### 5.2 Export Photos
```
POST /api/admin/photos/:eventId/export
```
**Request Body:**
```json
{
"photo_ids": [1, 5, 12, 23, 45],
"format": "xmp",
"options": {
"filename_format": "original",
"include_rating": true,
"include_label": true,
"include_description": true,
"include_keywords": true
}
}
```
**Or with filter (export all matching):**
```json
{
"filter": {
"min_rating": 4,
"has_likes": true
},
"format": "xmp",
"options": {...}
}
```
**Response:**
- For small exports: Direct file download
- For large exports: Background job with status polling
```json
{
"success": true,
"job_id": "export_abc123",
"status": "processing",
"progress": 45,
"estimated_time": 30
}
```
#### 5.3 Check Export Status
```
GET /api/admin/exports/:jobId/status
```
#### 5.4 Download Export
```
GET /api/admin/exports/:jobId/download
```
---
### 6. Backend Implementation
#### 6.1 New Files
| File | Purpose |
|------|---------|
| `/backend/src/routes/adminPhotoExport.js` | Export API routes |
| `/backend/src/services/photoExportService.js` | Export logic |
| `/backend/src/services/xmpGenerator.js` | XMP file generation |
| `/backend/src/utils/photoFilterBuilder.js` | Query builder for filters |
#### 6.2 XMP Generator Service
```javascript
// /backend/src/services/xmpGenerator.js
class XmpGenerator {
/**
* Generate XMP sidecar content for a photo
*/
generateXmp(photo, options = {}) {
const rating = this.mapRating(photo.average_rating);
const label = this.mapLabel(photo.average_rating);
return `<?xml version="1.0" encoding="UTF-8"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
xmp:Rating="${rating}"
${label ? `xmp:Label="${label}"` : ''}>
${this.generateDescription(photo, options)}
${this.generateKeywords(photo, options)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>`;
}
mapRating(avgRating) {
if (!avgRating || avgRating === 0) return 0;
if (avgRating >= 4.5) return 5;
if (avgRating >= 3.5) return 4;
if (avgRating >= 2.5) return 3;
if (avgRating >= 1.5) return 2;
return 1;
}
mapLabel(avgRating) {
if (!avgRating || avgRating === 0) return null;
if (avgRating >= 4.5) return 'Red'; // Top picks
if (avgRating >= 3.5) return 'Yellow'; // Good
if (avgRating >= 2.5) return 'Green'; // Average
if (avgRating >= 1.5) return 'Blue'; // Below average
return 'Purple'; // Low
}
generateDescription(photo, options) {
if (!options.include_description) return '';
const desc = `PicPeak Guest Feedback: ${photo.average_rating?.toFixed(1) || 0} stars, ${photo.like_count || 0} likes, ${photo.favorite_count || 0} favorites`;
return `<dc:description><rdf:Alt><rdf:li xml:lang="x-default">${desc}</rdf:li></rdf:Alt></dc:description>`;
}
generateKeywords(photo, options) {
if (!options.include_keywords) return '';
const keywords = ['picpeak-export'];
if (photo.average_rating >= 4) keywords.push('guest-pick');
if (photo.category_name) keywords.push(photo.category_name.toLowerCase());
return `<dc:subject><rdf:Bag>${keywords.map(k => `<rdf:li>${k}</rdf:li>`).join('')}</rdf:Bag></dc:subject>`;
}
}
```
#### 6.3 Export Service
```javascript
// /backend/src/services/photoExportService.js
class PhotoExportService {
async exportPhotos(eventId, photoIds, format, options) {
const photos = await this.getPhotosWithFeedback(eventId, photoIds);
switch (format) {
case 'txt':
return this.exportAsTxt(photos, options);
case 'csv':
return this.exportAsCsv(photos, options);
case 'xmp':
return this.exportAsXmpZip(photos, options);
case 'photos':
return this.exportPhotosZip(photos, options);
case 'json':
return this.exportAsJson(photos, options);
default:
throw new Error(`Unknown format: ${format}`);
}
}
async exportAsXmpZip(photos, options) {
const archive = archiver('zip', { zlib: { level: 9 } });
const xmpGenerator = new XmpGenerator();
for (const photo of photos) {
const filename = options.filename_format === 'original'
? photo.original_filename
: photo.filename;
const xmpFilename = filename.replace(/\.[^.]+$/, '.xmp');
const xmpContent = xmpGenerator.generateXmp(photo, options);
archive.append(xmpContent, { name: xmpFilename });
}
return archive;
}
// ... other export methods
}
```
---
### 7. Frontend Implementation
#### 7.1 New Components
| Component | Purpose |
|-----------|---------|
| `PhotoFilterPanel.tsx` | Filter controls UI |
| `PhotoExportMenu.tsx` | Export dropdown menu |
| `ExportSettingsDialog.tsx` | Export configuration modal |
| `ExportProgressModal.tsx` | Progress indicator for large exports |
#### 7.2 Filter Panel Component
```typescript
// /frontend/src/components/admin/PhotoFilterPanel.tsx
interface PhotoFilters {
minRating: number | null;
hasLikes: boolean;
hasFavorites: boolean;
hasComments: boolean;
logic: 'AND' | 'OR';
}
interface PhotoFilterPanelProps {
filters: PhotoFilters;
onChange: (filters: PhotoFilters) => void;
summary: {
total: number;
filtered: number;
withRatings: number;
withLikes: number;
withFavorites: number;
};
}
export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
filters,
onChange,
summary
}) => {
// Component implementation
};
```
#### 7.3 State Management
```typescript
// Use React Query for filtered photos
const useFilteredPhotos = (eventId: string, filters: PhotoFilters) => {
return useQuery({
queryKey: ['photos', eventId, 'filtered', filters],
queryFn: () => photoService.getFilteredPhotos(eventId, filters),
keepPreviousData: true
});
};
// Selection state
const [selectedPhotoIds, setSelectedPhotoIds] = useState<Set<number>>(new Set());
```
---
### 8. Database Considerations
#### 8.1 Indexing for Filter Performance
```sql
-- Add indexes for common filter queries
CREATE INDEX idx_photos_average_rating ON photos(event_id, average_rating) WHERE average_rating > 0;
CREATE INDEX idx_photos_like_count ON photos(event_id, like_count) WHERE like_count > 0;
CREATE INDEX idx_photos_favorite_count ON photos(event_id, favorite_count) WHERE favorite_count > 0;
```
#### 8.2 Export Jobs Table (for large exports)
```sql
CREATE TABLE export_jobs (
id SERIAL PRIMARY KEY,
job_id VARCHAR(50) UNIQUE NOT NULL,
event_id INTEGER REFERENCES events(id),
admin_user_id INTEGER REFERENCES admin_users(id),
format VARCHAR(20) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
progress INTEGER DEFAULT 0,
total_photos INTEGER,
options JSONB,
file_path VARCHAR(500),
file_size BIGINT,
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP
);
```
---
## Implementation Checklist
### Phase 1: Backend - Filtering
- [ ] Create `photoFilterBuilder.js` utility
- [ ] Add filtered photos endpoint to admin routes
- [ ] Add database indexes for performance
- [ ] Write unit tests for filter logic
### Phase 2: Backend - Export
- [ ] Create `xmpGenerator.js` service
- [ ] Create `photoExportService.js` service
- [ ] Add export endpoint to admin routes
- [ ] Implement TXT export
- [ ] Implement CSV export
- [ ] Implement XMP ZIP export
- [ ] Implement Photos ZIP export
- [ ] Implement JSON export
- [ ] Add background job support for large exports
- [ ] Write unit tests for export formats
### Phase 3: Frontend - Filter UI
- [ ] Create `PhotoFilterPanel.tsx` component
- [ ] Integrate filter panel into Photos tab
- [ ] Add filter state management
- [ ] Implement filter summary display
- [ ] Add preset filter buttons
### Phase 4: Frontend - Selection
- [ ] Add photo selection checkboxes
- [ ] Implement "Select All Filtered" functionality
- [ ] Add selection counter
- [ ] Persist selection across filter changes
### Phase 5: Frontend - Export
- [ ] Create `PhotoExportMenu.tsx` component
- [ ] Create `ExportSettingsDialog.tsx` component
- [ ] Create `ExportProgressModal.tsx` component
- [ ] Integrate export functionality
- [ ] Handle download responses
### Phase 6: Testing & Documentation
- [ ] End-to-end testing with real Lightroom import
- [ ] Test Capture One XMP compatibility
- [ ] Test with large photo sets (500+ photos)
- [ ] Document workflow guides for users
- [ ] Update API documentation
---
## Test Scenarios
| # | Scenario | Expected Result |
|---|----------|-----------------|
| 1 | Filter by rating ≥4 stars | Only high-rated photos shown |
| 2 | Filter by likes AND favorites | Photos with both shown |
| 3 | Filter by likes OR favorites | Photos with either shown |
| 4 | Export XMP for 10 photos | ZIP with 10 .xmp files |
| 5 | Import XMP into Lightroom | Ratings appear on matched photos |
| 6 | Export TXT list | Newline-separated filename list |
| 7 | Export large set (500 photos) | Background job with progress |
---
## Acceptance Criteria
1. ✅ Admin can filter photos by rating threshold
2. ✅ Admin can filter photos by likes/favorites/comments
3. ✅ Admin can combine filters with AND/OR logic
4. ✅ Filter counts shown in UI
5. ✅ Admin can select individual or all filtered photos
6. ✅ Export menu shows all format options
7. ✅ TXT export contains filename list
8. ✅ CSV export contains metadata columns
9. ✅ XMP export creates valid sidecar files
10. ✅ XMP files import correctly into Lightroom
11. ✅ ZIP export includes organized photos
12. ✅ Large exports show progress indicator
13. ✅ Export settings can be customized
---
## Security Considerations
1. Admin authentication required for all endpoints
2. Event ownership validation before export
3. Rate limiting on export endpoints
4. Temporary file cleanup after download
5. No sensitive data in exported files
---
## Related Files
### Backend
- `/backend/src/routes/adminPhotos.js` - Existing photo routes
- `/backend/src/routes/adminFeedback.js` - Existing feedback export
- `/backend/src/services/feedbackService.js` - Feedback queries
### Frontend
- `/frontend/src/components/admin/EventPhotos.tsx` - Photos tab
- `/frontend/src/components/admin/PhotoGrid.tsx` - Photo display
- `/frontend/src/services/photoService.ts` - Photo API service
---
## Future Enhancements
1. **Scheduled exports**: Auto-export daily/weekly
2. **Email delivery**: Send export to admin email
3. **Cloud storage**: Export directly to Dropbox/Google Drive
4. **Custom XMP templates**: User-defined XMP structures
5. **Batch operations**: Apply ratings/labels in bulk from PicPeak
6. **Two-way sync**: Import ratings back from Lightroom
-338
View File
@@ -1,338 +0,0 @@
# Test Specification: Custom CSS Gallery Templates
This document specifies the test cases for the Custom CSS Gallery Templates feature, which allows administrators to create and manage up to 3 custom CSS templates for gallery styling.
## Prerequisites
- Local Docker environment running (`docker-compose up`)
- Access to admin dashboard
- Backend migrations applied (052_add_css_templates.js)
## Test Cases
### 1. Template Editor Access
#### TC-CCT-001: Access CSS Templates Tab
**Steps:**
1. Navigate to Settings page
2. Look for "Custom CSS Templates" or "Styling" section
**Expected Result:**
- CSS Templates editor is accessible
- Three template slots are visible as tabs
#### TC-CCT-002: Default Template Content
**Steps:**
1. Navigate to CSS Templates editor
2. Select Template 1 tab
**Expected Result:**
- Template 1 named "Elegant Dark"
- Contains pre-populated CSS content
- Is marked as enabled
- Is marked as default
### 2. Template Editing
#### TC-CCT-003: Edit Template Name
**Steps:**
1. Select Template 2
2. Change name from "Untitled" to "My Custom Theme"
3. Save template
**Expected Result:**
- Name updates in tab
- Save confirmation shown
- Name persists after refresh
#### TC-CCT-004: Edit CSS Content
**Steps:**
1. Select Template 2
2. Add CSS: `.gallery-page { background: #ff0000; }`
3. Save template
**Expected Result:**
- CSS saved successfully
- No sanitization warnings for valid CSS
- Character count updates
#### TC-CCT-005: Enable/Disable Template
**Steps:**
1. Select Template 2
2. Toggle "Enable this template" checkbox
3. Save template
**Expected Result:**
- Template status changes
- Tab shows check mark when enabled
- Disabled templates not available in event form
### 3. CSS Sanitization
#### TC-CCT-006: Block JavaScript Expressions
**Steps:**
1. Enter CSS with `expression(alert('xss'))`
2. Save template
**Expected Result:**
- Pattern blocked (replaced with /* BLOCKED */)
- Sanitization warning shown
- Template saves with sanitized content
#### TC-CCT-007: Block @import Rules
**Steps:**
1. Enter CSS with `@import url('http://evil.com/styles.css');`
2. Save template
**Expected Result:**
- @import blocked
- Warning shown
- External resource not loaded
#### TC-CCT-008: Block External URLs
**Steps:**
1. Enter CSS with `background-image: url('http://external.com/image.jpg');`
2. Save template
**Expected Result:**
- External URL blocked
- Only data: URIs allowed for images
- Warning shown
#### TC-CCT-009: Allow Safe CSS Properties
**Steps:**
1. Enter CSS with standard properties:
```css
.gallery-page {
background-color: #333;
color: white;
font-family: Arial, sans-serif;
padding: 20px;
}
```
2. Save template
**Expected Result:**
- All properties saved as-is
- No sanitization warnings
- CSS valid
### 4. Template Size Limits
#### TC-CCT-010: CSS Size Limit
**Steps:**
1. Try to save CSS content > 100KB
2. Attempt to save
**Expected Result:**
- Error message about size limit
- Template not saved
- User informed of 100KB limit
### 5. Reset to Default
#### TC-CCT-011: Reset Template 1
**Steps:**
1. Modify Template 1 CSS
2. Save changes
3. Click "Reset to Default"
4. Confirm action
**Expected Result:**
- Template reverts to default "Elegant Dark" content
- Name reset to "Elegant Dark"
- Enable status reset to true
#### TC-CCT-012: Reset Button Only on Template 1
**Steps:**
1. Select Template 2
2. Look for Reset button
**Expected Result:**
- Reset to Default button NOT shown for Template 2 or 3
- Only Template 1 has reset option
### 6. Event Integration
#### TC-CCT-013: Template Dropdown in Event Form
**Steps:**
1. Enable at least one CSS template
2. Navigate to Create Event page
3. Look for CSS Template selector
**Expected Result:**
- Dropdown shows "None (Use default theme)" option
- Enabled templates appear in list
- Disabled templates NOT shown
#### TC-CCT-014: Assign Template to Event
**Steps:**
1. Create new event
2. Select an enabled CSS template
3. Save event
**Expected Result:**
- Event created with template assigned
- Template ID stored in database
- Event edit shows selected template
#### TC-CCT-015: Update Event Template
**Steps:**
1. Edit existing event
2. Change CSS template selection
3. Save event
**Expected Result:**
- Template updated successfully
- Gallery reflects new template
### 7. Gallery CSS Loading
#### TC-CCT-016: Gallery Loads Custom CSS
**Steps:**
1. Assign template to an event
2. View gallery as guest
3. Inspect page source/styles
**Expected Result:**
- Custom CSS injected via `<style id="gallery-custom-css">`
- Gallery styling matches template
- CSS scoped to .gallery-page
#### TC-CCT-017: Gallery Without Template
**Steps:**
1. Create event without template (select "None")
2. View gallery
**Expected Result:**
- No custom CSS loaded
- Default theme used
- No errors
#### TC-CCT-018: Disabled Template Not Applied
**Steps:**
1. Assign template to event
2. Disable the template in settings
3. View gallery
**Expected Result:**
- Custom CSS NOT loaded
- Gallery uses default styling
- No errors
### 8. API Tests
#### TC-CCT-019: Get All Templates
**Steps:**
1. Call API: `GET /api/admin/css-templates`
**Expected Result:**
- Returns array of 3 templates
- Each has: id, slot_number, name, css_content, is_enabled, is_default, updated_at
#### TC-CCT-020: Get Enabled Templates
**Steps:**
1. Call API: `GET /api/admin/css-templates/enabled`
**Expected Result:**
- Returns only enabled templates
- Each has: id, name, slot_number
#### TC-CCT-021: Update Template
**Steps:**
1. Call API: `PUT /api/admin/css-templates/2`
Body: `{ "name": "Test", "css_content": "...", "is_enabled": true }`
**Expected Result:**
- Returns 200 OK
- Template updated
- Sanitization warnings array included
#### TC-CCT-022: Gallery CSS Endpoint
**Steps:**
1. Assign template to event with slug "test-gallery"
2. Call API: `GET /api/gallery/test-gallery/css-template`
**Expected Result:**
- Returns 200 OK with Content-Type: text/css
- Body contains sanitized CSS content
#### TC-CCT-023: Gallery CSS Not Found
**Steps:**
1. Create event without template
2. Call API: `GET /api/gallery/no-template-event/css-template`
**Expected Result:**
- Returns 204 No Content
- No body
### 9. Edge Cases
#### TC-CCT-024: Empty CSS Content
**Steps:**
1. Save template with empty CSS content
2. Assign to event
3. View gallery
**Expected Result:**
- Template saves successfully
- Gallery loads without custom CSS
- No errors
#### TC-CCT-025: Invalid CSS Syntax
**Steps:**
1. Enter CSS with mismatched braces: `{ color: red;`
2. Try to save
**Expected Result:**
- Validation error shown
- Template not saved
- Error message indicates syntax issue
### 10. Persistence Tests
#### TC-CCT-026: Template Persists After Restart
**Steps:**
1. Create and save custom template
2. Restart backend container
3. Reload template editor
**Expected Result:**
- Template content preserved
- All settings intact
- No data loss
## Files Created/Modified
### Backend
- `/backend/migrations/core/052_add_css_templates.js`
- `/backend/src/utils/cssSanitizer.js`
- `/backend/src/routes/adminCssTemplates.js`
- `/backend/src/routes/gallery.js`
- `/backend/server.js`
### Frontend
- `/frontend/src/services/cssTemplates.service.ts`
- `/frontend/src/components/admin/CssTemplateEditor.tsx`
- `/frontend/src/components/admin/index.ts`
- `/frontend/src/hooks/useGalleryCustomCss.ts`
## Integration Notes
The following additional integrations are recommended:
1. Add CssTemplateEditor to Settings page styling tab
2. Add CSS template dropdown to CreateEventPageEnhanced.tsx
3. Add CSS template dropdown to CreateEventPage.tsx
4. Update event edit forms to show/edit template selection
5. Update GalleryPage.tsx to use useGalleryCustomCss hook
6. Add `.gallery-page` class to gallery container components
## Automated Testing Notes
For Playwright tests:
1. Login to admin dashboard
2. Navigate to CSS Templates editor
3. Manipulate template tabs, inputs, and checkboxes
4. Verify save operations via API calls
5. Navigate to Create Event, verify template dropdown
6. View gallery, verify custom CSS is applied
-342
View File
@@ -1,342 +0,0 @@
# Test Specification: Event Rename Feature
## Overview
This document specifies the test cases for the Event Rename feature, which allows administrators to rename events from the admin panel.
---
## Test Environment
- **URL**: http://localhost:7100
- **Admin Credentials**: admin / AdminTest@2026!
- **Prerequisites**: At least one active (non-archived) event with photos
---
## Test Cases
### TC-RN-001: Access Rename Dialog
**Description**: Verify the Rename button is visible and opens the rename dialog
**Steps**:
1. Login to admin panel
2. Navigate to an active event's detail page
3. Verify "Rename" button is visible in the header action buttons
4. Click the "Rename" button
5. Verify rename dialog opens
**Expected Results**:
- Rename button visible next to Edit button
- Dialog opens with current event name pre-filled
- Dialog shows input field for new name
---
### TC-RN-002: Validate New Name - Too Short
**Description**: Verify validation error for names shorter than 3 characters
**Steps**:
1. Open rename dialog for an event
2. Enter a name with 2 characters (e.g., "AB")
3. Observe validation state
**Expected Results**:
- Rename button remains disabled
- No API call made for validation
---
### TC-RN-003: Validate New Name - Same as Current
**Description**: Verify error when new name generates same slug as current
**Steps**:
1. Open rename dialog for an event
2. Enter the same name as current (or minor variation that results in same slug)
3. Wait for validation
**Expected Results**:
- Error message: "New name generates the same URL as the current name"
- Rename button disabled
---
### TC-RN-004: Validate New Name - Duplicate Slug
**Description**: Verify error when new name would conflict with existing event
**Steps**:
1. Create two events with different names on the same date
2. Open rename dialog for event A
3. Enter event B's name
4. Wait for validation
**Expected Results**:
- Error message: "An event with this name already exists for the same date"
- Rename button disabled
---
### TC-RN-005: Validate New Name - Valid
**Description**: Verify successful validation of a valid new name
**Steps**:
1. Open rename dialog for an event
2. Enter a valid, unique new name (at least 3 characters)
3. Wait for validation
**Expected Results**:
- New URL preview shown in green box
- Rename button becomes enabled
- No error messages
---
### TC-RN-006: Successful Rename
**Description**: Verify complete rename operation
**Steps**:
1. Open rename dialog for an event with photos
2. Enter a valid new name
3. Wait for validation to complete
4. Click "Rename Event" button
5. Wait for operation to complete
**Expected Results**:
- Progress indicator shown during operation
- Success message displayed
- New share link shown
- Files renamed count displayed
- Event name updated in UI
- Share link updated
---
### TC-RN-007: Rename with Email Notification
**Description**: Verify rename with email notification option
**Steps**:
1. Open rename dialog for event with customer email
2. Enter valid new name
3. Check "Resend invitation email with new gallery link" checkbox
4. Click "Rename Event"
5. Wait for completion
**Expected Results**:
- Operation completes successfully
- Email sent confirmation shown
- (If SMTP configured) Email received with new link
---
### TC-RN-008: Old URL Redirects to New URL
**Description**: Verify old gallery URLs redirect to new location
**Steps**:
1. Note the current gallery share link before rename
2. Rename the event
3. Try to access the old share link
4. Verify redirect to new URL
**Expected Results**:
- Old URL returns 301 redirect response
- Browser redirects to new gallery URL
- Gallery is accessible at new URL
---
### TC-RN-009: Archived Events Cannot Be Renamed
**Description**: Verify rename button is not available for archived events
**Steps**:
1. Navigate to an archived event's detail page
2. Check for Rename button presence
**Expected Results**:
- Rename button is NOT visible
- Only archive-related actions available
---
### TC-RN-010: Rename Dialog Cancel
**Description**: Verify cancel functionality in rename dialog
**Steps**:
1. Open rename dialog
2. Enter a new name
3. Click Cancel button
4. Verify dialog closes
5. Verify event name unchanged
**Expected Results**:
- Dialog closes
- No changes made to event
- Event name remains original
---
### TC-RN-011: Rename Dialog Close (X Button)
**Description**: Verify X button closes dialog without changes
**Steps**:
1. Open rename dialog
2. Enter a new name
3. Click X button in top-right
4. Verify dialog closes
5. Verify event name unchanged
**Expected Results**:
- Dialog closes
- No changes made to event
---
### TC-RN-012: Photo File Paths Updated
**Description**: Verify photo file paths are updated after rename
**Steps**:
1. Create event with photos
2. Note photo paths in database
3. Rename the event
4. Verify photos are still accessible
5. Check photo paths in database
**Expected Results**:
- All photos remain accessible
- Photo paths updated to reflect new slug
- Thumbnails still work
---
### TC-RN-013: Activity Log Entry Created
**Description**: Verify rename operation is logged
**Steps**:
1. Rename an event
2. Check activity logs in database or admin panel
**Expected Results**:
- Activity log entry created with type "event_renamed"
- Metadata includes old name, new name, old slug, new slug
- Actor information recorded
---
## API Test Cases
### TC-RN-API-001: POST /api/admin/events/:id/rename
**Description**: Test rename API endpoint
**Request**:
```json
{
"newEventName": "New Event Name",
"resendEmail": false
}
```
**Expected Response (200)**:
```json
{
"success": true,
"message": "Event renamed successfully",
"data": {
"eventId": 1,
"oldName": "Old Event Name",
"newName": "New Event Name",
"oldSlug": "wedding-old-event-name-2026-01-01",
"newSlug": "wedding-new-event-name-2026-01-01",
"newShareLink": "/gallery/wedding-new-event-name-2026-01-01/abc123...",
"emailSent": false,
"filesRenamed": 3
}
}
```
---
### TC-RN-API-002: POST /api/admin/events/:id/validate-rename
**Description**: Test rename validation endpoint
**Request**:
```json
{
"newEventName": "New Event Name"
}
```
**Expected Response (200)**:
```json
{
"valid": true,
"newSlug": "wedding-new-event-name-2026-01-01"
}
```
---
### TC-RN-API-003: Rename Requires Authentication
**Description**: Verify API requires admin authentication
**Steps**:
1. Call rename endpoint without auth token
2. Call rename endpoint with invalid token
**Expected Results**:
- 401 Unauthorized response
---
## Database Verification
### TC-RN-DB-001: Events Table Updated
After successful rename, verify:
- `event_name` updated to new name
- `slug` updated to new slug
- `share_link` updated with new slug
### TC-RN-DB-002: Photos Table Updated
After successful rename, verify:
- `path` column updated for all event photos
- `thumbnail_path` column updated for all event photos
### TC-RN-DB-003: Slug Redirects Table Populated
After successful rename, verify:
- Entry created in `slug_redirects` table
- `old_slug` contains previous slug
- `new_slug` contains new slug
- `event_id` references correct event
---
## Error Handling
### TC-RN-ERR-001: Database Error During Rename
**Description**: Verify rollback on database error
**Expected Behavior**:
- File system changes rolled back
- Original event state preserved
- Error message returned to user
### TC-RN-ERR-002: File System Error During Rename
**Description**: Verify handling of file system errors
**Expected Behavior**:
- Transaction rolled back
- Error message returned to user
- Event remains unchanged
---
## Performance
### TC-RN-PERF-001: Rename Large Event
**Description**: Verify performance with events containing many photos
**Steps**:
1. Create event with 100+ photos
2. Rename the event
3. Measure time taken
**Expected Results**:
- Operation completes within reasonable time
- Progress indicator keeps user informed
- All photos remain accessible
-245
View File
@@ -1,245 +0,0 @@
# Test Specification: Optional Event Fields
This document specifies the test cases for the Optional Event Fields feature, which allows administrators to configure whether customer name, customer email, and admin email are required when creating events.
## Prerequisites
- Local Docker environment running (`docker-compose up`)
- Access to admin dashboard
- Backend migrations applied
## Test Cases
### 1. Settings Page - Event Creation Tab
#### TC-OEF-001: Event Creation Tab Visibility
**Steps:**
1. Navigate to Settings page (`/admin/settings`)
2. Verify "Event Creation" tab is visible
**Expected Result:**
- Tab labeled "Event Creation" or similar should be present in the settings navigation
#### TC-OEF-002: Default Field Requirements
**Steps:**
1. Navigate to Settings > Event Creation tab
2. Check initial state of all three toggles
**Expected Result:**
- "Require Customer Name" toggle is ON (enabled)
- "Require Customer Email" toggle is ON (enabled)
- "Require Admin Email" toggle is ON (enabled)
#### TC-OEF-003: Toggle Customer Name Requirement
**Steps:**
1. Navigate to Settings > Event Creation
2. Toggle OFF "Require Customer Name"
3. Click Save
**Expected Result:**
- Setting saves successfully
- Toast notification confirms save
- Toggle remains OFF after page refresh
#### TC-OEF-004: Toggle Customer Email Requirement
**Steps:**
1. Navigate to Settings > Event Creation
2. Toggle OFF "Require Customer Email"
3. Click Save
**Expected Result:**
- Setting saves successfully
- Warning message about email functionality is shown
- Toggle remains OFF after page refresh
#### TC-OEF-005: Toggle Admin Email Requirement
**Steps:**
1. Navigate to Settings > Event Creation
2. Toggle OFF "Require Admin Email"
3. Click Save
**Expected Result:**
- Setting saves successfully
- Warning message about notifications is shown
- Toggle remains OFF after page refresh
### 2. Create Event Form - Conditional Validation
#### TC-OEF-006: All Fields Required (Default)
**Steps:**
1. Ensure all three settings are ON in Settings > Event Creation
2. Navigate to Create Event page
3. Try to submit form without filling customer name, customer email, or admin email
**Expected Result:**
- Validation errors shown for all three empty fields
- Form does not submit
#### TC-OEF-007: Customer Name Optional
**Steps:**
1. Set "Require Customer Name" to OFF in Settings
2. Navigate to Create Event page
3. Verify Host Name field label shows "(optional)"
4. Submit form without customer name (but with required fields filled)
**Expected Result:**
- Host Name label shows "(optional)" suffix
- Form submits successfully without customer name
- Event is created
#### TC-OEF-008: Customer Email Optional
**Steps:**
1. Set "Require Customer Email" to OFF in Settings
2. Navigate to Create Event page
3. Verify Host Email field label shows "(optional)"
4. Submit form without customer email (but with required fields filled)
**Expected Result:**
- Host Email label shows "(optional)" suffix
- Form submits successfully without customer email
- Event is created
#### TC-OEF-009: Admin Email Optional
**Steps:**
1. Set "Require Admin Email" to OFF in Settings
2. Navigate to Create Event page
3. Verify Admin Email field label shows "(optional)"
4. Submit form without admin email (but with required fields filled)
**Expected Result:**
- Admin Email label shows "(optional)" suffix
- Form submits successfully without admin email
- Event is created
#### TC-OEF-010: All Fields Optional
**Steps:**
1. Set all three settings to OFF in Settings
2. Navigate to Create Event page
3. Submit form with only event name, date, and password
**Expected Result:**
- All three optional fields show "(optional)" suffix
- Form submits successfully
- Event is created with null/empty contact fields
### 3. Backend Validation
#### TC-OEF-011: Backend Respects Settings
**Steps:**
1. Set "Require Customer Email" to OFF
2. Make direct API call to create event without customer_email:
```
POST /api/admin/events
{ event_name: "Test", event_date: "2025-01-15", ... }
```
**Expected Result:**
- API accepts the request
- Returns 201 Created
- Event is created without customer_email
#### TC-OEF-012: Backend Rejects When Required
**Steps:**
1. Set "Require Customer Email" to ON
2. Make direct API call to create event without customer_email
**Expected Result:**
- API rejects the request
- Returns 400 Bad Request with validation error
- Error message indicates customer_email is required
### 4. Format Validation for Optional Fields
#### TC-OEF-013: Invalid Email Format Still Rejected
**Steps:**
1. Set "Require Customer Email" to OFF
2. Navigate to Create Event page
3. Enter invalid email format (e.g., "notanemail")
4. Submit form
**Expected Result:**
- Validation error for invalid email format
- Form does not submit
- Error message: "Invalid email format"
### 5. Create Event Enhanced Page
#### TC-OEF-014: Enhanced Page Respects Settings
**Steps:**
1. Set all three settings to OFF
2. Navigate to enhanced Create Event page (`/admin/events/create`)
3. Verify all three fields show "(optional)"
4. Submit form without contact fields
**Expected Result:**
- All optional labels visible
- Form submits successfully
- Event is created
### 6. Settings Persistence
#### TC-OEF-015: Settings Persist Across Sessions
**Steps:**
1. Set specific combination (e.g., customer name OFF, emails ON)
2. Log out
3. Log back in
4. Navigate to Settings > Event Creation
**Expected Result:**
- Settings remain as configured
- Toggle states match what was saved
#### TC-OEF-016: Settings Persist Across Backend Restart
**Steps:**
1. Set specific combination of settings
2. Restart backend container
3. Create new event
**Expected Result:**
- Validation behavior matches saved settings
- Database persisted settings correctly
## Edge Cases
### TC-OEF-017: Empty String vs Null
**Steps:**
1. Set field to optional
2. Create event with empty string for that field
3. View event details
**Expected Result:**
- Field stores empty value appropriately
- No errors in display
### TC-OEF-018: Rapid Toggle Changes
**Steps:**
1. Quickly toggle settings on/off multiple times
2. Save after each change
**Expected Result:**
- Each save completes without error
- Final state matches last save action
## Automated Testing Notes
For Playwright tests:
1. Login to admin dashboard
2. Navigate to Settings
3. Click Event Creation tab
4. Manipulate toggles using checkbox selectors
5. Navigate to Create Event
6. Verify label text contains or doesn't contain "(optional)"
7. Attempt form submission with various field combinations
8. Assert on validation messages and success/failure states
## Files Modified
### Backend
- `/backend/migrations/core/050_add_optional_event_fields_settings.js`
- `/backend/src/routes/adminEvents.js`
- `/backend/src/routes/publicSettings.js`
### Frontend
- `/frontend/src/pages/admin/SettingsPage.tsx`
- `/frontend/src/pages/admin/CreateEventPage.tsx`
- `/frontend/src/pages/admin/CreateEventPageEnhanced.tsx`
-327
View File
@@ -1,327 +0,0 @@
# Test Specification: Photo Filtering & Export
This document specifies the test cases for the Photo Filtering & Export feature, which allows administrators to filter photos by guest feedback and export filtered selections in various formats.
## Prerequisites
- Local Docker environment running (`docker-compose up`)
- Access to admin dashboard
- Event with photos that have feedback (ratings, likes, favorites, comments)
- Backend migrations applied (051_add_photo_filter_indexes.js)
## Test Cases
### 1. Filter Panel UI
#### TC-PFE-001: Filter Panel Visibility
**Steps:**
1. Navigate to Event Details > Photos tab
2. Verify "Feedback Filters" panel is visible
**Expected Result:**
- Filter panel shows rating dropdown
- Checkboxes for: Has likes, Has favorites, Has comments
- Summary counts displayed
#### TC-PFE-002: Rating Filter Options
**Steps:**
1. Open the Rating dropdown in filter panel
2. Verify all rating options are present
**Expected Result:**
- Options: All photos, Any rating, 1+ stars, 2+ stars, 3+ stars, 4+ stars, 5 stars only
- Dropdown is functional
#### TC-PFE-003: Filter by Rating
**Steps:**
1. Select "4+ stars" from rating dropdown
2. Observe changes
**Expected Result:**
- Filter is applied
- Export menu becomes enabled (if photos match)
#### TC-PFE-004: Filter by Likes
**Steps:**
1. Check "Has likes" checkbox
2. Observe changes
**Expected Result:**
- Count shows number of photos with likes
- Filter is applied
#### TC-PFE-005: Filter by Favorites
**Steps:**
1. Check "Has favorites" checkbox
2. Observe changes
**Expected Result:**
- Count shows number of favorited photos
- Filter is applied
#### TC-PFE-006: Filter by Comments
**Steps:**
1. Check "Has comments" checkbox
2. Observe changes
**Expected Result:**
- Count shows number of commented photos
- Filter is applied
#### TC-PFE-007: AND/OR Logic Toggle
**Steps:**
1. Check multiple feedback filters (e.g., Has likes AND Has favorites)
2. Toggle between AND and OR
3. Observe export button state
**Expected Result:**
- AND: Photos must have both likes AND favorites
- OR: Photos can have likes OR favorites
- Toggle is visible when multiple filters selected
#### TC-PFE-008: Clear Filters
**Steps:**
1. Apply multiple filters
2. Click "Clear" button
**Expected Result:**
- All filters reset to default
- Rating dropdown shows "All photos"
- All checkboxes unchecked
### 2. Export Menu
#### TC-PFE-009: Export Menu Disabled State
**Steps:**
1. Clear all feedback filters
2. Ensure no photos are selected
3. Observe Export button
**Expected Result:**
- Export button is disabled
- Hint text: "Select photos or apply filters to export"
#### TC-PFE-010: Export Menu Enabled with Filters
**Steps:**
1. Apply a feedback filter (e.g., rating >= 3)
2. Observe Export button
**Expected Result:**
- Export button becomes enabled
- Dropdown shows export format options
#### TC-PFE-011: Export Format Options
**Steps:**
1. Enable Export button with filters
2. Click to open dropdown
**Expected Result:**
- Four format options visible:
- Filename List (TXT)
- Filename List (CSV)
- XMP Sidecar Files (ZIP)
- Metadata (JSON)
- Each has description text
### 3. Export Functionality
#### TC-PFE-012: Export TXT Format
**Steps:**
1. Apply filter (e.g., has favorites)
2. Click Export > Filename List (TXT)
3. Open downloaded file
**Expected Result:**
- File downloads with .txt extension
- Contains one filename per line
- Uses original filenames (e.g., IMG_0001.jpg)
#### TC-PFE-013: Export CSV Format
**Steps:**
1. Apply filter
2. Click Export > Filename List (CSV)
3. Open in spreadsheet
**Expected Result:**
- File downloads with .csv extension
- Headers: filename, original_filename, rating, rating_count, likes, favorites, comments, category, etc.
- Data rows for each filtered photo
#### TC-PFE-014: Export XMP Format
**Steps:**
1. Apply filter
2. Click Export > XMP Sidecar Files (ZIP)
3. Extract ZIP and inspect files
**Expected Result:**
- ZIP file downloads
- Contains .xmp files for each photo
- XMP files contain:
- xmp:Rating (1-5)
- xmp:Label (color)
- Description with feedback summary
- Keywords including "picpeak-export"
#### TC-PFE-015: Export JSON Format
**Steps:**
1. Apply filter
2. Click Export > Metadata (JSON)
3. Open/parse JSON file
**Expected Result:**
- JSON file downloads
- Contains export_info (event name, date, exported_at, total_photos)
- Contains photos array with full metadata
#### TC-PFE-016: XMP Rating Mapping
**Steps:**
1. Have photos with various ratings
2. Export XMP
3. Check xmp:Rating values
**Expected Result:**
- 4.5-5.0 stars → xmp:Rating="5", Label="Red"
- 3.5-4.4 stars → xmp:Rating="4", Label="Yellow"
- 2.5-3.4 stars → xmp:Rating="3", Label="Green"
- 1.5-2.4 stars → xmp:Rating="2", Label="Blue"
- 0.5-1.4 stars → xmp:Rating="1", Label="Purple"
### 4. Backend API Tests
#### TC-PFE-017: Filtered Photos Endpoint
**Steps:**
1. Call API: `GET /api/admin/photo-export/:eventId/filtered?min_rating=4&has_likes=true`
**Expected Result:**
- Returns 200 OK
- Response includes photos, pagination, and summary
- Only photos matching filter returned
#### TC-PFE-018: Filter Summary Endpoint
**Steps:**
1. Call API: `GET /api/admin/photo-export/:eventId/filter-summary`
**Expected Result:**
- Returns 200 OK
- Response includes: total, withRatings, withLikes, withFavorites, withComments
#### TC-PFE-019: Export Endpoint with Photo IDs
**Steps:**
1. Call API: `POST /api/admin/photo-export/:eventId/export`
Body: `{ "photo_ids": [1, 2, 3], "format": "csv" }`
**Expected Result:**
- Returns CSV file
- Contains only specified photos
#### TC-PFE-020: Export Endpoint with Filters
**Steps:**
1. Call API: `POST /api/admin/photo-export/:eventId/export`
Body: `{ "filter": { "minRating": 4 }, "format": "txt" }`
**Expected Result:**
- Returns TXT file
- Contains all photos matching filter
### 5. Edge Cases
#### TC-PFE-021: Empty Filter Results
**Steps:**
1. Apply filter that matches no photos (e.g., rating = 5 when no 5-star photos exist)
2. Try to export
**Expected Result:**
- Export button disabled or shows "0 photos"
- Error message if attempting export
#### TC-PFE-022: Large Export
**Steps:**
1. Filter to include 100+ photos
2. Export as XMP ZIP
**Expected Result:**
- Export completes (may take time)
- ZIP contains all matching .xmp files
- Loading indicator shown during export
#### TC-PFE-023: Special Characters in Filenames
**Steps:**
1. Have photo with special characters in original filename
2. Export CSV
**Expected Result:**
- Filename properly escaped in CSV
- No parsing errors
### 6. Integration Tests
#### TC-PFE-024: XMP Import to Lightroom
**Steps:**
1. Export XMP files
2. Place XMP files next to original photos
3. In Lightroom: Select photos > Metadata > Read Metadata from Files
**Expected Result:**
- Lightroom reads XMP files
- Ratings appear on photos
- Color labels applied
- Keywords visible in metadata panel
#### TC-PFE-025: TXT List in Lightroom Search
**Steps:**
1. Export TXT filename list
2. In Lightroom: Library > Filter > Text > Filename > Contains
3. Paste comma-separated list
**Expected Result:**
- Lightroom filters to matching files
- Can select and add to collection
### 7. Performance Tests
#### TC-PFE-026: Filter Performance
**Steps:**
1. Event with 500+ photos
2. Apply feedback filter
3. Measure response time
**Expected Result:**
- Filter applied within 2 seconds
- UI remains responsive
#### TC-PFE-027: Export Performance
**Steps:**
1. Export 200 photos as XMP ZIP
2. Measure download time
**Expected Result:**
- Export completes within reasonable time
- Progress indicator shown for large exports
## Automated Testing Notes
For Playwright tests:
1. Login to admin dashboard
2. Navigate to an event with photos
3. Go to Photos tab
4. Manipulate filter panel controls
5. Click export menu
6. Verify file downloads
7. For API tests, use direct fetch/axios calls
## Files Modified
### Backend
- `/backend/migrations/core/051_add_photo_filter_indexes.js`
- `/backend/src/utils/photoFilterBuilder.js`
- `/backend/src/services/xmpGenerator.js`
- `/backend/src/services/photoExportService.js`
- `/backend/src/routes/adminPhotoExport.js`
- `/backend/server.js`
### Frontend
- `/frontend/src/services/photos.service.ts`
- `/frontend/src/components/admin/PhotoFilterPanel.tsx`
- `/frontend/src/components/admin/PhotoExportMenu.tsx`
- `/frontend/src/components/admin/index.ts`
- `/frontend/src/pages/admin/EventDetailsPage.tsx`