Duffer Derek
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