Duffer Derek

Current Path : /var/www/api-mk-planner.bitkit.dk/httpdocs/Backend/src/services/api/
Upload File :
Current File : /var/www/api-mk-planner.bitkit.dk/httpdocs/Backend/src/services/api/task.js

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