initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
/**
* @deprecated Use Vue refs or reactive state instead
*/
export function getElementHtml(id) {
return document.getElementById(id)?.innerHTML ?? ''
}
/**
* @deprecated Use Vue refs or reactive state instead
*/
export function setElementHtml(id, html) {
const el = document.getElementById(id)
if (el) el.innerHTML = html
}
/**
* @deprecated Use v-model instead
*/
export function getElementValue(id) {
return document.getElementById(id)?.value ?? ''
}
/**
* @deprecated Use v-model instead
*/
export function setElementValue(id, value) {
const el = document.getElementById(id)
if (el) el.value = value
}
/**
* @deprecated Use reactive state instead of DOM removal
*/
export function removeElementById(id) {
const el = document.getElementById(id)
if (el?.parentNode) {
el.parentNode.removeChild(el)
return true
}
return false
}

View File

@@ -0,0 +1,133 @@
// resources/js/utils/RequestData.js
import { useHashKeyCache } from '../composables/useHashKeyCache.js';
/**
* Modern implementation of the RequestData pattern.
* Supports method chaining and integration with HashKeyCache.
*/
export class RequestData {
constructor(withHashCheck = false) {
this._url = '';
this._type = 'GET';
this._data = null;
this._success = null;
this._error = null;
this._fromVarCache = false;
this._withHashCheck = withHashCheck;
this._hashCache = useHashKeyCache();
}
/** Set the request URL */
url(u) {
this._url = u;
return this;
}
/** Set the HTTP method (GET, POST, etc.) */
type(t) {
this._type = t.toUpperCase();
return this;
}
/** Set the request payload */
data(d) {
this._data = d;
return this;
}
/** Set the success callback */
success(callback) {
this._success = callback;
return this;
}
/** Set the error callback */
error(callback) {
this._error = callback;
return this;
}
/** Enable or disable cache-first behavior */
fromVarCache(bool) {
this._fromVarCache = bool;
return this;
}
/** Execute the request */
async go() {
if (!this._url) {
console.error('[RequestData] URL is missing');
return;
}
// 1. Check HashKey/Payload in URL first if applicable
const urlMatch = this._hashCache.parseHashUrl(this._url);
if (urlMatch.type !== 'none' && urlMatch.data) {
if (this._success) this._success(urlMatch.data, urlMatch.value);
return;
}
// 2. Check Var Cache (in-memory) if requested
if (this._fromVarCache) {
const cachedData = this._hashCache.getHashData(this._url);
if (cachedData) {
if (this._success) this._success(cachedData);
// Revalidate in background (Stale-While-Revalidate pattern)
this._fetchFromServer(true);
return;
}
}
// 3. Perform network request
return this._fetchFromServer();
}
/** Internal fetch implementation */
async _fetchFromServer(background = false) {
try {
const options = {
method: this._type,
headers: {
'Accept': 'application/json',
}
};
if (this._data && (this._type === 'POST' || this._type === 'PUT')) {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(this._data);
}
const response = await fetch(this._url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
// Update cache
if (this._fromVarCache) {
this._hashCache.setHashData(this._url, result);
}
// If background revalidation found non-identical data, you might want to call success again
// or use Pinia to update the UI reactively.
if (this._success) {
// For simplicity, we call success again if it's a background fetch
// but usually you want a way to avoid jitter if data is the same.
this._success(result);
}
return result;
} catch (err) {
if (!background && this._error) {
this._error(err);
}
if (background) {
console.warn('[RequestData] Background revalidation failed:', err);
}
}
}
}
/** Wrapper function for easier instantiation */
export function request(withHashCheck = false) {
return new RequestData(withHashCheck);
}

View File

@@ -0,0 +1,19 @@
export const UserTypes = {
ULTIMATE: 'ult',
SUPER_OPERATOR: 'super operator',
OPERATOR: 'operator',
COORDINATOR: 'coordinator',
COOP_OFFICER: 'coop officer',
COOP_MEMBER: 'coop member',
SUPPLIER_OVERSEER: 'supplier overseer',
WHOLESALE_BUYER: 'wholesale buyer',
SUPPLIER: 'supplier',
STORE_OWNER: 'store owner',
STORE_MANAGER: 'store manager',
USER: 'user',
RIDER: 'rider',
AUDIT: 'audit',
POS_TERMINAL: 'pos terminal',
ANY_USER: 'default',
PUBLIC: 'public'
};

View File

@@ -0,0 +1,66 @@
/**
* Finds duplicate sub-arrays in a multidimensional array.
*
* @param {Array<Array>} arr - The multidimensional array to check
* @returns {Array<Array>|false} - Array of duplicates or false if none
*
* @example
* findDuplicatesInMultidimensionalArray([[1,2],[3,4],[1,2]])
* // returns [[1,2]]
*/
export function findDuplicatesInMultidimensionalArray(arr) {
const seen = new Set()
const duplicates = []
for (const subArr of arr) {
const key = JSON.stringify(subArr)
if (seen.has(key)) {
duplicates.push(subArr)
} else {
seen.add(key)
}
}
return duplicates.length ? duplicates : false
}
/**
* Joins array elements with a hyphen.
*
* @param {Array<string|number>} arr
* @returns {string}
*
* @example
* implodeArrayWithHyphen([1,2,3]) // "1-2-3"
*/
export function implodeArrayWithHyphen(arr) {
if (!Array.isArray(arr)) return ''
return arr.join('-')
}
/**
* Shuffles an array in-place using the FisherYates algorithm.
*
* @param {Array} array - The array to shuffle
* @returns {Array} The shuffled array
*
* @example
* shuffleArray([1,2,3,4])
* // might return [3,1,4,2]
*/
export function shuffleArray(array) {
if (!Array.isArray(array)) return []
let currentIndex = array.length
let randomIndex
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex)
currentIndex--
;[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]
}
return array
}

View File

@@ -0,0 +1,26 @@
import manifest from '../../cdn-manifest.json';
const ASSETS = manifest.assets || {};
function resolveBase() {
if (typeof window !== 'undefined' && window.__CDN_BASE__) {
return window.__CDN_BASE__;
}
// Fallback for SSR or pre-mount calls; the SHA matches resources/cdn-manifest.json
return `https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@${manifest.version}`;
}
export function cdnAsset(logicalName) {
const path = ASSETS[logicalName];
const base = resolveBase();
if (!path) {
return `${base}/missing/${logicalName}`;
}
return `${base}/${path}`;
}
export function cdnBase() {
return resolveBase();
}
export default cdnAsset;

View File

@@ -0,0 +1,25 @@
/**
* Formats a number as Philippine Peso currency (PHP) with commas and optional decimals.
*
* @param {number} number - The numeric value to format
* @param {boolean} [withDecimal=true] - Whether to include decimal places
* @returns {string} - Formatted currency string (e.g., "P1,234.56")
*
* @example
* formatCurrency(1234.56) // "P1,234.56"
* formatCurrency(1234.56, false) // "P1,234"
*/
export function formatCurrency(number, withDecimal = true) {
if (isNaN(number)) return 'P0'
let [integerPart, decimalPart] = Number(number)
.toFixed(2)
.toString()
.split('.')
if (!withDecimal) decimalPart = ''
integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `P${integerPart}${decimalPart ? '.' + decimalPart : ''}`
}

105
resources/js/utils/date.js Normal file
View File

@@ -0,0 +1,105 @@
/**
* Converts a 24-hour time string to 12-hour format.
*
* @param {string} time24 - Time in HH:mm or HH:mm:ss format
* @returns {string} Time in 12-hour format (e.g. "3:45 PM")
*
* @example
* convertTo12HRTime('14:30') // "2:30 PM"
*/
export function convertTo12HRTime(time24) {
if (!time24) return ''
return new Date(`1970-01-01T${time24}Z`).toLocaleTimeString('en-US', {
timeZone: 'UTC',
hour: 'numeric',
minute: 'numeric',
hour12: true
})
}
/**
* Formats a Date object into "Mon DD, YYYY".
*
* @param {Date} date
* @returns {string}
*
* @example
* formatDate(new Date()) // "Jan 31, 2026"
*/
export function formatDate(date) {
if (!(date instanceof Date)) return ''
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}
/**
* Returns today's date in GMT+8 timezone in "YYYY-MM-DD" format.
*
* @returns {string} - Date string in GMT+8
*
* @example
* getDateInGMT8() // "2026-01-31"
*/
export function getDateInGMT8() {
const now = new Date()
const utcTime = now.getTime()
const gmt8Offset = 8 * 60 * 60 * 1000
const gmt8Time = new Date(utcTime + gmt8Offset)
return gmt8Time.toISOString().split('T')[0]
}
/**
* Converts a date-time string into a readable format.
*
* Supports:
* - "YYYY-MM-DD"
* - "YYYY-MM-DD HH:mm:ss"
* - ISO strings
*
* @param {string} dateTimeString
* @returns {string}
*
* @example
* formatDateTimetoReadable('2026-01-31 14:30')
* // "January 31, 2026 2:30PM"
*/
export function formatDateTimetoReadable(dateTimeString) {
if (!dateTimeString) return ''
const normalized = dateTimeString.includes(' ')
? dateTimeString.replace(' ', 'T')
: dateTimeString
const date = new Date(normalized)
if (isNaN(date.getTime())) return ''
const datePart = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
// Only show time if original string had time
if (!dateTimeString.includes(' ')) {
return datePart
}
let hours = date.getHours()
const minutes = date.getMinutes()
const ampm = hours >= 12 ? 'PM' : 'AM'
hours = hours % 12 || 12
const paddedMinutes = minutes.toString().padStart(2, '0')
return `${datePart} ${hours}:${paddedMinutes}${ampm}`
}

View File

@@ -0,0 +1,25 @@
export function deepEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
}
if (typeof a === 'object' && typeof b === 'object') {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
}
return true;
}
return a === b;
}

View File

@@ -0,0 +1,31 @@
// utils/executeRequest.js
export async function executeRequest(request) {
// URL string → GET
if (typeof request === 'string') {
const res = await fetch(request)
return res.json()
}
// Request object
const {
url,
method = 'GET',
data = null,
headers = {}
} = request
const options = {
method,
headers: {
'Content-Type': 'application/json',
...headers
}
}
if (method !== 'GET' && data !== null) {
options.body = JSON.stringify(data)
}
const res = await fetch(url, options)
return res.json()
}

View File

@@ -0,0 +1,67 @@
import { deepEqual } from '@/utils/deepEqual'
import { executeRequest } from './executeRequest'
const cacheStore = new Map()
/**
* Cache-first, stale-while-revalidate data fetcher.
*
* Supports:
* - URL string (GET)
* - Request object (GET / POST)
* - Custom async fetcher function
*
* @param {Object} options
* @param {string} options.key - Unique cache key
* @param {string|Object} [options.request] - URL or request config
* @param {Function} [options.fetcher] - Custom async function
* @param {Function} options.onUpdate - Callback with data
*
* @example
* fetchWithCache({
* key: 'users.list',
* request: '/api/users',
* onUpdate: (data, source) => {
* users.value = data
* }
* })
*/
export async function fetchWithCache({
key,
request = null,
fetcher = null,
onUpdate = null
}) {
if (!key) {
throw new Error('fetchWithCache requires a cache key')
}
// 1⃣ Emit cached data immediately
if (cacheStore.has(key)) {
onUpdate?.(cacheStore.get(key), 'cache')
}
// 2⃣ Build fetcher if not provided
let resolvedFetcher = fetcher
if (!resolvedFetcher && request) {
resolvedFetcher = () => executeRequest(request)
}
if (typeof resolvedFetcher !== 'function') {
throw new Error('fetchWithCache requires either fetcher or request')
}
// 3⃣ Fetch fresh data
try {
const fresh = await resolvedFetcher()
const cached = cacheStore.get(key)
if (!cached || !deepEqual(cached, fresh)) {
cacheStore.set(key, fresh)
onUpdate?.(fresh, 'fresh')
}
} catch (err) {
console.error('[fetchWithCache] fetch failed:', err)
}
}

View File

@@ -0,0 +1,62 @@
/**
* Resize an image file while preserving aspect ratio.
*
* @param {File|Blob} file
* @param {number} maxWidth
* @param {number} maxHeight
* @param {number} [quality=0.7] - JPEG/WebP quality (01)
* @returns {Promise<Blob>}
*
* @example
* const resized = await resizeImage(file, 800, 800)
*/
export function resizeImage(file, maxWidth, maxHeight, quality = 0.7) {
return new Promise((resolve, reject) => {
if (!(file instanceof Blob)) {
reject(new Error('Invalid file'))
return
}
const reader = new FileReader()
reader.onload = e => {
const img = new Image()
img.onload = () => {
let { width, height } = img
const widthRatio = maxWidth / width
const heightRatio = maxHeight / height
const ratio = Math.min(widthRatio, heightRatio, 1)
width = Math.round(width * ratio)
height = Math.round(height * ratio)
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height)
canvas.toBlob(
blob => {
if (!blob) {
reject(new Error('Image resize failed'))
return
}
resolve(blob)
},
file.type || 'image/jpeg',
quality
)
}
img.onerror = () => reject(new Error('Invalid image'))
img.src = e.target.result
}
reader.onerror = () => reject(new Error('File read error'))
reader.readAsDataURL(file)
})
}

View File

@@ -0,0 +1,822 @@
var content_div_id = "content_wrapper";
var historylist = [];
var targetlist = [];
var fileBlobURLList = {};
if (typeof currentPage === 'undefined') {
var currentPage = '';
}
function InitDataPageFuncOBJ() {
if (typeof LoadDataPageFunc === 'undefined') {
LoadDataPageFunc = {};
logDev('Initialize LoadDataPageFunc');
}
}
function newPageLoadDataPageFuncOBJ() {
LoadDataPageFunc = {};
LoadDataPageFunc.PageTitle = '';
LoadDataPageFunc.MainDetailsURL = '';
LoadDataPageFunc.Details = {};
LoadDataPageFunc.Settings = {};
LoadDataPageFunc.currentPage = '';
LoadDataPageFunc.Disabled = null;
defaultBackOnclick = null;
LoadDataPageFunc.URL = {};
LoadDataPageFunc.URL.Details = null;
}
/*
window.addEventListener('load', function() {
window.history.pushState({}, ''); window.history.pushState({}, ''); window.history.pushState({}, ''); window.history.pushState({}, '');
});
window.addEventListener('popstate', function() {
window.history.pushState({}, ''); window.history.pushState({}, ''); window.history.pushState({}, ''); window.history.pushState({}, ''); window.history.pushState({}, '');
});
*/
function loadandexecute(targeturl, targetid = "main-body", targethtml = '') {
CurrentPageisLoading = true;
// console.log('loadandexecute started');
if (!targetid) { targetid = 'main-body'; }
if (targethtml) {
return new Promise((resolve, reject) => {
try {
targethtml = base64Decode(targethtml);
$("#" + targetid).html(targethtml);
CurrentPageisLoading = false;
return resolve('loaded');
}
catch (err) {
CurrentPageisLoading = false;
return reject(err);
}
});
} else {
// console.log('no target html '+targethtml);
}
if (reqcacheload(targeturl)) {
return new Promise((resolve, reject) => {
$("#" + targetid).html(reqcacheload(targeturl));
// $('#' + targetid).fadeIn();
CurrentPageisLoading = false;
return resolve('loaded');
});
}
CurrentPageisLoading = false;
return new Promise((resolve, reject) => {
Preloaders.PrecachePage(targeturl, targetid, () => {
CurrentPageisLoading = false;
resolve('loaded');
});
});
}
DropZoneFunc = {
AcceptedFilesString: {
images: 'image/*',
docs: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx',
documents: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx'
},
AddPreviouslyUploadedFiles: function (dropzoneobject, filesarray) {
/* const previousFiles = [
{
name: "example.jpg",
size: 12345,
url: "/path/to/example.jpg",
thumbnail: "/path/to/thumbnail/example.jpg"
},
// Add more files as needed
];*/
filesarray.forEach(file => {
const mockFile = {
name: file.name,
size: file.size,
url: file.url
};
dropzoneobject.emit("addedfile", mockFile);
dropzoneobject.emit("thumbnail", mockFile, file.thumbnail);
dropzoneobject.emit("complete", mockFile);
dropzoneobject.emit("url", mockFile, file.url, file.name);
// Optionally, if you have server-side file handling, you may need to set this to true
// dropzoneobject.emit("success", mockFile, file.serverResponse);
});
},
ReplaceDropzoneFiles: function (dropzoneid, newFilesArray) {
const dropzoneobject = window.currentDropzone[dropzoneid];
if (!dropzoneobject) {
console.warn(`Dropzone '${dropzoneid}' not found.`);
return;
}
DropZoneFunc.ClearDropzoneUpload(dropzoneid, dropzoneobject);
DropZoneFunc.AddPreviouslyUploadedFiles(dropzoneobject, newFilesArray);
window.Target_Uploaded_Files = newFilesArray.map(f => f.hashkey || f.name);
},
ResetDropzoneUpload: function (url, obj = null, reqtype = 'POST', successfunc = false) {
function ReplaceDropZoneFilesfrom(filesarray) {
DropZoneFunc.ClearDropzoneUpload();
DropZoneFunc.AddPreviouslyUploadedFilesDropZone(myDropzone, filesarray);
Target_Uploaded_Files = filesarray.map(file => file.hashkey);
}
if (!reqtype) { reqtype = 'POST'; }
if (!obj) { obj = null; }
let DRrequest = RequestData(false);
DRrequest.url(url).data(obj).type(reqtype).fromVarCache(false).success(function (response) {
ReplaceDropZoneFilesfrom(response);
if (typeof successfunc === 'function') { successfunc(response); }
}).go();
},
ClearDropzoneUpload: function (dropzonetargetid = '', dropzoneobject = '') {
//if (myDropzone){myDropzone.removeAllFiles(true);}
if (!dropzonetargetid) {
$('#' + dropzonetargetid + ' .dz-preview.dz-complete').remove();
console.log('#ClearUploadButton-' + dropzonetargetid);
// $('#ClearUploadButton-' + dropzonetargetid).hide();
} else {
$('.dz-preview.dz-complete').remove();
}
if (!dropzoneobject) { myDropzone.emit("reset") } else {
dropzoneobject.emit("reset");
}
//$('.dz-default.dz-message').show()
Target_Uploaded_Files = [];
},
hasOngoingUploads: function (dropzoneobject) {
return dropzoneobject.getUploadingFiles().length > 0;
},
RemoveLastUploadedDropzone: function (dropzoneobject) {
const uploadedFiles = dropzoneobject.getAcceptedFiles();
if (uploadedFiles.length > 0) {
const lastFile = uploadedFiles[uploadedFiles.length - 1];
dropzoneobject.removeFile(lastFile);
}
},
InitializeDropZone: function (url, successcallback, dropzoneid = 'myAwesomeDropZone', acceptedFiles, filename, maxfilesize = 100, updatedropzonestatusFUNC = '', errorcallback = false) {
const dropzoneconfig = {
paramName: filename,
maxFilesize: maxfilesize,
url: url,
acceptedFiles: acceptedFiles || null,
init: function () {
const dropzoneobject = this;
let ongoingUploads = 0;
function updateUploadStatus() {
if (typeof updatedropzonestatusFUNC === 'function') {
updatedropzonestatusFUNC();
}
}
this.on("addedfile", function (file) {
ongoingUploads++;
updateUploadStatus();
});
this.on("uploadprogress", function (file, progress) {
// optional
});
this.on("complete", function (file) {
ongoingUploads--;
updateUploadStatus();
});
this.on("url", function (file, url, name) {
// Make filename clickable
file.previewElement.classList.add("dz-success");
file.previewElement.querySelector("[data-dz-name]").innerHTML =
`<a href="${url}" target="_blank">${name}</a>`;
// Use the real image as thumbnail if its an image
// if (file.type.startsWith("image/")) {
// file.previewElement.querySelector("img").src = response.url;
// }
// Preserve your existing success callback
});
this.on("success", function (file, response) {
if (response && response.url) {
// Make filename clickable
file.previewElement.classList.add("dz-success");
file.previewElement.querySelector("[data-dz-name]").innerHTML =
`<a href="${response.url}" target="_blank">${response.name}</a>`;
// Use the real image as thumbnail if its an image
// if (file.type.startsWith("image/")) {
// file.previewElement.querySelector("img").src = response.url;
// }
}
// Preserve your existing success callback
if (typeof successcallback === 'function') { successcallback(response, file); }
});
// 🔴 catch network/server errors (like HTTP 500)
this.on("error", function (file, errorMessage, xhr) {
ongoingUploads--;
updateUploadStatus();
console.warn("Dropzone upload error:", errorMessage);
// Make sure it visually shows as failed
file.previewElement?.classList.add("dz-error");
const errorNode = file.previewElement?.querySelector("[data-dz-errormessage]");
if (errorNode) {
const msg =
(xhr && xhr.responseText) ||
errorMessage ||
"Upload failed";
errorNode.textContent = msg;
}
// Optionally auto-remove failed files
setTimeout(() => {
if (file && this.files.includes(file)) this.removeFile(file);
}, 2000);
// Run custom error callback if provided
if (typeof errorcallback === "function") {
errorcallback(file, errorMessage, xhr);
}
});
},
success: function (file, response) {
if (response && response.success) {
if (typeof successcallback === "function") successcallback(response, file);
} else {
// Even if backend returned 200, force an error state
const msg = response?.error || "Upload failed";
file.previewElement?.classList.add("dz-error");
const errorNode = file.previewElement?.querySelector("[data-dz-errormessage]");
if (errorNode) errorNode.textContent = msg;
this.emit("error", file, msg);
}
},
};
const myDropzone = new Dropzone(`#${dropzoneid}`, dropzoneconfig);
return myDropzone;
},
AddFiles: function (url = '/File/Upload', errorcallback = '', filename = 'file', maxfilesize = 100, acceptedFiles = 'image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx', dropzoneid = 'my-awesome-dropzone', dropzonemodal = 'Dropzone-Modal') {
function successcallback(response, file) {
// if (!response || isNumeric(response)) {
// RemoveLastUploadedDropzone();
// return false;
// } else {
// Target_Uploaded_Files.push(response);
// }
if (response.success && response.hashkey) {
Target_Uploaded_Files.push(response.hashkey);
} else {
RemoveLastUploadedDropzone();
return false;
}
}
if (!myDropzone) {
DropZoneFunc.InitializeDropZone(url, successcallback, dropzoneid = 'myAwesomeDropZone', acceptedFiles, filename, maxfilesize = 100);
}
if (view_transaction_reset_once) {
DropZoneFunc.ResetDropzoneUpload();
view_transaction_reset_once = 0
}
if (!filename) { filename = 'file'; }
if (!dropzonemodal) { dropzonemodal = 'Dropzone-Modal'; }
if (ElementExists(dropzonemodal)) { showmodal(dropzonemodal); }
}
};
function button(content, value = '', onclick = '', idtext = '', setclass = '', addtionaldata = '', img = '') {
if (img) { img = imgiconuserdefault(img); } else { img = ''; }
return '<button value="' + value + '" ' + addtionaldata + ' ' + img + ' id="' + idtext + '" class="' + setclass + '" onclick="' + onclick + '">' + content + '</button>';
}
function buttonGOTOPage(text, targetpage, targetdata = 0, idtext = '', addonclick = '', setclass = '', addtionaldata = '', img = '') {
if (!text || !targetpage) { return false; }
const onclick = `gotoPage('${targetpage}','${targetdata}'); ` + addonclick;
return button(text, text, onclick, idtext, setclass + ' btn btn-default', addtionaldata, img);
}
function buttonwarning(content, value = '', onclick = '', block = '', idtext = '', addclass = '', addtionaldata = '', img = '') {
if (block) { block = 'btn-block'; } else { block = ''; }
return button(content, value, onclick, idtext, 'btn btn-warning ' + block + ' ' + addclass, addtionaldata, img);
}
function buttondanger(content, value = '', onclick = '', block = '', idtext = '', addclass = '', addtionaldata = '', img = '') {
if (block) { block = 'btn-block'; } else { block = ''; }
return button(content, value, onclick, idtext, 'btn btn-danger ' + block + ' ' + addclass, addtionaldata, img);
}
function buttonprimary(content, value = '', onclick = '', block = '', idtext = '', addclass = '', addtionaldata = '', img = '') {
if (block) { block = 'btn-block'; } else { block = ''; }
return button(content, value, onclick, idtext, 'btn btn-primary ' + block + ' ' + addclass, addtionaldata, img);
}
function textinput(idtext = '', setclass = '', required = '', placeholder = '', value = '') {
if (required) { required = 'required'; } else { required = ''; }
return '<input type="text" class="' + setclass + '" id="' + idtext + '" placeholder="' + placeholder + '" ' + required + ' value="' + value + '">';
}
function textformcontrol(idtext = '', addclass = '', required = '', placeholder = '', value = '') {
return textinput(idtext, 'form-control ' + addclass, required, placeholder, value);
}
function HomeMenuButtons(buttonicon, buttonText, buttonGo, buttonVariabletoPass = '', iconwidth = '', iconheight = '', buttonstyle = '', buttononclick = '', divclass = '', divid = '', buttonid = '', textclass = '') {
// buttonvariabletopass is just currenttarget
if (!buttonstyle) { buttonstyle = 'bg-transparent border-0 btn-block' }
if (!divclass) { divclass = 'col-6'; }
if (!buttonVariabletoPass) { buttonVariabletoPass = 0; }
if (!buttonGo && buttononclick) {
buttontoclick = buttononclick;
} else if (buttonGo) {
buttontoclick = `ButtonGo('` + buttonGo + `', '` + buttonVariabletoPass + `')`;
} else {
buttontoclick = '';
}
if (!iconwidth) { iconwidth = '20%'; }
if (!iconheight) { iconheight = '40%'; }
if (!textclass) { textclass = 'profile-username text-center'; }
return `<div class="` + divclass + `" id="` + divid + `"><button id="` + buttonid + `" onclick="` + buttontoclick + `" class="bg-transparent border-0 btn-block" style="">
<div class="card-body box-profile">
<div class="text-center">
<img class=" img-circle rounded mx-auto d-block" src="`+ buttonicon + `" width="` + iconwidth + `" height="` + iconheight + `" style="overflow:hidden;"></div>
<h3 class="profile-username text-center">`+ buttonText + `</h3>
<p class="text-muted text-center"></p>
</div>
</button></div>`;
}
function createCard(cardtitle = '', cardid = '', cardtools = '', cardbodyid = '', cardbodytext = '', cardbodyclassadd = '', maincardstyle = '', titlecardhide = false, maincardaddclass = '') {
if (titlecardhide) { titlecardhide = 'display:none;'; } else { titlecardhide = ';' }
if (!cardbodyid && cardid) { cardbodyid = 'card-body-' + cardid; }
return `<div class="card ${maincardaddclass}" id="` + cardid + `" style="` + maincardstyle + `">
<div id="cardheader-${cardid}" class="card-header ui-sortable-handle" style="cursor: move;` + titlecardhide + `">
<h3 class="card-title" style="" id="card-title-${cardid}">` + cardtitle + `</h3>
<div class="card-tools" id="card-tools-${cardid}">
`+ cardtools + `
</div>
</div>
<div class="card-body `+ cardbodyclassadd + `" id='` + cardbodyid + `'>
`+ cardbodytext + `
</div>
</div>`;
}
function CreateCardSimple(title = '', text = '', id = '', style = '') {
let titlehidden = false;
if (!title) { titlehidden = true; }
return createCard(title, id, '', '', text, '', style, titlehidden);
}
function CreateBalanceCard(titletext = '', maincardid = 'main-card-body', firstrowtext = 'Total Balance', firstrowvalueid = 'total-balance', firstrowvaluetext = '...', secondrowtext = '', secondrowvalueid = '', secodrowvaluetext = '', secondrowvisible = false, footerrowid = '', footerrowtext = '', thirdrowtext = '', thirdrowvalueid = '', thirdrowvaluetext = '', thirdrowvisible = false) {
if (titletext === false || titletext === null) { titletext = 'Welcome!'; }
if (!secondrowvisible) { secondrowvisible = 'display:none;'; } else {
secondrowvisible = '';
}
let thirdrowhtml = '';
if (thirdrowtext || thirdrowvalueid || thirdrowvaluetext || thirdrowvisible) {
if (thirdrowvisible) { thirdrowvisible = ''; } else { thirdrowvisible = 'display:none;'; }
thirdrowhtml = `
<div class="row">
<div class="col">
`+ thirdrowtext + `
</div>
<div class="col text-right" id="`+ thirdrowvalueid + `">
`+ thirdrowvaluetext + `
</div>
</div>
`;
}
return `<div class="card-body card-info" id="` + maincardid + `" style="">
<div class="row">
<div class="col-md-18">
`+ titletext + `
</div>
</div>
<br>
<div class="row card-info">
<div class="col-md-18 card text-xl border-rounded balancecard" style="width:100%;height:30%;">
<div class="row">
<div class="col">
`+ firstrowtext + `
</div>
<div class="col text-right" id="`+ firstrowvalueid + `">
`+ firstrowvaluetext + `
</div>
</div>
<div class="row" style="`+ secondrowvisible + `">
<div class="col">
`+ secondrowtext + `
</div>
<div class="col text-right" id="`+ secondrowvalueid + `">
`+ secodrowvaluetext + `
</div>
</div>`
+ thirdrowhtml +
`<br><br>
<div class="row ">
<div class="col text-sm" id="`+ footerrowid + `">
`+ footerrowtext + `
</div>
<div class="col">
</div>
</div>
</div>
</div>`;
}
function clearCacheAndReload(redirectTo = '/') {
localStorage.clear();
sessionStorage.clear();
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(registrations => {
for (let registration of registrations) {
logDev('Unregistering Old Service Worker');
registration.unregister();
}
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
logDev('Deleting Old Cache');
return caches.delete(cacheName);
})
);
}).then(() => {
caches.keys().then(function (names) {
for (let name of names) {
caches.delete(name);
}
});
if (redirectTo !== null && redirectTo !== false) {
window.location.href = redirectTo;
}
});
});
} else {
caches.keys().then(function (names) {
for (let name of names) {
caches.delete(name);
}
});
if (redirectTo !== null && redirectTo !== false) {
window.location.href = redirectTo;
}
}
}
function logoutnow() {
window.location.href = '/go/logoutnow';
}
function clearCacheAndLogout() {
clearCacheAndReload('/go/logoutnow');
}
function ResetBrowserAndCache() {
logDev('Resetting Browser Cache');
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(registrations => {
for (let registration of registrations) {
registration.unregister();
}
window.location.reload(true);
});
} else {
console.warn('Service workers are not supported in this browser.');
};
}
function GenerateTheadFromArraySimple(array) {
html = `<tr>`;
if (Array.isArray(array)) {
array.forEach((item, index) => {
html += `<th>` + item + `</th>`;
});
} else if (array !== null && typeof array === 'object') {
Object.entries(array).forEach(([key, value]) => {
html += `<th>` + value + `</th>`;
});
} else {
return;
}
html += `</tr>`;
return html;
}
function GenerateTableRowFromArraySimple(array) {
html = `<tr>`;
if (Array.isArray(array)) {
array.forEach((item, index) => {
html += `<td>` + item + `</td>`;
});
} else if (array !== null && typeof array === 'object') {
Object.entries(array).forEach(([key, value]) => {
html += `<td>` + value + `</td>`;
});
} else {
return;
}
html += `</tr>`;
return html;
}
function ArraytoDatalist(id, array, replace = true) {
if (!id || !array) { return false; }
if (replace) {
const datalist = document.getElementById(id);
if (!datalist) { return false; }
datalist.innerHTML = "";
array.forEach(item => {
const option = document.createElement('option');
option.value = item;
datalist.appendChild(option);
});
return datalist;
}
if (!replace) {
let htmlstring = '<datalist id="' + id + '">';
array.forEach(item => {
htmlstring += '<option value="' + item + '">';
});
htmlstring += '</datalist>';
return htmlstring;
}
}
function QueryandReplaceDatalist(datalistid, url, method = 'POST', datatosend = null, fromvarcache = false, successfunc = false, errorfunc = false) {
if (!document.getElementById(datalistid) || !url) {
return false;
}
let RequestDataDatalist = new RequestData(false);
RequestDataDatalist.url(url).type(method).fromVarCache(fromvarcache).data(datatosend)
.success((response) => {
if (typeof successfunc === 'function') {
successfunc(response);
}
ArraytoDatalist(datalistid, response, replace = true);
}).error((response) => {
if (errorfunc && typeof errorfunc === 'function') {
errorfunc(response);
}
}).go();;
}
function ReqCachetoDatalist(url, id, replace = true) {
const data = reqcacheload(url, datavalue = '', object = '');
if (!data) { return false; }
return ArraytoDatalist(id, data, replace);
}
function CreateTable(id, theadinnerhtml, bodyinnerhtml) {
return `<table id="` + id + `">
<thead>
`+ theadinnerhtml + `
</thead>
<tbody>
`+ bodyinnerhtml + `
</tbody>
</table>`;
}
function LoadPhotosCard(photosdivid, onclick = '', photosarray = '', photoscardid = 'PhotosCard', maxwidth = '300px', maxheight = '300px', preloadFileBlobURL = false) {
// onclick sample ButtonGo('ViewAllPhotos','${currenttarget}');
if (!maxwidth && !maxheight) { maxwidth = '300px'; maxheight = '300px'; }
let photosdiv = $('#' + photosdivid);
if (photosdiv.length === 0) { return false; }
if (!photosarray) { photosdiv.html('No Photos.<br>'); return false; }
if (photosarray && typeof photosarray === 'string') {
try {
photolist = JSON.parse(photosarray);
} catch (error) {
photolist = [photosarray];
}
} else {
photolist = photosarray;
}
if (preloadFileBlobURL) {
photolist.forEach(function (photo) {
Preloaders.getfileBlobURL(photo);
});
}
if (!photolist || photolist.length === 0) { photosdiv.html('No Photo.<br>'); return false; }
let htmlbody = $(`<div class="splide" role="group" aria-label="photosSplide">
<div class="splide__track">
<ul class="splide__list">
</ul> </div></div>`);
const NewSplideLIImage = function (imgsrc) {
return `<li class="splide__slide" onclick="${onclick}"><img src="${imgsrc}" style="max-width: ${maxwidth};
max-height: ${maxheight};
width: auto;
height: auto;"></li>`;
};
let splidebody = htmlbody.find('ul');
LoadDataPageFunc.PhotoBlobs = [];
photolist.forEach(function (photo) {
LoadAndCreateURLfromFileHash(photo).then(bloburl => {
splidebody.append(NewSplideLIImage(bloburl));
if (typeof LoadDataPageFunc.PhotoBlobs === 'undefined') {
LoadDataPageFunc.PhotoBlobs = [];
}
LoadDataPageFunc.PhotoBlobs.push(bloburl);
});
});
photosdiv.html(htmlbody);
new Splide('.splide').mount();
};
function LoadPhototoIMG(photimgid, photohash, onclick = '', maxwidth = '300px', maxheight = '300px') {
if (typeof photohash === 'array') {
photohash = photohash[0];
}
LoadAndCreateURLfromFileHash(photohash).then(bloburl => {
$('#' + photimgid).attr('src', bloburl);
});
}
function LoadPhotoIMGTargetClass(imgclassname, settodisplaylater = true) {
if (!imgclassname) {
return false;
}
const images = document.querySelectorAll(`img.${imgclassname}`);
images.forEach((img) => {
const photohash = img.getAttribute('src');
LoadAndCreateURLfromFileHash(photohash).then(bloburl => {
img.setAttribute('src', bloburl);
if (settodisplaylater) {
img.style.display = 'block';
}
}).catch(error => {
});
});
}
function HideBrokenPhotowithClass(imgclassname) {
if (!imgclassname) {
return false;
}
const images = document.querySelectorAll(`img.${imgclassname}`);
images.forEach((img) => {
img.addEventListener('error', () => {
img.style.display = 'none';
});
});
}
/**
* Validates Input Form
* @param {Array} ArrayOfRequiredInput - Array of required input field IDs
* @returns {boolean|Array} - Returns an array of values if all required fields are filled, false otherwise
*/
function validateInputForm(ArrayOfRequiredInput) {
const inputsArray = getInputElementsValuesObjectbyCSSClassname(LoadDataPageFunc.formclass);
if (isObjectEmpty(inputsArray)) {
return false;
}
let values = [];
for (let id of ArrayOfRequiredInput) {
const value = inputsArray[id];
if (value === undefined || value === null || String(value).trim() === "") {
return false;
}
values.push(value);
}
return values;
}

View File

@@ -0,0 +1,71 @@
import { reactive } from 'vue'
/**
* Vue 3 composable for memoizing functions.
* Works across components and SPA navigation (module-level singleton cache).
*
* Usage:
* const { memoize, memoizeFull } = useMemoize()
* const add10 = (n) => n + 10
* const cachedAdd = memoize(add10)
* cachedAdd(5) // calculates
* cachedAdd(5) // returns cached result
*/
export function useMemoize() {
// reactive singleton caches
const cacheSingle = reactive({})
const cacheMulti = reactive({})
/**
* Memoize a single-argument function.
* @param {Function} fn - Function with one argument
* @returns {Function} Memoized function
*/
function memoize(fn) {
return (arg) => {
if (arg in cacheSingle) {
// console.log('Fetching from cache')
return cacheSingle[arg]
}
// console.log('Calculating result')
const result = fn(arg)
cacheSingle[arg] = result
return result
}
}
/**
* Memoize a function with multiple arguments.
* @param {Function} fn - Function with any number of arguments
* @returns {Function} Memoized function
*/
function memoizeFull(fn) {
return (...args) => {
const key = JSON.stringify(args)
if (cacheMulti[key]) {
// console.log('Fetching from cache for:', args)
return cacheMulti[key]
}
const result = fn(...args)
cacheMulti[key] = result
return result
}
}
/**
* Optional: clear caches
*/
function clearCache() {
Object.keys(cacheSingle).forEach(k => delete cacheSingle[k])
Object.keys(cacheMulti).forEach(k => delete cacheMulti[k])
}
return {
memoize,
memoizeFull,
cacheSingle,
cacheMulti,
clearCache
}
}

View File

@@ -0,0 +1,29 @@
import { onMounted, onBeforeUnmount } from 'vue'
/**
* Watch for DOM changes on a target element and run a callback.
*
* @param {HTMLElement | null} targetElement - Element to monitor
* @param {Function} callback - Function to run on mutations
*/
export function useMutationObserver(targetElement, callback) {
let observer = null
onMounted(() => {
if (!targetElement) return
observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
callback()
}
})
})
observer.observe(targetElement, { childList: true, subtree: true })
})
onBeforeUnmount(() => {
if (observer) observer.disconnect()
})
}

View File

@@ -0,0 +1,50 @@
import axios from 'axios';
import { getCurrentInstance } from 'vue'
const navigate = async ({ page }) => {
try {
// Show loading spinner if you want
loading.value = true;
// Request page data from server
const response = await axios.get(`/${page}`);
const data = response.data;
// Expected server response:
// { component: 'Home', props: {...} }
currentPage.value = data.component;
currentProps.value = data.props || {};
} catch (error) {
console.error('Navigation error', error);
currentPage.value = 'NotFound';
currentProps.value = {};
} finally {
loading.value = false;
}
};
/**
* Reloads the current SPA page
* without changing history.
*
* Vue replacement for legacy ReloadPage()
*/
export function reloadPage() {
const instance = getCurrentInstance()
const navigate =
instance?.proxy?.$navigate ||
window.$navigate
if (!navigate) {
console.warn('No SPA navigator available')
return
}
navigate({
page: instance.proxy.currentPage,
props: instance.proxy.currentProps,
nohistory: true,
redundantpage: true
})
}

View File

@@ -0,0 +1,41 @@
export function setNotif(title, body = '', icon = '', tag = '') {
Notification.requestPermission(result => {
if (result === 'granted') {
navigator.serviceWorker.ready.then(reg =>
reg.showNotification(title, { body, icon, tag })
)
}
})
}
export function eraseAllNotif() {
Notification.requestPermission(result => {
if (result === 'granted') {
navigator.serviceWorker.ready.then(reg =>
reg.getNotifications().then(n => n.forEach(x => x.close()))
)
}
})
}
export function EraseNotifwithTag(tag) {
Notification.requestPermission(function (result) {
if (result === 'granted') {
// Replace 'specific-tag' with the tag you want to target
const specificTag = tag;
// Check if service workers are available
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(function (registration) {
registration.getNotifications().then(function (notifications) {
notifications.forEach(function (notification) {
if (notification.tag === specificTag) {
notification.close();
}
});
});
});
}
}
});
}

View File

@@ -0,0 +1,32 @@
/**
* Checks whether an object has only empty values.
*
* Empty values:
* - undefined
* - null
* - empty string
* - empty array
* - empty object
*
* @param {Object} obj
* @returns {boolean}
*
* @example
* isObjectEmpty({}) // true
* isObjectEmpty({ a: '' }) // true
* isObjectEmpty({ a: [] }) // true
* isObjectEmpty({ a: {} }) // true
* isObjectEmpty({ a: 1 }) // false
*/
export function isObjectEmpty(obj) {
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
return false
}
return Object.values(obj).every(value => {
if (value === undefined || value === null || value === '') return true
if (Array.isArray(value)) return value.length === 0
if (typeof value === 'object') return Object.keys(value).length === 0
return false
})
}

View File

