Upload markdown files and organize them in a tree structure
⚠️ Warning: This action cannot be undone!
controllers/expensesTypeController.php (620 lines)
expensestype
dailyentryfun.php)
initiateStaticSessionCommingWithCurl.php)
initiateStaticSessionCommingWithCurl.php (Line 8)
// CURL POST detection throughout controller
">if (isset($_POST['curlpost039;]) && $_POST[039;curlpost039;] == 1) {
// JSON response for external systems
$data = array('status039; => 1, 039;message039; => 039;تمت العمليه بنجاح039;, 039;message_en039; => 039;Success039;);
">echo json_encode($data);
} else {
// Standard web response
header("location:?do=sucess");
}
include_once("dailyentryfun.php") (Line 9)
addTreeElement() and editTreeElement()
expensestype
CREATE TABLE expensestype (
expensestypeid INT PRIMARY KEY AUTO_INCREMENT,
expensestypename VARCHAR(256) NOT NULL,
expensestypedetails TEXT,
expensestypedate DATE NOT NULL,
conditions TINYINT NOT NULL DEFAULT 0, -- 0=active, 1=deleted
userid INT NOT NULL,
parent INT NOT NULL DEFAULT 0, -- parent expense type ID
type INT NOT NULL DEFAULT 0, -- expense classification
saveid INT NOT NULL DEFAULT 0, -- linked cash box/save
addOnlyGroupIds VARCHAR(500), -- comma-separated user group IDs
treeType INT NOT NULL DEFAULT 0, -- tree classification (0,1,2)
treeId INT NOT NULL DEFAULT 0, -- corresponding account tree ID
withinsupervision_ratio TINYINT NOT NULL DEFAULT 0, -- supervision required
supervision_ratiotype INT NOT NULL DEFAULT 0, -- supervision type
supervision_amount DECIMAL(10,2) DEFAULT 0, -- supervision limit
webApiId INT NOT NULL DEFAULT 0 -- external system integration
);
expenses - Actual Expense Records
CREATE TABLE expenses (
expensesid INT PRIMARY KEY AUTO_INCREMENT,
expensestypeid INT NOT NULL, -- Foreign key to expensestype
expensesValue DECIMAL(10,2) NOT NULL,
expensesDetails TEXT,
expensesDate DATE NOT NULL,
conditions TINYINT NOT NULL DEFAULT 0,
userid INT NOT NULL,
saveid INT NOT NULL,
dailyentryid INT NOT NULL DEFAULT 0
);
accountstree - Chart of Accounts Integration
CREATE TABLE accountstree (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(256) NOT NULL,
customName VARCHAR(256),
parent INT NOT NULL DEFAULT 0,
level INT NOT NULL DEFAULT 0,
conditions TINYINT NOT NULL DEFAULT 0,
userid INT NOT NULL,
creationDate DATE NOT NULL
);
save - Cash Boxes/Safes
CREATE TABLE save (
saveid INT PRIMARY KEY AUTO_INCREMENT,
savename VARCHAR(256) NOT NULL,
saveamount DECIMAL(10,2) NOT NULL DEFAULT 0,
conditions TINYINT NOT NULL DEFAULT 0,
userid INT NOT NULL
);
usergroup - User Group Permissions
CREATE TABLE usergroup (
usergroupid INT PRIMARY KEY AUTO_INCREMENT,
usergroupname VARCHAR(256) NOT NULL,
conditions TINYINT NOT NULL DEFAULT 0
);
add() function)
-- Insert new expense type (via ExpensestypeMySqlDAO.insert)
INSERT INTO expensestype (
expensestypename, expensestypedetails, expensestypedate, conditions, userid,
parent, type, saveid, addOnlyGroupIds, treeType, treeId,
withinsupervision_ratio, supervision_ratiotype, supervision_amount, webApiId
) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-- Create corresponding account in chart of accounts
INSERT INTO accountstree (name, parent, level, conditions, userid, creationDate)
VALUES (?, ?, 3, 0, ?, ?)
-- Update expense type with tree ID
UPDATE expensestype SET treeId = ? WHERE expensestypeid = ?
show() function)
-- Get hierarchical expense types without expenses (from getTypesWithoutExpenses)
SELECT expensestype.*
FROM expensestype
LEFT JOIN expenses ON expenses.expensestypeid = expensestype.expensestypeid
WHERE expenses.expensestypeid IS NULL
AND expensestype.conditions = 0
AND expensestype.parent = ?
-- With save filtering:
-- Additional condition: AND (expensestype.saveid = 0 OR expensestype.saveid IN (?))
-- With specific save: AND expensestype.saveid = ?
-- Search by name
SELECT * FROM expensestype
WHERE conditions = 0
AND expensestypename LIKE ?
ORDER BY expensestypeid DESC
update() function)
-- Load existing data
SELECT * FROM expensestype WHERE expensestypeid = ?
-- Update expense type
UPDATE expensestype SET
expensestypename = ?, expensestypedetails = ?, expensestypedate = ?,
conditions = ?, userid = ?, parent = ?, type = ?, saveid = ?,
addOnlyGroupIds = ?, treeType = ?, treeId = ?,
withinsupervision_ratio = ?, supervision_ratiotype = ?, supervision_amount = ?
WHERE expensestypeid = ?
-- Update corresponding account tree
UPDATE accountstree SET
name = ?, customName = ?, parent = ?
WHERE id = ?
-- Check for expense dependencies
SELECT * FROM expenses WHERE expensestypeid = ?
-- Check for child expense types
SELECT * FROM expensestype WHERE parent = ?
-- If no dependencies, soft delete (via updateConditions)
UPDATE expensestype SET
expensestypedate = ?, conditions = 1, userid = ?
WHERE expensestypeid = ?
-- Delete from account tree
DELETE FROM accountstree WHERE name = ?
-- Get expense types by parent (for tree building)
SELECT *
FROM expensestype
WHERE parent = ? AND conditions = 0
ORDER BY expensestypeid
-- Get root level expense types
SELECT *
FROM expensestype
WHERE parent = 0 AND conditions = 0
-- Get expense types without children (leaf nodes)
SELECT *
FROM expensestype
WHERE conditions = 0
AND expensestypeid NOT IN (
SELECT DISTINCT parent
FROM expensestype
WHERE conditions = 0 AND parent <> 0
)
ORDER BY expensestypeid DESC
-- Get expense types with parent names
SELECT expensestype.*, theParent.expensestypename as parentexpensestypename
FROM expensestype
LEFT JOIN expensestype theParent ON expensestype.parent = theParent.expensestypeid
WHERE expensestype.conditions = 0
-- Get all saves/cash boxes
SELECT * FROM save WHERE conditions = 0
-- Get saves for user
SELECT * FROM save WHERE saveid IN (?) AND conditions = 0
-- Get user groups
SELECT * FROM usergroup WHERE conditions = 0
-- Sum expenses by type
SELECT expensestype.expensestypename, expensestype.conditions,
SUM(expenses.expensesValue) as sumExpensesValue
FROM expensestype
JOIN expenses ON expensestype.expensestypeid = expenses.expensestypeid
WHERE expenses.conditions = 0
GROUP BY expensestype.expensestypeid
expensesTypeController.php
Method: GET
Purpose: Display add expense type form with hierarchical parents
Template: expensesTypeview/add.html
SQL Operations:
-- Build hierarchical tree (via orderExtepensesTypeParentsAsTree)
SELECT expensestype.*
FROM expensestype
LEFT JOIN expenses ON expenses.expensestypeid = expensestype.expensestypeid
WHERE expenses.expensestypeid IS NULL
AND expensestype.conditions = 0
AND expensestype.parent = ?
-- Get available saves
SELECT * FROM save WHERE conditions = 0
-- Get user groups
SELECT * FROM usergroup WHERE conditions = 0
Business Logic:
expensesTypeController.php?do=add
Method: POST
Purpose: Create new expense type with accounting integration
SQL Operations:
-- Insert expense type
INSERT INTO expensestype (
expensestypename, expensestypedetails, expensestypedate, conditions, userid,
parent, type, saveid, addOnlyGroupIds, treeType, treeId,
withinsupervision_ratio, supervision_ratiotype, supervision_amount, webApiId
) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-- Get parent tree ID if parent exists
SELECT treeId FROM expensestype WHERE expensestypeid = ?
-- Create account tree entry
INSERT INTO accountstree (name, parent, level, conditions, userid, creationDate)
VALUES (?, ?, 3, 0, ?, ?)
-- Update with tree ID
UPDATE expensestype SET treeId = ? WHERE expensestypeid = ?
Business Logic:
addTreeElement()
expensesTypeController.php?do=show
Method: GET
Purpose: Display hierarchical expense types with filtering
Template: expensesTypeview/show.html
SQL Operations:
-- Build hierarchical tree
SELECT expensestype.*
FROM expensestype
LEFT JOIN expenses ON expenses.expensestypeid = expensestype.expensestypeid
WHERE expenses.expensestypeid IS NULL
AND expensestype.conditions = 0
AND expensestype.parent = ?
-- Filter by name if specified
SELECT * FROM expensestype
WHERE conditions = 0
AND expensestypename LIKE ?
ORDER BY expensestypeid DESC
Business Logic:
expensesTypeController.php?do=executeOperation
Method: POST
Purpose: Bulk operations on selected expense types
SQL Operations:
-- For bulk delete (operation=1):
-- Check dependencies for each expense type:
SELECT * FROM expenses WHERE expensestypeid = ?
SELECT * FROM expensestype WHERE parent = ?
-- If no dependencies, soft delete:
UPDATE expensestype SET
expensestypedate = ?, conditions = 1, userid = ?
WHERE expensestypeid = ?
Business Logic:
expensesTypeController.php?do=editprint&id={expensestypeid}
Method: GET
Purpose: Print-friendly edit view
Template: expensesTypeview/editprint.html
SQL Operations:
-- Load expense type data
SELECT * FROM expensestype WHERE expensestypeid = ?
-- Build parent hierarchy for selection
SELECT expensestype.*
FROM expensestype
LEFT JOIN expenses ON expenses.expensestypeid = expensestype.expensestypeid
WHERE expenses.expensestypeid IS NULL
AND expensestype.conditions = 0
AND expensestype.expensestypeid != ?
-- Load saves
SELECT * FROM save WHERE conditions = 0
Business Logic: Same as edit but with print styling
expensesTypeController.php?do=edit&id={expensestypeid}
Method: GET
Purpose: Display edit form for expense type
Template: expensesTypeview/edit.html
SQL Operations:
-- Load expense type data
SELECT * FROM expensestype WHERE expensestypeid = ?
-- Build parent hierarchy (excluding current type to prevent circular references)
SELECT expensestype.*
FROM expensestype
LEFT JOIN expenses ON expenses.expensestypeid = expensestype.expensestypeid
WHERE expenses.expensestypeid IS NULL
AND expensestype.conditions = 0
AND expensestype.expensestypeid != ?
-- Load saves with user permissions
SELECT * FROM save WHERE saveid IN (?) AND conditions = 0
-- Load user groups
SELECT * FROM usergroup WHERE conditions = 0
Business Logic:
expensesTypeController.php?do=update
Method: POST
Purpose: Update existing expense type with tree repositioning
SQL Operations:
-- Load existing data
SELECT * FROM expensestype WHERE expensestypeid = ?
-- Update expense type
UPDATE expensestype SET
expensestypename = ?, expensestypedetails = ?, expensestypedate = ?,
conditions = ?, userid = ?, parent = ?, type = ?, saveid = ?,
addOnlyGroupIds = ?, treeType = ?, treeId = ?,
withinsupervision_ratio = ?, supervision_ratiotype = ?, supervision_amount = ?
WHERE expensestypeid = ?
-- Get parent data for tree update
SELECT treeId FROM expensestype WHERE expensestypeid = ?
-- Update account tree
SELECT * FROM accountstree WHERE id = ?
UPDATE accountstree SET name = ?, customName = ?, parent = ? WHERE id = ?
Business Logic:
editTreeElement()
expensesTypeController.php?do=delete&id={expensestypeid}
Method: GET
Purpose: Delete expense type with comprehensive dependency checking
SQL Operations:
-- Check for expense dependencies
SELECT * FROM expenses WHERE expensestypeid = ?
-- Check for child expense types
SELECT * FROM expensestype WHERE parent = ?
-- If no dependencies exist:
-- Load expense type for tree deletion
SELECT * FROM expensestype WHERE expensestypeid = ?
-- Delete from account tree
DELETE FROM accountstree WHERE name = ?
-- Soft delete expense type
UPDATE expensestype SET
expensestypedate = ?, conditions = 1, userid = ?
WHERE expensestypeid = ?
Business Logic:
delTreeElement()
/api/v1/expense-types
GET /api/v1/expense-types
Query Parameters:
page: Page number (default: 1)
limit: Items per page (default: 20, max: 100)
search: Search in expense type name
parent_id: Filter by parent expense type
tree_type: Filter by tree classification (0, 1, 2)
save_id: Filter by cash box assignment
conditions: Filter by status (0=active, 1=deleted)
with_hierarchy: Boolean, return hierarchical tree structure
leaf_only: Boolean, return only leaf nodes (no children)
{
"success": true,
"data": [
{
"expensestypeid": 1,
"expensestypename": "مصروفات عمومية",
"expensestypedetails": "المصروفات الأساسية للشركة",
"expensestypedate": "2024-01-15",
"conditions": 0,
"userid": 1,
"parent": 0,
"type": 0,
"saveid": 1,
"addOnlyGroupIds": "1,2,3",
"treeType": 0,
"treeId": 414,
"withinsupervision_ratio": 1,
"supervision_ratiotype": 1,
"supervision_amount": 5000.00,
"webApiId": 0,
"created_by": "Admin User",
"parent_name": null,
"save_name": "الخزنة الرئيسية",
"children_count": 5,
"has_expenses": false,
"hierarchy_level": 0,
"user_groups": [
{"id": 1, "name": "مديرين"},
{"id": 2, "name": "محاسبين"}
]
}
],
"hierarchy": [
{
"expensestypeid": 1,
"expensestypename": "مصروفات عمومية",
"level": 0,
"children": [
{
"expensestypeid": 5,
"expensestypename": "مصروفات كهرباء",
"level": 1,
"children": []
}
]
}
],
"pagination": {
"current_page": 1,
"total_pages": 8,
"total_items": 147,
"per_page": 20
}
}
GET /api/v1/expense-types/{id}
Response:
{
"success": true,
"data": {
"expensestypeid": 1,
"expensestypename": "مصروفات عمومية",
"expensestypedetails": "المصروفات الأساسية للشركة",
"expensestypedate": "2024-01-15",
"conditions": 0,
"userid": 1,
"parent": 0,
"type": 0,
"saveid": 1,
"treeType": 0,
"treeId": 414,
"supervision": {
"withinsupervision_ratio": 1,
"supervision_ratiotype": 1,
"supervision_amount": 5000.00
},
"permissions": {
"addOnlyGroupIds": "1,2,3",
"user_groups": [
{"id": 1, "name": "مديرين"},
{"id": 2, "name": "محاسبين"}
]
},
"relationships": {
"parent": {
"expensestypeid": 0,
"expensestypename": null
},
"children": [
{
"expensestypeid": 5,
"expensestypename": "مصروفات كهرباء"
}
],
"save": {
"saveid": 1,
"savename": "الخزنة الرئيسية"
}
},
"accounting": {
"treeId": 414,
"account_name": "مصروفات عمومية",
"account_parent": 413
},
"statistics": {
"children_count": 5,
"total_expenses": 125000.50,
"expense_records_count": 245
},
"created_by": "Admin User",
"webApiId": 0
}
}
POST /api/v1/expense-types
Request Body:
{
"expensestypename": "مصروفات جديدة",
"expensestypedetails": "وصف نوع المصروف",
"parent": 1,
"type": 0,
"saveid": 1,
"treeType": 0,
"supervision": {
"withinsupervision_ratio": 1,
"supervision_ratiotype": 1,
"supervision_amount": 1000.00
},
"addOnlyGroupIds": [1, 2, 3],
"webApiId": 0
}
Response:
{
"success": true,
"message": "تم إنشاء نوع المصروف بنجاح",
"message_en": "Expense type created successfully",
"data": {
"expensestypeid": 25,
"expensestypename": "مصروفات جديدة",
"treeId": 455,
"account_created": true,
"hierarchy_level": 1,
"parent_name": "مصروفات عمومية"
}
}
PUT /api/v1/expense-types/{id}
Request Body: (Same as create)
Response:
{
"success": true,
"message": "تم تحديث نوع المصروف بنجاح",
"message_en": "Expense type updated successfully",
"data": {
"expensestypeid": 25,
"expensestypename": "مصروفات محدثة",
"hierarchy_changed": true,
"account_updated": true
}
}
DELETE /api/v1/expense-types/{id}
Response Success:
{
"success": true,
"message": "تم حذف نوع المصروف بنجاح",
"message_en": "Expense type deleted successfully"
}
Response Error (Has Dependencies):
{
"success": false,
"error": {
"code": "EXPENSE_TYPE_HAS_DEPENDENCIES",
"message": "لا يمكن حذف هذا النوع لأنه مرتبط ببيانات أخرى",
"message_en": "Cannot delete this expense type as it has related data",
"details": {
"child_types": 3,
"expense_records": 15,
"blocking_reasons": [
"Has 3 child expense types",
"Has 15 expense records"
]
}
}
}
GET /api/v1/expense-types/hierarchy
Query Parameters:
save_id: Filter by cash box
tree_type: Filter by tree classification
exclude_id: Exclude specific expense type (for parent selection)
{
"success": true,
"data": {
"tree": [
{
"expensestypeid": 1,
"expensestypename": "مصروفات عمومية",
"level": 0,
"has_children": true,
"children": [
{
"expensestypeid": 5,
"expensestypename": "_مصروفات كهرباء",
"level": 1,
"has_children": false,
"children": []
}
]
}
],
"flat_list": [
{
"expensestypeid": 1,
"expensestypename": "مصروفات عمومية",
"level": 0,
"formatted_name": "مصروفات عمومية"
},
{
"expensestypeid": 5,
"expensestypename": "مصروفات كهرباء",
"level": 1,
"formatted_name": "_ مصروفات كهرباء"
}
]
}
}
POST /api/v1/expense-types/bulk
Request Body:
{
"operation": "delete",
"expense_type_ids": [5, 7, 9, 12]
}
Response:
{
"success": true,
"message": "تمت العملية المجمعة بنجاح",
"results": [
{
"expensestypeid": 5,
"expensestypename": "مصروفات كهرباء",
"status": "success",
"message": "تم الحذف بنجاح"
},
{
"expensestypeid": 7,
"expensestypename": "مصروفات مياه",
"status": "error",
"message": "لا يمكن حذف هذا النوع لأنه مرتبط ببيانات أخرى",
"details": {
"child_types": 2,
"expense_records": 5
}
}
],
"summary": {
"total": 4,
"successful": 3,
"failed": 1
}
}
GET /api/v1/expense-types/search
Query Parameters:
q: Search query
save_id: Filter by cash box
leaf_only: Boolean, only leaf nodes
limit: Maximum results (default: 50)
{
"success": true,
"data": [
{
"expensestypeid": 15,
"expensestypename": "مصروفات كهرباء",
"parent_name": "مصروفات عمومية",
"hierarchy_path": "مصروفات عمومية > مصروفات كهرباء",
"level": 1,
"has_children": false,
"total_expenses": 25000.50
}
]
}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "البيانات المدخلة غير صحيحة",
"message_en": "The given data was invalid",
"details": {
"expensestypename": ["اسم نوع المصروف مطلوب"],
"parent": ["نوع المصروف الأب غير موجود"],
"saveid": ["الخزنة المحددة غير صحيحة"]
}
}
}
// Expense type creation validation
$rules = [
'expensestypename039; => 039;required|string|max:256039;,
'expensestypedetails039; => 039;nullable|string|max:1000039;,
'parent039; => 039;nullable|integer|exists:expensestype,expensestypeid039;,
'type039; => 039;integer|in:0,1,2039;,
'saveid039; => 039;required|integer|exists:save,saveid039;,
'treeType039; => 039;integer|in:0,1,2039;,
'addOnlyGroupIds039; => 039;array039;,
'addOnlyGroupIds.*039; => 039;integer|exists:usergroup,usergroupid039;,
'supervision_amount039; => 039;numeric|min:0039;
];
// Business rule validations
">if ($parent && !ExpenseType::where('expensestypeid039;, $parent)->where(039;conditions039;, 0)->exists()) {
throw new ValidationException('Parent expense type not found or inactive039;);
}
// Prevent circular references
">if ($parent && $this->wouldCreateCircularReference(">$expensestypeid, $parent)) {
throw new ValidationException('Cannot set parent - would create circular reference039;);
}
// Dependency check before deletion
$childCount = ExpenseType::where('parent039;, $expensestypeid)->count();
$expenseCount = Expense::where('expensestypeid039;, $expensestypeid)->count();
">if (">$childCount > 0 || $expenseCount > 0) {
throw new BusinessException('Cannot delete expense type - has dependencies039;);
}
-- Base hierarchical query (from orderExtepensesTypeParentsAsTree)
SELECT expensestype.*
FROM expensestype
LEFT JOIN expenses ON expenses.expensestypeid = expensestype.expensestypeid
WHERE expenses.expensestypeid IS NULL
AND expensestype.conditions = 0
AND expensestype.parent = ?
ORDER BY expensestype.expensestypeid
-- With filters:
-- Save filter: AND (expensestype.saveid = 0 OR expensestype.saveid IN (?))
-- Tree type filter: AND expensestype.treeType = ?
-- Search filter: AND expensestype.expensestypename LIKE "%?%"
BEGIN TRANSACTION;
-- Insert expense type
INSERT INTO expensestype (
expensestypename, expensestypedetails, expensestypedate, conditions, userid,
parent, type, saveid, addOnlyGroupIds, treeType, treeId,
withinsupervision_ratio, supervision_ratiotype, supervision_amount, webApiId
) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
SET @expense_type_id = LAST_INSERT_ID();
-- Get parent tree ID if exists
IF (@parent_id > 0) THEN
SELECT treeId INTO @parent_tree_id FROM expensestype WHERE expensestypeid = @parent_id;
ELSE
-- Set default parent based on tree type
SET @parent_tree_id = CASE @tree_type
WHEN 0 THEN 414 -- مصروفات عمومية
WHEN 1 THEN 413 -- مصروفات ادارية
WHEN 2 THEN 412 -- مصروفات بيعية
END;
END IF;
-- Create account tree entry
INSERT INTO accountstree (name, parent, level, conditions, userid, creationDate)
VALUES (?, @parent_tree_id, 3, 0, ?, ?);
SET @tree_id = LAST_INSERT_ID();
-- Update expense type with tree ID
UPDATE expensestype SET treeId = @tree_id WHERE expensestypeid = @expense_type_id;
COMMIT;
BEGIN TRANSACTION;
-- Load existing data
SELECT * FROM expensestype WHERE expensestypeid = ?;
-- Update expense type
UPDATE expensestype SET
expensestypename = ?, expensestypedetails = ?, expensestypedate = ?,
userid = ?, parent = ?, type = ?, saveid = ?,
addOnlyGroupIds = ?, treeType = ?,
withinsupervision_ratio = ?, supervision_ratiotype = ?, supervision_amount = ?
WHERE expensestypeid = ?;
-- Update account tree
UPDATE accountstree SET name = ?, parent = ? WHERE id = ?;
COMMIT;
-- Check dependencies
SELECT COUNT(*) as child_count FROM expensestype WHERE parent = ?;
SELECT COUNT(*) as expense_count FROM expenses WHERE expensestypeid = ?;
-- If no dependencies, delete
IF (@child_count = 0 AND @expense_count = 0) THEN
-- Get expense type name for tree deletion
SELECT expensestypename INTO @type_name FROM expensestype WHERE expensestypeid = ?;
-- Delete from account tree
DELETE FROM accountstree WHERE name = @type_name;
-- Soft delete expense type
UPDATE expensestype SET
expensestypedate = ?, conditions = 1, userid = ?
WHERE expensestypeid = ?;
END IF;
-- Recursive hierarchy building
WITH RECURSIVE expense_tree AS (
-- Root level
SELECT expensestypeid, expensestypename, parent, 0 as level
FROM expensestype
WHERE parent = 0 AND conditions = 0
UNION ALL
-- Child levels
SELECT e.expensestypeid, e.expensestypename, e.parent, t.level + 1
FROM expensestype e
JOIN expense_tree t ON e.parent = t.expensestypeid
WHERE e.conditions = 0
)
SELECT * FROM expense_tree ORDER BY level, expensestypeid;
-- Essential indexes for expense types
CREATE INDEX idx_expensestype_name ON expensestype(expensestypename);
CREATE INDEX idx_expensestype_parent ON expensestype(parent);
CREATE INDEX idx_expensestype_conditions ON expensestype(conditions);
CREATE INDEX idx_expensestype_saveid ON expensestype(saveid);
CREATE INDEX idx_expensestype_treetype ON expensestype(treeType);
CREATE INDEX idx_expensestype_treeid ON expensestype(treeId);
-- Hierarchy query optimization
CREATE INDEX idx_expensestype_hierarchy ON expensestype(parent, conditions, expensestypeid);
-- Expense relationship index
CREATE INDEX idx_expenses_type ON expenses(expensestypeid, conditions);
-- Account tree integration
CREATE INDEX idx_accountstree_name ON accountstree(name);
CREATE INDEX idx_accountstree_parent ON accountstree(parent);
-- Daily entry system indexes
CREATE INDEX idx_dailyentry_date ON dailyentry(dailyentrydate);
CREATE INDEX idx_dailyentry_conditions ON dailyentry(conditions);
CREATE INDEX idx_dailyentrydebtor_entry ON dailyentrydebtor(dailyentryid);
CREATE INDEX idx_dailyentrydebtor_account ON dailyentrydebtor(accountstreeid);
CREATE INDEX idx_dailyentrycreditor_entry ON dailyentrycreditor(dailyentryid);
CREATE INDEX idx_dailyentrycreditor_account ON dailyentrycreditor(accountstreeid);
dailyentry - Journal Entry Header
CREATE TABLE dailyentry (
dailyentryid INT PRIMARY KEY AUTO_INCREMENT,
dailyentrydate DATE NOT NULL,
dailyentrycomment TEXT,
dailyentrytotalvalue DECIMAL(15,2) NOT NULL DEFAULT 0,
conditions TINYINT NOT NULL DEFAULT 0, -- 0=active, 1=deleted
userid INT NOT NULL,
creationDate DATETIME NOT NULL,
operationId INT NOT NULL DEFAULT 0,
operationDetailLink VARCHAR(500),
costCenterID INT NOT NULL DEFAULT 0
);
dailyentrydebtor - Debit Entries
CREATE TABLE dailyentrydebtor (
dailyentrydebtoreid INT PRIMARY KEY AUTO_INCREMENT,
dailyentryid INT NOT NULL, -- Foreign key to dailyentry
accountstreeid INT NOT NULL, -- Foreign key to accountstree
dailyentrydebtorevalue DECIMAL(15,2) NOT NULL,
dailyentrydebtorecomment TEXT,
conditions TINYINT NOT NULL DEFAULT 0,
costCenterID INT NOT NULL DEFAULT 0
);
dailyentrycreditor - Credit Entries
CREATE TABLE dailyentrycreditor (
dailyentrycreditorid INT PRIMARY KEY AUTO_INCREMENT,
dailyentryid INT NOT NULL, -- Foreign key to dailyentry
accountstreeid INT NOT NULL, -- Foreign key to accountstree
dailyentrycreditorvalue DECIMAL(15,2) NOT NULL,
dailyentrycreditorcomment TEXT,
conditions TINYINT NOT NULL DEFAULT 0,
costCenterID INT NOT NULL DEFAULT 0
);
dailyentryfun.php)
/
- ◆ Creates journal entries with debtor and creditor records
- ◆ This is the core function that maintains double-entry bookkeeping
*/
function insertEntery($dailyEntryObj, $dailyEntryDebtorArray, $dailyEntryCreditorArray,
$stopEntryTransaction = 0, $operationId = 0, $operationDetailLink = '039;) {
// Creates dailyentry record
// Creates multiple dailyentrydebtor records
// Creates multiple dailyentrycreditor records
// Calls affectAccount() for each account affected
}
/
- ◆ Creates or updates account tree elements for expense types
*/
function addTreeElement(">$name, $parent, ">$itemtype, $itemfrom, $itemtype2,
">$notes = '039;, $theOrder = 0, ">$theValue = 0, $reportid = 0) {
// itemtype: 0=expenses, 1=liabilities, 2=revenue, 3=assets, 4=capital
// itemfrom: 0=from program, 1=from tree only
// itemtype2: 0=parent node, 1=end element (used in entries)
}
/
- ◆ Updates account balances based on transaction type
*/
">function affectAccount($CreditorOrDebtorObj, $type) {
// type: 'debtor039; or 039;creditor039;
// Updates accountstree.theValue based on account type and transaction
}
INSERT INTO expensestype (...) VALUES (...)
addTreeElement())
INSERT INTO accountstree (
name, parent, itemtype, itemfrom, itemtype2,
accountNature, notes, theValue, userid, sysdate, reportid
) VALUES (?, ?, 0, 0, 1, ?, ?, 0, ?, ?, ?)
UPDATE expensestype SET treeId = ? WHERE expensestypeid = ?
INSERT INTO expenses (expensestypeid, expensesValue, ...) VALUES (...)
insertEntery())
-- Create journal header
INSERT INTO dailyentry (
dailyentrydate, dailyentrycomment, dailyentrytotalvalue,
conditions, userid, creationDate, operationId
) VALUES (?, ?, ?, 0, ?, ?, ?)
-- Create debit entry (expense account)
INSERT INTO dailyentrydebtor (
dailyentryid, accountstreeid, dailyentrydebtorevalue,
dailyentrydebtorecomment, conditions
) VALUES (?, ?, ?, ?, 0)
-- Create credit entry (cash/bank account)
INSERT INTO dailyentrycreditor (
dailyentryid, accountstreeid, dailyentrycreditorvalue,
dailyentrycreditorcomment, conditions
) VALUES (?, ?, ?, ?, 0)
affectAccount())
-- Update expense account balance (increase with debit)
UPDATE accountstree SET theValue = theValue + ? WHERE id = ?
-- Update cash/bank account balance (decrease with credit)
UPDATE accountstree SET theValue = theValue - ? WHERE id = ?
// From dailyentryfun.php - Account behavior rules
">function whatToDo(">$accountType, ">$numSign, $type) {
// $accountType: 0=expenses, 1=liabilities, 2=revenue, 3=assets, 4=capital
// $numSign: +/- amount
// $type: 'debtor039; or 039;creditor039;
switch($accountType) {
case 0: // مصروفات (Expenses)
case 3: // أصول (Assets)
// Debit increases, Credit decreases
">if ($type == 'debtor039;) return $numSign; // +amount
">if ($type == 'creditor039;) return -$numSign; // -amount
break;
case 1: // خصوم (Liabilities)
case 2: // إيرادات (Revenue)
case 4: // رأس المال (Capital)
// Debit decreases, Credit increases
">if ($type == 'debtor039;) return -$numSign; // -amount
">if ($type == 'creditor039;) return $numSign; // +amount
break;
}
}
affectplugins.php):
// Plugin mapping for expense types
$pluginMapArr = array(
'expenses039; => 411, // Main expenses account tree ID
'save039; => 40, // Cash accounts
'bank039; => 38 // Bank accounts
);
// When expense is recorded, plugin system routes to appropriate handlers
">function affectPlugin(">$whatIsIt, ">$val, ">$id, $operation, ">$elementName, $comment,
$controllerName, $costCenterID, $accountstreeid, $whichSideisIt,
$dailyEntry, $AllDailyEntryDebtor, $AllDailyEntryCreditor) {
// Routes expense transactions to proper account handling
// Updates cash/bank balances
// Creates audit trail
}
// API Endpoint: POST /api/v1/expense-types/{id}/expenses
">public function createExpense(Request $request, $expenseTypeId) {
DB::transaction(">function() ">use ($request, $expenseTypeId) {
// 1. Create expense record
$expense = Expense::create([
'expensestypeid039; => $expenseTypeId,
'expensesValue039; => $request->amount,
'expensesDetails039; => $request->description,
'saveid039; => $request->save_id,
'userid039; => Auth::id()
]);
// 2. Get expense type and account tree info
$expenseType = ExpenseType::with('accountTree039;)->find($expenseTypeId);
">$save = Save::find($request->save_id);
// 3. Prepare daily entry data
$dailyEntry = [
'dailyentrydate039; => date(039;Y-m-d039;),
'dailyentrycomment039; => "مصروف: {$expenseType->expensestypename}",
'dailyentrytotalvalue039; => $request->amount,
'operationId039; => $expense->expensesid,
'operationDetailLink039; => 039;expenses039;
];
$debtorArray = [
[
'accountstreeid039; => $expenseType->treeId, // Expense account
'value039; => $request->amount,
'comment039; => $request->description
]
];
$creditorArray = [
[
'accountstreeid039; => $save->accountTreeId, // Cash/Bank account
'value039; => $request->amount,
'comment039; => "دفع مصروف: {$expenseType->expensestypename}"
]
];
// 4. Create journal entry (calls insertEntery function)
insertEntery($dailyEntry, $debtorArray, $creditorArray, 0,
$expense->expensesid, 'expenses039;);
});
}
curl -X POST http://localhost/api/v1/expense-types \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-jwt-token" \
-d '{
"expensestypename": "مصروفات صيانة",
"expensestypedetails": "مصروفات صيانة المعدات والأجهزة",
"parent": 1,
"type": 0,
"saveid": 1,
"treeType": 0,
"supervision": {
"withinsupervision_ratio": 1,
"supervision_ratiotype": 1,
"supervision_amount": 2000.00
},
"addOnlyGroupIds": [1, 2]
}'
curl -X GET "http://localhost/api/v1/expense-types/hierarchy?save_id=1&tree_type=0" \
-H "Authorization: Bearer your-jwt-token"
curl -X POST http://localhost/api/v1/expense-types/bulk \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-jwt-token" \
-d '{
"operation": "delete",
"expense_type_ids": [5, 7, 9]
}'
class ExpenseTypeAPI {
constructor(baseURL, token) {
this.baseURL = baseURL;
this.token = token;
}
async getExpenseTypes(filters = {}) {
const params = new URLSearchParams(filters);
const response = ">await fetch(${this.baseURL}/api/v1/expense-types?${params}, {
headers: { 'Authorization039;: Bearer ${this.token} }
});
return await response.json();
}
async getHierarchy(filters = {}) {
const params = new URLSearchParams(filters);
const response = ">await fetch(${this.baseURL}/api/v1/expense-types/hierarchy?${params}, {
headers: { 'Authorization039;: Bearer ${this.token} }
});
return await response.json();
}
async createExpenseType(expenseTypeData) {
const response = ">await fetch(${this.baseURL}/api/v1/expense-types, {
method: 'POST039;,
headers: {
'Content-Type039;: 039;application/json039;,
'Authorization039;: Bearer ${this.token}
},
body: JSON.stringify(expenseTypeData)
});
return await response.json();
}
async updateExpenseType(expenseTypeId, updateData) {
const response = ">await fetch(${this.baseURL}/api/v1/expense-types/${expenseTypeId}, {
method: 'PUT039;,
headers: {
'Content-Type039;: 039;application/json039;,
'Authorization039;: Bearer ${this.token}
},
body: JSON.stringify(updateData)
});
return await response.json();
}
async deleteExpenseType(expenseTypeId) {
const response = ">await fetch(${this.baseURL}/api/v1/expense-types/${expenseTypeId}, {
method: 'DELETE039;,
headers: { 'Authorization039;: Bearer ${this.token} }
});
return await response.json();
}
async bulkOperation(operation, expenseTypeIds) {
const response = ">await fetch(${this.baseURL}/api/v1/expense-types/bulk, {
method: 'POST039;,
headers: {
'Content-Type039;: 039;application/json039;,
'Authorization039;: Bearer ${this.token}
},
body: JSON.stringify({
operation,
expense_type_ids: expenseTypeIds
})
});
return await response.json();
}
// Build hierarchical tree from flat data
buildTree(flatData, parentId = 0) {
const tree = [];
for (const item of flatData) {
if (item.parent === parentId) {
const children = this.buildTree(flatData, item.expensestypeid);
if (children.length > 0) {
item.children = children;
}
tree.push(item);
}
}
return tree;
}
}
// Usage
">const expenseTypeAPI = new ExpenseTypeAPI('http://localhost039;, 039;your-token039;);
// Get hierarchical expense types
const hierarchy = await expenseTypeAPI.getHierarchy({ save_id: 1 });
console.log('Expense Type Hierarchy:039;, hierarchy);
// Create new expense type
">const newExpenseType = await expenseTypeAPI.createExpenseType({
expensestypename: 'مصروفات جديدة039;,
parent: 1,
saveid: 1,
treeType: 0
});
/api/v1/, /api/v2/
// Hierarchical caching strategy
Cache::remember("expense_type_hierarchy_{">$saveId}_{$treeType}", 1800, ">function() ">use (">$saveId, $treeType) {
return ExpenseType::buildHierarchy(">$saveId, $treeType);
});
// Optimized tree building
$expenseTypes = ExpenseType::select('expensestypeid039;, 039;expensestypename039;, 039;parent039;, 039;conditions039;)
->where('conditions039;, 0)
->where(">function($query) ">use ($saveId) {
">if ($saveId > 0) {
$query->where('saveid039;, $saveId)->orWhere(039;saveid039;, 0);
}
})
->orderBy('parent039;)
->orderBy('expensestypeid039;)
->get();
// Recursive tree building with memoization
function buildTree(">$items, $parentId = 0, &$memo = []) {
$key = "tree_{$parentId}";
">if (isset(">$memo[$key])) {
return ">$memo[$key];
}
$tree = [];
foreach (">$items as $item) {
">if ($item->parent == $parentId) {
$children = buildTree(">$items, ">$item->expensestypeid, $memo);
">if (!empty($children)) {
$item->children = $children;
}
$tree[] = $item;
}
}
">$memo[">$key] = $tree;
return $tree;
}
">public function test_can_create_expense_type_with_accounting_integration()
{
$expenseTypeData = [
'expensestypename039; => 039;Test Expense Type039;,
'parent039; => 1,
'saveid039; => 1,
'treeType039; => 0,
'addOnlyGroupIds039; => [1, 2]
];
$response = ">$this->postJson('/api/v1/expense-types039;, $expenseTypeData);
$response->assertStatus(201)
->assertJson([
'success039; => true,
'data039; => [
'expensestypename039; => 039;Test Expense Type039;
]
]);
// Verify database records
$this->assertDatabaseHas('expensestype039;, [
'expensestypename039; => 039;Test Expense Type039;
]);
// Verify accounting integration
$this->assertDatabaseHas('accountstree039;, [
'name039; => 039;Test Expense Type039;
]);
}
">public function test_cannot_delete_expense_type_with_children()
{
$parent = ExpenseType::factory()->create();
$child = ExpenseType::factory()->create(['parent039; => $parent->expensestypeid]);
$response = ">$this->deleteJson("/api/v1/expense-types/{$parent->expensestypeid}");
$response->assertStatus(422)
->assertJson([
'success039; => false,
'error039; => [
'code039; => 039;EXPENSE_TYPE_HAS_DEPENDENCIES039;
]
]);
}
">public function test_builds_hierarchical_tree_correctly()
{
$parent1 = ExpenseType::factory()->create(['parent039; => 0]);
$parent2 = ExpenseType::factory()->create(['parent039; => 0]);
$child1 = ExpenseType::factory()->create(['parent039; => $parent1->expensestypeid]);
$child2 = ExpenseType::factory()->create(['parent039; => $parent1->expensestypeid]);
$response = $this->getJson('/api/v1/expense-types/hierarchy039;);
$response->assertStatus(200)
->assertJsonStructure([
'success039;,
'data039; => [
'tree039; => [
'*039; => [
'expensestypeid039;,
'expensestypename039;,
'level039;,
'children039;
]
]
]
]);
$tree = $response->json('data.tree039;);
$this->assertCount(2, $tree); // Two root level items
$this->assertCount(2, $tree[0]['children039;]); // Parent1 has 2 children
}
---
addTreeElement() and editTreeElement() documented
dailyentryfun.php)
accountstree)