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/officePartner.js

import { Op, Sequelize } from "sequelize";
import tasks from "../../database/models/tasks.js";
import inquiry from "../../database/models/inquiry.js";
import product from "../../database/models/product.js";
import productLine from "../../database/models/productLine.js";
import files from "../../database/models/files.js";
import User from "../../database/models/users.js";
import { dateTimeFormat } from "../commonFunctions.js";
import Log from "../../configs/logger.js";
import { categories } from "../../configs/categories.config.js";
import { taskTypes, landsdel } from "../../configs/general.config.js";
import { getLandsdelColor } from "./tasks.js";
import { getAllUsers } from "./users.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";

export const getOfficePartnerDashboard = async (officePartnerItemId, filters = {}) => {
  try {
    let response = {
      assigned: {
        tasks: [],
        products: [],
        partners: [],
        categories: categories,
        inquiry: [],
      },
      filters: {},
    };

    // Fetch all products
    const allProducts = await product.findAll({
      attributes: ["id", "item_id", "product_name", "status", "unit_price"],
      where: {
        status: {
          [Op.ne]: null,
        },
      },
      order: [["sorting_sequence", "ASC"]],
    });

    response.assigned.products = allProducts.map((prod) => ({
      id: prod.id,
      item_id: prod.item_id,
      name: prod.product_name,
      status: prod.status,
      unit_price: prod.unit_price ? parseFloat(prod.unit_price) : null,
    }));

    // Fetch all partners (type 0) - same as admin dashboard
    const allPartners = await getAllUsers(0);
    response.assigned.partners = allPartners || [];

    // Build where condition for tasks with filters
    let taskWhereCondition = {
      office_partner: officePartnerItemId,
    };

    // Apply status filter if provided (check both task_status and status for backward compatibility)
    // Otherwise default to exclude cancelled statuses
    if (filters.task_status && filters.task_status.id) {
      taskWhereCondition.status = filters.task_status.id;
    } else if (filters.status && filters.status.id) {
      taskWhereCondition.status = filters.status.id;
    } else {
      // Default: exclude cancelled statuses
      taskWhereCondition.status = {
        [Op.notIn]: [5, 8],
      };
    }

    // Apply date range filter if provided
    if (filters.startDate && filters.endDate) {
      taskWhereCondition[Op.or] = [
        {
          start_date: {
            [Op.between]: [
              dateTimeFormat(filters.startDate) + "T00:00:00.000Z",
              dateTimeFormat(filters.endDate) + "T23:59:59.999Z",
            ],
          },
        },
        {
          end_date: {
            [Op.between]: [
              dateTimeFormat(filters.startDate) + "T00:00:00.000Z",
              dateTimeFormat(filters.endDate) + "T23:59:59.999Z",
            ],
          },
        },
      ];
    }

    // Apply type filter if provided
    if (filters.type && filters.type.id) {
      taskWhereCondition.type = filters.type.id;
    }

    // Apply service manager filter if provided
    if (filters.service_managers && filters.service_managers.id) {
      taskWhereCondition.service_manager_id = filters.service_managers.id;
    }

    // Apply countryside/lansdel filter if provided
    // Handle both flat structure (filters.lansdel.id) and nested structure (filters.lansdel.value.id)
    if (filters.lansdel) {
      const lansdelId = filters.lansdel.id || (filters.lansdel.value && filters.lansdel.value.id);
      if (lansdelId) {
        taskWhereCondition.countryside = lansdelId;
      }
    }

    // Fetch tasks assigned to the current office partner
    if (officePartnerItemId) {
      const assignedTasks = await tasks.findAll({
        attributes: {
          exclude: ["created_at", "updated_at"],
        },
        where: taskWhereCondition,
        include: [
          {
            model: User,
            as: "serviceManager",
            attributes: [["item_id", "id"], "name", "email", "phone"],
            required: false,
          },
          {
            model: productLine,
            as: "productLines",
            attributes: [
              "id",
              "item_id",
              "title",
              "product",
              "room",
              "section",
              "quantity",
              "unit_price",
              "total",
              "inquiry",
              "task",
            ],
            required: false,
            include: [
              {
                model: product,
                as: "productRelation",
                attributes: ["id", "item_id", "product_name", "standard_product_text", "status", "unit_price"],
                required: false,
              },
            ],
          },
          {
            model: files,
            attributes: ["file_id", "keyword", "filename", "field"],
            where: {
              [Op.or]: [
                { keyword: "office_partner" },
                { keyword: "customer_image" },
              ],
            },
            required: false,
          },
        ],
      });

      // Fetch inquiry titles for all tasks that have painterkanon_inquiry_offer
      const inquiryItemIds = assignedTasks
        .filter(task => task.painterkanon_inquiry_offer)
        .map(task => task.painterkanon_inquiry_offer);
      
      const inquiryTitlesMap = {};
      if (inquiryItemIds.length > 0) {
        try {
          const inquiries = await inquiry.findAll({
            attributes: ["item_id", "title", "first_name", "last_name"],
            where: { item_id: { [Op.in]: inquiryItemIds } },
          });
          
          inquiries.forEach((inq) => {
            const title = inq.title || 
              `${inq.first_name || ""} ${inq.last_name || ""}`.trim() || 
              null;
            inquiryTitlesMap[inq.item_id] = title;
          });
        } catch (error) {
          Log.error("Error fetching inquiry titles for painterkanon_inquiry_offer:", error);
        }
      }

      // Format tasks
      response.assigned.tasks = assignedTasks.map((task) => {
        const startDate = task.start_date
          ? dateTimeFormat(task.start_date, "day_month_year")
          : null;
        const endDate = task.end_date
          ? dateTimeFormat(task.end_date, "day_month_year")
          : null;

        // Format countryside
        let countrysideValue = "";
        const countrysideOptions = [
          "Hovedstaden",
          "Sjælland",
          "Fyn",
          "Sønderjylland",
          "Østjylland",
          "Vestjylland",
          "Nordjylland",
        ];
        if (task.countryside === 1) {
          countrysideValue = "Sjælland";
        } else if (task.countryside === 2) {
          countrysideValue = "Fyn";
        } else if (task.countryside === 3) {
          countrysideValue = "Jylland";
        }

        // Format product lines
        const formattedProductLines = task.productLines
          ? task.productLines.map((pl) => {
              return {
                id: pl.id,
                item_id: pl.item_id,
                name: pl.title || null, // Use title from product_line table
                description: pl.productRelation
                  ? pl.productRelation.standard_product_text
                  : null,
                section: pl.section || null, // Section value is already stored in DB
                price: pl.unit_price ? parseFloat(pl.unit_price) : 0,
                Rum: pl.room || null,
                Quantity: pl.quantity ? parseFloat(pl.quantity) : 0,
                total: pl.total ? parseFloat(pl.total) : 0,
                product: pl.product,
                inquiry: pl.inquiry,
                task: pl.task,
              };
            })
          : [];

        // Format files - group by field name
        // Initialize with all known image fields in correct order (based on db_fields.md)
        const formattedFiles = {
          "vedhaeft-billeder": [], // Vedhæft billeder (attach_images)
          "vedhaeft-billeder-from-billede-modtagelse": [], // Vedhæft billeder from Billede modtagelse (attach_images_received)
          "images-from-partners": [], // Billeder fra maler inden opstart (images_before_start)
          "billeder-mandag": [], // Billeder Mandag (images_monday)
          "billeder-tirsdag": [], // Billeder Tirsdag (images_tuesday)
          "billeder-onsdag": [], // Billeder Onsdag (images_wednesday)
          "billeder-torsdag": [], // Billeder Torsdag (images_thursday)
          "billeder-fredag": [], // Billeder Fredag (images_friday)
          "billeder-lordag": [], // Billeder Lørdag (images_saturday)
          "billeder-sondag": [], // Billeder Søndag (images_sunday)
          "vedhaeft-faktura": [], // Vedhæft Faktura (attach_invoice)
          "reklamations-billeder": [], // Reklamations billeder (complaint_images)
        };
        
        if (task.files && task.files.length > 0) {
          Log.info(`Processing ${task.files.length} files for task ${task.item_id}`);
          
          // Get storage path for file existence check
          const __filename = fileURLToPath(import.meta.url);
          const __dirname = dirname(__filename);
          const storagePath = path.join(__dirname, '../../storage/public/files');
          
          task.files.forEach((file) => {
            // Files are stored as: storage/public/files/{item_id}/{filename}
            // Handle filename that might already include path or just be the filename
            const filename = file.filename.startsWith('/') 
              ? file.filename 
              : `/${task.item_id}/${file.filename}`;
            const filePath = filename;
            
            // Check if file actually exists on disk
            const fullFilePath = path.join(storagePath, task.item_id.toString(), file.filename.startsWith('/') ? file.filename.replace(/^\//, '') : file.filename);
            const fileExists = fs.existsSync(fullFilePath);
            
            if (!fileExists) {
              Log.warn(`File not found on disk (database record exists but file missing):`, {
                file_id: file.file_id,
                filename: file.filename,
                expected_path: fullFilePath,
                item_id: task.item_id,
                keyword: file.keyword,
                field: file.field
              });
              // Skip this file - don't add it to formattedFiles
              return;
            }
            
            // Map files with keyword "customer_image" to "vedhaeft-billeder" field
            // Files synced with customer_image keyword don't have a field property set
            let fieldName = file.field || "unknown";
            if (file.keyword === "customer_image" && (!file.field || file.field === null)) {
              fieldName = "vedhaeft-billeder";
            }
            
            // Log files with vedhaeft-billeder-from-billede-modtagelse field for debugging
            if (fieldName === "vedhaeft-billeder-from-billede-modtagelse") {
              Log.info(`Found file with vedhaeft-billeder-from-billede-modtagelse field:`, {
                file_id: file.file_id,
                filename: file.filename,
                field: file.field,
                keyword: file.keyword,
                item_id: task.item_id
              });
            }
            
            // Initialize field if it doesn't exist (for any unknown fields)
            if (!formattedFiles[fieldName]) {
              formattedFiles[fieldName] = [];
            }
            
            // Construct full URL with APP_BASE_URL from environment
            const baseUrl = process.env.APP_BASE_URL || "";
            const previewUrl = baseUrl 
              ? `${baseUrl}/files${filePath}` 
              : `/files${filePath}`;
            
            formattedFiles[fieldName].push({
              file_id: file.file_id,
              filename: file.filename,
              field: fieldName, // Use the mapped field name
              previewUrl: previewUrl,
            });
          });
        } else {
          Log.info(`No files found for task ${task.item_id}, but vedhaeft-billeder-from-billede-modtagelse will still be included as empty array`);
        }

        // Get all task fields as plain object, excluding timestamps
        const taskData = task.get({ plain: true });
        delete taskData.created_at;
        delete taskData.updated_at;
        delete taskData.serviceManager;
        delete taskData.productLines;
        delete taskData.files;

        // Get inquiry title from map if available
        // If painterkanon_inquiry_offer is null/empty, set title to null
        const inquiryTitle = task.painterkanon_inquiry_offer && task.painterkanon_inquiry_offer !== null
          ? (inquiryTitlesMap[task.painterkanon_inquiry_offer] || null)
          : null;

        // Map user_item_id to partner field for frontend compatibility
        // Frontend expects partner to be an object with item_id or a number/string
        const partner = task.user_item_id 
          ? { item_id: task.user_item_id } 
          : null;

        return {
          ...taskData,
          title: `${task.customer_first_name || ""} ${task.customer_last_name || ""}`.trim(),
          unassigned: false,
          assignedBy: task.serviceManager ? task.serviceManager.name : null,
          startDate: startDate,
          endDate: endDate,
          countryside: {
            value: countrysideValue,
            options: countrysideOptions,
          },
          customer_address: task.customer_address || "",
          first_name: task.customer_first_name || "",
          last_name: task.customer_last_name || "",
          phone: task.customer_phone_number || "",
          email: task.customer_email || null,
          ProductLine: formattedProductLines,
          files: formattedFiles,
          painterkanon_inquiry_offer_title: inquiryTitle,
          partner: partner, // Add partner field mapped from user_item_id
        };
      });
    }

    // Build where condition for inquiries with filters
    let inquiryWhereCondition = {};
    if (officePartnerItemId) {
      inquiryWhereCondition.office_partner = officePartnerItemId;
    }

    // Apply date range filter if provided (for inquiries, use execution_date only - Udførelsesdato)
    // db_fields.md: Date :execution_date_start, execution_date_end
    if (filters.startDate && filters.endDate) {
      inquiryWhereCondition[Op.or] = [
        {
          execution_date_start: {
            [Op.between]: [
              dateTimeFormat(filters.startDate) + "T00:00:00.000Z",
              dateTimeFormat(filters.endDate) + "T23:59:59.999Z",
            ],
          },
        },
        {
          execution_date_end: {
            [Op.between]: [
              dateTimeFormat(filters.startDate) + "T00:00:00.000Z",
              dateTimeFormat(filters.endDate) + "T23:59:59.999Z",
            ],
          },
        },
      ];
    }

    // Apply service manager filter if provided (for inquiries)
    // db_fields.md: Service manager :assignedBy (maps to gk_service_manager in DB)
    // gk_service_manager stores the name (text), not ID, so we need to get the name from the service manager ID
    if (filters.service_managers && filters.service_managers.id) {
      const serviceManager = await User.findOne({
        attributes: ["name"],
        where: {
          item_id: filters.service_managers.id,
          type: 2, // Service manager type
        },
      });
      if (serviceManager && serviceManager.name) {
        inquiryWhereCondition.gk_service_manager = serviceManager.name;
      }
    }

    // Apply status filter if provided (for inquiries)
    // status field stores text value, not ID, so we need to use the name
    if (filters.inquiry_status && (filters.inquiry_status.name || filters.inquiry_status.value)) {
      inquiryWhereCondition.status = filters.inquiry_status.name || filters.inquiry_status.value;
    }

    // Apply lansdel filter if provided (for inquiries)
    // db_fields.md: lansdel :region
    // region field stores text (name) not ID, so we need to use the name
    // Handle both flat structure (filters.lansdel.name) and nested structure (filters.lansdel.value.name)
    if (filters.lansdel) {
      const lansdelName = filters.lansdel.name || (filters.lansdel.value && filters.lansdel.value.name);
      if (lansdelName) {
        inquiryWhereCondition.region = lansdelName;
      }
    }

    // Fetch inquiries assigned to the current office partner
    const allInquiries = await inquiry.findAll({
      attributes: {
        exclude: ["created_at", "updated_at"],
      },
      where: inquiryWhereCondition,
      include: [
        {
          model: productLine,
          as: "productLines",
          attributes: [
            "id",
            "item_id",
            "title",
            "product",
            "room",
            "section",
            "quantity",
            "unit_price",
            "total",
            "inquiry",
            "task",
          ],
          required: false,
          include: [
            {
              model: product,
              as: "productRelation",
              attributes: ["id", "item_id", "product_name", "standard_product_text", "status", "unit_price"],
              required: false,
            },
          ],
        },
        {
          model: files,
          attributes: ["file_id", "keyword", "filename", "field"],
          where: {
            keyword: "office_partner",
          },
          required: false,
        },
      ],
    });

    // Format inquiries
    response.assigned.inquiry = allInquiries.map((inq) => {
      // Use execution dates for listing (Udførelsesdato), not inspection dates
      const startDate = inq.execution_date_start
        ? dateTimeFormat(inq.execution_date_start, "day_month_year")
        : null;
      const endDate = inq.execution_date_end
        ? dateTimeFormat(inq.execution_date_end, "day_month_year")
        : null;

      // Format countryside/region
      let countrysideValue = "";
      const countrysideOptions = [
        "Hovedstaden",
        "Sjælland",
        "Fyn",
        "Sønderjylland",
        "Østjylland",
        "Vestjylland",
        "Nordjylland",
      ];
      if (inq.region) {
        // Map region to countryside if needed
        countrysideValue = inq.region;
      }

      // Format product lines
      const formattedProductLines = inq.productLines
        ? inq.productLines.map((pl) => {
            return {
              id: pl.id,
              item_id: pl.item_id,
              name: pl.title || null, // Use title from product_line table
              description: pl.productRelation
                ? pl.productRelation.standard_product_text
                : null,
              section: pl.section || null, // Section value is already stored in DB
              price: pl.unit_price ? parseFloat(pl.unit_price) : 0,
              Rum: pl.room || null,
              Quantity: pl.quantity ? parseFloat(pl.quantity) : 0,
              total: pl.total ? parseFloat(pl.total) : 0,
              product: pl.product,
              inquiry: pl.inquiry,
              task: pl.task,
            };
          })
        : [];

      // Format files - group by field name
      // Initialize with all known image fields to ensure they're always present
      const formattedFiles = {
        "vedhaeft-billeder": [],
        "vedhaeft-billeder-from-billede-modtagelse": [],
        "images-from-partners": [],
        "vedhaeft-faktura": [],
        "billeder-mandag": [],
        "billeder-tirsdag": [],
        "billeder-onsdag": [],
        "billeder-torsdag": [],
        "billeder-fredag": [],
        "billeder-lordag": [],
        "billeder-sondag": [],
      };
      
      if (inq.files && inq.files.length > 0) {
        Log.info(`Processing ${inq.files.length} files for inquiry ${inq.item_id}`);
        inq.files.forEach((file) => {
          // Files are stored as: storage/public/files/{item_id}/{filename}
          // Handle filename that might already include path or just be the filename
          const filename = file.filename.startsWith('/') 
            ? file.filename 
            : `/${inq.item_id}/${file.filename}`;
          const filePath = filename;
          const fieldName = file.field || "unknown";
          
          // Log files with vedhaeft-billeder-from-billede-modtagelse field for debugging
          if (fieldName === "vedhaeft-billeder-from-billede-modtagelse") {
            Log.info(`Found file with vedhaeft-billeder-from-billede-modtagelse field for inquiry:`, {
              file_id: file.file_id,
              filename: file.filename,
              field: file.field,
              item_id: inq.item_id
            });
          }
          
          // Initialize field if it doesn't exist (for any unknown fields)
          if (!formattedFiles[fieldName]) {
            formattedFiles[fieldName] = [];
          }
          
          // Construct full URL with APP_BASE_URL from environment
          const baseUrl = process.env.APP_BASE_URL || "";
          const previewUrl = baseUrl 
            ? `${baseUrl}/files${filePath}` 
            : `/files${filePath}`;
          
          formattedFiles[fieldName].push({
            file_id: file.file_id,
            filename: file.filename,
            field: file.field,
            previewUrl: previewUrl,
          });
        });
      } else {
        Log.info(`No files found for inquiry ${inq.item_id}, but vedhaeft-billeder-from-billede-modtagelse will still be included as empty array`);
      }

      // Get all inquiry fields as plain object, excluding timestamps
      const inquiryData = inq.get({ plain: true });
      delete inquiryData.created_at;
      delete inquiryData.updated_at;
      delete inquiryData.productLines;
      delete inquiryData.files;

      // Build inquiry text
      const inquiryText = `Contact: ${inq.phone_number || ""} | Address: ${inq.full_address || ""}`;

      return {
        ...inquiryData,
        title: inq.title || `${inq.first_name || ""} ${inq.last_name || ""}`.trim(),
        assignedBy: inq.gk_service_manager || null,
        startDate: startDate,
        endDate: endDate,
        inquiry: inquiryText,
        countryside: {
          value: countrysideValue,
          options: countrysideOptions,
        },
        customer_address: inq.full_address || "",
        first_name: inq.first_name || "",
        last_name: inq.last_name || "",
        phone: inq.phone_number || "",
        email: inq.email || "",
        ProductLine: formattedProductLines,
        files: formattedFiles,
      };
    });

    // Add filter data to response (for both tasks and inquiries)
    // Always return filter options, even if some are empty
    response.filters = {};
    response.filters.types = taskTypes || [];
    // Ensure service_managers is always an array, never null
    try {
      const serviceManagers = await getAllUsers(2);
      response.filters.service_managers = Array.isArray(serviceManagers) ? serviceManagers : [];
    } catch (error) {
      Log.error("Error fetching service managers:", error);
      response.filters.service_managers = [];
    }
    response.filters.lansdel = await getLandsdelColor(landsdel) || [];
    
    // Add task status filter options from categories.config.js
    // Values from db_fields.md: task_status
    // Always set, use empty array as fallback
    response.filters.task_status = (categories.tasks && categories.tasks.status) 
      ? categories.tasks.status 
      : [];
    
    // Add inquiry status filter options from categories.config.js
    // Values from db_fields.md: inquiry_status
    // Always set, use empty array as fallback
    response.filters.inquiry_status = (categories.inquiry && categories.inquiry.status) 
      ? categories.inquiry.status 
      : [];

    return response;
  } catch (error) {
    Log.error("Error in getOfficePartnerDashboard:", error);
    throw error;
  }
};

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists