From cf71bcfd9641ab0804fdb3d624499c4dbec46987 Mon Sep 17 00:00:00 2001
From: DJP
Date: Fri, 17 Oct 2025 16:43:26 -0400
Subject: [PATCH] =?UTF-8?q?Complete=20workflow=20V3:=20Add=20Upload=20(A2?=
=?UTF-8?q?=E2=86=92A3)=20and=20Rework=20(A5=E2=86=92A6)=20workflows?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
NEW: Upload Workflow (A2→A3)
- Load campaigns with status A2 (assets sent to agency)
- Select campaign and find upload target folder (Final Assets)
- Multi-file upload with drag-drop interface
- AssetUploader class with multipart/form-data support
- Upload files with metadata to DAM
- Update status to A3 after upload
NEW: Rework Workflow (A5→A6)
- Load campaigns with status A5 (rework needed from agency)
- Select campaign and get rework assets
- Download individual or bulk rework assets
- Beautiful metadata display
- Update status to A6 when rework assets received
Features:
- Three complete workflow tabs (Download, Upload, Rework)
- Status transitions: A1→A2, A2→A3, A5→A6
- Session-based workflow state management
- Clear workflow data button for each tab
- Campaign selection with visual feedback
- Download confirmed working (tested with real asset)
- OAuth2 auto-refresh throughout all workflows
Technical:
- AssetUploader class for multipart file uploads
- Proper metadata JSON structure for uploads
- Support for multiple file formats (JPG, PNG, PDF, AI, PSD, etc.)
- Error handling for each workflow step
- Beautiful UI with color-coded status badges
Ready for testing with real campaign data!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
src/AssetUploader.php | 172 +++++++++++++
workflow_v3.php | 546 +++++++++++++++++++++++++++++++++++++++++-
2 files changed, 709 insertions(+), 9 deletions(-)
create mode 100644 src/AssetUploader.php
diff --git a/src/AssetUploader.php b/src/AssetUploader.php
new file mode 100644
index 0000000..6a1539e
--- /dev/null
+++ b/src/AssetUploader.php
@@ -0,0 +1,172 @@
+apiClient = $apiClient;
+ $this->targetFolderId = $targetFolderId;
+ }
+
+ /**
+ * Upload a file to DAM with metadata
+ *
+ * @param string $filePath Local file path
+ * @param string $folderId Target folder ID (campaign folder)
+ * @param array $metadata Optional metadata to set
+ * @return array Upload result
+ */
+ public function uploadFile($filePath, $folderId, $metadata = [])
+ {
+ if (!file_exists($filePath)) {
+ return [
+ 'success' => false,
+ 'error' => 'File not found: ' . $filePath
+ ];
+ }
+
+ $filename = basename($filePath);
+ $mimeType = mime_content_type($filePath);
+
+ // Build multipart form data
+ $boundary = '----WebKitFormBoundary' . uniqid();
+
+ // Build asset representation JSON
+ $assetRepresentation = $this->buildAssetRepresentation($filename, $metadata);
+
+ // Build multipart body
+ $body = '';
+
+ // Add asset_representation part
+ $body .= "--{$boundary}\r\n";
+ $body .= "Content-Disposition: form-data; name=\"asset_representation\"\r\n";
+ $body .= "Content-Type: application/json\r\n\r\n";
+ $body .= json_encode($assetRepresentation) . "\r\n";
+
+ // Add parent_folder_id part
+ $body .= "--{$boundary}\r\n";
+ $body .= "Content-Disposition: form-data; name=\"parent_folder_id\"\r\n\r\n";
+ $body .= $folderId . "\r\n";
+
+ // Add file part
+ $body .= "--{$boundary}\r\n";
+ $body .= "Content-Disposition: form-data; name=\"file\"; filename=\"{$filename}\"\r\n";
+ $body .= "Content-Type: {$mimeType}\r\n\r\n";
+ $body .= file_get_contents($filePath) . "\r\n";
+
+ $body .= "--{$boundary}--\r\n";
+
+ // Make upload request
+ $request = [
+ 'method' => 'POST',
+ 'url' => '/v6/assets',
+ 'body' => $body
+ ];
+
+ // Set multipart content-type header
+ $this->apiClient->setHeader('Content-Type', "multipart/form-data; boundary={$boundary}");
+
+ $response = $this->apiClient->executeRequest($request);
+
+ if ($response['success'] && $response['http_code'] == 201) {
+ $responseData = json_decode($response['body'], true);
+
+ return [
+ 'success' => true,
+ 'asset_id' => $responseData['asset_resource_list']['asset_resource'][0]['asset']['asset_id'] ?? null,
+ 'filename' => $filename,
+ 'response' => $responseData
+ ];
+ }
+
+ return [
+ 'success' => false,
+ 'error' => 'Upload failed',
+ 'http_code' => $response['http_code'],
+ 'response' => $response['body']
+ ];
+ }
+
+ /**
+ * Build asset representation for upload
+ */
+ private function buildAssetRepresentation($filename, $metadata = [])
+ {
+ $assetRep = [
+ 'asset_resource' => [
+ 'asset' => [
+ 'metadata' => [
+ 'metadata_element_list' => []
+ ]
+ ]
+ ]
+ ];
+
+ // Add filename metadata
+ $assetRep['asset_resource']['asset']['metadata']['metadata_element_list'][] = [
+ 'id' => 'ARTESIA.FIELD.ASSET NAME',
+ 'type' => 'com.artesia.metadata.MetadataField',
+ 'value' => [
+ 'cascading_domain_value' => false,
+ 'domain_value' => false,
+ 'value' => [
+ 'type' => 'string',
+ 'value' => $filename
+ ]
+ ]
+ ];
+
+ // Add custom metadata fields
+ foreach ($metadata as $fieldId => $fieldValue) {
+ $assetRep['asset_resource']['asset']['metadata']['metadata_element_list'][] = [
+ 'id' => $fieldId,
+ 'type' => 'com.artesia.metadata.MetadataField',
+ 'value' => [
+ 'cascading_domain_value' => false,
+ 'domain_value' => false,
+ 'value' => [
+ 'type' => 'string',
+ 'value' => $fieldValue
+ ]
+ ]
+ ];
+ }
+
+ return $assetRep;
+ }
+
+ /**
+ * Upload multiple files
+ */
+ public function uploadMultipleFiles($files, $folderId, $metadata = [])
+ {
+ $results = [
+ 'total' => count($files),
+ 'successful' => 0,
+ 'failed' => 0,
+ 'details' => []
+ ];
+
+ foreach ($files as $file) {
+ $result = $this->uploadFile($file, $folderId, $metadata);
+
+ if ($result['success']) {
+ $results['successful']++;
+ } else {
+ $results['failed']++;
+ }
+
+ $results['details'][] = $result;
+ }
+
+ return $results;
+ }
+}
diff --git a/workflow_v3.php b/workflow_v3.php
index f488d63..0e32cb6 100644
--- a/workflow_v3.php
+++ b/workflow_v3.php
@@ -24,6 +24,7 @@ require_once 'src/AssetDownloader.php';
require_once 'src/MetadataExtractor.php';
require_once 'src/StatusManager.php';
require_once 'src/ApiClient.php';
+require_once 'src/AssetUploader.php';
$configV3 = new ConfigV3();
$errors = $configV3->validate();
@@ -163,11 +164,158 @@ if ($_POST && $testRunner) {
$currentTab = 'upload';
break;
+ case 'select_campaign_a2':
+ $campaignId = $_POST['campaign_id'] ?? '';
+ if ($campaignId && isset($results['a2_campaigns'])) {
+ foreach ($results['a2_campaigns'] as $campaign) {
+ if ($campaign['asset_id'] === $campaignId) {
+ $results['selected_campaign_a2'] = $campaign;
+ $success = "Campaign selected: " . $campaign['campaign_name'];
+ $currentTab = 'upload';
+ break;
+ }
+ }
+ }
+ break;
+
+ case 'get_upload_folder':
+ if (isset($results['selected_campaign_a2'])) {
+ $campaignId = $results['selected_campaign_a2']['asset_id'];
+ // Find Final Assets or Localized Assets folder
+ $uploadFolderId = findUploadFolder($testRunner, $campaignId, $configV3);
+
+ if ($uploadFolderId) {
+ $results['upload_folder_id'] = $uploadFolderId;
+ $success = "Found upload target folder";
+ } else {
+ $error = "Upload folder not found in campaign";
+ }
+ }
+ $currentTab = 'upload';
+ break;
+
+ case 'upload_files':
+ if (isset($_FILES['upload_files']) && isset($results['upload_folder_id'])) {
+ $uploadResults = uploadFiles($testRunner, $_FILES['upload_files'], $results['upload_folder_id']);
+ $results['upload_results'] = $uploadResults;
+
+ if ($uploadResults['successful'] > 0) {
+ $success = "Uploaded {$uploadResults['successful']} of {$uploadResults['total']} files";
+ } else {
+ $error = "All uploads failed";
+ }
+ }
+ $currentTab = 'upload';
+ break;
+
+ case 'update_status_to_a3':
+ if (isset($results['selected_campaign_a2'])) {
+ $statusManager = createStatusManager($testRunner);
+ $statusResult = $statusManager->updateCampaignStatus(
+ $results['selected_campaign_a2']['asset_id'],
+ 'A3'
+ );
+
+ if ($statusResult['success']) {
+ $results['status_update_a3'] = $statusResult;
+ $success = "Campaign status updated to A3: Localized Asset received from Agency";
+ } else {
+ $error = "Failed to update status";
+ }
+ }
+ $currentTab = 'upload';
+ break;
+
case 'load_campaigns_a5':
$results['a5_campaigns'] = loadCampaignsByStatus($testRunner, 'A5');
$success = "Loaded " . count($results['a5_campaigns']) . " campaigns with status A5";
$currentTab = 'rework';
break;
+
+ case 'select_campaign_a5':
+ $campaignId = $_POST['campaign_id'] ?? '';
+ if ($campaignId && isset($results['a5_campaigns'])) {
+ foreach ($results['a5_campaigns'] as $campaign) {
+ if ($campaign['asset_id'] === $campaignId) {
+ $results['selected_campaign_a5'] = $campaign;
+ $success = "Campaign selected: " . $campaign['campaign_name'];
+ $currentTab = 'rework';
+ break;
+ }
+ }
+ }
+ break;
+
+ case 'get_rework_assets':
+ if (isset($results['selected_campaign_a5'])) {
+ $campaignId = $results['selected_campaign_a5']['asset_id'];
+ // Find folder with rework assets (could be Final Assets or Rework folder)
+ $reworkFolderId = findUploadFolder($testRunner, $campaignId, $configV3);
+
+ if ($reworkFolderId) {
+ $results['rework_folder_id'] = $reworkFolderId;
+ $results['rework_assets'] = getAssetsFromFolder($testRunner, $reworkFolderId);
+ $success = "Found " . count($results['rework_assets']) . " assets needing rework";
+ } else {
+ $error = "Rework assets folder not found";
+ }
+ }
+ $currentTab = 'rework';
+ break;
+
+ case 'download_rework_asset':
+ $assetId = $_POST['asset_id'] ?? '';
+ $filename = $_POST['filename'] ?? '';
+
+ if ($assetId && isset($results['selected_campaign_a5'])) {
+ $assetData = null;
+ if (isset($results['rework_assets'])) {
+ foreach ($results['rework_assets'] as $asset) {
+ if ($asset['asset_id'] === $assetId) {
+ $assetData = $asset;
+ break;
+ }
+ }
+ }
+
+ $result = downloadAsset($testRunner, $assetId, $filename, $results['selected_campaign_a5']['campaign_id'], $assetData);
+ $results['last_rework_download'] = $result;
+
+ if ($result['success']) {
+ $success = "Downloaded rework asset: " . $result['filename'];
+ } else {
+ $error = "Download failed: " . $result['error'];
+ }
+ }
+ $currentTab = 'rework';
+ break;
+
+ case 'download_all_rework':
+ if (isset($results['rework_assets']) && isset($results['selected_campaign_a5'])) {
+ $downloadResults = downloadAllAssets($testRunner, $results['rework_assets'], $results['selected_campaign_a5']['campaign_id']);
+ $results['rework_bulk_download'] = $downloadResults;
+ $success = "Downloaded {$downloadResults['successful']} of {$downloadResults['total']} rework assets";
+ }
+ $currentTab = 'rework';
+ break;
+
+ case 'update_status_to_a6':
+ if (isset($results['selected_campaign_a5'])) {
+ $statusManager = createStatusManager($testRunner);
+ $statusResult = $statusManager->updateCampaignStatus(
+ $results['selected_campaign_a5']['asset_id'],
+ 'A6'
+ );
+
+ if ($statusResult['success']) {
+ $results['status_update_a6'] = $statusResult;
+ $success = "Campaign status updated to A6: Assets to be reworked received by the Agency";
+ } else {
+ $error = "Failed to update status";
+ }
+ }
+ $currentTab = 'rework';
+ break;
}
} catch (Exception $e) {
$error = "Action failed: " . $e->getMessage();
@@ -509,6 +657,108 @@ function createStatusManager($testRunner)
return new StatusManager($apiClient);
}
+function findUploadFolder($testRunner, $campaignId, $configV3)
+{
+ $finalFolderName = $configV3->getFolderName('final_assets');
+
+ $requests = $testRunner->getAvailableRequests();
+
+ foreach ($requests as $index => $request) {
+ $name = strtolower($request['name']);
+ if (strpos($name, 'retrieve master asset folder') !== false ||
+ strpos($name, 'master asset folder and final') !== false) {
+
+ $modifiedRequest = $request;
+ $url = is_array($request['request']['url']) ? $request['request']['url']['raw'] : $request['request']['url'];
+ $url = preg_replace('/folders\/[a-f0-9]+\//', "folders/{$campaignId}/", $url);
+ $url = str_replace('{{baseUrl}}', $configV3->getBaseUrl(), $url);
+
+ if (is_array($modifiedRequest['request']['url'])) {
+ $modifiedRequest['request']['url']['raw'] = $url;
+ } else {
+ $modifiedRequest['request']['url'] = $url;
+ }
+
+ $result = $testRunner->runSingleTest($modifiedRequest, $index);
+
+ if ($result['status'] === 'PASS') {
+ $data = json_decode($result['response']['body'], true);
+ $folders = $data['folder_children']['asset_list'] ?? [];
+
+ foreach ($folders as $folder) {
+ $folderName = extractFolderName($folder);
+ // Look for Final Assets or Localized folder
+ if (strpos($folderName, 'Final') !== false ||
+ strpos($folderName, 'Localized') !== false ||
+ $folderName === $finalFolderName) {
+ return $folder['asset_id'];
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ return null;
+}
+
+function uploadFiles($testRunner, $uploadedFiles, $folderId)
+{
+ $apiClient = new ApiClient();
+ $configV3 = new ConfigV3();
+ $apiClient->setBaseUrl($configV3->getBaseUrl());
+
+ $oauth2Handler = new ReflectionProperty($testRunner, 'oauth2Handler');
+ $oauth2Handler->setAccessible(true);
+ $oauth2HandlerInstance = $oauth2Handler->getValue($testRunner);
+ if ($oauth2HandlerInstance) {
+ $apiClient->setHeader('Authorization', $oauth2HandlerInstance->getAuthHeader());
+ }
+
+ $uploader = new AssetUploader($apiClient, $folderId);
+
+ $results = [
+ 'total' => 0,
+ 'successful' => 0,
+ 'failed' => 0,
+ 'details' => []
+ ];
+
+ // Handle multiple file upload
+ if (isset($uploadedFiles['tmp_name'])) {
+ $fileCount = is_array($uploadedFiles['tmp_name']) ? count($uploadedFiles['tmp_name']) : 1;
+ $results['total'] = $fileCount;
+
+ for ($i = 0; $i < $fileCount; $i++) {
+ $tmpName = is_array($uploadedFiles['tmp_name']) ? $uploadedFiles['tmp_name'][$i] : $uploadedFiles['tmp_name'];
+ $fileName = is_array($uploadedFiles['name']) ? $uploadedFiles['name'][$i] : $uploadedFiles['name'];
+ $fileError = is_array($uploadedFiles['error']) ? $uploadedFiles['error'][$i] : $uploadedFiles['error'];
+
+ if ($fileError !== UPLOAD_ERR_OK) {
+ $results['details'][] = [
+ 'success' => false,
+ 'filename' => $fileName,
+ 'error' => 'Upload error code: ' . $fileError
+ ];
+ $results['failed']++;
+ continue;
+ }
+
+ $result = $uploader->uploadFile($tmpName, $folderId);
+
+ if ($result['success']) {
+ $results['successful']++;
+ } else {
+ $results['failed']++;
+ }
+
+ $results['details'][] = $result;
+ }
+ }
+
+ return $results;
+}
+
$oauth2Status = $testRunner ? $testRunner->getOAuth2Status() : null;
$envInfo = $configV3->getEnvironmentInfo();
?>
@@ -1076,18 +1326,132 @@ $envInfo = $configV3->getEnvironmentInfo();
📤 Upload Workflow: A2 → A3
- Load campaigns with status A2, upload processed assets, and update status to A3
+ Load campaigns with status A2 (assets sent to agency), upload processed files, and update status to A3
-
-
- ℹ️ Coming Soon: Upload functionality will be implemented in next phase
-
+
+
+
+
+
+
Found = count($results['a2_campaigns']) ?> Campaigns
+
+
+
+
= htmlspecialchars($campaign['campaign_name'] ?? 'Unnamed Campaign') ?>
+
Campaign ID: = htmlspecialchars($campaign['campaign_id'] ?? 'N/A') ?>
+
Asset ID: = htmlspecialchars($campaign['asset_id']) ?>
+
Brand: = htmlspecialchars($campaign['brand'] ?? 'N/A') ?>
+
Market: = htmlspecialchars($campaign['market'] ?? 'N/A') ?>
+
A2 - Assets Sent to Agency
+
+
+
+
+
+
+
+
+
+ Selected Campaign:
+ = htmlspecialchars($results['selected_campaign_a2']['campaign_name']) ?>
+ (= htmlspecialchars($results['selected_campaign_a2']['campaign_id']) ?>)
+
+
+
+
+
+
+
+
+
+ ✅ Upload target folder found
+
+
+
Upload Processed Files
+
+
+
+
+
+
+
Upload Results:
+ = $ur['successful'] ?> of = $ur['total'] ?> files uploaded successfully
+
+
+
+ View upload details
+
+ $detail): ?>
+
+
+ ✅ = htmlspecialchars($detail['filename']) ?>
+ Asset ID: = htmlspecialchars($detail['asset_id'] ?? 'N/A') ?>
+
+ ❌ = htmlspecialchars($detail['filename'] ?? 'Unknown') ?>
+ Error: = htmlspecialchars($detail['error']) ?>
+
+
+
+
+
+
+
+
+ 0): ?>
+
+
✅ Update Status to A3
+
+ Files have been uploaded. Update the campaign status to A3
+ (Localized Asset received from Agency)
+
+
+
+
+
@@ -1097,15 +1461,159 @@ $envInfo = $configV3->getEnvironmentInfo();
Load campaigns with status A5 (rework needed), download assets for rework, and update status to A6
-
-
- ℹ️ Coming Soon: Rework workflow will be implemented in next phase
-
+
+
+
+
+
+ Found = count($results['a5_campaigns']) ?> Campaigns Needing Rework
+
+
+
+
= htmlspecialchars($campaign['campaign_name'] ?? 'Unnamed Campaign') ?>
+
Campaign ID: = htmlspecialchars($campaign['campaign_id'] ?? 'N/A') ?>
+
Asset ID: = htmlspecialchars($campaign['asset_id']) ?>
+
Brand: = htmlspecialchars($campaign['brand'] ?? 'N/A') ?>
+
Market: = htmlspecialchars($campaign['market'] ?? 'N/A') ?>
+
A5 - Rework Needed
+
+
+
+
+
+
+
+
+
+ Selected Campaign:
+ = htmlspecialchars($results['selected_campaign_a5']['campaign_name']) ?>
+ (= htmlspecialchars($results['selected_campaign_a5']['campaign_id']) ?>)
+
+
+
+
+
+
+ Rework Assets (= count($results['rework_assets']) ?> files)
+
+
+
+
+ $asset): ?>
+
+
+
+ = htmlspecialchars($assetName) ?>
+ ID: = htmlspecialchars($assetId) ?>
+ Type: = htmlspecialchars($asset['mime_type'] ?? 'Unknown') ?>
+
+ | Size: = number_format($asset['file_size']) ?> bytes
+
+
+
+
+
+ 📋 Metadata
+
+
+
+
+
+
+
+
+
⚠️ Update Status to A6
+
+ Once rework assets are downloaded, update the campaign status to A6
+ (Assets to be reworked received by the Agency)
+
+
+
+
+
+
+
+
+
+ ✅ Download Successful: = htmlspecialchars($dl['filename']) ?>
+ Size: = number_format($dl['size']) ?> bytes
+
+ ❌ Download Failed: = htmlspecialchars($dl['error']) ?>
+
+
+
+
+
+
+ Bulk Download Results:
+ = $results['rework_bulk_download']['successful'] ?> of = $results['rework_bulk_download']['total'] ?> rework assets downloaded
+
+
@@ -1140,6 +1648,26 @@ $envInfo = $configV3->getEnvironmentInfo();
metadataDiv.style.display = metadataDiv.style.display === 'none' ? 'block' : 'none';
}
}
+
+ function selectCampaignA2(campaignId) {
+ document.querySelectorAll('.campaign-card').forEach(card => {
+ card.classList.remove('selected');
+ });
+
+ event.currentTarget.classList.add('selected');
+ document.getElementById('selected-campaign-a2-id').value = campaignId;
+ document.getElementById('select-campaign-a2-btn').disabled = false;
+ }
+
+ function selectCampaignA5(campaignId) {
+ document.querySelectorAll('.campaign-card').forEach(card => {
+ card.classList.remove('selected');
+ });
+
+ event.currentTarget.classList.add('selected');
+ document.getElementById('selected-campaign-a5-id').value = campaignId;
+ document.getElementById('select-campaign-a5-btn').disabled = false;
+ }