Duffer Derek
import Podio from "@phasesdk/api-client-for-podio";
import axios from "axios";
import FormData from "form-data";
import fs from "fs";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { appAuthentication } from "../podio/podio.js";
import { uploadFile } from "../fileUpload.js";
import tasks from "../../database/models/tasks.js";
import filesModel from "../../database/models/files.js";
import Log from "../../configs/logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const filePath = `${__dirname}/../../storage/public/files`;
// Mapping of database field names to Podio external IDs for image fields
// Based on db_fields.md - correct order and field names
const IMAGE_FIELD_MAPPING = {
"attach_images": "vedhaeft-billeder", // Vedhæft billeder
"attach_images_received": "vedhaeft-billeder-from-billede-modtagelse", // Vedhæft billeder from Billede modtagelse
"images_before_start": "images-from-partners", // Billeder fra maler inden opstart (Podio external ID: images-from-partners)
"images_monday": "billeder-mandag", // Billeder Mandag
"images_tuesday": "billeder-tirsdag", // Billeder Tirsdag
"images_wednesday": "billeder-onsdag", // Billeder Onsdag
"images_thursday": "billeder-torsdag", // Billeder Torsdag
"images_friday": "billeder-fredag", // Billeder Fredag
"images_saturday": "billeder-lordag", // Billeder Lørdag
"images_sunday": "billeder-sondag", // Billeder Søndag
"attach_invoice": "vedhaeft-faktura", // Vedhæft Faktura
"complaint_images": "reklamations-billeder", // Reklamations billeder
};
/**
* Convert ISO date string to Podio date format (YYYY-MM-DD)
* @param {string} isoDateString - ISO 8601 date string (e.g., "2025-02-15T00:00:00Z")
* @returns {string} - Formatted date string (e.g., "2025-02-15")
*/
const formatDateForPodio = (isoDateString) => {
if (!isoDateString) return null;
try {
const date = new Date(isoDateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
} catch (error) {
Log.error("Error formatting date for Podio:", error);
return null;
}
};
/**
* Upload file to Podio using Task app credentials
* @param {string} filePath - Local file path
* @param {string} fileName - File name
* @returns {Promise<number>} Podio file_id
*/
const podioFileUpload = async (filePath, fileName) => {
try {
// Use TASK_APP_ID and TASK_APP_TOKEN for office partner file uploads
// Files are uploaded to the task app, not a separate file app
if (!process.env.TASK_APP_ID || !process.env.TASK_APP_TOKEN) {
Log.error("TASK_APP_ID or TASK_APP_TOKEN is not set");
return 0;
}
Log.info(`Uploading file to Podio using TASK_APP_ID: ${process.env.TASK_APP_ID}`);
const auth = await appAuthentication(
process.env.TASK_APP_ID,
process.env.TASK_APP_TOKEN
);
if (!auth || !auth.access_token) {
Log.error("Failed to authenticate with Podio for file upload");
return 0;
}
let data = new FormData();
data.append("source", fs.createReadStream(filePath));
data.append("filename", fileName);
// Use default Podio API URL if not set in environment
const podioApiUrl = process.env.PODIO_API_URL || "https://api.podio.com/";
const fileUploadUrl = podioApiUrl.endsWith("/")
? `${podioApiUrl}file/`
: `${podioApiUrl}/file/`;
Log.info(`Uploading file to Podio URL: ${fileUploadUrl}`);
const config = {
method: "post",
url: fileUploadUrl,
headers: {
Authorization: `Bearer ${auth.access_token}`,
"Content-Type": "multipart/form-data",
...data.getHeaders(),
},
data: data,
};
try {
let res = await axios(config);
if (res.data && res.data.file_id) {
Log.info(`File uploaded successfully to Podio, file_id: ${res.data.file_id}`);
return res.data.file_id;
} else {
Log.error("Podio file upload response missing file_id:", res.data);
return 0;
}
} catch (err) {
Log.error("Error uploading file to Podio:", err);
if (err.response) {
Log.error("Podio API error response:", err.response.data);
}
return 0;
}
} catch (err) {
Log.error("Error in podioFileUpload:", err);
if (err.response) {
Log.error("Podio API error response:", err.response.data);
}
return 0;
}
};
/**
* Upload task files to server and Podio, then save to database
* @param {Array} files - Array of file objects from multer
* @param {number} item_id - Task item_id
* @param {string} fieldName - Database field name (e.g., "images_monday")
* @returns {Promise<Array>} Array of Podio file_ids
*/
const uploadTaskFiles = async (files, item_id, fieldName) => {
const podioFileIds = [];
if (!files || files.length === 0) {
Log.warn(`uploadTaskFiles: No files provided for field: ${fieldName}`);
return podioFileIds;
}
Log.info(`uploadTaskFiles: Processing ${files.length} file(s) for item_id: ${item_id}, field: ${fieldName}`);
for (const file of files) {
try {
Log.info(`Processing file: ${file.originalname}, size: ${file.size}, mimetype: ${file.mimetype}`);
const lastDotIndex = file.originalname.lastIndexOf(".");
const extension = lastDotIndex > 0 ? file.originalname.substring(lastDotIndex) : "";
const randomString = Math.random().toString(36).substring(2, 10);
const filenameWithoutExtension = lastDotIndex > 0
? file.originalname.substring(0, lastDotIndex)
: file.originalname;
const fileName = `${filenameWithoutExtension}_${randomString}${extension}`;
const path = `${filePath}/${item_id}/${fileName}`;
Log.info(`Saving file to server: ${path}`);
// Save file to server
await uploadFile(path, file);
Log.info(`File saved to server successfully: ${fileName}`);
// Upload to Podio
Log.info(`Uploading file to Podio: ${fileName}`);
const podioFileId = await podioFileUpload(path, fileName);
if (podioFileId && podioFileId > 0) {
Log.info(`File uploaded to Podio successfully: ${fileName}, file_id: ${podioFileId}`);
// Get Podio field name from mapping
const podioFieldName = IMAGE_FIELD_MAPPING[fieldName] || fieldName;
// Use "customer_image" keyword for "vedhaeft-billeder" field (attach_images)
// This matches the syncing logic in syncPodioData.js
const keyword = (fieldName === "attach_images" || podioFieldName === "vedhaeft-billeder")
? "customer_image"
: "office_partner";
// Save to database - save only filename (without path) to match other fields format
const insertFile = {
item_id: item_id,
keyword: keyword,
filename: fileName, // Save only filename, not path
file_id: podioFileId,
field: podioFieldName,
};
await filesModel.create(insertFile);
Log.info("File saved to database successfully:", insertFile);
podioFileIds.push(podioFileId);
} else {
Log.error(`Failed to upload file to Podio: ${fileName}, podioFileId: ${podioFileId}`);
}
} catch (error) {
Log.error(`Error uploading task file ${file.originalname}:`, error);
Log.error("Error stack:", error.stack);
}
}
Log.info(`uploadTaskFiles: Completed. Uploaded ${podioFileIds.length} out of ${files.length} file(s) for field: ${fieldName}`);
return podioFileIds;
};
/**
* Update a task in Podio and database (for office partner)
* @param {Object} data - Task data
* @param {number} data.item_id - Task item_id from Podio (required)
* @param {string} data.customer_first_name - Customer first name
* @param {string} data.customer_last_name - Customer last name
* @param {string} data.customer_address - Customer full address
* @param {Object|null} data.housing_type - Housing type object {id: number, value: string}
* @param {string} data.customer_email - Customer email
* @param {string} data.customer_phone_number - Customer phone number
* @param {Object|null} data.status - Status object {id: number, value: string}
* @param {Object|null} data.customer_has_paid - Customer has paid object {id: number, value: string}
* @param {Object|null} data.subcontractor_has_paid - Subcontractor has paid object {id: number, value: string}
* @param {Object|null} data.type - Type object {id: number, value: string}
* @param {Object|null} data.product_type - Product type object {id: number, value: string}
* @param {string} data.estimated_material_usage - Estimated material usage
* @param {Object|null} data.start_date - Start date object {start: string, end: string} (ISO strings)
* @param {string} data.next_sms_date_for_partners - Next SMS date for partners (ISO string)
* @param {string} data.deadline_date - Deadline date (ISO string)
* @param {string} data.sms_time_for_customer - SMS time for customer
* @param {string} data.final_deadline_date - Final deadline date (ISO string)
* @param {number} data.paid_amount_to_painter - Paid amount to painter
* @param {string} data.extra_services - Extra services
* @param {string} data.internal_notes - Internal notes
* @param {string} data.internal_notes_to_admin - Internal notes to admin
* @param {string} data.invoice_description - Invoice description
* @param {number} data.price - Price for the task
* @param {string} data.estimated_task_time - Estimated task time
* @param {string} data.not_included_price - Not included price
* @param {string} data.without_calculation - Without calculation
* @param {number} data.hours_used - Hours used
* @param {number} data.materials_incl_vat - Materials incl VAT
* @param {string} data.create_partner_description - Create partner description
* @param {Object|null} data.task_performed_by - Task performed by object {id: number, value: string}
* @param {Object|null} data.partner - Partner object {item_id: number} or {id: number}
* @param {number} data.add_product - Add product item_id (app reference)
* @param {number} data.mk_standard_offer_price - MK standard offer price
* @param {string} data.mk_standard_offer_price_text - MK standard offer price text
* @param {Object|null} data.generate_pdf - Generate PDF object {id: number, value: string}
* @param {Object|null} data.confirmation_email - Confirmation email object {id: number, value: string}
* @param {string} data.expected_arrival_hour - Expected arrival hour
* @param {string} data.expected_arrival_minute - Expected arrival minute
* @param {Object|null} data.send_sms - Send SMS object {id: number, value: string}
* @param {string} data.short_complaint_link - Short complaint link
* @param {Object|null} data.send_complaint_images_to_painter - Send complaint images to painter object {id: number, value: string}
* @param {string} data.order_comment - Order comment
* @param {string} data.material_pickup_date - Material pickup date (ISO string)
* @param {string} data.delivery_address - Delivery address
* @param {string} data.painter_name_picking_up - Painter name picking up
* @param {Object|null} data.material_order - Material order object {id: number, value: string}
* @param {number} data.misc_expenses - Misc expenses
* @param {Object|null} data.date - Date object {start: string, end: string} (ISO strings)
* @param {Object} req - Express request object (for file uploads)
* @returns {Promise<Object>} Updated task
*/
export const updateOfficePartnerTask = async (data, req) => {
try {
const {
item_id,
customer_first_name,
customer_last_name,
customer_address,
housing_type,
customer_email,
customer_phone_number,
status,
customer_has_paid,
subcontractor_has_paid,
type,
product_type,
estimated_material_usage,
start_date,
next_sms_date_for_partners,
end_date,
deadline_date, // Keep for backward compatibility, but end_date takes precedence
sms_time_for_customer,
final_deadline_date,
paid_amount_to_painter,
extra_services,
free_text_for_additional_services,
internal_notes,
internal_notes_to_admin,
invoice_description,
price,
estimated_task_time,
not_included_price,
without_calculation,
hours_used,
materials_incl_vat,
create_partner_description,
task_performed_by,
partner,
mk_standard_offer_price,
mk_standard_offer_price_text,
generate_pdf,
confirmation_email,
expected_arrival_hour,
expected_arrival_minute,
send_sms,
short_complaint_link,
send_complaint_images_to_painter,
order_comment,
material_pickup_date,
delivery_address,
painter_name_picking_up,
material_order,
misc_expenses,
date,
add_product,
painter_extra_description,
further_description_for_painting_is_displayed_on_pdf,
} = data;
// Validate required fields
if (!item_id) {
throw new Error("item_id is required");
}
// Check if task exists
const existingTask = await tasks.findOne({
where: { item_id: item_id },
});
if (!existingTask) {
throw new Error(`Task not found with item_id: ${item_id}`);
}
// Prepare Podio fields
const podioFields = {};
// Text fields
if (customer_first_name !== undefined) podioFields["title"] = customer_first_name;
if (customer_last_name !== undefined) podioFields["kundens-efternavn"] = customer_last_name;
if (customer_address !== undefined) podioFields["kundens-fulde-adresse-2"] = customer_address;
if (customer_email !== undefined) podioFields["kundens-e-mail"] = customer_email;
if (customer_phone_number !== undefined) podioFields["kundens-telefonnummer"] = customer_phone_number;
if (estimated_material_usage !== undefined) podioFields["estimeret-materiale-forbrug"] = estimated_material_usage;
if (sms_time_for_customer !== undefined) podioFields["tidspunkt"] = sms_time_for_customer;
if (extra_services !== undefined) podioFields["ekstra-ydelser"] = extra_services;
if (free_text_for_additional_services !== undefined) podioFields["fritekst-til-ekstraydelser"] = free_text_for_additional_services;
if (internal_notes !== undefined) podioFields["noter"] = internal_notes;
if (internal_notes_to_admin !== undefined) podioFields["interne-admin-noter"] = internal_notes_to_admin;
if (invoice_description !== undefined) podioFields["beskrivelse-af-hvad-der-skal-laves-pa-opgaven"] = invoice_description;
if (estimated_task_time !== undefined) podioFields["estimeret-tid-for-opgaven"] = estimated_task_time;
if (not_included_price !== undefined) podioFields["ikke-inkluderet-i-tilbuddet"] = not_included_price;
if (without_calculation !== undefined) podioFields["uden-beregning"] = without_calculation;
// further_description_for_painting_is_displayed_on_pdf is a text field for "Yderligere beskrivelse til Maler"
// Accept both field names for backward compatibility
const painterDescription = further_description_for_painting_is_displayed_on_pdf !== undefined
? further_description_for_painting_is_displayed_on_pdf
: painter_extra_description;
if (painterDescription !== undefined) {
// Handle null/empty values
const trimmedDescription = typeof painterDescription === 'string' ? painterDescription.trim() : painterDescription;
if (trimmedDescription !== null && trimmedDescription !== '') {
podioFields["yderligere-beskrivelse-til-maler-vises-pa-pdf"] = trimmedDescription;
} else {
// Try to explicitly set to null to clear the field in Podio
// If Podio doesn't accept null, we'll need to handle it differently
podioFields["yderligere-beskrivelse-til-maler-vises-pa-pdf"] = null;
}
}
if (mk_standard_offer_price_text !== undefined) podioFields["tekst-mk-std-tilbudspris"] = mk_standard_offer_price_text;
if (expected_arrival_hour !== undefined) podioFields["forventet-ankomst-time"] = expected_arrival_hour;
if (expected_arrival_minute !== undefined) podioFields["forventet-ankomst-minutter"] = expected_arrival_minute;
if (short_complaint_link !== undefined) podioFields["short-reklamation-link"] = short_complaint_link;
if (order_comment !== undefined) podioFields["kommentar-til-bestilling"] = order_comment;
if (delivery_address !== undefined) podioFields["leveringsadresse-til-kunde"] = delivery_address;
if (painter_name_picking_up !== undefined) podioFields["navn-pa-maler-der-afhenter"] = painter_name_picking_up;
// Category fields (need ID for Podio)
// Handle null values: if explicitly null, clear the field in Podio
if (housing_type !== undefined) {
if (housing_type === null || housing_type.id === null || housing_type.id === undefined) {
podioFields["boligtype"] = []; // Clear the field
} else if (housing_type.id !== undefined) {
podioFields["boligtype"] = [housing_type.id];
}
}
if (status !== undefined) {
if (status === null || (status.id === null || status.id === undefined && !status.name && !status.value)) {
podioFields["status"] = []; // Clear the field
} else if (status.id !== undefined) {
podioFields["status"] = [status.id];
}
}
if (customer_has_paid !== undefined) {
if (customer_has_paid === null || customer_has_paid.id === null || customer_has_paid.id === undefined) {
podioFields["kunde-og-ue-betalt"] = []; // Clear the field
} else if (customer_has_paid.id !== undefined) {
podioFields["kunde-og-ue-betalt"] = [customer_has_paid.id];
}
}
if (subcontractor_has_paid !== undefined) {
if (subcontractor_has_paid === null || subcontractor_has_paid.id === null || subcontractor_has_paid.id === undefined) {
podioFields["ue-er-betalt"] = []; // Clear the field
} else if (subcontractor_has_paid.id !== undefined) {
podioFields["ue-er-betalt"] = [subcontractor_has_paid.id];
}
}
if (type !== undefined) {
if (type === null || type.id === null || type.id === undefined) {
podioFields["type"] = []; // Clear the field
} else if (type.id !== undefined) {
podioFields["type"] = [type.id];
}
}
if (product_type !== undefined) {
if (product_type === null || product_type.id === null || product_type.id === undefined) {
podioFields["produkttype"] = []; // Clear the field
} else if (product_type.id !== undefined) {
podioFields["produkttype"] = [product_type.id];
}
}
if (task_performed_by !== undefined) {
if (task_performed_by === null || task_performed_by.id === null || task_performed_by.id === undefined) {
podioFields["opgave-udfort-af"] = []; // Clear the field
} else if (task_performed_by.id !== undefined) {
podioFields["opgave-udfort-af"] = [task_performed_by.id];
}
}
if (generate_pdf !== undefined) {
if (generate_pdf === null || generate_pdf.id === null || generate_pdf.id === undefined) {
podioFields["generer-pdf"] = []; // Clear the field
} else if (generate_pdf.id !== undefined) {
podioFields["generer-pdf"] = [generate_pdf.id];
}
}
if (confirmation_email !== undefined) {
if (confirmation_email === null || confirmation_email.id === null || confirmation_email.id === undefined) {
podioFields["bekraeftelsesmail"] = []; // Clear the field
} else if (confirmation_email.id !== undefined) {
podioFields["bekraeftelsesmail"] = [confirmation_email.id];
}
}
if (send_sms !== undefined) {
if (send_sms === null || send_sms.id === null || send_sms.id === undefined) {
podioFields["send-sms"] = []; // Clear the field
} else if (send_sms.id !== undefined) {
podioFields["send-sms"] = [send_sms.id];
}
}
if (send_complaint_images_to_painter !== undefined) {
if (send_complaint_images_to_painter === null || send_complaint_images_to_painter.id === null || send_complaint_images_to_painter.id === undefined) {
podioFields["send-reklamations-billeder-til-maler"] = []; // Clear the field
} else if (send_complaint_images_to_painter.id !== undefined) {
podioFields["send-reklamations-billeder-til-maler"] = [send_complaint_images_to_painter.id];
}
}
if (create_partner_description !== undefined) {
if (create_partner_description === null || create_partner_description.id === null || create_partner_description.id === undefined) {
podioFields["create-partner-description"] = []; // Clear the field
} else if (create_partner_description.id !== undefined) {
podioFields["create-partner-description"] = [create_partner_description.id];
}
}
if (material_order !== undefined) {
if (material_order === null || material_order.id === null || material_order.id === undefined) {
podioFields["materiale-bestilling"] = []; // Clear the field
} else if (material_order.id !== undefined) {
podioFields["materiale-bestilling"] = [material_order.id];
}
}
// Date fields - convert ISO format to Podio format (YYYY-MM-DD)
// Udførelsesdato (dato-for-hvornar-opgaven-skal-pabegyndes) maps to start_date only
if (start_date !== undefined) {
if (start_date && start_date.start) {
podioFields["dato-for-hvornar-opgaven-skal-pabegyndes"] = {
start_date: formatDateForPodio(start_date.start),
};
} else {
podioFields["dato-for-hvornar-opgaven-skal-pabegyndes"] = null;
}
}
if (next_sms_date_for_partners !== undefined) {
podioFields["next-sms-date-for-partners"] = next_sms_date_for_partners
? { start_date: formatDateForPodio(next_sms_date_for_partners) }
: null;
}
// Deadline Date (deadline-date) maps to end_date in database
// Use end_date if provided, otherwise fall back to deadline_date for backward compatibility
const deadlineDateForPodio = end_date !== undefined ? end_date : deadline_date;
if (deadlineDateForPodio !== undefined) {
podioFields["deadline-date"] = deadlineDateForPodio
? { start_date: formatDateForPodio(deadlineDateForPodio) }
: null;
}
if (final_deadline_date !== undefined) {
podioFields["deadline-date-must-be-finished-this-day"] = final_deadline_date
? { start_date: formatDateForPodio(final_deadline_date) }
: null;
}
if (material_pickup_date !== undefined) {
podioFields["materiale-afhentningsdato"] = material_pickup_date
? { start_date: formatDateForPodio(material_pickup_date) }
: null;
}
if (date !== undefined) {
if (date && (date.start || date.end)) {
podioFields["date"] = {
start_date: date.start ? formatDateForPodio(date.start) : null,
end_date: date.end ? formatDateForPodio(date.end) : null,
};
} else {
podioFields["date"] = null;
}
}
// Number fields
if (paid_amount_to_painter !== undefined) podioFields["antal-date-tilradighed"] = paid_amount_to_painter;
if (price !== undefined) podioFields["pris-for-opgaven-inkl-moms"] = price;
if (hours_used !== undefined) podioFields["timer-brugt"] = hours_used;
if (materials_incl_vat !== undefined) podioFields["materialer-i-kr"] = materials_incl_vat;
if (mk_standard_offer_price !== undefined) podioFields["mk-std-tilbudspris"] = mk_standard_offer_price;
if (misc_expenses !== undefined) podioFields["diverse-udgifter-bil-marketing-mv"] = misc_expenses;
// App reference fields
if (partner !== undefined) {
if (partner?.item_id) {
podioFields["partner-2"] = [partner.item_id];
} else if (partner?.id) {
// If only id is provided, assume it's the item_id
podioFields["partner-2"] = [partner.id];
} else if (partner === null) {
podioFields["partner-2"] = null;
}
}
// Add product field (app reference)
if (add_product !== undefined) {
if (add_product && add_product !== null) {
// add_product is sent as item_id (number) from frontend
const productItemId = typeof add_product === 'number' ? add_product : parseInt(add_product, 10);
if (!isNaN(productItemId) && productItemId > 0) {
podioFields["tilfoj-produkt-vaelg-et-produkt-ad-gangen"] = [productItemId];
} else {
podioFields["tilfoj-produkt-vaelg-et-produkt-ad-gangen"] = null;
}
} else {
podioFields["tilfoj-produkt-vaelg-et-produkt-ad-gangen"] = null;
}
}
// Remove null/undefined values from Podio fields, but keep null for fields that need to be cleared
// Note: Some Podio fields may accept null to clear them, but text fields with min length don't
Object.keys(podioFields).forEach((key) => {
if (podioFields[key] === undefined) {
delete podioFields[key];
}
// Keep null values - Podio may accept them for some field types to clear them
// For text fields with min length, null might not work, but we'll try
});
// Authenticate with Podio
const auth = await appAuthentication(
process.env.TASK_APP_ID,
process.env.TASK_APP_TOKEN
);
// Handle file uploads and deletions for image fields
// Check if we have files to upload or deletions to process
// Multer stores files in req.files when using upload.array("file")
const hasFiles = req && req.files && (
(Array.isArray(req.files) && req.files.length > 0) ||
(req.files && typeof req.files === 'object' && Object.keys(req.files).length > 0)
);
const hasDeletion = data.deletion && (
(typeof data.deletion === 'string' && data.deletion.trim() !== '' && data.deletion !== 'null' && data.deletion !== 'undefined') ||
(Array.isArray(data.deletion) && data.deletion.length > 0)
);
Log.info("=== File upload check for task ===");
Log.info("req exists:", !!req);
Log.info("req.files:", req?.files);
Log.info("req.files type:", Array.isArray(req.files) ? 'array' : typeof req.files);
Log.info("req.files length:", Array.isArray(req.files) ? req.files.length : (req.files ? Object.keys(req.files).length : 0));
Log.info("hasFiles:", hasFiles);
Log.info("data.deletion:", data.deletion);
Log.info("hasDeletion:", hasDeletion);
// Get files grouped by fieldname - upload.any() returns array of files with fieldname property
let filesByField = {};
if (req && req.files) {
if (Array.isArray(req.files)) {
// Group files by fieldname
req.files.forEach(file => {
const fieldName = file.fieldname;
if (!filesByField[fieldName]) {
filesByField[fieldName] = [];
}
filesByField[fieldName].push(file);
});
} else if (typeof req.files === 'object') {
// If it's already an object, use it directly
filesByField = req.files;
}
}
const totalFiles = Object.values(filesByField).flat().length;
Log.info("Files grouped by field:", Object.keys(filesByField));
Log.info("Total files:", totalFiles);
// Process file uploads if files exist
if (req && (totalFiles > 0 || hasDeletion)) {
Log.info("=== Starting image field update for task ===");
// Parse deletion data - can be an object with field names as keys
let deletionData = {};
if (hasDeletion) {
try {
const parsed = typeof data.deletion === 'string'
? JSON.parse(data.deletion)
: data.deletion;
if (typeof parsed === 'object' && !Array.isArray(parsed)) {
deletionData = parsed;
} else {
Log.warn("Deletion data is not an object, ignoring");
deletionData = {};
}
} catch (error) {
Log.error("Error parsing deletion data:", error);
deletionData = {};
}
}
Log.info("Deletion data:", deletionData);
// Process each image field that has files or deletions
// Frontend sends files with fieldname matching the database field name
// Track processed Podio field names to avoid processing the same field twice
const processedPodioFields = new Set();
const imageFields = Object.keys(IMAGE_FIELD_MAPPING);
for (const fieldName of imageFields) {
const podioFieldName = IMAGE_FIELD_MAPPING[fieldName];
// Skip if we've already processed this Podio field (avoid duplicates)
if (processedPodioFields.has(podioFieldName)) {
Log.info(`Skipping duplicate Podio field: ${podioFieldName} (mapped from: ${fieldName})`);
continue;
}
processedPodioFields.add(podioFieldName);
// Use "customer_image" keyword for "vedhaeft-billeder" field (attach_images)
// This matches the syncing logic in syncPodioData.js
const keyword = (fieldName === "attach_images" || podioFieldName === "vedhaeft-billeder")
? "customer_image"
: "office_partner";
// Get existing file_ids from database for this field
// For "vedhaeft-billeder", also check files with "customer_image" keyword (for backward compatibility)
const whereCondition = (fieldName === "attach_images" || podioFieldName === "vedhaeft-billeder")
? {
item_id: item_id,
field: podioFieldName,
keyword: "customer_image",
}
: {
item_id: item_id,
field: podioFieldName,
keyword: "office_partner",
};
const existingFiles = await filesModel.findAll({
where: whereCondition,
});
const existingFileIds = existingFiles.map((f) => f.file_id);
Log.info(`Existing file_ids for ${fieldName} (${podioFieldName}):`, existingFileIds);
// Get deletion IDs for this field - check both fieldName and podioFieldName, and handle hyphenated versions
let deletionIds = deletionData[fieldName] ||
deletionData[podioFieldName] ||
deletionData[fieldName.replace(/_/g, '-')] ||
deletionData[podioFieldName.replace(/-/g, '_')] ||
[];
if (!Array.isArray(deletionIds)) {
deletionIds = [];
}
Log.info(`File IDs to delete for ${fieldName}:`, deletionIds);
// Get files for this field - check fieldName, podioFieldName, and handle hyphenated/underscore variations
// Frontend sends "images-from-partners" but mapping uses "images_from_partners"
const fieldFiles = filesByField[fieldName] ||
filesByField[podioFieldName] ||
filesByField[fieldName.replace(/_/g, '-')] ||
filesByField[podioFieldName.replace(/-/g, '_')] ||
[];
// Upload new files for this field
let newFileIds = [];
if (fieldFiles.length > 0) {
Log.info(`Uploading ${fieldFiles.length} files for field: ${fieldName} (Podio: ${podioFieldName})`);
newFileIds = await uploadTaskFiles(fieldFiles, item_id, fieldName);
Log.info(`New file_ids for ${fieldName}:`, newFileIds);
}
// Delete files from database and server
if (deletionIds.length > 0) {
for (const fileId of deletionIds) {
try {
// For "vedhaeft-billeder", check both "customer_image" and "office_partner" keywords
// (for backward compatibility with files that might have been uploaded with wrong keyword)
const fileToDelete = await filesModel.findOne({
where: (fieldName === "attach_images" || podioFieldName === "vedhaeft-billeder")
? {
item_id: item_id,
file_id: fileId,
field: podioFieldName,
keyword: "customer_image",
}
: {
item_id: item_id,
file_id: fileId,
field: podioFieldName,
keyword: "office_partner",
},
});
if (fileToDelete) {
// Delete from server
const serverPath = `${filePath}${fileToDelete.filename}`;
if (fs.existsSync(serverPath)) {
fs.unlinkSync(serverPath);
Log.info("File deleted from server:", serverPath);
}
// Delete from database
await filesModel.destroy({
where: {
id: fileToDelete.id,
},
});
Log.info("File deleted from database:", fileId);
}
} catch (error) {
Log.error(`Error deleting file ${fileId}:`, error);
}
}
}
// Calculate final file_ids: existing - deleted + new
const finalFileIds = [
...existingFileIds.filter((id) => !deletionIds.includes(id)),
...newFileIds,
];
Log.info(`Final file_ids for ${fieldName} (${podioFieldName}):`, finalFileIds);
// Update Podio field if there are changes
if (newFileIds.length > 0 || deletionIds.length > 0 || fieldFiles.length > 0) {
podioFields[podioFieldName] = finalFileIds;
Log.info(`Updating Podio field ${podioFieldName} with file_ids:`, finalFileIds);
}
}
} else {
Log.info("=== Skipping image field update for task ===");
Log.info("Reason: No files to upload and no deletions to process");
}
// Update in Podio only if there are actual fields to update
// When there's a deletion, only update Podio if there are other field changes
// Don't send empty podioFields to Podio as it might clear fields
const hasValidFields = Object.keys(podioFields).length > 0;
if (hasValidFields) {
try {
await Podio.api.item(auth).update(item_id, {
fields: podioFields,
});
Log.info("Task updated in Podio:", item_id, "Fields:", Object.keys(podioFields));
} catch (podioError) {
// If error is due to null value in text field, try removing it and updating without that field
if (podioError.message && podioError.message.includes("must be at least 1 characters long")) {
Log.warn("Podio rejected null/empty value for text field, removing from update:", podioError.message);
// Remove the problematic field and try again
const fieldsWithoutProblem = { ...podioFields };
delete fieldsWithoutProblem["yderligere-beskrivelse-til-maler-vises-pa-pdf"];
if (Object.keys(fieldsWithoutProblem).length > 0) {
await Podio.api.item(auth).update(item_id, {
fields: fieldsWithoutProblem,
});
Log.info("Task updated in Podio (without problematic field):", item_id);
}
// Note: Field will remain in Podio with old value, but database is updated to null
Log.warn("Note: Text field cannot be cleared in Podio via API (min length requirement). Field cleared in database only.");
} else {
throw podioError;
}
}
} else {
Log.info("Skipping Podio update - no valid fields to update (only file deletions, no other field changes)");
}
// Prepare database record (store values, not IDs for category fields)
const dbRecord = {};
// Text fields
if (customer_first_name !== undefined) dbRecord.customer_first_name = customer_first_name;
if (customer_last_name !== undefined) dbRecord.customer_last_name = customer_last_name;
if (customer_address !== undefined) dbRecord.customer_address = customer_address;
if (customer_email !== undefined) dbRecord.customer_email = customer_email;
if (customer_phone_number !== undefined) dbRecord.customer_phone_number = customer_phone_number;
if (estimated_material_usage !== undefined) dbRecord.estimated_material_usage = estimated_material_usage;
if (sms_time_for_customer !== undefined) dbRecord.sms_time_for_customer = sms_time_for_customer;
if (extra_services !== undefined) dbRecord.extra_services = extra_services;
if (free_text_for_additional_services !== undefined) dbRecord.free_text_for_additional_services = free_text_for_additional_services;
if (internal_notes !== undefined) dbRecord.internal_notes = internal_notes;
if (internal_notes_to_admin !== undefined) dbRecord.internal_notes_to_admin = internal_notes_to_admin;
if (invoice_description !== undefined) dbRecord.invoice_description = invoice_description;
if (estimated_task_time !== undefined) dbRecord.estimated_task_time = estimated_task_time;
if (not_included_price !== undefined) dbRecord.not_included_price = not_included_price;
if (without_calculation !== undefined) dbRecord.without_calculation = without_calculation;
// Handle further_description_for_painting_is_displayed_on_pdf (text field)
// Accept both field names for backward compatibility
const painterDescriptionForDB = further_description_for_painting_is_displayed_on_pdf !== undefined
? further_description_for_painting_is_displayed_on_pdf
: painter_extra_description;
if (painterDescriptionForDB !== undefined) {
// Handle null/empty values - set to null to clear the field in database
const trimmedDescriptionForDB = typeof painterDescriptionForDB === 'string' ? painterDescriptionForDB.trim() : painterDescriptionForDB;
if (trimmedDescriptionForDB === null || trimmedDescriptionForDB === '') {
dbRecord.further_description_for_painting_is_displayed_on_pdf = null;
} else {
dbRecord.further_description_for_painting_is_displayed_on_pdf = trimmedDescriptionForDB;
}
}
// Handle create_partner_description category field - store value in database
// This is a separate field from further_description_for_painting_is_displayed_on_pdf
if (create_partner_description !== undefined) {
if (create_partner_description === null || create_partner_description.id === null || create_partner_description.id === undefined) {
dbRecord.create_partner_description = null; // Clear the field
} else if (create_partner_description.value !== undefined) {
dbRecord.create_partner_description = create_partner_description.value;
}
}
if (mk_standard_offer_price_text !== undefined) dbRecord.mk_standard_offer_price_text = mk_standard_offer_price_text;
if (expected_arrival_hour !== undefined) dbRecord.expected_arrival_hour = expected_arrival_hour;
if (expected_arrival_minute !== undefined) dbRecord.expected_arrival_minute = expected_arrival_minute;
if (short_complaint_link !== undefined) dbRecord.short_complaint_link = short_complaint_link;
if (order_comment !== undefined) dbRecord.order_comment = order_comment;
if (delivery_address !== undefined) dbRecord.delivery_address = delivery_address;
if (painter_name_picking_up !== undefined) dbRecord.painter_name_picking_up = painter_name_picking_up;
// Category fields - store value in database (except status and type which store IDs)
// Handle null values: if explicitly null, set to null in database
if (housing_type !== undefined) {
if (housing_type === null || housing_type.id === null || housing_type.id === undefined) {
dbRecord.housing_type = null; // Clear the field
} else if (housing_type.value !== undefined) {
dbRecord.housing_type = housing_type.value;
}
}
if (status !== undefined) {
if (status === null || (status.id === null || status.id === undefined && !status.name && !status.value)) {
dbRecord.status = null; // Clear the field
} else if (status.id !== undefined) {
dbRecord.status = status.id; // Status stores ID in database
}
}
if (customer_has_paid !== undefined) {
if (customer_has_paid === null || customer_has_paid.id === null || customer_has_paid.id === undefined) {
dbRecord.customer_has_paid = null; // Clear the field
} else if (customer_has_paid.value !== undefined) {
dbRecord.customer_has_paid = customer_has_paid.value;
}
}
if (subcontractor_has_paid !== undefined) {
if (subcontractor_has_paid === null || subcontractor_has_paid.id === null || subcontractor_has_paid.id === undefined) {
dbRecord.subcontractor_has_paid = null; // Clear the field
} else if (subcontractor_has_paid.value !== undefined) {
dbRecord.subcontractor_has_paid = subcontractor_has_paid.value;
}
}
if (type !== undefined) {
if (type === null || type.id === null || type.id === undefined) {
dbRecord.type = null; // Clear the field
} else if (type.id !== undefined) {
dbRecord.type = type.id; // Type stores ID in database
}
}
if (product_type !== undefined) {
if (product_type === null || product_type.id === null || product_type.id === undefined) {
dbRecord.product_type = null; // Clear the field
} else if (product_type.value !== undefined) {
dbRecord.product_type = product_type.value;
}
}
if (task_performed_by !== undefined) {
if (task_performed_by === null || task_performed_by.id === null || task_performed_by.id === undefined) {
dbRecord.task_performed_by = null; // Clear the field
} else if (task_performed_by.value !== undefined) {
dbRecord.task_performed_by = task_performed_by.value;
}
}
if (generate_pdf !== undefined) {
if (generate_pdf === null || generate_pdf.id === null || generate_pdf.id === undefined) {
dbRecord.generate_pdf = null; // Clear the field
} else if (generate_pdf.value !== undefined) {
dbRecord.generate_pdf = generate_pdf.value;
}
}
if (confirmation_email !== undefined) {
if (confirmation_email === null || confirmation_email.id === null || confirmation_email.id === undefined) {
dbRecord.confirmation_email = null; // Clear the field
} else if (confirmation_email.value !== undefined) {
dbRecord.confirmation_email = confirmation_email.value;
}
}
if (send_sms !== undefined) {
if (send_sms === null || send_sms.id === null || send_sms.id === undefined) {
dbRecord.send_sms = null; // Clear the field
} else if (send_sms.value !== undefined) {
dbRecord.send_sms = send_sms.value;
}
}
if (send_complaint_images_to_painter !== undefined) {
if (send_complaint_images_to_painter === null || send_complaint_images_to_painter.id === null || send_complaint_images_to_painter.id === undefined) {
dbRecord.send_complaint_images_to_painter = null; // Clear the field
} else if (send_complaint_images_to_painter.value !== undefined) {
dbRecord.send_complaint_images_to_painter = send_complaint_images_to_painter.value;
}
}
if (material_order !== undefined) {
if (material_order === null || material_order.id === null || material_order.id === undefined) {
dbRecord.material_order = null; // Clear the field
} else if (material_order.value !== undefined) {
dbRecord.material_order = material_order.value;
}
}
// Date fields
// Udførelsesdato (dato-for-hvornar-opgaven-skal-pabegyndes) maps to start_date only
if (start_date !== undefined) {
if (start_date && start_date.start) {
dbRecord.start_date = new Date(start_date.start);
} else {
dbRecord.start_date = null;
}
// Note: According to db_fields.md, Udførelsesdato only maps to start_date, not end_date
}
if (next_sms_date_for_partners !== undefined) dbRecord.next_sms_date_for_partners = next_sms_date_for_partners ? new Date(next_sms_date_for_partners) : null;
// Deadline Date (deadline-date) maps to end_date in database
// Use end_date if provided, otherwise fall back to deadline_date for backward compatibility
const deadlineDateForDB = end_date !== undefined ? end_date : deadline_date;
if (deadlineDateForDB !== undefined) {
dbRecord.end_date = deadlineDateForDB ? new Date(deadlineDateForDB) : null;
}
if (final_deadline_date !== undefined) dbRecord.final_deadline_date = final_deadline_date ? new Date(final_deadline_date) : null;
if (material_pickup_date !== undefined) dbRecord.material_pickup_date = material_pickup_date ? new Date(material_pickup_date) : null;
if (date !== undefined) {
if (date && date.start) {
dbRecord.date_start = new Date(date.start);
} else {
dbRecord.date_start = null;
}
if (date && date.end) {
dbRecord.date_end = new Date(date.end);
} else {
dbRecord.date_end = null;
}
}
// Number fields
if (paid_amount_to_painter !== undefined) dbRecord.paid_amount_to_painter = paid_amount_to_painter ? parseFloat(paid_amount_to_painter) : null;
if (price !== undefined) dbRecord.price = price ? parseFloat(price) : null;
if (hours_used !== undefined) dbRecord.hours_used = hours_used ? parseFloat(hours_used) : null;
if (materials_incl_vat !== undefined) dbRecord.materials_incl_vat = materials_incl_vat ? parseFloat(materials_incl_vat) : null;
if (mk_standard_offer_price !== undefined) dbRecord.mk_standard_offer_price = mk_standard_offer_price ? parseFloat(mk_standard_offer_price) : null;
if (misc_expenses !== undefined) dbRecord.misc_expenses = misc_expenses ? parseFloat(misc_expenses) : null;
// Partner field - store item_id
if (partner !== undefined) {
if (partner?.item_id) {
dbRecord.user_item_id = partner.item_id;
} else if (partner?.id) {
dbRecord.user_item_id = partner.id;
} else if (partner === null) {
dbRecord.user_item_id = null;
}
}
// Add product field - store item_id
if (add_product !== undefined) {
if (add_product && add_product !== null) {
const productItemId = typeof add_product === 'number' ? add_product : parseInt(add_product, 10);
dbRecord.add_product = !isNaN(productItemId) && productItemId > 0 ? productItemId : null;
} else {
dbRecord.add_product = null;
}
}
// Update in database
await tasks.update(dbRecord, {
where: { item_id: item_id },
});
Log.info("Task updated in database:", item_id);
// Fetch and return updated task
const updatedTask = await tasks.findOne({
where: { item_id: item_id },
});
return updatedTask;
} catch (error) {
Log.error("Error updating task:", error);
throw error;
}
};
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists