var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { RegisterClass, UUIDsEqual } from "@memberjunction/global";
import { Metadata, BaseEntity, BaseInfo, RunView, EntityFieldTSType, EntityPermissionType, BaseEntityResult } from "@memberjunction/core";
import { MJUserViewEntity, } from "../generated/entity_subclasses.js";
import { ResourcePermissionEngine } from "./ResourcePermissions/ResourcePermissionEngine.js";
let MJUserViewEntityExtended = class MJUserViewEntityExtended extends MJUserViewEntity {
    constructor() {
        super(...arguments);
        this._ViewEntityInfo = null;
        this._cachedCanUserEdit = null;
        this._cachedCanUserView = null;
        this._cachedUserCanDelete = null;
        this._ViewResourceTypeID = null;
    }
    /**
     * This is a read-only property that returns the filters for this view. This information
     * is persisted in a JSON format in the FilterState column of the MJUserViewEntity table. To access
     * the filters easily, use this property.
     * @readonly
     * @type {ViewFilterInfo[]}
     * @memberof UserViewEntitySubclass
     */
    get Filter() {
        if (this.FilterState) {
            return [new ViewFilterInfo(JSON.parse(this.FilterState))];
        }
        else
            return [];
    }
    /**
     * This is a read-only property that returns the columns for this view. This information
     * is persisted in a JSON format in the GridState column of the MJUserViewEntity table. To access
     * the columns easily, use this property.
     */
    get Columns() {
        // now, we need to do some post-processing once we've loaded the raw data so that our 
        // columns and filters are set up correctly
        if (this.GridState) {
            const gridState = JSON.parse(this.GridState);
            if (gridState && gridState.columnSettings) {
                const columns = gridState.columnSettings.map(c => {
                    // find the entity field and put it in place inside the View Metadata for easy access
                    if (c) {
                        // check to make sure the current item is non-null to ensure metadata isn't messed up 
                        const field = this.ViewEntityInfo.Fields.find(f => f.Name.trim().toLowerCase() === c.Name.trim().toLowerCase());
                        return new ViewColumnInfo({ ...c, EntityField: field });
                    }
                    else {
                        console.log('null column setting found in view grid state for columns - ViewID: ' + this.ID);
                    }
                });
                return columns;
            }
        }
        // if we get here, we don't have column info, return an empty array
        return [];
    }
    /**
     * The entity info for the entity that this view is based on
     * @readonly
     * @type {EntityInfo}
     * @memberof UserViewEntitySubclass
     */
    get ViewEntityInfo() {
        if (!this._ViewEntityInfo && this.EntityID) {
            // Lazily resolve when _ViewEntityInfo hasn't been set yet.
            // This can happen when the entity is loaded via InnerLoad() → SetMany() → SetLocal(),
            // which bypasses both the Load() and LoadFromData() overrides that normally set this.
            const md = this.ProviderToUse;
            this._ViewEntityInfo = md.Entities.find(e => UUIDsEqual(e.ID, this.EntityID));
        }
        return this._ViewEntityInfo;
    }
    get ViewSortInfo() {
        if (this.SortState) {
            const sortTemp = JSON.parse(this.SortState);
            if (sortTemp && sortTemp.length > 0)
                return sortTemp.map(s => new ViewSortInfo(s));
        }
        // if we get here return a blank array
        return [];
    }
    get OrderByClause() {
        if (this.ViewSortInfo && this.ViewSortInfo.length > 0) {
            return this.ViewSortInfo.map(s => {
                let dir;
                if (typeof s.direction === 'string')
                    dir = s.direction.trim().toLowerCase();
                else if (s.direction === 1) // some legacy view metadata has 1/2 for asc/desc
                    dir = 'asc';
                else if (s.direction === 2)
                    dir = 'desc';
                else
                    dir = '';
                const desc = dir === ViewSortDirectionInfo.Desc.trim().toLowerCase();
                return s.field + (desc ? ' DESC' : '');
            }).join(', ');
        }
        // Fall back to GridState.sortSettings when SortState is empty.
        // Sort may be stored only in GridState if the view was configured via the
        // grid UI before SortState sync was added.
        return this.buildOrderByFromGridState();
    }
    /**
     * Attempts to build an ORDER BY clause from GridState.sortSettings.
     * Returns an empty string if GridState has no sort settings.
     */
    buildOrderByFromGridState() {
        if (!this.GridState)
            return '';
        try {
            const gridState = JSON.parse(this.GridState);
            if (gridState.sortSettings && gridState.sortSettings.length > 0) {
                return gridState.sortSettings
                    .map(s => s.field + (s.dir === 'desc' ? ' DESC' : ''))
                    .join(', ');
            }
        }
        catch {
            // Invalid GridState JSON — ignore
        }
        return '';
    }
    /**
     * Get the parsed DisplayState object from the DisplayState JSON column.
     * DisplayState contains view mode preferences and mode-specific configuration (e.g., timeline settings).
     * @returns The parsed ViewDisplayState object or null if not set/invalid
     */
    get ParsedDisplayState() {
        if (this.DisplayState) {
            try {
                return JSON.parse(this.DisplayState);
            }
            catch (e) {
                console.warn('Failed to parse DisplayState JSON:', e);
                return null;
            }
        }
        return null;
    }
    /**
     * Set the DisplayState from a ViewDisplayState object.
     * @param state The ViewDisplayState object to serialize and store
     */
    setDisplayState(state) {
        this.DisplayStateObject = state;
    }
    /**
     * Get the default view mode from DisplayState, or 'grid' if not set.
     */
    get DefaultViewMode() {
        return this.ParsedDisplayState?.defaultMode || 'grid';
    }
    /**
     * Get the timeline configuration from DisplayState.
     * @returns TimelineState or null if not configured
     */
    get TimelineConfig() {
        return this.ParsedDisplayState?.timeline || null;
    }
    /**
     * Update timeline configuration in DisplayState.
     * Creates DisplayState if it doesn't exist.
     * @param config The timeline configuration to set
     */
    setTimelineConfig(config) {
        const current = this.ParsedDisplayState || { defaultMode: 'grid' };
        current.timeline = config;
        this.setDisplayState(current);
    }
    /**
     * Check if a specific view mode is enabled.
     * By default, all modes are enabled unless explicitly disabled.
     * @param mode The view mode to check
     */
    isViewModeEnabled(mode) {
        const enabledModes = this.ParsedDisplayState?.enabledModes;
        if (!enabledModes)
            return true; // All modes enabled by default
        return enabledModes[mode] !== false;
    }
    /**
     * Get the best date field for timeline display based on entity metadata.
     * Priority: 1) DefaultInView=true date fields (lowest Sequence wins)
     *           2) Any date field (lowest Sequence wins)
     * @returns The field name or null if no date fields exist
     */
    getDefaultTimelineDateField() {
        if (!this._ViewEntityInfo)
            return null;
        const dateFields = this._ViewEntityInfo.Fields.filter(f => f.TSType === EntityFieldTSType.Date);
        if (dateFields.length === 0)
            return null;
        // Priority 1: DefaultInView date fields, sorted by Sequence
        const defaultInViewDateFields = dateFields
            .filter(f => f.DefaultInView)
            .sort((a, b) => a.Sequence - b.Sequence);
        if (defaultInViewDateFields.length > 0) {
            return defaultInViewDateFields[0].Name;
        }
        // Priority 2: Any date field, sorted by Sequence
        const sortedDateFields = dateFields.sort((a, b) => a.Sequence - b.Sequence);
        return sortedDateFields[0].Name;
    }
    /**
     * Get all date fields available for timeline configuration.
     * @returns Array of field info objects for date fields
     */
    getAvailableDateFields() {
        if (!this._ViewEntityInfo)
            return [];
        return this._ViewEntityInfo.Fields.filter(f => f.TSType === EntityFieldTSType.Date).sort((a, b) => a.Sequence - b.Sequence);
    }
    async LoadFromData(data) {
        // in this case we need to make sure we ge the _ViewEntityInfo property set up correctly
        if (data && data.EntityID) {
            const md = this.ProviderToUse;
            const match = md.Entities.find(e => UUIDsEqual(e.ID, data.EntityID));
            if (match) {
                this._ViewEntityInfo = match;
            }
            else {
                throw new Error('Unable to find entity info for entity ID: ' + data.EntityID);
            }
        }
        return await super.LoadFromData(data);
    }
    /**
     * This property determines if the specified user can edit the view object. All of the below assumes the user has base Create/Update permissions on the "MJ: User Views" entity.
     * The flow of the logic is:
     *  1) The view is a new record, by definition the user can edit this because it is new
     *  2) The user is the owner of the current view - e.g. UserID in the view record matches the ID of the user provided, allowed
     *  3) The user is a sysadmin (Type === 'Owner' in the User object), allowed
     *  4) If neither of the above conditions are met, the Resource Permissions are checked to see if the user, directly or via Roles, has either Edit or Owner permissions on the current view
     * @returns
     */
    get UserCanEdit() {
        if (this._cachedCanUserEdit === null) {
            this._cachedCanUserEdit = this.CalculateUserCanEdit();
        }
        return this._cachedCanUserEdit;
    }
    /**
     * This property determines if the specified user can view the view at all.
     */
    get UserCanView() {
        if (this._cachedCanUserView === null) {
            this._cachedCanUserView = this.CalculateUserCanView();
        }
        return this._cachedCanUserView;
    }
    CalculateUserCanView() {
        const md = this.ProviderToUse;
        // Prefer the context user (set on server-side / per-request rendering) over the global
        // current user, consistent with CalculateUserCanEdit/CalculateUserCanDelete. Without this,
        // server-side view-list filtering would evaluate visibility against the wrong user.
        const user = this.ContextCurrentUser || md.CurrentUser;
        const bOwner = UUIDsEqual(this.UserID, user.ID);
        if (bOwner) {
            return true;
        }
        else {
            // not the owner, let's see if the user has permissions or not.
            // Resolve the resource type via ViewResourceTypeID (which matches on .Entity ===
            // 'MJ: User Views'), consistent with UserCanEdit/UserCanDelete below. Matching on
            // .Name === 'MJ: User Views' was a bug: the seeded ResourceType.Name is 'User Views'
            // (the 'MJ: ' prefixed value lives on .Entity), so the lookup never matched and threw.
            // That throw propagated out of UserViewEngine.GetAccessibleViewsForEntity()'s
            // `.filter(v => v.UserCanView)` — aborting the whole filter and leaving the view
            // selector empty ("No saved views yet") whenever an entity had any view shared by
            // another user, taking the current user's own views down with it.
            const permLevel = ResourcePermissionEngine.Instance.GetUserResourcePermissionLevel(this.ViewResourceTypeID, this.ID, user);
            if (permLevel) // any permission level allows view access
                return true;
            else // perm level not found so return false
                return false;
        }
    }
    /**
     * This property determines if the specified user can delete the view object. All of the below assumes the user has base Delete permissions on the "MJ: User Views" entity.
     * The flow of the logic is:
     *  1) The view is a new record, by definition the user can't delete this because it is new
     *  2) The user is the owner of the current view - e.g. UserID in the view record matches the ID of the user provided, allowed
     *  3) The user is a sysadmin (Type === 'Owner' in the User object), allowed
     *  4) If neither of the above conditions are met, the Resource Permissions are checked to see if the user, directly or via Roles, has OWNER permissions on the current view
     */
    get UserCanDelete() {
        if (this._cachedUserCanDelete === null) {
            this._cachedUserCanDelete = this.CalculateUserCanDelete();
        }
        return this._cachedUserCanDelete;
    }
    ResetCachedCanUserSettings() {
        this._cachedCanUserEdit = null;
        this._cachedUserCanDelete = null;
        this._cachedCanUserView = null;
    }
    CalculateUserCanDelete() {
        if (!this.IsSaved)
            return false; // new records can't be deleted
        else {
            // EXISTING record in the database
            // check to see if the current user is the OWNER of this view via the UserID property in the record, if there's a match, the user OWNS this views
            const md = this.ProviderToUse;
            const user = this.ContextCurrentUser || md.CurrentUser; // take the context current user if it is set, otherwise use the global current user
            if (UUIDsEqual(this.UserID, user.ID) || user.Type.trim().toLowerCase() === 'owner') {
                return this.CheckPermissions(EntityPermissionType.Delete, false); // exsiting records OWNED by current user, can be edited so long as we have Update permissions;
            }
            else {
                // if the user is not an admin, and they are NOT the owner of the view, we check the permissions on the resource
                const perms = ResourcePermissionEngine.Instance.GetUserResourcePermissionLevel(this.ViewResourceTypeID, this.ID, user);
                return perms === 'Owner'; // this is the only level that can delete a view
            }
        }
    }
    CalculateUserCanEdit() {
        if (!this.IsSaved) {
            return this.CheckPermissions(EntityPermissionType.Create, false); // new records an be edited so long as we have Create permissions
        }
        else {
            // EXISTING record in the database
            // check to see if the current user is the OWNER of this view via the UserID property in the record, if there's a match, the user OWNS this views
            // so of course they can save it
            const md = this.ProviderToUse;
            const user = this.ContextCurrentUser || md.CurrentUser; // take the context current user if it is set, otherwise use the global current user
            if (UUIDsEqual(this.UserID, user.ID) || user.Type.trim().toLowerCase() === 'owner') {
                return this.CheckPermissions(EntityPermissionType.Update, false); // exsiting records OWNED by current user, can be edited so long as we have Update permissions;
            }
            else {
                // if the user is not an admin, and they are NOT the owner of the view, we check the permissions on the resource
                const perms = ResourcePermissionEngine.Instance.GetUserResourcePermissionLevel(this.ViewResourceTypeID, this.ID, user);
                return perms === 'Owner' || perms === 'Edit'; // these are the only two levels that can save a view
            }
        }
    }
    /**
     * Returns the ID of the Resource Type metadata record that corresponds to the User Views entity
     */
    get ViewResourceTypeID() {
        if (!this._ViewResourceTypeID) {
            const rt = ResourcePermissionEngine.Instance.ResourceTypes;
            const rtUV = rt.find(r => r.Entity === 'MJ: User Views');
            if (!rtUV)
                throw new Error('Unable to find Resource Type for User Views entity');
            this._ViewResourceTypeID = rtUV.ID;
        }
        return this._ViewResourceTypeID;
    }
    async Load(ID, EntityRelationshipsToLoad) {
        // first load up the view info, use the superclass to do this
        const result = await super.Load(ID, EntityRelationshipsToLoad);
        if (result) {
            const md = this.ProviderToUse;
            // first, cache a copy of the entity info for the entity that is used in this view
            const match = md.Entities.find(e => UUIDsEqual(e.ID, this.EntityID));
            if (match)
                this._ViewEntityInfo = match;
            else
                throw new Error('Unable to find entity info for entity ID: ' + this.EntityID);
        }
        this.ResetCachedCanUserSettings();
        return result;
    }
    async Delete() {
        if (this.UserCanDelete) {
            // if we get here, the user can delete the view, so we delete it
            if (super.Delete()) {
                this.ResetCachedCanUserSettings();
                return true;
            }
            else
                return false;
        }
        else {
            // if we get here, the user can't delete the view, so we don't delete it, add a last error and return false
            const res = new BaseEntityResult();
            res.Success = false;
            res.Message = 'User does not have permission to delete this view';
            res.StartedAt = new Date();
            res.EndedAt = new Date();
            this.RegisterResultHistoryEntry(res);
            return false;
        }
    }
    async Save(options) {
        if (this.UserCanEdit) {
            // we want to preprocess the Save() call because we need to regenerate the WhereClause in some situations
            const id = this.ID;
            const filterStateField = this.Fields.find(c => c.Name.toLowerCase() == 'filterstate');
            const smartFilterEnabledField = this.Fields.find(c => c.Name.toLowerCase() == 'smartfilterenabled');
            const smartFilterPromptField = this.Fields.find(c => c.Name.toLowerCase() == 'smartfilterprompt');
            if (!this.ID ||
                options?.IgnoreDirtyState ||
                filterStateField?.Dirty ||
                smartFilterEnabledField?.Dirty ||
                smartFilterPromptField?.Dirty) {
                // either we're ignoring dirty state or the filter state is dirty, so we need to update the where clause
                await this.UpdateWhereClause(options?.IgnoreDirtyState);
            }
            // now call our superclass to do the actual save()
            if (await super.Save(options)) {
                this.ResetCachedCanUserSettings();
                return true;
            }
            else
                return false;
        }
        else {
            // if we get here, the user can't edit the view, so we don't save it, add a last error and return false
            const res = new BaseEntityResult();
            res.Success = false;
            res.Message = this.ID ? 'User does not have permission to edit this view' : 'User does not have permission to create a new view';
            res.StartedAt = new Date();
            res.EndedAt = new Date();
            this.RegisterResultHistoryEntry(res);
            return false;
        }
    }
    async SetDefaultsFromEntity(e) {
        this.EntityID = e.ID;
        const newGridState = new ViewGridState();
        newGridState.columnSettings = [];
        e.Fields.filter(f => f.DefaultInView).forEach(f => {
            newGridState.columnSettings.push({
                ID: f.ID,
                DisplayName: f.DisplayName,
                Name: f.Name,
                hidden: false,
                width: f.DefaultColumnWidth,
                orderIndex: newGridState.columnSettings.length
            });
        });
        this.GridStateObject = newGridState; // default columns for a view are the ones with DefaultInView turned on
    }
    NewRecord() {
        const result = super.NewRecord();
        if (result) {
            if (this.ContextCurrentUser) {
                this.UserID = this.ContextCurrentUser.ID;
            }
            else {
                const md = this.ProviderToUse;
                if (md.CurrentUser)
                    this.UserID = md.CurrentUser.ID;
                else
                    throw new Error('Unable to determine current user for new view record');
            }
            this.Name = '';
            this.IsShared = false;
            this.IsDefault = false;
            this.WhereClause = '';
            this.Description = '';
            this.FilterStateObject = { logic: 'and', filters: [] }; // blank default for filter
            this.GridStateObject = {}; // blank object initially
            this.CustomFilterState = false;
            this.CustomWhereClause = false;
            //this.SmartFilterEnabled = false;
        }
        this.ResetCachedCanUserSettings();
        return result;
    }
    /**
     * This method is used to update the view's where clause based on the following logic.
     * 1) If the view has a regular Filter State (which is typically set by an end-user in a UI), the FilterState will be processed and a WHERE clause will be generated
     * 2) If SmartFilterEnabled === 1 and the view has a SmartFilterPrompt, the SmartFilterPrompt will be processed by AI and the SmartFilterWhereClause will be generated. SmartFilterWhereClause will only be generated whenever the SmartFilterPrompt changes.
     * 3) If CustomWhereClause === 1, this function will NOT modify the WhereClause because the sysadmin has set CustomWhereClause === 1 which means we don't want any changes to this particular view's WhereClause
     * IMPORTANT NOTE: This method does not save the record. You still call .Save() to save the record as desired. If you want to get the new WhereClause based on the FilterState but not
     * update the FilterState column, call the GenerateWhereClause() method.
     * KEY ASSUMPTION: The server code must set a property in the MJGlobal.Properties array with a key of OPENAI_API_KEY to use the AI functionality. If this property is not set, the AI functionality will not work.
     */
    async UpdateWhereClause(ignoreDirtyState) {
        if (this.CustomWhereClause && (this.CustomWhereClause === true || this.CustomWhereClause === 1))
            // if the CustomWhereClause is set to true or 1, we don't want to update the WhereClause
            return;
        // if we get here, we need to update the WhereClause, first check to see if we have a Smart Filter or not
        if (this.SmartFilterEnabled && (this.SmartFilterEnabled === true || this.SmartFilterEnabled === 1) &&
            this.SmartFilterPrompt && this.SmartFilterPrompt.length > 0) {
            if (this.SmartFilterImplemented) {
                // The following block of code is only intended to execute when we're in an execution context where the 
                // lowest level sub-class supports SmartFilter WHere Clause generation, which is determined by the class
                // implementation by overriding the SmartFilterImplemented property and returning true
                // So, if we're here we handle the Smart Filter                
                // we have a smart filter prompt (e.g. a prompt for the AI to create the where clause)
                // if the SmartFilterPrompt has changed, then we need to update the SmartFilterWhereClause using AI
                // otherwise, we don't need to do anything other than just use the SmartFilterWhereClause as it is
                if (!this.ID || ignoreDirtyState || this.Fields.find(c => c.Name.toLowerCase() == 'smartfilterprompt')?.Dirty) {
                    // the prompt has changed (or is newly populated, either way it is dirty) so use the AI to figure this out
                    const result = await this.GenerateSmartFilterWhereClause(this.SmartFilterPrompt, this.ViewEntityInfo);
                    this.SmartFilterWhereClause = result.whereClause;
                    this.SmartFilterExplanation = result.userExplanation;
                }
                // now that we have the SmartFilterWhereClause, we need to update the WhereClause property
                this.WhereClause = this.SmartFilterWhereClause;
            }
            else {
                // while we do have smart filter in this view and the prompt is populated and might be dirty/etc, we don't have a sub-class that supports smart filter
                // so we do NOTHING here. The idea is that this code will execute again on the SERVER side where the sub-class will be able to handle the smart filter
                // and properly generate the SmartFilterWhereClause.
            }
        }
        else {
            this.WhereClause = this.GenerateWhereClause(this.FilterState, this.ViewEntityInfo);
        }
    }
    /**
     * This is a stub method - the intent is for the server-only sub-class to override this method and implement the AI functionality to generate a where clause based on the prompt provided
     * @param prompt - string from the end user describing the filter they want to apply
     * @param entityInfo - entity info for the entity that the view is based on
     */
    async GenerateSmartFilterWhereClause(prompt, entityInfo) {
        return { whereClause: '', userExplanation: '' }; // stub function returns blank where clause. Sub-Class will do this.
    }
    /**
     * This is a stub method that always returns false - the intent is for the server-only sub-class to override this and return true if it supprots smart filters and then the rest of the smart filter
     * infrastructure will be enabled from the UpdateWhereClause() method.
     */
    get SmartFilterImplemented() {
        return false; // stub function returns false. Sub-Class will do this.
    }
    Set(FieldName, Value) {
        // call the superclass first and set the value internally there
        super.Set(FieldName, Value);
        if (FieldName.toLowerCase() == 'entityid') {
            // we're updating the entityID, need to upate the _ViewEntityInfo property so it is always in sync
            const md = this.ProviderToUse;
            const match = md.Entities.find(e => UUIDsEqual(e.ID, Value));
            if (match)
                this._ViewEntityInfo = match;
            else
                throw new Error('Unable to find entity info for entity ID: ' + Value);
        }
    }
    /**
     * Create a where clause for SQL from the structured filter state JSON information
     * @param FilterState A string containing a valid Filter State JSON string - this uses the format that the Kendo Filter component uses which is generic and can be used anywhere
     * with/without kendo
     * @param EntityInfo The entity info for the entity that the UserView is based on
     * @returns a string that represents a valid SQL WHERE clause
     * @memberof UserViewEntitySubclass
     * @example Example Filter State JSON below
        FilterState = `{
          "logic": "or",
          "filters": [{
            "field": "Name",
            "operator": "startswith",
            "value": "A"
          }, {
            "logic": "or",
            "filters": [{
              "field": "TotalRevenue",
              "operator": "gt",
              "value": 10000000
            }, {
              "field": "NumberEmployees",
              "operator": "gte",
              "value": 25
            }, {
              "field": "InformationTechnologyExpense",
              "operator": "gte",
              "value": 500000
            }, {
              "logic": "and",
              "filters": [{
                "field": "City",
                "operator": "eq",
                "value": "Chicago"
              }, {
                "field": "ActivityCount",
                "operator": "gte",
                "value": 5
              }]
            }]
          }, {
            "field": "LatestActivityDate",
            "operator": "gte",
            "value": "2023-01-01T06:00:00.000Z"
          }]
        }`;
     */
    GenerateWhereClause(FilterState, entityInfo) {
        return this.processFilterGroup(JSON.parse(FilterState), entityInfo);
    }
    wrapQuotes(value, needQuotes) {
        return needQuotes ? `'${value}'` : value;
    }
    convertFilterToSQL(field, operator, value, entity) {
        let op = '';
        const f = entity.Fields.find((f) => f.Name.trim().toLowerCase() === field.trim().toLowerCase());
        if (!f)
            throw new Error('Unable to find field ' + field + ' in entity ' + entity.Name);
        let newValue = value;
        if (f.TSType === EntityFieldTSType.Boolean) {
            if (typeof value === 'boolean') {
                newValue = value ? '1' : '0';
            }
            else if (typeof value === 'string') {
                newValue = value.trim().toLowerCase() === 'true' ? '1' : '0';
            }
            else {
                // Handle numbers, null, undefined, etc.
                newValue = value ? '1' : '0';
            }
        }
        switch (operator) {
            case 'eq':
                op = '= ' + this.wrapQuotes(newValue, f.NeedsQuotes);
                break;
            case 'neq':
                op = '<> ' + this.wrapQuotes(newValue, f.NeedsQuotes);
                break;
            case 'gt':
                op = '> ' + this.wrapQuotes(newValue, f.NeedsQuotes);
                break;
            case 'gte':
                op = '>= ' + this.wrapQuotes(newValue, f.NeedsQuotes);
                break;
            case 'lt':
                op = '< ' + this.wrapQuotes(newValue, f.NeedsQuotes);
                break;
            case 'lte':
                op = '<= ' + this.wrapQuotes(newValue, f.NeedsQuotes);
                break;
            case 'startswith':
                op = `LIKE '${newValue}%'`;
                break;
            case 'endswith':
                op = `LIKE '%${newValue}'`;
                break;
            case 'contains':
                op = `LIKE '%${newValue}%'`;
                break;
            case 'doesnotcontain':
                op = `NOT LIKE '%${newValue}%'`;
                break;
            case 'isnull':
            case 'isempty':
                op = 'IS NULL';
                break;
            case 'isnotnull':
            case 'isnotempty':
                op = 'IS NOT NULL';
                break;
        }
        return `[${field}] ${op}`;
    }
    processFilterGroup(filterGroup, entity) {
        // each filter has two properties, logic and filters
        // logic is either 'and' or 'or' and is what we use to determine the SQL logic operator
        // filters is an array of filters, each filter has a field, operator, and value,
        let whereClause = '';
        let bFirst = true;
        const logic = filterGroup.logic.toUpperCase();
        for (const filter of filterGroup.filters) {
            if (!bFirst)
                whereClause += ` ${logic} `;
            else
                bFirst = false;
            // if an individual filter has a "logic" property, it's a group and we need to process it with parenthesis and recurisely
            if (filter.logic && filter.logic.length > 0) {
                // this is a group, we process it with parenthesis
                whereClause += `(${this.processFilterGroup(filter, entity)})`;
            }
            else {
                // this is an individual filter, easy to process
                whereClause += `(${this.convertFilterToSQL(filter.field, filter.operator, filter.value, entity)})`;
            }
        }
        return whereClause;
    }
};
MJUserViewEntityExtended = __decorate([
    RegisterClass(BaseEntity, 'MJ: User Views')
], MJUserViewEntityExtended);
export { MJUserViewEntityExtended };
export class ViewInfo {
    /**
     * Returns a list of views for the specified user. If no user is specified, the current user is used.
     * @param contextUser optional, the user to use for context when loading the view
     * @param entityId optional, the entity ID to filter the views by, if not provided, there is no filter on EntityID and all views for the user are returned
     * @returns an array of UserViewEntityBase objects
     * @memberof ViewInfo
     * @static
     * @async
     */
    static async GetViewsForUser(entityId, contextUser) {
        const rv = new RunView();
        const md = new Metadata(); // global-provider-ok: utility helper — single-provider context
        const result = await rv.RunView({
            EntityName: 'MJ: User Views',
            ExtraFilter: `UserID = '${contextUser ? contextUser.ID : md.CurrentUser.ID}'
                         ${entityId ? ` AND EntityID = '${entityId}'` : ''}`
        });
        const rd = result?.Results;
        if (result && result.Success && rd)
            return rd;
    }
    /**
     * Returns a view ID for a given viewName
     * @param viewName Name of the view to lookup the ID for
     * @returns the ID of the User View record that matches the provided name, if found
     */
    static async GetViewID(viewName) {
        const rv = new RunView();
        const result = await rv.RunView({ EntityName: 'MJ: User Views', ExtraFilter: `Name = '${viewName}'` });
        const rd = result?.Results;
        if (result && result.Success && rd && rd.length > 0) {
            return rd[0].ID;
        }
        else {
            throw new Error('Unable to find view with name: ' + viewName);
        }
    }
    /**
     * Gets a User View entity from the UserViewEngine cache.
     * @param viewId record ID for the view to load
     * @param contextUser optional, the user context for loading views
     * @returns UserViewEntityBase (or a subclass of it)
     * @throws Error if the view is not found in the engine cache
     * @memberof ViewInfo
     * @static
     * @async
     */
    static async GetViewEntity(viewId, contextUser) {
        const { UserViewEngine } = await import('../engines/UserViewEngine.js');
        // Ensure the engine is configured before use
        await UserViewEngine.Instance.Config(false, contextUser);
        const view = UserViewEngine.Instance.GetViewById(viewId);
        if (view) {
            return view;
        }
        throw new Error('Unable to find view with ID: ' + viewId);
    }
    /**
     * Gets a User View entity from the UserViewEngine cache by name.
     * @param viewName name for the view to load
     * @param contextUser optional, the user context for loading views
     * @returns UserViewEntityBase (or a subclass of it)
     * @throws Error if the view is not found in the engine cache
     * @memberof ViewInfo
     * @static
     * @async
     */
    static async GetViewEntityByName(viewName, contextUser) {
        const { UserViewEngine } = await import('../engines/UserViewEngine.js');
        // Ensure the engine is configured before use
        await UserViewEngine.Instance.Config(false, contextUser);
        const view = UserViewEngine.Instance.GetViewByName(viewName);
        if (view) {
            return view;
        }
        throw new Error('Unable to find view with name: ' + viewName);
    }
}
/**
 * Column information for a saved view, including AG Grid-specific properties
 */
