contactcats/ang/crmContactcats.js
2025-04-02 11:58:27 +01:00

270 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function(angular, $, _) {
// Declare a list of dependencies.
angular.module("crmContactcats", CRM.angRequires("crmContactcats"));
angular.module("crmContactcats").component("crmContactCategorySettings", {
templateUrl: "~/crmContactcats/crmContactCategorySettings.html",
bindings: {
// things listed here become properties on the controller using
// values from attributes. @ means a fixed string is passed
// e.g. <crm-reset-password token='foo' />
// token: '@',
// & is special. In the child, call ctrl.onMyAction with an
// Object whose keys provide parameter names for the code in the parent.
// onMyAction: '&',
// '<' means one-way binding (parent»child) and I think is what you are
// supposed to use for components.
// v: '<'
},
controller: function($scope, $timeout, crmApi4, crmStatus, $document) {
var ts = ($scope.ts = CRM.ts(null)),
ctrl = this;
// this.$onInit gets run after the this controller is called, and after the bindings have been applied.
this.$onInit = async function() {
ctrl.dirty = 'pristine'; //pristine|dirty
ctrl.view = 'list';
ctrl.uxOrderField = 'execution_order';
ctrl.moveIdx = null;
ctrl.categoryToEdit = null;
ctrl.presentation_order = [];
ctrl.execution_order = [];
const blankDefn = {
id: 0,
execution_order: 10,
presentation_order: 10,
label:'',
search_type: 'search',
search_data: {},
color: '#666666',
icon: 'fa-circle-half-stroke',
description: '',
};
catDefs = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { execution_order: 'ASC' }, withLabels: true });
if (catDefs.count === 0) {
// First time.
catDefs = [];
}
// Ensure we have the minimum of what we need.
if (!catDefs.find(c => c.search_type === 'default')) {
console.log("Adding default category as was missing");
// There's no default one.
catDefs.push(Object.assign({}, blankDefn, {
execution_order: catDefs.length,
presentation_order: catDefs.length,
label: ts('Default'),
search_type: 'default',
icon: 'fa-person-circle-question',
description: ts('Any contact not matching any of the categories is assigned this category.'),
}));
}
// Ensure the fields are all present in the data.
catDefs.forEach((item, idx) => {
catDefs[idx] = Object.assign({}, blankDefn, item);
});
// Create ordered arrays.
['presentation_order', 'execution_order'].forEach(orderField => {
ctrl[orderField] = catDefs.slice();
ctrl[orderField].sort((a, b) => {
// console.log({orderField, aST: a.search_type, bST: b.search_type, aOrd:a[orderField], bOrd:b[orderField], aLab:a.label, bLab:b.label});
if (orderField === 'execution_order') {
// Enforce default at end for assignment order
if (a.search_type === 'default' && b.search_type !== 'default') return 1;
if (b.search_type === 'default' && a.search_type !== 'default') return -1;
}
if (a[orderField] > b[orderField]) return 1;
if (a[orderField] < b[orderField]) return -1;
if (a.label < b.label) return 1;
if (a.label > b.label) return -1;
return 0;
});
});
updateOrders();
$scope.$digest();
// Functions follow.
/**
* Copy execution to presenation or vice versa.
*/
ctrl.copyOrder = (from) => {
ctrl[ctrl.uxOrderField] = ctrl[from].slice();
updateOrders();
};
/**
* We set execution_order to the array index to keep things simpler.
*/
function updateOrders() {
// Update execution_order or presentation_order field to match the specified order.
ctrl[ctrl.uxOrderField].forEach((item, idx) => {
item[ctrl.uxOrderField] = idx;
});
}
/**
* Begin editing something
*/
ctrl.edit = idx => {
if (idx === -1) {
// New item.
// Create a random dark colour
let [r, g, b] = [Math.random(), Math.random(), Math.random()],
min = Math.min(r, g, b),
range = Math.max(r, g, b) - min,
conv = (x) => ('0' + Math.ceil((x-min)/range * 128).toString(16)).replace(/^.*(..)$/, '$1');
// Create a blank
ctrl.categoryToEdit = Object.assign({}, blankDefn, {
execution_order: -1,
presentation_order: -1,
search_type: 'search',
search_data: { saved_search_id: null },
color: '#' + [r, g, b].map(conv).join(''),
});
}
else {
ctrl.categoryToEdit = Object.assign({}, JSON.parse(JSON.stringify(catDefs[idx])));
}
ctrl.view = 'edit';
};
/**
* End editing something
*/
ctrl.updateEditedThing = () => {
const edited = ctrl.categoryToEdit;
// @todo validate, e.g.
if (!edited.label) {
alert("No name");
return;
}
if (edited.execution_order === -1) {
// This is a new category, we need to insert it before the default one.
const newIdx = ctrl.execution_order.length - 1;
edited.presentation_order = newIdx;
edited.execution_order = newIdx;
ctrl.execution_order.splice(newIdx, 0, edited);
ctrl.presentation_order.splice(newIdx, 0, edited);
}
else {
console.log("Update:", ctrl.uxOrderField);
const targetCat = ctrl[ctrl.uxOrderField][edited[ctrl.uxOrderField]];
Object.keys(blankDefn).forEach(k => {
console.log("Setting", k, "to ", edited[k], 'on item', targetCat);
targetCat[k] = edited[k];
});
}
ctrl.categoryToEdit = null;
updateOrders();
ctrl.dirty = 'dirty';
ctrl.view = 'list';
}
ctrl.canMoveBelow = function(row, idx) {
// Only show move buttons when moving.
if (ctrl.moveIdx === null) return false;
if (ctrl.uxOrderField === 'execution_order') {
// The default MUST be the last item in execution_order.
if (row.search_type === 'default') return false;
}
return (idx + 1 < ctrl.moveIdx || idx > ctrl.moveIdx);
};
/**
* Move a category within the currently selected order.
*/
ctrl.moveTo = idx => {
let item = ctrl[ctrl.uxOrderField].splice(ctrl.moveIdx, 1)[0];
if (idx > ctrl.moveIdx) {
idx--;
}
ctrl[ctrl.uxOrderField].splice(idx, 0, item);
ctrl.moveIdx = null;
updateOrders();
ctrl.dirty = 'dirty';
};
/**
* Mark a category for deletion.
*/
ctrl.deleteCategory = idx => {
if (!confirm(ts(
'Confirm deleting category %1. You will lose history related to this category. Sure?',
{1: catDefs[idx].label}
))) {
return;
}
ctrl[ctrl.uxOrderField][idx].deleted = true;
ctrl.categoryToEdit = null;
updateOrders();
ctrl.view = 'list';
ctrl.dirty = 'dirty';
};
/**
* We need to ensure the search_data object contains the
* expected fields and no more.
*/
ctrl.fixSearchData = () => {
const search_data = ctrl.categoryToEdit.search_data;
if (ctrl.categoryToEdit.search_type === 'group') {
Object.assign(search_data, {group_id: null});
let {group_id} = search_data;
ctrl.categoryToEdit.search_data = {group_id};
}
else if (ctrl.categoryToEdit.search_type === 'search') {
Object.assign(search_data, {saved_search_id: null});
let {saved_search_id} = search_data;
ctrl.categoryToEdit.search_data = {saved_search_id};
}
};
ctrl.loadFundraisingScheme = async () => {
if (!confirm(ts("This will DELETE all your categories and REPLACE them with a shipped set of fundraising focussed searches. Continue?"))) {
return;
}
await crmApi4('ContactCategoryDefinition', 'setup', {scheme: 'ForumZFDFundraising1'});
window.location.reload();
};
ctrl.save = () => {
if (!confirm(ts("Confirm saving changes to categories? Note that categories will not be fully applied until tomorrow."))) { return; }
const records = ctrl[ctrl.uxOrderField];
// Handle deletions first.
const deletedIds = records.filter(d => d.deleted && d.id > 0).map(d => d.id);
const chain = Promise.resolve();
if (deletedIds.length) {
chain.then(() => crmApi4('ContactCategoryDefinition', 'delete', { where: [ ['id', 'IN', deletedIds] ] }));
}
// Now enact the deletions on our local models
ctrl.presentation_order = ctrl.presentation_order.filter(d => !d.deleted);
ctrl.execution_order = ctrl.execution_order.filter(d => !d.deleted);
updateOrders();
if (ctrl.presentation_order.length) {
// Save if anything to save.
chain.then(() => crmApi4('ContactCategoryDefinition', 'save', {records}));
}
chain.then(() => {
ctrl.dirty = 'pristine';
})
crmStatus({ start: ts('Saving...'), success: ts('Saved')}, chain);
};
};
// this.$onChange = function(changes) {
// // changes is object keyed by name what '<' binding changed in
// // the parent (e.g. 'v'), and value is another obj with keys
// // something like previous, new, ...?...
// };
}
});
})(angular, CRM.$, CRM._);