@@ -0,0 +1,42 @@
/**
* Generates a unique random hash string.
*
* Uses `window.crypto` if available, otherwise falls back to Math.random.
*
* @param {number} [length=16] - Desired length of the hash
* @returns {string}
*
* @example
* createUniqueRandomHash(8) // "3f4a1b9c"
*/
export function createUniqueRandomHash(length = 16) {
if (window.crypto && window.crypto.getRandomValues) {
const array = new Uint8Array(length)
window.crypto.getRandomValues(array)
return Array.from(array)
.map(byte => (byte % 16).toString(16))
.join('')
}
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let hash = ''
for (let i = 0; i < length; i++) {
hash += chars.charAt(Math.floor(Math.random() * chars.length))
}
return hash
}
/**
* Generates a short unique key string.
* Uses `Math.random()` and base36 conversion.
*
* @returns {string} - A random alphanumeric string (length ~9)
*
* @example
* const key = generateUniqueKey() // e.g., "5gk8q2m1a"
*/
export function generateUniqueKey() {
return Math.random().toString(36).substring(2, 11)
}

View File

@@ -0,0 +1,59 @@
/**
* UI Helper Utilities
* Extracted from Legacy/UIALT.js — pure functions that don't map to Vue components.
*/
/**
* Get values of all input/textarea elements by CSS class name.
* Returns an array of { id, value } objects.
*
* @param {string} className
* @returns {{ id: string, value: string }[]}
*/
export function getInputAndTextareaValuesByClassName(className) {
const elements = document.getElementsByClassName(className)
const results = []
Array.from(elements).forEach(element => {
results.push({ id: element.id, value: element.value })
})
return results
}
/**
* Get values of all input elements by CSS class name.
* Returns an object keyed by element id.
*
* @param {string} className
* @returns {Record<string, string>}
*/
export function getInputElementsValuesObjectByClassName(className) {
const elements = document.getElementsByClassName(className)
const results = {}
Array.from(elements).forEach(element => {
results[element.id] = element.value
})
return results
}
/**
* Inject dynamic CSS into a <style> tag with id="dynamic-css".
* Creates the tag if it doesn't exist.
*
* @param {string} cssText
*/
export function setDynamicCSS(cssText) {
let styleTag = document.getElementById('dynamic-css')
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.id = 'dynamic-css'
document.head.appendChild(styleTag)
}
styleTag.innerHTML = cssText
}
/**
* Clear all dynamic CSS.
*/
export function resetDynamicCSS() {
setDynamicCSS('')
}

View File

@@ -0,0 +1,63 @@
import { useUIStore } from '../stores/ui'
const CORPORATE_LABELS = {
'ult': 'Ultimate',
'super operator': 'Super Operator',
'operator': 'Operator',
'coordinator': 'Coordinator',
'supplier overseer': 'Supplier Overseer',
'wholesale buyer': 'Wholesale Buyer',
'supplier': 'Supplier',
'store owner': 'Store Owner',
'store manager': 'Store Manager',
'user': 'User',
'rider': 'Rider',
'audit': 'Audit',
'pos terminal': 'POS Terminal',
}
const COOPERATIVE_LABELS = {
'ult': 'Chairperson',
'super operator': 'General Manager',
'operator': 'Officer',
'coordinator': 'Chapter Coordinator',
'supplier overseer': 'Supplier Liaison',
'wholesale buyer': 'Bulk Buyer',
'supplier': 'Member-Supplier',
'store owner': 'Branch Owner',
'store manager': 'Branch Manager',
'user': 'Member',
'rider': 'Rider',
'audit': 'Auditor',
'pos terminal': 'POS Terminal',
}
const TYPE_BADGE_CLASS = {
'ult': 'bg-danger',
'super operator': 'bg-warning text-dark',
'operator': 'bg-primary',
'coordinator': 'bg-info text-dark',
'store owner': 'bg-success',
'store manager': 'bg-info text-white',
'user': 'bg-secondary',
}
export function getUserTypeLabel(type, mode) {
if (!type) return 'Unknown'
const key = String(type).toLowerCase()
const map = mode === 'cooperative' ? COOPERATIVE_LABELS : CORPORATE_LABELS
return map[key] || CORPORATE_LABELS[key] || (key.charAt(0).toUpperCase() + key.slice(1))
}
export function getUserTypeBadgeClass(type) {
if (!type) return 'bg-secondary'
return TYPE_BADGE_CLASS[String(type).toLowerCase()] || 'bg-secondary'
}
export function useUserTypeLabels() {
const ui = useUIStore()
return {
label: (type) => getUserTypeLabel(type, ui.app_mode),
badgeClass: (type) => getUserTypeBadgeClass(type),
}
}

View File

@@ -0,0 +1,79 @@
/**
* Checks if an email address is valid.
*
* @param {string} email
* @returns {boolean}
*
* @example
* isValidEmail('test@example.com') // true
*/
export function isValidEmail(email = '') {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailPattern.test(email)
}
/**
* Validates Philippine mobile number format.
*
* Must start with 09 and contain exactly 11 digits.
*
* @param {string} mobile
* @returns {boolean}
*
* @example
* hasValidMobileFormat('09171234567') // true
*/
export function hasValidMobileFormat(mobile = '') {
const pattern = /^09\d{9}$/
return pattern.test(mobile)
}
/**
* Checks whether a value is numeric.
*
* Accepts strings or numbers.
* Rejects empty strings, whitespace, and NaN.
*
* @param {string|number} value
* @returns {boolean}
*
* @example
* isNumeric('123') // true
* isNumeric(45) // true
* isNumeric('12.3') // true
* isNumeric('') // false
* isNumeric(' ') // false
*/
export function isNumeric(value) {
if (value === null || value === undefined) return false
if (typeof value === 'string' && value.trim() === '') return false
return !Number.isNaN(Number(value))
}
/**
* Checks whether a response is a "hash-like" string.
*
* A valid hash is:
* - not empty
* - not `true`
* - not numeric
* - does not contain spaces
*
* @param {*} response - The value to check
* @returns {string|false} - Returns the string if it is a hash, otherwise false
*
* @example
* isResponseAHash('abc123') // 'abc123'
* isResponseAHash('123') // false
* isResponseAHash(true) // false
* isResponseAHash('hello world') // false
*/
export function isResponseAHash(response) {
if (!response || response === true) return false
const result = String(response)
if (result.includes(' ') || !isNaN(result)) return false
return result
}