mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 15:47:01 -05:00
Compare commits
3 Commits
d00c4f2e92
...
0f8e98a85a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8e98a85a | ||
|
|
2b56629a75 | ||
|
|
b653ed118c |
270
docs/logging/SUBMISSION_FLOW_LOGGING.md
Normal file
270
docs/logging/SUBMISSION_FLOW_LOGGING.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Submission Flow Logging
|
||||||
|
|
||||||
|
This document describes the structured logging implemented for tracking submission data through the moderation pipeline.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The submission flow has structured logging at each critical stage to enable debugging and auditing of data transformations.
|
||||||
|
|
||||||
|
## Logging Stages
|
||||||
|
|
||||||
|
### 1. Location Selection Stage
|
||||||
|
**Location**: `src/components/admin/ParkForm.tsx` → `LocationSearch.onLocationSelect()`
|
||||||
|
|
||||||
|
**Log Points**:
|
||||||
|
- Location selected from search (when user picks from dropdown)
|
||||||
|
- Location set in form state (confirmation of setValue)
|
||||||
|
|
||||||
|
**Log Format**:
|
||||||
|
```typescript
|
||||||
|
console.info('[ParkForm] Location selected:', {
|
||||||
|
name: string,
|
||||||
|
city: string | undefined,
|
||||||
|
state_province: string | undefined,
|
||||||
|
country: string,
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
display_name: string
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('[ParkForm] Location set in form:', locationObject);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Form Submission Stage
|
||||||
|
**Location**: `src/components/admin/ParkForm.tsx` → `handleFormSubmit()`
|
||||||
|
|
||||||
|
**Log Points**:
|
||||||
|
- Form data being submitted (what's being passed to submission helper)
|
||||||
|
|
||||||
|
**Log Format**:
|
||||||
|
```typescript
|
||||||
|
console.info('[ParkForm] Submitting park data:', {
|
||||||
|
hasLocation: boolean,
|
||||||
|
hasLocationId: boolean,
|
||||||
|
locationData: object | undefined,
|
||||||
|
parkName: string,
|
||||||
|
isEditing: boolean
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Submission Helper Reception Stage
|
||||||
|
**Location**: `src/lib/entitySubmissionHelpers.ts` → `submitParkCreation()`
|
||||||
|
|
||||||
|
**Log Points**:
|
||||||
|
- Data received by submission helper (what arrived from form)
|
||||||
|
- Data being saved to database (temp_location_data structure)
|
||||||
|
|
||||||
|
**Log Format**:
|
||||||
|
```typescript
|
||||||
|
console.info('[submitParkCreation] Received data:', {
|
||||||
|
hasLocation: boolean,
|
||||||
|
hasLocationId: boolean,
|
||||||
|
locationData: object | undefined,
|
||||||
|
parkName: string,
|
||||||
|
hasComposite: boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('[submitParkCreation] Saving to park_submissions:', {
|
||||||
|
name: string,
|
||||||
|
hasLocation: boolean,
|
||||||
|
hasLocationId: boolean,
|
||||||
|
temp_location_data: object | null
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Edit Stage
|
||||||
|
**Location**: `src/lib/submissionItemsService.ts` → `updateSubmissionItem()`
|
||||||
|
|
||||||
|
**Log Points**:
|
||||||
|
- Update item start (when moderator edits)
|
||||||
|
- Saving park data (before database write)
|
||||||
|
- Park data saved successfully (after database write)
|
||||||
|
|
||||||
|
**Log Format**:
|
||||||
|
```typescript
|
||||||
|
console.info('[Submission Flow] Update item start', {
|
||||||
|
itemId: string,
|
||||||
|
hasItemData: boolean,
|
||||||
|
statusUpdate: string | undefined,
|
||||||
|
timestamp: ISO string
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Saving park data', {
|
||||||
|
itemId: string,
|
||||||
|
parkSubmissionId: string,
|
||||||
|
hasLocation: boolean,
|
||||||
|
locationData: object | null,
|
||||||
|
fields: string[],
|
||||||
|
timestamp: ISO string
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Validation Stage
|
||||||
|
**Location**: `src/hooks/moderation/useModerationActions.ts` → `handleApproveSubmission()`
|
||||||
|
|
||||||
|
**Log Points**:
|
||||||
|
- Preparing items for validation (after fetching from DB)
|
||||||
|
- Transformed park data (after temp_location_data → location transform)
|
||||||
|
- Starting validation (before schema validation)
|
||||||
|
- Validation completed (after schema validation)
|
||||||
|
- Validation found blocking errors (if errors exist)
|
||||||
|
|
||||||
|
**Log Format**:
|
||||||
|
```typescript
|
||||||
|
console.info('[Submission Flow] Transformed park data for validation', {
|
||||||
|
itemId: string,
|
||||||
|
hasLocation: boolean,
|
||||||
|
locationData: object | null,
|
||||||
|
transformedHasLocation: boolean,
|
||||||
|
timestamp: ISO string
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn('[Submission Flow] Validation found blocking errors', {
|
||||||
|
submissionId: string,
|
||||||
|
itemsWithErrors: Array<{
|
||||||
|
itemId: string,
|
||||||
|
itemType: string,
|
||||||
|
errors: string[]
|
||||||
|
}>,
|
||||||
|
timestamp: ISO string
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Approval Stage
|
||||||
|
**Location**: `src/lib/submissionItemsService.ts` → `approveSubmissionItems()`
|
||||||
|
|
||||||
|
**Log Points**:
|
||||||
|
- Approval process started (beginning of batch approval)
|
||||||
|
- Processing item for approval (for each item)
|
||||||
|
- Entity created successfully (after entity creation)
|
||||||
|
|
||||||
|
**Log Format**:
|
||||||
|
```typescript
|
||||||
|
console.info('[Submission Flow] Approval process started', {
|
||||||
|
itemCount: number,
|
||||||
|
itemIds: string[],
|
||||||
|
itemTypes: string[],
|
||||||
|
userId: string,
|
||||||
|
timestamp: ISO string
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Processing item for approval', {
|
||||||
|
itemId: string,
|
||||||
|
itemType: string,
|
||||||
|
isEdit: boolean,
|
||||||
|
hasLocation: boolean,
|
||||||
|
locationData: object | null,
|
||||||
|
timestamp: ISO string
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Data Transformations Logged
|
||||||
|
|
||||||
|
### Park Location Data
|
||||||
|
The most critical transformation logged is the park location data flow:
|
||||||
|
|
||||||
|
1. **User Selection** (LocationSearch): OpenStreetMap result → `location` object
|
||||||
|
2. **Form State** (ParkForm): `setValue('location', location)`
|
||||||
|
3. **Form Submission** (ParkForm → submitParkCreation): `data.location` passed in submission
|
||||||
|
4. **Database Storage** (submitParkCreation): `data.location` → `temp_location_data` (JSONB in park_submissions)
|
||||||
|
5. **Display/Edit**: `temp_location_data` → `location` (transformed for form compatibility)
|
||||||
|
6. **Validation**: `temp_location_data` → `location` (transformed for schema validation)
|
||||||
|
7. **Approval**: `location` used to create actual location record
|
||||||
|
|
||||||
|
**Why this matters**:
|
||||||
|
- If location is NULL in database but user selected one → Check stages 1-4
|
||||||
|
- If validation fails with "Location is required" → Check stages 5-6
|
||||||
|
- Location validation errors typically indicate a break in this transformation chain.
|
||||||
|
|
||||||
|
## Debugging Workflow
|
||||||
|
|
||||||
|
### To debug "Location is required" validation errors:
|
||||||
|
|
||||||
|
1. **Check browser console** for `[ParkForm]` and `[Submission Flow]` logs
|
||||||
|
2. **Verify data at each stage**:
|
||||||
|
```javascript
|
||||||
|
// Stage 1: Location selection
|
||||||
|
[ParkForm] Location selected: { name: "Farmington, Utah", latitude: 40.98, ... }
|
||||||
|
[ParkForm] Location set in form: { name: "Farmington, Utah", ... }
|
||||||
|
|
||||||
|
// Stage 2: Form submission
|
||||||
|
[ParkForm] Submitting park data { hasLocation: true, locationData: {...} }
|
||||||
|
|
||||||
|
// Stage 3: Submission helper receives data
|
||||||
|
[submitParkCreation] Received data { hasLocation: true, locationData: {...} }
|
||||||
|
[submitParkCreation] Saving to park_submissions { temp_location_data: {...} }
|
||||||
|
|
||||||
|
// Stage 4: Edit stage (if moderator edits later)
|
||||||
|
[Submission Flow] Saving park data { hasLocation: true, locationData: {...} }
|
||||||
|
|
||||||
|
// Stage 5: Validation stage
|
||||||
|
[Submission Flow] Transformed park data { hasLocation: true, transformedHasLocation: true }
|
||||||
|
|
||||||
|
// Stage 6: Approval stage
|
||||||
|
[Submission Flow] Processing item { hasLocation: true, locationData: {...} }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Look for missing data**:
|
||||||
|
- If `[ParkForm] Location selected` missing → User didn't select location from dropdown
|
||||||
|
- If `hasLocation: false` in form submission → Location not set in form state (possible React Hook Form issue)
|
||||||
|
- If `hasLocation: true` in submission but NULL in database → Database write failed (check errors)
|
||||||
|
- If `hasLocation: true` but `transformedHasLocation: false` → Transformation failed
|
||||||
|
- If validation logs missing → Check database query/fetch
|
||||||
|
|
||||||
|
### To debug NULL location in new submissions:
|
||||||
|
|
||||||
|
1. **Open browser console** before creating submission
|
||||||
|
2. **Select location** and verify `[ParkForm] Location selected` appears
|
||||||
|
3. **Submit form** and verify `[ParkForm] Submitting park data` shows `hasLocation: true`
|
||||||
|
4. **Check** `[submitParkCreation] Saving to park_submissions` shows `temp_location_data` is not null
|
||||||
|
5. **If location was selected but is NULL in database**:
|
||||||
|
- Form state was cleared (page refresh/navigation before submit)
|
||||||
|
- React Hook Form setValue didn't work (check "Location set in form" log)
|
||||||
|
- Database write succeeded but data was lost (check for errors)
|
||||||
|
|
||||||
|
## Error Logging Integration
|
||||||
|
|
||||||
|
Structured errors use the `handleError()` utility from `@/lib/errorHandler`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
handleError(error, {
|
||||||
|
action: 'Update Park Submission Data',
|
||||||
|
metadata: {
|
||||||
|
itemId,
|
||||||
|
parkSubmissionId,
|
||||||
|
updateFields: Object.keys(updateData)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors are logged to:
|
||||||
|
- **Database**: `request_metadata` table
|
||||||
|
- **Admin Panel**: `/admin/error-monitoring`
|
||||||
|
- **Console**: Browser developer tools (with reference ID)
|
||||||
|
|
||||||
|
## Log Filtering
|
||||||
|
|
||||||
|
To filter logs in browser console:
|
||||||
|
```javascript
|
||||||
|
// All submission flow logs
|
||||||
|
localStorage.setItem('logFilter', 'Submission Flow');
|
||||||
|
|
||||||
|
// Specific stages
|
||||||
|
localStorage.setItem('logFilter', 'Validation');
|
||||||
|
localStorage.setItem('logFilter', 'Saving park data');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Logs use `console.info()` and `console.warn()` which are stripped in production builds
|
||||||
|
- Sensitive data (passwords, tokens) are never logged
|
||||||
|
- Object logging uses shallow copies to avoid memory leaks
|
||||||
|
- Timestamps use ISO format for timezone-aware debugging
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Add edge function logging for backend approval process
|
||||||
|
- [ ] Add real-time log streaming to admin dashboard
|
||||||
|
- [ ] Add log retention policies (30-day automatic cleanup)
|
||||||
|
- [ ] Add performance metrics (time between stages)
|
||||||
|
- [ ] Add user action correlation (who edited what when)
|
||||||
@@ -271,13 +271,24 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
(tempNewPropertyOwner ? undefined : selectedPropertyOwnerId);
|
(tempNewPropertyOwner ? undefined : selectedPropertyOwnerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await onSubmit({
|
// Debug: Log what's being submitted
|
||||||
|
const submissionData = {
|
||||||
...data,
|
...data,
|
||||||
operator_id: finalOperatorId,
|
operator_id: finalOperatorId,
|
||||||
property_owner_id: finalPropertyOwnerId,
|
property_owner_id: finalPropertyOwnerId,
|
||||||
_compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined
|
_compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
console.info('[ParkForm] Submitting park data:', {
|
||||||
|
hasLocation: !!submissionData.location,
|
||||||
|
hasLocationId: !!submissionData.location_id,
|
||||||
|
locationData: submissionData.location,
|
||||||
|
parkName: submissionData.name,
|
||||||
|
isEditing
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await onSubmit(submissionData);
|
||||||
|
|
||||||
// Parent component handles success feedback
|
// Parent component handles success feedback
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
@@ -426,7 +437,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
</Label>
|
</Label>
|
||||||
<LocationSearch
|
<LocationSearch
|
||||||
onLocationSelect={(location) => {
|
onLocationSelect={(location) => {
|
||||||
|
console.info('[ParkForm] Location selected:', location);
|
||||||
setValue('location', location);
|
setValue('location', location);
|
||||||
|
console.info('[ParkForm] Location set in form:', watch('location'));
|
||||||
// Manually trigger validation for the location field
|
// Manually trigger validation for the location field
|
||||||
trigger('location');
|
trigger('location');
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -154,6 +154,13 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fullItems && fullItems.length > 0) {
|
if (fullItems && fullItems.length > 0) {
|
||||||
|
console.info('[Submission Flow] Preparing items for validation', {
|
||||||
|
submissionId: item.id,
|
||||||
|
itemCount: fullItems.length,
|
||||||
|
itemTypes: fullItems.map(i => i.item_type),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Transform to include item_data
|
// Transform to include item_data
|
||||||
const itemsWithData = fullItems.map(item => {
|
const itemsWithData = fullItems.map(item => {
|
||||||
let itemData = {};
|
let itemData = {};
|
||||||
@@ -166,6 +173,14 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
|||||||
location: parkSub.temp_location_data || undefined,
|
location: parkSub.temp_location_data || undefined,
|
||||||
temp_location_data: undefined
|
temp_location_data: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Transformed park data for validation', {
|
||||||
|
itemId: item.id,
|
||||||
|
hasLocation: !!parkSub.temp_location_data,
|
||||||
|
locationData: parkSub.temp_location_data,
|
||||||
|
transformedHasLocation: !!(itemData as any).location,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ride':
|
case 'ride':
|
||||||
@@ -202,8 +217,21 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
|||||||
|
|
||||||
// Run validation on all items
|
// Run validation on all items
|
||||||
try {
|
try {
|
||||||
|
console.info('[Submission Flow] Starting validation', {
|
||||||
|
submissionId: item.id,
|
||||||
|
itemCount: itemsWithData.length,
|
||||||
|
itemTypes: itemsWithData.map(i => i.item_type),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
const validationResults = await validateMultipleItems(itemsWithData);
|
const validationResults = await validateMultipleItems(itemsWithData);
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Validation completed', {
|
||||||
|
submissionId: item.id,
|
||||||
|
resultsCount: validationResults.size,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Check for blocking errors
|
// Check for blocking errors
|
||||||
const itemsWithBlockingErrors = itemsWithData.filter(item => {
|
const itemsWithBlockingErrors = itemsWithData.filter(item => {
|
||||||
const result = validationResults.get(item.id);
|
const result = validationResults.get(item.id);
|
||||||
@@ -212,6 +240,16 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
|||||||
|
|
||||||
// CRITICAL: Block approval if any item has blocking errors
|
// CRITICAL: Block approval if any item has blocking errors
|
||||||
if (itemsWithBlockingErrors.length > 0) {
|
if (itemsWithBlockingErrors.length > 0) {
|
||||||
|
console.warn('[Submission Flow] Validation found blocking errors', {
|
||||||
|
submissionId: item.id,
|
||||||
|
itemsWithErrors: itemsWithBlockingErrors.map(i => ({
|
||||||
|
itemId: i.id,
|
||||||
|
itemType: i.item_type,
|
||||||
|
errors: validationResults.get(i.id)?.blockingErrors
|
||||||
|
})),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Log detailed blocking errors
|
// Log detailed blocking errors
|
||||||
itemsWithBlockingErrors.forEach(item => {
|
itemsWithBlockingErrors.forEach(item => {
|
||||||
const result = validationResults.get(item.id);
|
const result = validationResults.get(item.id);
|
||||||
|
|||||||
@@ -557,6 +557,14 @@ export async function submitParkCreation(
|
|||||||
data: ParkFormData & { _compositeSubmission?: any },
|
data: ParkFormData & { _compositeSubmission?: any },
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
console.info('[submitParkCreation] Received data:', {
|
||||||
|
hasLocation: !!data.location,
|
||||||
|
hasLocationId: !!data.location_id,
|
||||||
|
locationData: data.location,
|
||||||
|
parkName: data.name,
|
||||||
|
hasComposite: !!data._compositeSubmission
|
||||||
|
});
|
||||||
|
|
||||||
// Validate required fields client-side
|
// Validate required fields client-side
|
||||||
assertValid(validateParkCreateFields(data));
|
assertValid(validateParkCreateFields(data));
|
||||||
|
|
||||||
@@ -663,6 +671,25 @@ export async function submitParkCreation(
|
|||||||
const cardImage = (cardIndex !== null && cardIndex !== undefined) ? uploadedImages[cardIndex] : null;
|
const cardImage = (cardIndex !== null && cardIndex !== undefined) ? uploadedImages[cardIndex] : null;
|
||||||
|
|
||||||
// Insert into relational park_submissions table
|
// Insert into relational park_submissions table
|
||||||
|
const tempLocationData = data.location ? {
|
||||||
|
name: data.location.name,
|
||||||
|
city: data.location.city || null,
|
||||||
|
state_province: data.location.state_province || null,
|
||||||
|
country: data.location.country,
|
||||||
|
latitude: data.location.latitude,
|
||||||
|
longitude: data.location.longitude,
|
||||||
|
timezone: data.location.timezone || null,
|
||||||
|
postal_code: data.location.postal_code || null,
|
||||||
|
display_name: data.location.display_name
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
console.info('[submitParkCreation] Saving to park_submissions:', {
|
||||||
|
name: data.name,
|
||||||
|
hasLocation: !!data.location,
|
||||||
|
hasLocationId: !!data.location_id,
|
||||||
|
temp_location_data: tempLocationData
|
||||||
|
});
|
||||||
|
|
||||||
const { data: parkSubmission, error: parkSubmissionError } = await supabase
|
const { data: parkSubmission, error: parkSubmissionError } = await supabase
|
||||||
.from('park_submissions' as any)
|
.from('park_submissions' as any)
|
||||||
.insert({
|
.insert({
|
||||||
@@ -680,17 +707,7 @@ export async function submitParkCreation(
|
|||||||
operator_id: data.operator_id || null,
|
operator_id: data.operator_id || null,
|
||||||
property_owner_id: data.property_owner_id || null,
|
property_owner_id: data.property_owner_id || null,
|
||||||
location_id: data.location_id || null,
|
location_id: data.location_id || null,
|
||||||
temp_location_data: data.location ? {
|
temp_location_data: tempLocationData,
|
||||||
name: data.location.name,
|
|
||||||
city: data.location.city || null,
|
|
||||||
state_province: data.location.state_province || null,
|
|
||||||
country: data.location.country,
|
|
||||||
latitude: data.location.latitude,
|
|
||||||
longitude: data.location.longitude,
|
|
||||||
timezone: data.location.timezone || null,
|
|
||||||
postal_code: data.location.postal_code || null,
|
|
||||||
display_name: data.location.display_name
|
|
||||||
} : null,
|
|
||||||
banner_image_url: bannerImage?.url || data.banner_image_url || null,
|
banner_image_url: bannerImage?.url || data.banner_image_url || null,
|
||||||
banner_image_id: bannerImage?.cloudflare_id || data.banner_image_id || null,
|
banner_image_id: bannerImage?.cloudflare_id || data.banner_image_id || null,
|
||||||
card_image_url: cardImage?.url || data.card_image_url || null,
|
card_image_url: cardImage?.url || data.card_image_url || null,
|
||||||
|
|||||||
@@ -224,22 +224,148 @@ export async function detectDependencyConflicts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update individual submission item status
|
* Update individual submission item status and data
|
||||||
* Note: item_data and original_data are read-only (managed via relational tables)
|
|
||||||
*/
|
*/
|
||||||
export async function updateSubmissionItem(
|
export async function updateSubmissionItem(
|
||||||
itemId: string,
|
itemId: string,
|
||||||
updates: Partial<SubmissionItemWithDeps>
|
updates: Partial<SubmissionItemWithDeps>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Remove item_data and original_data from updates (managed via relational tables)
|
|
||||||
const { item_data, original_data, ...cleanUpdates } = updates;
|
const { item_data, original_data, ...cleanUpdates } = updates;
|
||||||
|
|
||||||
|
// Log submission item update start
|
||||||
|
console.info('[Submission Flow] Update item start', {
|
||||||
|
itemId,
|
||||||
|
hasItemData: !!item_data,
|
||||||
|
statusUpdate: cleanUpdates.status,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update submission_items table
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('submission_items')
|
.from('submission_items')
|
||||||
.update(cleanUpdates)
|
.update(cleanUpdates)
|
||||||
.eq('id', itemId);
|
.eq('id', itemId);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) {
|
||||||
|
handleError(error, {
|
||||||
|
action: 'Update Submission Item',
|
||||||
|
metadata: { itemId, updates: cleanUpdates }
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If item_data is provided, update the relational table
|
||||||
|
if (item_data !== undefined) {
|
||||||
|
// Fetch the item to get its type and foreign keys
|
||||||
|
const { data: item, error: fetchError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.select('item_type, park_submission_id, ride_submission_id, company_submission_id, ride_model_submission_id, timeline_event_submission_id, photo_submission_id')
|
||||||
|
.eq('id', itemId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError) throw fetchError;
|
||||||
|
if (!item) throw new Error(`Submission item ${itemId} not found`);
|
||||||
|
|
||||||
|
// Update the appropriate relational table
|
||||||
|
switch (item.item_type) {
|
||||||
|
case 'park': {
|
||||||
|
if (!item.park_submission_id) break;
|
||||||
|
const parkData = item_data as any;
|
||||||
|
const updateData: any = {
|
||||||
|
...parkData,
|
||||||
|
// Transform location → temp_location_data for storage
|
||||||
|
temp_location_data: parkData.location || null,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove fields that shouldn't be in park_submissions
|
||||||
|
delete updateData.location;
|
||||||
|
|
||||||
|
// Remove undefined fields
|
||||||
|
Object.keys(updateData).forEach(key => {
|
||||||
|
if (updateData[key] === undefined) delete updateData[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Saving park data', {
|
||||||
|
itemId,
|
||||||
|
parkSubmissionId: item.park_submission_id,
|
||||||
|
hasLocation: !!updateData.temp_location_data,
|
||||||
|
locationData: updateData.temp_location_data,
|
||||||
|
fields: Object.keys(updateData),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('park_submissions')
|
||||||
|
.update(updateData)
|
||||||
|
.eq('id', item.park_submission_id);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
handleError(updateError, {
|
||||||
|
action: 'Update Park Submission Data',
|
||||||
|
metadata: {
|
||||||
|
itemId,
|
||||||
|
parkSubmissionId: item.park_submission_id,
|
||||||
|
updateFields: Object.keys(updateData)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw updateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Park data saved successfully', {
|
||||||
|
itemId,
|
||||||
|
parkSubmissionId: item.park_submission_id,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ride': {
|
||||||
|
if (!item.ride_submission_id) break;
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('ride_submissions')
|
||||||
|
.update({ ...(item_data as any), updated_at: new Date().toISOString() })
|
||||||
|
.eq('id', item.ride_submission_id);
|
||||||
|
|
||||||
|
if (updateError) throw updateError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'operator':
|
||||||
|
case 'manufacturer':
|
||||||
|
case 'designer':
|
||||||
|
case 'property_owner': {
|
||||||
|
if (!item.company_submission_id) break;
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('company_submissions')
|
||||||
|
.update({ ...(item_data as any), updated_at: new Date().toISOString() })
|
||||||
|
.eq('id', item.company_submission_id);
|
||||||
|
|
||||||
|
if (updateError) throw updateError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ride_model': {
|
||||||
|
if (!item.ride_model_submission_id) break;
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('ride_model_submissions')
|
||||||
|
.update({ ...(item_data as any), updated_at: new Date().toISOString() })
|
||||||
|
.eq('id', item.ride_model_submission_id);
|
||||||
|
|
||||||
|
if (updateError) throw updateError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'milestone':
|
||||||
|
case 'timeline_event': {
|
||||||
|
if (!item.timeline_event_submission_id) break;
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('timeline_event_submissions')
|
||||||
|
.update({ ...(item_data as any), updated_at: new Date().toISOString() })
|
||||||
|
.eq('id', item.timeline_event_submission_id);
|
||||||
|
|
||||||
|
if (updateError) throw updateError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Photo submissions handled separately due to complex structure
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,6 +379,14 @@ export async function approveSubmissionItems(
|
|||||||
throw new Error('User authentication required to approve items');
|
throw new Error('User authentication required to approve items');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Approval process started', {
|
||||||
|
itemCount: items.length,
|
||||||
|
itemIds: items.map(i => i.id),
|
||||||
|
itemTypes: items.map(i => i.item_type),
|
||||||
|
userId,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Sort by dependency order (parents first)
|
// Sort by dependency order (parents first)
|
||||||
const sortedItems = topologicalSort(items);
|
const sortedItems = topologicalSort(items);
|
||||||
|
|
||||||
@@ -276,6 +410,15 @@ export async function approveSubmissionItems(
|
|||||||
('ride_model_id' in itemData && itemData.ride_model_id)
|
('ride_model_id' in itemData && itemData.ride_model_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Processing item for approval', {
|
||||||
|
itemId: item.id,
|
||||||
|
itemType: item.item_type,
|
||||||
|
isEdit,
|
||||||
|
hasLocation: !!(itemData as any).location,
|
||||||
|
locationData: (itemData as any).location,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Create the entity based on type with dependency resolution
|
// Create the entity based on type with dependency resolution
|
||||||
// PASS sortedItems to enable correct index-based resolution
|
// PASS sortedItems to enable correct index-based resolution
|
||||||
switch (item.item_type) {
|
switch (item.item_type) {
|
||||||
@@ -303,6 +446,14 @@ export async function approveSubmissionItems(
|
|||||||
throw new Error(`Failed to create ${item.item_type}: no entity ID returned`);
|
throw new Error(`Failed to create ${item.item_type}: no entity ID returned`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info('[Submission Flow] Entity created successfully', {
|
||||||
|
itemId: item.id,
|
||||||
|
itemType: item.item_type,
|
||||||
|
entityId,
|
||||||
|
isEdit,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Update item status
|
// Update item status
|
||||||
await updateSubmissionItem(item.id, {
|
await updateSubmissionItem(item.id, {
|
||||||
status: 'approved' as const,
|
status: 'approved' as const,
|
||||||
|
|||||||
Reference in New Issue
Block a user