export class ViewColumnInfo extends BaseInfo {
    constructor(initData = null) {
        super();
        /** Entity field ID */
        this.ID = null;
        /** Field name */
        this.Name = null;
        /** Display name for column header (from entity metadata) */
        this.DisplayName = null;
        /** User-defined display name override for column header */
        this.userDisplayName = null;
        /** Whether column is hidden */
        this.hidden = null;
        /** Column width in pixels */
        this.width = null;
        /** Column order index */
        this.orderIndex = null;
        // AG Grid-specific properties
        /** Column pinning position ('left', 'right', or null for not pinned) */
        this.pinned = null;
        /** Flex grow factor (for auto-sizing columns) */
        this.flex = null;
        /** Minimum column width */
        this.minWidth = null;
        /** Maximum column width */
        this.maxWidth = null;
        /** Column formatting configuration */
        this.format = null;
        /** Reference to the entity field metadata (not persisted) */
        this.EntityField = null;
        this.copyInitData(initData);
    }
}
export const ViewFilterLogicInfo = {
    And: 'And',
    Or: 'Or',
};
export class ViewFilterInfo extends BaseInfo {
    constructor(initData = null) {
        super();
        this.logicOperator = null;
        this.field = null;
        this.operator = null;
        this.value = null;
        this.filters = [];
        this.copyInitData(initData);
        if (initData && initData.logic) {
            this.logicOperator = initData.logic.trim().toLowerCase() == 'and' ? ViewFilterLogicInfo.And : ViewFilterLogicInfo.Or;
        }
        if (initData && initData.filters) {
            this.filters = initData.filters.map(f => new ViewFilterInfo(f));
        }
    }
}
export const ViewSortDirectionInfo = {
    Asc: 'Asc',
    Desc: 'Desc',
};
export class ViewSortInfo extends BaseInfo {
    constructor(initData = null) {
        super();
        this.field = null;
        this.direction = null;
        this.copyInitData(initData);
        if (initData && initData.dir && typeof initData.dir == 'string') {
            this.direction = initData.dir.trim().toLowerCase() == 'asc' ? ViewSortDirectionInfo.Asc : ViewSortDirectionInfo.Desc;
        }
    }
}
/**
 * Grid state persisted in UserView.GridState column
 * Contains column configuration, sort settings, and optional filter state
 */
export class ViewGridState {
}
/**
 * Default display settings for aggregates
 */
export const DEFAULT_AGGREGATE_DISPLAY = {
    showColumnAggregates: true,
    columnPosition: 'bottom',
    showCardAggregates: true,
    cardPosition: 'right',
    cardPanelWidth: 280,
    cardLayout: 'vertical',
    cardGridColumns: 2,
    cardPanelTitle: 'Summary',
    cardPanelCollapsible: true,
    cardPanelStartCollapsed: false
};
//# sourceMappingURL=MJUserViewEntityExtended.js.map