{"sha":"86424db2bf27231b3de07ffa5810bd64cbb522f6","node_id":"C_kwDOLgQlp9oAKDg2NDI0ZGIyYmYyNzIzMWIzZGUwN2ZmYTU4MTBiZDY0Y2JiNTIyZjY","commit":{"author":{"name":"Sigurd Nes","email":"sigurdne@users.noreply.github.com","date":"2026-07-03T16:49:13Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-07-03T16:49:13Z"},"message":"APP: Prototype for twig/Client/Rest-API (#1223)","tree":{"sha":"d22db2241afbd0c73ea50e4a2d42628a6d303f9e","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/git/trees/d22db2241afbd0c73ea50e4a2d42628a6d303f9e"},"url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/git/commits/86424db2bf27231b3de07ffa5810bd64cbb522f6","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqR+gJCRC1aQ7uu5UhlAAAARoQAIHJin3ls76jLjsQiuUWCTHN\nkYkmP/2KRBt2gRZt3BvhNqSozb7hoETVcDZIByWeMBkvnq0XJRZT/tj8/47IP+S9\n9cZRvyUDhTBtE3w5rbMMqHTl4seGDwVw1CHwEu/0wlCXS+dAreoUkUPzJdKIdHZJ\nnYykodneWndWGiW9xyzCVrcTEcPS5/5GZwDN1IHTrz6wPyydDoMhmOJmM0JU6rg+\nrO7E3wuyRSnvMFevIs2jvZI6ufhypUNv7ZVzhX8GTXRf7et9XDE6NIeslXum/xFh\n/oEFjWC+O0jWBd5rpe2Y+JCCmgZ77HjNBNUe3KhC6H/0ViINApko59IXXXvyFHZ+\nSl8wmuQRGKO5ESJrK3iPOhSCYFygZPomP9JL+yp3LxElX0HNj87TtpD2b4c/Od9P\nYgY/rrmqnq4ddHvwqX/2GCIN1wya07byqxgcno7wMI9Nn3hV/KZnMt88cBpK5Aui\nJh9pyb87ld8Tf3W3c7Pj+DhHTzAZR+sXwwaaW5B7WstgHAhylhX8TwZL5CsnKTG5\nA4JW172bqKRb/T4iMluj+dcgnJS91P9mXBn96QVLal3XX22vBhY0oQPaQJw2qnMO\nLerFnnRbJT2jXDqfE5nb+7HpBOf/BDZNh/Nd0WF7GWkYsvmoeBu6TtVdg5wYr+pK\n1cZ86VK1lsjnE/paxnf9\n=ZAJS\n-----END PGP SIGNATURE-----\n","payload":"tree d22db2241afbd0c73ea50e4a2d42628a6d303f9e\nparent 21925c2e01c1a258820e13ea25438a067b5f6a0f\nauthor Sigurd Nes <sigurdne@users.noreply.github.com> 1783097353 +0200\ncommitter GitHub <noreply@github.com> 1783097353 +0200\n\nAPP: Prototype for twig/Client/Rest-API (#1223)","verified_at":"2026-07-03T16:49:14Z"}},"url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/86424db2bf27231b3de07ffa5810bd64cbb522f6","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/86424db2bf27231b3de07ffa5810bd64cbb522f6","comments_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/86424db2bf27231b3de07ffa5810bd64cbb522f6/comments","author":{"login":"sigurdne","id":12719970,"node_id":"MDQ6VXNlcjEyNzE5OTcw","avatar_url":"https://avatars.githubusercontent.com/u/12719970?v=4","gravatar_id":"","url":"https://api.github.com/users/sigurdne","html_url":"https://github.com/sigurdne","followers_url":"https://api.github.com/users/sigurdne/followers","following_url":"https://api.github.com/users/sigurdne/following{/other_user}","gists_url":"https://api.github.com/users/sigurdne/gists{/gist_id}","starred_url":"https://api.github.com/users/sigurdne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sigurdne/subscriptions","organizations_url":"https://api.github.com/users/sigurdne/orgs","repos_url":"https://api.github.com/users/sigurdne/repos","events_url":"https://api.github.com/users/sigurdne/events{/privacy}","received_events_url":"https://api.github.com/users/sigurdne/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"21925c2e01c1a258820e13ea25438a067b5f6a0f","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/21925c2e01c1a258820e13ea25438a067b5f6a0f","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/21925c2e01c1a258820e13ea25438a067b5f6a0f"}],"stats":{"total":11874,"additions":9287,"deletions":2587},"files":[{"sha":"f206d1780c64b3fdcd446fb5448b5974beb47db9","filename":"composer.json","status":"modified","additions":2,"deletions":1,"changes":3,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/composer.json","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/composer.json","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/composer.json?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -32,7 +32,8 @@\n \t\t\"predis/predis\": \"^2.3\",\n \t\t\"lbuchs/webauthn\": \"^2.0\",\n \t\t\"webklex/php-imap\": \"^6.0\",\n-\t\t\"twig/twig\": \"^3.23\"\n+\t\t\"twig/twig\": \"^3.23\",\n+\t\t\"slim/csrf\": \"^1.5.1\"\n \t},\n \t\"require-dev\": {\n \t\t\"phpunit/phpunit\": \"^9.0\","},{"sha":"0f72cfdab5af7cc39ba926a3dfd7601debc9f63d","filename":"composer.lock","status":"modified","additions":205,"deletions":159,"changes":364,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/composer.lock","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/composer.lock","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/composer.lock?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -4,7 +4,7 @@\n         \"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies\",\n         \"This file is @generated automatically\"\n     ],\n-    \"content-hash\": \"27e722760e6234bd35e8b460955b4eac\",\n+    \"content-hash\": \"ae1aa1f64529bc9588f6d85c9b4e4c61\",\n     \"packages\": [\n         {\n             \"name\": \"amnuts/opcache-gui\",\n@@ -70,23 +70,22 @@\n         },\n         {\n             \"name\": \"brick/math\",\n-            \"version\": \"0.14.8\",\n+            \"version\": \"0.18.0\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/brick/math.git\",\n-                \"reference\": \"63422359a44b7f06cae63c3b429b59e8efcc0629\"\n+                \"reference\": \"82944324d1c1bdb2c2618e89978d4e2ad78d69ad\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629\",\n-                \"reference\": \"63422359a44b7f06cae63c3b429b59e8efcc0629\",\n+                \"url\": \"https://api.github.com/repos/brick/math/zipball/82944324d1c1bdb2c2618e89978d4e2ad78d69ad\",\n+                \"reference\": \"82944324d1c1bdb2c2618e89978d4e2ad78d69ad\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n                 \"php\": \"^8.2\"\n             },\n             \"require-dev\": {\n-                \"php-coveralls/php-coveralls\": \"^2.2\",\n                 \"phpstan/phpstan\": \"2.1.22\",\n                 \"phpunit/phpunit\": \"^11.5\"\n             },\n@@ -118,15 +117,15 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/brick/math/issues\",\n-                \"source\": \"https://github.com/brick/math/tree/0.14.8\"\n+                \"source\": \"https://github.com/brick/math/tree/0.18.0\"\n             },\n             \"funding\": [\n                 {\n                     \"url\": \"https://github.com/BenMorel\",\n                     \"type\": \"github\"\n                 }\n             ],\n-            \"time\": \"2026-02-10T14:33:43+00:00\"\n+            \"time\": \"2026-06-14T18:21:03+00:00\"\n         },\n         {\n             \"name\": \"carbonphp/carbon-doctrine-types\",\n@@ -1181,26 +1180,26 @@\n         },\n         {\n             \"name\": \"guzzlehttp/guzzle\",\n-            \"version\": \"7.11.1\",\n+            \"version\": \"7.13.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/guzzle/guzzle.git\",\n-                \"reference\": \"5af96f374e0ab4ebd747b8310888c99d3adb0a8c\"\n+                \"reference\": \"55901a76dfd2006a0cc012b9e3c5b487f796478d\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/guzzle/guzzle/zipball/5af96f374e0ab4ebd747b8310888c99d3adb0a8c\",\n-                \"reference\": \"5af96f374e0ab4ebd747b8310888c99d3adb0a8c\",\n+                \"url\": \"https://api.github.com/repos/guzzle/guzzle/zipball/55901a76dfd2006a0cc012b9e3c5b487f796478d\",\n+                \"reference\": \"55901a76dfd2006a0cc012b9e3c5b487f796478d\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n                 \"ext-json\": \"*\",\n                 \"guzzlehttp/promises\": \"^2.5\",\n-                \"guzzlehttp/psr7\": \"^2.11\",\n+                \"guzzlehttp/psr7\": \"^2.12.3\",\n                 \"php\": \"^7.2.5 || ^8.0\",\n                 \"psr/http-client\": \"^1.0\",\n                 \"symfony/deprecation-contracts\": \"^2.5 || ^3.0\",\n-                \"symfony/polyfill-php80\": \"^1.24\"\n+                \"symfony/polyfill-php80\": \"^1.25\"\n             },\n             \"provide\": {\n                 \"psr/http-client-implementation\": \"1.0\"\n@@ -1209,7 +1208,7 @@\n                 \"bamarni/composer-bin-plugin\": \"^1.8.2\",\n                 \"ext-curl\": \"*\",\n                 \"guzzle/client-integration-tests\": \"3.0.2\",\n-                \"guzzlehttp/test-server\": \"^0.5\",\n+                \"guzzlehttp/test-server\": \"^0.6\",\n                 \"php-http/message-factory\": \"^1.1\",\n                 \"phpunit/phpunit\": \"^8.5.52 || ^9.6.34\",\n                 \"psr/log\": \"^1.1 || ^2.0 || ^3.0\"\n@@ -1289,7 +1288,7 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/guzzle/guzzle/issues\",\n-                \"source\": \"https://github.com/guzzle/guzzle/tree/7.11.1\"\n+                \"source\": \"https://github.com/guzzle/guzzle/tree/7.13.1\"\n             },\n             \"funding\": [\n                 {\n@@ -1305,7 +1304,7 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-06-07T22:54:06+00:00\"\n+            \"time\": \"2026-06-29T20:14:18+00:00\"\n         },\n         {\n             \"name\": \"guzzlehttp/promises\",\n@@ -1393,16 +1392,16 @@\n         },\n         {\n             \"name\": \"guzzlehttp/psr7\",\n-            \"version\": \"2.11.0\",\n+            \"version\": \"2.12.3\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/guzzle/psr7.git\",\n-                \"reference\": \"bbb5e61349fa5cb822b3e87842b951088b76b81f\"\n+                \"reference\": \"7ec62dc3f44aa218487dbed81a9bf9bc647be55d\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f\",\n-                \"reference\": \"bbb5e61349fa5cb822b3e87842b951088b76b81f\",\n+                \"url\": \"https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d\",\n+                \"reference\": \"7ec62dc3f44aa218487dbed81a9bf9bc647be55d\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -1411,7 +1410,7 @@\n                 \"psr/http-message\": \"^1.1 || ^2.0\",\n                 \"ralouphie/getallheaders\": \"^3.0\",\n                 \"symfony/deprecation-contracts\": \"^2.5 || ^3.0\",\n-                \"symfony/polyfill-php80\": \"^1.24\"\n+                \"symfony/polyfill-php80\": \"^1.25\"\n             },\n             \"provide\": {\n                 \"psr/http-factory-implementation\": \"1.0\",\n@@ -1492,7 +1491,7 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/guzzle/psr7/issues\",\n-                \"source\": \"https://github.com/guzzle/psr7/tree/2.11.0\"\n+                \"source\": \"https://github.com/guzzle/psr7/tree/2.12.3\"\n             },\n             \"funding\": [\n                 {\n@@ -1508,7 +1507,7 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-06-02T12:30:48+00:00\"\n+            \"time\": \"2026-06-23T15:21:08+00:00\"\n         },\n         {\n             \"name\": \"html2text/html2text\",\n@@ -1550,16 +1549,16 @@\n         },\n         {\n             \"name\": \"illuminate/collections\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/collections.git\",\n-                \"reference\": \"1dbd2611b2cd025d3526e64bd7670033ff883173\"\n+                \"reference\": \"8f10727c854250bd7c4c50cd04610722dd3007b5\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/illuminate/collections/zipball/1dbd2611b2cd025d3526e64bd7670033ff883173\",\n-                \"reference\": \"1dbd2611b2cd025d3526e64bd7670033ff883173\",\n+                \"url\": \"https://api.github.com/repos/illuminate/collections/zipball/8f10727c854250bd7c4c50cd04610722dd3007b5\",\n+                \"reference\": \"8f10727c854250bd7c4c50cd04610722dd3007b5\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -1606,11 +1605,11 @@\n                 \"issues\": \"https://github.com/laravel/framework/issues\",\n                 \"source\": \"https://github.com/laravel/framework\"\n             },\n-            \"time\": \"2026-05-19T14:10:53+00:00\"\n+            \"time\": \"2026-06-25T16:46:05+00:00\"\n         },\n         {\n             \"name\": \"illuminate/conditionable\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/conditionable.git\",\n@@ -1656,16 +1655,16 @@\n         },\n         {\n             \"name\": \"illuminate/contracts\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/contracts.git\",\n-                \"reference\": \"dc805389d17f13821726423bf4fc90d3162bf90d\"\n+                \"reference\": \"a108b67e086a933e92abebe69bb8e15c1218d3c8\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/illuminate/contracts/zipball/dc805389d17f13821726423bf4fc90d3162bf90d\",\n-                \"reference\": \"dc805389d17f13821726423bf4fc90d3162bf90d\",\n+                \"url\": \"https://api.github.com/repos/illuminate/contracts/zipball/a108b67e086a933e92abebe69bb8e15c1218d3c8\",\n+                \"reference\": \"a108b67e086a933e92abebe69bb8e15c1218d3c8\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -1700,11 +1699,11 @@\n                 \"issues\": \"https://github.com/laravel/framework/issues\",\n                 \"source\": \"https://github.com/laravel/framework\"\n             },\n-            \"time\": \"2026-05-21T13:18:48+00:00\"\n+            \"time\": \"2026-06-25T18:32:48+00:00\"\n         },\n         {\n             \"name\": \"illuminate/macroable\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/macroable.git\",\n@@ -1750,16 +1749,16 @@\n         },\n         {\n             \"name\": \"illuminate/pagination\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/pagination.git\",\n-                \"reference\": \"0e788e59a857f4c6fd6f084fc4848714c8cd5bbb\"\n+                \"reference\": \"20f10920affbea754c374c0eea99b17256c41573\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/illuminate/pagination/zipball/0e788e59a857f4c6fd6f084fc4848714c8cd5bbb\",\n-                \"reference\": \"0e788e59a857f4c6fd6f084fc4848714c8cd5bbb\",\n+                \"url\": \"https://api.github.com/repos/illuminate/pagination/zipball/20f10920affbea754c374c0eea99b17256c41573\",\n+                \"reference\": \"20f10920affbea754c374c0eea99b17256c41573\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -1796,20 +1795,20 @@\n                 \"issues\": \"https://github.com/laravel/framework/issues\",\n                 \"source\": \"https://github.com/laravel/framework\"\n             },\n-            \"time\": \"2026-05-13T14:00:22+00:00\"\n+            \"time\": \"2026-06-25T16:46:05+00:00\"\n         },\n         {\n             \"name\": \"illuminate/reflection\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/reflection.git\",\n-                \"reference\": \"4fe1659f068ab2b50131cf906c5d8bba4e34df0c\"\n+                \"reference\": \"178cdb5eb08c5369ae6b71518e557bed68e74577\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/illuminate/reflection/zipball/4fe1659f068ab2b50131cf906c5d8bba4e34df0c\",\n-                \"reference\": \"4fe1659f068ab2b50131cf906c5d8bba4e34df0c\",\n+                \"url\": \"https://api.github.com/repos/illuminate/reflection/zipball/178cdb5eb08c5369ae6b71518e557bed68e74577\",\n+                \"reference\": \"178cdb5eb08c5369ae6b71518e557bed68e74577\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -1847,20 +1846,20 @@\n                 \"issues\": \"https://github.com/laravel/framework/issues\",\n                 \"source\": \"https://github.com/laravel/framework\"\n             },\n-            \"time\": \"2026-03-10T20:04:12+00:00\"\n+            \"time\": \"2026-06-18T15:47:27+00:00\"\n         },\n         {\n             \"name\": \"illuminate/support\",\n-            \"version\": \"v13.15.0\",\n+            \"version\": \"v13.18.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/illuminate/support.git\",\n-                \"reference\": \"1283a6f6e6b6b28e17bb92280a71a832294814c0\"\n+                \"reference\": \"06bffd844a3ac5671b7933b37b67d6e16e57f1af\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/illuminate/support/zipball/1283a6f6e6b6b28e17bb92280a71a832294814c0\",\n-                \"reference\": \"1283a6f6e6b6b28e17bb92280a71a832294814c0\",\n+                \"url\": \"https://api.github.com/repos/illuminate/support/zipball/06bffd844a3ac5671b7933b37b67d6e16e57f1af\",\n+                \"reference\": \"06bffd844a3ac5671b7933b37b67d6e16e57f1af\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -1926,7 +1925,7 @@\n                 \"issues\": \"https://github.com/laravel/framework/issues\",\n                 \"source\": \"https://github.com/laravel/framework\"\n             },\n-            \"time\": \"2026-06-08T13:47:24+00:00\"\n+            \"time\": \"2026-07-01T18:20:12+00:00\"\n         },\n         {\n             \"name\": \"infamoustrey/smartsheet\",\n@@ -2240,16 +2239,16 @@\n         },\n         {\n             \"name\": \"league/flysystem\",\n-            \"version\": \"3.34.0\",\n+            \"version\": \"3.35.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/thephpleague/flysystem.git\",\n-                \"reference\": \"2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e\"\n+                \"reference\": \"f23af6c5aafd958a7593029a271d77baf5ed793c\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e\",\n-                \"reference\": \"2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e\",\n+                \"url\": \"https://api.github.com/repos/thephpleague/flysystem/zipball/f23af6c5aafd958a7593029a271d77baf5ed793c\",\n+                \"reference\": \"f23af6c5aafd958a7593029a271d77baf5ed793c\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -2317,9 +2316,9 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/thephpleague/flysystem/issues\",\n-                \"source\": \"https://github.com/thephpleague/flysystem/tree/3.34.0\"\n+                \"source\": \"https://github.com/thephpleague/flysystem/tree/3.35.1\"\n             },\n-            \"time\": \"2026-05-14T10:28:08+00:00\"\n+            \"time\": \"2026-06-25T06:52:23+00:00\"\n         },\n         {\n             \"name\": \"league/flysystem-ftp\",\n@@ -3315,16 +3314,16 @@\n         },\n         {\n             \"name\": \"nesbot/carbon\",\n-            \"version\": \"3.12.3\",\n+            \"version\": \"3.13.0\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/CarbonPHP/carbon.git\",\n-                \"reference\": \"6e7853a668c3107294aff38d42bf760ec02126b6\"\n+                \"reference\": \"40f6618f052df16b545f626fbf9a878e6497d16a\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/CarbonPHP/carbon/zipball/6e7853a668c3107294aff38d42bf760ec02126b6\",\n-                \"reference\": \"6e7853a668c3107294aff38d42bf760ec02126b6\",\n+                \"url\": \"https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a\",\n+                \"reference\": \"40f6618f052df16b545f626fbf9a878e6497d16a\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -3416,7 +3415,7 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-06-14T20:41:42+00:00\"\n+            \"time\": \"2026-06-18T13:49:15+00:00\"\n         },\n         {\n             \"name\": \"nikic/fast-route\",\n@@ -5435,20 +5434,20 @@\n         },\n         {\n             \"name\": \"ramsey/uuid\",\n-            \"version\": \"4.9.2\",\n+            \"version\": \"4.9.3\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/ramsey/uuid.git\",\n-                \"reference\": \"8429c78ca35a09f27565311b98101e2826affde0\"\n+                \"reference\": \"1df15849d00943a67d677dc9cfd80795f038c9f8\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0\",\n-                \"reference\": \"8429c78ca35a09f27565311b98101e2826affde0\",\n+                \"url\": \"https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8\",\n+                \"reference\": \"1df15849d00943a67d677dc9cfd80795f038c9f8\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n-                \"brick/math\": \"^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14\",\n+                \"brick/math\": \">=0.8.16 <=0.18\",\n                 \"php\": \"^8.0\",\n                 \"ramsey/collection\": \"^1.2 || ^2.0\"\n             },\n@@ -5507,9 +5506,9 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/ramsey/uuid/issues\",\n-                \"source\": \"https://github.com/ramsey/uuid/tree/4.9.2\"\n+                \"source\": \"https://github.com/ramsey/uuid/tree/4.9.3\"\n             },\n-            \"time\": \"2025-12-14T04:43:48+00:00\"\n+            \"time\": \"2026-06-18T03:57:49+00:00\"\n         },\n         {\n             \"name\": \"ratchet/rfc6455\",\n@@ -6172,6 +6171,62 @@\n             },\n             \"time\": \"2023-04-20T20:52:13+00:00\"\n         },\n+        {\n+            \"name\": \"slim/csrf\",\n+            \"version\": \"1.5.1\",\n+            \"source\": {\n+                \"type\": \"git\",\n+                \"url\": \"https://github.com/slimphp/Slim-Csrf.git\",\n+                \"reference\": \"a476a61e38451e138c400f6b4ca96037f3c2dd39\"\n+            },\n+            \"dist\": {\n+                \"type\": \"zip\",\n+                \"url\": \"https://api.github.com/repos/slimphp/Slim-Csrf/zipball/a476a61e38451e138c400f6b4ca96037f3c2dd39\",\n+                \"reference\": \"a476a61e38451e138c400f6b4ca96037f3c2dd39\",\n+                \"shasum\": \"\"\n+            },\n+            \"require\": {\n+                \"php\": \"^7.4 || ^8.0\",\n+                \"psr/http-factory\": \"^1.1\",\n+                \"psr/http-message\": \"^1.0 || ^2.0\",\n+                \"psr/http-server-handler\": \"^1.0\",\n+                \"psr/http-server-middleware\": \"^1.0\"\n+            },\n+            \"require-dev\": {\n+                \"phpunit/phpunit\": \"^9.6\",\n+                \"squizlabs/php_codesniffer\": \"^3.10\"\n+            },\n+            \"type\": \"library\",\n+            \"autoload\": {\n+                \"psr-4\": {\n+                    \"Slim\\\\Csrf\\\\\": \"src\"\n+                }\n+            },\n+            \"notification-url\": \"https://packagist.org/downloads/\",\n+            \"license\": [\n+                \"MIT\"\n+            ],\n+            \"authors\": [\n+                {\n+                    \"name\": \"Josh Lockhart\",\n+                    \"email\": \"hello@joshlockhart.com\",\n+                    \"homepage\": \"http://joshlockhart.com\"\n+                }\n+            ],\n+            \"description\": \"Slim Framework 4 CSRF protection PSR-15 middleware\",\n+            \"homepage\": \"https://www.slimframework.com\",\n+            \"keywords\": [\n+                \"csrf\",\n+                \"framework\",\n+                \"middleware\",\n+                \"slim\"\n+            ],\n+            \"support\": {\n+                \"issues\": \"https://github.com/slimphp/Slim-Csrf/issues\",\n+                \"source\": \"https://github.com/slimphp/Slim-Csrf/tree/1.5.1\"\n+            },\n+            \"time\": \"2025-11-02T14:58:28+00:00\"\n+        },\n         {\n             \"name\": \"slim/psr7\",\n             \"version\": \"1.8.0\",\n@@ -6408,16 +6463,16 @@\n         },\n         {\n             \"name\": \"swagger-api/swagger-ui\",\n-            \"version\": \"v5.32.6\",\n+            \"version\": \"v5.32.8\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/swagger-api/swagger-ui.git\",\n-                \"reference\": \"dcdca62c8b64a0a54e4decd4e1a6c3c712fdcc60\"\n+                \"reference\": \"4e0d3f88de7db0395a80ef0541ff428f0926d484\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/swagger-api/swagger-ui/zipball/dcdca62c8b64a0a54e4decd4e1a6c3c712fdcc60\",\n-                \"reference\": \"dcdca62c8b64a0a54e4decd4e1a6c3c712fdcc60\",\n+                \"url\": \"https://api.github.com/repos/swagger-api/swagger-ui/zipball/4e0d3f88de7db0395a80ef0541ff428f0926d484\",\n+                \"reference\": \"4e0d3f88de7db0395a80ef0541ff428f0926d484\",\n                 \"shasum\": \"\"\n             },\n             \"type\": \"library\",\n@@ -6463,28 +6518,27 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/swagger-api/swagger-ui/issues\",\n-                \"source\": \"https://github.com/swagger-api/swagger-ui/tree/v5.32.6\"\n+                \"source\": \"https://github.com/swagger-api/swagger-ui/tree/v5.32.8\"\n             },\n-            \"time\": \"2026-05-12T09:35:37+00:00\"\n+            \"time\": \"2026-06-23T08:15:35+00:00\"\n         },\n         {\n             \"name\": \"symfony/clock\",\n-            \"version\": \"v7.4.8\",\n+            \"version\": \"v8.1.0\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/clock.git\",\n-                \"reference\": \"674fa3b98e21531dd040e613479f5f6fa8f32111\"\n+                \"reference\": \"701ef4de9705d6c32292ebee5e8044094a09fbf6\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111\",\n-                \"reference\": \"674fa3b98e21531dd040e613479f5f6fa8f32111\",\n+                \"url\": \"https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6\",\n+                \"reference\": \"701ef4de9705d6c32292ebee5e8044094a09fbf6\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n-                \"php\": \">=8.2\",\n-                \"psr/clock\": \"^1.0\",\n-                \"symfony/polyfill-php83\": \"^1.28\"\n+                \"php\": \">=8.4.1\",\n+                \"psr/clock\": \"^1.0\"\n             },\n             \"provide\": {\n                 \"psr/clock-implementation\": \"1.0\"\n@@ -6523,7 +6577,7 @@\n                 \"time\"\n             ],\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/clock/tree/v7.4.8\"\n+                \"source\": \"https://github.com/symfony/clock/tree/v8.1.0\"\n             },\n             \"funding\": [\n                 {\n@@ -6543,20 +6597,20 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-03-24T13:12:05+00:00\"\n+            \"time\": \"2026-05-29T05:06:50+00:00\"\n         },\n         {\n             \"name\": \"symfony/deprecation-contracts\",\n-            \"version\": \"v3.7.0\",\n+            \"version\": \"v3.7.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/deprecation-contracts.git\",\n-                \"reference\": \"50f59d1f3ca46d41ac911f97a78626b6756af35b\"\n+                \"reference\": \"f3202fa1b5097b0af062dc978b32ecf63404e31d\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b\",\n-                \"reference\": \"50f59d1f3ca46d41ac911f97a78626b6756af35b\",\n+                \"url\": \"https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d\",\n+                \"reference\": \"f3202fa1b5097b0af062dc978b32ecf63404e31d\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -6594,7 +6648,7 @@\n             \"description\": \"A generic function and convention to trigger deprecation notices\",\n             \"homepage\": \"https://symfony.com\",\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/deprecation-contracts/tree/v3.7.0\"\n+                \"source\": \"https://github.com/symfony/deprecation-contracts/tree/v3.7.1\"\n             },\n             \"funding\": [\n                 {\n@@ -6614,27 +6668,27 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-04-13T15:52:40+00:00\"\n+            \"time\": \"2026-06-05T06:23:12+00:00\"\n         },\n         {\n             \"name\": \"symfony/finder\",\n-            \"version\": \"v7.4.8\",\n+            \"version\": \"v8.1.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/finder.git\",\n-                \"reference\": \"e0be088d22278583a82da281886e8c3592fbf149\"\n+                \"reference\": \"e2989e762c70f9490fa3a00a0ac0fae5aa97a531\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149\",\n-                \"reference\": \"e0be088d22278583a82da281886e8c3592fbf149\",\n+                \"url\": \"https://api.github.com/repos/symfony/finder/zipball/e2989e762c70f9490fa3a00a0ac0fae5aa97a531\",\n+                \"reference\": \"e2989e762c70f9490fa3a00a0ac0fae5aa97a531\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n-                \"php\": \">=8.2\"\n+                \"php\": \">=8.4.1\"\n             },\n             \"require-dev\": {\n-                \"symfony/filesystem\": \"^6.4|^7.0|^8.0\"\n+                \"symfony/filesystem\": \"^7.4|^8.0\"\n             },\n             \"type\": \"library\",\n             \"autoload\": {\n@@ -6662,7 +6716,7 @@\n             \"description\": \"Finds files and directories via an intuitive fluent interface\",\n             \"homepage\": \"https://symfony.com\",\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/finder/tree/v7.4.8\"\n+                \"source\": \"https://github.com/symfony/finder/tree/v8.1.1\"\n             },\n             \"funding\": [\n                 {\n@@ -6682,20 +6736,20 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-03-24T13:12:05+00:00\"\n+            \"time\": \"2026-06-27T09:05:56+00:00\"\n         },\n         {\n             \"name\": \"symfony/http-foundation\",\n-            \"version\": \"v6.4.41\",\n+            \"version\": \"v6.4.42\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/http-foundation.git\",\n-                \"reference\": \"48d76c29a67a301e0f7779a512bf76417395ffef\"\n+                \"reference\": \"23dcf8e0f59cbbfa9d2894f58fd8be6833794372\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/http-foundation/zipball/48d76c29a67a301e0f7779a512bf76417395ffef\",\n-                \"reference\": \"48d76c29a67a301e0f7779a512bf76417395ffef\",\n+                \"url\": \"https://api.github.com/repos/symfony/http-foundation/zipball/23dcf8e0f59cbbfa9d2894f58fd8be6833794372\",\n+                \"reference\": \"23dcf8e0f59cbbfa9d2894f58fd8be6833794372\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -6743,7 +6797,7 @@\n             \"description\": \"Defines an object-oriented layer for the HTTP specification\",\n             \"homepage\": \"https://symfony.com\",\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/http-foundation/tree/v6.4.41\"\n+                \"source\": \"https://github.com/symfony/http-foundation/tree/v6.4.42\"\n             },\n             \"funding\": [\n                 {\n@@ -6763,7 +6817,7 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-05-24T10:54:17+00:00\"\n+            \"time\": \"2026-06-16T10:12:24+00:00\"\n         },\n         {\n             \"name\": \"symfony/polyfill-ctype\",\n@@ -7506,52 +7560,45 @@\n         },\n         {\n             \"name\": \"symfony/translation\",\n-            \"version\": \"v7.4.10\",\n+            \"version\": \"v8.1.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/translation.git\",\n-                \"reference\": \"ada7578c30dd5feaa8259cff3e885069ea81ddde\"\n+                \"reference\": \"342b4218630dc2cf284cedcb2080c80b13404014\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde\",\n-                \"reference\": \"ada7578c30dd5feaa8259cff3e885069ea81ddde\",\n+                \"url\": \"https://api.github.com/repos/symfony/translation/zipball/342b4218630dc2cf284cedcb2080c80b13404014\",\n+                \"reference\": \"342b4218630dc2cf284cedcb2080c80b13404014\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n-                \"php\": \">=8.2\",\n-                \"symfony/deprecation-contracts\": \"^2.5|^3\",\n-                \"symfony/polyfill-mbstring\": \"~1.0\",\n-                \"symfony/translation-contracts\": \"^2.5.3|^3.3\"\n+                \"php\": \">=8.4.1\",\n+                \"symfony/polyfill-mbstring\": \"^1.0\",\n+                \"symfony/translation-contracts\": \"^3.6.1\"\n             },\n             \"conflict\": {\n                 \"nikic/php-parser\": \"<5.0\",\n-                \"symfony/config\": \"<6.4\",\n-                \"symfony/console\": \"<6.4\",\n-                \"symfony/dependency-injection\": \"<6.4\",\n                 \"symfony/http-client-contracts\": \"<2.5\",\n-                \"symfony/http-kernel\": \"<6.4\",\n-                \"symfony/service-contracts\": \"<2.5\",\n-                \"symfony/twig-bundle\": \"<6.4\",\n-                \"symfony/yaml\": \"<6.4\"\n+                \"symfony/service-contracts\": \"<2.5\"\n             },\n             \"provide\": {\n                 \"symfony/translation-implementation\": \"2.3|3.0\"\n             },\n             \"require-dev\": {\n                 \"nikic/php-parser\": \"^5.0\",\n                 \"psr/log\": \"^1|^2|^3\",\n-                \"symfony/config\": \"^6.4|^7.0|^8.0\",\n-                \"symfony/console\": \"^6.4|^7.0|^8.0\",\n-                \"symfony/dependency-injection\": \"^6.4|^7.0|^8.0\",\n-                \"symfony/finder\": \"^6.4|^7.0|^8.0\",\n+                \"symfony/config\": \"^7.4|^8.0\",\n+                \"symfony/console\": \"^7.4|^8.0\",\n+                \"symfony/dependency-injection\": \"^7.4|^8.0\",\n+                \"symfony/finder\": \"^7.4|^8.0\",\n                 \"symfony/http-client-contracts\": \"^2.5|^3.0\",\n-                \"symfony/http-kernel\": \"^6.4|^7.0|^8.0\",\n-                \"symfony/intl\": \"^6.4|^7.0|^8.0\",\n+                \"symfony/http-kernel\": \"^7.4|^8.0\",\n+                \"symfony/intl\": \"^7.4|^8.0\",\n                 \"symfony/polyfill-intl-icu\": \"^1.21\",\n-                \"symfony/routing\": \"^6.4|^7.0|^8.0\",\n+                \"symfony/routing\": \"^7.4|^8.0\",\n                 \"symfony/service-contracts\": \"^2.5|^3\",\n-                \"symfony/yaml\": \"^6.4|^7.0|^8.0\"\n+                \"symfony/yaml\": \"^7.4|^8.0\"\n             },\n             \"type\": \"library\",\n             \"autoload\": {\n@@ -7582,7 +7629,7 @@\n             \"description\": \"Provides tools to internationalize your application\",\n             \"homepage\": \"https://symfony.com\",\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/translation/tree/v7.4.10\"\n+                \"source\": \"https://github.com/symfony/translation/tree/v8.1.1\"\n             },\n             \"funding\": [\n                 {\n@@ -7602,20 +7649,20 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-05-06T11:19:24+00:00\"\n+            \"time\": \"2026-06-06T11:11:44+00:00\"\n         },\n         {\n             \"name\": \"symfony/translation-contracts\",\n-            \"version\": \"v3.7.0\",\n+            \"version\": \"v3.7.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/translation-contracts.git\",\n-                \"reference\": \"0ab302977a952b42fd51475c4ebac81f8da0a95d\"\n+                \"reference\": \"ccb206b98faccc511ebae8e5fad50f2dc0b30621\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d\",\n-                \"reference\": \"0ab302977a952b42fd51475c4ebac81f8da0a95d\",\n+                \"url\": \"https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621\",\n+                \"reference\": \"ccb206b98faccc511ebae8e5fad50f2dc0b30621\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n@@ -7664,7 +7711,7 @@\n                 \"standards\"\n             ],\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/translation-contracts/tree/v3.7.0\"\n+                \"source\": \"https://github.com/symfony/translation-contracts/tree/v3.7.1\"\n             },\n             \"funding\": [\n                 {\n@@ -7684,32 +7731,32 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-01-05T13:30:16+00:00\"\n+            \"time\": \"2026-06-05T06:23:12+00:00\"\n         },\n         {\n             \"name\": \"symfony/yaml\",\n-            \"version\": \"v7.4.13\",\n+            \"version\": \"v8.1.1\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/symfony/yaml.git\",\n-                \"reference\": \"a7ec3b1156faf8815db7683ec7c1e7338e6f977c\"\n+                \"reference\": \"8e4cdd4311683516be06944f4b85244063cdb886\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c\",\n-                \"reference\": \"a7ec3b1156faf8815db7683ec7c1e7338e6f977c\",\n+                \"url\": \"https://api.github.com/repos/symfony/yaml/zipball/8e4cdd4311683516be06944f4b85244063cdb886\",\n+                \"reference\": \"8e4cdd4311683516be06944f4b85244063cdb886\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n-                \"php\": \">=8.2\",\n-                \"symfony/deprecation-contracts\": \"^2.5|^3\",\n+                \"php\": \">=8.4.1\",\n                 \"symfony/polyfill-ctype\": \"^1.8\"\n             },\n             \"conflict\": {\n-                \"symfony/console\": \"<6.4\"\n+                \"symfony/console\": \"<7.4\"\n             },\n             \"require-dev\": {\n-                \"symfony/console\": \"^6.4|^7.0|^8.0\"\n+                \"symfony/console\": \"^7.4|^8.0\",\n+                \"yaml/yaml-test-suite\": \"*\"\n             },\n             \"bin\": [\n                 \"Resources/bin/yaml-lint\"\n@@ -7740,7 +7787,7 @@\n             \"description\": \"Loads and dumps YAML files\",\n             \"homepage\": \"https://symfony.com\",\n             \"support\": {\n-                \"source\": \"https://github.com/symfony/yaml/tree/v7.4.13\"\n+                \"source\": \"https://github.com/symfony/yaml/tree/v8.1.1\"\n             },\n             \"funding\": [\n                 {\n@@ -7760,7 +7807,7 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2026-05-25T06:06:12+00:00\"\n+            \"time\": \"2026-06-09T11:06:24+00:00\"\n         },\n         {\n             \"name\": \"tbachert/spi\",\n@@ -8243,30 +8290,29 @@\n         },\n         {\n             \"name\": \"doctrine/instantiator\",\n-            \"version\": \"2.0.0\",\n+            \"version\": \"2.1.0\",\n             \"source\": {\n                 \"type\": \"git\",\n                 \"url\": \"https://github.com/doctrine/instantiator.git\",\n-                \"reference\": \"c6222283fa3f4ac679f8b9ced9a4e23f163e80d0\"\n+                \"reference\": \"23da848e1a2308728fe5fdddabf4be17ff9720c7\"\n             },\n             \"dist\": {\n                 \"type\": \"zip\",\n-                \"url\": \"https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0\",\n-                \"reference\": \"c6222283fa3f4ac679f8b9ced9a4e23f163e80d0\",\n+                \"url\": \"https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7\",\n+                \"reference\": \"23da848e1a2308728fe5fdddabf4be17ff9720c7\",\n                 \"shasum\": \"\"\n             },\n             \"require\": {\n-                \"php\": \"^8.1\"\n+                \"php\": \"^8.4\"\n             },\n             \"require-dev\": {\n-                \"doctrine/coding-standard\": \"^11\",\n+                \"doctrine/coding-standard\": \"^14\",\n                 \"ext-pdo\": \"*\",\n                 \"ext-phar\": \"*\",\n                 \"phpbench/phpbench\": \"^1.2\",\n-                \"phpstan/phpstan\": \"^1.9.4\",\n-                \"phpstan/phpstan-phpunit\": \"^1.3\",\n-                \"phpunit/phpunit\": \"^9.5.27\",\n-                \"vimeo/psalm\": \"^5.4\"\n+                \"phpstan/phpstan\": \"^2.1\",\n+                \"phpstan/phpstan-phpunit\": \"^2.0\",\n+                \"phpunit/phpunit\": \"^10.5.58\"\n             },\n             \"type\": \"library\",\n             \"autoload\": {\n@@ -8293,7 +8339,7 @@\n             ],\n             \"support\": {\n                 \"issues\": \"https://github.com/doctrine/instantiator/issues\",\n-                \"source\": \"https://github.com/doctrine/instantiator/tree/2.0.0\"\n+                \"source\": \"https://github.com/doctrine/instantiator/tree/2.1.0\"\n             },\n             \"funding\": [\n                 {\n@@ -8309,7 +8355,7 @@\n                     \"type\": \"tidelift\"\n                 }\n             ],\n-            \"time\": \"2022-12-30T00:23:10+00:00\"\n+            \"time\": \"2026-01-05T06:47:08+00:00\"\n         },\n         {\n             \"name\": \"myclabs/deep-copy\",\n@@ -10254,9 +10300,9 @@\n     \"platform\": {\n         \"php\": \">=8.2\"\n     },\n-    \"platform-dev\": [],\n+    \"platform-dev\": {},\n     \"platform-overrides\": {\n         \"ext-zend-opcache\": \"8.4\"\n     },\n-    \"plugin-api-version\": \"2.6.0\"\n+    \"plugin-api-version\": \"2.9.0\"\n }"},{"sha":"1c0fe0b7ff34d348af76c0e28ae1de2dd1583b4b","filename":"generate-swagger.php","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/generate-swagger.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/generate-swagger.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/generate-swagger.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -32,6 +32,7 @@ class_exists('App\\Database\\Db') ?: class_alias('\\stdClass', '\\App\\Database\\Db');\n \tSRC_ROOT_PATH . '/modules/property/controllers/LocationController.php',\n \tSRC_ROOT_PATH . '/modules/property/controllers/ProjectController.php',\n \tSRC_ROOT_PATH . '/modules/property/controllers/WorkorderController.php',\n+\tSRC_ROOT_PATH . '/modules/todo/controllers/TodoController.php',\n ];\n \n $exclude = ["},{"sha":"6aae98ea7ad0da670a01a912a30bcc952bb6425a","filename":"src/modules/phpgwapi/controllers/SwaggerController.php","status":"modified","additions":60,"deletions":9,"changes":69,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Fcontrollers%2FSwaggerController.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Fcontrollers%2FSwaggerController.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Fcontrollers%2FSwaggerController.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -10,6 +10,54 @@ class SwaggerController\n {\n   private const PROPERTY_SUBMODULES = ['entity', 'location', 'project', 'workorder'];\n \n+  private function getCurrentServerUrl(Request $request): string\n+  {\n+    $uri = $request->getUri();\n+    $scheme = $uri->getScheme() ?: 'http';\n+    $host = $uri->getHost() ?: 'localhost';\n+    $port = $uri->getPort();\n+\n+    $defaultPort = ($scheme === 'https') ? 443 : 80;\n+    $portPart = ($port !== null && $port !== $defaultPort) ? ':' . $port : '';\n+\n+    return $scheme . '://' . $host . $portPart;\n+  }\n+\n+  private function withCurrentServer(array $spec, Request $request): array\n+  {\n+    $currentUrl = $this->getCurrentServerUrl($request);\n+    $servers = [];\n+\n+    $servers[] = [\n+      'url' => $currentUrl,\n+      'description' => 'Current server',\n+    ];\n+\n+    foreach ((array) ($spec['servers'] ?? []) as $server)\n+    {\n+      if (!is_array($server))\n+      {\n+        continue;\n+      }\n+\n+      $url = (string) ($server['url'] ?? '');\n+      if ($url === '' || $url === $currentUrl)\n+      {\n+        continue;\n+      }\n+\n+      $servers[] = $server;\n+    }\n+\n+    $spec['servers'] = $servers;\n+    if (empty($spec['openapi']))\n+    {\n+      $spec['openapi'] = '3.0.0';\n+    }\n+\n+    return $spec;\n+  }\n+\n   private function getSpecFilePath(): string\n   {\n     return __DIR__ . '/../../../../swagger_spec/openapi.json';\n@@ -24,7 +72,7 @@ private function getModulePathPrefixes(string $module): array\n \n     $prefixes = ['/' . $module];\n \n-    if (in_array($module, ['property', 'booking', 'rental', 'admin'], true))\n+    if (in_array($module, ['property', 'booking', 'rental', 'admin', 'todo'], true))\n     {\n       $prefixes[] = '/{module}/registry';\n     }\n@@ -60,7 +108,7 @@ private function jsonResponse(Response $response, array $payload, int $status =\n    */\n   public function index(Request $request, Response $response): Response\n   {\n-    $html = <<<HTML\n+    $html = <<<'HTML'\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n@@ -84,7 +132,7 @@ public function index(Request $request, Response $response): Response\n       const protocol = window.location.protocol;\n       const hostname = window.location.hostname;\n       const port = window.location.port ? window.location.port : (protocol === 'https:' ? '443' : '80');\n-      const baseUrl = `\\${protocol}//\\${hostname}:\\${port}`;\n+      const baseUrl = `${protocol}//${hostname}:${port}`;\n       \n       // First, fetch the OpenAPI spec file\n       fetch('/swagger/spec')\n@@ -94,11 +142,11 @@ public function index(Request $request, Response $response): Response\n           if (!spec.servers) {\n             spec.servers = [];\n           }\n-          // Add current server as first option\n-          spec.servers.unshift({\n-            url: baseUrl,\n-            description: \"Current server\"\n-          });\n+          spec.servers = [\n+            { url: baseUrl, description: \"Current server\" }\n+          ].concat(spec.servers.filter(function(server) {\n+            return server && server.url && server.url !== baseUrl;\n+          }));\n           \n           // Make sure openapi version exists\n           if (!spec.openapi) {\n@@ -143,7 +191,9 @@ public function getSpec(Request $request, Response $response): Response\n   {\n   try\n   {\n-    return $this->jsonResponse($response, $this->loadSpecData());\n+    $spec = $this->loadSpecData();\n+    $spec = $this->withCurrentServer($spec, $request);\n+    return $this->jsonResponse($response, $spec);\n   }\n   catch (\\RuntimeException $exception)\n   {\n@@ -223,6 +273,7 @@ public function getModuleSpec(Request $request, Response $response, array $args)\n       }));\n     }\n \n+    $spec = $this->withCurrentServer($spec, $request);\n     return $this->jsonResponse($response, $spec);\n   }\n }"},{"sha":"835055e5889fdd99df02b643f26a0fbfd2d0ae36","filename":"src/modules/phpgwapi/helpers/LegacyViewHelper.php","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Fhelpers%2FLegacyViewHelper.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Fhelpers%2FLegacyViewHelper.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Fhelpers%2FLegacyViewHelper.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -64,7 +64,7 @@ public function render(string $componentHtml, string|array $appNameOrPath = 'boo\n \n \t\tif (is_array($appNameOrPath)) {\n \t\t\t$appName = $appNameOrPath[0];\n-\t\t\t$menuSelection = implode('::', $appNameOrPath);\n+\t\t\t$menuSelection = !empty($menuSelection) ? $menuSelection : implode('::', $appNameOrPath);\n \t\t\t$appHeader = implode('::', array_map(fn($s) => lang($s), $appNameOrPath));\n \t\t\t$this->setFlags($appName, $menuSelection, $appHeader);\n \t\t} else {"},{"sha":"5b8bfd1ee916bab8e0c384a73e547c7d38e2aa78","filename":"src/modules/phpgwapi/helpers/TwigHelper.php","status":"modified","additions":4,"deletions":0,"changes":4,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Fhelpers%2FTwigHelper.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Fhelpers%2FTwigHelper.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Fhelpers%2FTwigHelper.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -72,6 +72,10 @@ private function registerPaths(): void\n \t\t// phpgwapi base + template set\n \t\t$this->addPathIfExists(PHPGW_SERVER_ROOT . '/phpgwapi/templates/base');\n \t\t$this->addPathIfExists(PHPGW_SERVER_ROOT . '/phpgwapi/templates/' . $templateSet);\n+\t\t// Shared Twig partials can live in digdir even when another template set is active.\n+\t\tif ($templateSet !== 'digdir') {\n+\t\t\t$this->addPathIfExists(PHPGW_SERVER_ROOT . '/phpgwapi/templates/digdir');\n+\t\t}\n \n \t\t// Designsystemet component templates\n \t\tif ($this->designSystem->isEnabled()) {"},{"sha":"e48c45115c4968d4360671c7c4095f0ac40563c4","filename":"src/modules/phpgwapi/inc/class.common.inc.php","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.common.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.common.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.common.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -435,7 +435,7 @@ public function error_list($errors, $text = 'Error')\n \t * @param string $lid Account login id\n \t * @param string $firstname Firstname\n \t * @param string $lastname Lastname\n-\t * @return Fullname\n+\t * @return string $name Fullname\n \t */\n \tpublic function display_fullname($lid = '', $firstname = '', $lastname = '')\n \t{"},{"sha":"2bad8ee26806bb0757e87af01db1556b27a64040","filename":"src/modules/phpgwapi/inc/class.historylog.inc.php","status":"modified","additions":41,"deletions":7,"changes":48,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.historylog.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.historylog.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.historylog.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -36,6 +36,39 @@ class phpgwapi_historylog\n \t);\n \tvar $alternate_handlers = array();\n \n+\tprivate function apply_alternate_handler($handler, $value)\n+\t{\n+\t\tif (!$handler)\n+\t\t{\n+\t\t\treturn $value;\n+\t\t}\n+\n+\t\tif (is_callable($handler))\n+\t\t{\n+\t\t\treturn call_user_func($handler, $value);\n+\t\t}\n+\n+\t\tif (is_string($handler))\n+\t\t{\n+\t\t\tif (preg_match('/^\\$this->([a-zA-Z_][a-zA-Z0-9_]*)->([a-zA-Z_][a-zA-Z0-9_]*)$/', $handler, $matches))\n+\t\t\t{\n+\t\t\t\t$property = $matches[1];\n+\t\t\t\t$method = $matches[2];\n+\t\t\t\tif (isset($this->$property) && is_object($this->$property) && is_callable(array($this->$property, $method)))\n+\t\t\t\t{\n+\t\t\t\t\treturn $this->$property->$method($value);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (function_exists($handler))\n+\t\t\t{\n+\t\t\t\treturn $handler($value);\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn $value;\n+\t}\n+\n \tfunction __construct($appname, $location = '.')\n \t{\n \t\t$flags = Settings::getInstance()->get('flags');\n@@ -46,6 +79,11 @@ function __construct($appname, $location = '.')\n \t\t$locations_obj = new Locations();\n \n \t\t$location_id = $locations_obj->get_id($appname, $location);\n+\t\tif(! $location_id && $location === '.')\n+\t\t{\n+\t\t\t$location_id = $locations_obj->get_id($appname, 'run');\n+\t\t}\n+\n \t\t$this->location_id = (int) $location_id;\n \t\t$this->db      = Db::getInstance();\n \t\t$this->phpgwapi_common = new \\phpgwapi_common();\n@@ -184,13 +222,9 @@ function return_html($filter_out, $orderby = '', $sort = '', $record_id = 0)\n \n \t\t\tif ($this->alternate_handlers[$value['status']])\n \t\t\t{\n-\t\t\t\teval('\\$s = ' . $this->alternate_handlers[$value['status']] . '(' . $value['new_value'] . ');');\n-\t\t\t\t$this->template->set_var('row_new_value', $s);\n-\t\t\t\tunset($s);\n-\n-\t\t\t\teval('\\$s = ' . $this->alternate_handlers[$value['status']] . '(' . $value['old_value'] . ');');\n-\t\t\t\t$this->template->set_var('row_old_value', $s);\n-\t\t\t\tunset($s);\n+\t\t\t\t$handler = $this->alternate_handlers[$value['status']];\n+\t\t\t\t$this->template->set_var('row_new_value', $this->apply_alternate_handler($handler, $value['new_value']));\n+\t\t\t\t$this->template->set_var('row_old_value', $this->apply_alternate_handler($handler, $value['old_value']));\n \t\t\t}\n \t\t\telse\n \t\t\t{"},{"sha":"4bb86ee498bb4136f719c27c4a4529202dc2ef77","filename":"src/modules/phpgwapi/inc/class.matrixview.inc.php","status":"modified","additions":40,"deletions":32,"changes":72,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.matrixview.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.matrixview.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Finc%2Fclass.matrixview.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -210,13 +210,21 @@ function out($form_link)\n \t\t$this->sumdays = $in['mday'];\n \t\t$this->monthname = $in['month'];\n \n+\t\techo '<div class=\"phpgw-matrixview\">' . \"\\n\";\n \t\t$this->out_monthyear($form_link);\n-\n-\t\techo \"<table border=\\\"0\\\" align=\\\"center\\\">\\n\";\n+\t\techo '<div class=\"phpgw-matrixview__table-wrap\">' . \"\\n\";\n+\t\techo '<table class=\"phpgw-matrixview__table\">' . \"\\n\";\n \n \t\t$this->out_header();\n \t\t$this->out_ruler();\n-\t\techo '<tbody>';\n+\t\techo '<tbody class=\"phpgw-matrixview__body\">';\n+\n+\t\tif (!$this->items_count)\n+\t\t{\n+\t\t\techo '<tr class=\"phpgw-matrixview__row phpgw-matrixview__row--empty\">' . \"\\n\";\n+\t\t\techo '<td class=\"phpgw-matrixview__empty\" colspan=\"' . ($this->sumdays + 1) . '\">' . lang('No entries') . '</td>' . \"\\n\";\n+\t\t\techo '</tr>' . \"\\n\";\n+\t\t}\n \n \t\t// loop through number of items\n \t\tfor ($z = 0; $z < $this->items_count; $z++)\n@@ -226,8 +234,8 @@ function out($form_link)\n \t\t\t$itemname  = $this->items_content[$z][0];\n \t\t\t$itemcolor = $this->items_color[$z];\n \n-\t\t\techo '<tr>' . \"\\n\";\n-\t\t\techo '<td>' . $itemname . '</td>' . \"\\n\";\n+\t\t\techo '<tr class=\"phpgw-matrixview__row\">' . \"\\n\";\n+\t\t\techo '<th class=\"phpgw-matrixview__item\" scope=\"row\">' . $itemname . '</th>' . \"\\n\";\n \n \t\t\t// loop through days of desired month\n \t\t\tfor ($r = 1; $r < $this->sumdays + 1; $r++)\n@@ -243,13 +251,20 @@ function out($form_link)\n \t\t\t\t{\n \t\t\t\t\t$color = $this->color_emptyfield;\n \t\t\t\t}\n-\t\t\t\techo '<td bgcolor=\"' . $color . '\" width=\"20\">&nbsp;</td>' . \"\\n\";\n+\n+\t\t\t\t$cellClass = isset($this->items_content[$z][$r]) && $this->items_content[$z][$r] == 'x'\n+\t\t\t\t\t? 'phpgw-matrixview__day phpgw-matrixview__day--active'\n+\t\t\t\t\t: 'phpgw-matrixview__day phpgw-matrixview__day--empty';\n+\n+\t\t\t\techo '<td class=\"' . $cellClass . '\" style=\"background-color: ' . $color . ';\">&nbsp;</td>' . \"\\n\";\n \t\t\t}\n \n \t\t\techo '</tr>' . \"\\n\";\n \t\t}\n \t\techo '</tbody>';\n \t\techo '</table>';\n+\t\techo '</div>' . \"\\n\";\n+\t\techo '</div>' . \"\\n\";\n \t}\n \n \t/**\n@@ -261,13 +276,13 @@ function out($form_link)\n \t */\n \tfunction out_header()\n \t{\n-\t\techo \"<thead>\\n\";\n-\t\techo '<tr>' . \"\\n\";\n-\t\techo '<td>' . lang('Title') . '</td>' . \"\\n\";\n+\t\techo '<thead class=\"phpgw-matrixview__head\">' . \"\\n\";\n+\t\techo '<tr class=\"phpgw-matrixview__header-row\">' . \"\\n\";\n+\t\techo '<th class=\"phpgw-matrixview__header phpgw-matrixview__header--title\">' . lang('Title') . '</th>' . \"\\n\";\n \n \t\tfor ($i = 1; $i < $this->sumdays + 1; $i++)\n \t\t{\n-\t\t\techo \"<td>{$i}</td>\\n\";\n+\t\t\techo '<th class=\"phpgw-matrixview__header phpgw-matrixview__header--day\">' . $i . '</th>' . \"\\n\";\n \t\t}\n \n \t\techo '</tr>' . \"\\n\";\n@@ -284,7 +299,9 @@ function out_header()\n \tfunction out_ruler()\n \t{\n \t\t$span = $this->sumdays + 1;\n-\t\techo \"<tfoot>\\n<tr>\\n<td colspan=\\\"{$span}\\\">&nbsp;</td>\\n</tr>\\n</tfoot>\\n\";\n+\t\techo '<tfoot class=\"phpgw-matrixview__foot\">' . \"\\n\";\n+\t\techo '<tr><td class=\"phpgw-matrixview__foot-cell\" colspan=\"' . $span . '\">&nbsp;</td></tr>' . \"\\n\";\n+\t\techo '</tfoot>' . \"\\n\";\n \t}\n \n \t/**\n@@ -296,52 +313,43 @@ function out_ruler()\n \t */\n \tfunction out_monthyear($form_link)\n \t{\n-\t\techo '<form action=\"' . $form_link . '\" method=\"post\">' . \"\\n\";\n-\t\techo '<h2>' . lang($this->monthname) . \" $this->year</h2>\\n\";\n-\t\techo '<table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">' . \"\\n\";\n-\t\techo '<tr>' . \"\\n\";\n+\t\techo '<form class=\"phpgw-matrixview__filters\" action=\"' . $form_link . '\" method=\"post\">' . \"\\n\";\n+\t\techo '<div class=\"phpgw-matrixview__headerbar\">' . \"\\n\";\n+\t\techo '<h2 class=\"phpgw-matrixview__title\">' . lang($this->monthname) . \" $this->year</h2>\\n\";\n \n \t\tif ($this->selection == 1)\n \t\t{\n-\t\t\techo '<td colspan=\"2\" align=\"right\">' . \"\\n\";\n-\t\t\techo '<select name=\"month\">';\n+\t\t\techo '<div class=\"phpgw-matrixview__controls\">' . \"\\n\";\n+\t\t\techo '<select class=\"phpgw-matrixview__select\" name=\"month\">';\n \n \t\t\tfor ($i = 1; $i < 13; $i++)\n \t\t\t{\n+\t\t\t\t$sel = '';\n \t\t\t\tif ($this->month == $i)\n \t\t\t\t{\n \t\t\t\t\t$sel = ' selected';\n \t\t\t\t}\n-\t\t\t\telse\n-\t\t\t\t{\n-\t\t\t\t\tunset($sel);\n-\t\t\t\t}\n-\t\t\t\techo \"<option value=\\\"{$i}\\\" $sel>{$i}</option>\";\n+\t\t\t\techo '<option value=\"' . $i . '\"' . $sel . '>' . lang(date('F', mktime(0, 0, 0, $i, 1, $this->year))) . '</option>';\n \t\t\t}\n \n \t\t\techo '</select>' . \"\\n\";\n-\t\t\techo '<select name=\"year\">';\n+\t\t\techo '<select class=\"phpgw-matrixview__select\" name=\"year\">';\n \n \t\t\tfor ($i = date('Y') - 2; $i < date('Y') + 5; $i++)\n \t\t\t{\n+\t\t\t\t$sel = '';\n \t\t\t\tif ($this->year == $i)\n \t\t\t\t{\n \t\t\t\t\t$sel = ' selected';\n \t\t\t\t}\n-\t\t\t\telse\n-\t\t\t\t{\n-\t\t\t\t\tunset($sel);\n-\t\t\t\t}\n-\t\t\t\techo \"<option value=\\\"{$i}\\\" $sel>{$i}</option>\";\n+\t\t\t\techo '<option value=\"' . $i . '\"' . $sel . '>' . $i . '</option>';\n \t\t\t}\n \n \t\t\techo '</select>' . \"\\n\";\n-\t\t\techo '&nbsp;&nbsp;<input type=\"submit\" name=\"selection\" value=\"' . lang('Filter') . '\">&nbsp;&nbsp;';\n-\t\t\techo '</td>' . \"\\n\";\n+\t\t\techo '</div>' . \"\\n\";\n \t\t}\n \n-\t\techo '</tr>' . \"\\n\";\n-\t\techo '</table>' . \"\\n\";\n+\t\techo '</div>' . \"\\n\";\n \t\techo '</form>' . \"\\n\";\n \t}\n }"},{"sha":"4098e5b511a17d8ac976b056ef9340e1a61b1f0a","filename":"src/modules/phpgwapi/templates/base/datatable2.twig","status":"added","additions":1822,"deletions":0,"changes":1822,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbase%2Fdatatable2.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbase%2Fdatatable2.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbase%2Fdatatable2.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,1822 @@\n+{% set data_root = data|default({}) %}\n+{% set datatable_config = datatable|default(data_root.datatable|default({})) %}\n+{% set form_config = form|default(data_root.form|default({})) %}\n+{% set toolbar_config = attribute(form_config, 'toolbar')|default({}) %}\n+{% set toolbar_items = attribute(toolbar_config, 'item')|default([]) %}\n+{% set list_actions = attribute(form_config, 'list_actions')|default({}) %}\n+{% set list_actions_items = attribute(list_actions, 'item')|default([]) %}\n+{% set filters = data_root.filters|default({}) %}\n+{% set top_toolbar = attribute(data_root, 'top-toolbar')|default({}) %}\n+{% set end_toolbar = attribute(data_root, 'end-toolbar')|default({}) %}\n+{% set datatable_fields = attribute(datatable_config, 'field')|default([]) %}\n+{% set top_toolbar_fields = attribute(attribute(top_toolbar, 'fields')|default({}), 'field')|default([]) %}\n+{% set end_toolbar_fields = attribute(attribute(end_toolbar, 'fields')|default({}), 'field')|default([]) %}\n+{% set left_click_action = attribute(datatable_config, 'left_click_action')|default(attribute(data_root, 'left_click_action')|default('')) %}\n+{% set dbl_click_action = attribute(datatable_config, 'dbl_click_action')|default(attribute(data_root, 'dbl_click_action')|default('')) %}\n+\n+<style>\n+\t/* Fallback styling for datatable2 toolbar when digdir utility CSS is unavailable. */\n+\t#queryForm .app-button {\n+\t\tdisplay: inline-block;\n+\t\tpadding: 0.4rem 0.75rem;\n+\t\tborder: 1px solid #bfc8d4;\n+\t\tborder-radius: 4px;\n+\t\tbackground: #f5f7fa;\n+\t\tcolor: #1d2a39;\n+\t\tcursor: pointer;\n+\t\ttext-decoration: none;\n+\t\tfont-size: 0.9rem;\n+\t}\n+\t#queryForm .app-button:hover {\n+\t\tbackground: #e9eef5;\n+\t}\n+\t#queryForm .app-button--primary {\n+\t\tbackground: #2f6fad;\n+\t\tborder-color: #2f6fad;\n+\t\tcolor: #fff;\n+\t}\n+\t#queryForm .app-button--secondary,\n+\t#queryForm .app-button--tertiary {\n+\t\tbackground: #f5f7fa;\n+\t\tcolor: #1d2a39;\n+\t}\n+\t#queryForm .app-label {\n+\t\tdisplay: block;\n+\t\tmargin-bottom: 0.3rem;\n+\t\tfont-weight: 600;\n+\t}\n+\t#queryForm .app-input,\n+\t#queryForm .app-select {\n+\t\tdisplay: block;\n+\t\twidth: 100%;\n+\t\tmin-height: 2.15rem;\n+\t\tpadding: 0.3rem 0.45rem;\n+\t\tborder: 1px solid #bfc8d4;\n+\t\tborder-radius: 4px;\n+\t\tbackground: #fff;\n+\t}\n+\t#queryForm .app-row {\n+\t\tdisplay: flex;\n+\t\tflex-wrap: wrap;\n+\t\tgap: 0.75rem;\n+\t}\n+\t#queryForm .app-col,\n+\t#queryForm .app-col-4,\n+\t#queryForm .app-col-6 {\n+\t\tflex: 1 1 220px;\n+\t}\n+\t#queryForm .app-collapse {\n+\t\tdisplay: none;\n+\t}\n+\t#queryForm .app-collapse.app-collapse--show {\n+\t\tdisplay: block;\n+\t}\n+\t#queryForm .dtable_custom_controls {\n+\t\tpadding: 0.75rem;\n+\t\tborder: 1px solid #d8dfe8;\n+\t\tborder-radius: 4px;\n+\t\tbackground: #fafbfd;\n+\t}\n+</style>\n+\n+<script>\n+\tvar show_filter_group = false;\n+\t{% if toolbar_config.show_filter_group is defined and toolbar_config.show_filter_group == '1' %}\n+\t\tshow_filter_group = true;\n+\t{% endif %}\n+\n+\tvar number_of_toolbar_items = 0;\n+\tvar filter_selects = {};\n+\tvar customFilters = {}; // Store custom filter values for modern DataTables compatibility\n+\tvar lang = {{ js_lang('Search')|raw }};\n+</script>\n+{% include 'jquery_phpgw_i18n.twig' %}\n+\n+<div id=\"queryForm\">\n+\t{% if toolbar_items|length > 0 %}\n+\t\t<div class=\"row ms-1\">\n+\t\t\t<div id=\"active_filters\"></div>\n+\t\t</div>\n+\t\t<div class=\"mt-2 mb-2 ms-1\">\n+\t\t\t<button class=\"app-button app-button--secondary\" type=\"button\" onclick=\"document.getElementById('democollapseBtn').classList.toggle('app-collapse--show');\" aria-expanded=\"false\" aria-controls=\"democollapseBtn\">\n+\t\t\t\t{{ lang('filter') }}\n+\t\t\t</button>\n+\t\t\t<button id=\"reset_filter\" class=\"ms-2 app-button app-button--tertiary\" type=\"button\" onclick=\"reset_filter();\" style=\"display: none;\">\n+\t\t\t\t{{ lang('reset filter') }}\n+\t\t\t</button>\n+\t\t</div>\n+\n+\t\t<div class=\"row mt-2 app-collapse\" id=\"democollapseBtn\">\n+\t\t\t<div id=\"toolbar\" class=\"dtable_custom_controls\">\n+\t\t\t\t<form>\n+\t\t\t\t\t<fieldset>\n+\t\t\t\t\t\t<div class=\"app-row mb-2\">\n+\t\t\t\t\t\t\t{% set count_items = toolbar_items|length %}\n+\t\t\t\t\t\t\t{% for item in toolbar_items %}\n+\t\t\t\t\t\t\t\t<script>\n+\t\t\t\t\t\t\t\t\tnumber_of_toolbar_items += 1;\n+\t\t\t\t\t\t\t\t</script>\n+\t\t\t\t\t\t\t\t<div class=\"{% if browser_support is defined and browser_support == 'legacy' %}pure-u-1 pure-u-md-1-3{% else %}{% if count_items > 6 %}app-col-4{% elseif count_items > 4 or count_items == 3 %}app-col-6{% else %}app-col{% endif %}{% endif %}\">\n+\t\t\t\t\t\t\t\t\t{% set filter_key = 'filter_' ~ item.name %}\n+\t\t\t\t\t\t\t\t\t{% set filter_key_name = 'filter_' ~ item.name ~ '_name' %}\n+\t\t\t\t\t\t\t\t\t{% set filter_key_id = 'filter_' ~ item.name ~ '_id' %}\n+\t\t\t\t\t\t\t\t\t{% if item.name is defined and item.name %}\n+\t\t\t\t\t\t\t\t\t\t<label class=\"app-label\" for=\"{{ item.name|default('') }}\">\n+\t\t\t\t\t\t\t\t\t\t\t{{ item.text|default('') }}\n+\t\t\t\t\t\t\t\t\t\t</label>\n+\t\t\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\t\t\t{% if item.type == 'date-picker' %}\n+\t\t\t\t\t\t\t\t\t\t<input class=\"app-input\" id=\"filter_{{ item.name }}\" name=\"filter_{{ item.name }}\" value=\"{{ item.value|default('') }}\" type=\"text\" title=\"{{ item.text|default('') }}\" />\n+\t\t\t\t\t\t\t\t\t{% elseif item.type == 'autocomplete' %}\n+\t\t\t\t\t\t\t\t\t\t<input id=\"filter_{{ item.name }}_name\" name=\"{{ item.name }}_name\" type=\"text\" class=\"app-input\" value=\"{{ attribute(filters, filter_key_name)|default('') }}\" title=\"{{ item.text|default('') }}\" />\n+\t\t\t\t\t\t\t\t\t\t<input id=\"filter_{{ item.name }}_id\" name=\"filter_{{ item.name }}_id\" type=\"hidden\" value=\"{{ attribute(filters, filter_key_id)|default('') }}\" />\n+\t\t\t\t\t\t\t\t\t\t<div id=\"filter_{{ item.name }}_container\"></div>\n+\t\t\t\t\t\t\t\t\t\t<script>\n+\t\t\t\t\t\t\t\t\t\t\t$(document).ready(function() {\n+\t\t\t\t\t\t\t\t\t\t\t\tvar app = \"{{ item.app|default('booking')|e('js') }}\";\n+\t\t\t\t\t\t\t\t\t\t\t\tvar FunctionName = \"{{ attribute(item, 'function')|default('index')|e('js') }}\";\n+\t\t\t\t\t\t\t\t\t\t\t\tvar label_attr = \"{{ item.label_attr|default('name')|e('js') }}\";\n+\t\t\t\t\t\t\t\t\t\t\t\tvar show_id = false;\n+\t\t\t\t\t\t\t\t\t\t\t\t{% if item.show_id is defined and item.show_id == 1 %}\n+\t\t\t\t\t\t\t\t\t\t\t\t\tshow_id = true;\n+\t\t\t\t\t\t\t\t\t\t\t\t{% endif %}\n+\n+\t\t\t\t\t\t\t\t\t\t\t\tvar name = \"{{ item.name|default('')|e('js') }}\";\n+\t\t\t\t\t\t\t\t\t\t\t\tvar ui = \"{{ item.ui|default('')|e('js') }}\";\n+\t\t\t\t\t\t\t\t\t\t\t\tvar requestGenerator = false;\n+\t\t\t\t\t\t\t\t\t\t\t\t{% if item.requestGenerator is defined and item.requestGenerator %}\n+\t\t\t\t\t\t\t\t\t\t\t\t\trequestGenerator = '{{ item.requestGenerator|e('js') }}';\n+\t\t\t\t\t\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\t\t\t\t\t\tvar depends = false;\n+\t\t\t\t\t\t\t\t\t\t\t\tvar filter_depends = \"\";\n+\t\t\t\t\t\t\t\t\t\t\t\tvar filter_selected = \"\";\n+\t\t\t\t\t\t\t\t\t\t\t\t{% if item.depends is defined and item.depends %}\n+\t\t\t\t\t\t\t\t\t\t\t\t\tdepends = \"{{ item.depends|e('js') }}\";\n+\t\t\t\t\t\t\t\t\t\t\t\t\t//filter_depends = $('#filer_'+depends+'_id').val();\n+\t\t\t\t\t\t\t\t\t\t\t\t\t$(\"#filter_\" + depends + \"_name\").on(\"autocompleteselect\", function(event, i){\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\tvar filter_select = i.item.value;\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\tfilter_depends = i.item.value;\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (filter_select != filter_selected){\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (filter_depends) {\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJqueryPortico.autocompleteHelper(phpGWLink('index.php', {menuaction:app + '.ui' + ui + '.' + FunctionName}) + '&filter_' + depends + '_id=' + filter_depends + '&',\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'filter_' + name + '_name', 'filter_' + name + '_id', 'filter_' + name + '_container', label_attr, show_id, requestGenerator);\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\toTable.dataTableSettings[0]['ajax']['data']['filter_' + name + '_id'] = \"\";\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#filter_' + name + '_name').val('');\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#filter_' + name + '_id').val('');\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfilter_selected = filter_select;\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t\t\t\t\t\t\t\t$(\"#filter_\" + depends + \"_name\").on(\"keyup\", function(){\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\tif ($(this).val() == ''){\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfilter_depends = false;\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (!filter_depends) {\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJqueryPortico.autocompleteHelper(phpGWLink('index.php', {menuaction:app + '.ui' + ui + '.' + FunctionName}) + '&',\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'filter_' + name + '_name', 'filter_' + name + '_id', 'filter_' + name + '_container', label_attr, show_id, requestGenerator);\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfilter_selected = \"\";\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\toTable.dataTableSettings[0]['ajax']['data']['filter_' + name + '_id'] = \"\";\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#filter_' + name + '_name').val('');\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#filter_' + name + '_id').val('');\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\t\t\t\t\t\tif (filter_depends) {\n+\t\t\t\t\t\t\t\t\t\t\t\t\tJqueryPortico.autocompleteHelper(phpGWLink('index.php', {menuaction:app + '.ui' + ui + '.' + FunctionName}) + '&filter_' + depends + '_id=' + filter_depends + '&',\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t'filter_' + name + '_name', 'filter_' + name + '_id', 'filter_' + name + '_container', label_attr, show_id, requestGenerator);\n+\t\t\t\t\t\t\t\t\t\t\t\t} else {\n+\t\t\t\t\t\t\t\t\t\t\t\t\tJqueryPortico.autocompleteHelper(phpGWLink('index.php', {menuaction:app + '.ui' + ui + '.' + FunctionName}) + '&',\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t'filter_' + name + '_name', 'filter_' + name + '_id', 'filter_' + name + '_container', label_attr, show_id, requestGenerator);\n+\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t\t\t\t\t</script>\n+\t\t\t\t\t\t\t\t\t{% elseif item.type == 'filter' %}\n+\t\t\t\t\t\t\t\t\t\t<script>\n+\t\t\t\t\t\t\t\t\t\t\tfilter_selects['{{ item.text|default('')|e('js') }}'] = '{{ item.name|default('')|e('js') }}';\n+\t\t\t\t\t\t\t\t\t\t</script>\n+\t\t\t\t\t\t\t\t\t\t<select id=\"{{ item.name|default('') }}\" name=\"{{ item.name|default('') }}\" class=\"app-select\"{% if item.multiple is defined and item.multiple %} multiple=\"true\"{% endif %} title=\"{{ item.text|default('') }}\">\n+\t\t\t\t\t\t\t\t\t\t\t{% for option in item.list|default([]) %}\n+\t\t\t\t\t\t\t\t\t\t\t\t{% if option.selected is defined and (option.selected == 'selected' or option.selected == '1') %}\n+\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"{{ option.id|default('') }}\" selected=\"selected\">{{ option.name|default('') }}</option>\n+\t\t\t\t\t\t\t\t\t\t\t\t{% else %}\n+\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"{{ option.id|default('') }}\">{{ option.name|default('') }}</option>\n+\t\t\t\t\t\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\t\t\t\t\t{% endfor %}\n+\t\t\t\t\t\t\t\t\t\t</select>\n+\t\t\t\t\t\t\t\t\t{% elseif item.type == 'link' %}\n+\t\t\t\t\t\t\t\t\t\t<label class=\"invisible\">{{ item.value|default('') }}</label>\n+\t\t\t\t\t\t\t\t\t\t<br />\n+\t\t\t\t\t\t\t\t\t\t<input type=\"button\" class=\"app-button app-button--primary ms-2\" value=\"{{ item.value|default('') }}\"{% if item.onclick is defined and item.onclick %} onclick=\"{{ item.onclick|raw }}\"{% else %} onclick=\"javascript:window.open('{{ item.href|default('')|e('js') }}', '_self');\"{% endif %} />\n+\t\t\t\t\t\t\t\t\t{% elseif item.type == 'hidden' %}\n+\t\t\t\t\t\t\t\t\t\t<input type=\"{{ item.type|default('') }}\" id=\"{{ item.id|default('') }}\" name=\"{{ item.name|default('') }}\" value=\"{{ item.value|default('') }}\" />\n+\t\t\t\t\t\t\t\t\t{% elseif item.type == 'label' %}\n+\t\t\t\t\t\t\t\t\t\t<label id=\"{{ item.id|default('') }}\"></label>\n+\t\t\t\t\t\t\t\t\t{% elseif item.type == 'checkbox' %}\n+\t\t\t\t\t\t\t\t\t\t<div class=\"app-checkbox-wrapper ps-0\">\n+\t\t\t\t\t\t\t\t\t\t\t<input id=\"innertoolbar_{{ item.name }}\" type=\"{{ item.type|default('') }}\" name=\"{{ item.name|default('') }}\" onclick=\"{{ item.onClick|default('')|raw }}\" value=\"{{ item.value|default('') }}\" class=\"app-checkbox ms-0{% if item.class is defined and item.class %} {{ item.class }}{% endif %}\"{% if item.checked is defined and item.checked|trim != '' %}{% if item.checked|trim == '1' %} checked=\"checked\"{% else %} checked=\"{{ item.checked|e('html_attr') }}\"{% endif %}{% endif %} />\n+\t\t\t\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t\t\t\t{% else %}\n+\t\t\t\t\t\t\t\t\t\t<input id=\"innertoolbar_{{ item.name }}\" type=\"{{ item.type|default('') }}\" name=\"{{ item.name|default('') }}\" onclick=\"{{ item.onClick|default('')|raw }}\" value=\"{{ item.value|default('') }}\" href=\"{{ item.href|default('') }}\" class=\"{{ item.class|default('') }}\" checked=\"{{ item.checked|default('') }}\" />\n+\t\t\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\t\t</div>\n+\t\t\t\t\t\t\t{% endfor %}\n+\t\t\t\t\t\t</div>\n+\t\t\t\t\t</fieldset>\n+\t\t\t\t</form>\n+\t\t\t</div>\n+\t\t</div>\n+\t{% endif %}\n+</div>\n+\n+<div id=\"list_flash\">\n+\t{% if msgbox_data is defined and msgbox_data %}\n+\t\t{% if msgbox_data is iterable and (msgbox_data|first is iterable) %}\n+\t\t\t{% for message in msgbox_data %}\n+\t\t\t\t<div class=\"{{ message.msgbox_class|default('error') }}\">\n+\t\t\t\t\t{% if message.msgbox_img %}\n+\t\t\t\t\t\t<img src=\"{{ message.msgbox_img }}\" alt=\"{{ message.msgbox_img_alt }}\" title=\"{{ message.msgbox_img_alt }}\" />\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t\t{{ message.msgbox_text|raw }}\n+\t\t\t\t</div>\n+\t\t\t{% endfor %}\n+\t\t{% else %}\n+\t\t\t<div class=\"{{ msgbox_data.msgbox_class|default('error') }}\">\n+\t\t\t\t{% if msgbox_data.msgbox_img %}\n+\t\t\t\t\t<img src=\"{{ msgbox_data.msgbox_img }}\" alt=\"{{ msgbox_data.msgbox_img_alt }}\" title=\"{{ msgbox_data.msgbox_img_alt }}\" />\n+\t\t\t\t{% endif %}\n+\t\t\t\t{{ msgbox_data.msgbox_text|raw }}\n+\t\t\t</div>\n+\t\t{% endif %}\n+\t{% endif %}\n+</div>\n+<div id=\"message\" class=\"message\"></div>\n+\n+{% if top_toolbar_fields|length > 0 %}\n+\t<div class=\"toolbar-container\">\n+\t\t<div class=\"toolbar\">\n+\t\t\t<form>\n+\t\t\t\t<div class=\"row\">\n+\t\t\t\t\t<div class=\"col-md-2\">\n+\t\t\t\t\t\t{{ datatable_config.workorder_data|default('')|raw }}\n+\t\t\t\t\t</div>\n+\t\t\t\t\t<div class=\"col-md-4\">\n+\t\t\t\t\t\t{% for field in top_toolbar_fields %}\n+\t\t\t\t\t\t\t{% if field.type == 'button' %}\n+\t\t\t\t\t\t\t\t<button id=\"{{ field.id|default('') }}\" type=\"{{ field.type|default('') }}\" class=\"app-button app-button--primary\">{{ field.value|default('') }}</button>\n+\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t{% endfor %}\n+\t\t\t\t\t</div>\n+\t\t\t\t</div>\n+\t\t\t</form>\n+\t\t</div>\n+\t</div>\n+{% endif %}\n+\n+<table id=\"datatable-container\" class=\"cell-border compact stripe\" style=\"width:100%\">\n+\t<thead>\n+\t\t<tr>\n+\t\t\t{% for field in datatable_fields %}\n+\t\t\t\t{% if field.hidden is defined %}\n+\t\t\t\t\t{% if field.hidden == 0 or field.hidden == '0' %}\n+\t\t\t\t\t\t<th>{{ field.label|default('') }}</th>\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% else %}\n+\t\t\t\t\t<th>{{ field.label|default('') }}</th>\n+\t\t\t\t{% endif %}\n+\t\t\t{% endfor %}\n+\t\t</tr>\n+\t</thead>\n+\t<tfoot>\n+\t\t<tr>\n+\t\t\t{% for field in datatable_fields %}\n+\t\t\t\t{% if field.hidden is defined %}\n+\t\t\t\t\t{% if field.hidden == 0 or field.hidden == '0' %}\n+\t\t\t\t\t\t<th>{{ field.value_footer|default('') }}</th>\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% else %}\n+\t\t\t\t\t<th>{{ field.value_footer|default('') }}</th>\n+\t\t\t\t{% endif %}\n+\t\t\t{% endfor %}\n+\t\t</tr>\n+\t</tfoot>\n+</table>\n+<form id=\"custom_values_form\" name=\"custom_values_form\"></form>\n+\n+{% if end_toolbar_fields|length > 0 %}\n+\t<div class=\"toolbar-container\">\n+\t\t<div class=\"toolbar\">\n+\t\t\t<form>\n+\t\t\t\t<div class=\"row\">\n+\t\t\t\t\t{% for field in end_toolbar_fields %}\n+\t\t\t\t\t\t<div class=\"col-md-2\">\n+\t\t\t\t\t\t\t{% if field.type == 'date-picker' %}\n+\t\t\t\t\t\t\t\t<input class=\"app-input\" id=\"filter_{{ field.name }}\" name=\"filter_{{ field.name }}\" value=\"{{ field.value|default('') }}\" type=\"text\" title=\"{{ field.text|default('') }}\" />\n+\t\t\t\t\t\t\t{% elseif field.type == 'button' %}\n+\t\t\t\t\t\t\t\t<button id=\"{{ field.id|default('') }}\" type=\"{{ field.type|default('') }}\" class=\"app-button app-button--primary\" onclick=\"{{ field.action|default('')|raw }}\">{{ field.value|default('') }}</button>\n+\t\t\t\t\t\t\t{% elseif field.type == 'label' %}\n+\t\t\t\t\t\t\t\t{{ field.value|default('') }}\n+\t\t\t\t\t\t\t{% else %}\n+\t\t\t\t\t\t\t\t<input id=\"{{ field.id|default('') }}\" type=\"{{ field.type|default('') }}\" name=\"{{ field.name|default('') }}\" value=\"{{ field.value|default('') }}\" class=\"app-input\"{% if field.type == 'checkbox' and field.checked == '1' %} checked=\"checked\"{% endif %} />\n+\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t</div>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</div>\n+\t\t\t</form>\n+\t\t</div>\n+\t</div>\n+{% endif %}\n+\n+<script>\n+\tvar columns = [\n+\t\t{% for field in datatable_fields %}\n+\t\t\t{\n+\t\t\t\tdata: \"{{ field.key|default('')|e('js') }}\",\n+\t\t\t\t{% if field.className is defined and field.className %}\n+\t\t\t\t\t{% if field.className == 'right' or field.className == 'center' %}\n+\t\t\t\t\t\t{% if field.className == 'right' %}\n+\t\t\t\t\t\t\tclass: 'dt-right',\n+\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t{% if field.className == 'center' %}\n+\t\t\t\t\t\t\tclass: 'dt-center',\n+\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t{% else %}\n+\t\t\t\t\t\tclass: \"{{ field.className|e('js') }}\",\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% endif %}\n+\t\t\t\torderable: {% if field.sortable is defined and field.sortable == 0 %}false{% else %}true{% endif %},\n+\t\t\t\t{% if field.searchable is defined %}\n+\t\t\t\t\t{% if field.searchable == 0 %}\n+\t\t\t\t\t\tsearchable: false,\n+\t\t\t\t\t{% elseif field.searchable == 1 %}\n+\t\t\t\t\t\tsearchable: true,\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% else %}\n+\t\t\t\t\tsearchable: false,\n+\t\t\t\t{% endif %}\n+\t\t\t\t{% if field.hidden is defined %}\n+\t\t\t\t\t{% if field.hidden == 0 %}\n+\t\t\t\t\t\tvisible: true,\n+\t\t\t\t\t{% elseif field.hidden == 1 %}\n+\t\t\t\t\t\tclass: 'none', //FIXME - virker ikke...'responsive' plukker den fram igjen\n+\t\t\t\t\t\tvisible: false,\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% else %}\n+\t\t\t\t\tvisible: true,\n+\t\t\t\t{% endif %}\n+\t\t\t\t{% if field.formatter is defined and field.formatter %}\n+\t\t\t\t\trender: function (dummy1, dummy2, oData) {\n+\t\t\t\t\t\ttry {\n+\t\t\t\t\t\t\tvar ret = {{ field.formatter|raw }}(\"{{ field.key|default('')|e('js') }}\", oData);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tcatch(err) {\n+\t\t\t\t\t\t\treturn err.message;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\treturn ret;\n+\t\t\t\t\t},\n+\t\t\t\t{% endif %}\n+\t\t\t\t{% if field.dir is defined and field.dir != '' %}\n+\t\t\t\t\tdir: \"{{ field.dir|e('js') }}\",\n+\t\t\t\t{% endif %}\n+\t\t\t\t{% if field.editor is defined %}\n+\t\t\t\t\t{% if field.editor == 0 %}\n+\t\t\t\t\t\teditor: false,\n+\t\t\t\t\t{% elseif field.editor == 1 %}\n+\t\t\t\t\t\teditor: true,\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% else %}\n+\t\t\t\t\teditor: false,\n+\t\t\t\t{% endif %}\n+\t\t\t\tdefaultContent: \"{{ field.defaultContent|default('')|e('js') }}\"\n+\t\t\t}{% if not loop.last %},{% endif %}\n+\t\t{% endfor %}\n+\t];\n+\tJqueryPortico.columns = [];\n+\tfor(i=0;i < columns.length;i++)\n+\t{\n+\t\tif ( columns[i]['visible'] == true )\n+\t\t{\n+\t\t\tJqueryPortico.columns.push(columns[i]);\n+\t\t}\n+\t}\n+\t// console.log(JqueryPortico.columns);\n+</script>\n+\n+<script class=\"init\">\n+\tvar lang_ButtonText_columns = \"{{ lang('columns')|e('js') }}\";\n+\n+\tvar oTable = null;\n+\t$(document).ready(function() {\n+\t\tvar ajax_url = '{{ datatable_config.source|default('')|replace({'&amp;':'&'})|e('js') }}';\n+\t\tvar order_def = [];\n+\t\t{% if datatable_config.sorted_by is defined and datatable_config.sorted_by.key is defined %}\n+\t\t\torder_def.push([{{ datatable_config.sorted_by.key }}, '{{ datatable_config.sorted_by.dir|default('')|e('js') }}']);\n+\t\t{% endif %}\n+\t\tvar responsive = true;\n+\t\t{% if datatable_config.responsive_show_details is defined and datatable_config.responsive_show_details == 1 %}\n+\t\t\tresponsive = {\n+\t\t\t\tdetails: {\n+\t\t\t\t\tdisplay: $.fn.dataTable.Responsive.display.childRowImmediate,\n+\t\t\t\t\ttype: ''\n+\t\t\t\t}\n+\t\t\t};\n+\t\t{% endif %}\n+\n+\t\tvar download_url = '{{ datatable_config.download|default('')|replace({'&amp;':'&'})|e('js') }}';\n+\t\tvar editor_cols = [];\n+\t\tvar editor_action = '{{ datatable_config.editor_action|default('')|replace({'&amp;':'&'})|e('js') }}';\n+\t\tvar disablePagination = '{{ datatable_config.disablePagination|default('')|e('js') }}';\n+\t\tvar select_all = '{{ datatable_config.select_all|default('')|e('js') }}';\n+\t\tvar initial_search = {\"search\": \"{{ datatable_config.query|default('')|e('js') }}\" };\n+\t\tvar action_def = [];\n+\t\tvar contextMenuItems = {};\n+\t\tvar InitContextMenu = false;\n+\t\tvar button_def = [];\n+\t\tvar group_buttons = false;\n+\n+\t\t{% if datatable_config.new_item is defined and datatable_config.new_item is not empty %}\n+\t\t\t{% if datatable_config.new_item.onclick is defined and datatable_config.new_item.onclick %}\n+\t\t\t\tbutton_def.push({\n+\t\t\t\t\ttext: \"{{ lang('new')|e('js') }}\",\n+\t\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t\t{{ datatable_config.new_item.onclick|raw }};\n+\t\t\t\t\t}\n+\t\t\t\t});\n+\t\t\t{% else %}\n+\t\t\t\tbutton_def.push({\n+\t\t\t\t\ttext: \"{{ lang('new')|e('js') }}\",\n+\t\t\t\t\t{% if datatable_config.bigmenubutton is defined and datatable_config.bigmenubutton %}\n+\t\t\t\t\t\tclassName: 'bigmenubutton',\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t\tsUrl: '{{ datatable_config.new_item|default('')|replace({'&amp;':'&'})|e('js') }}',\n+\n+\t\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t\tvar sUrl = config.sUrl;\n+\t\t\t\t\t\twindow.open(sUrl, '_self');\n+\t\t\t\t\t}\n+\t\t\t\t});\n+\t\t\t{% endif %}\n+\t\t{% endif %}\n+\t\t{% if datatable_config.extra_buttons is defined and datatable_config.extra_buttons is iterable %}\n+\t\t\t{% for btn in datatable_config.extra_buttons %}\n+\t\t\t\t{% if btn.text is defined and btn.text %}\n+\t\t\t\t\t{% if btn.onclick is defined and btn.onclick %}\n+\t\t\t\t\t\tbutton_def.push({\n+\t\t\t\t\t\t\ttext: \"{{ btn.text|e('js') }}\",\n+\t\t\t\t\t\t\t{% if btn.className is defined and btn.className %}\n+\t\t\t\t\t\t\t\tclassName: \"{{ btn.className|e('js') }}\",\n+\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t\t\t\t{{ btn.onclick|raw }};\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t});\n+\t\t\t\t\t{% elseif btn.url is defined and btn.url %}\n+\t\t\t\t\t\tbutton_def.push({\n+\t\t\t\t\t\t\ttext: \"{{ btn.text|e('js') }}\",\n+\t\t\t\t\t\t\t{% if btn.className is defined and btn.className %}\n+\t\t\t\t\t\t\t\tclassName: \"{{ btn.className|e('js') }}\",\n+\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\tsUrl: \"{{ btn.url|replace({'&amp;':'&'})|e('js') }}\",\n+\t\t\t\t\t\t\ttarget: \"{{ btn.target|default('_self')|e('js') }}\",\n+\t\t\t\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t\t\t\twindow.open(config.sUrl, config.target || '_self');\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t});\n+\t\t\t\t\t{% endif %}\n+\t\t\t\t{% endif %}\n+\t\t\t{% endfor %}\n+\t\t{% endif %}\n+\t\t{% if datatable_config.select_all is defined and datatable_config.select_all == '1' %}\n+\t\t\tbutton_def.push({\n+\t\t\t\ttext: \"{{ lang('select all')|e('js') }}\",\n+\t\t\t\taction: function () {\n+\t\t\t\t\tvar api = oTable.api();\n+\t\t\t\t\tapi.rows().select();\n+\t\t\t\t\t$(\".mychecks\").each(function()\n+\t\t\t\t\t{\n+\t\t\t\t\t\t$(this).prop(\"checked\", true);\n+\t\t\t\t\t});\n+\t\t\t\t\tvar selectedRows = api.rows( { selected: true } ).count();\n+\t\t\t\t\tapi.buttons( '.record' ).enable( selectedRows > 0 );\n+\n+\t\t\t\t}\n+\t\t\t});\n+\n+\t\t\tbutton_def.push({\n+\t\t\t\ttext: \"{{ lang('select none')|e('js') }}\",\n+\t\t\t\taction: function () {\n+\t\t\t\t\tvar api = oTable.api();\n+\t\t\t\t\tapi.rows().deselect();\n+\t\t\t\t\t$(\".mychecks\").each(function()\n+\t\t\t\t\t{\n+\t\t\t\t\t\t$(this).prop(\"checked\", false);\n+\t\t\t\t\t});\n+\t\t\t\t\tapi.buttons( '.record' ).enable( false );\n+\t\t\t\t}\n+\t\t\t});\n+\t\t{% endif %}\n+\t\tvar csv_download = JqueryPortico.i18n.csv_download();\n+\t\tif(csv_download.show_button == 1)\n+\t\t{\n+\t\t\tbutton_def.push({\n+\t\t\t\textend: 'csvHtml5',\n+\t\t\t\ttitleAttr: csv_download.title,\n+\t\t\t\tfieldSeparator: ';',\n+\t\t\t\tbom: true\n+\t\t\t});\n+\t\t}\n+\t\t{% if datatable_config.download is defined and datatable_config.download %}\n+\t\t\tbutton_def.push({\n+\t\t\t\ttext: \"{{ lang('download')|e('js') }}\",\n+\t\t\t\ttitleAttr: \"{{ lang('download data')|e('js') }}\",\n+\t\t\t\tclassName: 'download',\n+\t\t\t\tsUrl: '{{ datatable_config.download|default('')|replace({'&amp;':'&'})|e('js') }}',\n+\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\tvar sUrl = config.sUrl;\n+\n+\t\t\t\t\tvar oParams = {};\n+\t\t\t\t\toParams.length = -1;\n+\t\t\t\t\toParams.columns = null;\n+\t\t\t\t\toParams.start = null;\n+\t\t\t\t\toParams.draw = null;\n+\t\t\t\t\tvar addtional_filterdata = oTable.dataTableSettings[0]['oAjaxData'];\n+\n+\t\t\t\t\tfor (var attrname in addtional_filterdata)\n+\t\t\t\t\t{\n+\t\t\t\t\t\toParams[attrname] = addtional_filterdata[attrname];\n+\t\t\t\t\t}\n+\t\t\t\t\tvar iframe = document.createElement('iframe');\n+\t\t\t\t\tiframe.style.height = \"0px\";\n+\t\t\t\t\tiframe.style.width = \"0px\";\n+\n+\t\t\t\t\tif(typeof(oParams.order[0]) != 'undefined')\n+\t\t\t\t\t{\n+\t\t\t\t\t\tvar column = oParams.order[0].column;\n+\t\t\t\t\t\tvar dir = oParams.order[0].dir;\n+\t\t\t\t\t\tvar column_to_keep = oParams.columns[column];\n+\t\t\t\t\t\tdelete oParams.columns;\n+\t\t\t\t\t\toParams.columns = {};\n+\t\t\t\t\t\tif(JqueryPortico.columns[column]['orderable'] == true)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\toParams.columns[column] = column_to_keep;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t\telse\n+\t\t\t\t\t{\n+\t\t\t\t\t\t\tdelete oParams.columns;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tiframe.src = sUrl + \"&\" + $.param(oParams) + \"&export=1\" + \"&query=\" + $('.dt-search input[aria-controls=\"datatable-container\"]').val();\n+\n+\t\t\t\t\tif(confirm(\"This will take some time...\"))\n+\t\t\t\t\t{\n+\t\t\t\t\t\tdocument.body.appendChild( iframe );\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\n+\t\t\t});\n+\t\t{% endif %}\n+\t\t{% if datatable_config.columns is defined and datatable_config.columns %}\n+\t\t\tbutton_def.push({\n+\t\t\t\t{% if datatable_config.columns.name is defined and datatable_config.columns.name %}\n+\t\t\t\t\ttext: \"{{ datatable_config.columns.name|e('js') }}\",\n+\t\t\t\t\ttitleAttr: \"{{ datatable_config.columns.name|e('js') }}\",\n+\t\t\t\t{% else %}\n+\t\t\t\t\ttext: \"{{ lang('columns')|e('js') }}\",\n+\t\t\t\t\ttitleAttr: \"{{ lang('columns')|e('js') }}\",\n+\t\t\t\t{% endif %}\n+\t\t\t\tclassName: 'download',\n+\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t{{ datatable_config.columns.onclick|default('')|raw }};\n+\t\t\t\t}\n+\t\t\t});\n+\t\t{% endif %}\n+\t\t{% if datatable_config.column_search is defined and datatable_config.column_search %}\n+\t\t\tbutton_def.push({\n+\t\t\t\t{% if datatable_config.column_search.name is defined and datatable_config.column_search.name %}\n+\t\t\t\t\ttext: \"{{ datatable_config.column_search.name|e('js') }}\",\n+\t\t\t\t\ttitleAttr: \"{{ datatable_config.column_search.name|e('js') }}\",\n+\t\t\t\t{% else %}\n+\t\t\t\t\ttext: \"{{ lang('column search')|e('js') }}\",\n+\t\t\t\t\ttitleAttr: \"{{ lang('column search')|e('js') }}\",\n+\t\t\t\t{% endif %}\n+\t\t\t\tclassName: 'download',\n+\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t{{ datatable_config.column_search.onclick|default('')|raw }};\n+\t\t\t\t}\n+\t\t\t});\n+\t\t{% endif %}\n+\t\t{% if datatable_config.actions is defined and datatable_config.actions is not empty %}\n+\t\t\t{% for action in datatable_config.actions %}\n+\t\t\t\t{% if action.type is defined and action.type == 'custom' %}\n+\t\t\t\t\taction_def.push({\n+\t\t\t\t\t\ttext: \"{{ action.text|default('')|e('js') }}\",\n+\t\t\t\t\t\t{% if action.className is defined and action.className %}\n+\t\t\t\t\t\t\tclassName: \"{{ action.className|e('js') }}\",\n+\t\t\t\t\t\t{% else %}\n+\t\t\t\t\t\t\tenabled: false,\n+\t\t\t\t\t\t\tclassName: 'record',\n+\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\taction: function (e, dt, node, config)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t{% if action.confirm_msg is defined and action.confirm_msg %}\n+\t\t\t\t\t\t\t\tvar confirm_msg = \"{{ action.confirm_msg|e('js') }}\";\n+\t\t\t\t\t\t\t\tvar r = confirm(confirm_msg);\n+\t\t\t\t\t\t\t\tif (r != true) {\n+\t\t\t\t\t\t\t\t\treturn false;\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\t\tfnSetSelected(this, dt);\n+\t\t\t\t\t\t\t{{ action.custom_code|default('')|raw }}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t});\n+\t\t\t\t{% else %}\n+\t\t\t\t\taction_def.push({\n+\t\t\t\t\t\ttext: \"{{ action.text|default('')|e('js') }}\",\n+\t\t\t\t\t\t{% if action.className is defined and action.className %}\n+\t\t\t\t\t\t\tclassName: \"{{ action.className|e('js') }}\",\n+\t\t\t\t\t\t{% else %}\n+\t\t\t\t\t\t\tenabled: false,\n+\t\t\t\t\t\t\tclassName: 'record',\n+\t\t\t\t\t\t{% endif %}\n+\t\t\t\t\t\taction: function (e, dt, node, config) {\n+\t\t\t\t\t\t\tvar receiptmsg = [];\n+\t\t\t\t\t\t\tfnSetSelected(this, dt);\n+\n+\t\t\t\t\t\t\tvar selected = fnGetSelected();\n+\t\t\t\t\t\t\tvar numSelected = selected.length;\n+\n+\t\t\t\t\t\t\tif (numSelected == 0){\n+\t\t\t\t\t\t\t\talert('None selected');\n+\t\t\t\t\t\t\t\treturn false;\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t{% if action.confirm_msg is defined and action.confirm_msg %}\n+\t\t\t\t\t\t\t\tvar confirm_msg = \"{{ action.confirm_msg|e('js') }}\";\n+\t\t\t\t\t\t\t\tvar r = confirm(confirm_msg);\n+\t\t\t\t\t\t\t\tif (r != true) {\n+\t\t\t\t\t\t\t\t\treturn false;\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t{% endif %}\n+\n+\t\t\t\t\t\t\tvar target = \"{{ action.target|default('')|e('js') }}\";\n+\t\t\t\t\t\t\tif(!target)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\ttarget = '_self';\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\tif (numSelected > 1){\n+\t\t\t\t\t\t\t\ttarget = '_blank';\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\tvar n = 0;\n+\t\t\t\t\t\t\tfor (; n < numSelected; ) {\n+\t\t\t\t\t\t\t\tvar aData = oTable.api().rows(selected[n]).data()[0]; //complete dataset from json returned from server\n+\n+\t\t\t\t\t\t\t\tvar action = \"{{ action.action|default('')|replace({'&amp;':'&'})|e('js') }}\";\n+\t\t\t\t\t\t\t\tvar my_name = \"{{ action.my_name|default('')|replace({'&amp;':'&'})|e('js') }}\";\n+\n+\t\t\t\t\t\t\t\t{% if action.parameters is defined and action.parameters %}\n+\t\t\t\t\t\t\t\t\tvar parameters = {{ action.parameters|raw }};\n+\t\t\t\t\t\t\t\t\tvar i = 0;\n+\t\t\t\t\t\t\t\t\tlen = parameters.parameter.length;\n+\t\t\t\t\t\t\t\t\tfor (; i < len; ) {\n+\t\t\t\t\t\t\t\t\t\taction += '&' + parameters.parameter[i]['name'] + '=' + aData[parameters.parameter[i]['source']];\n+\t\t\t\t\t\t\t\t\t\ti++;\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t{% endif %}\n+\n+\t\t\t\t\t\t\t\t// look for the word \"DELETE\" in URL and my_name\n+\t\t\t\t\t\t\t\tif(substr_count(action,'delete')>0 || substr_count(my_name,'delete')>0)\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\taction += \"&confirm=yes&phpgw_return_as=json\";\n+\t\t\t\t\t\t\t\t\texecute_ajax(action, function(result){\n+\t\t\t\t\t\t\t\t\t\tdocument.getElementById(\"message\").innerHTML += '<br/>' + result;\n+\n+\t\t\t\t\t\t\t\t\t\toTable.api().draw('page');\n+\n+\t\t\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\telse if (target == 'ajax')\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\taction += \"&phpgw_return_as=json\";\n+\t\t\t\t\t\t\t\t\texecute_ajax(action, function(result){\n+\t\t\t\t\t\t\t\t\t\tdocument.getElementById(\"message\").innerHTML += '<br/>' + result;\n+\t\t\t\t\t\t\t\t\t\toTable.api().draw('page');\n+\t\t\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\telse\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\twindow.open(action,target);\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tn++;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t});\n+\t\t\t\t{% endif %}\n+\t\t\t{% endfor %}\n+\t\t\t{% if datatable_config.group_buttons is defined and datatable_config.group_buttons == '1' %}\n+\t\t\t\tgroup_buttons = true;\n+\t\t\t{% else %}\n+\t\t\t\tgroup_buttons = false;\n+\t\t\t{% endif %}\n+\n+\t\t\tvar item_name = '';\n+\t\t\tvar contextMenuItem = {};\n+\t\t\tfor(i=0;i < action_def.length;i++)\n+\t\t\t{\n+\t\t\t\tbutton_def.push(action_def[i]);\n+\t\t\t\tif( typeof(action_def[i]['className']) != 'undefined' && action_def[i]['className'] == 'record')\n+\t\t\t\t{\n+\t\t\t\t\tcontextMenuItems[i] = {name:action_def[i]['text'], callback:action_def[i]['action']};\n+\t\t\t\t\tInitContextMenu = true;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif(button_def.length > 10)\n+\t\t\t{\n+\t\t\t\tgroup_buttons = true;\n+\t\t\t}\n+\n+\t\t\tvar isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);\n+\t\t\tif($(document).width() < 1000)\n+\t\t\t{\n+\t\t\t\tgroup_buttons = true;\n+\t\t\t}\n+\n+\t\t\tif(isChrome == true)\n+\t\t\t{\n+\t\t\t\tgroup_buttons = false;\n+\t\t\t}\n+\t\t\t//disable grouping for now\n+\t\t\tgroup_buttons = false;\n+\t\t{% endif %}\n+\t\tif(button_def.length > 0)\n+\t\t{\n+\t\t\tif(group_buttons === true)\n+\t\t\t{\n+\t\t\t\tJqueryPortico.buttons = [\n+\t\t\t\t\t{\n+\t\t\t\t\t\textend: 'collection',\n+\t\t\t\t\t\tautoClose: true,\n+\t\t\t\t\t\ttext: \"{{ lang('toolbar')|e('js') }}\",\n+\t\t\t\t\t\tcollectionLayout: 'three-column',\n+\t\t\t\t\t\tbuttons: button_def\n+\t\t\t\t\t}\n+\t\t\t\t];\n+\t\t\t}\n+\t\t\telse\n+\t\t\t{\n+\t\t\t\tJqueryPortico.buttons = button_def;\n+\t\t\t}\n+\t\t}\n+\t\telse\n+\t\t{\n+\t\t\tJqueryPortico.buttons = null;\n+\t\t}\n+\n+\t\tfor(i=0;i < JqueryPortico.columns.length;i++)\n+\t\t{\n+\t\t\tif (JqueryPortico.columns[i]['editor'] === true)\n+\t\t\t{\n+\t\t\t\teditor_cols.push({sUpdateURL:editor_action + '&field_name=' + JqueryPortico.columns[i]['data']});\n+\t\t\t}\n+\t\t\telse\n+\t\t\t{\n+\t\t\t\teditor_cols.push(null);\n+\t\t\t}\n+\t\t}\n+\n+\t\tif(order_def.length == 0)\n+\t\t{\n+\t\t\tfor(i=0;i < JqueryPortico.columns.length;i++)\n+\t\t\t{\n+\t\t\t\tif (JqueryPortico.columns[i]['orderable'] === true && typeof(JqueryPortico.columns[i]['dir']) != 'undefined')\n+\t\t\t\t{\n+\t\t\t\t\tvar dir = JqueryPortico.columns[i]['dir'] || \"desc\";\n+\t\t\t\t\torder_def.push([i, dir]);\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\n+\t\t// Build columnDefs to only enable columnControl for orderable columns\n+\t\tvar columnDefs = [];\n+\t\tfor(i=0;i < JqueryPortico.columns.length;i++)\n+\t\t{\n+\t\t\tif (JqueryPortico.columns[i]['orderable'] == true)\n+\t\t\t{\n+\t\t\t\tcolumnDefs.push({\n+\t\t\t\t\ttarget: i,\n+\t\t\t\t\tcolumnControl: ['order']\n+\t\t\t\t});\n+\t\t\t}\n+\t\t}\n+\n+\t\tinit_multiselect = function(oControl)\n+\t\t{\n+\t\t\ttry\n+\t\t\t{\n+\t\t\t\toControl.multiselect({\n+\t\t\t\t\tbuttonClass: 'app-select',\n+\t\t\t\t\ttemplates: {\n+\t\t\t\t\t\tbutton: '<button type=\"button\" class=\"multiselect dropdown-toggle\" data-bs-toggle=\"dropdown\" data-toggle=\"dropdown\" aria-expanded=\"false\"><span class=\"multiselect-selected-text\"></span></button>',\n+\t\t\t\t\t},\n+\t\t\t\t\tbuttonWidth: 250,\n+\t\t\t\t\tincludeSelectAllOption: true,\n+\t\t\t\t\tenableFiltering: true,\n+\t\t\t\t\tenableCaseInsensitiveFiltering: true,\n+\n+\t\t\t\t\tonDropdownShown : function(event) {\n+\t\t\t\t\t\tsetTimeout(function(){\n+\t\t\t\t\t\t\toControl.parent().find(\"button.multiselect-clear-filter\").click();\n+\t\t\t\t\t\t\toControl.parent().find(\"input[type='search'].multiselect-search\").focus();\n+\t\t\t\t\t\t}, 100);\n+\t\t\t\t\t}\n+\t\t\t\t});\n+\n+\t\t\t}\n+\t\t\tcatch(err)\n+\t\t\t{\n+\t\t\t}\n+\t\t}\n+\n+\t\t/*\n+\t\t * Find and assign actions to filters\n+\t\t */\n+\t\tvar oControls = $('.dtable_custom_controls:first').find(':input[name]');\n+\n+\t\t$(document).ready(function() {\n+\n+\t\t\t/*\n+\t\t\t* For namespacing the state\n+\t\t\t*/\n+\t\t\tvar table_url = JqueryPortico.parseURL(window.location.href);\n+\t\t\tvar menuaction = 'dummy';\n+\t\t\tvar app_method = 'dummy';\n+\t\t\tvar app_method_referrer = 'dummy';\n+\n+\t\t\tvar column_search_is_initated = false;\n+\n+\t\t\ttry\n+\t\t\t{\n+\t\t\t\tmenuaction = table_url.searchObject.menuaction.replace(/\\./g, '_');\n+\t\t\t\tapp_method = menuaction.split('_')[0] + '_' + menuaction.split('_')[1];\n+\t\t\t\tlet refferer = new URL(document.referrer).searchParams.get('menuaction').replace(/\\./g, '_');\n+\t\t\t\tapp_method_referrer = refferer.split('_')[0] + '_' + refferer.split('_')[1];\n+\t\t\t}\n+\t\t\tcatch (e)\n+\t\t\t{\n+\t\t\t}\n+\t\t\t//clear state\n+\t\t\tvar clear_state = false;\n+\t\t\tif(typeof(table_url.searchObject.clear_state) != 'undefined' && table_url.searchObject.clear_state == 1)\n+\t\t\t{\n+\t\t\t\tclear_state = true;\n+\t\t\t}\n+\t\t\t//uiocation\n+\t\t\tif(typeof(table_url.searchObject.type_id) != 'undefined')\n+\t\t\t{\n+\t\t\t\tmenuaction += '_type_id' + table_url.searchObject.type_id;\n+\n+\t\t\t\tif(typeof(table_url.searchObject.location_code) != 'undefined')\n+\t\t\t\t{\n+\t\t\t\t\tlet location_code = table_url.searchObject.location_code.split('-');\n+\t\t\t\t\tapp_method += '_type_id' + location_code.length;\n+\t\t\t\t}\n+\t\t\t\telse\n+\t\t\t\t{\n+\t\t\t\t\tapp_method += '_type_id' + table_url.searchObject.type_id;\n+\t\t\t\t}\n+\n+\t\t\t\ttry\n+\t\t\t\t{\n+\t\t\t\t\tif(new URL(document.referrer).searchParams.get('location_code'))\n+\t\t\t\t\t{\n+\t\t\t\t\t\tlet location_code = new URL(document.referrer).searchParams.get('location_code').split('-');\n+\t\t\t\t\t\tapp_method_referrer += '_type_id' + location_code.length;\n+\t\t\t\t\t}\n+\t\t\t\t\telse\n+\t\t\t\t\t{\n+\t\t\t\t\t\tapp_method_referrer += '_type_id' + new URL(document.referrer).searchParams.get('type_id');\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\tcatch(e)\n+\t\t\t\t{\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\t//uientity\n+\t\t\tif(typeof(table_url.searchObject.entity_id) != 'undefined' && typeof(table_url.searchObject.cat_id) != 'undefined')\n+\t\t\t{\n+\t\t\t\tmenuaction += '_entity_id' + table_url.searchObject.entity_id + '_cat_id' + table_url.searchObject.cat_id;\n+\t\t\t\tapp_method += '_entity_id' + table_url.searchObject.entity_id + '_cat_id' + table_url.searchObject.cat_id;\n+\t\t\t}\n+\n+\t\t\t//uientity for document.referrer\n+\t\t\ttry\n+\t\t\t{\n+\t\t\t\tif(new URL(document.referrer).searchParams.get('entity_id') && new URL(document.referrer).searchParams.get('cat_id'))\n+\t\t\t\t{\n+\t\t\t\t\tapp_method_referrer += '_entity_id' + new URL(document.referrer).searchParams.get('entity_id') + '_cat_id' + new URL(document.referrer).searchParams.get('cat_id');\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcatch(e)\n+\t\t\t{\n+\t\t\t}\n+\n+\t\t\t//uigeneric\n+\t\t\tif(typeof(table_url.searchObject.type) != 'undefined' && menuaction.includes(\"uigeneric\"))\n+\t\t\t{\n+\t\t\t\tmenuaction += '_type_' + table_url.searchObject.type;\n+\t\t\t\tapp_method += '_type_' + table_url.searchObject.type;\n+\t\t\t}\n+\n+\t\t\t//uigeneric for document.referrer\n+\t\t\ttry\n+\t\t\t{\n+\t\t\t\tif(new URL(document.referrer).searchParams.get('type') && menuaction_referer.includes(\"uigeneric\"))\n+\t\t\t\t{\n+\t\t\t\t\tapp_method_referrer += '_type_' + new URL(document.referrer).searchParams.get('type');\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcatch(e)\n+\t\t\t{\n+\t\t\t}\n+\n+\t\t\tvar select = false;\n+\n+\t\t\tif(select_all)\n+\t\t\t{\n+\t\t\t\tselect = {style: 'multi'};\n+\t\t\t}\n+\t\t\telse\n+\t\t\t{\n+\t\t\t\tselect = true;\n+\t\t\t}\n+\n+\t\t\tif(button_def.length > 8)\n+\t\t\t{\n+\t\t\t\tvar layout = {\n+\t\t\t\t\t\t\ttop2Start: 'buttons',\n+\t\t\t\t\t\t\ttopStart: null,\n+\t\t\t\t\t\t\ttopEnd: 'search',\n+\t\t\t\t\t\t\tbottomStart: ['pageLength'],\n+\t\t\t\t\t\t\tbottomEnd: ['inputPaging'],\n+\t\t\t\t\t\t\tbottom2Start: 'info'\n+\t\t\t\t\t}\n+\t\t\t}\n+\t\t\telse\n+\t\t\t{\n+\t\t\t\tvar layout = {\n+\t\t\t\t\t\t\ttopStart: 'buttons',\n+\t\t\t\t\t\t\ttopEnd: 'search',\n+\t\t\t\t\t\t\tbottomStart: ['pageLength'],\n+\t\t\t\t\t\t\tbottomEnd: ['inputPaging'],\n+\t\t\t\t\t\t\tbottom2Start: 'info'\n+\t\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tinit_table = function()\n+\t\t\t{\n+\t\t\t\tvar stateSave = true;\n+\t\t\t\tvar pageReload = false;\n+\t\t\t\t// Detect page refresh\n+\t\t\t\tif (performance.getEntriesByType(\"navigation\")[0].type === \"reload\")\n+\t\t\t\t{\n+\t\t\t\t\tpageReload = true;\n+\t\t\t\t}\n+\n+\t\t\t\tconsole.log(app_method);\n+\t\t\t\tconsole.log(app_method_referrer);\n+\t\t\t\t//check referer and if it is the same as the current page, then clear state\n+\t\t\t\tif(!pageReload && app_method !== app_method_referrer)\n+\t\t\t\t{\n+\t\t\t\t\tstateSave = false;\n+\t\t\t\t}\n+\n+\t\t\t\toTable = $('#datatable-container').dataTable({\n+\t\t\t\t\tpaginate: disablePagination ? false : true,\n+\t\t\t\t\tsearchDelay: 1200,\n+\t\t\t\t\tprocessing: true,\n+\t\t\t\t\tserverSide: true,\n+\t\t\t\t\tresponsive: responsive,\n+\t\t\t\t\tselect: select,\n+\t\t\t\t\tdeferRender: true,\n+\t\t\t\t\tlayout: layout,\n+\t\t\t\t\tajax: {\n+\t\t\t\t\t\turl: ajax_url,\n+\t\t\t\t\t\tdata: function ( aoData ) {\n+\t\t\t\t\t\t\tvar _columns = aoData.columns;\n+\t\t\t\t\t\t\tdelete aoData.columns;\n+\t\t\t\t\t\t\taoData.columns = {};\n+\n+\t\t\t\t\t\t\tif(typeof(aoData.order[0]) != 'undefined')\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tvar column = aoData.order[0].column;\n+\t\t\t\t\t\t\t\tvar dir = aoData.order[0].dir;\n+\t\t\t\t\t\t\t\tvar column_to_keep = _columns[column];\n+\n+\t\t\t\t\t\t\t\tif(JqueryPortico.columns[column]['orderable'] == true)\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\taoData.columns[column] = column_to_keep;\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\tfor ( var i=0 ; i< _columns.length ; i++ )\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tif(_columns[i].searchable && _columns[i].search.value !==\"\")\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\taoData.columns[i] = _columns[i];\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\tactive_filters_html = [];\n+\t\t\t\t\t\t\tvar select = null;\n+\t\t\t\t\t\t\tfor (var i in filter_selects)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tselect = $(\"#\" + filter_selects[i]);\n+\t\t\t\t\t\t\t\tvar select_name = select.prop(\"name\");\n+\t\t\t\t\t\t\t\tvar select_value = select.val();\n+\t\t\t\t\t\t\t\taoData[select_name] = select_value;\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\toControls.each(function()\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\toControl = $(this);\n+\t\t\t\t\t\t\t\tif($(this).is(':checkbox'))\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tvar checkboxValue = $(this).is(':checked') ? 1 : 0;\n+\t\t\t\t\t\t\t\t\taoData[$(this).attr('name')] = checkboxValue;\n+\t\t\t\t\t\t\t\t\tif(checkboxValue === 1)\n+\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\tactive_filters_html.push($(this).attr('title'));\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\treturn;\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tvar test = $(this).val();\n+\t\t\t\t\t\t\t\t//\tconsole.log(test.constructor);\n+\t\t\t\t\t\t\t\tif ( $(this).attr('name') && test != null && test.constructor !== Array)\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tvalue = $(this).val().replace('\"', '\"');\n+\t\t\t\t\t\t\t\t\taoData[ $(this).attr('name') ] = value;\n+\t\t\t\t\t\t\t\t\tif(value && value != 0 )\n+\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\tactive_filters_html.push($(this).attr('title'));\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\tif ( $(this).attr('name') && test != null && test.constructor === Array)\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tvalue = $(this).val();\n+\t\t\t\t\t\t\t\t\taoData[ $(this).attr('name') ] = value;\n+\n+\t\t\t\t\t\t\t\t\tif(value.length > 0 )\n+\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\tactive_filters_html.push($(this).attr('title'));\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\tinit_multiselect(oControl);\n+\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t});\n+\n+\t\t\t\t\t\t\tif(active_filters_html.length > 0 )\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t$('#active_filters').html(\"Aktive filter: \" + active_filters_html.join(', '));\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tvar search_value = $('.dt-search input[aria-controls=\"datatable-container\"]').val();\n+\n+\t\t\t\t\t\t\tif(active_filters_html.length > 0 || search_value || column_search_is_initated)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t$('#reset_filter').show();\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\telse\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t$('#reset_filter').hide();\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t// Add custom filters for modern DataTables compatibility\n+\t\t\t\t\t\t\tif (customFilters) {\n+\t\t\t\t\t\t\t\tfor (var param in customFilters) {\n+\t\t\t\t\t\t\t\t\tif (customFilters.hasOwnProperty(param)) {\n+\t\t\t\t\t\t\t\t\t\taoData[param] = customFilters[param];\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t},\n+\t\t\t\t\t\tdataSrc: function ( json ) {\n+\t\t\t\t\t\t\tif (typeof(json.sessionExpired) != 'undefined' && json.sessionExpired == true)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\twindow.alert('sessionExpired - please log in');\n+\t\t\t\t\t\t\t\tJqueryPortico.lightboxlogin();//defined in common.js\n+\t\t\t\t\t\t\t\t//\toTable.api().ajax.reload( null, false ); // user paging is not reset on reload\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\telse\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\treturn json.data;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t},\n+\t\t\t\t\t\ttype: 'POST'\n+\t\t\t\t\t},\n+\t\t\t\t\tfnStateSaveParams: function ( oSettings, sValue ) {\n+\t\t\t\t\t\t//Save custom filters\n+\t\t\t\t\t\tvar temp = {};\n+\t\t\t\t\t\ttemp[menuaction] = {}\n+\t\t\t\t\t\toControls.each(function() {\n+\t\t\t\t\t\t\tif($(this).is(':checkbox'))\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tvar checkboxState = $(this).is(':checked') ? 1 : 0;\n+\t\t\t\t\t\t\t\tsValue[$(this).attr('name')] = checkboxState;\n+\t\t\t\t\t\t\t\ttemp[$(this).attr('name')] = checkboxState;\n+\t\t\t\t\t\t\t\treturn;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tif ( $(this).attr('name') && $(this).val() != null && $(this).val().constructor != Array)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tsValue[ $(this).attr('name') ] = $(this).val().replace('\"', '\"');\n+\t\t\t\t\t\t\t\ttemp[ $(this).attr('name') ] = $(this).val().replace('\"', '\"');\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tif ( $(this).attr('name') && $(this).val() != null && $(this).val().constructor == Array)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tsValue[ $(this).attr('name') ] = $(this).val();\n+\t\t\t\t\t\t\t\ttemp[ $(this).attr('name') ] = $(this).val();\n+\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t});\n+\t\t\t\t\t\tfor (var attrname in sValue)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\ttemp[attrname] = sValue[attrname];\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tlocalStorage.setItem('state_' + menuaction, JSON.stringify(temp));\n+\t\t\t\t\t\treturn sValue;\n+\t\t\t\t\t},\n+\t\t\t\t\tfnStateLoadParams: function ( oSettings, oData ) {\n+\t\t\t\t\t\t//Load custom filters\n+\t\t\t\t\t\tvar retrievedObject = localStorage.getItem('state_' + menuaction);\n+\t\t\t\t\t\tif(typeof(retrievedObject) != 'undefined')\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\tvar params = {};\n+\n+\t\t\t\t\t\t\ttry\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tparams = JSON.parse(retrievedObject);\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tcatch(err)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\t//\tconsole.log(oData);\n+\t\t\t\t\t\t//traverse oData.columns and remove search value\n+\t\t\t\t\t\t//\tif (clear_state == true)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\tfor (var attrname in oData.columns)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tif(typeof(oData.columns[attrname].search) != 'undefined')\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tdelete oData.columns[attrname].search;\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\t//\tconsole.log(params);\n+\t\t\t\t\t\tif(params !== null)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\toControls.each(function() {\n+\t\t\t\t\t\t\t\tvar oControl = $(this);\n+\t\t\t\t\t\t\t\t$.each(params, function(index, value) {\n+\t\t\t\t\t\t\t\t\tif ( index == oControl.attr('name') )\n+\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\tif (clear_state !== true)\n+\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\tif(oControl.is(':checkbox'))\n+\t\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\t\tvar shouldCheck = value === 1 || value === '1';\n+\t\t\t\t\t\t\t\t\t\t\t\toControl.prop('checked', shouldCheck);\n+\t\t\t\t\t\t\t\t\t\t\t\tcustomFilters[oControl.attr('name')] = shouldCheck ? 1 : 0;\n+\t\t\t\t\t\t\t\t\t\t\t\treturn;\n+\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\tif(value.constructor == Array)\n+\t\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\t\t$(oControl).find(\"option\").removeAttr('selected');\n+\n+\t\t\t\t\t\t\t\t\t\t\t\t$.each(value, function(i,e){\n+\t\t\t\t\t\t\t\t\t\t\t\t\toControl.find(\"option[value=\"+e+\"]\").prop(\"selected\", \"selected\");\n+\t\t\t\t\t\t\t\t\t\t\t\t});\n+\n+\t\t\t\t\t\t\t\t\t\t\t\t//\tinit_multiselect(oControl);\n+\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\telse\n+\t\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\t\toControl.val( value );\n+\t\t\t\t\t\t\t\t\t\t\t\ttry\n+\t\t\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\t\t\t$(oControl).removeAttr('selected').find(\"option[value='\"+value+\"']\").attr('selected', 'selected');\n+\n+\t\t\t\t\t\t\t\t\t\t\t\t\tif($(oControl).find(\"option\").length > 0)\n+\t\t\t\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\t//\t\t$(oControl).formSelect();\n+\t\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t\t\tcatch(err)\n+\t\t\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t\t});\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\treturn true;\n+\t\t\t\t\t},\n+\t\t\t\t\tfnCreatedRow: function( nRow, aData, iDataIndex ){\n+\t\t\t\t\t},\n+\t\t\t\t\tfnRowCallback: function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {\n+\t\t\t\t\t\tif(typeof(aData['priority']) != undefined && aData['priority'] > 0)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t$(nRow).addClass('priority' + aData['priority']);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\t//In case the row is folded as result of responsive behaviour\n+\t\t\t\t\t\t$('td', nRow).parents('tr').addClass('context-menu');\n+\t\t\t\t\t},\n+\t\t\t\t\tfnDrawCallback: function () {\n+\t\t\t\t\t\toTable.makeEditable({\n+\t\t\t\t\t\t\tsUpdateURL: editor_action,\n+\t\t\t\t\t\t\tfnOnEditing: function(input){\n+\t\t\t\t\t\t\t\tcell = input.parents(\"td\");\n+\n+\t\t\t\t\t\t\t\t//it is a hack...but it works\n+\t\t\t\t\t\t\t\tvar rowIndex = cell.parents(\"tr\")[0]._DT_RowIndex;\n+\n+\t\t\t\t\t\t\t\t//\tconsole.log(rowIndex);\n+\t\t\t\t\t\t\t\t//\tconsole.log(oTable);\n+\t\t\t\t\t\t\t\tvar aData = oTable.api().rows( rowIndex ).data()[0];\n+\t\t\t\t\t\t\t\t//\tconsole.log(aData);\n+\t\t\t\t\t\t\t\tid = aData.id;\n+\t\t\t\t\t\t\t\t//\tconsole.log(id);\n+\t\t\t\t\t\t\t\treturn true;\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\tfnOnEdited: function(status, sOldValue, sNewCellDisplayValue, aPos0, aPos1, aPos2)\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\tvar feedbackText = '';\n+\t\t\t\t\t\t\t\tvar feedbackStatus = '';\n+\n+\t\t\t\t\t\t\t\tif (typeof status === 'object' && status !== null)\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tfeedbackStatus = status.status || '';\n+\t\t\t\t\t\t\t\t\tfeedbackText = status.message || status.status || '';\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\telse if (typeof status === 'string')\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\ttry\n+\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\tvar parsedStatus = JSON.parse(status);\n+\t\t\t\t\t\t\t\t\t\tif (parsedStatus && typeof parsedStatus === 'object')\n+\t\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\t\tfeedbackStatus = parsedStatus.status || '';\n+\t\t\t\t\t\t\t\t\t\t\tfeedbackText = parsedStatus.message || parsedStatus.status || '';\n+\t\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\tcatch (e)\n+\t\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\t\tfeedbackText = status;\n+\t\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t\tif (!feedbackText)\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tfeedbackText = String(status);\n+\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t\tif (feedbackStatus === 'success')\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tfeedbackText = 'OK: ' + feedbackText;\n+\t\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\t\telse if (feedbackStatus === 'error')\n+\t\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\tfeedbackText = 'ERROR: ' + feedbackText;\n+\t\t\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t\t\tdocument.getElementById(\"message\").innerHTML += '<br/>' + feedbackText;\n+\t\t\t\t\t\t\t\tsetTimeout(function(){\n+\t\t\t\t\t\t\t\t\tdocument.getElementById(\"message\").innerHTML = '';\n+\t\t\t\t\t\t\t\t}, 1000);\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\toUpdateParameters: {\n+\t\t\t\t\t\t\t\t\"id\": function(){ return id; }\n+\t\t\t\t\t\t\t},\n+\t\t\t\t\t\t\taoColumns: editor_cols,\n+\t\t\t\t\t\t\tsSuccessResponse: \"IGNORE\",\n+\t\t\t\t\t\t\tfnShowError: function(){ return; }\n+\t\t\t\t\t\t});\n+\t\t\t\t\t\tif(typeof(addFooterDatatable) == 'function')\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\taddFooterDatatable(oTable);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t},\n+\t\t\t\t\tfnFooterCallback: function ( nRow, aaData, iStart, iEnd, aiDisplay ) {\n+\t\t\t\t\t\tif(typeof(addFooterDatatable2) == 'function')\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\taddFooterDatatable2(nRow, aaData, iStart, iEnd, aiDisplay, oTable);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t},//alternative\n+\t\t\t\t\tfnInitComplete: function (oSettings, json)\n+\t\t\t\t\t{\n+\t\t\t\t\t\t$(\".btn-group\").addClass('w-100');\n+\t\t\t\t\t\t$(\".dropdown-menu\").addClass('w-100');\n+\t\t\t\t\t\t$(\".multiselect \").addClass('app-input');\n+\t\t\t\t\t\t$(\".multiselect\").removeClass('btn');\n+\t\t\t\t\t\t$(\".multiselect\").removeClass('btn-default');\n+\n+\t\t\t\t\t\tif(typeof(initCompleteDatatable) == 'function')\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\tinitCompleteDatatable(oSettings, json, oTable);\n+\t\t\t\t\t\t}\n+\n+\t\t\t\t\t},\n+\t\t\t\t\tlengthMenu: JqueryPortico.i18n.lengthmenu(),\n+\t\t\t\t\tlanguage: JqueryPortico.i18n.datatable(),\n+\t\t\t\t\tcolumns: JqueryPortico.columns,\n+\t\t\t\t\tstateSave: stateSave,\n+\t\t\t\t\tstateDuration: -1, //sessionstorage\n+\t\t\t\t\ttabIndex: 1,\n+\t\t\t\t\t\"search\": initial_search,\n+\t\t\t\t\t\"order\": order_def,\n+\t\t\t\t\tautoWidth: true,\n+\t\t\t\t\tbuttons: JqueryPortico.buttons,\n+\t\t\t\t\tordering: {indicators: false, handler: false},\n+\t\t\t\t\tcolumnDefs: columnDefs\n+\t\t\t\t});\n+\t\t\t};\n+\n+\t\t\tinit_table();\n+\n+\t\t\trestore_temporary_hidden_columns = function()\n+\t\t\t{\n+\t\t\t\t$('#datatable-container thead th').each(function(colIdx)\n+\t\t\t\t{\n+\t\t\t\t\toTable.api().column(colIdx).visible(true);\n+\t\t\t\t});\n+\t\t\t};\n+\n+\t\t\trestore_temporary_hidden_columns();\n+\n+\n+\t\t\t$('#datatable-container tbody').on( 'click', 'tr', function () {\n+\t\t\t\tif($(this).hasClass('child'))\n+\t\t\t\t{\n+\t\t\t\t\treturn;\n+\t\t\t\t}\n+\t\t\t\t$(this).toggleClass('selected');\n+\t\t\t\tvar api = oTable.api();\n+\t\t\t\t//\talert( api.rows('.selected').data().length +' row(s) selected' );\n+\t\t\t\t//\tvar selectedRows = api.rows( { selected: true } ).count();\n+\t\t\t\tvar selectedRows = api.rows('.selected').data().length;\n+\t\t\t\tapi.buttons( '.record' ).enable( selectedRows > 0 );\n+\t\t\t\tvar row = $(this);\n+\t\t\t\tvar checkbox = row.find('input[type=\"checkbox\"]');\n+\n+\t\t\t\tif(checkbox && checkbox.hasClass('mychecks'))\n+\t\t\t\t{\n+\t\t\t\t\tif($(this).hasClass('selected'))\n+\t\t\t\t\t{\n+\t\t\t\t\t\tcheckbox.prop(\"checked\", true);\n+\t\t\t\t\t}\n+\t\t\t\t\telse\n+\t\t\t\t\t{\n+\t\t\t\t\t\tcheckbox.prop(\"checked\", false);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t} );\n+\n+\n+\t\t\tvar colunm_search = false;\n+\t\t\tvar hidden_columns = [];\n+\n+\t\t\tremove_column_search = function()\n+\t\t\t{\n+\t\t\t\t//show hidden columns\n+\t\t\t\tfor (var i = 0; i < hidden_columns.length; i++)\n+\t\t\t\t{\n+\t\t\t\t\toTable.api().column(hidden_columns[i]).visible(true);\n+\t\t\t\t}\n+\t\t\t\thidden_columns = [];\n+\n+\t\t\t\t//remove search input from header\n+\t\t\t\t$('#datatable-container thead th').each(function(colIdx)\n+\t\t\t\t{\n+\t\t\t\t\tif(oTable.api().settings()[0].aoColumns[colIdx].bSearchable)\n+\t\t\t\t\t{\n+\t\t\t\t\t\tif($(this).find('input.column_search').length > 0)\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\tvar placeholder = $(this).find('input.column_search').attr('placeholder');\n+\t\t\t\t\t\t\t$(this).html(placeholder.split(lang['Search'] + ' ')[1]);\n+\t\t\t\t\t\t\t//remove text input from header by classname\n+\t\t\t\t\t\t\t$(this).find('input.column_search').remove();\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\n+\t\t\t\t});\n+\n+\t\t\t\tcolunm_search = false;\n+\t\t\t\toTable.api().responsive.recalc();\n+\t\t\t};\n+\n+\t\t\t//---- START column search ----\n+\t\t\tinit_column_search = function()\n+\t\t\t{\n+\t\t\t\tif(colunm_search == true)\n+\t\t\t\t{\n+\t\t\t\t\tremove_column_search();\n+\t\t\t\t\treturn false;\n+\t\t\t\t}\n+\n+\t\t\t\tcolunm_search = true;\n+\t\t\t\tlet reset_filter_is_visible = false;\n+\t\t\t\tif ($('#reset_filter').is(':visible'))\n+\t\t\t\t{\n+\t\t\t\t\t// The #reset_filter button is visible\n+\t\t\t\t\treset_filter_is_visible = true;\n+\t\t\t\t}\n+\n+\t\t\t\toTable.api().responsive.recalc();\n+\t\t\t\tif(reset_filter_is_visible == true)\n+\t\t\t\t{\n+\t\t\t\t\t$('#reset_filter').show();\n+\t\t\t\t}\n+\n+\t\t\t\t// Setup - add a text input to each header cell\n+\t\t\t\t$('#datatable-container thead th').each(function(colIdx)\n+\t\t\t\t{\n+\t\t\t\t\tif(oTable.api().settings()[0].aoColumns[colIdx].bSearchable)\n+\t\t\t\t\t{\n+\t\t\t\t\t\tvar title = $(this).text();\n+\t\t\t\t\t\tvar search_value = oTable.api().column(colIdx).search();\n+\t\t\t\t\t\t$(this).html('<input class=\"column_search\" type=\"text\" placeholder=\"' + lang['Search'] + ' ' + title + '\" value=\"' + search_value + '\" title=\"' + title + '\"/>');\n+\t\t\t\t\t}\n+\t\t\t\t\telse //hide the column from table\n+\t\t\t\t\t{\n+\t\t\t\t\t\toTable.api().column(colIdx).visible(false);\n+\t\t\t\t\t\thidden_columns.push(colIdx);\n+\t\t\t\t\t}\n+\n+\t\t\t\t});\n+\n+\t\t\t\t// Apply the search\n+\t\t\t\toTable.api().columns().eq(0).each(function(colIdx)\n+\t\t\t\t{\n+\t\t\t\t\tvar lastSearcCallback = 0;\n+\t\t\t\t\tvar delay = 200;\n+\t\t\t\t\t$('input', oTable.api().column(colIdx).header()).on('keyup change', function()\n+\t\t\t\t\t{\n+\t\t\t\t\t\tcolumn_search_is_initated = true;\n+\n+\t\t\t\t\t\tif (lastSearcCallback >= (Date.now() - delay))\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\treturn;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tlastSearcCallback = Date.now();\n+\n+\t\t\t\t\t\toTable.api()\n+\t\t\t\t\t\t\t.column(colIdx)\n+\t\t\t\t\t\t\t.search(this.value)\n+\t\t\t\t\t\t\t.draw();\n+\n+\t\t\t\t\t\t$('#reset_filter').show();\n+\n+\t\t\t\t\t});\n+\n+\t\t\t\t\t$('input', oTable.api().column(colIdx).header()).on('click', function(e) {\n+\t\t\t\t\t\te.stopPropagation();\n+\t\t\t\t\t});\n+\t\t\t\t});\n+\t\t\t\t//---- END column search ----\n+\t\t\t};\n+\n+\t\t\tif(InitContextMenu === true)\n+\t\t\t{\n+\t\t\t\t$('#datatable-container').contextMenu({\n+\t\t\t\t\tselector: '.context-menu',\n+\t\t\t\t\titems: contextMenuItems,\n+\t\t\t\t});\n+\t\t\t}\n+\n+\t\t\tif(number_of_toolbar_items < 5 || show_filter_group == true)\n+\t\t\t{\n+\t\t\t\t$('#democollapseBtn').addClass(\"show\");\n+\t\t\t}\n+\n+\t\t\t$('.dt-search input[aria-controls=\"datatable-container\"]').focus();\n+\t\t});\n+\n+\t\t/**\n+\t\t* Add left click action..\n+\t\t*/\n+\t\t{% if left_click_action %}\n+\t\t\t$(\"#datatable-container\").on(\"click\", \"tbody tr\", function() {\n+\t\t\t\t//\tvar iPos = oTable.fnGetPosition( this );\n+\t\t\t\tvar iPos = oTable.api().row(this).index();\n+\t\t\t\t//\tvar aData = oTable.fnGetData( iPos ); //complete dataset from json returned from server\n+\t\t\t\tvar aData = oTable.api().rows( iPos ).data()[0];\n+\t\t\t\ttry {\n+\t\t\t\t\t{{ left_click_action|raw }}\n+\t\t\t\t}\n+\t\t\t\tcatch(err) {\n+\t\t\t\t\tdocument.getElementById(\"message\").innerHTML = err.message;\n+\t\t\t\t}\n+\t\t\t});\n+\t\t{% endif %}\n+\n+\t\t/**\n+\t\t* Add dbl click action..\n+\t\t*/\n+\t\t{% if dbl_click_action %}\n+\t\t\t$(\"#datatable-container\").on(\"dblclick\", \"tr\", function() {\n+\t\t\t\t//\tvar iPos = oTable.fnGetPosition( this );\n+\t\t\t\tvar iPos = oTable.api().row(this).index();\n+\t\t\t\t//\tvar aData = oTable.fnGetData( iPos ); //complete dataset from json returned from server\n+\t\t\t\tvar aData = oTable.api().rows( iPos ).data()[0];\n+\t\t\t\ttry {\n+\t\t\t\t\t{{ dbl_click_action|raw }}(aData);\n+\t\t\t\t}\n+\t\t\t\tcatch(err) {\n+\t\t\t\t\tdocument.getElementById(\"message\").innerHTML = err.message;\n+\t\t\t\t}\n+\t\t\t});\n+\t\t{% endif %}\n+\n+\t\t{% for item in toolbar_items %}\n+\t\t\t{% if item.type == 'filter' %}\n+\t\t\t\t{% if item.multiple is defined and item.multiple %}\n+\t\t\t\t\t$('select#{{ item.name|default('')|e('js') }}').change( function()\n+\t\t\t\t\t{\n+\t\t\t\t\t\tvar search = [];\n+\t\t\t\t\t\t$.each($('select#{{ item.name|default('')|e('js') }} option:selected'), function(){\n+\t\t\t\t\t\t\tsearch.push($(this).val());\n+\t\t\t\t\t\t});\n+\t\t\t\t\t\t{{ item.extra|default('')|raw }}\n+\t\t\t\t\t\tfilterData('{{ item.name|default('')|e('js') }}', search);\n+\t\t\t\t\t});\n+\t\t\t\t{% else %}\n+\t\t\t\t\t$('select#{{ item.name|default('')|e('js') }}').change( function()\n+\t\t\t\t\t{\n+\t\t\t\t\t\t{{ item.extra|default('')|raw }}\n+\t\t\t\t\t\tfilterData('{{ item.name|default('')|e('js') }}', $(this).val());\n+\t\t\t\t\t});\n+\t\t\t\t{% endif %}\n+\t\t\t{% endif %}\n+\t\t\t{% if item.type == 'date-picker' %}\n+\t\t\t\tvar previous_{{ item.id|default('')|e('js') }};\n+\t\t\t\t$(\"#filter_{{ item.id|default('')|e('js') }}\").on('keyup change', function ()\n+\t\t\t\t{\n+\t\t\t\t\tif ( $(this).val().trim() != (previous_{{ item.id|default('')|e('js') }} || '').trim() )\n+\t\t\t\t\t{\n+\t\t\t\t\t\tfilterData('{{ item.id|default('')|e('js') }}', $(this).val());\n+\t\t\t\t\t\tprevious_{{ item.id|default('')|e('js') }} = $(this).val();\n+\t\t\t\t\t}\n+\t\t\t\t});\n+\t\t\t{% endif %}\n+\t\t\t{% if item.type == 'checkbox' %}\n+\t\t\t\t$(\"#innertoolbar_{{ item.name|default('')|e('js') }}\").change( function()\n+\t\t\t\t{\n+\t\t\t\t\tif($(this).prop(\"checked\"))\n+\t\t\t\t\t{\n+\t\t\t\t\t\tfilterData('{{ item.name|default('')|e('js') }}', 1);\n+\t\t\t\t\t}\n+\t\t\t\t\telse\n+\t\t\t\t\t{\n+\t\t\t\t\t\tfilterData('{{ item.name|default('')|e('js') }}', 0);\n+\t\t\t\t\t}\n+\t\t\t\t});\n+\t\t\t{% endif %}\n+\t\t\t{% if item.type == 'autocomplete' %}\n+\t\t\t\t$(document).ready(function() {\n+\t\t\t\t\t$('input.ui-autocomplete-input#filter_{{ item.name|default('')|e('js') }}_name').on('autocompleteselect', function(event, ui){\n+\t\t\t\t\t\tfilterData('filter_{{ item.name|default('')|e('js') }}_id', ui.item.value);\n+\t\t\t\t\t});\n+\t\t\t\t\t$('input.ui-autocomplete-input#filter_{{ item.name|default('')|e('js') }}_name').on('keyup', function(){\n+\t\t\t\t\t\tif ($(this).val() == '')\n+\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t$('#filter_{{ item.name|default('')|e('js') }}_id').val('');\n+\t\t\t\t\t\t\tfilterData('filter_{{ item.name|default('')|e('js') }}_id', $(this).val());\n+\t\t\t\t\t\t}\n+\t\t\t\t\t});\n+\t\t\t\t});\n+\t\t\t{% endif %}\n+\t\t{% endfor %}\n+\n+\t\tfunction fnGetSelected()\n+\t\t{\n+\t\t\tvar aReturn = new Array();\n+\t\t\tvar aTrs = oTable.api().rows().nodes();\n+\t\t\tfor ( var i=0 ; i < aTrs.length ; i++ )\n+\t\t\t{\n+\t\t\t\tif ( $(aTrs[i]).hasClass('context-menu-active'))\n+\t\t\t\t{\n+\t\t\t\t\taReturn.push( i );\n+\t\t\t\t\treturn aReturn;\n+\t\t\t\t}\n+\t\t\t\tif ( $(aTrs[i]).hasClass('selected') )\n+\t\t\t\t{\n+\t\t\t\t\taReturn.push( i );\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\treturn aReturn;\n+\t\t}\n+\n+\t\tfunction fnSetSelected( row , dt)\n+\t\t{\n+\t\t\tvar table = oTable.DataTable();\n+\t\t\tif(typeof(dt.trigger) != 'undefined' && dt.trigger == 'right')\n+\t\t\t{\n+\t\t\t\tvar aTrs = oTable.api().rows().nodes();\n+\n+\t\t\t\tfor ( var i=0 ; i < aTrs.length ; i++ )\n+\t\t\t\t{\n+\t\t\t\t\tif ( $(aTrs[i]).hasClass('selected') )\n+\t\t\t\t\t{\n+\t\t\t\t\t\ttable.row( i ).deselect();\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif(typeof(row[0]) == 'undefined')\n+\t\t\t{\n+\t\t\t\treturn false;\n+\t\t\t}\n+\n+\t\t\tvar sectionRowIndex = row[0].sectionRowIndex;\n+\t\t\tif(typeof(sectionRowIndex) != 'undefined')\n+\t\t\t{\n+\t\t\t\tvar selected = table.row( sectionRowIndex ).select();\n+\t\t\t}\n+\t\t}\n+\n+\t\tfunction execute_ajax(requestUrl, callback, data, type, dataType)\n+\t\t{\n+\t\t\ttype = typeof type !== 'undefined' ? type : 'POST';\n+\t\t\tdataType = typeof dataType !== 'undefined' ? dataType : 'html';\n+\t\t\tdata = typeof data !== 'undefined' ? data : {};\n+\n+\t\t\t$.ajax({\n+\t\t\t\ttype: type,\n+\t\t\t\tdataType: dataType,\n+\t\t\t\tdata: data,\n+\t\t\t\turl: requestUrl,\n+\t\t\t\tsuccess: function(result) {\n+\t\t\t\t\tcallback(result);\n+\t\t\t\t}\n+\t\t\t});\n+\t\t}\n+\n+\t\tfunction substr_count( haystack, needle, offset, length )\n+\t\t{\n+\t\t\tvar pos = 0, cnt = 0;\n+\n+\t\t\thaystack += '';\n+\t\t\tneedle += '';\n+\t\t\tif(isNaN(offset)) offset = 0;\n+\t\t\tif(isNaN(length)) length = 0;\n+\t\t\toffset--;\n+\n+\t\t\twhile( (offset = haystack.indexOf(needle, offset+1)) != -1 )\n+\t\t\t{\n+\t\t\t\tif(length > 0 && (offset+needle.length) > length)\n+\t\t\t\t{\n+\t\t\t\t\treturn false;\n+\t\t\t\t}\n+\t\t\t\telse\n+\t\t\t\t{\n+\t\t\t\t\tcnt++;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\treturn cnt;\n+\t\t}\n+\t});\n+\n+\treset_filter = function()\n+\t{\n+\t\t// Clear custom filters for modern DataTables compatibility\n+\t\tcustomFilters = {};\n+\n+\t\tvar api = oTable.api();\n+\t\tfor (var i in filter_selects)\n+\t\t{\n+\t\t\tselect = $(\"#\" + filter_selects[i]);\n+\t\t\tselect.prop('selectedIndex', 0);\n+\t\t\ttry\n+\t\t\t{\n+\t\t\t\tif($(\"#\" + filter_selects[i]).attr('multiple'))\n+\t\t\t\t{\n+\t\t\t\t\t$(\"#\" + filter_selects[i]).multiselect('deselectAll', false);\n+\t\t\t\t\t//\t\t\t$(\"#\" + filter_selects[i]).multiselect({ buttonContainer: '' });\n+\t\t\t\t\t$(\"#\" + filter_selects[i]).multiselect('refresh');\n+\t\t\t\t}\n+\n+\t\t\t\tcolumn_search_is_initated = false;\n+\t\t\t}\n+\t\t\tcatch(e)\n+\t\t\t{}\n+\t\t}\n+\n+\t\tvar oControls = $('.dtable_custom_controls:first').find(':input[name]');\n+\n+\t\toControls.each(function()\n+\t\t{\n+\t\t\tif($(this).is(':checkbox'))\n+\t\t\t{\n+\t\t\t\t$(this).prop('checked', false);\n+\t\t\t\t$(this).trigger('change');\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tvar test = $(this).val();\n+\t\t\tif ( !$(this).is('select') && $(this).attr('name') && test != null && test.constructor !== Array)\n+\t\t\t{\n+\t\t\t\tvalue = $(this).val('');\n+\t\t\t}\n+\t\t});\n+\n+\t\tapi.state.clear();\n+\t\tapi.destroy();\n+\t\tclear_state = true;\n+\t\tinit_table();\n+\t\trestore_temporary_hidden_columns();\n+\t\tremove_column_search();\n+\t\t$('#reset_filter').hide();\n+\t\t$('#active_filters').html(\"\");\n+\t\t// Deselect all selected options in Select2 and select the first option\n+\t\t$('.select2').each(function() {\n+\t\t\tvar $select = $(this);\n+\t\t\t$select.val('0');\n+\t\t\t$select.trigger('change');\n+\t\t\t// Update the displayed text in Select2\n+\t\t\tvar $selection = $select.find('.select2-selection__rendered');\n+\t\t\t$selection.text('');\n+\t\t});\n+\t}\n+\n+\n+\tfunction searchData(query)\n+\t{\n+\t\tvar api = oTable.api();\n+\t\tapi.search( query ).draw();\n+\t}\n+\n+\tfunction filterData(param, value)\n+\t{\n+\t\t// Store the filter value in the custom filters object for modern DataTables compatibility\n+\t\tcustomFilters[param] = value;\n+\n+\t\t// Also update the legacy way for backward compatibility\n+\t\ttry {\n+\t\t\toTable.dataTableSettings[0]['ajax']['data'][param] = value;\n+\t\t} catch(e) {\n+\t\t\t// Legacy method failed, modern approach will handle it\n+\t\t}\n+\n+\t\t// Reload the table data\n+\t\toTable.api().ajax.reload();\n+\t}\n+\n+\tfunction clearFilterParam(param)\n+\t{\n+\t\t// Clear from custom filters\n+\t\tif (customFilters && customFilters.hasOwnProperty(param)) {\n+\t\t\tdelete customFilters[param];\n+\t\t}\n+\n+\t\t// Also clear from legacy way for backward compatibility\n+\t\ttry {\n+\t\t\toTable.dataTableSettings[0]['ajax']['data'][param] = '';\n+\t\t} catch(e) {\n+\t\t\t// Legacy method failed, modern approach will handle it\n+\t\t}\n+\t}\n+\n+\tfunction reloadData()\n+\t{\n+\t\tvar api = oTable.api();\n+\t\tapi.ajax.reload();\n+\t}\n+</script>\n+\n+<script>\n+\t{% if js_lang is defined and js_lang != '' %}\n+\t\tvar lang = {{ js_lang|raw }};\n+\t{% endif %}\n+</script>\n+\n+{% if list_actions_items|length > 0 %}\n+\t<form id=\"list_actions_form\" method=\"POST\">\n+\t\t<!-- Form action is set by javascript listener -->\n+\t\t<div id=\"list_actions\">\n+\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\n+\t\t\t\t<tr>\n+\t\t\t\t\t{% for item in list_actions_items %}\n+\t\t\t\t\t\t<td valign=\"top\">\n+\t\t\t\t\t\t\t<input id=\"innertoolbar\" type=\"{{ item.type|default('') }}\" name=\"{{ item.name|default('') }}\" onclick=\"{{ item.onClick|default('')|raw }}\" value=\"{{ item.value|default('') }}\" href=\"{{ item.href|default('') }}\" />\n+\t\t\t\t\t\t</td>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</tr>\n+\t\t\t</table>\n+\t\t</div>\n+\t</form>\n+{% endif %}"},{"sha":"169962d00fd108e002752b3ec7601a31cdd7cb4f","filename":"src/modules/todo/controllers/TodoController.php","status":"added","additions":1187,"deletions":0,"changes":1187,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fcontrollers%2FTodoController.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fcontrollers%2FTodoController.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fcontrollers%2FTodoController.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,1187 @@\n+<?php\n+\n+namespace App\\modules\\todo\\controllers;\n+\n+use App\\helpers\\ResponseHelper;\n+use App\\modules\\property\\helpers\\BoCommon;\n+use App\\modules\\phpgwapi\\security\\Acl;\n+use App\\modules\\phpgwapi\\services\\Settings;\n+use App\\modules\\phpgwapi\\controllers\\Accounts\\Accounts;\n+use OpenApi\\Annotations as OA;\n+use Psr\\Http\\Message\\ResponseInterface as Response;\n+use Psr\\Http\\Message\\ServerRequestInterface as Request;\n+\n+/**\n+ * @OA\\Tag(\n+ *     name=\"Todo\",\n+ *     description=\"REST API for todo items\"\n+ * )\n+ *\n+ * @OA\\Schema(\n+ *     schema=\"TodoItem\",\n+ *     type=\"object\",\n+ *     @OA\\Property(property=\"id\", type=\"integer\"),\n+ *     @OA\\Property(property=\"cat_id\", type=\"integer\"),\n+ *     @OA\\Property(property=\"title\", type=\"string\"),\n+ *     @OA\\Property(property=\"level\", type=\"integer\"),\n+ *     @OA\\Property(property=\"status\", type=\"integer\", minimum=0, maximum=100),\n+ *     @OA\\Property(property=\"pri\", type=\"string\"),\n+ *     @OA\\Property(property=\"sdate\", type=\"string\"),\n+ *     @OA\\Property(property=\"edate\", type=\"string\"),\n+ *     @OA\\Property(property=\"owner\", type=\"string\"),\n+ *     @OA\\Property(\n+ *         property=\"assigned_entries\",\n+ *         type=\"array\",\n+ *         @OA\\Items(\n+ *             type=\"object\",\n+ *             @OA\\Property(property=\"id\", type=\"integer\"),\n+ *             @OA\\Property(property=\"type\", type=\"string\", enum={\"user\", \"group\"}),\n+ *             @OA\\Property(property=\"name\", type=\"string\")\n+ *         )\n+ *     )\n+ * )\n+ *\n+ * @OA\\Schema(\n+ *     schema=\"TodoDetail\",\n+ *     type=\"object\",\n+ *     @OA\\Property(property=\"id\", type=\"integer\"),\n+ *     @OA\\Property(property=\"title\", type=\"string\"),\n+ *     @OA\\Property(property=\"descr\", type=\"string\"),\n+ *     @OA\\Property(property=\"category\", type=\"string\"),\n+ *     @OA\\Property(property=\"parent\", type=\"string\"),\n+ *     @OA\\Property(property=\"status\", type=\"integer\", minimum=0, maximum=100),\n+ *     @OA\\Property(property=\"pri\", type=\"string\"),\n+ *     @OA\\Property(property=\"access\", type=\"string\"),\n+ *     @OA\\Property(property=\"owner\", type=\"string\"),\n+ *     @OA\\Property(\n+ *         property=\"assigned_entries\",\n+ *         type=\"array\",\n+ *         @OA\\Items(\n+ *             type=\"object\",\n+ *             @OA\\Property(property=\"id\", type=\"integer\"),\n+ *             @OA\\Property(property=\"type\", type=\"string\", enum={\"user\", \"group\"}),\n+ *             @OA\\Property(property=\"name\", type=\"string\")\n+ *         )\n+ *     ),\n+ *     @OA\\Property(property=\"sdate\", type=\"string\"),\n+ *     @OA\\Property(property=\"edate\", type=\"string\"),\n+ *     @OA\\Property(property=\"has_subs\", type=\"boolean\")\n+ * )\n+ *\n+ * @OA\\Schema(\n+ *     schema=\"TodoCategory\",\n+ *     type=\"object\",\n+ *     @OA\\Property(property=\"id\", type=\"integer\"),\n+ *     @OA\\Property(property=\"name\", type=\"string\")\n+ * )\n+ *\n+ * @OA\\Schema(\n+ *     schema=\"TodoUpsertRequest\",\n+ *     type=\"object\",\n+ *     @OA\\Property(property=\"title\", type=\"string\"),\n+ *     @OA\\Property(property=\"descr\", type=\"string\"),\n+ *     @OA\\Property(property=\"cat\", type=\"integer\"),\n+ *     @OA\\Property(property=\"parent\", type=\"integer\"),\n+ *     @OA\\Property(property=\"pri\", type=\"integer\"),\n+ *     @OA\\Property(property=\"status\", type=\"integer\", minimum=0, maximum=100),\n+ *     @OA\\Property(property=\"access\", type=\"boolean\"),\n+ *     @OA\\Property(property=\"assigned\", oneOf={@OA\\Schema(type=\"string\"), @OA\\Schema(type=\"array\", @OA\\Items(type=\"string\"))}),\n+ *     @OA\\Property(property=\"assigned_group\", oneOf={@OA\\Schema(type=\"string\"), @OA\\Schema(type=\"array\", @OA\\Items(type=\"string\"))}),\n+ *     @OA\\Property(property=\"sday\", type=\"integer\"),\n+ *     @OA\\Property(property=\"smonth\", type=\"integer\"),\n+ *     @OA\\Property(property=\"syear\", type=\"integer\"),\n+ *     @OA\\Property(property=\"eday\", type=\"integer\"),\n+ *     @OA\\Property(property=\"emonth\", type=\"integer\"),\n+ *     @OA\\Property(property=\"eyear\", type=\"integer\")\n+ * )\n+ *\n+ * @OA\\Schema(\n+ *     schema=\"TodoErrorResponse\",\n+ *     type=\"object\",\n+ *     @OA\\Property(property=\"error\", type=\"string\"),\n+ *     @OA\\Property(property=\"reason\", type=\"string\"),\n+ *     @OA\\Property(property=\"todo_id\", type=\"integer\"),\n+ *     @OA\\Property(property=\"parent_id\", type=\"integer\")\n+ * )\n+ */\n+class TodoController\n+{\n+\tprivate function mapDataTableColumnToSortKey(string $columnKey): string\n+\t{\n+\t\t$map = [\n+\t\t\t'id' => 'id',\n+\t\t\t'title' => 'title',\n+\t\t\t'status' => 'status',\n+\t\t\t'pri' => 'priority',\n+\t\t\t'sdate' => 'created',\n+\t\t\t'edate' => 'due',\n+\t\t\t'owner' => 'owner',\n+\t\t];\n+\n+\t\treturn $map[$columnKey] ?? 'id';\n+\t}\n+\n+\tprivate function isCircularParentAssignment($botodo, int $todoId, int $parentId): bool\n+\t{\n+\t\tif ($parentId <= 0)\n+\t\t{\n+\t\t\treturn false;\n+\t\t}\n+\n+\t\tif ($parentId === $todoId)\n+\t\t{\n+\t\t\treturn true;\n+\t\t}\n+\n+\t\t$descendants = (string) $botodo->sotodo->find_subs((string) $todoId);\n+\t\tif ($descendants === '')\n+\t\t{\n+\t\t\treturn false;\n+\t\t}\n+\n+\t\t$descendantIds = array_filter(array_map('intval', explode(',', $descendants)));\n+\t\treturn in_array($parentId, $descendantIds, true);\n+\t}\n+\n+\tprivate function getTodoHistoryData(int $id): array\n+\t{\n+\t\t$historylog = \\CreateObject('phpgwapi.historylog', 'todo', '.todo');\n+\t\t$userSettings = Settings::getInstance()->get('user');\n+\t\t$dateFormat = (string) ($userSettings['preferences']['common']['dateformat'] ?? 'Y-m-d');\n+\t\t$dateTimeFormat = trim($dateFormat . ' H:i');\n+\t\t$phpgwapiCommon = new \\phpgwapi_common();\n+\t\t$statusLabels = array(\n+\t\t\t'A' => lang('Entry added'),\n+\t\t\t'C' => lang('Category changed'),\n+\t\t\t'S' => lang('Start date changed'),\n+\t\t\t'E' => lang('End date changed'),\n+\t\t\t'U' => lang('Urgency changed'),\n+\t\t\t's' => lang('Status changed'),\n+\t\t\t'T' => lang('Title changed'),\n+\t\t\t'D' => lang('Description changed'),\n+\t\t\t'a' => lang('Access changed'),\n+\t\t\t'P' => lang('Parent changed'),\n+\t\t);\n+\n+\t\t$rows = (array) $historylog->return_array(array(), array(), '', '', $id);\n+\t\t$normalized = array();\n+\t\tforeach ($rows as $row)\n+\t\t{\n+\t\t\t$statusCode = (string) ($row['status'] ?? '');\n+\t\t\t$rawDatetime = $row['datetime'] ?? '';\n+\t\t\t$formattedDatetime = (string) $rawDatetime;\n+\t\t\tif (is_numeric($rawDatetime))\n+\t\t\t{\n+\t\t\t\t$timestamp = (int) $rawDatetime;\n+\t\t\t\tif ($timestamp > 9999999999)\n+\t\t\t\t{\n+\t\t\t\t\t$timestamp = (int) floor($timestamp / 1000);\n+\t\t\t\t}\n+\n+\t\t\t\tif ($timestamp > 0)\n+\t\t\t\t{\n+\t\t\t\t\t$formattedDatetime = (string) $phpgwapiCommon->show_date($timestamp, $dateTimeFormat);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\t$normalized[] = [\n+\t\t\t\t'id' => (int) ($row['id'] ?? 0),\n+\t\t\t\t'record_id' => (int) ($row['record_id'] ?? 0),\n+\t\t\t\t'owner' => (string) ($row['owner'] ?? ''),\n+\t\t\t\t'status' => $statusCode,\n+\t\t\t\t'status_label' => (string) ($statusLabels[$statusCode] ?? $statusCode),\n+\t\t\t\t'new_value' => (string) ($row['new_value'] ?? ''),\n+\t\t\t\t'old_value' => (string) ($row['old_value'] ?? ''),\n+\t\t\t\t'datetime' => $formattedDatetime,\n+\t\t\t\t'publish' => $row['publish'] ?? null,\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn $normalized;\n+\t}\n+\n+\tprivate function normalizeUrl(string $url): string\n+\t{\n+\t\treturn html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8');\n+\t}\n+\n+\tprivate function normalizeIdListCsv($value): string\n+\t{\n+\t\t$rawValues = is_array($value) ? $value : explode(',', (string) $value);\n+\t\t$ids = [];\n+\n+\t\tforeach ($rawValues as $raw)\n+\t\t{\n+\t\t\t$id = (int) $raw;\n+\t\t\tif ($id > 0)\n+\t\t\t{\n+\t\t\t\t$ids[] = $id;\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn implode(',', array_values(array_unique($ids)));\n+\t}\n+\n+\tprivate function sanitizePlainText($value): string\n+\t{\n+\t\treturn trim((string) \\phpgw::strip_html((string) $value));\n+\t}\n+\n+\tprivate function getCommonQueryParams(Request $request): array\n+\t{\n+\t\t$query = $request->getQueryParams();\n+\t\t$body = (array) ($request->getParsedBody() ?: []);\n+\t\t$params = array_merge($query, $body);\n+\n+\t\t$draw = isset($params['draw']) ? (int) $params['draw'] : 0;\n+\t\t$start = isset($params['start']) ? (int) $params['start'] : 0;\n+\t\t$limit = isset($params['limit']) ? (int) $params['limit'] : 100;\n+\t\tif (isset($params['length']))\n+\t\t{\n+\t\t\t$limit = (int) $params['length'];\n+\t\t}\n+\t\tif ($limit < 1)\n+\t\t{\n+\t\t\t$limit = 100;\n+\t\t}\n+\t\tif ($limit > 2000)\n+\t\t{\n+\t\t\t$limit = 2000;\n+\t\t}\n+\n+\t\t$search = '';\n+\t\tif (isset($params['search']) && is_array($params['search']))\n+\t\t{\n+\t\t\t$search = (string) ($params['search']['value'] ?? '');\n+\t\t}\n+\t\telse if (isset($params['search']))\n+\t\t{\n+\t\t\t$search = (string) $params['search'];\n+\t\t}\n+\n+\t\t$filter = isset($params['filter']) ? (string) $params['filter'] : 'none';\n+\t\t$catId = isset($params['cat_id']) ? (int) $params['cat_id'] : 0;\n+\n+\t\t$sortKey = (string) ($params['sort'] ?? 'id');\n+\t\t$dir = strtoupper((string) ($params['dir'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC';\n+\n+\t\tif (empty($params['sort']) && isset($params['order'][0]) && is_array($params['order'][0]))\n+\t\t{\n+\t\t\t$dtOrder = $params['order'][0];\n+\t\t\t$columnIndex = isset($dtOrder['column']) ? (int) $dtOrder['column'] : -1;\n+\t\t\tif ($columnIndex >= 0 && isset($params['columns'][$columnIndex]['data']))\n+\t\t\t{\n+\t\t\t\t$sortKey = $this->mapDataTableColumnToSortKey((string) $params['columns'][$columnIndex]['data']);\n+\t\t\t}\n+\t\t\t$dir = strtoupper((string) ($dtOrder['dir'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC';\n+\t\t}\n+\n+\t\t$sort = $this->mapSortKey($sortKey);\n+\n+\t\treturn [\n+\t\t\t'draw' => $draw,\n+\t\t\t'start' => $start,\n+\t\t\t'limit' => $limit,\n+\t\t\t'search' => $search,\n+\t\t\t'filter' => $filter,\n+\t\t\t'cat_id' => $catId,\n+\t\t\t'sort' => $sort,\n+\t\t\t'dir' => $dir,\n+\t\t];\n+\t}\n+\n+\tprivate function formatPriority($priority): string\n+\t{\n+\t\tswitch ((int) $priority)\n+\t\t{\n+\t\t\tcase 1:\n+\t\t\t\treturn lang('Low');\n+\t\t\tcase 2:\n+\t\t\t\treturn lang('normal');\n+\t\t\tcase 3:\n+\t\t\t\treturn lang('high');\n+\t\t\tdefault:\n+\t\t\t\treturn '';\n+\t\t}\n+\t}\n+\n+\tprivate function formatTodoTitle(array $todo): string\n+\t{\n+\t\t$title = \\phpgw::strip_html((string) ($todo['title'] ?? ''));\n+\t\tif (!$title)\n+\t\t{\n+\t\t\t$words = explode(' ', \\phpgw::strip_html((string) ($todo['descr'] ?? '')));\n+\t\t\t$title = trim(implode(' ', array_slice($words, 0, 4)) . ' ...');\n+\t\t}\n+\n+\t\treturn $title;\n+\t}\n+\n+\tprivate function normalizeAssignedIds($botodo, $raw): array\n+\t{\n+\t\tif (is_array($raw))\n+\t\t{\n+\t\t\t$values = $raw;\n+\t\t}\n+\t\telse\n+\t\t{\n+\t\t\t$values = $botodo->format_assigned((string) $raw);\n+\t\t}\n+\n+\t\t$ids = [];\n+\t\tforeach ((array) $values as $value)\n+\t\t{\n+\t\t\t$id = (int) $value;\n+\t\t\tif ($id > 0)\n+\t\t\t{\n+\t\t\t\t$ids[] = $id;\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn array_values(array_unique($ids));\n+\t}\n+\n+\tprivate function resolveAssignedName($botodo, int $accountId): string\n+\t{\n+\t\t$cached = $botodo->cached_accounts($accountId);\n+\t\tif (is_object($cached) && isset($cached->lid, $cached->firstname, $cached->lastname))\n+\t\t{\n+\t\t\t$phpgwapiCommon = new \\phpgwapi_common();\n+\t\t\treturn (string) $phpgwapiCommon->display_fullname(\n+\t\t\t\t(string) $cached->lid,\n+\t\t\t\t(string) $cached->firstname,\n+\t\t\t\t(string) $cached->lastname\n+\t\t\t);\n+\t\t}\n+\n+\t\t$accountsObj = new Accounts();\n+\t\treturn (string) $accountsObj->id2name($accountId);\n+\t}\n+\n+\tprivate function mapAssignedEntries($botodo, array $item): array\n+\t{\n+\t\t$entries = [];\n+\t\t$seen = [];\n+\n+\t\t$userIds = $this->normalizeAssignedIds($botodo, $item['assigned'] ?? '');\n+\t\tforeach ($userIds as $userId)\n+\t\t{\n+\t\t\t$key = 'user:' . $userId;\n+\t\t\tif (isset($seen[$key]))\n+\t\t\t{\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\t$seen[$key] = true;\n+\n+\t\t\t$entries[] = [\n+\t\t\t\t'id' => $userId,\n+\t\t\t\t'type' => 'user',\n+\t\t\t\t'name' => $this->resolveAssignedName($botodo, $userId),\n+\t\t\t];\n+\t\t}\n+\n+\t\t$groupIds = $this->normalizeAssignedIds($botodo, $item['assigned_group'] ?? '');\n+\t\tforeach ($groupIds as $groupId)\n+\t\t{\n+\t\t\t$key = 'group:' . $groupId;\n+\t\t\tif (isset($seen[$key]))\n+\t\t\t{\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\t$seen[$key] = true;\n+\n+\t\t\t$entries[] = [\n+\t\t\t\t'id' => $groupId,\n+\t\t\t\t'type' => 'group',\n+\t\t\t\t'name' => $this->resolveAssignedName($botodo, $groupId),\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn $entries;\n+\t}\n+\n+\tprivate function assignedEntriesToText(array $entries): string\n+\t{\n+\t\t$lines = array_values(array_filter(array_map(static function ($entry)\n+\t\t{\n+\t\t\treturn (string) ($entry['name'] ?? '');\n+\t\t}, $entries)));\n+\n+\t\treturn implode(\"\\n\", $lines);\n+\t}\n+\n+\tprivate function mapTodoDetail(array $item, $botodo): array\n+\t{\n+\t\t$userSettings = Settings::getInstance()->get('user');\n+\t\t$dateFormat = (string) ($userSettings['preferences']['common']['dateformat'] ?? 'Y-m-d');\n+\n+\t\t$ownerId = (int) ($item['owner'] ?? 0);\n+\t\t$ownerName = (string) ($item['owner'] ?? '');\n+\t\t$cached = $botodo->cached_accounts($ownerId);\n+\t\tif (is_object($cached) && isset($cached->lid, $cached->firstname, $cached->lastname))\n+\t\t{\n+\t\t\t$phpgwapiCommon = new \\phpgwapi_common();\n+\t\t\t$ownerName = (string) $phpgwapiCommon->display_fullname(\n+\t\t\t\t(string) $cached->lid,\n+\t\t\t\t(string) $cached->firstname,\n+\t\t\t\t(string) $cached->lastname\n+\t\t\t);\n+\t\t}\n+\n+\t\t$assignedEntries = $this->mapAssignedEntries($botodo, (array) $item);\n+\t\t$phpgwapiCommon = new \\phpgwapi_common();\n+\n+\t\t$categoryName = '';\n+\t\tif (!empty($item['cat']))\n+\t\t{\n+\t\t\t$cats = \\CreateObject('phpgwapi.categories');\n+\t\t\t$categoryName = (string) $cats->id2name((int) $item['cat']);\n+\t\t}\n+\n+\t\t$parentTitle = '';\n+\t\t$parentId = (int) ($item['parent'] ?? 0);\n+\t\tif ($parentId > 0)\n+\t\t{\n+\t\t\t$parent = $botodo->read($parentId);\n+\t\t\tif (is_array($parent))\n+\t\t\t{\n+\t\t\t\t$parentTitle = (string) \\phpgw::strip_html((string) ($parent['title'] ?? ''));\n+\t\t\t}\n+\t\t}\n+\n+\t\t$tzOffset = (int) ($botodo->datetime->tz_offset ?? 0);\n+\t\t$sdateTs = (int) ($item['sdate'] ?? 0);\n+\t\t$edateTs = (int) ($item['edate'] ?? 0);\n+\n+\t\t$startDate = '';\n+\t\tif ($sdateTs > 0)\n+\t\t{\n+\t\t\t$startDate = (string) $phpgwapiCommon->show_date($sdateTs - $tzOffset, $dateFormat);\n+\t\t}\n+\n+\t\t$endDate = '';\n+\t\tif ($edateTs > 0)\n+\t\t{\n+\t\t\t$endDate = (string) $phpgwapiCommon->show_date($edateTs - $tzOffset, $dateFormat);\n+\t\t}\n+\n+\t\t$priority = $this->formatPriority($item['pri'] ?? 0);\n+\n+\t\treturn [\n+\t\t\t'id' => (int) ($item['id'] ?? 0),\n+\t\t\t'title' => (string) \\phpgw::strip_html((string) ($item['title'] ?? '')),\n+\t\t\t'descr' => (string) \\phpgw::strip_html((string) ($item['descr'] ?? '')),\n+\t\t\t'category' => $categoryName,\n+\t\t\t'parent' => $parentTitle,\n+\t\t\t'status' => (int) ($item['status'] ?? 0),\n+\t\t\t'pri' => $priority,\n+\t\t\t'access' => (string) ($item['access'] ?? ''),\n+\t\t\t'owner' => $ownerName,\n+\t\t\t'assigned_entries' => $assignedEntries,\n+\t\t\t'sdate' => $startDate,\n+\t\t\t'edate' => $endDate,\n+\t\t\t'has_subs' => (bool) $botodo->exists((int) ($item['id'] ?? 0)),\n+\t\t];\n+\t}\n+\n+\tprivate function mapTodoItems(array $todoList, $botodo, array $grants, int $catId): array\n+\t{\n+\t\t$userSettings = Settings::getInstance()->get('user');\n+\t\t$currentAccountId = (int) ($userSettings['account_id'] ?? 0);\n+\t\t$accountsObj = new Accounts();\n+\n+\t\t$rows = [];\n+\t\tforeach ($todoList as $todo)\n+\t\t{\n+\t\t\t$id = (int) ($todo['id'] ?? 0);\n+\t\t\t$ownerId = (int) ($todo['owner_id'] ?? 0);\n+\t\t\t$canEdit = $botodo->check_perms($ownerId, $grants, ACL_EDIT) || $ownerId === $currentAccountId;\n+\t\t\t$canDelete = $botodo->check_perms($ownerId, $grants, ACL_DELETE) || $ownerId === $currentAccountId;\n+\n+\t\t\t$assignedEntries = $this->mapAssignedEntries($botodo, (array) $todo);\n+\n+\t\t\t$rows[] = [\n+\t\t\t\t'id' => $id,\n+\t\t\t\t'cat_id' => (int) ($todo['cat'] ?? $catId),\n+\t\t\t\t'title' => $this->formatTodoTitle((array) $todo),\n+\t\t\t\t'level' => max(0, (int) ($todo['level'] ?? 0)),\n+\t\t\t\t'status' => (string) ($todo['status'] ?? ''),\n+\t\t\t\t'pri' => $this->formatPriority($todo['pri'] ?? 0),\n+\t\t\t\t'sdate' => (string) ($todo['sdate'] ?? ''),\n+\t\t\t\t'edate' => (string) ($todo['edate'] ?? ''),\n+\t\t\t\t'owner' => (string) ($todo['owner'] ? $accountsObj->id2name((int) $todo['owner']) : ''),\n+\t\t\t\t'assigned_entries' => $assignedEntries\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn $rows;\n+\t}\n+\n+\tprivate function getTodoGrants(): array\n+\t{\n+\t\ttry\n+\t\t{\n+\t\t\t$grants = Acl::getInstance()->get_grants('todo', '.');\n+\t\t\treturn is_array($grants) ? $grants : [];\n+\t\t}\n+\t\tcatch (\\Throwable $e)\n+\t\t{\n+\t\t\treturn [];\n+\t\t}\n+\t}\n+\n+\tprivate function readPayload(Request $request): array\n+\t{\n+\t\t$data = $request->getParsedBody();\n+\t\tif (!is_array($data))\n+\t\t{\n+\t\t\t$raw = (string) $request->getBody();\n+\t\t\tif ($raw !== '')\n+\t\t\t{\n+\t\t\t\t$decoded = json_decode($raw, true);\n+\t\t\t\tif (is_array($decoded))\n+\t\t\t\t{\n+\t\t\t\t\t$data = $decoded;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\n+\t\t$data = is_array($data) ? $data : [];\n+\n+\t\tif (isset($data['title']))\n+\t\t{\n+\t\t\t$data['title'] = $this->sanitizePlainText($data['title']);\n+\t\t}\n+\n+\t\tif (isset($data['descr']))\n+\t\t{\n+\t\t\t$data['descr'] = $this->sanitizePlainText($data['descr']);\n+\t\t}\n+\n+\t\tforeach (['cat', 'parent', 'pri', 'status', 'sday', 'smonth', 'syear', 'eday', 'emonth', 'eyear', 'daysfromstart'] as $intField)\n+\t\t{\n+\t\t\tif (isset($data[$intField]))\n+\t\t\t{\n+\t\t\t\t$data[$intField] = (int) $data[$intField];\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (isset($data['access']))\n+\t\t{\n+\t\t\t$data['access'] = in_array(strtolower((string) $data['access']), ['1', 'true', 'yes', 'on'], true);\n+\t\t}\n+\n+\t\tif (isset($data['assigned']))\n+\t\t{\n+\t\t\t$data['assigned'] = $this->normalizeIdListCsv($data['assigned']);\n+\t\t}\n+\n+\t\tif (isset($data['assigned_group']))\n+\t\t{\n+\t\t\t$data['assigned_group'] = $this->normalizeIdListCsv($data['assigned_group']);\n+\t\t}\n+\n+\t\treturn $data;\n+\t}\n+\n+\tprivate function mapSortKey(string $key): string\n+\t{\n+\t\t$map = [\n+\t\t\t'id' => 'todo_id',\n+\t\t\t'title' => 'todo_title',\n+\t\t\t'status' => 'todo_status',\n+\t\t\t'priority' => 'todo_pri',\n+\t\t\t'created' => 'todo_startdate',\n+\t\t\t'start_date' => 'todo_startdate',\n+\t\t\t'due' => 'todo_enddate',\n+\t\t\t'end_date' => 'todo_enddate',\n+\t\t\t'owner' => 'todo_owner',\n+\t\t];\n+\n+\t\treturn $map[$key] ?? 'todo_id';\n+\t}\n+\n+\t/**\n+\t * GET /todo/todos\n+\t *\n+\t * @OA\\Get(\n+\t *     path=\"/todo/todos\",\n+\t *     summary=\"List todo items\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Parameter(name=\"start\", in=\"query\", @OA\\Schema(type=\"integer\", default=0)),\n+\t *     @OA\\Parameter(name=\"limit\", in=\"query\", @OA\\Schema(type=\"integer\", default=100)),\n+\t *     @OA\\Parameter(name=\"search\", in=\"query\", @OA\\Schema(type=\"string\")),\n+\t *     @OA\\Parameter(name=\"filter\", in=\"query\", @OA\\Schema(type=\"string\", default=\"none\")),\n+\t *     @OA\\Parameter(name=\"cat_id\", in=\"query\", @OA\\Schema(type=\"integer\", default=0)),\n+\t *     @OA\\Parameter(name=\"sort\", in=\"query\", @OA\\Schema(type=\"string\")),\n+\t *     @OA\\Parameter(name=\"dir\", in=\"query\", @OA\\Schema(type=\"string\", enum={\"ASC\", \"DESC\"}, default=\"ASC\")),\n+\t *     @OA\\Response(\n+\t *         response=200,\n+\t *         description=\"Todo list\",\n+\t *         @OA\\JsonContent(\n+\t *             type=\"object\",\n+\t *             @OA\\Property(property=\"total\", type=\"integer\"),\n+\t *             @OA\\Property(property=\"items\", type=\"array\", @OA\\Items(ref=\"#/components/schemas/TodoItem\"))\n+\t *         )\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function index(Request $request, Response $response): Response\n+\t{\n+\t\t$params = $this->getCommonQueryParams($request);\n+\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$grants = $this->getTodoGrants();\n+\t\t$todoList = $botodo->_list(\n+\t\t\t$params['start'],\n+\t\t\t$params['limit'],\n+\t\t\t$params['search'],\n+\t\t\t$params['filter'],\n+\t\t\t$params['sort'],\n+\t\t\t$params['dir'],\n+\t\t\t$params['cat_id'],\n+\t\t\t'all'\n+\t\t);\n+\t\t$items = $this->mapTodoItems(is_array($todoList) ? $todoList : [], $botodo, $grants, (int) $params['cat_id']);\n+\n+\t\tif ((int) ($params['draw'] ?? 0) > 0)\n+\t\t{\n+\t\t\treturn ResponseHelper::sendJSONResponse([\n+\t\t\t\t'draw' => (int) $params['draw'],\n+\t\t\t\t'recordsTotal' => (int) $botodo->total_records,\n+\t\t\t\t'recordsFiltered' => (int) $botodo->total_records,\n+\t\t\t\t'data' => $items,\n+\t\t\t]);\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse([\n+\t\t\t'total' => (int) $botodo->total_records,\n+\t\t\t'items' => $items,\n+\t\t]);\n+\t}\n+\n+\t/**\n+\t * GET /todo/categories\n+\t *\n+\t * @OA\\Get(\n+\t *     path=\"/todo/categories\",\n+\t *     summary=\"List todo categories\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Response(\n+\t *         response=200,\n+\t *         description=\"Category list\",\n+\t *         @OA\\JsonContent(\n+\t *             type=\"object\",\n+\t *             @OA\\Property(property=\"items\", type=\"array\", @OA\\Items(ref=\"#/components/schemas/TodoCategory\"))\n+\t *         )\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function categories(Request $request, Response $response): Response\n+\t{\n+\t\t$cats = \\CreateObject('phpgwapi.categories', -1, 'todo', '.task');\n+\t\t$categories = $cats->return_sorted_array(0, false, '', '', '', true, 0, false);\n+\n+\t\t$list = [\n+\t\t\t['id' => 0, 'name' => lang('All')],\n+\t\t];\n+\n+\t\tforeach ((array) $categories as $category)\n+\t\t{\n+\t\t\t$list[] = [\n+\t\t\t\t'id' => (int) ($category['id'] ?? 0),\n+\t\t\t\t'name' => (string) ($category['name'] ?? ''),\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse(['items' => $list]);\n+\t}\n+\n+\t/**\n+\t * GET /todo/todos/export/csv\n+\t *\n+\t * @OA\\Get(\n+\t *     path=\"/todo/todos/export/csv\",\n+\t *     summary=\"Export todo list as CSV\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Response(response=200, description=\"CSV file\"),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function exportCsv(Request $request, Response $response): Response\n+\t{\n+\t\t$params = $this->getCommonQueryParams($request);\n+\t\t$params['start'] = 0;\n+\t\t$params['limit'] = 2000;\n+\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$grants = $this->getTodoGrants();\n+\t\t$todoList = $botodo->_list(\n+\t\t\t$params['start'],\n+\t\t\t$params['limit'],\n+\t\t\t$params['search'],\n+\t\t\t$params['filter'],\n+\t\t\t$params['sort'],\n+\t\t\t$params['dir'],\n+\t\t\t$params['cat_id'],\n+\t\t\t'all'\n+\t\t);\n+\t\t$items = $this->mapTodoItems(is_array($todoList) ? $todoList : [], $botodo, $grants, (int) $params['cat_id']);\n+\n+\t\t$list = [];\n+\t\tforeach ($items as $item)\n+\t\t{\n+\t\t\t$list[] = [\n+\t\t\t\t'id' => $item['id'],\n+\t\t\t\t'title' => $item['title'],\n+\t\t\t\t'status' => $item['status'],\n+\t\t\t\t'pri' => $item['pri'],\n+\t\t\t\t'sdate' => $item['sdate'],\n+\t\t\t\t'edate' => $item['edate'],\n+\t\t\t\t'owner' => $item['owner'],\n+\t\t\t\t'assigned' => $this->assignedEntriesToText((array) ($item['assigned_entries'] ?? [])),\n+\t\t\t];\n+\t\t}\n+\n+\t\t$boCommon = new BoCommon();\n+\t\t$boCommon->userSettings['preferences']['common']['export_format'] = 'csv';\n+\t\t$boCommon->performDownload(\n+\t\t\t$list,\n+\t\t\t['id', 'title', 'status', 'pri', 'sdate', 'edate', 'owner', 'assigned'],\n+\t\t\t['ID', 'Title', 'Status', 'Urgency', 'Start date', 'End date', 'Created by', 'Assigned to'],\n+\t\t\t['text', 'text', 'text', 'text', 'text', 'text', 'text', 'text'],\n+\t\t\t[],\n+\t\t\t'todo-list.csv'\n+\t\t);\n+\n+\t\treturn $response;\n+\t}\n+\n+\t/**\n+\t * GET /todo/todos/{id}\n+\t *\n+\t * @OA\\Get(\n+\t *     path=\"/todo/todos/{id}\",\n+\t *     summary=\"Get single todo\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Parameter(name=\"id\", in=\"path\", required=true, @OA\\Schema(type=\"integer\")),\n+\t *     @OA\\Response(\n+\t *         response=200,\n+\t *         description=\"Todo item\",\n+\t *         @OA\\JsonContent(\n+\t *             type=\"object\",\n+\t *             @OA\\Property(property=\"item\", type=\"object\"),\n+\t *             @OA\\Property(property=\"detail\", ref=\"#/components/schemas/TodoDetail\"),\n+\t *             @OA\\Property(\n+\t *                 property=\"history\",\n+\t *                 type=\"array\",\n+\t *                 @OA\\Items(\n+\t *                     type=\"object\",\n+\t *                     @OA\\Property(property=\"id\", type=\"integer\"),\n+\t *                     @OA\\Property(property=\"record_id\", type=\"integer\"),\n+\t *                     @OA\\Property(property=\"owner\", type=\"string\"),\n+\t *                     @OA\\Property(property=\"status\", type=\"string\"),\n+\t *                     @OA\\Property(property=\"status_label\", type=\"string\"),\n+\t *                     @OA\\Property(property=\"new_value\", type=\"string\"),\n+\t *                     @OA\\Property(property=\"old_value\", type=\"string\"),\n+\t *                     @OA\\Property(property=\"datetime\", type=\"string\")\n+\t *                 )\n+\t *             )\n+\t *         )\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=404,\n+\t *         description=\"Todo not found\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function show(Request $request, Response $response, array $args): Response\n+\t{\n+\t\t$id = (int) ($args['id'] ?? 0);\n+\t\tif (!$id) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t}\n+\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$item = $botodo->read($id);\n+\n+\t\tif (!$item) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Todo not found'], 404);\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse([\n+\t\t\t'item' => $item,\n+\t\t\t'detail' => $this->mapTodoDetail((array) $item, $botodo),\n+\t\t\t'history' => $this->getTodoHistoryData($id),\n+\t\t]);\n+\t}\n+\n+\t/**\n+\t * POST /todo/todos\n+\t *\n+\t * @OA\\Post(\n+\t *     path=\"/todo/todos\",\n+\t *     summary=\"Create todo\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\RequestBody(\n+\t *         required=true,\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoUpsertRequest\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=201,\n+\t *         description=\"Created\",\n+\t *         @OA\\JsonContent(type=\"object\", @OA\\Property(property=\"id\", type=\"integer\"))\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=400,\n+\t *         description=\"Validation error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function store(Request $request, Response $response): Response\n+\t{\n+\t\t$query = $request->getQueryParams();\n+\t\t$parsedBody = $request->getParsedBody();\n+\t\t$parsedBody = is_array($parsedBody) ? $parsedBody : [];\n+\n+\t\t// DataTables server-side requests use POST to this endpoint in datatable2.\n+\t\t// Route those requests to the list handler instead of create.\n+\t\tif (\n+\t\t\tisset($parsedBody['draw'])\n+\t\t\t|| isset($parsedBody['columns'])\n+\t\t\t|| isset($parsedBody['order'])\n+\t\t\t|| isset($query['draw'])\n+\t\t\t|| isset($query['columns'])\n+\t\t\t|| isset($query['order'])\n+\t\t)\n+\t\t{\n+\t\t\treturn $this->index($request, $response);\n+\t\t}\n+\n+\t\t$values = $this->readPayload($request);\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\n+\t\t$error = $botodo->check_values($values);\n+\t\tif (is_array($error) && count($error)) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => implode('; ', $error)], 400);\n+\t\t}\n+\n+\t\t$newId = $botodo->save($values);\n+\t\tif (!$newId) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Failed to create todo'], 500);\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse(['id' => (int) $newId], 201);\n+\t}\n+\n+\t/**\n+\t * PUT /todo/todos/{id}\n+\t *\n+\t * @OA\\Put(\n+\t *     path=\"/todo/todos/{id}\",\n+\t *     summary=\"Update todo\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Parameter(name=\"id\", in=\"path\", required=true, @OA\\Schema(type=\"integer\")),\n+\t *     @OA\\RequestBody(\n+\t *         required=true,\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoUpsertRequest\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=200,\n+\t *         description=\"Updated\",\n+\t *         @OA\\JsonContent(type=\"object\", @OA\\Property(property=\"id\", type=\"integer\"))\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=400,\n+\t *         description=\"Invalid request\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=404,\n+\t *         description=\"Todo not found\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function update(Request $request, Response $response, array $args): Response\n+\t{\n+\t\t$id = (int) ($args['id'] ?? 0);\n+\t\tif (!$id) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t}\n+\n+\t\t$values = $this->readPayload($request);\n+\t\t$values['id'] = $id;\n+\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$parentId = (int) ($values['parent'] ?? 0);\n+\t\tif ($this->isCircularParentAssignment($botodo, $id, $parentId))\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Invalid parent selection: circular references are not allowed'], 400);\n+\t\t}\n+\n+\t\t$error = $botodo->check_values($values);\n+\t\tif (is_array($error) && count($error)) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => implode('; ', $error)], 400);\n+\t\t}\n+\n+\t\t$ok = $botodo->save($values, 'edit');\n+\t\tif (!$ok) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Failed to update todo'], 500);\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse(['id' => $id]);\n+\t}\n+\n+\t/**\n+\t * PATCH /todo/todos/{id}/status\n+\t *\n+\t * @OA\\Patch(\n+\t *     path=\"/todo/todos/{id}/status\",\n+\t *     summary=\"Update todo completion percentage\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Parameter(name=\"id\", in=\"path\", required=true, @OA\\Schema(type=\"integer\")),\n+\t *     @OA\\RequestBody(\n+\t *         required=true,\n+\t *         @OA\\JsonContent(\n+\t *             required={\"status\"},\n+\t *             @OA\\Property(property=\"status\", type=\"integer\", minimum=0, maximum=100)\n+\t *         )\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=200,\n+\t *         description=\"Status updated\",\n+\t *         @OA\\JsonContent(\n+\t *             type=\"object\",\n+\t *             @OA\\Property(property=\"id\", type=\"integer\"),\n+\t *             @OA\\Property(property=\"status\", type=\"integer\", minimum=0, maximum=100)\n+\t *         )\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=400,\n+\t *         description=\"Invalid status value\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=404,\n+\t *         description=\"Todo not found\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function updateStatus(Request $request, Response $response, array $args): Response\n+\t{\n+\t\t$id = (int) ($args['id'] ?? 0);\n+\t\tif (!$id)\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t}\n+\n+\t\t$payload = $this->readPayload($request);\n+\t\tif (!isset($payload['status']))\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing status value'], 400);\n+\t\t}\n+\n+\t\t$status = (int) $payload['status'];\n+\t\tif ($status < 0 || $status > 100)\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Status must be between 0 and 100'], 400);\n+\t\t}\n+\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$current = $botodo->read($id);\n+\t\tif (!$current)\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Todo not found'], 404);\n+\t\t}\n+\n+\t\t$values = [\n+\t\t\t'id' => $id,\n+\t\t\t'title' => (string) ($current['title'] ?? ''),\n+\t\t\t'descr' => (string) ($current['descr'] ?? ''),\n+\t\t\t'cat' => (int) ($current['cat'] ?? 0),\n+\t\t\t'parent' => (int) ($current['parent'] ?? 0),\n+\t\t\t'pri' => (int) ($current['pri'] ?? 2),\n+\t\t\t'status' => $status,\n+\t\t\t'access' => ((string) ($current['access'] ?? '') === 'private'),\n+\t\t\t'assigned' => (string) ($current['assigned'] ?? ''),\n+\t\t\t'assigned_group' => (string) ($current['assigned_group'] ?? ''),\n+\t\t\t'sdate' => (int) ($current['sdate'] ?? 0),\n+\t\t\t'edate' => (int) ($current['edate'] ?? 0),\n+\t\t];\n+\n+\t\t$error = $botodo->check_values($values);\n+\t\tif (is_array($error) && count($error))\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => implode('; ', $error)], 400);\n+\t\t}\n+\n+\t\t$ok = $botodo->save($values, 'edit');\n+\t\tif (!$ok)\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Failed to update todo status'], 500);\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse([\n+\t\t\t'id' => $id,\n+\t\t\t'status' => $status,\n+\t\t]);\n+\t}\n+\n+\t/**\n+\t * DELETE /todo/todos/{id}\n+\t *\n+\t * @OA\\Delete(\n+\t *     path=\"/todo/todos/{id}\",\n+\t *     summary=\"Delete todo\",\n+\t *     tags={\"Todo\"},\n+\t *     @OA\\Parameter(name=\"id\", in=\"path\", required=true, @OA\\Schema(type=\"integer\")),\n+\t *     @OA\\Parameter(name=\"subs\", in=\"query\", required=false, @OA\\Schema(type=\"boolean\", default=false)),\n+\t *     @OA\\Response(\n+\t *         response=200,\n+\t *         description=\"Delete result\",\n+\t *         @OA\\JsonContent(\n+\t *             type=\"object\",\n+\t *             @OA\\Property(property=\"deleted\", type=\"boolean\"),\n+\t *             @OA\\Property(property=\"subs\", type=\"boolean\")\n+\t *         )\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=403,\n+\t *         description=\"Delete denied\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=404,\n+\t *         description=\"Todo not found\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=401,\n+\t *         description=\"Unauthorized\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     ),\n+\t *     @OA\\Response(\n+\t *         response=500,\n+\t *         description=\"Internal server error\",\n+\t *         @OA\\JsonContent(ref=\"#/components/schemas/TodoErrorResponse\")\n+\t *     )\n+\t * )\n+\t */\n+\tpublic function destroy(Request $request, Response $response, array $args): Response\n+\t{\n+\t\t$id = (int) ($args['id'] ?? 0);\n+\t\tif (!$id) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t}\n+\n+\t\t$query = $request->getQueryParams();\n+\t\t$subsRaw = $query['subs'] ?? '0';\n+\t\t$deleteSubs = in_array((string) $subsRaw, ['1', 'true', 'on', 'yes'], true);\n+\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$deleteResult = $botodo->delete($id, $deleteSubs);\n+\n+\t\tif (is_array($deleteResult) && empty($deleteResult['ok']))\n+\t\t{\n+\t\t\t$reason = (string) ($deleteResult['reason'] ?? 'delete_denied');\n+\t\t\t$message = (string) ($deleteResult['message'] ?? lang('Delete denied'));\n+\n+\t\t\t$statusCode = 403;\n+\t\t\tif ($reason === 'not_found' || $reason === 'parent_not_found')\n+\t\t\t{\n+\t\t\t\t$statusCode = 404;\n+\t\t\t}\n+\n+\t\t\treturn ResponseHelper::sendErrorResponse([\n+\t\t\t\t'error' => $message,\n+\t\t\t\t'reason' => $reason,\n+\t\t\t\t'todo_id' => (int) ($deleteResult['todo_id'] ?? $id),\n+\t\t\t\t'parent_id' => (int) ($deleteResult['parent_id'] ?? 0),\n+\t\t\t], $statusCode);\n+\t\t}\n+\n+\t\tif ($deleteResult === false)\n+\t\t{\n+\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Failed to delete todo'], 500);\n+\t\t}\n+\n+\t\treturn ResponseHelper::sendJSONResponse([\n+\t\t\t'deleted' => true,\n+\t\t\t'subs' => $deleteSubs,\n+\t\t\t'result' => is_array($deleteResult) ? $deleteResult : null,\n+\t\t]);\n+\t}\n+}"},{"sha":"8fc29df2d51923fa91a3dacfcac42642a958286a","filename":"src/modules/todo/doc/TODO_MODULE_CLIENT_REST_CONVERSION_GUIDE.md","status":"added","additions":477,"deletions":0,"changes":477,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fdoc%2FTODO_MODULE_CLIENT_REST_CONVERSION_GUIDE.md","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fdoc%2FTODO_MODULE_CLIENT_REST_CONVERSION_GUIDE.md","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fdoc%2FTODO_MODULE_CLIENT_REST_CONVERSION_GUIDE.md?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,477 @@\n+# Todo Module Client/REST Conversion Guide\n+\n+## 1. Purpose and Scope\n+\n+This document explains how the `todo` module works today, how it was converted from legacy page-centric behavior to a client + REST API architecture, and how to reuse the same approach as a prototype for converting other modules.\n+\n+Primary goals:\n+\n+- Preserve existing business behavior and access rules.\n+- Move data operations to JSON endpoints.\n+- Keep navigation and page routing stable for end users.\n+- Render UI on the client with explicit, documented payload contracts.\n+- Add security hardening (input normalization, CSRF protection for mutating calls).\n+\n+This guide is written as a practical migration playbook for future agent sessions.\n+\n+---\n+\n+## 2. Module Structure\n+\n+Top-level `todo` module layout:\n+\n+- `routes/Routes.php`: route registration and route middleware.\n+- `controllers/TodoController.php`: REST API endpoints and payload mapping.\n+- `viewcontrollers/TodoViewController.php`: page shell endpoints (Twig + legacy wrapper).\n+- `inc/class.botodo.inc.php`: business layer wrapper over storage layer.\n+- `inc/class.sotodo.inc.php`: storage/database layer.\n+- `html/todo/**`: Twig templates and client-side JS/CSS.\n+- `setup/phpgw_*.lang`: localization keys.\n+\n+Important split:\n+\n+- View controllers return HTML shells/pages.\n+- API controller returns JSON payloads.\n+- Client JS calls API and renders dynamic content.\n+\n+---\n+\n+## 3. Route Model (Legacy Navigation + REST Data)\n+\n+The module intentionally keeps classic view navigation while separating data operations into REST endpoints.\n+\n+### 3.1 View routes\n+\n+Prefix: `/todo/view`\n+\n+Examples:\n+\n+- `/todo/view/todos`\n+- `/todo/view/todos/add`\n+- `/todo/view/todos/{id}`\n+- `/todo/view/todos/{id}/edit`\n+- `/todo/view/todos/{id}/delete`\n+- `/todo/view/todos/matrix`\n+\n+These return HTML pages (Twig components wrapped in legacy layout rendering).\n+\n+### 3.2 API routes\n+\n+Prefix: `/todo`\n+\n+Examples:\n+\n+- `GET /todo/todos`\n+- `GET /todo/todos/{id}`\n+- `POST /todo/todos`\n+- `PUT /todo/todos/{id}`\n+- `PATCH /todo/todos/{id}/status`\n+- `DELETE /todo/todos/{id}`\n+- `GET /todo/todos/export/csv`\n+- `GET /todo/categories`\n+\n+These return JSON (except CSV export).\n+\n+### 3.3 Route middleware\n+\n+Module route group uses:\n+\n+- session middleware\n+- access verification middleware\n+- CSRF middleware for mutating endpoints and related view pages\n+\n+CSRF behavior details are in Section 8.\n+\n+---\n+\n+## 4. Layered Architecture\n+\n+## 4.1 Storage layer (`class.sotodo.inc.php`)\n+\n+Responsibilities:\n+\n+- SQL read/write operations.\n+- ACL-aware filtering in listing queries.\n+- Recursive tree/sub-task operations.\n+- History writes for changes.\n+\n+Current behavior notes:\n+\n+- Writes (`add_todo`, `edit_todo`) use prepared statements.\n+- List query has legacy dynamic SQL construction but was hardened with integer enforcement for key list fragments.\n+- Group-preserving ordering for hierarchy sorting was implemented.\n+\n+## 4.2 Business layer (`class.botodo.inc.php`)\n+\n+Responsibilities:\n+\n+- Session-bound list behavior and defaults.\n+- Date formatting to user preference for list output.\n+- Assigned/user group transformations.\n+- Permission helper (`check_perms`).\n+- Validation (`check_values`) and save orchestration.\n+\n+## 4.3 API controller (`TodoController.php`)\n+\n+Responsibilities:\n+\n+- Parse and normalize incoming API payloads.\n+- Call business layer operations.\n+- Map legacy structures to stable JSON contract.\n+- Return consistent API responses and errors.\n+- OpenAPI annotations.\n+\n+Important mapping helpers:\n+\n+- `mapTodoItems`\n+- `mapTodoDetail`\n+- `mapAssignedEntries`\n+- `getTodoHistoryData`\n+\n+## 4.4 View controller (`TodoViewController.php`)\n+\n+Responsibilities:\n+\n+- Return Twig page shells.\n+- Provide page bootstrapping data (URLs, language strings, defaults).\n+- Build matrix rendering input and wrapper.\n+- Provide CSRF token metadata to Twig pages.\n+\n+## 4.5 Client layer (`html/todo/**`)\n+\n+Responsibilities:\n+\n+- Fetch data from REST API.\n+- Render dynamic UI content.\n+- Submit JSON for create/update/delete/status operations.\n+- Include CSRF tokens in mutating requests.\n+\n+---\n+\n+## 5. Data Contract Strategy\n+\n+Key migration principle:\n+\n+- Avoid returning server-rendered HTML blobs for dynamic tables/details when possible.\n+- Return structured arrays/objects and render in client JS.\n+\n+### 5.1 Example: Assigned entities\n+\n+Old style:\n+\n+- String field with line breaks or HTML-style formatting.\n+\n+New style:\n+\n+- `assigned_entries: [{ id, type, name }]`\n+\n+Benefits:\n+\n+- Stronger client control over rendering.\n+- Easier localization and formatting changes.\n+- Cleaner API schema and tests.\n+\n+### 5.2 Example: History\n+\n+Old style:\n+\n+- Server-rendered history HTML table.\n+\n+New style:\n+\n+- `history` array with normalized fields (`status_label`, `new_value`, `old_value`, `datetime`).\n+- Client renders table.\n+\n+Datetime behavior:\n+\n+- Unix timestamps are formatted server-side according to user date format plus `H:i`.\n+\n+---\n+\n+## 6. Frontend Rendering Patterns\n+\n+## 6.1 Main list (`todo_datatable.twig`)\n+\n+Patterns:\n+\n+- formatter function per column (`renderIdLink`, `todoFormatTitle`, `renderAssigned`).\n+- ID rendered as direct view link.\n+- `assigned_entries` rendered as escaped multiline text.\n+\n+## 6.2 View page (`todo_view.js`)\n+\n+Patterns:\n+\n+- Fetch detail + history by ID.\n+- Tabbed details/history rendering.\n+- History table generated from structured API array.\n+- Assigned entities rendered from `assigned_entries` only.\n+\n+## 6.3 Add/edit pages (`todo_add.js`, `todo_edit.js`)\n+\n+Patterns:\n+\n+- Build JSON payload from form.\n+- Date parsing from user format.\n+- Multi-select arrays for assigned users/groups.\n+- CSRF headers and payload token fields sent on submit.\n+\n+## 6.4 Delete page (`todo_delete.js`)\n+\n+Patterns:\n+\n+- Fetch detail for confirmation.\n+- Call DELETE API with optional `subs=1`.\n+- Map backend reason codes to user-friendly messages.\n+- Send CSRF headers.\n+\n+## 6.5 Matrix page (`todo_matrix.js`)\n+\n+Patterns:\n+\n+- Inline status edit via PATCH.\n+- Integer-only status validation (0..100).\n+- Auto-submit month/year filters on change.\n+- Hidden CSRF fields injected into matrix filter form.\n+\n+---\n+\n+## 7. OpenAPI / Swagger Approach\n+\n+`TodoController.php` includes `@OA` annotations for:\n+\n+- tag\n+- reusable schemas (`TodoItem`, `TodoDetail`, `TodoCategory`, `TodoUpsertRequest`, `TodoErrorResponse`)\n+- endpoint responses including common error response refs\n+\n+Guidance for other modules:\n+\n+1. Define schema objects first.\n+2. Annotate every endpoint with success + common error responses.\n+3. Keep response contract aligned with actual payloads (do not document removed fields).\n+\n+---\n+\n+## 8. CSRF Design (Slim)\n+\n+The module uses `slim/csrf` (v1.5.1) and applies it at route level in `routes/Routes.php`.\n+\n+### 8.1 Why route-level middleware\n+\n+- Scope is explicit for module conversion.\n+- Easy to prototype before global rollout.\n+- Allows special-case handling for known legacy behaviors.\n+\n+### 8.2 Mutating endpoint protection\n+\n+CSRF is enforced for:\n+\n+- `POST /todo/todos`\n+- `PUT /todo/todos/{id}`\n+- `PATCH /todo/todos/{id}/status`\n+- `DELETE /todo/todos/{id}`\n+\n+### 8.3 Token transport and compatibility\n+\n+To handle browser/proxy differences and JSON parsing timing:\n+\n+- Tokens are provided to view pages from request attributes (`csrf_name`, `csrf_value`).\n+- JS sends tokens in:\n+  - headers: `csrf_name`, `csrf_value`\n+  - fallback headers: `X-CSRF-NAME`, `X-CSRF-VALUE`\n+  - JSON payload fields for mutating requests\n+- Route middleware pre-parses JSON body for mutating methods before CSRF validation.\n+\n+### 8.4 DataTables exception\n+\n+`POST /todo/todos` is also used by DataTables server-side listing.\n+\n+A bypass is implemented for recognized list payloads (`draw/columns/order`) to avoid breaking read-only list requests.\n+\n+---\n+\n+## 9. Input Security and Normalization\n+\n+## 9.1 Implemented hardening in API controller\n+\n+`readPayload` now normalizes:\n+\n+- `title`, `descr`: plain-text sanitization (HTML stripped, trimmed).\n+- numeric fields: cast to integers.\n+- `access`: strict boolean conversion.\n+- `assigned`, `assigned_group`: positive integer ID list CSV normalization.\n+\n+## 9.2 Storage layer hardening\n+\n+In list query construction:\n+\n+- account/group/public user IDs are forced to integer lists before SQL fragment composition.\n+- dynamic `IN (...)` fragments are built from sanitized int arrays.\n+\n+## 9.3 Remaining security considerations\n+\n+- Legacy query-building in list reads still exists (not fully rewritten to prepared dynamic builders).\n+- CSRF is currently module-scoped; global strategy may be desired later.\n+\n+---\n+\n+## 10. Hierarchy and Sorting Behavior\n+\n+Important module-specific behavior implemented as prototype logic:\n+\n+- When sorting, children remain grouped under their top parent.\n+- Ordering is based on top/root parent sort key and stable subtree order.\n+- Matrix includes all hierarchy levels, not only shallow levels.\n+\n+This is critical for migrating other tree-structured modules.\n+\n+---\n+\n+## 11. Migration Blueprint for Other Modules\n+\n+Use this staged approach.\n+\n+### Stage A: Discover and freeze behavior\n+\n+- Identify existing UI routes and user flows.\n+- Inventory business rules and ACL checks.\n+- Document output contract currently expected by UI.\n+\n+### Stage B: Add REST controller alongside legacy view controller\n+\n+- Keep old navigation URLs for pages.\n+- Build JSON endpoints in parallel.\n+- Map legacy model data to stable contract objects.\n+\n+### Stage C: Move rendering to client incrementally\n+\n+- Start with list page read-only render.\n+- Convert detail page second.\n+- Convert add/edit/delete operations to JSON.\n+- Keep old route wrappers until parity is validated.\n+\n+### Stage D: Security hardening\n+\n+- Add CSRF middleware for mutating routes.\n+- Normalize payloads in one place (`readPayload`-style helper).\n+- Ensure write SQL uses prepared statements.\n+- Add explicit integer allowlists for dynamic SQL fragments.\n+\n+### Stage E: API documentation\n+\n+- Add OpenAPI schemas and endpoint annotations.\n+- Reuse common error schema.\n+- Validate docs against real payloads.\n+\n+### Stage F: Cleanup\n+\n+- Remove server-rendered fallback fields once client uses structured data.\n+- Remove dead helper methods after reference scan.\n+- Keep CSV/export adapters where plain-text flattening is needed.\n+\n+---\n+\n+## 12. Recommended File Pattern for Future Conversions\n+\n+Per module, mirror this structure:\n+\n+- `routes/Routes.php`\n+- `controllers/<Module>Controller.php` (REST)\n+- `viewcontrollers/<Module>ViewController.php` (page shell)\n+- `html/<module>/{index,view,add,edit,delete}/...`\n+- `inc/class.bo<module>.inc.php`\n+- `inc/class.so<module>.inc.php`\n+- `doc/<MODULE>_CLIENT_REST_CONVERSION_GUIDE.md`\n+\n+---\n+\n+## 13. Testing and Verification Checklist\n+\n+Before merge:\n+\n+1. Routing\n+- All existing view URLs still load.\n+- API endpoints reachable and ACL-protected.\n+\n+2. Behavior parity\n+- List/filter/sort/category behavior matches legacy expectations.\n+- Parent/child tree integrity preserved.\n+- Delete semantics (with and without sub-items) unchanged.\n+\n+3. Security\n+- CSRF failures return 400 for mutating calls without token.\n+- Valid token path works for POST/PUT/PATCH/DELETE.\n+- Input normalization is applied consistently.\n+\n+4. Contract\n+- Frontend only consumes documented fields.\n+- Removed fields are not referenced by client code.\n+\n+5. Validation\n+- `php -l` passes for modified PHP files.\n+- No diagnostics errors in modified JS/Twig files.\n+\n+---\n+\n+## 14. Operational Notes and Gotchas\n+\n+- DataTables can use POST for list reads. If CSRF is strict on all POST blindly, list may break.\n+- Some environments may drop underscore header names; keep fallback header support if needed.\n+- If mutating API uses JSON body, CSRF middleware may require explicit parsed-body handling depending on middleware order.\n+- Keep user date format handling centralized to avoid mismatch between API and UI.\n+\n+---\n+\n+## 15. Known Decisions in This Prototype\n+\n+- Legacy navigation preserved under `/todo/view/*`.\n+- Data moved to REST under `/todo/*`.\n+- Structured arrays preferred over pre-rendered HTML in API payloads.\n+- CSRF implemented route-locally for Todo first.\n+- Assigned string in API replaced by `assigned_entries` (CSV export still flattens to string for file output).\n+\n+---\n+\n+## 16. Suggested Next Improvements (Optional)\n+\n+1. Extract reusable CSRF middleware helper into shared module utilities for reuse.\n+2. Move more dynamic list SQL fragments to prepared-query builder patterns.\n+3. Add integration tests for mutating endpoints with and without CSRF token.\n+4. Add contract tests for JSON payload shape (`TodoItem`, `TodoDetail`, `history`).\n+5. Standardize a shared payload normalizer trait for all converted modules.\n+\n+---\n+\n+## 17. Quick Reference: Key Todo Files\n+\n+- Routing and middleware: `src/modules/todo/routes/Routes.php`\n+- API controller: `src/modules/todo/controllers/TodoController.php`\n+- View controller: `src/modules/todo/viewcontrollers/TodoViewController.php`\n+- Business layer: `src/modules/todo/inc/class.botodo.inc.php`\n+- Storage layer: `src/modules/todo/inc/class.sotodo.inc.php`\n+- Main list page: `src/modules/todo/html/todo/index/todo_datatable.twig`\n+- Detail page client: `src/modules/todo/html/todo/view/todo_view.js`\n+- Add/edit clients: `src/modules/todo/html/todo/add/todo_add.js`, `src/modules/todo/html/todo/edit/todo_edit.js`\n+- Delete client: `src/modules/todo/html/todo/delete/todo_delete.js`\n+- Matrix client: `src/modules/todo/html/todo/matrix/todo_matrix.js`\n+\n+---\n+\n+## 18. Session Handoff Notes Template\n+\n+Use this template in future agent sessions:\n+\n+- Scope converted:\n+  - [module + endpoints + pages]\n+- Legacy behavior preserved:\n+  - [list of invariants]\n+- New REST contract:\n+  - [schemas and removed fields]\n+- Security status:\n+  - [CSRF strategy, input normalization, remaining gaps]\n+- Validation status:\n+  - [lint/tests/manual checks]\n+- Follow-up tasks:\n+  - [ordered TODO list]\n+\n+This keeps cross-session continuation deterministic and reduces re-discovery cost."},{"sha":"5f1aba07a582dab47f39ee14fecfa0ffccc2fe67","filename":"src/modules/todo/helpers/RedirectHelper.php","status":"modified","additions":1,"deletions":5,"changes":6,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhelpers%2FRedirectHelper.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhelpers%2FRedirectHelper.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhelpers%2FRedirectHelper.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -6,10 +6,6 @@ class RedirectHelper\n {\n \tpublic function process()\n \t{\n-\t\t$parms = array(\n-\t\t\t'menuaction' => 'todo.uitodo.show_list'\n-\t\t);\n-\n-\t\t\\phpgw::redirect_link('/', $parms);\n+\t\t\\phpgw::redirect_link('/todo/view/todos');\n \t}\n }"},{"sha":"a210746aa8899eac010a0b3406b847aff5c418bd","filename":"src/modules/todo/html/_bare.twig","status":"added","additions":5,"deletions":0,"changes":5,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2F_bare.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2F_bare.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2F_bare.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,5 @@\n+{% block styles %}{% endblock %}\n+{{ render_styles() }}\n+{% block content %}{% endblock %}\n+{{ render_scripts() }}\n+{% block scripts %}{% endblock %}"},{"sha":"a0f9968a2dea0ecf1aeca0657a12b83a26fa3f14","filename":"src/modules/todo/html/todo/add/todo_add.css","status":"added","additions":84,"deletions":0,"changes":84,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.css","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.css","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.css?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,84 @@\n+.todo-add {\n+\tpadding: 1rem;\n+\tmax-width: 980px;\n+}\n+\n+.todo-add__title {\n+\tmargin: 0 0 1rem;\n+}\n+\n+.todo-add__error {\n+\tmargin-bottom: 1rem;\n+\tpadding: 0.75rem;\n+\tborder: 1px solid #d38e8e;\n+\tbackground: #fdeaea;\n+\tcolor: #7b1f1f;\n+}\n+\n+.todo-add__grid {\n+\tdisplay: grid;\n+\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n+\tgap: 1rem;\n+}\n+\n+.todo-add__field {\n+\tdisplay: flex;\n+\tflex-direction: column;\n+\tgap: 0.4rem;\n+}\n+\n+.todo-add__field--full {\n+\tgrid-column: 1 / -1;\n+}\n+\n+.todo-add__input,\n+.todo-add__select,\n+.todo-add__textarea {\n+\tpadding: 0.5rem;\n+\tborder: 1px solid #c6c6c6;\n+\tborder-radius: 4px;\n+}\n+\n+.todo-add__textarea {\n+\tmin-height: 130px;\n+\tresize: vertical;\n+}\n+\n+.todo-add__checkline {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tgap: 0.4rem;\n+}\n+\n+.todo-add__actions {\n+\tmargin-top: 1rem;\n+\tdisplay: flex;\n+\tgap: 0.75rem;\n+}\n+\n+.todo-add__button {\n+\tpadding: 0.5rem 0.9rem;\n+\tborder: 1px solid #20568f;\n+\tborder-radius: 4px;\n+\tbackground: #2a72ba;\n+\tcolor: #fff;\n+\tcursor: pointer;\n+}\n+\n+.todo-add__button--secondary {\n+\tbackground: #fff;\n+\tcolor: #20568f;\n+\ttext-decoration: none;\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+}\n+\n+.todo-add__select2-option {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tgap: 0.45rem;\n+}\n+\n+.todo-add__select2-checkbox {\n+\tpointer-events: none;\n+}\n\\ No newline at end of file"},{"sha":"6e468c5c3a4586424f0f3ba4bde030743c1e2520","filename":"src/modules/todo/html/todo/add/todo_add.js","status":"added","additions":209,"deletions":0,"changes":209,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.js","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.js","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.js?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,209 @@\n+(function () {\n+\tvar root = document.getElementById('todo-add-app');\n+\tif (!root) {\n+\t\treturn;\n+\t}\n+\n+\tvar form = document.getElementById('todo-add-form');\n+\tvar saveBtn = document.getElementById('todo-save');\n+\tvar errorEl = document.getElementById('todo-add-error');\n+\tvar apiUrl = root.dataset.apiUrl;\n+\tvar backUrl = root.dataset.backUrl;\n+\tvar csrfNameKey = root.dataset.csrfNameKey || '';\n+\tvar csrfValueKey = root.dataset.csrfValueKey || '';\n+\tvar csrfName = root.dataset.csrfName || '';\n+\tvar csrfValue = root.dataset.csrfValue || '';\n+\tvar phpDateFormat = root.dataset.dateFormat || 'Y-m-d';\n+\n+\tfunction toJqueryDateFormat(phpFormat) {\n+\t\treturn String(phpFormat || 'Y-m-d')\n+\t\t\t.replace(/Y/g, 'yy')\n+\t\t\t.replace(/y/g, 'y')\n+\t\t\t.replace(/m/g, 'mm')\n+\t\t\t.replace(/n/g, 'm')\n+\t\t\t.replace(/d/g, 'dd')\n+\t\t\t.replace(/j/g, 'd');\n+\t}\n+\n+\tvar jqueryDateFormat = toJqueryDateFormat(phpDateFormat);\n+\n+\tfunction initSelect2Multi(selector) {\n+\t\tif (typeof window.jQuery === 'undefined' || typeof window.jQuery.fn.select2 === 'undefined') {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar $el = window.jQuery(selector);\n+\t\tif (!$el.length) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\t$el.select2({\n+\t\t\twidth: '100%',\n+\t\t\tcloseOnSelect: false,\n+\t\t\tminimumResultsForSearch: 0,\n+\t\t\tplaceholder: $el.data('placeholder') || '',\n+\t\t\ttemplateResult: function (data) {\n+\t\t\t\tif (!data.id) {\n+\t\t\t\t\treturn data.text;\n+\t\t\t\t}\n+\n+\t\t\t\tvar selected = false;\n+\t\t\t\tif (data.element) {\n+\t\t\t\t\tselected = data.element.selected;\n+\t\t\t\t}\n+\n+\t\t\t\tvar span = document.createElement('span');\n+\t\t\t\tspan.className = 'todo-add__select2-option';\n+\n+\t\t\t\tvar checkbox = document.createElement('input');\n+\t\t\t\tcheckbox.type = 'checkbox';\n+\t\t\t\tcheckbox.className = 'todo-add__select2-checkbox';\n+\t\t\t\tcheckbox.checked = selected;\n+\t\t\t\tcheckbox.tabIndex = -1;\n+\n+\t\t\t\tvar label = document.createElement('span');\n+\t\t\t\tlabel.textContent = data.text;\n+\n+\t\t\t\tspan.appendChild(checkbox);\n+\t\t\t\tspan.appendChild(label);\n+\t\t\t\treturn span;\n+\t\t\t}\n+\t\t});\n+\n+\t}\n+\n+\tfunction showError(message) {\n+\t\terrorEl.hidden = false;\n+\t\terrorEl.textContent = message || root.dataset.langError || 'Error';\n+\t}\n+\n+\tfunction hideError() {\n+\t\terrorEl.hidden = true;\n+\t\terrorEl.textContent = '';\n+\t}\n+\n+\tfunction splitDate(value) {\n+\t\tif (!value) {\n+\t\t\treturn { year: '', month: '', day: '' };\n+\t\t}\n+\n+\t\tif (typeof window.jQuery !== 'undefined' && window.jQuery.datepicker) {\n+\t\t\ttry {\n+\t\t\t\tvar parsed = window.jQuery.datepicker.parseDate(jqueryDateFormat, value);\n+\t\t\t\treturn {\n+\t\t\t\t\tyear: parsed.getFullYear(),\n+\t\t\t\t\tmonth: parsed.getMonth() + 1,\n+\t\t\t\t\tday: parsed.getDate()\n+\t\t\t\t};\n+\t\t\t} catch (err) {\n+\t\t\t\t// Fall through to ISO fallback for robustness.\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (value.indexOf('-') === -1) {\n+\t\t\treturn { year: '', month: '', day: '' };\n+\t\t}\n+\n+\t\tvar parts = value.split('-');\n+\t\tif (parts.length !== 3) {\n+\t\t\treturn { year: '', month: '', day: '' };\n+\t\t}\n+\n+\t\treturn {\n+\t\t\tyear: parseInt(parts[0], 10) || '',\n+\t\t\tmonth: parseInt(parts[1], 10) || '',\n+\t\t\tday: parseInt(parts[2], 10) || ''\n+\t\t};\n+\t}\n+\n+\tfunction getMultiValues(el) {\n+\t\treturn Array.prototype.slice.call(el.selectedOptions || []).map(function (option) {\n+\t\t\treturn option.value;\n+\t\t});\n+\t}\n+\n+\tfunction buildPayload() {\n+\t\tvar sDate = splitDate(form.elements.sdate.value);\n+\t\tvar eDate = splitDate(form.elements.edate.value);\n+\t\tvar payload = {\n+\t\t\ttitle: form.elements.title.value || '',\n+\t\t\tdescr: form.elements.descr.value || '',\n+\t\t\tcat: parseInt(form.elements.cat.value || '0', 10) || 0,\n+\t\t\tparent: parseInt(form.elements.parent.value || '0', 10) || 0,\n+\t\t\tpri: parseInt(form.elements.pri.value || '2', 10) || 2,\n+\t\t\tstatus: parseInt(form.elements.status.value || '0', 10) || 0,\n+\t\t\taccess: form.elements.access.checked,\n+\t\t\tassigned: getMultiValues(form.elements.assigned),\n+\t\t\tassigned_group: getMultiValues(form.elements.assigned_group),\n+\t\t\tsyear: sDate.year,\n+\t\t\tsmonth: sDate.month,\n+\t\t\tsday: sDate.day,\n+\t\t\teyear: eDate.year,\n+\t\t\temonth: eDate.month,\n+\t\t\teday: eDate.day\n+\t\t};\n+\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\tpayload[csrfNameKey] = csrfName;\n+\t\t\tpayload[csrfValueKey] = csrfValue;\n+\t\t}\n+\n+\t\treturn payload;\n+\t}\n+\n+\tinitSelect2Multi('#todo-assigned');\n+\tinitSelect2Multi('#todo-assigned-group');\n+\n+\tif (typeof window.jQuery !== 'undefined' && window.jQuery.fn.datepicker) {\n+\t\twindow.jQuery('#todo-sdate, #todo-edate').datepicker({\n+\t\t\tdateFormat: jqueryDateFormat,\n+\t\t\tchangeMonth: true,\n+\t\t\tchangeYear: true\n+\t\t});\n+\t}\n+\n+\tform.addEventListener('submit', function (event) {\n+\t\tevent.preventDefault();\n+\t\thideError();\n+\n+\t\tvar oldText = saveBtn.textContent;\n+\t\tsaveBtn.disabled = true;\n+\t\tsaveBtn.textContent = root.dataset.langSaving || oldText;\n+\n+\t\tvar headers = {\n+\t\t\t'Content-Type': 'application/json',\n+\t\t\t'Accept': 'application/json'\n+\t\t};\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\theaders[csrfNameKey] = csrfName;\n+\t\t\theaders[csrfValueKey] = csrfValue;\n+\t\t\theaders['X-CSRF-NAME'] = csrfName;\n+\t\t\theaders['X-CSRF-VALUE'] = csrfValue;\n+\t\t}\n+\n+\t\tfetch(apiUrl, {\n+\t\t\tmethod: 'POST',\n+\t\t\theaders: headers,\n+\t\t\tcredentials: 'same-origin',\n+\t\t\tbody: JSON.stringify(buildPayload())\n+\t\t})\n+\t\t\t.then(function (response) {\n+\t\t\t\treturn response.json().catch(function () { return {}; }).then(function (data) {\n+\t\t\t\t\tif (!response.ok) {\n+\t\t\t\t\t\tthrow new Error(data.error || ('HTTP ' + response.status));\n+\t\t\t\t\t}\n+\t\t\t\t\treturn data;\n+\t\t\t\t});\n+\t\t\t})\n+\t\t\t.then(function () {\n+\t\t\t\twindow.location.href = backUrl;\n+\t\t\t})\n+\t\t\t.catch(function (error) {\n+\t\t\t\tshowError(error.message || root.dataset.langError);\n+\t\t\t})\n+\t\t\t.finally(function () {\n+\t\t\t\tsaveBtn.disabled = false;\n+\t\t\t\tsaveBtn.textContent = oldText;\n+\t\t\t});\n+\t});\n+})();"},{"sha":"b4c0fbaf4e7d3ce85852acc7bd38a50a3ba6be93","filename":"src/modules/todo/html/todo/add/todo_add.twig","status":"added","additions":119,"deletions":0,"changes":119,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fadd%2Ftodo_add.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,119 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('Add') }} {{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>{{ source('@views/todo/add/todo_add.css') }}</style>\n+{% endblock %}\n+\n+{% block content %}\n+<div id=\"todo-add-app\"\n+\tclass=\"todo-add\"\n+\tdata-api-url=\"{{ phpgw_link('/todo/todos') }}\"\n+\tdata-back-url=\"{{ phpgw_link('/todo/view/todos') }}\"\n+\tdata-default-cat-id=\"{{ selected_cat_id|default(0) }}\"\n+\tdata-default-parent-id=\"{{ selected_parent_id|default(0) }}\"\n+\tdata-date-format=\"{{ date_format|default('Y-m-d') }}\"\n+\tdata-lang-save=\"{{ lang('Save') }}\"\n+\tdata-lang-saving=\"{{ lang('common.loading') }}\"\n+\tdata-lang-error=\"{{ lang('common.error') }}\"\n+\tdata-csrf-name-key=\"{{ csrf.name_key|default('') }}\"\n+\tdata-csrf-value-key=\"{{ csrf.value_key|default('') }}\"\n+\tdata-csrf-name=\"{{ csrf.name|default('') }}\"\n+\tdata-csrf-value=\"{{ csrf.value|default('') }}\"\n+>\n+\t<h2 class=\"todo-add__title\">{{ lang('Add') }} {{ lang('todo') }}</h2>\n+\t<div id=\"todo-add-error\" class=\"todo-add__error\" hidden></div>\n+\n+\t<form id=\"todo-add-form\">\n+\t\t<div class=\"todo-add__grid\">\n+\t\t\t<div class=\"todo-add__field todo-add__field--full\">\n+\t\t\t\t<label for=\"todo-title\">{{ lang('Title') }}</label>\n+\t\t\t\t<input id=\"todo-title\" class=\"todo-add__input\" name=\"title\" type=\"text\" required />\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field todo-add__field--full\">\n+\t\t\t\t<label for=\"todo-descr\">{{ lang('Description') }}</label>\n+\t\t\t\t<textarea id=\"todo-descr\" class=\"todo-add__textarea\" name=\"descr\"></textarea>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-cat\">{{ lang('Category') }}</label>\n+\t\t\t\t<select id=\"todo-cat\" class=\"todo-add__select\" name=\"cat\">\n+\t\t\t\t\t{% for category in categories %}\n+\t\t\t\t\t\t<option value=\"{{ category.id }}\" {% if category.id == selected_cat_id %}selected{% endif %}>{{ category.name }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-parent\">{{ lang('Parent project') }}</label>\n+\t\t\t\t<select id=\"todo-parent\" class=\"todo-add__select\" name=\"parent\">\n+\t\t\t\t\t{% for todo in parentTodos %}\n+\t\t\t\t\t\t<option value=\"{{ todo.id }}\" {% if todo.id == selected_parent_id %}selected{% endif %}>{{ todo.title }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-pri\">{{ lang('Urgency') }}</label>\n+\t\t\t\t<select id=\"todo-pri\" class=\"todo-add__select\" name=\"pri\">\n+\t\t\t\t\t<option value=\"1\">{{ lang('Low') }}</option>\n+\t\t\t\t\t<option value=\"2\" selected>{{ lang('normal') }}</option>\n+\t\t\t\t\t<option value=\"3\">{{ lang('high') }}</option>\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-status\">{{ lang('Completed') }}</label>\n+\t\t\t\t<select id=\"todo-status\" class=\"todo-add__select\" name=\"status\">\n+\t\t\t\t\t{% for i in 0..100 %}\n+\t\t\t\t\t\t<option value=\"{{ i }}\" {% if i == 0 %}selected{% endif %}>{{ i }}%</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-sdate\">{{ lang('Start Date') }}</label>\n+\t\t\t\t<input id=\"todo-sdate\" class=\"todo-add__input\" name=\"sdate\" type=\"text\" autocomplete=\"off\" placeholder=\"{{ date_format|default('Y-m-d') }}\" title=\"{{ date_format|default('Y-m-d') }}\" />\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-edate\">{{ lang('End Date') }}</label>\n+\t\t\t\t<input id=\"todo-edate\" class=\"todo-add__input\" name=\"edate\" type=\"text\" autocomplete=\"off\" placeholder=\"{{ date_format|default('Y-m-d') }}\" title=\"{{ date_format|default('Y-m-d') }}\" />\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field todo-add__field--full\">\n+\t\t\t\t<label class=\"todo-add__checkline\"><input id=\"todo-private\" type=\"checkbox\" name=\"access\" /> {{ lang('Private') }}</label>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-assigned\">{{ lang('Assigned to user') }}</label>\n+\t\t\t\t<select id=\"todo-assigned\" class=\"todo-add__select todo-add__select--multi\" name=\"assigned\" multiple size=\"8\" data-placeholder=\"{{ lang('Select') }} {{ lang('Assigned to user') }}\">\n+\t\t\t\t\t{% for user in users %}\n+\t\t\t\t\t\t<option value=\"{{ user.id }}\">{{ user.name }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-assigned-group\">{{ lang('Assigned to group') }}</label>\n+\t\t\t\t<select id=\"todo-assigned-group\" class=\"todo-add__select todo-add__select--multi\" name=\"assigned_group\" multiple size=\"8\" data-placeholder=\"{{ lang('Select') }} {{ lang('Assigned to group') }}\">\n+\t\t\t\t\t{% for group in groups %}\n+\t\t\t\t\t\t<option value=\"{{ group.id }}\">{{ group.name }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\t\t</div>\n+\n+\t\t<div class=\"todo-add__actions\">\n+\t\t\t<button id=\"todo-save\" class=\"todo-add__button\" type=\"submit\">{{ lang('Save') }}</button>\n+\t\t\t<a class=\"todo-add__button todo-add__button--secondary\" href=\"{{ phpgw_link('/todo/view/todos') }}\">{{ lang('Done') }}</a>\n+\t\t</div>\n+\t</form>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}\n+<script>{{ source('@views/todo/add/todo_add.js') }}</script>\n+{% endblock %}"},{"sha":"ebbc9898d5b63bb24d605d014ebfa5002285389f","filename":"src/modules/todo/html/todo/delete/todo_delete.css","status":"added","additions":54,"deletions":0,"changes":54,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.css","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.css","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.css?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,54 @@\n+.todo-delete {\n+\tpadding: 1rem;\n+\tmax-width: 760px;\n+}\n+\n+.todo-delete__title {\n+\tmargin: 0 0 1rem;\n+}\n+\n+.todo-delete__message {\n+\tmargin-bottom: 1rem;\n+}\n+\n+.todo-delete__message--error {\n+\tpadding: 0.75rem;\n+\tborder: 1px solid #d38e8e;\n+\tbackground: #fdeaea;\n+\tcolor: #7b1f1f;\n+}\n+\n+.todo-delete__confirm {\n+\tmargin: 0 0 0.6rem;\n+}\n+\n+.todo-delete__summary {\n+\tmargin-bottom: 0.9rem;\n+}\n+\n+.todo-delete__checkline {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tgap: 0.5rem;\n+\tmargin-bottom: 1rem;\n+}\n+\n+.todo-delete__actions {\n+\tdisplay: flex;\n+\tgap: 0.75rem;\n+}\n+\n+.todo-delete__button {\n+\tpadding: 0.5rem 0.9rem;\n+\tborder: 1px solid #20568f;\n+\tborder-radius: 4px;\n+\tbackground: #2a72ba;\n+\tcolor: #fff;\n+\ttext-decoration: none;\n+\tcursor: pointer;\n+}\n+\n+.todo-delete__button--secondary {\n+\tbackground: #fff;\n+\tcolor: #20568f;\n+}"},{"sha":"7d06aa6523f6fb3cbf1bf32b3407375f10f72219","filename":"src/modules/todo/html/todo/delete/todo_delete.js","status":"added","additions":116,"deletions":0,"changes":116,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.js","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.js","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.js?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,116 @@\n+(function () {\n+\tvar root = document.getElementById('todo-delete-app');\n+\tif (!root) {\n+\t\treturn;\n+\t}\n+\n+\tvar loadingEl = document.getElementById('todo-delete-loading');\n+\tvar errorEl = document.getElementById('todo-delete-error');\n+\tvar contentEl = document.getElementById('todo-delete-content');\n+\tvar titleEl = document.getElementById('todo-delete-title');\n+\tvar subsWrapEl = document.getElementById('todo-delete-subs-wrap');\n+\tvar subsEl = document.getElementById('todo-delete-subs');\n+\tvar confirmBtn = document.getElementById('todo-delete-confirm');\n+\tvar detailUrl = root.dataset.detailUrl;\n+\tvar apiUrl = root.dataset.apiUrl;\n+\tvar backUrl = root.dataset.backUrl;\n+\tvar csrfNameKey = root.dataset.csrfNameKey || '';\n+\tvar csrfValueKey = root.dataset.csrfValueKey || '';\n+\tvar csrfName = root.dataset.csrfName || '';\n+\tvar csrfValue = root.dataset.csrfValue || '';\n+\n+\tfunction getDeleteReasonMessage(reason, fallback) {\n+\t\tswitch (reason) {\n+\t\t\tcase 'insufficient_grants_owner':\n+\t\t\t\treturn root.dataset.langDeleteDeniedOwner || fallback;\n+\t\t\tcase 'insufficient_grants_parent':\n+\t\t\t\treturn root.dataset.langDeleteDeniedParent || fallback;\n+\t\t\tcase 'parent_not_found':\n+\t\t\t\treturn root.dataset.langDeleteParentNotFound || fallback;\n+\t\t\tcase 'not_found':\n+\t\t\t\treturn root.dataset.langDeleteNotFound || fallback;\n+\t\t\tdefault:\n+\t\t\t\treturn fallback;\n+\t\t}\n+\t}\n+\n+\tfunction showError(message) {\n+\t\tloadingEl.hidden = true;\n+\t\tcontentEl.hidden = true;\n+\t\terrorEl.hidden = false;\n+\t\terrorEl.textContent = message || root.dataset.langError || 'Error';\n+\t}\n+\n+\tfunction showContent(detail) {\n+\t\tloadingEl.hidden = true;\n+\t\terrorEl.hidden = true;\n+\t\tcontentEl.hidden = false;\n+\n+\t\ttitleEl.textContent = detail.title || '-';\n+\t\tif (detail.has_subs) {\n+\t\t\tsubsWrapEl.hidden = false;\n+\t\t}\n+\t}\n+\n+\tfunction loadDetail() {\n+\t\tfetch(detailUrl, { credentials: 'same-origin' })\n+\t\t\t.then(function (response) {\n+\t\t\t\tif (!response.ok) {\n+\t\t\t\t\tthrow new Error('HTTP ' + response.status);\n+\t\t\t\t}\n+\t\t\t\treturn response.json();\n+\t\t\t})\n+\t\t\t.then(function (payload) {\n+\t\t\t\tif (!payload || !payload.detail) {\n+\t\t\t\t\tthrow new Error(root.dataset.langError || 'Error');\n+\t\t\t\t}\n+\t\t\t\tshowContent(payload.detail);\n+\t\t\t})\n+\t\t\t.catch(function (error) {\n+\t\t\t\tshowError(error.message);\n+\t\t\t});\n+\t}\n+\n+\tconfirmBtn.addEventListener('click', function () {\n+\t\tconfirmBtn.disabled = true;\n+\n+\t\tvar url = apiUrl;\n+\t\tif (subsEl && subsEl.checked) {\n+\t\t\turl += '?subs=1';\n+\t\t}\n+\n+\t\tvar headers = {\n+\t\t\t'Accept': 'application/json'\n+\t\t};\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\theaders[csrfNameKey] = csrfName;\n+\t\t\theaders[csrfValueKey] = csrfValue;\n+\t\t\theaders['X-CSRF-NAME'] = csrfName;\n+\t\t\theaders['X-CSRF-VALUE'] = csrfValue;\n+\t\t}\n+\n+\t\tfetch(url, {\n+\t\t\tmethod: 'DELETE',\n+\t\t\tcredentials: 'same-origin',\n+\t\t\theaders: headers\n+\t\t})\n+\t\t\t.then(function (response) {\n+\t\t\t\treturn response.json().catch(function () { return {}; }).then(function (data) {\n+\t\t\t\t\tif (!response.ok) {\n+\t\t\t\t\t\tvar fallbackMessage = data.error || ('HTTP ' + response.status);\n+\t\t\t\t\t\tthrow new Error(getDeleteReasonMessage(data.reason, fallbackMessage));\n+\t\t\t\t\t}\n+\t\t\t\t\treturn data;\n+\t\t\t\t});\n+\t\t\t})\n+\t\t\t.then(function () {\n+\t\t\t\twindow.location.href = backUrl;\n+\t\t\t})\n+\t\t\t.catch(function (error) {\n+\t\t\t\tconfirmBtn.disabled = false;\n+\t\t\t\tshowError(error.message);\n+\t\t\t});\n+\t});\n+\n+\tloadDetail();\n+})();"},{"sha":"7cc92341905b17d3390e96d6efda023628674fd6","filename":"src/modules/todo/html/todo/delete/todo_delete.twig","status":"added","additions":52,"deletions":0,"changes":52,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fdelete%2Ftodo_delete.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,52 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('Delete') }} {{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>{{ source('@views/todo/delete/todo_delete.css') }}</style>\n+{% endblock %}\n+\n+{% block content %}\n+<div id=\"todo-delete-app\"\n+\tclass=\"todo-delete\"\n+\tdata-api-url=\"{{ phpgw_link('/todo/todos/' ~ todo_id) }}\"\n+\tdata-detail-url=\"{{ phpgw_link('/todo/todos/' ~ todo_id) }}\"\n+\tdata-back-url=\"{{ phpgw_link('/todo/view/todos') }}\"\n+\tdata-lang-loading=\"{{ lang('common.loading') }}\"\n+\tdata-lang-error=\"{{ lang('common.error') }}\"\n+\tdata-lang-confirm=\"{{ lang('Are you sure you want to delete this entry') }}\"\n+\tdata-lang-delete-denied-owner=\"{{ lang('Delete denied: insufficient rights on todo owner') }}\"\n+\tdata-lang-delete-denied-parent=\"{{ lang('Delete denied: insufficient rights on parent entry') }}\"\n+\tdata-lang-delete-parent-not-found=\"{{ lang('Delete denied: parent entry not found') }}\"\n+\tdata-lang-delete-not-found=\"{{ lang('Todo entry not found') }}\"\n+\tdata-csrf-name-key=\"{{ csrf.name_key|default('') }}\"\n+\tdata-csrf-value-key=\"{{ csrf.value_key|default('') }}\"\n+\tdata-csrf-name=\"{{ csrf.name|default('') }}\"\n+\tdata-csrf-value=\"{{ csrf.value|default('') }}\"\n+>\n+\t<h2 class=\"todo-delete__title\">{{ lang('Delete') }} {{ lang('todo') }}</h2>\n+\t<div id=\"todo-delete-loading\" class=\"todo-delete__message\">{{ lang('common.loading') }}...</div>\n+\t<div id=\"todo-delete-error\" class=\"todo-delete__message todo-delete__message--error\" hidden></div>\n+\n+\t<div id=\"todo-delete-content\" hidden>\n+\t\t<p class=\"todo-delete__confirm\">{{ lang('Are you sure you want to delete this entry') }}</p>\n+\t\t<div class=\"todo-delete__summary\">\n+\t\t\t<strong>{{ lang('Title') }}:</strong> <span id=\"todo-delete-title\">-</span>\n+\t\t</div>\n+\n+\t\t<label id=\"todo-delete-subs-wrap\" class=\"todo-delete__checkline\" hidden>\n+\t\t\t<input id=\"todo-delete-subs\" type=\"checkbox\" />\n+\t\t\t{{ lang('Do you also want to delete all sub projects ?') }}\n+\t\t</label>\n+\n+\t\t<div class=\"todo-delete__actions\">\n+\t\t\t<button id=\"todo-delete-confirm\" class=\"todo-delete__button\" type=\"button\">{{ lang('Yes') }}</button>\n+\t\t\t<a class=\"todo-delete__button todo-delete__button--secondary\" href=\"{{ phpgw_link('/todo/view/todos') }}\">{{ lang('No') }}</a>\n+\t\t</div>\n+\t</div>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}\n+<script>{{ source('@views/todo/delete/todo_delete.js') }}</script>\n+{% endblock %}"},{"sha":"c8b23512c2fbd135b1061a48cb37c2056c36481d","filename":"src/modules/todo/html/todo/edit/todo_edit.js","status":"added","additions":208,"deletions":0,"changes":208,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fedit%2Ftodo_edit.js","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fedit%2Ftodo_edit.js","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fedit%2Ftodo_edit.js?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,208 @@\n+(function () {\n+\tvar root = document.getElementById('todo-edit-app');\n+\tif (!root) {\n+\t\treturn;\n+\t}\n+\n+\tvar form = document.getElementById('todo-edit-form');\n+\tvar saveBtn = document.getElementById('todo-save');\n+\tvar errorEl = document.getElementById('todo-edit-error');\n+\tvar apiUrl = root.dataset.apiUrl;\n+\tvar backUrl = root.dataset.backUrl;\n+\tvar csrfNameKey = root.dataset.csrfNameKey || '';\n+\tvar csrfValueKey = root.dataset.csrfValueKey || '';\n+\tvar csrfName = root.dataset.csrfName || '';\n+\tvar csrfValue = root.dataset.csrfValue || '';\n+\tvar phpDateFormat = root.dataset.dateFormat || 'Y-m-d';\n+\n+\tfunction toJqueryDateFormat(phpFormat) {\n+\t\treturn String(phpFormat || 'Y-m-d')\n+\t\t\t.replace(/Y/g, 'yy')\n+\t\t\t.replace(/y/g, 'y')\n+\t\t\t.replace(/m/g, 'mm')\n+\t\t\t.replace(/n/g, 'm')\n+\t\t\t.replace(/d/g, 'dd')\n+\t\t\t.replace(/j/g, 'd');\n+\t}\n+\n+\tvar jqueryDateFormat = toJqueryDateFormat(phpDateFormat);\n+\n+\tfunction initSelect2Multi(selector) {\n+\t\tif (typeof window.jQuery === 'undefined' || typeof window.jQuery.fn.select2 === 'undefined') {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar $el = window.jQuery(selector);\n+\t\tif (!$el.length) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\t$el.select2({\n+\t\t\twidth: '100%',\n+\t\t\tcloseOnSelect: false,\n+\t\t\tminimumResultsForSearch: 0,\n+\t\t\tplaceholder: $el.data('placeholder') || '',\n+\t\t\ttemplateResult: function (data) {\n+\t\t\t\tif (!data.id) {\n+\t\t\t\t\treturn data.text;\n+\t\t\t\t}\n+\n+\t\t\t\tvar selected = false;\n+\t\t\t\tif (data.element) {\n+\t\t\t\t\tselected = data.element.selected;\n+\t\t\t\t}\n+\n+\t\t\t\tvar span = document.createElement('span');\n+\t\t\t\tspan.className = 'todo-add__select2-option';\n+\n+\t\t\t\tvar checkbox = document.createElement('input');\n+\t\t\t\tcheckbox.type = 'checkbox';\n+\t\t\t\tcheckbox.className = 'todo-add__select2-checkbox';\n+\t\t\t\tcheckbox.checked = selected;\n+\t\t\t\tcheckbox.tabIndex = -1;\n+\n+\t\t\t\tvar label = document.createElement('span');\n+\t\t\t\tlabel.textContent = data.text;\n+\n+\t\t\t\tspan.appendChild(checkbox);\n+\t\t\t\tspan.appendChild(label);\n+\t\t\t\treturn span;\n+\t\t\t}\n+\t\t});\n+\t}\n+\n+\tfunction showError(message) {\n+\t\terrorEl.hidden = false;\n+\t\terrorEl.textContent = message || root.dataset.langError || 'Error';\n+\t}\n+\n+\tfunction hideError() {\n+\t\terrorEl.hidden = true;\n+\t\terrorEl.textContent = '';\n+\t}\n+\n+\tfunction splitDate(value) {\n+\t\tif (!value) {\n+\t\t\treturn { year: '', month: '', day: '' };\n+\t\t}\n+\n+\t\tif (typeof window.jQuery !== 'undefined' && window.jQuery.datepicker) {\n+\t\t\ttry {\n+\t\t\t\tvar parsed = window.jQuery.datepicker.parseDate(jqueryDateFormat, value);\n+\t\t\t\treturn {\n+\t\t\t\t\tyear: parsed.getFullYear(),\n+\t\t\t\t\tmonth: parsed.getMonth() + 1,\n+\t\t\t\t\tday: parsed.getDate()\n+\t\t\t\t};\n+\t\t\t} catch (err) {\n+\t\t\t\t// Fall through to ISO fallback for robustness.\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (value.indexOf('-') === -1) {\n+\t\t\treturn { year: '', month: '', day: '' };\n+\t\t}\n+\n+\t\tvar parts = value.split('-');\n+\t\tif (parts.length !== 3) {\n+\t\t\treturn { year: '', month: '', day: '' };\n+\t\t}\n+\n+\t\treturn {\n+\t\t\tyear: parseInt(parts[0], 10) || '',\n+\t\t\tmonth: parseInt(parts[1], 10) || '',\n+\t\t\tday: parseInt(parts[2], 10) || ''\n+\t\t};\n+\t}\n+\n+\tfunction getMultiValues(el) {\n+\t\treturn Array.prototype.slice.call(el.selectedOptions || []).map(function (option) {\n+\t\t\treturn option.value;\n+\t\t});\n+\t}\n+\n+\tfunction buildPayload() {\n+\t\tvar sDate = splitDate(form.elements.sdate.value);\n+\t\tvar eDate = splitDate(form.elements.edate.value);\n+\t\tvar payload = {\n+\t\t\ttitle: form.elements.title.value || '',\n+\t\t\tdescr: form.elements.descr.value || '',\n+\t\t\tcat: parseInt(form.elements.cat.value || '0', 10) || 0,\n+\t\t\tparent: parseInt(form.elements.parent.value || '0', 10) || 0,\n+\t\t\tpri: parseInt(form.elements.pri.value || '2', 10) || 2,\n+\t\t\tstatus: parseInt(form.elements.status.value || '0', 10) || 0,\n+\t\t\taccess: form.elements.access.checked,\n+\t\t\tassigned: getMultiValues(form.elements.assigned),\n+\t\t\tassigned_group: getMultiValues(form.elements.assigned_group),\n+\t\t\tsyear: sDate.year,\n+\t\t\tsmonth: sDate.month,\n+\t\t\tsday: sDate.day,\n+\t\t\teyear: eDate.year,\n+\t\t\temonth: eDate.month,\n+\t\t\teday: eDate.day\n+\t\t};\n+\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\tpayload[csrfNameKey] = csrfName;\n+\t\t\tpayload[csrfValueKey] = csrfValue;\n+\t\t}\n+\n+\t\treturn payload;\n+\t}\n+\n+\tinitSelect2Multi('#todo-assigned');\n+\tinitSelect2Multi('#todo-assigned-group');\n+\n+\tif (typeof window.jQuery !== 'undefined' && window.jQuery.fn.datepicker) {\n+\t\twindow.jQuery('#todo-sdate, #todo-edate').datepicker({\n+\t\t\tdateFormat: jqueryDateFormat,\n+\t\t\tchangeMonth: true,\n+\t\t\tchangeYear: true\n+\t\t});\n+\t}\n+\n+\tform.addEventListener('submit', function (event) {\n+\t\tevent.preventDefault();\n+\t\thideError();\n+\n+\t\tvar oldText = saveBtn.textContent;\n+\t\tsaveBtn.disabled = true;\n+\t\tsaveBtn.textContent = root.dataset.langSaving || oldText;\n+\n+\t\tvar headers = {\n+\t\t\t'Content-Type': 'application/json',\n+\t\t\t'Accept': 'application/json'\n+\t\t};\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\theaders[csrfNameKey] = csrfName;\n+\t\t\theaders[csrfValueKey] = csrfValue;\n+\t\t\theaders['X-CSRF-NAME'] = csrfName;\n+\t\t\theaders['X-CSRF-VALUE'] = csrfValue;\n+\t\t}\n+\n+\t\tfetch(apiUrl, {\n+\t\t\tmethod: 'PUT',\n+\t\t\theaders: headers,\n+\t\t\tcredentials: 'same-origin',\n+\t\t\tbody: JSON.stringify(buildPayload())\n+\t\t})\n+\t\t\t.then(function (response) {\n+\t\t\t\treturn response.json().catch(function () { return {}; }).then(function (data) {\n+\t\t\t\t\tif (!response.ok) {\n+\t\t\t\t\t\tthrow new Error(data.error || ('HTTP ' + response.status));\n+\t\t\t\t\t}\n+\t\t\t\t\treturn data;\n+\t\t\t\t});\n+\t\t\t})\n+\t\t\t.then(function () {\n+\t\t\t\twindow.location.href = backUrl;\n+\t\t\t})\n+\t\t\t.catch(function (error) {\n+\t\t\t\tshowError(error.message || root.dataset.langError);\n+\t\t\t})\n+\t\t\t.finally(function () {\n+\t\t\t\tsaveBtn.disabled = false;\n+\t\t\t\tsaveBtn.textContent = oldText;\n+\t\t\t});\n+\t});\n+})();"},{"sha":"caf38e0320f54386cee94784c1d97ec97a849073","filename":"src/modules/todo/html/todo/edit/todo_edit.twig","status":"added","additions":117,"deletions":0,"changes":117,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fedit%2Ftodo_edit.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fedit%2Ftodo_edit.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fedit%2Ftodo_edit.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,117 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('Edit') }} {{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>{{ source('@views/todo/add/todo_add.css') }}</style>\n+{% endblock %}\n+\n+{% block content %}\n+<div id=\"todo-edit-app\"\n+\tclass=\"todo-add\"\n+\tdata-api-url=\"{{ phpgw_link('/todo/todos/' ~ todo.id) }}\"\n+\tdata-back-url=\"{{ phpgw_link('/todo/view/todos') }}\"\n+\tdata-date-format=\"{{ date_format|default('Y-m-d') }}\"\n+\tdata-lang-save=\"{{ lang('Save') }}\"\n+\tdata-lang-saving=\"{{ lang('common.loading') }}\"\n+\tdata-lang-error=\"{{ lang('common.error') }}\"\n+\tdata-csrf-name-key=\"{{ csrf.name_key|default('') }}\"\n+\tdata-csrf-value-key=\"{{ csrf.value_key|default('') }}\"\n+\tdata-csrf-name=\"{{ csrf.name|default('') }}\"\n+\tdata-csrf-value=\"{{ csrf.value|default('') }}\"\n+>\n+\t<h2 class=\"todo-add__title\">{{ lang('Edit') }} {{ lang('todo') }}</h2>\n+\t<div id=\"todo-edit-error\" class=\"todo-add__error\" hidden></div>\n+\n+\t<form id=\"todo-edit-form\">\n+\t\t<div class=\"todo-add__grid\">\n+\t\t\t<div class=\"todo-add__field todo-add__field--full\">\n+\t\t\t\t<label for=\"todo-title\">{{ lang('Title') }}</label>\n+\t\t\t\t<input id=\"todo-title\" class=\"todo-add__input\" name=\"title\" type=\"text\" value=\"{{ todo.title }}\" required />\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field todo-add__field--full\">\n+\t\t\t\t<label for=\"todo-descr\">{{ lang('Description') }}</label>\n+\t\t\t\t<textarea id=\"todo-descr\" class=\"todo-add__textarea\" name=\"descr\">{{ todo.descr }}</textarea>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-cat\">{{ lang('Category') }}</label>\n+\t\t\t\t<select id=\"todo-cat\" class=\"todo-add__select\" name=\"cat\">\n+\t\t\t\t\t{% for category in categories %}\n+\t\t\t\t\t\t<option value=\"{{ category.id }}\" {% if category.id == todo.cat %}selected{% endif %}>{{ category.name }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-parent\">{{ lang('Parent project') }}</label>\n+\t\t\t\t<select id=\"todo-parent\" class=\"todo-add__select\" name=\"parent\">\n+\t\t\t\t\t{% for parentTodo in parentTodos %}\n+\t\t\t\t\t\t<option value=\"{{ parentTodo.id }}\" {% if parentTodo.id == todo.parent %}selected{% endif %}>{{ parentTodo.title }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-pri\">{{ lang('Urgency') }}</label>\n+\t\t\t\t<select id=\"todo-pri\" class=\"todo-add__select\" name=\"pri\">\n+\t\t\t\t\t<option value=\"1\" {% if todo.pri == 1 %}selected{% endif %}>{{ lang('Low') }}</option>\n+\t\t\t\t\t<option value=\"2\" {% if todo.pri == 2 %}selected{% endif %}>{{ lang('normal') }}</option>\n+\t\t\t\t\t<option value=\"3\" {% if todo.pri == 3 %}selected{% endif %}>{{ lang('high') }}</option>\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-status\">{{ lang('Completed') }}</label>\n+\t\t\t\t<select id=\"todo-status\" class=\"todo-add__select\" name=\"status\">\n+\t\t\t\t\t{% for i in 0..100 %}\n+\t\t\t\t\t\t<option value=\"{{ i }}\" {% if i == todo.status %}selected{% endif %}>{{ i }}%</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-sdate\">{{ lang('Start Date') }}</label>\n+\t\t\t\t<input id=\"todo-sdate\" class=\"todo-add__input\" name=\"sdate\" type=\"text\" autocomplete=\"off\" placeholder=\"{{ date_format|default('Y-m-d') }}\" title=\"{{ date_format|default('Y-m-d') }}\" value=\"{{ todo.sdate }}\" />\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-edate\">{{ lang('End Date') }}</label>\n+\t\t\t\t<input id=\"todo-edate\" class=\"todo-add__input\" name=\"edate\" type=\"text\" autocomplete=\"off\" placeholder=\"{{ date_format|default('Y-m-d') }}\" title=\"{{ date_format|default('Y-m-d') }}\" value=\"{{ todo.edate }}\" />\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field todo-add__field--full\">\n+\t\t\t\t<label class=\"todo-add__checkline\"><input id=\"todo-private\" type=\"checkbox\" name=\"access\" {% if todo.access_private %}checked{% endif %} /> {{ lang('Private') }}</label>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-assigned\">{{ lang('Assigned to user') }}</label>\n+\t\t\t\t<select id=\"todo-assigned\" class=\"todo-add__select todo-add__select--multi\" name=\"assigned\" multiple size=\"8\" data-placeholder=\"{{ lang('Select') }} {{ lang('Assigned to user') }}\">\n+\t\t\t\t\t{% for user in users %}\n+\t\t\t\t\t\t<option value=\"{{ user.id }}\" {% if user.id in todo.assigned_ids %}selected{% endif %}>{{ user.name }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\n+\t\t\t<div class=\"todo-add__field\">\n+\t\t\t\t<label for=\"todo-assigned-group\">{{ lang('Assigned to group') }}</label>\n+\t\t\t\t<select id=\"todo-assigned-group\" class=\"todo-add__select todo-add__select--multi\" name=\"assigned_group\" multiple size=\"8\" data-placeholder=\"{{ lang('Select') }} {{ lang('Assigned to group') }}\">\n+\t\t\t\t\t{% for group in groups %}\n+\t\t\t\t\t\t<option value=\"{{ group.id }}\" {% if group.id in todo.assigned_group_ids %}selected{% endif %}>{{ group.name }}</option>\n+\t\t\t\t\t{% endfor %}\n+\t\t\t\t</select>\n+\t\t\t</div>\n+\t\t</div>\n+\n+\t\t<div class=\"todo-add__actions\">\n+\t\t\t<button id=\"todo-save\" class=\"todo-add__button\" type=\"submit\">{{ lang('Save') }}</button>\n+\t\t\t<a class=\"todo-add__button todo-add__button--secondary\" href=\"{{ phpgw_link('/todo/view/todos') }}\">{{ lang('Done') }}</a>\n+\t\t</div>\n+\t</form>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}\n+<script>{{ source('@views/todo/edit/todo_edit.js') }}</script>\n+{% endblock %}"},{"sha":"8cb93d1127cfe84ab844b2f4538361ea1b23e59a","filename":"src/modules/todo/html/todo/index/todo_datatable.twig","status":"added","additions":195,"deletions":0,"changes":195,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_datatable.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_datatable.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_datatable.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,195 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>\n+\t.todo-title {\n+\t\tdisplay: inline-block;\n+\t\twhite-space: nowrap;\n+\t}\n+\t.todo-title__marker {\n+\t\tdisplay: inline-block;\n+\t\tmargin-right: 0.35rem;\n+\t\tcolor: #7a8a9a;\n+\t\tfont-weight: 700;\n+\t}\n+</style>\n+{% endblock %}\n+\n+{% block content %}\n+<script>\n+\tfunction todoEscapeHtml(value)\n+\t{\n+\t\treturn String(value === undefined || value === null ? '' : value)\n+\t\t\t.replace(/&/g, '&amp;')\n+\t\t\t.replace(/</g, '&lt;')\n+\t\t\t.replace(/>/g, '&gt;')\n+\t\t\t.replace(/\"/g, '&quot;')\n+\t\t\t.replace(/'/g, '&#039;');\n+\t}\n+\n+\tfunction todoFormatTitle(key, row)\n+\t{\n+\t\tvar level = parseInt(row.level || 0, 10);\n+\t\tif (isNaN(level) || level < 0)\n+\t\t{\n+\t\t\tlevel = 0;\n+\t\t}\n+\n+\t\tvar marker = '';\n+\t\tif (level > 0)\n+\t\t{\n+\t\t\tmarker = '<span class=\"todo-title__marker\">' + '&#9500;'.repeat(Math.min(level, 4)) + '</span>';\n+\t\t}\n+\n+\t\tvar title = todoEscapeHtml(row.title || '');\n+\t\tvar margin = level * 1.1;\n+\t\treturn '<span class=\"todo-title\" style=\"margin-left:' + margin + 'rem\">' + marker + title + '</span>';\n+\t}\n+\n+\tfunction todoRenderMultiline(value)\n+\t{\n+\t\tvar normalized = String(value === undefined || value === null ? '' : value)\n+\t\t\t.replace(/<br\\s*\\/?>/gi, '\\n')\n+\t\t\t.replace(/\\r\\n/g, '\\n')\n+\t\t\t.replace(/\\r/g, '\\n');\n+\n+\t\treturn todoEscapeHtml(normalized).replace(/\\n/g, '<br>');\n+\t}\n+\n+\tfunction renderIdLink(key, row)\n+\t{\n+\t\tvar id = row && row.id != null ? String(row.id) : '';\n+\t\tif (!id)\n+\t\t{\n+\t\t\treturn '';\n+\t\t}\n+\n+\t\tvar href = row && row.actions && row.actions.view\n+\t\t\t? String(row.actions.view)\n+\t\t\t: ('/todo/view/todos/' + encodeURIComponent(id));\n+\n+\t\treturn '<a href=\"' + todoEscapeHtml(href) + '\">' + todoEscapeHtml(id) + '</a>';\n+\t}\n+\n+\tfunction renderAssigned(key, row)\n+\t{\n+\t\tvar entries = Array.isArray(row && row.assigned_entries) ? row.assigned_entries : [];\n+\t\tif (entries.length)\n+\t\t{\n+\t\t\tvar lines = entries\n+\t\t\t\t.map(function (entry) {\n+\t\t\t\t\treturn entry && entry.name != null ? String(entry.name) : '';\n+\t\t\t\t})\n+\t\t\t\t.filter(function (line) {\n+\t\t\t\t\treturn line !== '';\n+\t\t\t\t});\n+\n+\t\t\tif (lines.length)\n+\t\t\t{\n+\t\t\t\treturn lines.map(function (line) { return todoEscapeHtml(line); }).join('<br>');\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn '-';\n+\t}\n+</script>\n+\n+{% set actions = [\n+\t{\n+\t\t'my_name': 'view',\n+\t\t'text': lang('view'),\n+\t\t'type': 'custom',\n+\t\t'custom_code': \"\n+\t\t\tvar api = oTable.api();\n+\t\t\tvar selected = api.rows({ selected: true }).data();\n+\t\t\tif (!selected.length) { return false; }\n+\t\t\tvar aData = selected[0];\n+\t\t\twindow.open('/todo/view/todos/' + aData['id'], '_self');\n+\t\t\"\n+\t},\n+\t{\n+\t\t'my_name': 'edit',\n+\t\t'text': lang('edit'),\n+\t\t'type': 'custom',\n+\t\t'custom_code': \"\n+\t\t\tvar api = oTable.api();\n+\t\t\tvar selected = api.rows({ selected: true }).data();\n+\t\t\tif (!selected.length) { return false; }\n+\t\t\tvar aData = selected[0];\n+\t\t\twindow.open('/todo/view/todos/' + aData['id'] + '/edit', '_self');\n+\t\t\"\n+\t},\n+\t{\n+\t\t'my_name': 'new',\n+\t\t'text': lang('add sub project'),\n+\t\t'type': 'custom',\n+\t\t'custom_code': \"\n+\t\t\tvar api = oTable.api();\n+\t\t\tvar selected = api.rows({ selected: true }).data();\n+\t\t\tif (!selected.length) { return false; }\n+\t\t\tvar aData = selected[0];\n+\t\t\twindow.open('/todo/view/todos/add?parent=' + aData['id'] + '&cat_id=' + aData['cat_id'], '_self');\n+\t\t\"\n+\t}\n+] %}\n+\n+{% if acl_delete|default(false) %}\n+\t{% set actions = actions|merge([\n+\t\t{\n+\t\t\t'my_name': 'delete',\n+\t\t\t'text': lang('delete'),\n+\t\t\t'type': 'custom',\n+\t\t\t'custom_code': \"\n+\t\t\t\tvar api = oTable.api();\n+\t\t\t\tvar selected = api.rows({ selected: true }).data();\n+\t\t\t\tif (!selected.length) { return false; }\n+\t\t\t\tvar aData = selected[0];\n+\t\t\t\twindow.open('/todo/view/todos/' + aData['id'] + '/delete', '_self');\n+\t\t\t\"\n+\t\t}\n+\t]) %}\n+{% endif %}\n+\n+{% set datatable = {\n+\t'source': phpgw_link('/todo/todos'),\n+\t'download': phpgw_link('/todo/todos/export/csv'),\n+\t'new_item': phpgw_link('/todo/view/todos/add'),\n+\t'extra_buttons': [\n+\t\t{\n+\t\t\t'text': lang('view matrix of actual month'),\n+\t\t\t'url': matrix_url|default(phpgw_link('/todo/view/todos/matrix')),\n+\t\t\t'className': 'app-button--secondary'\n+\t\t}\n+\t],\n+\t'actions': actions,\n+\t'query': search_query|default(''),\n+\t'sorted_by': {'key': 0, 'dir': 'asc'},\n+\t'field': [\n+\t\t{'key': 'id', 'label': lang('ID'), 'sortable': 1, 'searchable': 0, 'hidden': 0, 'formatter': 'renderIdLink'},\n+\t\t{'key': 'title', 'label': lang('Title'), 'sortable': 1, 'searchable': 1, 'hidden': 0, 'formatter': 'todoFormatTitle'},\n+\t\t{'key': 'status', 'label': lang('Status'), 'sortable': 0, 'searchable': 1, 'hidden': 0},\n+\t\t{'key': 'pri', 'label': lang('Urgency'), 'sortable': 1, 'searchable': 0, 'hidden': 0},\n+\t\t{'key': 'sdate', 'label': lang('start date'), 'sortable': 1, 'searchable': 0, 'hidden': 0},\n+\t\t{'key': 'edate', 'label': lang('end date'), 'sortable': 1, 'searchable': 0, 'hidden': 0},\n+\t\t{'key': 'owner', 'label': lang('created by'), 'sortable': 0, 'searchable': 1, 'hidden': 0},\n+\t\t{'key': 'assigned_entries', 'label': lang('assigned to'), 'sortable': 0, 'searchable': 1, 'hidden': 0, 'formatter': 'renderAssigned'}\n+\t]\n+} %}\n+\n+{% set form = {\n+\t'toolbar': {\n+\t\t'item': [\n+\t\t\t{'name': 'cat_id', 'text': lang('Category'), 'type': 'filter', 'list': categories|default([])},\n+\t\t\t{'name': 'filter', 'text': lang('Filter'), 'type': 'filter', 'list': filters|default([])}\n+\t\t]\n+\t}\n+} %}\n+\n+{% include 'datatable2.twig' with {\n+\t'data': {'filters': {}},\n+\t'datatable': datatable,\n+\t'form': form\n+} %}\n+{% endblock %}"},{"sha":"e72e8b6a68cc03426b3748ec218ad2e1fc7d5936","filename":"src/modules/todo/html/todo/index/todo_index.css","status":"added","additions":121,"deletions":0,"changes":121,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.css","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.css","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.css?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,121 @@\n+.todo-app {\n+\tpadding: 1rem;\n+}\n+\n+.todo-app__toolbar {\n+\tdisplay: flex;\n+\tflex-wrap: wrap;\n+\tgap: 0.75rem;\n+\tmargin-bottom: 1rem;\n+}\n+\n+.todo-app__search,\n+.todo-app__select {\n+\tpadding: 0.5rem;\n+\tborder: 1px solid #c6c6c6;\n+\tborder-radius: 4px;\n+}\n+\n+.todo-app__search {\n+\tmin-width: 260px;\n+}\n+\n+.todo-app__select--compact {\n+\tpadding: 0.45rem 0.5rem;\n+\tmin-width: 5.5rem;\n+}\n+\n+.todo-app__button {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tpadding: 0.5rem 0.75rem;\n+\tborder: 1px solid #20568f;\n+\tborder-radius: 4px;\n+\tbackground: #2a72ba;\n+\tcolor: #fff;\n+\ttext-decoration: none;\n+\tcursor: pointer;\n+}\n+\n+.todo-app__button:hover {\n+\tbackground: #225f9c;\n+}\n+\n+.todo-app__button--secondary {\n+\tbackground: #fff;\n+\tcolor: #20568f;\n+}\n+\n+.todo-app__message {\n+\tpadding: 0.5rem 0;\n+}\n+\n+.todo-app__message--error {\n+\tcolor: #b30000;\n+}\n+\n+.todo-app__table {\n+\twidth: 100%;\n+\tborder-collapse: collapse;\n+}\n+\n+.todo-app__table th,\n+.todo-app__table td {\n+\tpadding: 0.5rem;\n+\tborder-bottom: 1px solid #e5e5e5;\n+\ttext-align: left;\n+\tvertical-align: top;\n+}\n+\n+.todo-app__title-link {\n+\tdisplay: inline-block;\n+\tcolor: inherit;\n+\ttext-decoration: none;\n+}\n+\n+.todo-app__title-link:hover {\n+\ttext-decoration: underline;\n+}\n+\n+.todo-app__title {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tgap: 0.35rem;\n+\tpadding-left: calc(var(--todo-level, 0) * 1rem);\n+}\n+\n+.todo-app__title-marker {\n+\tcolor: #6f7f8f;\n+\tfont-weight: 600;\n+}\n+\n+.todo-app__pagination {\n+\tdisplay: flex;\n+\tjustify-content: space-between;\n+\talign-items: center;\n+\tgap: 1rem;\n+\tmargin-top: 0.5rem;\n+}\n+\n+.todo-app__pagination-summary,\n+.todo-app__pagination-status {\n+\tcolor: #455a70;\n+}\n+\n+.todo-app__pagination-controls {\n+\tdisplay: flex;\n+\talign-items: center;\n+\tgap: 0.75rem;\n+\tflex-wrap: wrap;\n+}\n+\n+.todo-app__pagination-label {\n+\tcolor: #455a70;\n+}\n+\n+@media (max-width: 700px) {\n+\t.todo-app__pagination {\n+\t\tflex-direction: column;\n+\t\talign-items: flex-start;\n+\t}\n+}"},{"sha":"f821d1833c54c3f7c5a733ca7d26840682afd9a7","filename":"src/modules/todo/html/todo/index/todo_index.js","status":"added","additions":356,"deletions":0,"changes":356,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.js","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.js","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.js?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,356 @@\n+(function () {\n+\tvar root = document.getElementById('todo-app');\n+\tif (!root) {\n+\t\treturn;\n+\t}\n+\n+\tvar apiUrl = root.dataset.apiUrl;\n+\tvar viewUrl = root.dataset.viewUrl;\n+\tvar csvUrl = root.dataset.csvUrl;\n+\tvar loadingEl = document.getElementById('todo-loading');\n+\tvar errorEl = document.getElementById('todo-error');\n+\tvar tableEl = document.getElementById('todo-table');\n+\tvar tbodyEl = document.getElementById('todo-table-body');\n+\tvar searchEl = document.getElementById('todo-search');\n+\tvar filterEl = document.getElementById('todo-filter');\n+\tvar catEl = document.getElementById('todo-cat');\n+\tvar csvBtn = document.getElementById('todo-csv');\n+\tvar paginationEl = document.getElementById('todo-pagination');\n+\tvar paginationSummaryEl = document.getElementById('todo-pagination-summary');\n+\tvar pageStatusEl = document.getElementById('todo-page-status');\n+\tvar pageSizeEl = document.getElementById('todo-page-size');\n+\tvar prevBtn = document.getElementById('todo-page-prev');\n+\tvar nextBtn = document.getElementById('todo-page-next');\n+\tvar sortButtons = Array.prototype.slice.call(root.querySelectorAll('.todo-app__sort'));\n+\tvar initialParams = new URLSearchParams(window.location.search);\n+\tvar initialCatId = initialParams.get('cat_id') || '0';\n+\tvar defaultPageSize = parseInt(root.dataset.defaultPageSize || '25', 10) || 25;\n+\tvar pageSize = defaultPageSize;\n+\tvar currentPage = Math.max(parseInt(initialParams.get('page') || '1', 10) || 1, 1);\n+\tvar totalItems = 0;\n+\tvar allowedSortKeys = {\n+\t\tid: true,\n+\t\ttitle: true,\n+\t\tpriority: true,\n+\t\tcreated: true,\n+\t\tdue: true\n+\t};\n+\tvar sortKey = allowedSortKeys[initialParams.get('sort')] ? initialParams.get('sort') : 'created';\n+\tvar sortDir = String(initialParams.get('dir') || 'DESC').toUpperCase() === 'ASC' ? 'ASC' : 'DESC';\n+\n+\tif (initialParams.get('filter')) {\n+\t\tfilterEl.value = initialParams.get('filter');\n+\t}\n+\n+\tif (initialParams.get('search')) {\n+\t\tsearchEl.value = initialParams.get('search');\n+\t}\n+\n+\tif (catEl && initialCatId) {\n+\t\tcatEl.value = initialCatId;\n+\t}\n+\n+\tif (pageSizeEl) {\n+\t\tvar initialLimit = parseInt(initialParams.get('limit') || String(defaultPageSize), 10);\n+\t\tvar hasMatchingOption = Array.prototype.some.call(pageSizeEl.options, function (option) {\n+\t\t\treturn parseInt(option.value, 10) === initialLimit;\n+\t\t});\n+\n+\t\tif (hasMatchingOption) {\n+\t\t\tpageSize = initialLimit;\n+\t\t}\n+\n+\t\tpageSizeEl.value = String(pageSize);\n+\t}\n+\n+\tfunction makeActionLink(url, label) {\n+\t\tif (!url) {\n+\t\t\treturn '&nbsp;';\n+\t\t}\n+\t\treturn '<a href=\"' + escapeAttribute(url) + '\">' + escapeHtml(label) + '</a>';\n+\t}\n+\n+\tfunction escapeAttribute(value) {\n+\t\treturn String(value == null ? '' : value)\n+\t\t\t.replace(/\"/g, '&quot;')\n+\t\t\t.replace(/</g, '&lt;')\n+\t\t\t.replace(/>/g, '&gt;')\n+\t\t\t.replace(/'/g, '&#039;');\n+\t}\n+\n+\tfunction escapeHtml(value) {\n+\t\treturn String(value == null ? '' : value)\n+\t\t\t.replace(/&/g, '&amp;')\n+\t\t\t.replace(/</g, '&lt;')\n+\t\t\t.replace(/>/g, '&gt;')\n+\t\t\t.replace(/\\\"/g, '&quot;')\n+\t\t\t.replace(/'/g, '&#039;');\n+\t}\n+\n+\tfunction renderMultiline(value) {\n+\t\tvar normalized = String(value == null ? '' : value)\n+\t\t\t.replace(/<br\\s*\\/?>/gi, '\\n')\n+\t\t\t.replace(/\\r\\n/g, '\\n')\n+\t\t\t.replace(/\\r/g, '\\n');\n+\n+\t\treturn escapeHtml(normalized).replace(/\\n/g, '<br>');\n+\t}\n+\n+\tfunction renderTitleCell(item, href) {\n+\t\tvar level = Math.max(parseInt(item.level || 0, 10) || 0, 0);\n+\t\tvar marker = level > 0 ? '<span class=\"todo-app__title-marker\" aria-hidden=\"true\">↳</span>' : '';\n+\t\tvar title = escapeHtml(item.title || '');\n+\n+\t\treturn '<a class=\"todo-app__title-link\" href=\"' + escapeAttribute(href) + '\">' +\n+\t\t\t'<span class=\"todo-app__title\" style=\"--todo-level:' + level + '\">' +\n+\t\t\tmarker +\n+\t\t\ttitle +\n+\t\t\t'</span>' +\n+\t\t'</a>';\n+\t}\n+\n+\tfunction updateSortButtons() {\n+\t\tsortButtons.forEach(function (button) {\n+\t\t\tvar key = button.getAttribute('data-sort-key') || '';\n+\t\t\tvar active = key === sortKey;\n+\t\t\tbutton.classList.toggle('is-active', active);\n+\t\t\tbutton.setAttribute('aria-sort', active ? (sortDir === 'ASC' ? 'ascending' : 'descending') : 'none');\n+\t\t\tbutton.textContent = button.textContent.replace(/\\s[↑↓]$/, '');\n+\t\t\tif (active) {\n+\t\t\t\tbutton.textContent += sortDir === 'ASC' ? ' ↑' : ' ↓';\n+\t\t\t}\n+\t\t});\n+\t}\n+\n+\tfunction setState(state, errorMessage) {\n+\t\tloadingEl.hidden = state !== 'loading';\n+\t\terrorEl.hidden = state !== 'error';\n+\t\ttableEl.hidden = state !== 'ready';\n+\t\tif (paginationEl) {\n+\t\t\tpaginationEl.hidden = state !== 'ready';\n+\t\t}\n+\t\tif (state === 'error') {\n+\t\t\terrorEl.textContent = errorMessage || root.dataset.langError;\n+\t\t}\n+\t}\n+\n+\tfunction updateBrowserUrl() {\n+\t\tvar params = new URLSearchParams();\n+\t\tif (searchEl.value) {\n+\t\t\tparams.set('search', searchEl.value);\n+\t\t}\n+\t\tif (filterEl.value && filterEl.value !== 'none') {\n+\t\t\tparams.set('filter', filterEl.value);\n+\t\t}\n+\t\tif (catEl && catEl.value && catEl.value !== '0') {\n+\t\t\tparams.set('cat_id', catEl.value);\n+\t\t}\n+\t\tif (currentPage > 1) {\n+\t\t\tparams.set('page', String(currentPage));\n+\t\t}\n+\t\tif (pageSize !== defaultPageSize) {\n+\t\t\tparams.set('limit', String(pageSize));\n+\t\t}\n+\t\tif (sortKey !== 'created') {\n+\t\t\tparams.set('sort', sortKey);\n+\t\t}\n+\t\tif (sortDir !== 'DESC') {\n+\t\t\tparams.set('dir', sortDir);\n+\t\t}\n+\n+\t\tvar query = params.toString();\n+\t\tvar url = window.location.pathname + (query ? '?' + query : '');\n+\t\twindow.history.replaceState(null, '', url);\n+\t}\n+\n+\tfunction renderPagination(itemCount) {\n+\t\tif (!paginationEl || !paginationSummaryEl || !pageStatusEl || !prevBtn || !nextBtn) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);\n+\t\tvar startItem = totalItems === 0 ? 0 : ((currentPage - 1) * pageSize) + 1;\n+\t\tvar endItem = totalItems === 0 ? 0 : startItem + itemCount - 1;\n+\n+\t\tpaginationSummaryEl.textContent = (root.dataset.langShowing || 'Showing') + ' ' + startItem + '-' + endItem + ' / ' + totalItems;\n+\t\tpageStatusEl.textContent = (root.dataset.langPage || 'Page') + ' ' + currentPage + ' ' + (root.dataset.langOf || 'of') + ' ' + totalPages;\n+\t\tprevBtn.disabled = currentPage <= 1;\n+\t\tnextBtn.disabled = currentPage >= totalPages || totalItems === 0;\n+\t}\n+\n+\tfunction renderRows(items) {\n+\t\ttbodyEl.innerHTML = '';\n+\t\tif (!items || !items.length) {\n+\t\t\tvar emptyRow = document.createElement('tr');\n+\t\t\temptyRow.innerHTML = '<td colspan=\"12\">' + escapeHtml(root.dataset.langEmpty) + '</td>';\n+\t\t\ttbodyEl.appendChild(emptyRow);\n+\t\t\treturn;\n+\t\t}\n+\n+\t\titems.forEach(function (item) {\n+\t\t\tvar row = document.createElement('tr');\n+\t\t\tvar actions = item.actions || {};\n+\t\t\tvar href = actions.view || (viewUrl + '&todo_id=' + encodeURIComponent(item.id));\n+\t\t\trow.innerHTML = '' +\n+\t\t\t\t'<td>' + escapeHtml(item.id || '') + '</td>' +\n+\t\t\t\t'<td>' + renderTitleCell(item, href) + '</td>' +\n+\t\t\t\t'<td>' + escapeHtml(item.status || '') + '</td>' +\n+\t\t\t\t'<td>' + escapeHtml(item.pri || '') + '</td>' +\n+\t\t\t\t'<td>' + escapeHtml(item.sdate || '') + '</td>' +\n+\t\t\t\t'<td>' + escapeHtml(item.edate || '') + '</td>' +\n+\t\t\t\t'<td>' + escapeHtml(item.owner || '') + '</td>' +\n+\t\t\t\t'<td>' + renderMultiline(item.assigned || '') + '</td>' +\n+\t\t\t\t'<td>' + makeActionLink(actions.view, root.dataset.langView || 'View') + '</td>' +\n+\t\t\t\t'<td>' + makeActionLink(actions.edit, root.dataset.langEdit || 'Edit') + '</td>' +\n+\t\t\t\t'<td>' + makeActionLink(actions.delete, root.dataset.langDelete || 'Delete') + '</td>' +\n+\t\t\t\t'<td>' + makeActionLink(actions.subadd, root.dataset.langAddSub || 'Add Sub Project') + '</td>';\n+\t\t\ttbodyEl.appendChild(row);\n+\t\t});\n+\t}\n+\n+\tfunction buildQueryParams() {\n+\t\tvar params = buildFilterParams();\n+\t\tparams.set('start', String((currentPage - 1) * pageSize));\n+\t\tparams.set('limit', String(pageSize));\n+\t\tparams.set('sort', sortKey);\n+\t\tparams.set('dir', sortDir);\n+\t\treturn params;\n+\t}\n+\n+\tfunction buildFilterParams() {\n+\t\tvar params = new URLSearchParams();\n+\t\tparams.set('filter', filterEl.value || 'none');\n+\t\tparams.set('search', searchEl.value || '');\n+\t\tif (catEl && catEl.value) {\n+\t\t\tparams.set('cat_id', catEl.value);\n+\t\t}\n+\t\treturn params;\n+\t}\n+\n+\tfunction buildRequestUrl(baseUrl, queryParams) {\n+\t\tvar url = new URL(baseUrl, window.location.origin);\n+\t\tqueryParams.forEach(function (value, key) {\n+\t\t\turl.searchParams.set(key, value);\n+\t\t});\n+\t\treturn url.toString();\n+\t}\n+\n+\tfunction loadTodos() {\n+\t\tsetState('loading');\n+\n+\t\tvar params = buildQueryParams();\n+\n+\t\tfetch(buildRequestUrl(apiUrl, params), {\n+\t\t\tcredentials: 'same-origin'\n+\t\t})\n+\t\t\t.then(function (res) {\n+\t\t\t\tif (!res.ok) {\n+\t\t\t\t\tthrow new Error('HTTP ' + res.status);\n+\t\t\t\t}\n+\t\t\t\treturn res.json();\n+\t\t\t})\n+\t\t\t.then(function (payload) {\n+\t\t\t\ttotalItems = parseInt(payload.total || 0, 10) || 0;\n+\t\t\t\tvar totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);\n+\t\t\t\tif (currentPage > totalPages) {\n+\t\t\t\t\tcurrentPage = totalPages;\n+\t\t\t\t\tloadTodos();\n+\t\t\t\t\treturn;\n+\t\t\t\t}\n+\n+\t\t\t\trenderRows(payload.items || []);\n+\t\t\t\trenderPagination((payload.items || []).length);\n+\t\t\t\tupdateBrowserUrl();\n+\t\t\t\tsetState('ready');\n+\t\t\t})\n+\t\t\t.catch(function (err) {\n+\t\t\t\tsetState('error', err.message);\n+\t\t\t});\n+\t}\n+\n+\tvar searchTimer = null;\n+\tsearchEl.addEventListener('input', function () {\n+\t\tcurrentPage = 1;\n+\t\tif (searchTimer) {\n+\t\t\tclearTimeout(searchTimer);\n+\t\t}\n+\t\tsearchTimer = setTimeout(loadTodos, 250);\n+\t});\n+\n+\tfilterEl.addEventListener('change', function () {\n+\t\tcurrentPage = 1;\n+\t\tloadTodos();\n+\t});\n+\n+\tif (catEl) {\n+\t\tcatEl.addEventListener('change', function () {\n+\t\t\tcurrentPage = 1;\n+\t\t\tloadTodos();\n+\t\t});\n+\t}\n+\n+\tif (pageSizeEl) {\n+\t\tpageSizeEl.addEventListener('change', function () {\n+\t\t\tvar selectedSize = parseInt(pageSizeEl.value || String(defaultPageSize), 10) || defaultPageSize;\n+\t\t\tpageSize = selectedSize;\n+\t\t\tcurrentPage = 1;\n+\t\t\tloadTodos();\n+\t\t});\n+\t}\n+\n+\tif (prevBtn) {\n+\t\tprevBtn.addEventListener('click', function () {\n+\t\t\tif (currentPage <= 1) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tcurrentPage -= 1;\n+\t\t\tloadTodos();\n+\t\t});\n+\t}\n+\n+\tif (nextBtn) {\n+\t\tnextBtn.addEventListener('click', function () {\n+\t\t\tvar totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);\n+\t\t\tif (currentPage >= totalPages) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tcurrentPage += 1;\n+\t\t\tloadTodos();\n+\t\t});\n+\t}\n+\n+\tif (csvBtn) {\n+\t\tcsvBtn.addEventListener('click', function () {\n+\t\t\tvar params = buildFilterParams();\n+\t\t\tparams.set('sort', sortKey);\n+\t\t\tparams.set('dir', sortDir);\n+\t\t\twindow.location.href = buildRequestUrl(csvUrl, params);\n+\t\t});\n+\t}\n+\n+\tsortButtons.forEach(function (button) {\n+\t\tbutton.addEventListener('click', function () {\n+\t\t\tvar key = button.getAttribute('data-sort-key') || '';\n+\t\t\tif (!allowedSortKeys[key]) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tif (sortKey === key) {\n+\t\t\t\tsortDir = sortDir === 'ASC' ? 'DESC' : 'ASC';\n+\t\t\t} else {\n+\t\t\t\tsortKey = key;\n+\t\t\t\tsortDir = 'ASC';\n+\t\t\t}\n+\n+\t\t\tcurrentPage = 1;\n+\t\t\tupdateSortButtons();\n+\t\t\tloadTodos();\n+\t\t});\n+\t});\n+\n+\tupdateSortButtons();\n+\n+\tloadTodos();\n+})();"},{"sha":"b7e742d25d6b794724ff221c5ec1d064e1d9fb95","filename":"src/modules/todo/html/todo/index/todo_index.twig","status":"added","additions":118,"deletions":0,"changes":118,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Findex%2Ftodo_index.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,118 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>{{ source('@views/todo/index/todo_index.css') }}</style>\n+{% endblock %}\n+\n+{% block content %}\n+<div id=\"todo-app\"\n+\tclass=\"todo-app\"\n+\tdata-api-url=\"{{ phpgw_link('/todo/todos') }}\"\n+\tdata-csv-url=\"{{ phpgw_link('/todo/todos/export/csv') }}\"\n+\tdata-view-url=\"{{ phpgw_link('/index.php', {'menuaction': 'todo.uitodo.view'}) }}\"\n+\tdata-add-url=\"{{ phpgw_link('/todo/view/todos/add') }}\"\n+\tdata-edit-url=\"{{ phpgw_link('/index.php', {'menuaction': 'todo.uitodo.edit'}) }}\"\n+\tdata-delete-url=\"{{ phpgw_link('/index.php', {'menuaction': 'todo.uitodo.delete'}) }}\"\n+\tdata-subadd-url=\"{{ phpgw_link('/index.php', {'menuaction': 'todo.uitodo.add'}) }}\"\n+\tdata-lang-loading=\"{{ lang('common.loading') }}\"\n+\tdata-lang-error=\"{{ lang('common.error') }}\"\n+\tdata-lang-empty=\"{{ lang('No entries') }}\"\n+\tdata-lang-new=\"{{ lang('new') }}\"\n+\tdata-lang-csv=\"{{ lang('download') }}\"\n+\tdata-lang-view=\"{{ lang('View') }}\"\n+\tdata-lang-edit=\"{{ lang('Edit') }}\"\n+\tdata-lang-delete=\"{{ lang('Delete') }}\"\n+\tdata-lang-add-sub=\"{{ lang('Add Sub Project') }}\"\n+\tdata-lang-search=\"{{ lang('common.search') }}\"\n+\tdata-lang-category=\"{{ lang('Category') }}\"\n+\tdata-lang-filter=\"{{ lang('Filter') }}\"\n+\tdata-lang-all=\"{{ lang('All') }}\"\n+\tdata-lang-private=\"{{ lang('Private') }}\"\n+\tdata-lang-id=\"{{ lang('ID') }}\"\n+\tdata-lang-title=\"{{ lang('Title') }}\"\n+\tdata-lang-status=\"{{ lang('Status') }}\"\n+\tdata-lang-urgency=\"{{ lang('Urgency') }}\"\n+\tdata-lang-start=\"{{ lang('start date') }}\"\n+\tdata-lang-end=\"{{ lang('end date') }}\"\n+\tdata-lang-owner=\"{{ lang('created by') }}\"\n+\tdata-lang-assigned=\"{{ lang('assigned to') }}\"\n+\tdata-lang-prev=\"{{ lang('Previous') }}\"\n+\tdata-lang-next=\"{{ lang('Next') }}\"\n+\tdata-lang-page=\"{{ lang('Page') }}\"\n+\tdata-lang-of=\"{{ lang('of') }}\"\n+\tdata-lang-showing=\"{{ lang('Showing') }}\"\n+\tdata-lang-page-size=\"{{ lang('Page size') }}\"\n+\tdata-default-page-size=\"{{ default_page_size|default(25) }}\"\n+>\n+\t<div class=\"todo-app__toolbar\">\n+\t\t<select id=\"todo-cat\" class=\"todo-app__select\">\n+\t\t\t{% for category in categories %}\n+\t\t\t\t<option value=\"{{ category.id }}\">{{ category.name }}</option>\n+\t\t\t{% endfor %}\n+\t\t</select>\n+\n+\t\t<input id=\"todo-search\" class=\"todo-app__search\" type=\"search\" placeholder=\"{{ lang('common.search') }}\" />\n+\t\t<select id=\"todo-filter\" class=\"todo-app__select\">\n+\t\t\t<option value=\"none\">{{ lang('All') }}</option>\n+\t\t\t<option value=\"private\">{{ lang('Private') }}</option>\n+\t\t</select>\n+\t\t<a id=\"todo-matrix\" class=\"todo-app__button todo-app__button--secondary\" href=\"{{ phpgw_link('/todo/view/todos/matrix', {'month': 'now'|date('m'), 'year': 'now'|date('Y')})|raw }}\">{{ lang('view matrix of actual month') }}</a>\n+\t\t<a id=\"todo-new\" class=\"todo-app__button\" href=\"{{ phpgw_link('/todo/view/todos/add') }}\">{{ lang('new') }}</a>\n+\t\t<button id=\"todo-csv\" class=\"todo-app__button todo-app__button--secondary\" type=\"button\">{{ lang('download') }}</button>\n+\t</div>\n+\n+\t<div id=\"todo-loading\" class=\"todo-app__message\">{{ lang('common.loading') }}...</div>\n+\t<div id=\"todo-error\" class=\"todo-app__message todo-app__message--error\" hidden></div>\n+\n+\t<table id=\"todo-table\" class=\"todo-app__table\" hidden>\n+\t\t<thead>\n+\t\t\t<tr>\n+\t\t\t\t<th>\n+\t\t\t\t\t<button type=\"button\" class=\"todo-app__sort\" data-sort-key=\"id\">{{ lang('ID') }}</button>\n+\t\t\t\t</th>\n+\t\t\t\t<th>\n+\t\t\t\t\t<button type=\"button\" class=\"todo-app__sort\" data-sort-key=\"title\">{{ lang('Title') }}</button>\n+\t\t\t\t</th>\n+\t\t\t\t<th>{{ lang('Status') }}</th>\n+\t\t\t\t<th>\n+\t\t\t\t\t<button type=\"button\" class=\"todo-app__sort\" data-sort-key=\"priority\">{{ lang('Urgency') }}</button>\n+\t\t\t\t</th>\n+\t\t\t\t<th>\n+\t\t\t\t\t<button type=\"button\" class=\"todo-app__sort\" data-sort-key=\"created\">{{ lang('start date') }}</button>\n+\t\t\t\t</th>\n+\t\t\t\t<th>\n+\t\t\t\t\t<button type=\"button\" class=\"todo-app__sort\" data-sort-key=\"due\">{{ lang('end date') }}</button>\n+\t\t\t\t</th>\n+\t\t\t\t<th>{{ lang('created by') }}</th>\n+\t\t\t\t<th>{{ lang('assigned to') }}</th>\n+\t\t\t\t<th>{{ lang('View') }}</th>\n+\t\t\t\t<th>{{ lang('Edit') }}</th>\n+\t\t\t\t<th>{{ lang('Delete') }}</th>\n+\t\t\t\t<th>{{ lang('Add Sub Project') }}</th>\n+\t\t\t</tr>\n+\t\t</thead>\n+\t\t<tbody id=\"todo-table-body\"></tbody>\n+\t</table>\n+\n+\t<div id=\"todo-pagination\" class=\"todo-app__pagination\" hidden>\n+\t\t<div id=\"todo-pagination-summary\" class=\"todo-app__pagination-summary\"></div>\n+\t\t<div class=\"todo-app__pagination-controls\">\n+\t\t\t<label class=\"todo-app__pagination-label\" for=\"todo-page-size\">{{ lang('Page size') }}</label>\n+\t\t\t<select id=\"todo-page-size\" class=\"todo-app__select todo-app__select--compact\">\n+\t\t\t\t{% for option in page_size_options|default([default_page_size|default(25)]) %}\n+\t\t\t\t\t<option value=\"{{ option }}\">{{ option }}</option>\n+\t\t\t\t{% endfor %}\n+\t\t\t</select>\n+\t\t\t<button id=\"todo-page-prev\" class=\"todo-app__button todo-app__button--secondary\" type=\"button\">{{ lang('Previous') }}</button>\n+\t\t\t<span id=\"todo-page-status\" class=\"todo-app__pagination-status\"></span>\n+\t\t\t<button id=\"todo-page-next\" class=\"todo-app__button todo-app__button--secondary\" type=\"button\">{{ lang('Next') }}</button>\n+\t\t</div>\n+\t</div>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}\n+<script>{{ source('@views/todo/index/todo_index.js') }}</script>\n+{% endblock %}"},{"sha":"9acc9c89a8be5cc2105c15c11cf9dec1a801622b","filename":"src/modules/todo/html/todo/matrix/todo_matrix.css","status":"added","additions":279,"deletions":0,"changes":279,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.css","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.css","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.css?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,279 @@\n+.todo-matrix {\n+\tpadding: 1rem;\n+\tmax-width: 100%;\n+}\n+\n+.todo-matrix__toolbar {\n+\tmargin-bottom: 0.75rem;\n+\tdisplay: flex;\n+\tjustify-content: flex-end;\n+}\n+\n+.todo-matrix__back {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tpadding: 0.45rem 0.8rem;\n+\tborder: 1px solid #20568f;\n+\tborder-radius: 4px;\n+\tbackground: #fff;\n+\tcolor: #20568f;\n+\ttext-decoration: none;\n+}\n+\n+.todo-matrix__back:hover {\n+\tbackground: #eef5fc;\n+}\n+\n+.todo-matrix__content {\n+\tpadding: 1rem;\n+\tborder: 1px solid #d8e2ee;\n+\tborder-radius: 12px;\n+\tbackground: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);\n+\tbox-shadow: 0 10px 30px rgba(32, 86, 143, 0.08);\n+\toverflow: hidden;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview {\n+\tdisplay: grid;\n+\tgap: 1rem;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__headerbar {\n+\tdisplay: flex;\n+\tflex-wrap: wrap;\n+\tjustify-content: space-between;\n+\talign-items: center;\n+\tgap: 0.75rem;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__title {\n+\tmargin: 0;\n+\tfont-size: 1.35rem;\n+\tcolor: #12385f;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__controls {\n+\tdisplay: flex;\n+\tflex-wrap: wrap;\n+\tgap: 0.5rem;\n+\talign-items: center;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__select,\n+.todo-matrix__content .phpgw-matrixview__submit {\n+\tpadding: 0.5rem 0.75rem;\n+\tborder: 1px solid #c8d5e4;\n+\tborder-radius: 8px;\n+\tfont: inherit;\n+\tbackground: #fff;\n+\tcolor: #163f68;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__submit {\n+\tborder-color: #20568f;\n+\tbackground: #2a72ba;\n+\tcolor: #fff;\n+\tcursor: pointer;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__submit:hover {\n+\tbackground: #225f9c;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__table-wrap {\n+\toverflow-x: auto;\n+\tpadding-bottom: 0.25rem;\n+}\n+\n+\n+.todo-matrix__content .phpgw-matrixview__table {\n+\tmin-width: max-content;\n+\twidth: 100%;\n+\tborder-collapse: collapse;\n+\tborder-spacing: 0;\n+}\n+\n+\n+.todo-matrix__content .phpgw-matrixview__header,\n+.todo-matrix__content .phpgw-matrixview__item,\n+.todo-matrix__content .phpgw-matrixview__day,\n+.todo-matrix__content .phpgw-matrixview__empty {\n+\tpadding: 0.45rem 0.5rem;\n+\tvertical-align: top;\n+\tborder-bottom: 1px solid #edf2f7;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__head .phpgw-matrixview__header {\n+\tposition: sticky;\n+\ttop: 0;\n+\tbackground: #eef5fc;\n+\tcolor: #153d66;\n+\tfont-weight: 700;\n+\ttext-align: center;\n+\tz-index: 1;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__header--title,\n+.todo-matrix__content .phpgw-matrixview__item {\n+\tposition: sticky;\n+\tleft: 0;\n+\ttext-align: left;\n+\tbackground: #fff;\n+\tz-index: 2;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__head .phpgw-matrixview__header--title {\n+\tbackground: #eef5fc;\n+\tz-index: 3;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__item {\n+\tmin-width: 240px;\n+\tmax-width: 320px;\n+\tfont-weight: 600;\n+\tcolor: #183e66;\n+\tword-break: break-word;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__item a {\n+\tcolor: #20568f;\n+\ttext-decoration: none;\n+}\n+\n+.todo-matrix__content .todo-matrix__node {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tgap: 0.35rem;\n+\tline-height: 1.2;\n+\tpadding: 0.08rem 0.35rem;\n+\tborder-radius: 4px;\n+}\n+\n+.todo-matrix__content .todo-matrix__node-prefix {\n+\tcolor: #5b6f84;\n+\tfont-family: monospace;\n+\twhite-space: nowrap;\n+\tfont-weight: 400;\n+}\n+\n+.todo-matrix__content .todo-matrix__node--level-0 {\n+\tfont-weight: 700;\n+}\n+\n+.todo-matrix__content .todo-matrix__node--level-1,\n+.todo-matrix__content .todo-matrix__node--level-2,\n+.todo-matrix__content .todo-matrix__node--level-3,\n+.todo-matrix__content .todo-matrix__node--level-4,\n+.todo-matrix__content .todo-matrix__node--level-5,\n+.todo-matrix__content .todo-matrix__node--level-6 {\n+\tfont-weight: 600;\n+}\n+\n+.todo-matrix__content .todo-matrix__node--band-a {\n+\tbackground: #f4f8fc;\n+\tbox-shadow: inset 3px 0 0 #d5e3f2;\n+}\n+\n+.todo-matrix__content .todo-matrix__node--band-b {\n+\tbackground: #f9fbfe;\n+\tbox-shadow: inset 3px 0 0 #e3ecf6;\n+}\n+\n+.todo-matrix__content .todo-matrix__status {\n+\tdisplay: inline-flex;\n+\talign-items: center;\n+\tgap: 0.3rem;\n+\tmargin-left: 0.1rem;\n+\tfont-size: 0.76rem;\n+\tcolor: #44617e;\n+\tborder: 1px solid #c7d7e7;\n+\tbackground: #ffffff;\n+\tpadding: 0.12rem 0.3rem;\n+\tborder-radius: 999px;\n+\tcursor: pointer;\n+}\n+\n+.todo-matrix__content .todo-matrix__status:hover {\n+\tbackground: #f1f6fc;\n+\tborder-color: #9fb9d4;\n+}\n+\n+.todo-matrix__content .todo-matrix__status:focus {\n+\toutline: 2px solid #2a72ba;\n+\toutline-offset: 1px;\n+}\n+\n+.todo-matrix__content .todo-matrix__status:disabled {\n+\topacity: 0.65;\n+\tcursor: wait;\n+}\n+\n+.todo-matrix__content .todo-matrix__status-value {\n+\tmin-width: 2.6rem;\n+\ttext-align: right;\n+\tfont-variant-numeric: tabular-nums;\n+}\n+\n+.todo-matrix__content .todo-matrix__status-bar {\n+\tposition: relative;\n+\tdisplay: inline-block;\n+\twidth: 3.1rem;\n+\theight: 0.34rem;\n+\tborder-radius: 999px;\n+\tbackground: #d7e4f2;\n+\toverflow: hidden;\n+}\n+\n+.todo-matrix__content .todo-matrix__status-fill {\n+\tdisplay: block;\n+\theight: 100%;\n+\tbackground: linear-gradient(90deg, #4e9c6f, #2d7f52);\n+\tborder-radius: 999px;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__item a:hover {\n+\ttext-decoration: underline;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__header--day,\n+.todo-matrix__content .phpgw-matrixview__day {\n+\tmin-width: 2rem;\n+\ttext-align: center;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__day {\n+\theight: 2rem;\n+\tpadding: 0;\n+\tborder: 1px solid #fff;\n+\tbackground-clip: padding-box;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__day--active {\n+\tbox-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__day--empty {\n+\topacity: 0.5;\n+\tbox-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__foot {\n+\tdisplay: none;\n+}\n+\n+.todo-matrix__content .phpgw-matrixview__empty {\n+\tpadding: 1rem;\n+\ttext-align: center;\n+\tcolor: #5f7287;\n+\tbackground: #fbfdff;\n+}\n+\n+@media (max-width: 900px) {\n+\t.todo-matrix__content {\n+\t\tpadding: 0.75rem;\n+\t}\n+\n+\t.todo-matrix__content .phpgw-matrixview__item {\n+\t\tmin-width: 180px;\n+\t}\n+}"},{"sha":"31bdb2fdc05be5f247b26895a6d08dabc1295b3f","filename":"src/modules/todo/html/todo/matrix/todo_matrix.js","status":"added","additions":175,"deletions":0,"changes":175,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.js","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.js","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.js?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,175 @@\n+(function () {\n+\tvar root = document.querySelector('.todo-matrix');\n+\tif (!root) {\n+\t\treturn;\n+\t}\n+\n+\tvar statusApiTemplate = root.dataset.statusApiTemplate || '';\n+\tif (!statusApiTemplate || statusApiTemplate.indexOf('__TODO_ID__') === -1) {\n+\t\treturn;\n+\t}\n+\n+\tvar langCompleted = root.dataset.langCompleted || 'Completed';\n+\tvar langClickToEdit = root.dataset.langClickToEdit || 'Click to edit';\n+\tvar langEnterCompleted = root.dataset.langEnterCompleted || 'Enter completed percentage (0-100)';\n+\tvar langInvalidStatus = root.dataset.langInvalidStatus || 'Status must be a number between 0 and 100';\n+\tvar langSaving = root.dataset.langSaving || 'Saving...';\n+\tvar langUpdateFailed = root.dataset.langUpdateFailed || 'Failed to update status';\n+\tvar csrfNameKey = root.dataset.csrfNameKey || '';\n+\tvar csrfValueKey = root.dataset.csrfValueKey || '';\n+\tvar csrfName = root.dataset.csrfName || '';\n+\tvar csrfValue = root.dataset.csrfValue || '';\n+\n+\tfunction appendHiddenCsrfInput(form, name, value) {\n+\t\tif (!name || !value) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar input = form.querySelector('input[name=\"' + name + '\"]');\n+\t\tif (!input) {\n+\t\t\tinput = document.createElement('input');\n+\t\t\tinput.type = 'hidden';\n+\t\t\tinput.name = name;\n+\t\t\tform.appendChild(input);\n+\t\t}\n+\t\tinput.value = value;\n+\t}\n+\n+\tvar filterForm = root.querySelector('.phpgw-matrixview__filters');\n+\tif (filterForm) {\n+\t\tappendHiddenCsrfInput(filterForm, csrfNameKey, csrfName);\n+\t\tappendHiddenCsrfInput(filterForm, csrfValueKey, csrfValue);\n+\n+\t\tfilterForm.addEventListener('change', function (event) {\n+\t\t\tvar target = event.target;\n+\t\t\tif (!target || !target.matches('.phpgw-matrixview__select')) {\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tif (typeof filterForm.requestSubmit === 'function') {\n+\t\t\t\tfilterForm.requestSubmit();\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tfilterForm.submit();\n+\t\t});\n+\t}\n+\n+\tfunction clampStatus(value) {\n+\t\tvar normalized = String(value == null ? '' : value).trim();\n+\t\tif (!/^\\d+$/.test(normalized)) {\n+\t\t\treturn null;\n+\t\t}\n+\n+\t\tvar parsed = Number(normalized);\n+\t\tif (!Number.isInteger(parsed)) {\n+\t\t\treturn null;\n+\t\t}\n+\t\tif (parsed < 0 || parsed > 100) {\n+\t\t\treturn null;\n+\t\t}\n+\t\treturn parsed;\n+\t}\n+\n+\tfunction setStatusVisual(button, status) {\n+\t\tbutton.dataset.status = String(status);\n+\t\tbutton.title = langCompleted + ': ' + status + '% - ' + langClickToEdit;\n+\n+\t\tvar valueEl = button.querySelector('.todo-matrix__status-value');\n+\t\tif (valueEl) {\n+\t\t\tvalueEl.textContent = status + '%';\n+\t\t}\n+\n+\t\tvar fillEl = button.querySelector('.todo-matrix__status-fill');\n+\t\tif (fillEl) {\n+\t\t\tfillEl.style.width = status + '%';\n+\t\t}\n+\t}\n+\n+\tfunction buildStatusUrl(todoId) {\n+\t\treturn statusApiTemplate.replace('__TODO_ID__', encodeURIComponent(String(todoId)));\n+\t}\n+\n+\tfunction updateStatus(button, todoId, status) {\n+\t\tvar headers = {\n+\t\t\t'Content-Type': 'application/json',\n+\t\t\t'Accept': 'application/json'\n+\t\t};\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\theaders[csrfNameKey] = csrfName;\n+\t\t\theaders[csrfValueKey] = csrfValue;\n+\t\t\theaders['X-CSRF-NAME'] = csrfName;\n+\t\t\theaders['X-CSRF-VALUE'] = csrfValue;\n+\t\t}\n+\n+\t\tvar payload = { status: status };\n+\t\tif (csrfNameKey && csrfValueKey && csrfName && csrfValue) {\n+\t\t\tpayload[csrfNameKey] = csrfName;\n+\t\t\tpayload[csrfValueKey] = csrfValue;\n+\t\t}\n+\n+\t\treturn fetch(buildStatusUrl(todoId), {\n+\t\t\tmethod: 'PATCH',\n+\t\t\theaders: headers,\n+\t\t\tcredentials: 'same-origin',\n+\t\t\tbody: JSON.stringify(payload)\n+\t\t}).then(function (response) {\n+\t\t\treturn response.json().catch(function () { return {}; }).then(function (data) {\n+\t\t\t\tif (!response.ok) {\n+\t\t\t\t\tthrow new Error(data.error || ('HTTP ' + response.status));\n+\t\t\t\t}\n+\t\t\t\treturn data;\n+\t\t\t});\n+\t\t});\n+\t}\n+\n+\troot.addEventListener('click', function (event) {\n+\t\tvar button = event.target.closest('.todo-matrix__status[data-todo-id]');\n+\t\tif (!button) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tevent.preventDefault();\n+\t\tif (button.dataset.busy === '1') {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar todoId = parseInt(button.dataset.todoId || '0', 10);\n+\t\tif (!todoId) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar currentStatus = parseInt(button.dataset.status || '0', 10) || 0;\n+\t\tvar entered = window.prompt(langEnterCompleted, String(currentStatus));\n+\t\tif (entered === null) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar nextStatus = clampStatus(entered);\n+\t\tif (nextStatus === null) {\n+\t\t\twindow.alert(langInvalidStatus);\n+\t\t\treturn;\n+\t\t}\n+\t\tif (nextStatus === currentStatus) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tbutton.dataset.busy = '1';\n+\t\tbutton.disabled = true;\n+\t\tvar previousTitle = button.title;\n+\t\tbutton.title = langSaving;\n+\n+\t\tupdateStatus(button, todoId, nextStatus)\n+\t\t\t.then(function () {\n+\t\t\t\tsetStatusVisual(button, nextStatus);\n+\t\t\t})\n+\t\t\t.catch(function (error) {\n+\t\t\t\twindow.alert((error && error.message) ? error.message : langUpdateFailed);\n+\t\t\t\tbutton.title = previousTitle;\n+\t\t\t})\n+\t\t\t.finally(function () {\n+\t\t\t\tbutton.disabled = false;\n+\t\t\t\tbutton.dataset.busy = '0';\n+\t\t\t});\n+\t});\n+})();"},{"sha":"e06632052ecf4312b13867632b4aa1b4401207b5","filename":"src/modules/todo/html/todo/matrix/todo_matrix.twig","status":"added","additions":34,"deletions":0,"changes":34,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fmatrix%2Ftodo_matrix.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,34 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('Matrix') }} {{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>{{ source('@views/todo/matrix/todo_matrix.css') }}</style>\n+{% endblock %}\n+\n+{% block content %}\n+<div class=\"todo-matrix\"\n+\tdata-status-api-template=\"{{ phpgw_link('/todo/todos/__TODO_ID__/status') }}\"\n+\tdata-lang-completed=\"{{ lang('Completed') }}\"\n+\tdata-lang-click-to-edit=\"{{ lang('click to edit') }}\"\n+\tdata-lang-enter-completed=\"{{ lang('Enter completed percentage (0-100)') }}\"\n+\tdata-lang-invalid-status=\"{{ lang('Status must be a number between 0 and 100') }}\"\n+\tdata-lang-saving=\"{{ lang('common.loading') }}\"\n+\tdata-lang-update-failed=\"{{ lang('Failed to update status') }}\"\n+\tdata-csrf-name-key=\"{{ csrf.name_key|default('') }}\"\n+\tdata-csrf-value-key=\"{{ csrf.value_key|default('') }}\"\n+\tdata-csrf-name=\"{{ csrf.name|default('') }}\"\n+\tdata-csrf-value=\"{{ csrf.value|default('') }}\"\n+>\n+\t<div class=\"todo-matrix__toolbar\">\n+\t\t<a class=\"todo-matrix__back\" href=\"{{ phpgw_link('/todo/view/todos') }}\">{{ lang('Done') }}</a>\n+\t</div>\n+\t<div class=\"todo-matrix__content\">\n+\t\t{{ matrix_html|raw }}\n+\t</div>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}\n+<script>{{ source('@views/todo/matrix/todo_matrix.js') }}</script>\n+{% endblock %}"},{"sha":"1c53b711de16f5e80e3548c82c4e4bfde27b8ced","filename":"src/modules/todo/html/todo/view/todo_view.css","status":"added","additions":118,"deletions":0,"changes":118,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.css","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.css","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.css?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,118 @@\n+.todo-view {\n+\tpadding: 1rem;\n+\tmax-width: 980px;\n+}\n+\n+.todo-view__title {\n+\tmargin: 0 0 1rem;\n+}\n+\n+.todo-view__message {\n+\tmargin-bottom: 1rem;\n+}\n+\n+.todo-view__message--error {\n+\tpadding: 0.75rem;\n+\tborder: 1px solid #d38e8e;\n+\tbackground: #fdeaea;\n+\tcolor: #7b1f1f;\n+}\n+\n+.todo-view__grid {\n+\tdisplay: grid;\n+\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n+\tgap: 0.9rem 1rem;\n+}\n+\n+.todo-view__tabs {\n+\tdisplay: flex;\n+\tgap: 0.5rem;\n+\tmargin: 0 0 1rem;\n+\tborder-bottom: 1px solid #cbd7e3;\n+\tpadding-bottom: 0.5rem;\n+}\n+\n+.todo-view__tab {\n+\tappearance: none;\n+\tborder: 1px solid #9fb7cf;\n+\tborder-bottom: 0;\n+\tborder-radius: 6px 6px 0 0;\n+\tbackground: #eef4fa;\n+\tcolor: #184a7a;\n+\tpadding: 0.55rem 0.9rem;\n+\tfont: inherit;\n+\tcursor: pointer;\n+}\n+\n+.todo-view__tab--active {\n+\tbackground: #fff;\n+\tcolor: #102c4a;\n+\tborder-color: #cbd7e3;\n+\tposition: relative;\n+\tbottom: -1px;\n+}\n+\n+.todo-view__panel {\n+\tpadding-top: 0.25rem;\n+}\n+\n+.todo-view__history {\n+\toverflow-x: auto;\n+}\n+\n+.todo-view__history table {\n+\twidth: 100%;\n+\tborder-collapse: collapse;\n+}\n+\n+.todo-view__history th,\n+.todo-view__history td {\n+\tpadding: 0.5rem 0.6rem;\n+\tvertical-align: top;\n+\ttext-align: left;\n+\tborder-bottom: 1px solid #dde6ef;\n+}\n+\n+.todo-view__history th {\n+\tfont-weight: 600;\n+}\n+\n+.todo-view__grid dt {\n+\tfont-weight: 600;\n+\tmargin-bottom: 0.15rem;\n+}\n+\n+.todo-view__grid dd {\n+\tmargin: 0;\n+\tword-break: break-word;\n+}\n+\n+.todo-view__full {\n+\tgrid-column: 1 / -1;\n+}\n+\n+.todo-view__actions {\n+\tmargin-top: 1rem;\n+\tdisplay: flex;\n+\tgap: 0.75rem;\n+}\n+\n+.todo-view__button {\n+\tpadding: 0.5rem 0.9rem;\n+\tborder: 1px solid #20568f;\n+\tborder-radius: 4px;\n+\tbackground: #2a72ba;\n+\tcolor: #fff;\n+\ttext-decoration: none;\n+}\n+\n+.todo-view__button--secondary {\n+\tbackground: #fff;\n+\tcolor: #20568f;\n+}\n+\n+.todo-view__button--danger {\n+\tbackground: #a83939;\n+\tborder-color: #8f2e2e;\n+\tcolor: #fff;\n+}"},{"sha":"7eca71a6c83fcced6d97bd9ebec18092b2fa6ee0","filename":"src/modules/todo/html/todo/view/todo_view.js","status":"added","additions":210,"deletions":0,"changes":210,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.js","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.js","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.js?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,210 @@\n+(function () {\n+\tvar root = document.getElementById('todo-view-app');\n+\tif (!root) {\n+\t\treturn;\n+\t}\n+\n+\tvar loadingEl = document.getElementById('todo-view-loading');\n+\tvar errorEl = document.getElementById('todo-view-error');\n+\tvar contentEl = document.getElementById('todo-view-content');\n+\tvar detailsTabEl = document.getElementById('todo-view-tab-details');\n+\tvar historyTabEl = document.getElementById('todo-view-tab-history');\n+\tvar detailsPanelEl = document.getElementById('todo-view-panel-details');\n+\tvar historyPanelEl = document.getElementById('todo-view-panel-history');\n+\tvar historyEl = document.getElementById('todo-view-history');\n+\tvar editLink = document.getElementById('todo-view-edit');\n+\tvar deleteLink = document.getElementById('todo-view-delete');\n+\tvar apiUrl = root.dataset.apiUrl;\n+\tvar editUrlBase = root.dataset.editUrlBase || '/todo/view/todos';\n+\tvar editUrl = root.dataset.editUrl || '';\n+\tvar deleteUrl = root.dataset.deleteUrl || '';\n+\tvar historyLabelUser = root.dataset.langHistoryUser || 'User';\n+\tvar historyLabelDate = root.dataset.langHistoryDate || 'Date';\n+\tvar historyLabelAction = root.dataset.langHistoryAction || 'Action';\n+\tvar historyLabelNewValue = root.dataset.langHistoryNewValue || 'New Value';\n+\tvar historyLabelOldValue = root.dataset.langHistoryOldValue || 'Old Value';\n+\tvar historyLabelEmpty = root.dataset.langHistoryEmpty || 'No history for this record';\n+\n+\tfunction setActiveTab(tabName) {\n+\t\tvar showHistory = tabName === 'history';\n+\n+\t\tif (detailsTabEl) {\n+\t\t\tdetailsTabEl.classList.toggle('todo-view__tab--active', !showHistory);\n+\t\t\tdetailsTabEl.setAttribute('aria-selected', showHistory ? 'false' : 'true');\n+\t\t}\n+\n+\t\tif (historyTabEl) {\n+\t\t\thistoryTabEl.classList.toggle('todo-view__tab--active', showHistory);\n+\t\t\thistoryTabEl.setAttribute('aria-selected', showHistory ? 'true' : 'false');\n+\t\t}\n+\n+\t\tif (detailsPanelEl) {\n+\t\t\tdetailsPanelEl.hidden = showHistory;\n+\t\t}\n+\n+\t\tif (historyPanelEl) {\n+\t\t\thistoryPanelEl.hidden = !showHistory;\n+\t\t}\n+\t}\n+\n+\tfunction escapeHtml(value) {\n+\t\treturn String(value == null ? '' : value)\n+\t\t\t.replace(/&/g, '&amp;')\n+\t\t\t.replace(/</g, '&lt;')\n+\t\t\t.replace(/>/g, '&gt;')\n+\t\t\t.replace(/\\\"/g, '&quot;')\n+\t\t\t.replace(/'/g, '&#039;');\n+\t}\n+\n+\tfunction renderMultiline(value) {\n+\t\tvar normalized = String(value == null ? '' : value)\n+\t\t\t.replace(/<br\\s*\\/?>/gi, '\\n')\n+\t\t\t.replace(/\\r\\n/g, '\\n')\n+\t\t\t.replace(/\\r/g, '\\n');\n+\n+\t\treturn escapeHtml(normalized).replace(/\\n/g, '<br>');\n+\t}\n+\n+\tfunction renderHistoryTable(historyRows) {\n+\t\tif (!historyEl) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar rows = Array.isArray(historyRows) ? historyRows : [];\n+\t\tif (!rows.length) {\n+\t\t\thistoryEl.innerHTML = '<p>' + escapeHtml(historyLabelEmpty) + '</p>';\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar html = ''\n+\t\t\t+ '<table>'\n+\t\t\t+ '<thead><tr>'\n+\t\t\t+ '<th>' + escapeHtml(historyLabelDate) + '</th>'\n+\t\t\t+ '<th>' + escapeHtml(historyLabelUser) + '</th>'\n+\t\t\t+ '<th>' + escapeHtml(historyLabelAction) + '</th>'\n+\t\t\t+ '<th>' + escapeHtml(historyLabelNewValue) + '</th>'\n+\t\t\t+ '<th>' + escapeHtml(historyLabelOldValue) + '</th>'\n+\t\t\t+ '</tr></thead><tbody>';\n+\n+\t\trows.forEach(function (row) {\n+\t\t\thtml += '<tr>'\n+\t\t\t\t+ '<td>' + escapeHtml(row && row.datetime != null ? row.datetime : '') + '</td>'\n+\t\t\t\t+ '<td>' + escapeHtml(row && row.owner != null ? row.owner : '') + '</td>'\n+\t\t\t\t+ '<td>' + escapeHtml(row && row.status_label != null ? row.status_label : (row && row.status != null ? row.status : '')) + '</td>'\n+\t\t\t\t+ '<td>' + renderMultiline(row && row.new_value != null ? row.new_value : '') + '</td>'\n+\t\t\t\t+ '<td>' + renderMultiline(row && row.old_value != null ? row.old_value : '') + '</td>'\n+\t\t\t\t+ '</tr>';\n+\t\t});\n+\n+\t\thtml += '</tbody></table>';\n+\t\thistoryEl.innerHTML = html;\n+\t}\n+\n+\tfunction renderAssigned(detail) {\n+\t\tvar assignedEl = document.getElementById('todo-view-assigned');\n+\t\tif (!assignedEl) {\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tvar entries = Array.isArray(detail && detail.assigned_entries) ? detail.assigned_entries : [];\n+\t\tif (entries.length) {\n+\t\t\tvar lines = entries\n+\t\t\t\t.map(function (entry) {\n+\t\t\t\t\treturn entry && entry.name != null ? String(entry.name) : '';\n+\t\t\t\t})\n+\t\t\t\t.filter(function (line) {\n+\t\t\t\t\treturn line !== '';\n+\t\t\t\t});\n+\n+\t\t\tif (lines.length) {\n+\t\t\t\tassignedEl.innerHTML = lines.map(function (line) { return escapeHtml(line); }).join('<br>');\n+\t\t\t\treturn;\n+\t\t\t}\n+\t\t}\n+\n+\t\tassignedEl.innerHTML = '-';\n+\t}\n+\n+\tfunction setText(id, value) {\n+\t\tvar el = document.getElementById(id);\n+\t\tif (!el) {\n+\t\t\treturn;\n+\t\t}\n+\t\tel.textContent = value == null || value === '' ? '-' : String(value);\n+\t}\n+\n+\tfunction showError(message) {\n+\t\tloadingEl.hidden = true;\n+\t\tcontentEl.hidden = true;\n+\t\terrorEl.hidden = false;\n+\t\terrorEl.textContent = message || root.dataset.langError || 'Error';\n+\t}\n+\n+\tfunction showContent(detail) {\n+\t\tloadingEl.hidden = true;\n+\t\terrorEl.hidden = true;\n+\t\tcontentEl.hidden = false;\n+\n+\t\tsetText('todo-view-title', detail.title);\n+\t\tsetText('todo-view-category', detail.category);\n+\t\tsetText('todo-view-descr', detail.descr);\n+\t\tsetText('todo-view-parent', detail.parent);\n+\t\tsetText('todo-view-status', detail.status + '%');\n+\t\tsetText('todo-view-pri', detail.pri);\n+\t\tsetText('todo-view-sdate', detail.sdate);\n+\t\tsetText('todo-view-edate', detail.edate);\n+\t\tsetText('todo-view-access', detail.access);\n+\t\tsetText('todo-view-owner', detail.owner);\n+\t\trenderAssigned(detail);\n+\n+\t\tif (editLink) {\n+\t\t\tif (editUrl) {\n+\t\t\t\teditLink.href = editUrl;\n+\t\t\t} else if (detail.id) {\n+\t\t\t\teditLink.href = editUrlBase + '/' + encodeURIComponent(detail.id) + '/edit';\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (deleteLink) {\n+\t\t\tif (deleteUrl) {\n+\t\t\t\tdeleteLink.href = deleteUrl;\n+\t\t\t} else if (detail.id) {\n+\t\t\t\tdeleteLink.href = editUrlBase + '/' + encodeURIComponent(detail.id) + '/delete';\n+\t\t\t}\n+\t\t}\n+\n+\t\tsetActiveTab('details');\n+\t}\n+\n+\tif (detailsTabEl) {\n+\t\tdetailsTabEl.addEventListener('click', function () {\n+\t\t\tsetActiveTab('details');\n+\t\t});\n+\t}\n+\n+\tif (historyTabEl) {\n+\t\thistoryTabEl.addEventListener('click', function () {\n+\t\t\tsetActiveTab('history');\n+\t\t});\n+\t}\n+\n+\tfetch(apiUrl, { credentials: 'same-origin' })\n+\t\t.then(function (response) {\n+\t\t\tif (!response.ok) {\n+\t\t\t\tthrow new Error('HTTP ' + response.status);\n+\t\t\t}\n+\t\t\treturn response.json();\n+\t\t})\n+\t\t.then(function (payload) {\n+\t\t\tif (!payload || !payload.detail) {\n+\t\t\t\tthrow new Error(root.dataset.langError || 'Error');\n+\t\t\t}\n+\n+\t\t\trenderHistoryTable(payload.history || []);\n+\n+\t\t\tshowContent(payload.detail);\n+\t\t})\n+\t\t.catch(function (error) {\n+\t\t\tshowError(error.message);\n+\t\t});\n+})();"},{"sha":"490c40a32a0992b2dd8ae41ff5a32562fbeb6d20","filename":"src/modules/todo/html/todo/view/todo_view.twig","status":"added","additions":67,"deletions":0,"changes":67,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.twig","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.twig","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fhtml%2Ftodo%2Fview%2Ftodo_view.twig?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,67 @@\n+{% extends layout|default('@views/_bare.twig') %}\n+\n+{% block title %}{{ lang('View') }} {{ lang('todo') }}{% endblock %}\n+\n+{% block styles %}\n+<style>{{ source('@views/todo/view/todo_view.css') }}</style>\n+{% endblock %}\n+\n+{% block content %}\n+<div id=\"todo-view-app\"\n+\tclass=\"todo-view\"\n+\tdata-api-url=\"{{ phpgw_link('/todo/todos/' ~ todo_id) }}\"\n+\tdata-edit-url-base=\"{{ phpgw_link('/todo/view/todos') }}\"\n+\tdata-edit-url=\"{{ phpgw_link('/todo/view/todos/' ~ todo_id ~ '/edit') }}\"\n+\tdata-delete-url=\"{{ phpgw_link('/todo/view/todos/' ~ todo_id ~ '/delete') }}\"\n+\tdata-back-url=\"{{ phpgw_link('/todo/view/todos') }}\"\n+\tdata-lang-loading=\"{{ lang('common.loading') }}\"\n+\tdata-lang-error=\"{{ lang('common.error') }}\"\n+\tdata-lang-history-user=\"{{ lang('User') }}\"\n+\tdata-lang-history-date=\"{{ lang('Date') }}\"\n+\tdata-lang-history-action=\"{{ lang('Action') }}\"\n+\tdata-lang-history-new-value=\"{{ lang('New Value') }}\"\n+\tdata-lang-history-old-value=\"{{ lang('Old Value') }}\"\n+\tdata-lang-history-empty=\"{{ lang('No history for this record') }}\"\n+>\n+\t<h2 class=\"todo-view__title\">{{ lang('View') }} {{ lang('todo') }}</h2>\n+\t<div id=\"todo-view-loading\" class=\"todo-view__message\">{{ lang('common.loading') }}...</div>\n+\t<div id=\"todo-view-error\" class=\"todo-view__message todo-view__message--error\" hidden></div>\n+\n+\t<div id=\"todo-view-content\" hidden>\n+\t\t<div class=\"todo-view__tabs\" role=\"tablist\" aria-label=\"{{ lang('todo') }}\">\n+\t\t\t<button id=\"todo-view-tab-details\" class=\"todo-view__tab todo-view__tab--active\" type=\"button\" role=\"tab\" aria-selected=\"true\" aria-controls=\"todo-view-panel-details\">{{ lang('Details') }}</button>\n+\t\t\t<button id=\"todo-view-tab-history\" class=\"todo-view__tab\" type=\"button\" role=\"tab\" aria-selected=\"false\" aria-controls=\"todo-view-panel-history\">{{ lang('History') }}</button>\n+\t\t</div>\n+\n+\t\t<div id=\"todo-view-panel-details\" class=\"todo-view__panel\" role=\"tabpanel\">\n+\t\t\t<dl class=\"todo-view__grid\">\n+\t\t\t\t<div><dt>{{ lang('Title') }}</dt><dd id=\"todo-view-title\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Category') }}</dt><dd id=\"todo-view-category\"></dd></div>\n+\t\t\t\t<div class=\"todo-view__full\"><dt>{{ lang('Description') }}</dt><dd id=\"todo-view-descr\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Parent project') }}</dt><dd id=\"todo-view-parent\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Completed') }}</dt><dd id=\"todo-view-status\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Urgency') }}</dt><dd id=\"todo-view-pri\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Start Date') }}</dt><dd id=\"todo-view-sdate\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('End Date') }}</dt><dd id=\"todo-view-edate\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Access') }}</dt><dd id=\"todo-view-access\"></dd></div>\n+\t\t\t\t<div><dt>{{ lang('Created by') }}</dt><dd id=\"todo-view-owner\"></dd></div>\n+\t\t\t\t<div class=\"todo-view__full\"><dt>{{ lang('assigned to') }}</dt><dd id=\"todo-view-assigned\"></dd></div>\n+\t\t\t</dl>\n+\t\t</div>\n+\n+\t\t<div id=\"todo-view-panel-history\" class=\"todo-view__panel\" role=\"tabpanel\" hidden>\n+\t\t\t<div id=\"todo-view-history\" class=\"todo-view__history\"></div>\n+\t\t</div>\n+\n+\t\t<div class=\"todo-view__actions\">\n+\t\t\t<a class=\"todo-view__button\" id=\"todo-view-edit\" href=\"{{ phpgw_link('/todo/view/todos/' ~ todo_id ~ '/edit') }}\">{{ lang('Edit') }}</a>\n+\t\t\t<a class=\"todo-view__button todo-view__button--danger\" id=\"todo-view-delete\" href=\"{{ phpgw_link('/todo/view/todos/' ~ todo_id ~ '/delete') }}\">{{ lang('Delete') }}</a>\n+\t\t\t<a class=\"todo-view__button todo-view__button--secondary\" href=\"{{ phpgw_link('/todo/view/todos') }}\">{{ lang('Done') }}</a>\n+\t\t</div>\n+\t</div>\n+</div>\n+{% endblock %}\n+\n+{% block scripts %}\n+<script>{{ source('@views/todo/view/todo_view.js') }}</script>\n+{% endblock %}"},{"sha":"5f48d3dfa0d237a7a00006db9aa60cbb6ca634ff","filename":"src/modules/todo/inc/class.bopreferences.inc.php","status":"removed","additions":0,"deletions":59,"changes":59,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Finc%2Fclass.bopreferences.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Finc%2Fclass.bopreferences.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fclass.bopreferences.inc.php?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,59 +0,0 @@\n-<?php\n-\n-/**\n- * Todo preferences\n- *\n- * @author Craig Knudsen <cknudsen@radix.net>\n- * @author Mark Peters <skeeter@phpgroupware.org>\n- * @copyright Copyright (C) Craig Knudsen <cknudsen@radix.net>\n- * @copyright Copyright (C) 2002,2005 Free Software Foundation, Inc. http://www.fsf.org/\n- * @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n- * @package todo\n- * @version $Id$\n- * @internal Based on Webcalendar by Craig Knudsen http://www.radix.net/~cknudsen\n- */\n-\n-use App\\modules\\phpgwapi\\services\\Settings;\n-\n-/**\n- * Todo preferences\n- *  \n- * @package todo\n- */\n-class todo_bopreferences\n-{\n-\tvar $public_functions = array(\n-\t\t'preferences'  => True\n-\t);\n-\n-\tvar $prefs;\n-\tvar $debug = False;\n-\n-\tfunction __construct()\n-\t{\n-\t\t$userSettings = Settings::getInstance()->get('user');\n-\n-\t\t$this->prefs['todo']    = $userSettings['preferences']['todo'];\n-\t}\n-\n-\tfunction preferences()\n-\t{\n-\t\t$submit = get_var('submit', array('POST'));\n-\t\tif ($submit)\n-\t\t{\n-\t\t\t$preferences = createObject('phpgwapi.preferences');\n-\t\t\t$prefs = get_var('prefs', array('POST'));\n-\t\t\tif ($prefs['mainscreen_showevents'] == True)\n-\t\t\t{\n-\t\t\t\t$preferences->add('todo', 'mainscreen_showevents', $prefs['mainscreen_showevents']);\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$preferences->delete('todo', 'mainscreen_showevents');\n-\t\t\t}\n-\n-\t\t\t$preferences->save_repository(True);\n-\t\t\tphpgw::redirect_link('/preferences/');\n-\t\t}\n-\t}\n-}"},{"sha":"476b6620abc440839f145bc4d5a07c9078832f25","filename":"src/modules/todo/inc/class.botodo.inc.php","status":"modified","additions":9,"deletions":107,"changes":116,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.botodo.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.botodo.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fclass.botodo.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -34,27 +34,14 @@ class todo_botodo\n \t/**\n \t * @var bool $debug enable debugging\n \t */\n-\tvar $debug = false;\n+\tvar bool $debug = false;\n \n-\tvar $public_functions = array(\n-\t\t'cached_accounts'  => True,\n-\t\t'_list'            => True,\n-\t\t'check_perms'      => True,\n-\t\t'check_values'     => True,\n-\t\t'select_todo_list' => True,\n-\t\t'save'             => True,\n-\t\t'_read'            => True,\n-\t\t'delete'           => True,\n-\t\t'exists'           => True,\n-\t\t'list_methods'     => True\n-\t);\n+\tvar todo_sotodo $sotodo;\n \n-\tvar $sotodo;\n-\n-\tvar $total_records = 0;\n-\tvar $use_session = False;\n-\tvar $phpgwapi_common;\n-\tvar\t$accounts_obj;\n+\tvar int $total_records = 0;\n+\tvar bool $use_session = False;\n+\tvar phpgwapi_common $phpgwapi_common;\n+\tvar\tAccounts $accounts_obj;\n \n \n \tfunction __construct($session = False)\n@@ -129,55 +116,6 @@ function __construct($session = False)\n \t\t$this->accounts_obj = new Accounts();\n \t}\n \n-\tfunction list_methods($_type)\n-\t{\n-\t\tif (is_array($_type))\n-\t\t{\n-\t\t\t$_type = $_type['type'];\n-\t\t}\n-\n-\t\tswitch ($_type)\n-\t\t{\n-\t\t\tcase 'xmlrpc':\n-\t\t\t\t$xml_functions = array(\n-\t\t\t\t\t'list_methods' => array(\n-\t\t\t\t\t\t'function'  => 'list_methods',\n-\t\t\t\t\t\t'signature' => array(array(xmlrpcStruct, xmlrpcString)),\n-\t\t\t\t\t\t'docstring' => lang('Read this list of methods.')\n-\t\t\t\t\t),\n-\t\t\t\t\t'list' => array(\n-\t\t\t\t\t\t'function'  => '_list',\n-\t\t\t\t\t\t'signature' => array(array(xmlrpcStruct, xmlrpcStruct)),\n-\t\t\t\t\t\t'docstring' => lang('Returns an array of todo items')\n-\t\t\t\t\t),\n-\t\t\t\t\t'save' => array(\n-\t\t\t\t\t\t'function'  => 'save',\n-\t\t\t\t\t\t'signature' => array(array(xmlrpcBoolean, xmlrpcStruct)),\n-\t\t\t\t\t\t'docstring' => lang('Adds or edits a todo item')\n-\t\t\t\t\t),\n-\t\t\t\t\t'delete' => array(\n-\t\t\t\t\t\t'function'  => 'delete',\n-\t\t\t\t\t\t'signature' => array(array(xmlrpcBoolean, xmlrpcInt)),\n-\t\t\t\t\t\t'docstring' => lang('Deletes a todo item')\n-\t\t\t\t\t),\n-\t\t\t\t\t'total_records' => array(\n-\t\t\t\t\t\t'function'  => 'total_records',\n-\t\t\t\t\t\t'signature' => array(array(xmlrpcInt)),\n-\t\t\t\t\t\t'docstring' => lang('Returns a the total number of records in the database, must call list_todos first')\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\treturn $xml_functions;\n-\t\t\t\tbreak;\n-\n-\t\t\tcase 'soap':\n-\t\t\t\treturn $this->soap_functions;\n-\t\t\t\tbreak;\n-\n-\t\t\tdefault:\n-\t\t\t\treturn array();\n-\t\t\t\tbreak;\n-\t\t}\n-\t}\n \n \tfunction get_grants()\n \t{\n@@ -235,7 +173,7 @@ function cached_accounts($account_id)\n \t\treturn $this->accounts_obj->get($account_id);\n \t}\n \n-\tfunction employee_list($type)\n+\tfunction employee_list(string $type)\n \t{\n \t\t$employees = $this->accounts_obj->get_list($type);\n \t\treturn $employees;\n@@ -251,41 +189,6 @@ function format_assigned($a = '')\n \t\treturn $a;\n \t}\n \n-\tfunction list_assigned($assi = '')\n-\t{\n-\t\t$aout = '';\n-\t\tif (is_array($assi))\n-\t\t{\n-\t\t\tforeach ($assi as $a)\n-\t\t\t{\n-\n-\t\t\t\t/**\n-\t\t\t\t * Begin Orlando Fix\n-\t\t\t\t * \n-\t\t\t\t * I had to comment the conditionals because variable $adata\n-\t\t\t\t * doesn't return the 'type' field of the accounts\n-\t\t\t\t */\n-\n-\t\t\t\t$adata = $this->cached_accounts($a);\n-\n-\t\t\t\t/*if ($adata[$a]['type'] == 'u')\n-\t\t\t\t\t{\n-\t\t\t\t\t\t$aout  .= $this->phpgwapi_common->display_fullname($adata[$a]['lid'],\n-\t\t\t\t\t\t\t\t\t\t$adata[$a]['firstname'],$adata[$a]['lastname']) . '<br>';\n-\t\t\t\t\t}\n-\t\t\t\t\telseif($adata[$a]['type'] == 'g')\n-\t\t\t\t\t{\n-\t\t\t\t\t\t$aout .= $adata[$a]['firstname'] . ' ' . lang('Group') . '<br>';\n-\t\t\t\t\t}*/\n-\n-\t\t\t\t$aout  .= $this->phpgwapi_common->display_fullname($adata->lid, $adata->firstname, $adata->lastname) . '<br>';\n-\t\t\t\t/**\n-\t\t\t\t * End Orlando Fix\n-\t\t\t\t */\n-\t\t\t}\n-\t\t}\n-\t\treturn $aout;\n-\t}\n \n \tfunction _list($start = 0, $limit = '', $query = '', $filter = '', $order = '', $sort = '', $cat_id = 0, $tree = '', $parent = '')\n \t{\n@@ -489,12 +392,11 @@ function delete($todo_id, $subs = False)\n \n \t\tif ($subs)\n \t\t{\n-\t\t\t$this->sotodo->delete_todo($todo_id, True);\n+\t\t\treturn $this->sotodo->delete_todo($todo_id, True);\n \t\t}\n \t\telse\n \t\t{\n-\t\t\t$this->sotodo->delete_todo($todo_id);\n+\t\t\treturn $this->sotodo->delete_todo($todo_id);\n \t\t}\n-\t\treturn True;\n \t}\n }"},{"sha":"639e1afe4ae1e3c73603e58162c59ee1dbdcf81f","filename":"src/modules/todo/inc/class.menu.inc.php","status":"modified","additions":13,"deletions":5,"changes":18,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.menu.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.menu.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fclass.menu.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -54,7 +54,7 @@ function get_menu()\n \t\t$menus['navbar'] = array(\n \t\t\t'todo' => array(\n \t\t\t\t'text'\t=> $translation->translate('todo', array(), true),\n-\t\t\t\t'url'\t=> phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.show_list')),\n+\t\t\t\t'url'\t=> phpgw::link('/todo/view/todos'),\n \t\t\t\t'image'\t=> array('todo', 'navbar'),\n \t\t\t\t'order'\t=> 10,\n \t\t\t\t'group'\t=> 'office'\n@@ -64,18 +64,26 @@ function get_menu()\n \t\t$menus['toolbar'] = array(\n \t\t\tarray(\n \t\t\t\t'text'\t=> $translation->translate('New', array(), true),\n-\t\t\t\t'url'\t=> phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.add')),\n+\t\t\t\t'url'\t=> phpgw::link('/todo/view/todos/add'),\n \t\t\t)\n \t\t);\n \n \t\tif (isset($userSettings['apps']['admin']))\n \t\t{\n \t\t\t$menus['admin'] = array(\n-\t\t\t\tarray(\n+\t\t\t\t'categories' =>array(\n \t\t\t\t\t'text'\t=> $translation->translate('Global Categories', array(), true),\n \t\t\t\t\t'url'\t=> phpgw::link('/index.php', array('menuaction' => 'admin.uicategories.index', 'appname' => 'todo', 'global_cats' => 1))\n-\t\t\t\t)\n+\t\t\t\t),\n+\t\t\t\t'acl' => array(\n+\t\t\t\t\t'text' => $translation->translate('Configure Access Permissions', array(), true),\n+\t\t\t\t\t'url' => phpgw::link('/index.php', array(\n+\t\t\t\t\t\t'menuaction' => 'preferences.uiadmin_acl.list_acl',\n+\t\t\t\t\t\t'acl_app' => 'todo'\n+\t\t\t\t\t))\n+\t\t\t\t),\n \t\t\t);\n+\n \t\t}\n \n \t\tif (isset($userSettings['apps']['preferences']))\n@@ -105,7 +113,7 @@ function get_menu()\n \t\t$menus['navigation'] =  array(\n \t\t\tarray(\n \t\t\t\t'text'\t=> $translation->translate('New', array(), true),\n-\t\t\t\t'url'\t=> phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.add')),\n+\t\t\t\t'url'\t=> phpgw::link('/todo/view/todos/add'),\n \t\t\t)\n \t\t);\n "},{"sha":"4a3d8c30bfaf7b5feb065c55b422018911defebf","filename":"src/modules/todo/inc/class.sotodo.inc.php","status":"modified","additions":348,"deletions":100,"changes":448,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.sotodo.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.sotodo.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fclass.sotodo.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -12,6 +12,7 @@\n  */\n \n use App\\Database\\Db;\n+use App\\traits\\DbRowTrait;\n use App\\modules\\phpgwapi\\security\\Acl;\n use App\\modules\\phpgwapi\\services\\Settings;\n use App\\modules\\phpgwapi\\controllers\\Accounts\\Accounts;\n@@ -24,6 +25,8 @@\n  */\n class todo_sotodo\n {\n+\tuse DbRowTrait;\n+\n \tvar $db;\n \tvar $grants;\n \tvar $historylog;\n@@ -42,10 +45,10 @@ function __construct()\n \t\t$this->db          = Db::getInstance();\n \t\t$this->join        = $this->db->join;\n \n-\t\t$this->grants      = Acl::getInstance()->get_grants2('todo', '.');\n+\t\t$this->grants      = Acl::getInstance()->get_grants2('todo', '.todo');\n \t\t$this->account     = $userSettings['account_id'];\n \t\t$this->user_groups = $accounts_obj->membership($this->account);\n-\t\t$this->historylog  = CreateObject('phpgwapi.historylog', 'todo', '.');\n+\t\t$this->historylog  = CreateObject('phpgwapi.historylog', 'todo', '.todo');\n \n \t\t// This is so our transactions follow across classes\n \t\t$this->historylog->db = &$this->db;\n@@ -68,19 +71,55 @@ function type($tree)\n \t\treturn '';\n \t}\n \n-\tfunction read_todos($start = 0, $limit = True, $query = '', $filter = '', $order = '', $sort = '', $cat_id = '', $tree = '', $parent = '')\n+\tprivate function resolve_order_column($order = '')\n \t{\n-\t\t$type = $this->type($tree);\n+\t\t$map = array(\n+\t\t\t'todo_id' => 'todo_id',\n+\t\t\t'todo_title' => 'todo_title',\n+\t\t\t'todo_status' => 'todo_status',\n+\t\t\t'todo_pri' => 'todo_pri',\n+\t\t\t'todo_startdate' => 'todo_startdate',\n+\t\t\t'todo_enddate' => 'todo_enddate',\n+\t\t\t'todo_owner' => 'todo_owner',\n+\t\t\t'todo_datecreated' => 'todo_datecreated',\n+\t\t);\n \n-\t\tif ($order)\n+\t\t$order = (string) $order;\n+\t\tif (!$order || !isset($map[$order]))\n \t\t{\n-\t\t\t$order = $this->db->db_addslashes($order);\n-\t\t\t$sort = $this->db->db_addslashes($sort);\n-\t\t\t$ordermethod = \"ORDER BY $order $sort\";\n+\t\t\treturn '';\n \t\t}\n-\t\telse\n+\n+\t\treturn $map[$order];\n+\t}\n+\n+\tprivate function build_order_method($order_column = '', $sort = '')\n+\t{\n+\t\t$sort = strtoupper((string) $sort) === 'DESC' ? 'DESC' : 'ASC';\n+\n+\t\tif (!$order_column)\n \t\t{\n-\t\t\t$ordermethod = 'ORDER BY todo_id_main, todo_id_parent, todo_level, todo_datecreated ASC';\n+\t\t\treturn 'ORDER BY phpgw_todo.todo_id_main ASC, phpgw_todo.todo_level ASC, phpgw_todo.todo_id_parent ASC, phpgw_todo.todo_datecreated ASC, phpgw_todo.todo_id ASC';\n+\t\t}\n+\n+\t\treturn 'ORDER BY '\n+\t\t\t. 'root_order_value ' . $sort . ', '\n+\t\t\t. 'phpgw_todo.todo_id_main ' . $sort . ', '\n+\t\t\t. 'phpgw_todo.todo_level ASC, '\n+\t\t\t. 'phpgw_todo.todo_id_parent ASC, '\n+\t\t\t. 'phpgw_todo.todo_datecreated ASC, '\n+\t\t\t. 'phpgw_todo.todo_id ASC';\n+\t}\n+\n+\tfunction read_todos($start = 0, $limit = True, $query = '', $filter = '', $order = '', $sort = '', $cat_id = '', $tree = '', $parent = '')\n+\t{\n+\t\t$type = $this->type($tree);\n+\t\t$order_column = $this->resolve_order_column($order);\n+\t\t$ordermethod = $this->build_order_method($order_column, $sort);\n+\t\t$root_order_select = '';\n+\t\tif ($order_column)\n+\t\t{\n+\t\t\t$root_order_select = ', (SELECT root.' . $order_column . ' FROM phpgw_todo root WHERE root.todo_id = phpgw_todo.todo_id_main) AS root_order_value';\n \t\t}\n \n \t\t$filter = strtolower($filter);\n@@ -90,7 +129,8 @@ function read_todos($start = 0, $limit = True, $query = '', $filter = '', $order\n \t\t\t$filter = 'none';\n \t\t}\n \n-\t\t$filtermethod = \"(( todo_owner = {$this->account} OR todo_assigned = '{$this->account}'\";\n+\t\t$account_id = (int) $this->account;\n+\t\t$filtermethod = \"(( todo_owner = {$account_id} OR todo_assigned = '{$account_id}'\";\n \n \t\t/**\n \t\t * Begin Orlando Fix\n@@ -103,7 +143,7 @@ function read_todos($start = 0, $limit = True, $query = '', $filter = '', $order\n \t\t\t$filtermethod .= \" OR assigned_group IN('0'\";\n \t\t\tforeach ($this->user_groups as $group)\n \t\t\t{\n-\t\t\t\t$filtermethod .= \",'\" . $group->id . \"' \";\n+\t\t\t\t$filtermethod .= \", '\" . (int) ($group->id ?? 0) . \"' \";\n \t\t\t}\n \t\t\t$filtermethod .= ')';\n \t\t}\n@@ -121,27 +161,41 @@ function read_todos($start = 0, $limit = True, $query = '', $filter = '', $order\n \t\t\t{\n \t\t\t\tforeach ($this->grants['accounts'] as $user => $_right)\n \t\t\t\t{\n-\t\t\t\t\t$public_user_list[] = $user;\n+\t\t\t\t\t$public_user_list[] = (int) $user;\n \t\t\t\t}\n+\t\t\t\t$public_user_list = array_values(array_filter(array_unique($public_user_list), static function ($id)\n+\t\t\t\t{\n+\t\t\t\t\treturn $id > 0;\n+\t\t\t\t}));\n \t\t\t\treset($public_user_list);\n-\t\t\t\t$filtermethod .= \" OR (todo_access='public' AND todo_owner IN(\" . implode(',', $public_user_list) . \"))\";\n+\t\t\t\tif ($public_user_list)\n+\t\t\t\t{\n+\t\t\t\t\t$filtermethod .= \" OR (todo_access='public' AND todo_owner IN(\" . implode(',', $public_user_list) . '))';\n+\t\t\t\t}\n \t\t\t}\n \n \t\t\t$public_group_list = array();\n \t\t\tif (is_array($this->grants['groups']) && $this->grants['groups'])\n \t\t\t{\n \t\t\t\tforeach ($this->grants['groups'] as $user => $_right)\n \t\t\t\t{\n-\t\t\t\t\t$public_group_list[] = $user;\n+\t\t\t\t\t$public_group_list[] = (int) $user;\n \t\t\t\t}\n+\t\t\t\t$public_group_list = array_values(array_filter(array_unique($public_group_list), static function ($id)\n+\t\t\t\t{\n+\t\t\t\t\treturn $id > 0;\n+\t\t\t\t}));\n \t\t\t\tunset($user);\n \t\t\t\treset($public_group_list);\n-\t\t\t\t$filtermethod .= \" OR todo_access='public' AND phpgw_group_map.group_id IN(\" . implode(',', $public_group_list) . \"))\";\n-\t\t\t\t$where = 'AND';\n+\t\t\t\tif ($public_group_list)\n+\t\t\t\t{\n+\t\t\t\t\t$filtermethod .= \" OR todo_access='public' AND phpgw_group_map.group_id IN(\" . implode(',', $public_group_list) . '))';\n+\t\t\t\t\t$where = 'AND';\n+\t\t\t\t}\n \t\t\t}\n \t\t\tif ($public_user_list && !$public_group_list)\n \t\t\t{\n-\t\t//\t\t$filtermethod .= ')';\n+\t\t\t\t//\t\t$filtermethod .= ')';\n \t\t\t}\n \t\t}\n \n@@ -171,10 +225,10 @@ function read_todos($start = 0, $limit = True, $query = '', $filter = '', $order\n \t\t{\n \t\t\t$parentmethod = ' AND todo_id_parent=' . (int) $parent;\n \t\t}\n-\t\t$sql = \"SELECT DISTINCT phpgw_todo.* FROM phpgw_todo\"\n-\t\t. \" {$this->join} phpgw_accounts ON ( phpgw_todo.todo_owner = phpgw_accounts.account_id)\"\n-\t\t. \" {$this->join} phpgw_group_map ON (phpgw_accounts.account_id = phpgw_group_map.account_id)\"\n-\t\t. \" WHERE $filtermethod $querymethod $type $parentmethod \";\n+\t\t$sql = \"SELECT DISTINCT phpgw_todo.*{$root_order_select} FROM phpgw_todo\"\n+\t\t\t. \" {$this->join} phpgw_accounts ON ( phpgw_todo.todo_owner = phpgw_accounts.account_id)\"\n+\t\t\t. \" {$this->join} phpgw_group_map ON (phpgw_accounts.account_id = phpgw_group_map.account_id)\"\n+\t\t\t. \" WHERE $filtermethod $querymethod $type $parentmethod \";\n \n \t\t$sql2 = \"SELECT count(*) as cnt FROM ({$sql}) as t\";\n \n@@ -204,8 +258,8 @@ function read_todos($start = 0, $limit = True, $query = '', $filter = '', $order\n \t\t\t\t'owner_id'\t\t\t=> $this->db->f('todo_owner'),\n \t\t\t\t'access'\t\t\t=> $this->db->f('todo_access'),\n \t\t\t\t'cat'\t\t\t\t=> (int)$this->db->f('todo_cat'),\n-\t\t\t\t'title'\t\t\t\t=> $this->db->f('todo_title', true),\n-\t\t\t\t'descr'\t\t\t\t=> $this->db->f('todo_des', true),\n+\t\t\t\t'title'\t\t\t\t=> $this->dbStrip($this->db->f('todo_title')),\n+\t\t\t\t'descr'\t\t\t\t=> $this->dbStrip($this->db->f('todo_des')),\n \t\t\t\t'pri'\t\t\t\t=> (int)$this->db->f('todo_pri'),\n \t\t\t\t'status'\t\t\t=> (int)$this->db->f('todo_status'),\n \t\t\t\t'sdate'\t\t\t\t=> $this->db->f('todo_startdate'),\n@@ -221,28 +275,32 @@ function read_todos($start = 0, $limit = True, $query = '', $filter = '', $order\n \n \tfunction read_single_todo($todo_id)\n \t{\n-\t\t$this->db->query('SELECT * FROM phpgw_todo WHERE todo_id = ' . (int) $todo_id, __LINE__, __FILE__);\n-\n-\t\t$todo = array();\n-\t\tif ($this->db->next_record())\n-\t\t{\n-\t\t\t$todo['id']\t\t\t\t= $this->db->f('todo_id');\n-\t\t\t$todo['main']\t\t\t= $this->db->f('todo_id_main');\n-\t\t\t$todo['parent']\t\t\t= $this->db->f('todo_id_parent');\n-\t\t\t$todo['level']\t\t\t= $this->db->f('todo_level');\n-\t\t\t$todo['owner']\t\t\t= $this->db->f('todo_owner');\n-\t\t\t$todo['access']\t\t\t= $this->db->f('todo_access');\n-\t\t\t$todo['cat']\t\t\t= $this->db->f('todo_cat');\n-\t\t\t$todo['title']\t\t\t= $this->db->f('todo_title');\n-\t\t\t$todo['descr']\t\t\t= $this->db->f('todo_des');\n-\t\t\t$todo['pri']\t\t\t= $this->db->f('todo_pri');\n-\t\t\t$todo['status']\t\t\t= $this->db->f('todo_status');\n-\t\t\t$todo['sdate']\t\t\t= $this->db->f('todo_startdate');\n-\t\t\t$todo['edate']\t\t\t= $this->db->f('todo_enddate');\n-\t\t\t$todo['assigned']\t\t= $this->db->f('todo_assigned');\n-\t\t\t$todo['assigned_group']\t= $this->db->f('assigned_group');\n-\t\t}\n-\t\treturn $todo;\n+\t\t$stmt = $this->db->prepare('SELECT * FROM phpgw_todo WHERE todo_id = :todo_id');\n+\t\t$stmt->execute([':todo_id' => (int) $todo_id]);\n+\n+\t\t$row = $stmt->fetch(\\PDO::FETCH_ASSOC);\n+\t\tif (!$row)\n+\t\t{\n+\t\t\treturn array();\n+\t\t}\n+\n+\t\treturn array(\n+\t\t\t'id'\t\t\t\t=> $row['todo_id'],\n+\t\t\t'main'\t\t\t=> $row['todo_id_main'],\n+\t\t\t'parent'\t\t\t=> $row['todo_id_parent'],\n+\t\t\t'level'\t\t\t=> $row['todo_level'],\n+\t\t\t'owner'\t\t\t=> $row['todo_owner'],\n+\t\t\t'access'\t\t\t=> $row['todo_access'],\n+\t\t\t'cat'\t\t\t\t=> $row['todo_cat'],\n+\t\t\t'title'\t\t\t=> $this->dbStrip($row['todo_title']),\n+\t\t\t'descr'\t\t\t=> $this->dbStrip($row['todo_des']),\n+\t\t\t'pri'\t\t\t\t=> $row['todo_pri'],\n+\t\t\t'status'\t\t\t=> $row['todo_status'],\n+\t\t\t'sdate'\t\t\t=> $row['todo_startdate'],\n+\t\t\t'edate'\t\t\t=> $row['todo_enddate'],\n+\t\t\t'assigned'\t\t=> $row['todo_assigned'],\n+\t\t\t'assigned_group'\t=> $row['assigned_group'],\n+\t\t);\n \t}\n \n \tfunction add_todo($values)\n@@ -264,10 +322,10 @@ function add_todo($values)\n \t\t\t$values['level']\t= $this->return_value($values['parent'], 'level') + 1;\n \t\t}\n \n-\t\t$values['title'] = $this->db->db_addslashes($values['title']);\n-\t\t$values['descr'] = $this->db->db_addslashes($values['descr']);\n-\t\t$values['assigned'] = $this->db->db_addslashes($values['assigned']);\n-\t\t$values['assigned_group'] = $this->db->db_addslashes($values['assigned_group']);\n+\t\t$title = (string) ($values['title'] ?? '');\n+\t\t$descr = (string) ($values['descr'] ?? '');\n+\t\t$assigned = (string) ($values['assigned'] ?? '');\n+\t\t$assigned_group = (string) ($values['assigned_group'] ?? '');\n \n \t\t/**\n \t\t * Begin Orlando Fix\n@@ -276,34 +334,37 @@ function add_todo($values)\n \t\t * because it didn't accept null values, and it now stores the actual time()\n \t\t */\n \t\t$this->db->transaction_begin();\n-\t\t$sql =   \"insert into phpgw_todo (todo_id_main,todo_id_parent,todo_level,todo_owner,todo_access,todo_cat,todo_des,todo_title,todo_pri,todo_status,todo_datecreated,todo_startdate,todo_enddate,todo_assigned,assigned_group,entry_date) \"\n-\t\t\t. \"values (\"\n-\t\t\t. (int)$values['main']\n-\t\t\t. \",\" . (int)$values['parent']\n-\t\t\t. \",\" . (int)$values['level']\n-\t\t\t. \" ,\" . $this->account . \",\"\n-\t\t\t. (int)!!$values['access']\n-\t\t\t. \",\" . (int)$values['cat']\n-\t\t\t. \",'\" . $values['descr'] . \"' \"\n-\t\t\t. \",'\" . $values['title'] . \"' \"\n-\t\t\t. \",\" . (int)$values['pri']\n-\t\t\t. \",\" . (int)$values['status']\n-\t\t\t. \",'\" . time() . \"'\"\n-\t\t\t. \",'\" . (int)$values['sdate'] . \"' \"\n-\t\t\t. ',' . (int)$values['edate']\n-\t\t\t. \",'\" . $values['assigned']\n-\t\t\t. \"','\" . $values['assigned_group'] . \"'\"\n-\t\t\t. \",\" . time() . \")\";\n-\n-\t\t$this->db->query($sql, __LINE__, __FILE__);\n+\t\t$sql = 'INSERT INTO phpgw_todo (todo_id_main, todo_id_parent, todo_level, todo_owner, todo_access, todo_cat, todo_des, todo_title, todo_pri, todo_status, todo_datecreated, todo_startdate, todo_enddate, todo_assigned, assigned_group, entry_date)'\n+\t\t\t. ' VALUES (:main, :parent, :level, :owner, :access, :cat, :descr, :title, :pri, :status, :datecreated, :sdate, :edate, :assigned, :assigned_group, :entry_date)';\n+\t\t$stmt = $this->db->prepare($sql);\n+\t\t$now = time();\n+\t\t$stmt->execute([\n+\t\t\t':main' => (int) $values['main'],\n+\t\t\t':parent' => (int) $values['parent'],\n+\t\t\t':level' => (int) $values['level'],\n+\t\t\t':owner' => (int) $this->account,\n+\t\t\t':access' => (int) !!$values['access'],\n+\t\t\t':cat' => (int) $values['cat'],\n+\t\t\t':descr' => $descr,\n+\t\t\t':title' => $title,\n+\t\t\t':pri' => (int) $values['pri'],\n+\t\t\t':status' => (int) $values['status'],\n+\t\t\t':datecreated' => $now,\n+\t\t\t':sdate' => (int) $values['sdate'],\n+\t\t\t':edate' => (int) $values['edate'],\n+\t\t\t':assigned' => $assigned,\n+\t\t\t':assigned_group' => $assigned_group,\n+\t\t\t':entry_date' => $now,\n+\t\t]);\n \t\t$todo_id = $this->db->get_last_insert_id('phpgw_todo', 'todo_id');\n \t\t/**\n \t\t * End Orlando Fix\n \t\t */\n \n \t\tif (!$values['parent'] || $values['parent'] == 0)\n \t\t{\n-\t\t\t$this->db->query('update phpgw_todo set todo_id_main=' . $todo_id . ' where todo_id=' . $todo_id, __LINE__, __FILE__);\n+\t\t\t$stmt = $this->db->prepare('UPDATE phpgw_todo SET todo_id_main = :todo_id_main WHERE todo_id = :todo_id');\n+\t\t\t$stmt->execute([':todo_id_main' => (int) $todo_id, ':todo_id' => (int) $todo_id]);\n \t\t}\n \t\t$this->historylog->add('A', $todo_id, '', '');\n \t\t$this->db->transaction_commit();\n@@ -316,16 +377,46 @@ function find_subs($list_parents = '', $list = '')\n \t\t{\n \t\t\treturn $list;\n \t\t}\n-\t\t$query = \"SELECT todo_id FROM phpgw_todo WHERE todo_id_parent IN ($list_parents)\";\n+\t\t$parents = array_filter(array_map('intval', explode(',', (string) $list_parents)));\n+\t\tif (!$parents)\n+\t\t{\n+\t\t\treturn $list;\n+\t\t}\n+\n+\t\t$parent_placeholders = array();\n+\t\t$params = array();\n+\t\tforeach (array_values($parents) as $index => $parent_id)\n+\t\t{\n+\t\t\t$key = ':parent_' . $index;\n+\t\t\t$parent_placeholders[] = $key;\n+\t\t\t$params[$key] = $parent_id;\n+\t\t}\n+\n+\t\t$query = 'SELECT todo_id FROM phpgw_todo WHERE todo_id_parent IN (' . implode(', ', $parent_placeholders) . ')';\n+\n \t\tif ($list <> '')\n \t\t{\n-\t\t\t$query .= \" AND todo_id NOT IN ($list)\";\n+\t\t\t$exclude_ids = array_filter(array_map('intval', explode(',', (string) $list)));\n+\t\t\tif ($exclude_ids)\n+\t\t\t{\n+\t\t\t\t$exclude_placeholders = array();\n+\t\t\t\t$offset = count($params);\n+\t\t\t\tforeach (array_values($exclude_ids) as $index => $exclude_id)\n+\t\t\t\t{\n+\t\t\t\t\t$key = ':exclude_' . ($offset + $index);\n+\t\t\t\t\t$exclude_placeholders[] = $key;\n+\t\t\t\t\t$params[$key] = $exclude_id;\n+\t\t\t\t}\n+\t\t\t\t$query .= ' AND todo_id NOT IN (' . implode(', ', $exclude_placeholders) . ')';\n+\t\t\t}\n \t\t}\n-\t\t$this->db->query($query, __LINE__, __FILE__);\n+\n+\t\t$stmt = $this->db->prepare($query);\n+\t\t$stmt->execute($params);\n \t\t$subs = array();\n-\t\twhile ($this->db->next_record())\n+\t\twhile ($row = $stmt->fetch(\\PDO::FETCH_ASSOC))\n \t\t{\n-\t\t\t$subs[] = $this->db->f('todo_id');\n+\t\t\t$subs[] = (int) $row['todo_id'];\n \t\t}\n \t\tif (count($subs))\n \t\t{\n@@ -339,34 +430,165 @@ function find_subs($list_parents = '', $list = '')\n \t\treturn $list;\n \t}\n \n+\tprivate function has_delete_grant_for_owner(int $owner_id): bool\n+\t{\n+\t\tif ($owner_id === (int) $this->account)\n+\t\t{\n+\t\t\treturn true;\n+\t\t}\n+\n+\t\tif (isset($this->grants['accounts'][$owner_id]) && ($this->grants['accounts'][$owner_id] & ACL_DELETE))\n+\t\t{\n+\t\t\treturn true;\n+\t\t}\n+\n+\t\t$accounts_obj = new Accounts();\n+\t\t$owner_membership = $accounts_obj->membership($owner_id);\n+\t\tforeach ((array) ($this->grants['groups'] ?? []) as $group_id => $right)\n+\t\t{\n+\t\t\tif (isset($owner_membership[$group_id]) && ($right & ACL_DELETE))\n+\t\t\t{\n+\t\t\t\treturn true;\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn false;\n+\t}\n+\n+\tprivate function read_todo_acl_row(int $todo_id): ?array\n+\t{\n+\t\t$stmt = $this->db->prepare('SELECT todo_id, todo_owner, todo_id_parent FROM phpgw_todo WHERE todo_id = :todo_id');\n+\t\t$stmt->execute([':todo_id' => $todo_id]);\n+\t\t$row = $stmt->fetch(\\PDO::FETCH_ASSOC);\n+\n+\t\tif (!$row)\n+\t\t{\n+\t\t\treturn null;\n+\t\t}\n+\n+\t\treturn [\n+\t\t\t'id' => (int) $row['todo_id'],\n+\t\t\t'owner' => (int) $row['todo_owner'],\n+\t\t\t'parent' => (int) $row['todo_id_parent'],\n+\t\t];\n+\t}\n+\n+\tprivate function deny_delete(string $reason, string $message, int $todo_id = 0, int $parent_id = 0): array\n+\t{\n+\t\treturn [\n+\t\t\t'ok' => false,\n+\t\t\t'reason' => $reason,\n+\t\t\t'message' => $message,\n+\t\t\t'todo_id' => $todo_id,\n+\t\t\t'parent_id' => $parent_id,\n+\t\t];\n+\t}\n+\n \tfunction delete_todo($todo_id, $sub = False)\n \t{\n-\t\t$this->db->transaction_begin();\n+\t\t$todo_id = (int) $todo_id;\n+\t\t$todo_acl_row = $this->read_todo_acl_row($todo_id);\n+\t\tif (!$todo_acl_row)\n+\t\t{\n+\t\t\treturn $this->deny_delete('not_found', lang('Todo entry not found'), $todo_id);\n+\t\t}\n+\n+\t\tif (!$this->has_delete_grant_for_owner((int) $todo_acl_row['owner']))\n+\t\t{\n+\t\t\treturn $this->deny_delete('insufficient_grants_owner', lang('Delete denied: insufficient rights on todo owner'), $todo_id);\n+\t\t}\n+\n+\t\t$parent_id = (int) $todo_acl_row['parent'];\n+\t\tif ($parent_id > 0)\n+\t\t{\n+\t\t\t$parent_acl_row = $this->read_todo_acl_row($parent_id);\n+\t\t\tif (!$parent_acl_row)\n+\t\t\t{\n+\t\t\t\treturn $this->deny_delete('parent_not_found', lang('Delete denied: parent entry not found'), $todo_id, $parent_id);\n+\t\t\t}\n+\n+\t\t\tif (!$this->has_delete_grant_for_owner((int) $parent_acl_row['owner']))\n+\t\t\t{\n+\t\t\t\treturn $this->deny_delete('insufficient_grants_parent', lang('Delete denied: insufficient rights on parent entry'), $todo_id, $parent_id);\n+\t\t\t}\n+\t\t}\n+\n \t\t$sub_todos = $this->find_subs($todo_id);\n-\t\t$subdelete = '';\n+\t\t$delete_ids = array($todo_id);\n \t\t$parent = 0;\n \t\tif ($sub_todos)\n \t\t{\n \t\t\tif ($sub)\n \t\t\t{\n-\t\t\t\t$subdelete = \" OR todo_id in ($sub_todos)\";\n+\t\t\t\t$delete_ids = array_merge($delete_ids, array_filter(array_map('intval', explode(',', (string) $sub_todos))));\n \t\t\t}\n \t\t\telse\n \t\t\t{\n \t\t\t\t$parent = $this->return_value($todo_id, 'parent');\n \t\t\t}\n \t\t}\n \n-\t\t$this->db->query('DELETE from phpgw_todo where todo_id=' . intval($todo_id) . $subdelete . \" AND ((todo_access='public' \"\n-\t\t\t. 'AND todo_owner != ' . $this->owner . ') OR (todo_owner=' . $this->owner . '))', __LINE__, __FILE__);\n+\t\tforeach (array_values(array_unique($delete_ids)) as $candidate_id)\n+\t\t{\n+\t\t\t$candidate_acl_row = $this->read_todo_acl_row((int) $candidate_id);\n+\t\t\tif (!$candidate_acl_row)\n+\t\t\t{\n+\t\t\t\treturn $this->deny_delete('not_found', lang('Todo entry not found'), (int) $candidate_id);\n+\t\t\t}\n+\n+\t\t\tif (!$this->has_delete_grant_for_owner((int) $candidate_acl_row['owner']))\n+\t\t\t{\n+\t\t\t\treturn $this->deny_delete('insufficient_grants_owner', lang('Delete denied: insufficient rights on todo owner'), (int) $candidate_id);\n+\t\t\t}\n+\t\t}\n+\n+\t\t$this->db->transaction_begin();\n+\n+\t\t$placeholders = array();\n+\t\t$params = array();\n+\t\tforeach (array_values(array_unique($delete_ids)) as $index => $delete_id)\n+\t\t{\n+\t\t\t$key = ':todo_id_' . $index;\n+\t\t\t$placeholders[] = $key;\n+\t\t\t$params[$key] = (int) $delete_id;\n+\t\t}\n+\n+\t\t$delete_sql = 'DELETE FROM phpgw_todo WHERE todo_id IN (' . implode(', ', $placeholders) . ')';\n+\t\t$stmt = $this->db->prepare($delete_sql);\n+\t\t$stmt->execute($params);\n+\t\t$deleted_count = (int) $stmt->rowCount();\n \n \t\tif (!$sub && $sub_todos)\n \t\t{\n-\t\t\t$this->db->query('UPDATE phpgw_todo set todo_id_parent=' . $parent . ' where todo_id_parent=' . $todo_id, __LINE__, __FILE__);\n-\t\t\t$this->db->query(\"UPDATE phpgw_todo set todo_level=todo_level-1 where todo_id in ($sub_todos)\", __LINE__, __FILE__);\n+\t\t\t$stmt = $this->db->prepare('UPDATE phpgw_todo SET todo_id_parent = :parent WHERE todo_id_parent = :todo_id');\n+\t\t\t$stmt->execute([':parent' => (int) $parent, ':todo_id' => $todo_id]);\n+\n+\t\t\t$sub_ids = array_filter(array_map('intval', explode(',', (string) $sub_todos)));\n+\t\t\tif ($sub_ids)\n+\t\t\t{\n+\t\t\t\t$sub_placeholders = array();\n+\t\t\t\t$sub_params = array();\n+\t\t\t\tforeach (array_values($sub_ids) as $index => $sub_id)\n+\t\t\t\t{\n+\t\t\t\t\t$key = ':sub_id_' . $index;\n+\t\t\t\t\t$sub_placeholders[] = $key;\n+\t\t\t\t\t$sub_params[$key] = $sub_id;\n+\t\t\t\t}\n+\n+\t\t\t\t$level_sql = 'UPDATE phpgw_todo SET todo_level = todo_level - 1 WHERE todo_id IN (' . implode(', ', $sub_placeholders) . ')';\n+\t\t\t\t$stmt = $this->db->prepare($level_sql);\n+\t\t\t\t$stmt->execute($sub_params);\n+\t\t\t}\n \t\t}\n \t\t$this->historylog->delete($todo_id);\n \t\t$this->db->transaction_commit();\n+\n+\t\treturn [\n+\t\t\t'ok' => true,\n+\t\t\t'deleted' => $deleted_count,\n+\t\t\t'subs' => (bool) $sub,\n+\t\t\t'todo_id' => $todo_id,\n+\t\t];\n \t}\n \n \tfunction edit_todo($values)\n@@ -433,43 +655,69 @@ function edit_todo($values)\n \t\t\t$this->historylog->add('C', $values['id'], $values['cat'], $old_values['cat']);\n \t\t}\n \n-\t\t$values['title'] = $this->db->db_addslashes($values['title']);\n-\t\t$values['descr'] = $this->db->db_addslashes($values['descr']);\n-\n-\t\t$this->db->query(\"update phpgw_todo set todo_des='\" . $values['descr'] . \"', todo_id_parent=\" . $values['parent']\n-\t\t\t. ', todo_pri=' . intval($values['pri']) . \", todo_status='\" . $values['status'] . \"', todo_id_main=\" . intval($values['main'])\n-\t\t\t. \", todo_access='\" . $values['access'] . \"', todo_level=\" . intval($values['level'])\n-\t\t\t. ', todo_startdate=' . intval($values['sdate']) . ', todo_enddate=' . intval($values['edate']) . \", todo_title='\" . $values['title']\n-\t\t\t. \"', todo_cat=\" . intval($values['cat']) . \", todo_assigned='\" . $values['assigned'] . \"', assigned_group='\" . $values['assigned_group']\n-\t\t\t. \"' where todo_id=\" . $values['id'], __LINE__, __FILE__);\n+\t\t$update_sql = 'UPDATE phpgw_todo SET'\n+\t\t\t. ' todo_des = :descr,'\n+\t\t\t. ' todo_id_parent = :parent,'\n+\t\t\t. ' todo_pri = :pri,'\n+\t\t\t. ' todo_status = :status,'\n+\t\t\t. ' todo_id_main = :main,'\n+\t\t\t. ' todo_access = :access,'\n+\t\t\t. ' todo_level = :level,'\n+\t\t\t. ' todo_startdate = :sdate,'\n+\t\t\t. ' todo_enddate = :edate,'\n+\t\t\t. ' todo_title = :title,'\n+\t\t\t. ' todo_cat = :cat,'\n+\t\t\t. ' todo_assigned = :assigned,'\n+\t\t\t. ' assigned_group = :assigned_group'\n+\t\t\t. ' WHERE todo_id = :id';\n+\t\t$stmt = $this->db->prepare($update_sql);\n+\t\t$stmt->execute([\n+\t\t\t':descr' => (string) ($values['descr'] ?? ''),\n+\t\t\t':parent' => (int) $values['parent'],\n+\t\t\t':pri' => (int) $values['pri'],\n+\t\t\t':status' => (int) $values['status'],\n+\t\t\t':main' => (int) $values['main'],\n+\t\t\t':access' => (string) ($values['access'] ?? ''),\n+\t\t\t':level' => (int) $values['level'],\n+\t\t\t':sdate' => (int) $values['sdate'],\n+\t\t\t':edate' => (int) $values['edate'],\n+\t\t\t':title' => (string) ($values['title'] ?? ''),\n+\t\t\t':cat' => (int) $values['cat'],\n+\t\t\t':assigned' => (string) ($values['assigned'] ?? ''),\n+\t\t\t':assigned_group' => (string) ($values['assigned_group'] ?? ''),\n+\t\t\t':id' => (int) $values['id'],\n+\t\t]);\n \t\t$this->db->transaction_commit();\n \t}\n \n \tfunction return_value($todo_id, $action = 'main')\n \t{\n+\t\t$item = 'todo_id_main';\n \t\tswitch ($action)\n \t\t{\n \t\t\tcase 'main':\n-\t\t\t\t$item = ' todo_id_main ';\n+\t\t\t\t$item = 'todo_id_main';\n \t\t\t\tbreak;\n \t\t\tcase 'level':\n-\t\t\t\t$item = ' todo_level ';\n+\t\t\t\t$item = 'todo_level';\n \t\t\t\tbreak;\n \t\t}\n \n-\t\t$this->db->query(\"select $item from phpgw_todo where todo_id=\" . intval($todo_id), __LINE__, __FILE__);\n-\t\tif ($this->db->next_record())\n+\t\t$stmt = $this->db->prepare(\"SELECT {$item} AS value FROM phpgw_todo WHERE todo_id = :todo_id\");\n+\t\t$stmt->execute([':todo_id' => (int) $todo_id]);\n+\t\tif ($row = $stmt->fetch(\\PDO::FETCH_ASSOC))\n \t\t{\n-\t\t\treturn $this->db->f($item);\n+\t\t\treturn $row['value'];\n \t\t}\n \t}\n \n \tfunction exists($todo_id)\n \t{\n-\t\t$this->db->query('select count(*) as cnt from phpgw_todo where todo_id_parent=' . intval($todo_id), __LINE__, __FILE__);\n-\t\t$this->db->next_record();\n+\t\t$stmt = $this->db->prepare('SELECT count(*) as cnt FROM phpgw_todo WHERE todo_id_parent = :todo_id_parent');\n+\t\t$stmt->execute([':todo_id_parent' => (int) $todo_id]);\n+\t\t$row = $stmt->fetch(\\PDO::FETCH_ASSOC) ?: array();\n \n-\t\tif ($this->db->f('cnt'))\n+\t\tif (!empty($row['cnt']))\n \t\t{\n \t\t\treturn True;\n \t\t}"},{"sha":"8eb4c5f11e9628af003662215d551c3dfd77cc91","filename":"src/modules/todo/inc/class.uipreferences.inc.php","status":"removed","additions":0,"deletions":109,"changes":109,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Finc%2Fclass.uipreferences.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Finc%2Fclass.uipreferences.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fclass.uipreferences.inc.php?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,109 +0,0 @@\n-<?php\n-\n-/**\n- * Todo preferences\n- *\n- * @author Craig Knudsen <cknudsen@radix.net>\n- * @author Mark Peters <skeeter@phpgroupware.org>\n- * @copyright Copyright (C) Craig Knudsen <cknudsen@radix.net>\n- * @copyright Copyright (C) 2002,2005 Free Software Foundation, Inc. http://www.fsf.org/\n- * @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n- * @package todo\n- * @version $Id$\n- * @internal Based on Webcalendar by Craig Knudsen http://www.radix.net/~cknudsen\n- */\n-\n-use App\\helpers\\Template;\n-use App\\modules\\phpgwapi\\services\\Settings;\n-\n-/**\n- * Todo preferences\n- *  \n- * @package todo\n- */\n-class todo_uipreferences\n-{\n-\t//\t\tvar $template_dir;\n-\tvar $template;\n-\n-\tvar $bo;\n-\n-\tvar $debug = False;\n-\t//\t\tvar $debug = True;\n-\n-\tvar $theme;\n-\n-\tvar $public_functions = array(\n-\t\t'preferences' => True\n-\t);\n-\n-\tvar $nextmatchs;\n-\tfunction __construct()\n-\t{\n-\t\t$theme = Settings::getInstance()->get('theme');\n-\n-\t\t$this->nextmatchs = CreateObject('phpgwapi.nextmatchs');\n-\t\t$this->template = Template::getInstance();\n-\t\t$this->theme = $theme;\n-\t\t$this->bo = CreateObject('todo.bopreferences');\n-\t}\n-\n-\tfunction preferences()\n-\t{\n-\n-\t\tSettings::getInstance()->update(\n-\t\t\t'flags',\n-\t\t\t[\n-\t\t\t\t'noheader' => false,\n-\t\t\t\t'nonavbar' => false,\n-\t\t\t\t'noappheader' => true,\n-\t\t\t\t'noappfooter' => true\n-\t\t\t]\n-\t\t);\n-\n-\t\t$phpgwapi_common = new \\phpgwapi_common();\n-\t\t$phpgwapi_common->phpgw_header();\n-\n-\t\t$this->template->set_file(\n-\t\t\tarray(\n-\t\t\t\t'pref_temp'      => 'pref.tpl'\n-\t\t\t)\n-\t\t);\n-\n-\t\t$this->template->set_block('pref_temp', 'pref', 'pref');\n-\t\t$this->template->set_block('pref_temp', 'pref_colspan', 'pref_colspan');\n-\t\t$this->template->set_block('pref_temp', 'pref_list', 'pref_list');\n-\n-\t\t$var = array(\n-\t\t\t'title'\t   \t=>\tlang('ToDo Preferences'),\n-\t\t\t'action_url'\t=>\tphpgw::link('/index.php', array('menuaction' => 'todo.bopreferences.preferences')),\n-\t\t\t'bg_color   '\t=>\t$this->theme['th_bg'],\n-\t\t\t'submit_lang'\t=>\tlang('submit'),\n-\t\t\t'text'   \t\t=> '&nbsp;'\n-\t\t);\n-\n-\t\t$this->output_template_array('row', 'pref_colspan', $var);\n-\n-\t\t$this->display_item(lang('Show ToDo items on main screen'), '<input type=\"checkbox\" name=\"prefs[mainscreen_showevents]\" value=\"True\"' . (@$this->bo->prefs['todo']['mainscreen_showevents'] ? ' checked' : '') . '>');\n-\n-\t\t$this->template->pparse('out', 'pref');\n-\t}\n-\n-\tfunction output_template_array($row, $list, $var)\n-\t{\n-\t\t$this->template->set_var($var);\n-\t\t$this->template->parse($row, $list, True);\n-\t}\n-\n-\tfunction display_item($field, $data)\n-\t{\n-\t\tstatic $tr_color;\n-\t\t$tr_color = $this->nextmatchs->alternate_row_color($tr_color);\n-\t\t$var = array(\n-\t\t\t'bg_color'\t=>\t$tr_color,\n-\t\t\t'field'\t\t=>\t$field,\n-\t\t\t'data'\t\t=>\t$data\n-\t\t);\n-\t\t$this->output_template_array('row', 'pref_list', $var);\n-\t}\n-}"},{"sha":"2676bdf0176ec8eb053717c591cb16a09ef3f425","filename":"src/modules/todo/inc/class.uitodo.inc.php","status":"modified","additions":52,"deletions":841,"changes":893,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.uitodo.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fclass.uitodo.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fclass.uitodo.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -1,902 +1,113 @@\n <?php\n \n /**\n- * Todo user interface\n+ * Todo user interface compatibility shim.\n+ *\n+ * Legacy menuaction endpoints are preserved and redirected to\n+ * the modern Twig/REST routes.\n  *\n- * @author Joseph Engo <jengo@phpgroupware.org>\n- * @author Bettina Gille [ceb@phpgroupware.org]\n- * @copyright Copyright (C) 2000-2003,2005 Free Software Foundation, Inc. http://www.fsf.org/\n- * @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n  * @package todo\n- * @version $Id$\n  */\n \n-use App\\helpers\\Template;\n-use App\\modules\\phpgwapi\\services\\Settings;\n-use App\\modules\\phpgwapi\\security\\Acl;\n-use App\\modules\\phpgwapi\\controllers\\Accounts\\Accounts;\n-\n-/*\n-\t* Import required classes\n-\t*/\n \n-phpgw::import_class('phpgwapi.sbox');\n-\n-/**\n- * Todo user interface\n- *  \n- * @package todo\n- */\n class todo_uitodo\n {\n-\tvar $grants;\n-\tvar $historylog;\n-\tvar $t;\n-\tvar $public_functions = array(\n-\t\t'show_list'\t=> True,\n-\t\t'view'      => True,\n-\t\t'add'       => True,\n-\t\t'edit'      => True,\n-\t\t'delete'\t=> True,\n-\t\t'matrix'\t=> True\n+\tpublic $public_functions = array(\n+\t\t'show_list' => true,\n+\t\t'view' => true,\n+\t\t'add' => true,\n+\t\t'edit' => true,\n+\t\t'delete' => true,\n+\t\t'matrix' => true,\n+\t\t'query' => true,\n \t);\n \n-\tvar $botodo;\n-\tvar $cats;\n-\tvar $matrix;\n-\tvar $account;\n-\tvar $start;\n-\tvar $query;\n-\tvar $filter;\n-\tvar $order;\n-\tvar $sort;\n-\tvar $cat_id;\n-\tvar $nextmatchs;\n-\tvar $phpgwapi_common;\n-\tvar $userSettings;\n-\n-\tfunction __construct()\n-\t{\n-\t\t$this->userSettings = Settings::getInstance()->get('user');\n-\t\t$this->botodo\t\t= CreateObject('todo.botodo', True);\n-\t\t$this->nextmatchs\t= CreateObject('phpgwapi.nextmatchs');\n-\t\t$this->historylog\t= CreateObject('phpgwapi.historylog', 'todo');\n-\t\t$this->historylog->types = array(\n-\t\t\t'A' => lang('Entry added'),\n-\t\t\t'C' => lang('Category changed'),\n-\t\t\t'S' => lang('Start date changed'),\n-\t\t\t'E' => lang('End date changed'),\n-\t\t\t'U' => lang('Urgency changed'),\n-\t\t\t's' => lang('Status changed'),\n-\t\t\t'T' => lang('Title changed'),\n-\t\t\t'D' => lang('Description changed'),\n-\t\t\t'a' => lang('Access changed'),\n-\t\t\t'P' => lang('Parent changed')\n-\t\t);\n-\n-\t\t$this->historylog->alternate_handlers = array(\n-\t\t\t'S' => '$this->phpgwapi_common->show_date',\n-\t\t\t'E' => '$this->phpgwapi_common->show_date',\n-//\t\t\t'C' => '$GLOBALS[\\'phpgw\\']->categories->id2name'\n-\t\t);\n-\n-\t\t$this->cats       = CreateObject('phpgwapi.categories');\n-\n-\t\t$this->phpgwapi_common = new \\phpgwapi_common();\n-\n-\t\t$this->account    = $this->userSettings['account_id'];\n-\t\t$this->t          = Template::getInstance($this->phpgwapi_common->get_tpl_dir('todo'));\n-\t\t$this->grants     = Acl::getInstance()->get_grants('todo', '.');\n-\n-\t\t$this->start      = $this->botodo->start;\n-\t\t$this->query      = $this->botodo->query;\n-\t\t$this->filter     = $this->botodo->filter;\n-\t\t$this->order      = $this->botodo->order;\n-\t\t$this->sort       = $this->botodo->sort;\n-\t\t$this->cat_id     = $this->botodo->cat_id;\n-\t}\n-\n-\tfunction save_sessiondata()\n-\t{\n-\t\t$data = array(\n-\t\t\t'start'\t\t=> $this->start,\n-\t\t\t'query'\t\t=> $this->query,\n-\t\t\t'filter'\t=> $this->filter,\n-\t\t\t'order'\t\t=> $this->order,\n-\t\t\t'sort'\t\t=> $this->sort,\n-\t\t\t'cat_id'\t=> $this->cat_id\n-\t\t);\n-\t\t$this->botodo->save_sessiondata($data);\n-\t}\n-\n-\tfunction set_app_langs()\n-\t{\n-\t\t$this->t->set_var('lang_category', lang('Category'));\n-\t\t$this->t->set_var('lang_select', lang('Select'));\n-\t\t$this->t->set_var('lang_descr', lang('Description'));\n-\t\t$this->t->set_var('lang_title', lang('Title'));\n-\t\t$this->t->set_var('lang_none', lang('None'));\n-\t\t$this->t->set_var('lang_nobody', lang('Nobody'));\n-\t\t$this->t->set_var('lang_urgency', lang('Urgency'));\n-\t\t$this->t->set_var('lang_completed', lang('Completed'));\n-\t\t$this->t->set_var('lang_start_date', lang('Start Date'));\n-\t\t$this->t->set_var('lang_end_date', lang('End Date'));\n-\t\t$this->t->set_var('lang_date_due', lang('date due'));\n-\t\t$this->t->set_var('lang_access', lang('Private'));\n-\t\t$this->t->set_var('lang_parent', lang('Parent project'));\n-\t\t$this->t->set_var('lang_submit', lang('Submit'));\n-\t\t$this->t->set_var('lang_save', lang('Save'));\n-\t\t$this->t->set_var('lang_done', lang('Done'));\n-\t\t$this->t->set_var('lang_assigned_group', lang('Assigned to group'));\n-\t\t$this->t->set_var('lang_assigned_user', lang('Assigned to user'));\n-\t\t$this->t->set_var('lang_owner', lang('Created by'));\n-\t}\n \n-\tfunction show_list()\n+\tpublic function __construct()\n \t{\n-\t\t$this->phpgwapi_common->phpgw_header(true);\n-\t\techo $this->show_list_body(True);\n \t}\n \n-\tfunction show_list_body($show_page_header = True)\n+\tpublic function show_list()\n \t{\n-\t\t$this->t->set_file('todo_list_t', 'list.tpl');\n-\t\t$this->t->set_block('todo_list_t', 'page_header', 'page_header');\n-\t\t$this->t->set_block('todo_list_t', 'table_header', 'table_header');\n-\t\t$this->t->set_block('todo_list_t', 'todo_list', 'todo_list');\n-\t\t$this->t->set_block('todo_list_t', 'table_footer', 'table_footer');\n-\t\t$this->t->set_block('todo_list_t', 'page_footer', 'page_footer');\n+\t\t$redirect_params = array();\n+\t\t$cat_id = Sanitizer::get_var('cat_id', 'int', 'REQUEST', 0);\n+\t\t$filter = Sanitizer::get_var('filter', 'string', 'REQUEST', 'none');\n \n-\t\t$body = '';\n-\t\t$this->set_app_langs();\n-\n-\t\t$this->t->set_var('lang_action', lang('todo list'));\n-\t\t$this->t->set_var('lang_all', lang('All'));\n-\n-\t\tif (!$this->start)\n+\t\tif ($cat_id)\n \t\t{\n-\t\t\t$this->start = 0;\n+\t\t\t$redirect_params['cat_id'] = $cat_id;\n \t\t}\n \n-\t\t$todo_list = $this->botodo->_list($this->start, True, $this->query, $this->filter, $this->order, $this->sort, $this->cat_id, 'all');\n-\n-\t\t// --------------------- nextmatch variable template-declarations ------------------------\n-\n-\t\tif ($show_page_header)\n+\t\tif ($filter)\n \t\t{\n-\t\t\t$left = $this->nextmatchs->left('/index.php', $this->start, $this->botodo->total_records, '&menuaction=todo.uitodo.show_list');\n-\t\t\t$right = $this->nextmatchs->right('/index.php', $this->start, $this->botodo->total_records, '&menuaction=todo.uitodo.show_list');\n-\t\t\t$this->t->set_var('left', $left);\n-\t\t\t$this->t->set_var('right', $right);\n-\n-\t\t\t$this->t->set_var('total_matchs', $this->nextmatchs->show_hits($this->botodo->total_records, $this->start));\n-\n-\t\t\t// ------------------------- end nextmatch template --------------------------------------\n-\n-\t\t\t$this->t->set_var('cat_action', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.show_list')));\n-\t\t\t$this->t->set_var('categories', $this->cats->formatted_list('select', 'all', $this->cat_id, 'True'));\n-\t\t\t$this->t->set_var('filter_action', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.show_list')));\n-\t\t\t$this->t->set_var('filter_list', $this->nextmatchs->filter(1, array('yours' => 1, 'filter' => $this->filter)));\n-\t\t\t$this->t->set_var('search_action', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.show_list')));\n-\t\t\t$this->t->set_var('search_list', $this->nextmatchs->search(array('search_obj' => 1, 'query' => $this->query)));\n-\n-\t\t\t$body .= $this->t->fp('out', 'page_header');\n-\t\t}\n-\n-\t\t// ---------------- list header variable template-declarations --------------------------\n-\n-\t\t$this->t->set_var('sort_status', $this->nextmatchs->show_sort_order($this->sort, 'todo_status', $this->order, '/todo/index.php', lang('Status')));\n-\t\t$this->t->set_var('sort_urgency', $this->nextmatchs->show_sort_order($this->sort, 'todo_pri', $this->order, '/todo/index.php', lang('Urgency')));\n-\t\t$this->t->set_var('sort_title', $this->nextmatchs->show_sort_order($this->sort, 'todo_title', $this->order, '/todo/index.php', lang('title')));\n-\t\t$this->t->set_var('sort_sdate', $this->nextmatchs->show_sort_order($this->sort, 'todo_startdate', $this->order, '/todo/index.php', lang('start date')));\n-\t\t$this->t->set_var('sort_edate', $this->nextmatchs->show_sort_order($this->sort, 'todo_enddate', $this->order, '/todo/index.php', lang('end date')));\n-\t\t$this->t->set_var('sort_owner', $this->nextmatchs->show_sort_order($this->sort, 'todo_owner', $this->order, '/todo/index.php', lang('created by')));\n-\t\t$this->t->set_var('sort_assigned', $this->nextmatchs->show_sort_order($this->sort, 'todo_assigned', $this->order, '/todo/index.php', lang('assigned to')));\n-\t\t$this->t->set_var('h_lang_sub', lang('Add Sub'));\n-\t\t$this->t->set_var('h_lang_view', lang('View'));\n-\t\t$this->t->set_var('h_lang_edit', lang('Edit'));\n-\n-\t\t$body .= $this->t->fp('out', 'table_header');\n-\n-\t\t// -------------- end header declaration --------------------------------------- \n-\n-\t\t$tr_class = '';\n-\t\tfor ($i = 0; $i < count($todo_list); $i++)\n-\t\t{\n-\t\t\t$this->t->set_var('tr_class', $this->nextmatchs->alternate_row_class($tr_class));\n-\t\t\t$level = $todo_list[$i]['level'];\n-\n-\t\t\t$title = phpgw::strip_html($todo_list[$i]['title']);\n-\n-\t\t\tif (!$title)\n-\t\t\t{\n-\t\t\t\t$words = explode(' ', phpgw::strip_html($todo_list[$i]['descr']));\n-\t\t\t\t$title = \"$words[0] $words[1] $words[2] $words[3] ...\";\n-\t\t\t}\n-\n-\t\t\tif ($level == 0)\n-\t\t\t{\n-\t\t\t\t$title = '<b>' . $title . '</b>';\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$space = '&nbsp;&nbsp;';\n-\t\t\t\t$spaceset = str_repeat($space, $level);\n-\t\t\t\t$title = $spaceset . $title;\n-\t\t\t}\n-\n-\t\t\tswitch ($todo_list[$i]['pri'])\n-\t\t\t{\n-\t\t\t\tcase 1:\n-\t\t\t\t\t$pri = lang('Low');\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase 2:\n-\t\t\t\t\t$pri = '<b>' . lang('normal') . '</b>';\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase 3:\n-\t\t\t\t\t$pri = '<font color=\"#CC0000\"><b>' . lang('high') . '</b></font>';\n-\t\t\t\t\tbreak;\n-\t\t\t}\n-\n-\t\t\tif ($todo_list[$i]['edate_epoch'] == 0)\n-\t\t\t{\n-\t\t\t\t$datedueout = '&nbsp;';\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$datedue = $todo_list[$i]['edate_epoch'];\n-\t\t\t\t$datedue = $datedue - $this->botodo->datetime->tz_offset;\n-\n-\t\t\t\t$month\t= $this->phpgwapi_common->show_date(time(), 'n');\n-\t\t\t\t$day\t= $this->phpgwapi_common->show_date(time(), 'd');\n-\t\t\t\t$year\t= $this->phpgwapi_common->show_date(time(), 'Y');\n-\t\t\t\t$currentdate = mktime(2, 0, 0, $month, $day, $year);\n-\n-\t\t\t\tif (($currentdate >= $datedue) && ($todo_list[$i]['status'] < 100))\n-\t\t\t\t{\n-\t\t\t\t\t$datedueout =  '<font color=\"#CC0000\"><b>';\n-\t\t\t\t}\n-\t\t\t\telse\n-\t\t\t\t{\n-\t\t\t\t\t$datedueout = '';\n-\t\t\t\t}\n-\n-\t\t\t\t$datedueout .= $todo_list[$i]['edate'];\n-\t\t\t\tif ($currentdate >= $datedue)\n-\t\t\t\t{\n-\t\t\t\t\t$datedueout .= '</b></font>';\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$assigned = $this->botodo->list_assigned($todo_list[$i]['assigned']);\n-\t\t\t$assigned .= $this->botodo->list_assigned($todo_list[$i]['assigned_group']);\n-\n-\t\t\t// --------------- template declaration for list records -------------------------------------\n-\n-\t\t\t$this->t->set_var(array(\n-\t\t\t\t'status'\t\t=> $todo_list[$i]['status'],\n-\t\t\t\t'pri'\t\t\t=> $pri,\n-\t\t\t\t'title'\t\t\t=> $title,\n-\t\t\t\t'datecreated'\t=> $todo_list[$i]['sdate'],\n-\t\t\t\t'datedue'\t\t=> $datedueout,\n-\t\t\t\t'owner'\t\t\t=> $todo_list[$i]['owner'],\n-\t\t\t\t'assigned'\t\t=> $assigned\n-\t\t\t));\n-\n-\t\t\t$this->t->set_var('view', '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.view', 'todo_id' => $todo_list[$i]['id']))\n-\t\t\t\t. '\">' . lang('View') . '</a>');\n-\n-\t\t\tif ($this->botodo->check_perms($todo_list[$i]['owner_id'], $this->grants, ACL_EDIT))\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('edit', '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.edit', 'todo_id' => $todo_list[$i]['id']))\n-\t\t\t\t\t. '\">' . lang('Edit') . '</a>');\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('edit', '&nbsp;');\n-\t\t\t}\n-\n-\t\t\tif ($this->botodo->check_perms($todo_list[$i]['owner_id'], $this->grants, ACL_DELETE))\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('delete', '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.delete', 'todo_id' => $todo_list[$i]['id']))\n-\t\t\t\t\t. '\">' . lang('Delete') . '</a>');\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('delete', '&nbsp;');\n-\t\t\t}\n-\n-\t\t\tif ($this->botodo->check_perms($todo_list[$i]['owner_id'], $this->grants, ACL_ADD))\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('subadd', '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.add', 'parent' => $todo_list[$i]['id'], 'cat_id' => $this->cat_id))\n-\t\t\t\t\t. '\">' . lang('Add Sub') . '</a>');\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('subadd', '&nbsp;');\n-\t\t\t}\n-\n-\t\t\t$body .= $this->t->fp('out', 'todo_list');\n+\t\t\t$redirect_params['filter'] = $filter;\n \t\t}\n \n-\t\t$body .= $this->t->fp('out', 'table_footer');\n-\n-\t\t// ------------------------- end record declaration ------------------------\n-\n-\t\t// --------------- template declaration for Add Form --------------------------\n-\n-\t\tif ($show_page_header)\n-\t\t{\n-\t\t\t$cat = array();\n-\t\t\tif ($this->cat_id && $this->cat_id != 0)\n-\t\t\t{\n-\t\t\t\t$cat = $this->cats->return_single($this->cat_id);\n-\t\t\t}\n-\n-\t\t\tif (!count($cat) || $cat[0]['app_name'] == 'phpgw' || $cat[0]['owner'] == '-1' || !$this->cat_id)\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('add', '<form method=\"POST\" action=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.add', 'cat_id' => $this->cat_id))\n-\t\t\t\t\t. '\"><input type=\"submit\" name=\"Add\" value=\"' . lang('Add') . '\"></form>');\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\tif ($this->botodo->check_perms($cat[0]['owner'], $this->grants, ACL_ADD) || $cat[0]['owner'] == $this->userSettings['account_id'])\n-\t\t\t\t{\n-\t\t\t\t\t$this->t->set_var('add', '<form method=\"POST\" action=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.add', 'cat_id' => $this->cat_id))\n-\t\t\t\t\t\t. '\"><input type=\"submit\" name=\"Add\" value=\"' . lang('Add') . '\"></form>');\n-\t\t\t\t}\n-\t\t\t\telse\n-\t\t\t\t{\n-\t\t\t\t\t$this->t->set_var('add', '');\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t// ----------------------- end Add form declaration ----------------------------\n-\n-\t\t\t// ------------ get actual date and year for matrixview arguments --------------\n-\n-\t\t\t$year = date('Y');\n-\t\t\t$month = date('m');\n-\n-\t\t\t$this->t->set_var('matrixview', '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.matrix', 'month' => $month, 'year' => $year)) . '\">'\n-\t\t\t\t. lang('View matrix of actual month') . \"</a>\\n\");\n-\n-\t\t\t$body .= $this->t->fp('out', 'page_footer');\n-\t\t}\n-\t\t$this->save_sessiondata();\n-\t\treturn $body;\n+\t\tphpgw::redirect_link('/todo/view/todos', $redirect_params);\n \t}\n \n-\tfunction formatted_user($type, $selected = '')\n-\t{\n-\t\tif (!$selected)\n-\t\t{\n-\t\t\t$selected = $this->account;\n-\t\t}\n-\n-\t\tif (! is_array($selected))\n-\t\t{\n-\t\t\t$selected = explode(',', $selected);\n-\t\t}\n-\n-\t\t$user_list = '';\n-\n-\t\t$accounts = $this->botodo->employee_list($type);\n-\t\t//_debug_array($accounts);\n-\t\t$accounts_obj = new Accounts();\n-\n-\t\tforeach ($accounts as $account)\n-\t\t{\n-\t\t\t$user_list .= '<option value=\"' . $account->id . '\"';\n-\t\t\tif (in_array($account->id, $selected))\n-\t\t\t{\n-\t\t\t\t$user_list .= ' selected';\n-\t\t\t}\n-\t\t\t$user_list .= '>' . $accounts_obj->id2name($account->id) . \"</option>\\n\";\n-\t\t}\n-\t\treturn $user_list;\n-\t}\n-\n-\tfunction formatted_todo($selected = '')\n-\t{\n-\t\t$todos = $this->botodo->_list(0, False);\n \n-\t\t$todo_select = '';\n-\t\tforeach ($todos as $todo)\n-\t\t{\n-\t\t\t$todo_select .= '<option value=\"' . $todo['id'] . '\"';\n-\t\t\tif ($todo['id'] == $selected)\n-\t\t\t{\n-\t\t\t\t$todo_select .= ' selected';\n-\t\t\t}\n-\t\t\tif (! $todo['title'])\n-\t\t\t{\n-\t\t\t\t$words = explode(' ', phpgw::strip_html($todo['descr']));\n-\t\t\t\t$title = \"$words[0] $words[1] $words[2] $words[3] ...\";\n-\t\t\t\t$todo_select .= \">$title\";\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$todo_select .= '>' . phpgw::strip_html($todo['title']);\n-\t\t\t}\n-\t\t\t$todo_select .= '</option>';\n-\t\t}\n-\t\treturn $todo_select;\n-\t}\n \n-\tfunction add()\n+\tpublic function add()\n \t{\n-\t\t$cat_id\t\t\t= Sanitizer::get_var('cat_id', 'int', 'REQUEST', 0);\n-\t\t$new_cat\t\t= Sanitizer::get_var('new_cat', 'int', 'REQUEST', 0);\n-\t\t$values\t\t\t= Sanitizer::get_var('values');\n-\t\t$submit\t\t\t= Sanitizer::get_var('submit', 'bool');\n-\t\t$new_parent\t\t= Sanitizer::get_var('new_parent', 'int', 'REQUEST', 0);\n-\t\t$parent\t\t\t= Sanitizer::get_var('parent', 'int', 'REQUEST', 0);\n-\t\t$assigned\t\t= Sanitizer::get_var('assigned');\n-\t\t$assigned_group\t= Sanitizer::get_var('assigned_group');\n+\t\t$redirect_params = array();\n+\t\t$cat_id = Sanitizer::get_var('cat_id', 'int', 'REQUEST', 0);\n+\t\t$parent = Sanitizer::get_var('parent', 'int', 'REQUEST', 0);\n \n-\t\tif ($new_parent)\n+\t\tif ($cat_id)\n \t\t{\n-\t\t\t$parent = $new_parent;\n+\t\t\t$redirect_params['cat_id'] = $cat_id;\n \t\t}\n \n-\t\tif ($new_cat)\n-\t\t{\n-\t\t\t$cat_id = $new_cat;\n-\t\t}\n-\n-\t\tif ($submit)\n-\t\t{\n-\t\t\t$values['cat'] = $cat_id;\n-\t\t\t$values['parent'] = $parent;\n-\t\t\tif (!isset($values['main']))\n-\t\t\t{\n-\t\t\t\t$values['main'] = $cat_id;\n-\t\t\t\t$values['level'] = 0;\n-\t\t\t}\n-\n-\t\t\t$values['assigned'] = '';\n-\t\t\tif (is_array($assigned))\n-\t\t\t{\n-\t\t\t\t$values['assigned'] = implode(',', $assigned);\n-\t\t\t\tif (count($assigned) > 1)\n-\t\t\t\t{\n-\t\t\t\t\t$values['assigned'] = \", {$values['assigned']} ,\";\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$values['assigned_group'] = '';\n-\t\t\tif (is_array($assigned_group))\n-\t\t\t{\n-\t\t\t\t$values['assigned_group'] = implode(',', $assigned_group);\n-\t\t\t\tif (count($assigned_group) > 1)\n-\t\t\t\t{\n-\t\t\t\t\t$values['assigned_group'] = ',' . $values['assigned_group'] . ',';\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$error = $this->botodo->check_values($values);\n-\t\t\tif (is_array($error))\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('error', $this->phpgwapi_common->error_list($error));\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$this->botodo->save($values);\n-\t\t\t\tphpgw::redirect_link('/index.php', array('menuaction' => 'todo.uitodo.show_list', 'cat_id' => (int) $cat_id));\n-\t\t\t\texit;\n-\t\t\t}\n-\t\t}\n-\n-\t\t$this->phpgwapi_common->phpgw_header(true);\n-\n-\t\t$this->t->set_file('todo_add', 'form.tpl');\n-\t\t$this->t->set_block('todo_add', 'add', 'addhandle');\n-\t\t$this->t->set_block('todo_add', 'edit', 'edithandle');\n-\n-\t\t$this->set_app_langs();\n-\t\t$this->t->set_var('actionurl', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.add')));\n-\n \t\tif ($parent)\n \t\t{\n-\t\t\t$this->t->set_var('lang_todo_action', lang('Add sub project'));\n-\t\t}\n-\t\telse\n-\t\t{\n-\t\t\t$this->t->set_var('lang_todo_action', lang('Add main project'));\n-\t\t}\n-\n-\t\tif (!isset($values['descr']) || !$values['descr'])\n-\t\t{\n-\t\t\t$values['descr'] = '';\n-\t\t}\n-\n-\t\tif (!isset($values['smonth']) || !$values['smonth'])\n-\t\t{\n-\t\t\t$values['smonth'] = date('m', time());\n+\t\t\t$redirect_params['parent'] = $parent;\n \t\t}\n \n-\t\tif (!isset($values['sday']) || !$values['sday'])\n-\t\t{\n-\t\t\t$values['sday'] = date('d', time());\n-\t\t}\n-\n-\t\tif (! isset($values['syear']) || !$values['syear'])\n-\t\t{\n-\t\t\t$values['syear'] = date('Y', time());\n-\t\t}\n-\n-\t\t$plus1week = strtotime('+1 week');\n-\t\tif (!isset($values['eday']) || !$values['eday'])\n-\t\t{\n-\t\t\t$values['eday'] = date('d', $plus1week);\n-\t\t}\n-\n-\t\tif (!isset($values['emonth']) || !$values['emonth'])\n-\t\t{\n-\t\t\t$values['emonth'] = date('m', $plus1week);\n-\t\t}\n-\n-\t\tif (!isset($values['eyear']) || !$values['eyear'])\n-\t\t{\n-\t\t\t$values['eyear'] = date('Y', $plus1week);\n-\t\t}\n-\t\tunset($plus1week);\n-\n-\t\t$this->t->set_var($values + array(\n-\t\t\t'cat_list'\t\t\t=> $this->cats->formatted_list('select', 'all', $cat_id, 'True'),\n-\t\t\t'todo_list'\t\t\t=> $this->formatted_todo($parent),\n-\t\t\t'pri_list'\t\t\t=> phpgwapi_sbox::getPriority('values[pri]'),\n-\t\t\t'stat_list'\t\t\t=> phpgwapi_sbox::getPercentage('values[status]', 0),\n-\t\t\t'user_list'\t\t\t=> $this->formatted_user('accounts', $assigned),\n-\t\t\t'group_list'\t\t=> $this->formatted_user('groups', $assigned_group),\n-\t\t\t'lang_selfortoday'\t=> lang('or: select for today:'),\n-\t\t\t'lang_daysfromstartdate' => lang('or: days from startdate:'),\n-\t\t\t'lang_submit'\t\t=> lang('Submit'),\n-\t\t\t'lang_reset'\t\t=> lang('Clear form'),\n-\t\t\t'edithandle'\t\t=> '',\n-\t\t\t'addhandle'\t\t\t=> '',\n-\t\t\t'start_select_date'\t=> $this->phpgwapi_common->dateformatorder(\n-\t\t\t\tphpgwapi_sbox::getYears('values[syear]', $values['syear']),\n-\t\t\t\tphpgwapi_sbox::getMonthText('values[smonth]', $values['smonth']),\n-\t\t\t\tphpgwapi_sbox::getDays('values[sday]', $values['sday'])\n-\t\t\t),\n-\t\t\t'end_select_date'\t=> $this->phpgwapi_common->dateformatorder(\n-\t\t\t\tphpgwapi_sbox::getYears('values[eyear]', $values['eyear']),\n-\t\t\t\tphpgwapi_sbox::getMonthText('values[emonth]', $values['emonth']),\n-\t\t\t\tphpgwapi_sbox::getDays('values[eday]', $values['eday'])\n-\t\t\t),\n-\t\t\t'selfortoday'\t\t=> '<input type=\"checkbox\" name=\"values[seltoday]\" value=\"True\">',\n-\t\t\t'daysfromstartdate'\t=> '<input type=\"text\" name=\"values[daysfromstart]\" size=\"3\" maxlength=\"3\">',\n-\t\t\t'access_list'\t\t=> '<input type=\"checkbox\" name=\"values[access]\" value=\"True\"' . (!isset($values['access']) || $values['access'] == 'private' ? ' checked' : '') . '>'\n-\t\t));\n-\n-\t\t$this->t->pfp('out', 'todo_add');\n-\t\t$this->t->pfp('addhandle', 'add');\n+\t\tphpgw::redirect_link('/todo/view/todos/add', $redirect_params);\n \t}\n \n-\tfunction view()\n+\tpublic function view()\n \t{\n-\t\t$this->phpgwapi_common->phpgw_header(true);\n-\n-\t\t$values = $this->botodo->read($_REQUEST['todo_id']);\n-\t\t$this->t->set_file('_view', 'view.tpl');\n-\n-\t\t$this->set_app_langs();\n-\n-\t\t$this->t->set_var('lang_todo_action', lang('View todo item'));\n-\t\t$this->t->set_var('value_title', phpgw::strip_html($values['title']));\n-\t\t$this->t->set_var('value_descr', phpgw::strip_html($values['descr']));\n-\t\t$this->t->set_var('value_category', $this->cats->id2name($values['cat']));\n-\n-\t\t$sdate = $values['sdate'] - $this->botodo->datetime->tz_offset;\n-\t\t$this->t->set_var('value_start_date', $this->phpgwapi_common->show_date($sdate, $this->userSettings['preferences']['common']['dateformat']));\n-\n-\t\tif ($values['edate'] && $values['edate'] != 0)\n-\t\t{\n-\t\t\t$edate = $values['edate'] - $this->botodo->datetime->tz_offset;\n-\t\t\t$this->t->set_var('value_end_date', $this->phpgwapi_common->show_date($edate, $this->userSettings['preferences']['common']['dateformat']));\n-\t\t}\n-\n-\t\t$parent_values = $this->botodo->read($values['parent']);\n-\t\tif (is_array($parent_values) && count($parent_values))\n-\t\t{\n-\t\t\t$this->t->set_var('value_parent', phpgw::strip_html($parent_values['title']));\n-\t\t}\n-\t\telse\n+\t\t$todo_id = Sanitizer::get_var('todo_id', 'int', 'REQUEST', 0);\n+\t\tif ($todo_id)\n \t\t{\n-\t\t\t$this->t->set_var('value_parent', '');\n+\t\t\tphpgw::redirect_link('/todo/view/todos/' . $todo_id);\n \t\t}\n \n-\t\t$this->t->set_var('value_completed', $values['status']);\n-\n-\t\t$assigned = $this->botodo->list_assigned($this->botodo->format_assigned($values['assigned']));\n-\t\t$assigned .= $this->botodo->list_assigned($this->botodo->format_assigned($values['assigned_group']));\n-\n-\t\t$this->t->set_var('assigned', $assigned);\n-\n-\t\t$cached_data = $this->botodo->cached_accounts($values['owner']);\n-\n-\n-\t\t/**\n-\t\t * Begin Orlando Fix\n-\t\t *\n-\t\t * I had to change how $cached_data variables were used( as arrays)\n-\t\t * so they can be read as: object -> attribute\n-\t\t */\n-\t\t$this->t->set_var('owner', $this->phpgwapi_common->display_fullname(\n-\t\t\t$cached_data->lid,\n-\t\t\t$cached_data->firstname,\n-\t\t\t$cached_data->lastname\n-\t\t));\n-\t\t/**\n-\t\t * End Orlando Fix\n-\t\t */\n-\n-\n-\t\tswitch ($values['pri'])\n-\t\t{\n-\t\t\tcase 1:\n-\t\t\t\t$pri = lang('Low');\n-\t\t\t\tbreak;\n-\t\t\tcase 2:\n-\t\t\t\t$pri = lang('normal');\n-\t\t\t\tbreak;\n-\t\t\tcase 3:\n-\t\t\t\t$pri = '<font color=\"CC0000\"><b>' . lang('high') . '</b></font>';\n-\t\t\t\tbreak;\n-\t\t}\n-\n-\t\t$this->t->set_var('value_urgency', $pri);\n-\n-\t\t$this->t->set_var('lang_access', lang('Access'));\n-\t\t$this->t->set_var('access', lang($values['access']));\n-\n-\t\t$this->t->set_var('history', $this->historylog->return_html(array(), '', '', $_REQUEST['todo_id']));\n-\t\t$this->t->set_var('done_action', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.show_list')));\n-\t\t$this->t->pfp('out', '_view');\n+\t\tphpgw::redirect_link('/todo/view/todos');\n \t}\n \n-\tfunction edit()\n+\tpublic function edit()\n \t{\n-\t\t$todo_id = isset($_REQUEST['todo_id']) ? (int) $_REQUEST['todo_id'] : 0;\n-\t\t$cat_id = isset($_POST['cat_id']) ? (int) $_POST['cat_id'] : 0;\n-\t\t$new_cat = isset($_POST['new_cat']) ? (int) $_POST['new_cat'] : 0;\n-\t\t$values = isset($_POST['values']) ? (array) $_POST['values'] : array();\n-\t\t$submit = isset($_POST['submit']) ? !!$_POST['submit'] : false;\n-\t\t$new_parent = isset($_POST['new_parent']) ? $_POST['new_parent'] : 0;\n-\t\t$parent = isset($_POST['parent']) ? (int) $_POST['parent'] : 0;\n-\t\t$assigned = isset($_POST['assigned']) ? $_POST['assigned'] : 0;\n-\t\t$assigned_group = isset($_POST['assigned_group']) ? $_POST['assigned_group'] : 0;\n-\n-\t\tif ($new_parent)\n-\t\t{\n-\t\t\t$parent = $new_parent;\n-\t\t}\n-\n-\t\tif ($new_cat)\n-\t\t{\n-\t\t\t$cat_id = $new_cat;\n-\t\t}\n-\n-\t\tif ($submit)\n-\t\t{\n-\t\t\t$values['cat'] = $cat_id;\n-\t\t\t$values['parent'] = $parent;\n-\t\t\t$values['id'] = $todo_id;\n-\n-\t\t\tif (is_array($assigned))\n-\t\t\t{\n-\t\t\t\t$values['assigned'] = implode(',', $assigned);\n-\t\t\t\tif (count($assigned) > 1)\n-\t\t\t\t{\n-\t\t\t\t\t$values['assigned'] = ',' . $values['assigned'] . ',';\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif (is_array($assigned_group))\n-\t\t\t{\n-\t\t\t\t$values['assigned_group'] = implode(',', $assigned_group);\n-\t\t\t\tif (count($assigned_group) > 1)\n-\t\t\t\t{\n-\t\t\t\t\t$values['assigned_group'] = ',' . $values['assigned_group'] . ',';\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$error = $this->botodo->check_values($values);\n-\t\t\tif (is_array($error))\n-\t\t\t{\n-\t\t\t\t$this->t->set_var('error', $this->phpgwapi_common->error_list($error));\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$this->botodo->save($values, 'edit');\n-\t\t\t\tphpgw::redirect_link('/', array('menuaction' => 'todo.uitodo.show_list', 'cat_id' => $cat_id));\n-\t\t\t\texit;\n-\t\t\t}\n-\t\t}\n-\n-\t\t$this->phpgwapi_common->phpgw_header(true);\n-\n-\t\t$this->t->set_file(array('todo_edit' => 'form.tpl'));\n-\t\t$this->t->set_block('todo_edit', 'add', 'addhandle');\n-\t\t$this->t->set_block('todo_edit', 'edit', 'edithandle');\n-\n-\t\t$this->set_app_langs();\n-\t\t$this->t->set_var('actionurl', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.edit', 'todo_id' => $todo_id)));\n-\n-\t\t$values = $this->botodo->read($todo_id);\n-\n-\t\tif ($values['parent'] != 0)\n-\t\t{\n-\t\t\t$this->t->set_var('lang_todo_action', lang('Edit sub project'));\n-\t\t}\n-\t\telse\n-\t\t{\n-\t\t\t$this->t->set_var('lang_todo_action', lang('Edit main project'));\n-\t\t}\n-\n-\t\t$this->t->set_var('cat_list', $this->cats->formatted_list('select', 'all', $values['cat'], 'True'));\n-\t\t$this->t->set_var('todo_list', $this->formatted_todo($values['parent']));\n-\n-\t\t$this->t->set_var('descr', phpgw::strip_html($values['descr']));\n-\t\t$this->t->set_var('title', phpgw::strip_html($values['title']));\n-\n-\t\t$this->t->set_var('pri_list', phpgwapi_sbox::getPriority('values[pri]', $values['pri']));\n-\t\t$this->t->set_var('stat_list', phpgwapi_sbox::getPercentage('values[status]', $values['status']));\n-\t\t$this->t->set_var('user_list', $this->formatted_user('accounts', $this->botodo->format_assigned($values['assigned'])));\n-\t\t$this->t->set_var('group_list', $this->formatted_user('groups', $this->botodo->format_assigned($values['assigned_group'])));\n-\n-\t\tif ($values['sdate'] == 0)\n-\t\t{\n-\t\t\t$values['sday'] = 0;\n-\t\t\t$values['smonth'] = 0;\n-\t\t\t$values['syear'] = 0;\n-\t\t}\n-\t\telse\n-\t\t{\n-\t\t\t$values['sday'] = date('d', $values['sdate']);\n-\t\t\t$values['smonth'] = date('m', $values['sdate']);\n-\t\t\t$values['syear'] = date('Y', $values['sdate']);\n-\t\t}\n-\n-\t\t$this->t->set_var('start_select_date', $this->phpgwapi_common->dateformatorder(\n-\t\t\tphpgwapi_sbox::getYears('values[syear]', $values['syear']),\n-\t\t\tphpgwapi_sbox::getMonthText('values[smonth]', $values['smonth']),\n-\t\t\tphpgwapi_sbox::getDays('values[sday]', $values['sday'])\n-\t\t));\n-\n-\t\tif ($values['edate'] == 0)\n-\t\t{\n-\t\t\t$values['eday'] = 0;\n-\t\t\t$values['emonth'] = 0;\n-\t\t\t$values['eyear'] = 0;\n-\t\t}\n-\t\telse\n-\t\t{\n-\t\t\t$values['eday'] = date('d', $values['edate']);\n-\t\t\t$values['emonth'] = date('m', $values['edate']);\n-\t\t\t$values['eyear'] = date('Y', $values['edate']);\n-\t\t}\n-\n-\t\t$this->t->set_var('end_select_date', $this->phpgwapi_common->dateformatorder(\n-\t\t\tphpgwapi_sbox::getYears('values[eyear]', $values['eyear']),\n-\t\t\tphpgwapi_sbox::getMonthText('values[emonth]', $values['emonth']),\n-\t\t\tphpgwapi_sbox::getDays('values[eday]', $values['eday'])\n-\t\t));\n-\n-\t\t$this->t->set_var('selfortoday', '&nbsp;');\n-\t\t$this->t->set_var('lang_selfortoday', '&nbsp;');\n-\t\t$this->t->set_var('lang_daysfromstartdate', '&nbsp;');\n-\t\t$this->t->set_var('daysfromstartdate', '&nbsp;');\n-\n-\t\t$this->t->set_var('access_list', '<input type=\"checkbox\" name=\"values[access]\" value=\"True\"' . ($values['access'] == 'private' ? ' checked' : '') . '>');\n-\n-\t\tif ($this->botodo->check_perms($values['owner'], $this->grants, ACL_DELETE) || $values['owner'] == $this->userSettings['account_id'])\n+\t\t$todo_id = Sanitizer::get_var('todo_id', 'int', 'REQUEST', 0);\n+\t\tif ($todo_id)\n \t\t{\n-\t\t\t$this->t->set_var('delete', '<form method=\"POST\" action=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.delete', 'todo_id' => $values['id']))\n-\t\t\t\t. '\"><input type=\"submit\" value=\"' . lang('Delete') . '\"></form>');\n-\t\t}\n-\t\telse\n-\t\t{\n-\t\t\t$this->t->set_var('delete', '&nbsp;');\n+\t\t\tphpgw::redirect_link('/todo/view/todos/' . $todo_id . '/edit');\n \t\t}\n \n-\t\t$this->t->set_var('lang_submit', lang('Submit'));\n-\t\t$this->t->set_var('edithandle', '');\n-\t\t$this->t->set_var('addhandle', '');\n-\t\t$this->t->pfp('out', 'todo_edit');\n-\t\t$this->t->pfp('edithandle', 'edit');\n+\t\tphpgw::redirect_link('/todo/view/todos');\n \t}\n \n-\tfunction delete()\n+\tpublic function delete()\n \t{\n-\t\t$todo_id = isset($_REQUEST['todo_id']) ? (int)$_REQUEST['todo_id'] : 0;\n-\n-\t\tif (isset($_POST['confirm']) && $_POST['confirm'])\n+\t\t$todo_id = Sanitizer::get_var('todo_id', 'int', 'REQUEST', 0);\n+\t\tif ($todo_id)\n \t\t{\n-\t\t\tif (isset($_POST['subs']) && $_POST['subs'])\n-\t\t\t{\n-\t\t\t\t$this->botodo->delete($todo_id, true);\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$this->botodo->delete($todo_id);\n-\t\t\t}\n-\t\t\tphpgw::redirect_link('/index.php', array('menuaction' => 'todo.uitodo.show_list'));\n+\t\t\tphpgw::redirect_link('/todo/view/todos/' . $todo_id . '/delete');\n \t\t}\n-\t\t$this->phpgwapi_common->phpgw_header(true);\n \n-\t\t$this->t->set_file('todo_delete', 'delete.tpl');\n-\t\t$this->t->set_var('actionurl', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.delete', 'todo_id' => $todo_id)));\n-\n-\t\t$exists = $this->botodo->exists($todo_id);\n-\n-\t\tif ($exists)\n-\t\t{\n-\t\t\t$this->t->set_var('lang_subs', lang('Do you also want to delete all sub projects ?'));\n-\t\t\t$this->t->set_var('subs', '<input type=\"checkbox\" name=\"subs\" value=\"True\">');\n-\t\t}\n-\t\telse\n-\t\t{\n-\t\t\t$this->t->set_var('lang_subs', '');\n-\t\t\t$this->t->set_var('subs', '');\n-\t\t}\n-\n-\t\t$this->t->set_var('nolink', phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.show_list')));\n-\t\t$this->t->set_var('deleteheader', lang('Are you sure you want to delete this entry'));\n-\t\t$this->t->set_var('lang_no', lang('No'));\n-\t\t$this->t->set_var('lang_yes', lang('Yes'));\n-\n-\t\t$this->t->pfp('out', 'todo_delete');\n+\t\tphpgw::redirect_link('/todo/view/todos');\n \t}\n \n-\tfunction matrix()\n+\tpublic function matrix()\n \t{\n-\t\t$o = 0;\n-\t\t$month = isset($_REQUEST['month']) ? (int) $_REQUEST['month'] : date('n');\n-\t\t$year = isset($_REQUEST['year']) ? (int) $_REQUEST['year'] : date('Y');\n-\n-\t\t$this->phpgwapi_common->phpgw_header(true);\n-\n-\t\t$colors = array(\n-\t\t\t'#cc0033',\n-\t\t\t'#006600',\n-\t\t\t'#00ccff',\n-\t\t\t'#ff6600',\n-\t\t\t'#0000ff'\n-\t\t);\n-\n-\t\t$matrix  = CreateObject('phpgwapi.matrixview', $month, $year);\n-\n+\t\t$month = Sanitizer::get_var('month', 'int', 'REQUEST', (int) date('n'));\n+\t\t$year = Sanitizer::get_var('year', 'int', 'REQUEST', (int) date('Y'));\n \n-\t\t$entries = $this->botodo->_list(0, 0, '', '', '', '', '', 'mains');\n-\n-\t\tforeach ($entries as $entry)\n-\t\t{\n-\t\t\t++$o;\n-\t\t\t$ind = $o % count($colors);\n-\n-\t\t\tif ($entry['sdate_epoch'] > 0 && $entry['edate_epoch'] > 0)\n-\t\t\t{\n-\t\t\t\t$title = '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.view', 'todo_id' => $entry['id'])) . '\">' . phpgw::strip_html($entry['title']) . '</a>';\n-\t\t\t\t$startd = date('Y', $entry['sdate_epoch']) . date('m', $entry['sdate_epoch']) . date('d', $entry['sdate_epoch']);\n-\t\t\t\t$endd = date('Y', $entry['edate_epoch']) . date('m', $entry['edate_epoch']) . date('d', $entry['edate_epoch']);\n-\t\t\t\t$matrix->setPeriod($title, $startd, $endd, $colors[$ind]);\n-\n-\t\t\t\t$subentries = $this->botodo->_list(0, 0, '', '', '', '', '', 'subs', $entry['id']);\n-\t\t\t\tforeach ($subentries as $subentry)\n-\t\t\t\t{\n-\t\t\t\t\tif ($subentry['sdate_epoch'] > 0 && $subentry['edate_epoch'] > 0)\n-\t\t\t\t\t{\n-\t\t\t\t\t\t$title = '<a href=\"' . phpgw::link('/index.php', array('menuaction' => 'todo.uitodo.view', 'todo_id' => $subentry['id'])) . '\">' . phpgw::strip_html($subentry['title']) . '</a>';\n-\t\t\t\t\t\t$startd = date('Y', $subentry['sdate_epoch']) . date('m', $subentry['sdate_epoch']) . date('d', $subentry['sdate_epoch']);\n-\t\t\t\t\t\t$endd = date('Y', $subentry['edate_epoch']) . date('m', $subentry['edate_epoch']) . date('d', $subentry['edate_epoch']);\n-\t\t\t\t\t\t$matrix->setPeriod(phpgw::strip_html($subentry['title']), $startd, $endd, $colors[$ind]);\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\t\t$matrix->out(phpgw::link('/', array('menuaction' => 'todo.uitodo.matrix')));\n+\t\tphpgw::redirect_link('/todo/view/todos/matrix', array(\n+\t\t\t'month' => $month,\n+\t\t\t'year' => $year,\n+\t\t));\n \t}\n }"},{"sha":"f194fc8d64b665eaa82e3dcb7cec336a5d4f021f","filename":"src/modules/todo/inc/hook_home.inc.php","status":"modified","additions":74,"deletions":16,"changes":90,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fhook_home.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fhook_home.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fhook_home.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -16,26 +16,84 @@\n \n $userSettings = Settings::getInstance()->get('user');\n \n-if (\n-\tisset($userSettings['preferences']['todo']['mainscreen_showevents'])\n-\t&& $userSettings['preferences']['todo']['mainscreen_showevents'] == True\n-)\n+$prefs = (array) ($userSettings['preferences']['todo'] ?? []);\n+$showOnMain = in_array(strtolower((string) ($prefs['mainscreen_showevents'] ?? '0')), ['1', 'true', 'yes'], true);\n+\n+if ($showOnMain)\n {\n-\t$todo = CreateObject('todo.ui');\n-\t$todo->bo->start = 0;\n-\t$todo->bo->limit = 5;\n-\t$todo->start = 0;\n-\t$todo->limit = 5;\n-\t$extra_data = '<td>' . \"\\n\" . $todo->show_list_body(False) . '</td>' . \"\\n\";\n+\t$flags = Settings::getInstance()->get('flags');\n+\t$saveApp = (string) ($flags['currentapp'] ?? '');\n+\t$flags['currentapp'] = 'todo';\n+\tSettings::getInstance()->set('flags', $flags);\n+\n+\t$maxmatches = (int) ($userSettings['preferences']['common']['maxmatchs'] ?? 0);\n+\t$userSettings['preferences']['common']['maxmatchs'] = 5;\n+\tSettings::getInstance()->set('user', $userSettings);\n+\n+\t$botodo = CreateObject('todo.botodo', True);\n+\t$todo_items = $botodo->_list(0, 5, '', '', 'todo_startdate', 'ASC', 0, 'all');\n+\n+\t$content = '';\n+\tif (is_array($todo_items) && count($todo_items))\n+\t{\n+\t\t$content .= '<ul class=\"todo-home-list\">';\n+\t\tforeach ($todo_items as $item)\n+\t\t{\n+\t\t\t$title = phpgw::strip_html((string) ($item['title'] ?? ''));\n+\t\t\tif (!$title)\n+\t\t\t{\n+\t\t\t\t$words = explode(' ', phpgw::strip_html((string) ($item['descr'] ?? '')));\n+\t\t\t\t$title = implode(' ', array_slice($words, 0, 4)) . ' ...';\n+\t\t\t}\n+\t\t\t$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');\n+\n+\t\t\t$url = phpgw::link('/todo/view/todos/' . (int) $item['id']);\n+\n+\t\t\t$content .= '<li><a href=\"' . $url . '\">' . $title . '</a></li>';\n+\t\t}\n+\t\t$content .= '</ul>';\n+\t}\n+\telse\n+\t{\n+\t\t$content .= '<p>' . lang('No entries') . '</p>';\n+\t}\n+\n+\t$content .= '<p><a href=\"' . phpgw::link('/todo/view/todos')\n+\t\t. '\">' . lang('Show all') . '</a></p>';\n+\n+\t$extra_data = '<td>' . \"\\n\" . $content . '</td>' . \"\\n\";\n \n \t$applications = new Applications();\n \t$app_id = $applications->name2id('todo');\n-\t$GLOBALS['portal_order'][] = $app_id;\n+\tif (!isset($GLOBALS['portal_order']) || !is_array($GLOBALS['portal_order']))\n+\t{\n+\t\t$GLOBALS['portal_order'] = [];\n+\t}\n+\tif (!in_array($app_id, $GLOBALS['portal_order'], true))\n+\t{\n+\t\t$GLOBALS['portal_order'][] = $app_id;\n+\t}\n \n-\t$portalbox = CreateObject('phpgwapi.portalbox');\n-\t$portalbox->set_params(array(\n-\t\t'app_id'\t=> $app_id,\n-\t\t'title'\t=> lang('todo')\n+\t$theme = Settings::getInstance()->get('theme');\n+\t$portalbox = CreateObject('phpgwapi.listbox', array(\n+\t\t'app_id' => $app_id,\n+\t\t'title' => lang('todo'),\n+\t\t'primary' => $theme['navbar_bg'] ?? '',\n+\t\t'secondary' => $theme['navbar_bg'] ?? '',\n+\t\t'tertiary' => $theme['navbar_bg'] ?? '',\n+\t\t'width' => '100%',\n+\t\t'outerborderwidth' => '0',\n+\t\t'header_background_image' => (new \\phpgwapi_common())->image('phpgwapi', 'bg_filler', '.png', False),\n \t));\n-\t$portalbox->draw($extra_data);\n+\t$html = $portalbox->draw($extra_data);\n+\n+\techo $html;\n+\t$flags['currentapp'] = $saveApp;\n+\tSettings::getInstance()->set('flags', $flags);\n+\n+\tif ($maxmatches > 0)\n+\t{\n+\t\t$userSettings['preferences']['common']['maxmatchs'] = $maxmatches;\n+\t\tSettings::getInstance()->set('user', $userSettings);\n+\t}\n }"},{"sha":"4082a8b031dfee3bafff7f0e9190fca3e13b2d00","filename":"src/modules/todo/inc/hook_settings.inc.php","status":"added","additions":17,"deletions":0,"changes":17,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fhook_settings.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Finc%2Fhook_settings.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Finc%2Fhook_settings.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,17 @@\n+<?php\n+\n+/**\n+ * Todo settings hook\n+ *\n+ * @package todo\n+ */\n+\n+use App\\modules\\preferences\\helpers\\PreferenceHelper;\n+\n+$preferenceHelper = PreferenceHelper::getInstance();\n+\n+$preferenceHelper->create_check_box(\n+\t'Show ToDo items on main screen',\n+\t'mainscreen_showevents',\n+\t'Display your todo items on the main screen'\n+);"},{"sha":"4f244b6e293fd7d7811138aa277aacd47b735fca","filename":"src/modules/todo/routes/Routes.php","status":"modified","additions":133,"deletions":0,"changes":133,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Froutes%2FRoutes.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Froutes%2FRoutes.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Froutes%2FRoutes.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -1,8 +1,141 @@\n <?php\n \n use App\\modules\\todo\\helpers\\RedirectHelper;\n+use App\\modules\\todo\\controllers\\TodoController;\n+use App\\modules\\todo\\viewcontrollers\\TodoViewController;\n use App\\modules\\phpgwapi\\security\\AccessVerifier;\n use App\\modules\\phpgwapi\\middleware\\SessionsMiddleware;\n+use Slim\\Csrf\\Guard;\n+use Slim\\Routing\\RouteCollectorProxy;\n+/** @var \\Slim\\App $app */\n+/** @var \\DI\\Container $container */\n+\n+$todoCsrfMiddleware = function ($request, $handler) use ($app)\n+{\n+\t$path = (string) $request->getUri()->getPath();\n+\t$method = strtoupper((string) $request->getMethod());\n+\t$csrfNameKey = 'csrf_name';\n+\t$csrfValueKey = 'csrf_value';\n+\n+\t// Keep DataTables server-side list POST working without CSRF requirement.\n+\tif ($method === 'POST' && $path === '/todo/todos')\n+\t{\n+\t\t$body = (array) ($request->getParsedBody() ?: []);\n+\t\t$query = $request->getQueryParams();\n+\t\tif (\n+\t\t\tisset($body['draw'])\n+\t\t\t|| isset($body['columns'])\n+\t\t\t|| isset($body['order'])\n+\t\t\t|| isset($query['draw'])\n+\t\t\t|| isset($query['columns'])\n+\t\t\t|| isset($query['order'])\n+\t\t)\n+\t\t{\n+\t\t\treturn $handler->handle($request);\n+\t\t}\n+\t}\n+\n+\tif (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true))\n+\t{\n+\t\t$query = $request->getQueryParams();\n+\t\t$headerName = $request->getHeaderLine($csrfNameKey);\n+\t\t$headerValue = $request->getHeaderLine($csrfValueKey);\n+\n+\t\t// Accept alternative header names and query params as fallback, then map\n+\t\t// to the header keys expected by slim/csrf.\n+\t\tif ($headerName === '' && $request->getHeaderLine('X-CSRF-NAME') !== '')\n+\t\t{\n+\t\t\t$headerName = $request->getHeaderLine('X-CSRF-NAME');\n+\t\t}\n+\t\tif ($headerValue === '' && $request->getHeaderLine('X-CSRF-VALUE') !== '')\n+\t\t{\n+\t\t\t$headerValue = $request->getHeaderLine('X-CSRF-VALUE');\n+\t\t}\n+\t\tif ($headerName === '' && !empty($query[$csrfNameKey]))\n+\t\t{\n+\t\t\t$headerName = (string) $query[$csrfNameKey];\n+\t\t}\n+\t\tif ($headerValue === '' && !empty($query[$csrfValueKey]))\n+\t\t{\n+\t\t\t$headerValue = (string) $query[$csrfValueKey];\n+\t\t}\n+\t\tif ($headerName !== '')\n+\t\t{\n+\t\t\t$request = $request->withHeader($csrfNameKey, $headerName);\n+\t\t}\n+\t\tif ($headerValue !== '')\n+\t\t{\n+\t\t\t$request = $request->withHeader($csrfValueKey, $headerValue);\n+\t\t}\n+\n+\t\t// slim/csrf reads parsed body arrays; decode JSON requests here so\n+\t\t// csrf_name/csrf_value in JSON payload can be validated.\n+\t\t$parsed = $request->getParsedBody();\n+\t\t$contentType = strtolower($request->getHeaderLine('Content-Type'));\n+\t\tif (!is_array($parsed) && str_contains($contentType, 'application/json'))\n+\t\t{\n+\t\t\t$raw = (string) $request->getBody();\n+\t\t\tif ($raw !== '')\n+\t\t\t{\n+\t\t\t\t$decoded = json_decode($raw, true);\n+\t\t\t\tif (is_array($decoded))\n+\t\t\t\t{\n+\t\t\t\t\t$request = $request->withParsedBody($decoded);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t$failureHandler = function ($request, $handler) use ($app)\n+\t{\n+\t\t$response = $app->getResponseFactory()->createResponse(400);\n+\t\t$path = (string) $request->getUri()->getPath();\n+\t\t$isApi = str_starts_with($path, '/todo/todos');\n+\n+\t\tif ($isApi)\n+\t\t{\n+\t\t\t$payload = json_encode(['error' => 'Failed CSRF check']);\n+\t\t\t$response->getBody()->write($payload !== false ? $payload : '{\"error\":\"Failed CSRF check\"}');\n+\t\t\treturn $response->withHeader('Content-Type', 'application/json');\n+\t\t}\n+\n+\t\t$response->getBody()->write('Failed CSRF check');\n+\t\treturn $response->withHeader('Content-Type', 'text/plain');\n+\t};\n+\n+\t$csrfStorage = null;\n+\t$guard = new Guard($app->getResponseFactory(), 'csrf', $csrfStorage, $failureHandler, 200, 16, true);\n+\treturn $guard->process($request, $handler);\n+};\n+\n+\n+$app->group('/todo', function (RouteCollectorProxy $group) use ($todoCsrfMiddleware)\n+{\n+\t$group->group('/view', function (RouteCollectorProxy $viewGroup)\n+\t{\n+\t\t$viewGroup->get('/todos', TodoViewController::class . ':index');\n+\t\t$viewGroup->map(['GET', 'POST'], '/todos/matrix', TodoViewController::class . ':matrix');\n+\t\t$viewGroup->get('/todos/add', TodoViewController::class . ':add');\n+\t\t$viewGroup->get('/todos/{id:[0-9]+}', TodoViewController::class . ':view');\n+\t\t$viewGroup->get('/todos/{id:[0-9]+}/delete', TodoViewController::class . ':delete');\n+\t\t$viewGroup->get('/todos/{id:[0-9]+}/edit', TodoViewController::class . ':edit');\n+\t})->add($todoCsrfMiddleware);\n+\n+\t$group->group('/todos', function (RouteCollectorProxy $todoGroup) use ($todoCsrfMiddleware)\n+\t{\n+\t\t$todoGroup->get('', TodoController::class . ':index');\n+\t\t$todoGroup->get('/export/csv', TodoController::class . ':exportCsv');\n+\t\t$todoGroup->post('', TodoController::class . ':store')->add($todoCsrfMiddleware);\n+\t\t$todoGroup->get('/{id:[0-9]+}', TodoController::class . ':show');\n+\t\t$todoGroup->put('/{id:[0-9]+}', TodoController::class . ':update')->add($todoCsrfMiddleware);\n+\t\t$todoGroup->patch('/{id:[0-9]+}/status', TodoController::class . ':updateStatus')->add($todoCsrfMiddleware);\n+\t\t$todoGroup->delete('/{id:[0-9]+}', TodoController::class . ':destroy')->add($todoCsrfMiddleware);\n+\t});\n+\n+\t$group->get('/categories', TodoController::class . ':categories');\n+})\n+\t->addMiddleware(new AccessVerifier($container))\n+\t->addMiddleware(new SessionsMiddleware($container));\n \n \n $app->get('/todo[/{params:.*}]', RedirectHelper::class . ':process')"},{"sha":"009d84605389c9faff27c7b7f17260d8eac2acbf","filename":"src/modules/todo/setup/phpgw_nn.lang","status":"modified","additions":62,"deletions":12,"changes":74,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Fphpgw_nn.lang","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Fphpgw_nn.lang","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fsetup%2Fphpgw_nn.lang?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -1,16 +1,66 @@\n-access type\ttodo\tnn\tAccess type\n-are you sure you want to delete this entry\ttodo\tnn\tEr du sikker på at du vil slette denne entry?\n-clear form\ttodo\tnn\tSlett innhold\n-completed\ttodo\tnn\tferdig\n-created by\ttodo\tnn\tLaget av\n+access type\ttodo\tnn\tTilgangstype\n+access changed\ttodo\tnn\tTilgang endra\n+add main project\ttodo\tnn\tLegg til hovudprosjekt\n+adds or edits a todo item\ttodo\tnn\tLegg til eller redigerer ei oppgåve\n+are you sure you want to delete this entry\ttodo\tnn\tEr du sikker på at du vil slette denne oppføringa?\n+assigned to\ttodo\tnn\tTildelt til\n+category changed\ttodo\tnn\tKategori endra\n+clear form\ttodo\tnn\tTøm innhald\n+completed\ttodo\tnn\tFerdig\n+created by\ttodo\tnn\tLaga av\n date due\ttodo\tnn\tFerdig før\n+deletes a todo item\ttodo\tnn\tSlettar ei oppgåve\n+description can not exceed 8000 characters in length\ttodo\tnn\tSkildringa kan ikkje vere lengre enn 8000 teikn !\n+description changed\ttodo\tnn\tSkildring endra\n+do you also want to delete all sub projects ?\ttodo\tnn\tVil du òg slette alle underprosjekt ?\n+edit main project\ttodo\tnn\tRediger hovudprosjekt\n+edit sub project\ttodo\tnn\tRediger underprosjekt\n+end date changed\ttodo\tnn\tSluttdato endra\n+end date\ttodo\tnn\tSluttdato\n+ending date can not be before start date\ttodo\tnn\tSluttdato kan ikkje vere før startdato !\n+entry added\ttodo\tnn\tOppføring lagt til\n+high\ttodo\tnn\tHøg\n last updated\ttodo\tnn\tSist oppdatert\n+low\ttodo\tnn\tLåg\n+nobody\ttodo\tnn\tIngen\n+normal\ttodo\tnn\tNormal\n+or: days from startdate:\ttodo\tnn\teller: dagar frå startdato:\n+or: select for today:\ttodo\tnn\teller: vel for i dag:\n+parent changed\ttodo\tnn\tForelder endra\n+parent project\ttodo\tnn\tForeldreprosjekt\n+please enter a title\ttodo\tnn\tVer venleg å skrive inn ein tittel !\n+returns a the total number of records in the database, must call list_todos first\ttodo\tnn\tReturnerer totalt tal oppføringar i databasen, må kalle list_todos først\n+show todo items on main screen\ttodo\tnn\tVis oppgåver på hovudskjerm\n+start date\ttodo\tnn\tStartdato\n+start date changed\ttodo\tnn\tStartdato endra\n status\ttodo\tnn\tStatus\n-todo\tcommon\tnn\tÅ-gjøre\n-todo list\tcommon\tnn\tÅ-gjøre liste\n-to do list\tcommon\tnn\tÅ-gjøre liste\n-todo list - add\ttodo\tnn\tÅ-gjøre liste - legg til\n-todo list - edit\ttodo\tnn\tÅ-gjøre liste - edit\n+status changed\ttodo\tnn\tStatus endra\n+title\ttodo\tnn\tTittel\n+title changed\ttodo\tnn\tTittel endra\n+details\ttodo\tnn\tDetaljar\n+id\ttodo\tnn\tID\n+old value\ttodo\tnn\tGammal verdi\n+user\ttodo\tnn\tBrukar\n+todo\tcommon\tnn\tÅ-gjere\n+todo list\tcommon\tnn\tÅ-gjere liste\n+to do list\tcommon\tnn\tÅ-gjere liste\n+todo list - add\ttodo\tnn\tÅ-gjere liste - legg til\n+todo list - edit\ttodo\tnn\tÅ-gjere liste - rediger\n+todo preferences\ttodo\tnn\tInnstillingar for oppgåver\n+page size\ttodo\tnn\tSidelengde\n+search\ttodo\tnn\tSøk\n urgency\ttodo\tnn\tPrioritet\n-which groups\ttodo\tnn\tHvilke grupper\n-you have entered an invailed date\ttodo\tnn\tdu har skrevet inn en ugyldig dato\n+urgency changed\ttodo\tnn\tPrioritet endra\n+view matrix of actual month\ttodo\tnn\tVis matrise for inneverande månad\n+view todo item\ttodo\tnn\tVis oppgåve\n+which groups\ttodo\tnn\tKva grupper\n+you can only enter numbers for days from now\ttodo\tnn\tDu kan berre skrive inn tal for dagar frå no !\n+you have entered an invalid date\ttodo\tnn\tDu har skrive inn ein ugyldig dato\n+you have entered an invalid start date\ttodo\tnn\tDu har skrive inn ein ugyldig startdato !\n+you have entered an invalid end date\ttodo\tnn\tDu har skrive inn ein ugyldig sluttdato !\n+add sub project\ttodo\tnn\tLegg til underprosjekt\n+all\ttodo\tnn\tAlle\n+select\ttodo\tnn\tVel\n+assigned to user\ttodo\tnn\tTildelt til brukar\n+assigned to group\ttodo\tnn\tTildelt til gruppe\n+%1 Group\tcommon\tnn\t%1 Gruppe\n\\ No newline at end of file"},{"sha":"a52e29b0fd4a4a949328513acd15769e12fc586f","filename":"src/modules/todo/setup/phpgw_no.lang","status":"modified","additions":55,"deletions":5,"changes":60,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Fphpgw_no.lang","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Fphpgw_no.lang","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fsetup%2Fphpgw_no.lang?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -1,16 +1,66 @@\n-access type\ttodo\tno\tAccess type\n-are you sure you want to delete this entry\ttodo\tno\tEr du sikker på at du vil slette denne entry?\n+access type\ttodo\tno\tTilgangstype\n+access changed\ttodo\tno\tTilgang endret\n+add main project\ttodo\tno\tLegg til hovedprosjekt\n+adds or edits a todo item\ttodo\tno\tLegger til eller redigerer en oppgave\n+are you sure you want to delete this entry\ttodo\tno\tEr du sikker på at du vil slette denne oppføringen?\n+assigned to\ttodo\tno\tTildelt til\n+category changed\ttodo\tno\tKategori endret\n clear form\ttodo\tno\tSlett innhold\n-completed\ttodo\tno\tferdig\n+completed\ttodo\tno\tFerdig\n created by\ttodo\tno\tLaget av\n date due\ttodo\tno\tFerdig før\n+deletes a todo item\ttodo\tno\tSletter en oppgave\n+description can not exceed 8000 characters in length\ttodo\tno\tBeskrivelsen kan ikke overstige 8000 tegn !\n+description changed\ttodo\tno\tBeskrivelse endret\n+do you also want to delete all sub projects ?\ttodo\tno\tVil du også slette alle underprosjekter ?\n+edit main project\ttodo\tno\tRediger hovedprosjekt\n+edit sub project\ttodo\tno\tRediger underprosjekt\n+end date changed\ttodo\tno\tSluttdato endret\n+end date\ttodo\tno\tSluttdato\n+ending date can not be before start date\ttodo\tno\tSluttdato kan ikke være før startdato !\n+entry added\ttodo\tno\tOppføring lagt til\n+high\ttodo\tno\tHøy\n last updated\ttodo\tno\tSist oppdatert\n+low\ttodo\tno\tLav\n+nobody\ttodo\tno\tIngen\n+normal\ttodo\tno\tNormal\n+or: days from startdate:\ttodo\tno\teller: dager fra startdato:\n+or: select for today:\ttodo\tno\teller: velg for i dag:\n+parent changed\ttodo\tno\tForelder endret\n+parent project\ttodo\tno\tForeldreprosjekt\n+please enter a title\ttodo\tno\tVennligst skriv inn en tittel !\n+returns a the total number of records in the database, must call list_todos first\ttodo\tno\tReturnerer totalt antall oppføringer i databasen, må kalle list_todos først\n+show todo items on main screen\ttodo\tno\tVis oppgaver på hovedskjerm\n+start date\ttodo\tno\tStartdato\n+start date changed\ttodo\tno\tStartdato endret\n status\ttodo\tno\tStatus\n+status changed\ttodo\tno\tStatus endret\n+title\ttodo\tno\tTittel\n+title changed\ttodo\tno\tTittel endret\n+details\ttodo\tno\tDetaljer\n+id\ttodo\tno\tID\n+old value\ttodo\tno\tGammel verdi\n+user\ttodo\tno\tBruker\n todo\tcommon\tno\tÅ-gjøre\n todo list\tcommon\tno\tÅ-gjøre liste\n to do list\tcommon\tno\tÅ-gjøre liste\n todo list - add\ttodo\tno\tÅ-gjøre liste - legg til\n-todo list - edit\ttodo\tno\tÅ-gjøre liste - edit\n+todo list - edit\ttodo\tno\tÅ-gjøre liste - rediger\n+todo preferences\ttodo\tno\tInnstillinger for oppgaver\n+page size\ttodo\tno\tSidelengde\n+search\ttodo\tno\tSøk\n urgency\ttodo\tno\tPrioritet\n+urgency changed\ttodo\tno\tPrioritet endret\n+view matrix of actual month\ttodo\tno\tVis matrise for inneværende måned\n+view todo item\ttodo\tno\tVis oppgave\n which groups\ttodo\tno\tHvilke grupper\n-you have entered an invailed date\ttodo\tno\tdu har skrevet inn en ugyldig dato\n+you can only enter numbers for days from now\ttodo\tno\tDu kan bare skrive inn tall for dager fra nå !\n+you have entered an invalid date\ttodo\tno\tDu har skrevet inn en ugyldig dato\n+you have entered an invalid start date\ttodo\tno\tDu har skrevet inn en ugyldig startdato !\n+you have entered an invalid end date\ttodo\tno\tDu har skrevet inn en ugyldig sluttdato !\n+add sub project\ttodo\tno\tLegg til underprosjekt\n+all\ttodo\tno\tAlle\n+select\ttodo\tno\tVelg\n+assigned to user\ttodo\tno\tTildelt til bruker\n+assigned to group\ttodo\tno\tTildelt til gruppe\n+%1 Group\tcommon\tno\t%1 Gruppe"},{"sha":"b26b1affc2c9b025675f01f47a042276c3a5cbc9","filename":"src/modules/todo/setup/setup.inc.php","status":"modified","additions":59,"deletions":57,"changes":116,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Fsetup.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Fsetup.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fsetup%2Fsetup.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -1,68 +1,70 @@\n <?php\n-\t/**\n-\t* Todo - setup\n-\t*\n-\t* @author Joseph Engo <jengo@phpgroupware.org>\n-\t* @copyright Copyright (C) 2000-2005 Free Software Foundation, Inc. http://www.fsf.org/\n-\t* @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n-\t* @package todo\n-\t* @subpackage setup\n-\t* @version $Id$\n-\t*/\n \n-\t$setup_info['todo']['name']\t\t= 'todo';\n-\t$setup_info['todo']['version']\t\t= '0.9.15.003';\n-\t$setup_info['todo']['app_order']\t= 9;\n-\t$setup_info['todo']['tables']\t\t= array('phpgw_todo');\n-\t$setup_info['todo']['enable']\t\t= 1;\n-\t$setup_info['todo']['app_group']\t= 'office';\n+/**\n+ * Todo - setup\n+ *\n+ * @author Joseph Engo <jengo@phpgroupware.org>\n+ * @copyright Copyright (C) 2000-2005 Free Software Foundation, Inc. http://www.fsf.org/\n+ * @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n+ * @package todo\n+ * @subpackage setup\n+ * @version $Id$\n+ */\n \n-\t$setup_info['todo']['description'] = 'phpGroupWare\\'s standard ToDo list';\n+$setup_info['todo']['name']\t\t= 'todo';\n+$setup_info['todo']['version']\t\t= '1.0.0.000';\n+$setup_info['todo']['app_order']\t= 9;\n+$setup_info['todo']['tables']\t\t= array('phpgw_todo');\n+$setup_info['todo']['enable']\t\t= 1;\n+$setup_info['todo']['app_group']\t= 'office';\n \n-\t$setup_info['todo']['author'][]= array\n-\t(\n-\t\t'name'\t=> 'Joseph Engo',\n-\t\t'email'\t=> 'jengo@phpgroupware.org'\n-\t);\n+$setup_info['todo']['description'] = 'Basic ToDo list';\n \n-\t$setup_info['todo']['author'][]= array\n-\t(\n-\t\t'name'\t=> 'Mark Logemann'\n-\t);\n+$setup_info['todo']['author'][] = array(\n+\t'name'\t=> 'Joseph Engo',\n+\t'email'\t=> 'jengo@phpgroupware.org'\n+);\n \n-\t$setup_info['todo']['author'][]= array\n-\t(\n-\t\t'name'\t=> 'Bettina Gille',\n-\t\t'email'\t=> 'ceb@phpgroupware.org'\n-\t);\n+$setup_info['todo']['author'][] = array(\n+\t'name'\t=> 'Mark Logemann'\n+);\n \n-\t$setup_info['todo']['maintainer'][] = array\n-\t(\n-\t\t'name'\t=> 'Bettina Gille',\n-\t\t'email'\t=> 'ceb@phpgroupware.org'\n-\t);\n+$setup_info['todo']['author'][] = array(\n+\t'name'\t=> 'Bettina Gille',\n+\t'email'\t=> 'ceb@phpgroupware.org'\n+);\n \n-\t/* The hooks this app includes, needed for hooks registration */\n-\t$setup_info['todo']['hooks'] = array\n-\t(\n-\t\t'home',\n-\t\t'deleteaccount',\n-\t\t'manual',\n-\t\t'menu'\t=> 'todo.menu.get_menu'\n-\t);\n+$setup_info['todo']['author'][] = array(\n+\t'name'\t=> 'Sigurd Nes',\n+\t'email'\t=> 'sigurdne@gmail.com'\n+);\n \n-\t/* Dependencies for this app to work */\n-\t$setup_info['todo']['depends'][] = array(\n-\t\t'appname' => 'phpgwapi',\n-\t\t'versions' => Array('0.9.17', '0.9.18')\n-\t);\n+$setup_info['todo']['maintainer'][] = array(\n+\t'name'\t=> 'Sigurd Nes',\n+\t'email'\t=> 'sigurdne@gmail.com'\n+);\n \n-\t$setup_info['todo']['depends'][] = array(\n-\t\t'appname' => 'admin',\n-\t\t'versions' => Array('0.9.17', '0.9.18')\n-\t);\n+/* The hooks this app includes, needed for hooks registration */\n+$setup_info['todo']['hooks'] = array(\n+\t'home',\n+\t'deleteaccount',\n+\t'manual',\n+\t'settings',\n+\t'menu'\t=> 'todo.menu.get_menu'\n+);\n \n-\t$setup_info['todo']['depends'][] = array(\n-\t\t'appname' => 'preferences',\n-\t\t'versions' => Array('0.9.17', '0.9.18')\n-\t);\n+/* Dependencies for this app to work */\n+$setup_info['todo']['depends'][] = array(\n+\t'appname' => 'phpgwapi',\n+\t'versions' => array('0.9.17', '0.9.18')\n+);\n+\n+$setup_info['todo']['depends'][] = array(\n+\t'appname' => 'admin',\n+\t'versions' => array('0.9.17', '0.9.18')\n+);\n+\n+$setup_info['todo']['depends'][] = array(\n+\t'appname' => 'preferences',\n+\t'versions' => array('0.9.17', '0.9.18')\n+);"},{"sha":"bee07f83bba5f90645e91bb51d46c14e154bcf27","filename":"src/modules/todo/setup/tables_update.inc.php","status":"modified","additions":582,"deletions":738,"changes":1320,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Ftables_update.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fsetup%2Ftables_update.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fsetup%2Ftables_update.inc.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -1,764 +1,608 @@\n <?php\n-\t/**\n-\t* Todo - setup\n-\t*\n-\t* @author Joseph Engo <jengo@phpgroupware.org>\n-\t* @copyright Copyright (C) 2000-2005 Free Software Foundation, Inc. http://www.fsf.org/\n-\t* @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n-\t* @package todo\n-\t* @subpackage setup\n-\t* @version $Id$\n-\t*/\n-\n-\t/**\n-\t * Update from 0.9.2 to 0.9.3\n-\t * \n-\t * @param string $table Table name\n-\t * @param string $field Field name\n-\t */\n-\tfunction todo_v0_9_2to0_9_3update_owner($table, $field)\n-\t{\n-\t\tglobal $phpgw_setup;\n \n-\t\t$phpgw_setup->oProc->query(\"select distinct($field) from $table\");\n-\t\tif ($phpgw_setup->oProc->num_rows())\n+/**\n+ * Todo - setup\n+ *\n+ * @author Joseph Engo <jengo@phpgroupware.org>\n+ * @copyright Copyright (C) 2000-2005 Free Software Foundation, Inc. http://www.fsf.org/\n+ * @license http://www.gnu.org/licenses/gpl.html GNU General Public License\n+ * @package todo\n+ * @subpackage setup\n+ * @version $Id$\n+ */\n+\n+/**\n+ * Update from 0.9.2 to 0.9.3\n+ * \n+ * @param string $table Table name\n+ * @param string $field Field name\n+ */\n+function todo_v0_9_2to0_9_3update_owner($table, $field, $oProc)\n+{\n+\t$oProc->query(\"select distinct($field) from $table\");\n+\t$owner = array();\n+\tif ($oProc->num_rows())\n+\t{\n+\t\twhile ($oProc->next_record())\n \t\t{\n-\t\t\twhile ($phpgw_setup->oProc->next_record())\n-\t\t\t{\n-\t\t\t\t$owner[count($owner)] = $phpgw_setup->oProc->f($field);\n-\t\t\t}\n-\t\t\tif($phpgw_setup->alessthanb($setup_info['phpgwapi']['currentver'],'0.9.10pre4'))\n-\t\t\t{\n-\t\t\t\t$acctstbl = 'accounts';\n-\t\t\t}\n-\t\t\telse\n-\t\t\t{\n-\t\t\t\t$acctstbl = 'phpgw_accounts';\n-\t\t\t}\n-\t\t\tfor($i=0;$i<count($owner);$i++)\n-\t\t\t{\n-\t\t\t\t$phpgw_setup->oProc->query(\"SELECT account_id FROM $acctstbl WHERE account_lid='\".$owner[$i].\"'\");\n-\t\t\t\t$phpgw_setup->oProc->next_record();\n-\t\t\t\t$phpgw_setup->oProc->query(\"UPDATE $table SET $field=\".$phpgw_setup->oProc->f(\"account_id\").\" WHERE $field='\".$owner[$i].\"'\");\n-\t\t\t}\n+\t\t\t$owner[count($owner)] = $oProc->f($field);\n+\t\t}\n+\t\tif ($oProc->alessthanb($GLOBALS['setup_info']['phpgwapi']['currentver'], '0.9.10pre4'))\n+\t\t{\n+\t\t\t$acctstbl = 'accounts';\n+\t\t}\n+\t\telse\n+\t\t{\n+\t\t\t$acctstbl = 'phpgw_accounts';\n+\t\t}\n+\t\tfor ($i = 0; $i < count($owner); $i++)\n+\t\t{\n+\t\t\t$oProc->query(\"SELECT account_id FROM $acctstbl WHERE account_lid='\" . $owner[$i] . \"'\");\n+\t\t\t$oProc->next_record();\n+\t\t\t$oProc->query(\"UPDATE $table SET $field=\" . $oProc->f(\"account_id\") . \" WHERE $field='\" . $owner[$i] . \"'\");\n \t\t}\n-\t\t$phpgw_setup->oProc->AlterColumn($table, $field, array('type' => 'int', 'precision' => 4, 'nullable' => false, 'default' => 0));\n-\t}\n-\n-\t$test[] = '0.9.1';\n-\tfunction todo_upgrade0_9_1()\n-\t{\n-\t\tglobal $setup_info;\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.2';\n-\tfunction todo_upgrade0_9_2()\n-\t{\n-\t\tglobal $setup_info;\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre1';\n-\tfunction todo_upgrade0_9_3pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\n-\t\ttodo_v0_9_2to0_9_3update_owner('todo','todo_owner');\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre2';\n-\tfunction todo_upgrade0_9_3pre2()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre3';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre3';\n-\tfunction todo_upgrade0_9_3pre3()\n-\t{\n-\t\tglobal $setup_info, $phpgw_setup;\n-\n-\t\t$phpgw_setup->oProc->AddColumn(\"todo\", \"todo_id_parent\", array(\"type\" => \"int\", \"precision\" => 4, \"nullable\" => false, \"default\" => \"0\"));\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre4';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre4';\n-\tfunction todo_upgrade0_9_3pre4()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre4';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre5';\n-\tfunction todo_upgrade0_9_3pre5()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre6';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre6';\n-\tfunction todo_upgrade0_9_3pre6()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre7';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre7';\n-\tfunction todo_upgrade0_9_3pre7()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre8';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre8';\n-\tfunction todo_upgrade0_9_3pre8()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre9';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre9';\n-\tfunction todo_upgrade0_9_3pre9()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3pre10';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3pre10';\n-\tfunction todo_upgrade0_9_3pre10()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.3';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.3';\n-\tfunction todo_upgrade0_9_3()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.4pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.4pre1';\n-\tfunction todo_upgrade0_9_4pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.4pre2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.4pre2';\n-\tfunction todo_upgrade0_9_4pre2()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.4pre3';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.4pre3';\n-\tfunction todo_upgrade0_9_4pre3()\n-\t{\n-\t\tglobal $phpgw_info, $phpgw_setup;\n-\n-\t\t$phpgw_setup->oProc->AddColumn(\"todo\", \"todo_startdate\", array(\"type\" => \"int\", \"precision\" => 4));\n-\t\t$phpgw_setup->oProc->RenameColumn(\"todo\", \"todo_datedue\", \"todo_enddate\");\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.4pre4';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.4pre4';\n-\tfunction todo_upgrade0_9_4pre4()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.4pre5';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.4pre5';\n-\tfunction todo_upgrade0_9_4pre5()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.4';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.4';\n-\tfunction todo_upgrade0_9_4()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.5pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\n-\t$test[] = '0.9.5pre1';\n-\tfunction todo_upgrade0_9_5pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.5pre2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.5pre2';\n-\tfunction todo_upgrade0_9_5pre2()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.5';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.5';\n-\tfunction todo_upgrade0_9_5()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.6';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.6';\n-\tfunction todo_upgrade0_9_6()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.7pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.7pre1';\n-\tfunction todo_upgrade0_9_7pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.7pre2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.7pre2';\n-\tfunction todo_upgrade0_9_7pre2()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.7pre3';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.7pre3';\n-\tfunction todo_upgrade0_9_7pre3()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.7';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.7';\n-\tfunction todo_upgrade0_9_7()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.8pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.8pre1';\n-\tfunction todo_upgrade0_9_8pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.8pre2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.8pre2';\n-\tfunction todo_upgrade0_9_8pre2()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.8pre3';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.8pre3';\n-\tfunction todo_upgrade0_9_8pre3()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.8pre4';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.8pre4';\n-\tfunction todo_upgrade0_9_8pre4()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.8pre5';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.8pre5';\n-\tfunction todo_upgrade0_9_8pre5()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.9pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.9pre1';\n-\tfunction todo_upgrade0_9_9pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.9';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.9';\n-\tfunction todo_upgrade0_9_9()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre1';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre1';\n-\tfunction todo_upgrade0_9_10pre1()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre2';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre2';\n-\tfunction todo_upgrade0_9_10pre2()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre3';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre3';\n-\tfunction todo_upgrade0_9_10pre3()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre4';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre4';\n-\tfunction todo_upgrade0_9_10pre4()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre5';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre5';\n-\tfunction todo_upgrade0_9_10pre5()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre6';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre6';\n-\tfunction todo_upgrade0_9_10pre6()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre7';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre7';\n-\tfunction todo_upgrade0_9_10pre7()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre8';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre8';\n-\tfunction todo_upgrade0_9_10pre8()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre9';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre9';\n-\tfunction todo_upgrade0_9_10pre9()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre10';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre10';\n-\tfunction todo_upgrade0_9_10pre10()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre11';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre11';\n-\tfunction todo_upgrade0_9_10pre11()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre12';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre12';\n-\tfunction todo_upgrade0_9_10pre12()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre13';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre13';\n-\tfunction todo_upgrade0_9_10pre13()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre14';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre14';\n-\tfunction todo_upgrade0_9_10pre14()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre15';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre15';\n-\tfunction todo_upgrade0_9_10pre15()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre16';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre16';\n-\tfunction todo_upgrade0_9_10pre16()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre17';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre17';\n-\tfunction todo_upgrade0_9_10pre17()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre18';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre18';\n-\tfunction todo_upgrade0_9_10pre18()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre19';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre19';\n-\tfunction todo_upgrade0_9_10pre19()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre20';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre20';\n-\tfunction todo_upgrade0_9_10pre20()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre21';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre21';\n-\tfunction todo_upgrade0_9_10pre21()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre22';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre22';\n-\tfunction todo_upgrade0_9_10pre22()\n-\t{\n-\t\tglobal $setup_info, $phpgw_setup;\n-\n-\t\t$phpgw_setup->oProc->RenameTable('todo','phpgw_todo');\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre23';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre23';\n-\tfunction todo_upgrade0_9_10pre23()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre24';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre24';\n-\tfunction todo_upgrade0_9_10pre24()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre25';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre25';\n-\tfunction todo_upgrade0_9_10pre25()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre26';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre26';\n-\tfunction todo_upgrade0_9_10pre26()\n-\t{\n-\t\tglobal $setup_info,$phpgw_setup;\n-\n-\t\t$phpgw_setup->oProc->AddColumn('phpgw_todo','todo_cat',array('type' => 'int','precision' => 4,'nullable' => True));\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre27';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre27';\n-\tfunction todo_upgrade0_9_10pre27()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10pre28';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10pre28';\n-\tfunction todo_upgrade0_9_10pre28()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.10';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.10';\n-\tfunction todo_upgrade0_9_10()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.001';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11';\n-\tfunction todo_upgrade0_9_11()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.001';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.001';\n-\tfunction todo_upgrade0_9_11_001()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.002';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.003';\n-\tfunction todo_upgrade0_9_11_003()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.004';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.004';\n-\tfunction todo_upgrade0_9_11_004()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.005';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.005';\n-\tfunction todo_upgrade0_9_11_005()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.006';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.006';\n-\tfunction todo_upgrade0_9_11_006()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.007';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.007';\n-\tfunction todo_upgrade0_9_11_007()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.008';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.008';\n-\tfunction todo_upgrade0_9_11_008()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.009';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.009';\n-\tfunction todo_upgrade0_9_11_009()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.010';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.010';\n-\tfunction todo_upgrade0_9_11_010()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.11.011';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.11.011';\n-\tfunction todo_upgrade0_9_11_011()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.13.001';\n-\t\treturn $setup_info['todo']['currentver'];\n \t}\n-\n-\t$test[] = '0.9.13.001';\n-\tfunction todo_upgrade0_9_13_001()\n-\t{\n-\t\tglobal $setup_info;\n-\t\t$setup_info['todo']['currentver'] = '0.9.13.002';\n-\t\treturn $setup_info['todo']['currentver'];\n+\t$oProc->AlterColumn($table, $field, array('type' => 'int', 'precision' => 4, 'nullable' => false, 'default' => 0));\n+}\n+\n+$test[] = '0.9.1';\n+function todo_upgrade0_9_1($oProc)\n+{\n+\treturn '0.9.2';\n+}\n+\n+$test[] = '0.9.2';\n+function todo_upgrade0_9_2($oProc)\n+{\n+\treturn '0.9.3pre1';\n+}\n+\n+$test[] = '0.9.3pre1';\n+function todo_upgrade0_9_3pre1($oProc)\n+{\n+\ttodo_v0_9_2to0_9_3update_owner('todo', 'todo_owner', $oProc);\n+\n+\treturn '0.9.3pre2';\n+}\n+\n+$test[] = '0.9.3pre2';\n+function todo_upgrade0_9_3pre2($oProc)\n+{\n+\treturn '0.9.3pre3';\n+}\n+\n+$test[] = '0.9.3pre3';\n+function todo_upgrade0_9_3pre3($oProc)\n+{\n+\t$oProc->AddColumn(\"todo\", \"todo_id_parent\", array(\"type\" => \"int\", \"precision\" => 4, \"nullable\" => false, \"default\" => \"0\"));\n+\n+\treturn '0.9.3pre4';\n+}\n+\n+$test[] = '0.9.3pre4';\n+function todo_upgrade0_9_3pre4($oProc)\n+{\n+\treturn '0.9.3pre4';\n+}\n+\n+$test[] = '0.9.3pre5';\n+function todo_upgrade0_9_3pre5($oProc)\n+{\n+\treturn '0.9.3pre6';\n+}\n+\n+$test[] = '0.9.3pre6';\n+function todo_upgrade0_9_3pre6($oProc)\n+{\n+\treturn '0.9.3pre7';\n+}\n+\n+$test[] = '0.9.3pre7';\n+function todo_upgrade0_9_3pre7($oProc)\n+{\n+\treturn '0.9.3pre8';\n+}\n+\n+$test[] = '0.9.3pre8';\n+function todo_upgrade0_9_3pre8($oProc)\n+{\n+\treturn '0.9.3pre9';\n+}\n+\n+$test[] = '0.9.3pre9';\n+function todo_upgrade0_9_3pre9($oProc)\n+{\n+\treturn '0.9.3pre10';\n+}\n+\n+$test[] = '0.9.3pre10';\n+function todo_upgrade0_9_3pre10($oProc)\n+{\n+\treturn '0.9.3';\n+}\n+\n+$test[] = '0.9.3';\n+function todo_upgrade0_9_3($oProc)\n+{\n+\treturn '0.9.4pre1';\n+}\n+\n+$test[] = '0.9.4pre1';\n+function todo_upgrade0_9_4pre1($oProc)\n+{\n+\treturn '0.9.4pre2';\n+}\n+\n+$test[] = '0.9.4pre2';\n+function todo_upgrade0_9_4pre2($oProc)\n+{\n+\treturn '0.9.4pre3';\n+}\n+\n+$test[] = '0.9.4pre3';\n+function todo_upgrade0_9_4pre3($oProc)\n+{\n+\t$oProc->AddColumn(\"todo\", \"todo_startdate\", array(\"type\" => \"int\", \"precision\" => 4));\n+\t$oProc->RenameColumn(\"todo\", \"todo_datedue\", \"todo_enddate\");\n+\n+\treturn '0.9.4pre4';\n+}\n+\n+$test[] = '0.9.4pre4';\n+function todo_upgrade0_9_4pre4($oProc)\n+{\n+\treturn '0.9.4pre5';\n+}\n+\n+$test[] = '0.9.4pre5';\n+function todo_upgrade0_9_4pre5($oProc)\n+{\n+\treturn '0.9.4';\n+}\n+\n+$test[] = '0.9.4';\n+function todo_upgrade0_9_4($oProc)\n+{\n+\treturn '0.9.5pre1';\n+}\n+\n+\n+$test[] = '0.9.5pre1';\n+function todo_upgrade0_9_5pre1($oProc)\n+{\n+\treturn '0.9.5pre2';\n+}\n+\n+$test[] = '0.9.5pre2';\n+function todo_upgrade0_9_5pre2($oProc)\n+{\n+\treturn '0.9.5';\n+}\n+\n+$test[] = '0.9.5';\n+function todo_upgrade0_9_5($oProc)\n+{\n+\treturn '0.9.6';\n+}\n+\n+$test[] = '0.9.6';\n+function todo_upgrade0_9_6($oProc)\n+{\n+\treturn '0.9.7pre1';\n+}\n+\n+$test[] = '0.9.7pre1';\n+function todo_upgrade0_9_7pre1($oProc)\n+{\n+\treturn '0.9.7pre2';\n+}\n+\n+$test[] = '0.9.7pre2';\n+function todo_upgrade0_9_7pre2($oProc)\n+{\n+\treturn '0.9.7pre3';\n+}\n+\n+$test[] = '0.9.7pre3';\n+function todo_upgrade0_9_7pre3($oProc)\n+{\n+\treturn '0.9.7';\n+}\n+\n+$test[] = '0.9.7';\n+function todo_upgrade0_9_7($oProc)\n+{\n+\treturn '0.9.8pre1';\n+}\n+\n+$test[] = '0.9.8pre1';\n+function todo_upgrade0_9_8pre1($oProc)\n+{\n+\treturn '0.9.8pre2';\n+}\n+\n+$test[] = '0.9.8pre2';\n+function todo_upgrade0_9_8pre2($oProc)\n+{\n+\treturn '0.9.8pre3';\n+}\n+\n+$test[] = '0.9.8pre3';\n+function todo_upgrade0_9_8pre3($oProc)\n+{\n+\treturn '0.9.8pre4';\n+}\n+\n+$test[] = '0.9.8pre4';\n+function todo_upgrade0_9_8pre4($oProc)\n+{\n+\treturn '0.9.8pre5';\n+}\n+\n+$test[] = '0.9.8pre5';\n+function todo_upgrade0_9_8pre5($oProc)\n+{\n+\treturn '0.9.9pre1';\n+}\n+\n+$test[] = '0.9.9pre1';\n+function todo_upgrade0_9_9pre1($oProc)\n+{\n+\treturn '0.9.9';\n+}\n+\n+$test[] = '0.9.9';\n+function todo_upgrade0_9_9($oProc)\n+{\n+\treturn '0.9.10pre1';\n+}\n+\n+$test[] = '0.9.10pre1';\n+function todo_upgrade0_9_10pre1($oProc)\n+{\n+\treturn '0.9.10pre2';\n+}\n+\n+$test[] = '0.9.10pre2';\n+function todo_upgrade0_9_10pre2($oProc)\n+{\n+\treturn '0.9.10pre3';\n+}\n+\n+$test[] = '0.9.10pre3';\n+function todo_upgrade0_9_10pre3($oProc)\n+{\n+\treturn '0.9.10pre4';\n+}\n+\n+$test[] = '0.9.10pre4';\n+function todo_upgrade0_9_10pre4($oProc)\n+{\n+\treturn '0.9.10pre5';\n+}\n+\n+$test[] = '0.9.10pre5';\n+function todo_upgrade0_9_10pre5($oProc)\n+{\n+\treturn '0.9.10pre6';\n+}\n+\n+$test[] = '0.9.10pre6';\n+function todo_upgrade0_9_10pre6($oProc)\n+{\n+\treturn '0.9.10pre7';\n+}\n+\n+$test[] = '0.9.10pre7';\n+function todo_upgrade0_9_10pre7($oProc)\n+{\n+\treturn '0.9.10pre8';\n+}\n+\n+$test[] = '0.9.10pre8';\n+function todo_upgrade0_9_10pre8($oProc)\n+{\n+\treturn '0.9.10pre9';\n+}\n+\n+$test[] = '0.9.10pre9';\n+function todo_upgrade0_9_10pre9($oProc)\n+{\n+\treturn '0.9.10pre10';\n+}\n+\n+$test[] = '0.9.10pre10';\n+function todo_upgrade0_9_10pre10($oProc)\n+{\n+\treturn '0.9.10pre11';\n+}\n+\n+$test[] = '0.9.10pre11';\n+function todo_upgrade0_9_10pre11($oProc)\n+{\n+\treturn '0.9.10pre12';\n+}\n+\n+$test[] = '0.9.10pre12';\n+function todo_upgrade0_9_10pre12($oProc)\n+{\n+\treturn '0.9.10pre13';\n+}\n+\n+$test[] = '0.9.10pre13';\n+function todo_upgrade0_9_10pre13($oProc)\n+{\n+\treturn '0.9.10pre14';\n+}\n+\n+$test[] = '0.9.10pre14';\n+function todo_upgrade0_9_10pre14($oProc)\n+{\n+\treturn '0.9.10pre15';\n+}\n+\n+$test[] = '0.9.10pre15';\n+function todo_upgrade0_9_10pre15($oProc)\n+{\n+\treturn '0.9.10pre16';\n+}\n+\n+$test[] = '0.9.10pre16';\n+function todo_upgrade0_9_10pre16($oProc)\n+{\n+\treturn '0.9.10pre17';\n+}\n+\n+$test[] = '0.9.10pre17';\n+function todo_upgrade0_9_10pre17($oProc)\n+{\n+\treturn '0.9.10pre18';\n+}\n+\n+$test[] = '0.9.10pre18';\n+function todo_upgrade0_9_10pre18($oProc)\n+{\n+\treturn '0.9.10pre19';\n+}\n+\n+$test[] = '0.9.10pre19';\n+function todo_upgrade0_9_10pre19($oProc)\n+{\n+\treturn '0.9.10pre20';\n+}\n+\n+$test[] = '0.9.10pre20';\n+function todo_upgrade0_9_10pre20($oProc)\n+{\n+\treturn '0.9.10pre21';\n+}\n+\n+$test[] = '0.9.10pre21';\n+function todo_upgrade0_9_10pre21($oProc)\n+{\n+\treturn '0.9.10pre22';\n+}\n+\n+$test[] = '0.9.10pre22';\n+function todo_upgrade0_9_10pre22($oProc)\n+{\n+\t$oProc->RenameTable('todo', 'phpgw_todo');\n+\n+\treturn '0.9.10pre23';\n+}\n+\n+$test[] = '0.9.10pre23';\n+function todo_upgrade0_9_10pre23($oProc)\n+{\n+\treturn '0.9.10pre24';\n+}\n+\n+$test[] = '0.9.10pre24';\n+function todo_upgrade0_9_10pre24($oProc)\n+{\n+\treturn '0.9.10pre25';\n+}\n+\n+$test[] = '0.9.10pre25';\n+function todo_upgrade0_9_10pre25($oProc)\n+{\n+\treturn '0.9.10pre26';\n+}\n+\n+$test[] = '0.9.10pre26';\n+function todo_upgrade0_9_10pre26($oProc)\n+{\n+\t$oProc->AddColumn('phpgw_todo', 'todo_cat', array('type' => 'int', 'precision' => 4, 'nullable' => True));\n+\n+\treturn '0.9.10pre27';\n+}\n+\n+$test[] = '0.9.10pre27';\n+function todo_upgrade0_9_10pre27($oProc)\n+{\n+\treturn '0.9.10pre28';\n+}\n+\n+$test[] = '0.9.10pre28';\n+function todo_upgrade0_9_10pre28($oProc)\n+{\n+\treturn '0.9.10';\n+}\n+\n+$test[] = '0.9.10';\n+function todo_upgrade0_9_10($oProc)\n+{\n+\treturn '0.9.11.001';\n+}\n+\n+$test[] = '0.9.11';\n+function todo_upgrade0_9_11($oProc)\n+{\n+\treturn '0.9.11.001';\n+}\n+\n+$test[] = '0.9.11.001';\n+function todo_upgrade0_9_11_001($oProc)\n+{\n+\treturn '0.9.11.002';\n+}\n+\n+$test[] = '0.9.11.003';\n+function todo_upgrade0_9_11_003($oProc)\n+{\n+\treturn '0.9.11.004';\n+}\n+\n+$test[] = '0.9.11.004';\n+function todo_upgrade0_9_11_004($oProc)\n+{\n+\treturn '0.9.11.005';\n+}\n+\n+$test[] = '0.9.11.005';\n+function todo_upgrade0_9_11_005($oProc)\n+{\n+\treturn '0.9.11.006';\n+}\n+\n+$test[] = '0.9.11.006';\n+function todo_upgrade0_9_11_006($oProc)\n+{\n+\treturn '0.9.11.007';\n+}\n+\n+$test[] = '0.9.11.007';\n+function todo_upgrade0_9_11_007($oProc)\n+{\n+\treturn '0.9.11.008';\n+}\n+\n+$test[] = '0.9.11.008';\n+function todo_upgrade0_9_11_008($oProc)\n+{\n+\treturn '0.9.11.009';\n+}\n+\n+$test[] = '0.9.11.009';\n+function todo_upgrade0_9_11_009($oProc)\n+{\n+\treturn '0.9.11.010';\n+}\n+\n+$test[] = '0.9.11.010';\n+function todo_upgrade0_9_11_010($oProc)\n+{\n+\treturn '0.9.11.011';\n+}\n+\n+$test[] = '0.9.11.011';\n+function todo_upgrade0_9_11_011($oProc)\n+{\n+\treturn '0.9.13.001';\n+}\n+\n+$test[] = '0.9.13.001';\n+function todo_upgrade0_9_13_001($oProc)\n+{\n+\treturn '0.9.13.002';\n+}\n+\n+$test[] = '0.9.13.002';\n+function todo_upgrade0_9_13_002($oProc)\n+{\n+\t$oProc->AddColumn('phpgw_todo', 'todo_id_main', array('type' => 'int', 'precision' => 4, 'default' => 0, 'nullable' => False));\n+\t$oProc->AddColumn('phpgw_todo', 'todo_level', array('type' => 'int', 'precision' => 2, 'default' => 0, 'nullable' => False));\n+\t$oProc->AlterColumn('phpgw_todo', 'todo_id_parent', array('type' => 'int', 'precision' => 4, 'default' => 0, 'nullable' => False));\n+\t$oProc->AlterColumn('phpgw_todo', 'todo_cat', array('type' => 'int', 'precision' => 4, 'default' => 0, 'nullable' => False));\n+\t$oProc->AlterColumn('phpgw_todo', 'todo_enddate', array('type' => 'int', 'precision' => 4, 'default' => 0, 'nullable' => False));\n+\n+\t$db = $oProc->db;\n+\n+\t$oProc->query(\"select todo_id from phpgw_todo where todo_id_main='0'\");\n+\n+\twhile ($oProc->next_record())\n+\t{\n+\t\t$db->query(\"update phpgw_todo set todo_id_main='\" . $oProc->f('todo_id') . \"' \"\n+\t\t\t. \"where todo_id='\" . $oProc->f('todo_id') . \"'\");\n+\t}\n+\n+\t$oProc->query(\"select todo_id_parent from phpgw_todo\");\n+\n+\twhile ($oProc->next_record())\n+\t{\n+\t\tif ($oProc->f('todo_id_parent') != 0)\n+\t\t{\n+\t\t\t$db->query(\"update phpgw_todo set todo_id_main='\" . $oProc->f('todo_id_parent') . \"',\"\n+\t\t\t\t. \"todo_level='1' where todo_id_parent='\" . $oProc->f('todo_id_parent') . \"'\");\n+\t\t}\n \t}\n \n-\t$test[] = '0.9.13.002';\n-\tfunction todo_upgrade0_9_13_002()\n-\t{\n-\t\tglobal $setup_info, $phpgw_setup;\n-\n-\t\t$phpgw_setup->oProc->AddColumn('phpgw_todo','todo_id_main',array('type' => 'int','precision' => 4,'default' => 0,'nullable' => False));\n-\t\t$phpgw_setup->oProc->AddColumn('phpgw_todo','todo_level',array('type' => 'int','precision' => 2,'default' => 0,'nullable' => False));\n-\t\t$phpgw_setup->oProc->AlterColumn('phpgw_todo','todo_id_parent',array('type' => 'int','precision' => 4,'default' => 0,'nullable' => False));\n-\t\t$phpgw_setup->oProc->AlterColumn('phpgw_todo','todo_cat',array('type' => 'int','precision' => 4,'default' => 0,'nullable' => False));\n-\t\t$phpgw_setup->oProc->AlterColumn('phpgw_todo','todo_enddate',array('type' => 'int','precision' => 4,'default' => 0,'nullable' => False));\n+\treturn '0.9.13.003';\n+}\n \n-\t\t$db = $phpgw_setup->db;\n+$test[] = '0.9.13.003';\n+function todo_upgrade0_9_13_003($oProc)\n+{\n+\t$oProc->AddColumn('phpgw_todo', 'todo_title', array('type' => 'varchar', 'precision' => 255, 'nullable' => False));\n+\t$oProc->AlterColumn('phpgw_todo', 'todo_owner', array('type' => 'int', 'precision' => 4, 'default' => 0, 'nullable' => False));\n \n-\t\t$phpgw_setup->oProc->query(\"select todo_id from phpgw_todo where todo_id_main='0'\");\n+\treturn '0.9.13.004';\n+}\n \n-\t\twhile ($phpgw_setup->oProc->next_record())\n-\t\t{\n-\t\t\t$db->query(\"update phpgw_todo set todo_id_main='\" . $phpgw_setup->oProc->f('todo_id') . \"' \"\n-\t\t\t\t\t\t. \"where todo_id='\" . $phpgw_setup->oProc->f('todo_id') . \"'\");\n+$test[] = '0.9.13.004';\n+function todo_upgrade0_9_13_004($oProc)\n+{\n+\treturn '0.9.15.001';\n+}\n \n-\t\t}\n+$test[] = '0.9.14';\n+function todo_upgrade0_9_14($oProc)\n+{\n+\treturn '0.9.15.001';\n+}\n \n-\t\t$phpgw_setup->oProc->query(\"select todo_id_parent from phpgw_todo\");\n+$test[] = '0.9.14.500';\n+function todo_upgrade0_9_14_500($oProc)\n+{\n+\treturn '0.9.15.001';\n+}\n \n-\t\twhile ($phpgw_setup->oProc->next_record())\n-\t\t{\n-\t\t\tif ($phpgw_setup->oProc->f('todo_id_parent') != 0)\n-\t\t\t{\n-\t\t\t\t$db->query(\"update phpgw_todo set todo_id_main='\" . $phpgw_setup->oProc->f('todo_id_parent') . \"',\"\n-\t\t\t\t\t\t\t. \"todo_level='1' where todo_id_parent='\" . $phpgw_setup->oProc->f('todo_id_parent') . \"'\");\n-\t\t\t}\n-\t\t}\n+$test[] = '0.9.15.001';\n+function todo_upgrade0_9_15_001($oProc)\n+{\n+\t$oProc->m_odb->transaction_begin();\n+\t$oProc->AddColumn('phpgw_todo', 'todo_assigned', array('type' => 'varchar', 'precision' => 255, 'nullable' => False));\n+\t$oProc->AddColumn('phpgw_todo', 'assigned_group', array('type' => 'varchar', 'precision' => 255, 'nullable' => False));\n \n-\t\t$setup_info['todo']['currentver'] = '0.9.13.003';\n-\t\treturn $setup_info['todo']['currentver'];\n-\t}\n-\n-\t$test[] = '0.9.13.003';\n-\tfunction todo_upgrade0_9_13_003()\n+\tif ($oProc->m_odb->transaction_commit())\n \t{\n-\t\tglobal $setup_info, $phpgw_setup;\n-\n-\t\t$phpgw_setup->oProc->AddColumn('phpgw_todo','todo_title',array('type' => 'varchar','precision' => 255,'nullable' => False));\n-\t\t$phpgw_setup->oProc->AlterColumn('phpgw_todo','todo_owner',array('type' => 'int','precision' => 4,'default' => 0,'nullable' => False));\n-\n-\t\t$setup_info['todo']['currentver'] = '0.9.13.004';\n-\t\treturn $setup_info['todo']['currentver'];\n+\t\treturn '0.9.15.002';\n \t}\n+}\n \n-\t$test[] = '0.9.13.004';\n-\tfunction todo_upgrade0_9_13_004()\n-\t{\n-\t\t$GLOBALS['setup_info']['todo']['currentver'] = '0.9.15.001';\n-\t\treturn $GLOBALS['setup_info']['todo']['currentver'];\n-\n-\t}\n+$test[] = '0.9.15.002';\n+function todo_upgrade0_9_15_002($oProc)\n+{\n+\t$oProc->m_odb->transaction_begin();\n \n-\t$test[] = '0.9.14';\n-\tfunction todo_upgrade0_9_14()\n-\t{\n-\t\t$GLOBALS['setup_info']['todo']['currentver'] = '0.9.15.001';\n-\t\treturn $GLOBALS['setup_info']['todo']['currentver'];\n-\t}\n+\t$oProc->AddColumn('phpgw_todo', 'entry_date', array('type' => 'int', 'precision' => 4, 'default' => 0, 'nullable' => False));\n \n-\t$test[] = '0.9.14.500';\n-\tfunction todo_upgrade0_9_14_500()\n+\tif ($oProc->m_odb->transaction_commit())\n \t{\n-\t\t$GLOBALS['setup_info']['todo']['currentver'] = '0.9.15.001';\n-\t\treturn $GLOBALS['setup_info']['todo']['currentver'];\n+\t\treturn '0.9.15.003';\n \t}\n+}\n \n-\t$test[] = '0.9.15.001';\n-\tfunction todo_upgrade0_9_15_001()\n-\t{\n-\t\t$GLOBALS['phpgw_setup']->oProc->m_odb->transaction_begin();\n-\t\t$GLOBALS['phpgw_setup']->oProc->AddColumn('phpgw_todo','todo_assigned',array('type' => 'varchar','precision' => 255,'nullable' => False));\n-\t\t$GLOBALS['phpgw_setup']->oProc->AddColumn('phpgw_todo','assigned_group',array('type' => 'varchar','precision' => 255,'nullable' => False));\n+$test[] = '0.9.15.003';\n+function todo_upgrade0_9_15_003($oProc)\n+{\n+\t$oProc->m_odb->transaction_begin();\n \n-\t\tif($GLOBALS['phpgw_setup']->oProc->m_odb->transaction_commit())\n-\t\t{\n-\t\t\t$GLOBALS['setup_info']['todo']['currentver'] = '0.9.15.002';\n-\t\t\treturn $GLOBALS['setup_info']['todo']['currentver'];\n-\t\t}\n-\t}\n+\t$location_obj = new \\App\\modules\\phpgwapi\\controllers\\Locations();\n+\t$location_obj->add('.todo', 'todo', 'todo');\n \n-\t$test[] = '0.9.15.002';\n-\tfunction todo_upgrade0_9_15_002()\n+\tif ($oProc->m_odb->transaction_commit())\n \t{\n-\t\t$GLOBALS['phpgw_setup']->oProc->m_odb->transaction_begin();\n-\n-\t\t$GLOBALS['phpgw_setup']->oProc->AddColumn('phpgw_todo','entry_date',array('type' => 'int','precision' => 4,'default' => 0,'nullable' => False));\n-\n-\t\tif($GLOBALS['phpgw_setup']->oProc->m_odb->transaction_commit())\n-\t\t{\n-\t\t\t$GLOBALS['setup_info']['todo']['currentver'] = '0.9.15.003';\n-\t\t\treturn $GLOBALS['setup_info']['todo']['currentver'];\n-\t\t}\n+\t\treturn '1.0.0.000';\n \t}\n+}"},{"sha":"8868d6ca2280300834d991ab2372de77150be8eb","filename":"src/modules/todo/templates/base/config.tpl","status":"added","additions":36,"deletions":0,"changes":36,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fconfig.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fconfig.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fconfig.tpl?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,36 @@\n+<!-- BEGIN header -->\n+<form method=\"post\" action=\"{action_url}\">\n+\t<table class=\"pure-table pure-table-bordered pure-table-striped pure-form\">\n+\t\t<th>\n+\t\t<td colspan=\"2\">\n+\t\t\t<font color=\"{th_text}\">&nbsp;<b>{title}</b></font>\n+\t\t</td>\n+\t\t</th>\n+\t\t<tr bgcolor=\"{th_err}\">\n+\t\t\t<td colspan=\"2\">&nbsp;<b>{error}</b></font>\n+\t\t\t</td>\n+\t\t</tr>\n+\t\t<!-- END header -->\n+\t\t<!-- BEGIN body -->\n+\t\t<tr class=\"row_on\">\n+\t\t\t<td colspan=\"2\">&nbsp;</td>\n+\t\t</tr>\n+\t\t<tr class=\"row_off\">\n+\t\t\t<td colspan=\"2\">&nbsp;<b>{lang_todo}/{lang_Settings}</b></font>\n+\t\t\t</td>\n+\t\t</tr>\n+\n+\t\t<!-- END body -->\n+\t\t<!-- BEGIN footer -->\n+\t\t<tr class=\"{th}\">\n+\t\t\t<td colspan=\"2\">&nbsp;</td>\n+\t\t</tr>\n+\t\t<tr>\n+\t\t\t<td colspan=\"2\" align=\"center\">\n+\t\t\t\t<input type=\"submit\" name=\"submit\" value=\"{lang_submit}\" class=\"pure-button\" />\n+\t\t\t\t<input type=\"submit\" name=\"cancel\" value=\"{lang_cancel}\" class=\"pure-button\" />\n+\t\t\t</td>\n+\t\t</tr>\n+\t</table>\n+</form>\n+<!-- END footer -->\n\\ No newline at end of file"},{"sha":"36269a59ba3ddd2566f12721b6aac895dec3dea6","filename":"src/modules/todo/templates/base/delete.tpl","status":"removed","additions":0,"deletions":21,"changes":21,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fdelete.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fdelete.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fdelete.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,21 +0,0 @@\n-<!-- $Id$ -->\n-<center>\n-<table border=\"0\" with=\"65%\">\n-<form method=\"POST\" action=\"{actionurl}\">\n-\t<tr colspan=\"2\">\n-\t\t<td align=\"center\">{deleteheader}</td>\n-\t</tr>\n-\t<tr>\n-\t\t<td align=\"center\">{lang_subs}</td>\n-\t\t<td align=\"center\">{subs}</td>\n-\t</tr>\n-\t<tr>\n-\t\t<td>\n-\t\t\t<input type=\"submit\" name=\"confirm\" value=\"{lang_yes}\"></form>\n-\t\t</td>\n-\t\t<td>\n-\t\t\t<a href=\"{nolink}\">{lang_no}</a>\n-\t\t</td>\n-\t</tr>\n-</table>\n-</center>"},{"sha":"728f087748fbbbd6351c1f063635b16839e4fb29","filename":"src/modules/todo/templates/base/form.tpl","status":"removed","additions":0,"deletions":83,"changes":83,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fform.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fform.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fform.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,83 +0,0 @@\n-<!-- $Id$ -->\n-<h1>{lang_todo_action}</h1>\n-{error}\n-<form method=\"post\" action=\"{actionurl}\">\n-\t\t<label for=\"values_title\">{lang_title}:</label>\n-\t\t<input id=\"values_title\" type=\"text\" name=\"values[title]\" value=\"{title}\" size=\"50\"><br>\n-\t\t\n-\t\t<label for=\"values_descr\">{lang_descr}:</label>\n-\t\t<textarea name=\"values[descr]\" rows=\"10\" cols=\"80\">{descr}</textarea><br>\n-\t\t\n-\t\t\n-\t\t<label for=\"values_new_parent\">{lang_parent}</label>\n-\t\t<select id=\"values_new_parent\" name=\"new_parent\"><option value=\"\">{lang_none}</option>{todo_list}</select><br>\n-\n-\t\t<label for=\"values_start_date\">{lang_start_date}:</label>\n-\t\t{start_select_date}\n-\t\t{selfortoday}\n-\t\t<label for=\"selfortoday\">{lang_selfortoday}</label><br>\n-\n-\t\t<td>{lang_end_date}:</td>\n-\t\t<td>{end_select_date}</td>\n-\t\t<td>{lang_daysfromstartdate}</td>\n-\t\t<td>{daysfromstartdate}</td>\n-\n-\t\t<label for=\"\">{lang_completed}:</td>\n-\t\t<td>{stat_list}</td><br>\n-\n-\t\t<label for=\"new_cat\">{lang_category}</label>\n-\t\t<select id=\"new_cat\" name=\"new_cat\"><option value=\"0\">{lang_none}</option>{cat_list}</select><br>\n-\n-\t\t<td>{lang_urgency}:</td>\n-\t\t<td>{pri_list}</td><br>\n-\t\t<label for=\"assinged_user\">{lang_assigned_user}:</label>\n-\t\t<select id=\"assigned_user\" name=\"assigned[]\" multiple><option value=\"\">{lang_nobody}</option>{user_list}</select><br>\n-\t\t\n-\t\t<td>{lang_access}:</td>\n-\t\t<td>{access_list}</td><br>\n-\n-\t\t<label for=\"assigned_group\">{lang_assigned_group}</label>\n-\t\t<select id=\"assigned_group\" name=\"assigned_group[]\" multiple><option value=\"\">{lang_none}</option>{group_list}</select><br>\n-\t</tr>\n-</table>\n-\n-<!-- BEGIN add -->\n-\n-<table width=\"90%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n-\t<tr valign=\"bottom\">\n-\t\t<td height=\"35\" width=\"50%\">\n-\t\t\t<div align=\"center\">\n-\t\t\t<input type=\"submit\" name=\"submit\" value=\"{lang_save}\">\n-\t\t</div>\n-\t\t</td>\n-\t\t<td height=\"35\" width=\"50%\">\n-\t\t\t<div align=\"center\">\n-\t\t\t<input type=\"reset\" name=\"reset\" value=\"{lang_reset}\">\n-\t\t\t</div>\n-\t\t</td>\n-\t</tr>\n-</table>\n-</form>\n-</center>\n-         \n-<!-- END add -->\n-\n-<!-- BEGIN edit -->\n-\n-<table width=\"90%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n-\t<tr valign=\"bottom\">\n-\t\t<td height=\"35\" width=\"50%\">\n-\t\t\t<div align=\"center\">\n-\t\t\t<input type=\"submit\" name=\"submit\" value=\"{lang_save}\">\n-\t\t</div></form>\n-\t\t</td>\n-\t\t<td height=\"35\" width=\"50%\">\n-\t\t\t<div align=\"center\">\n-\t\t\t{delete}\n-\t\t\t</div>\n-\t\t</td>\n-\t</tr>\n-</table>\n-</center>\n-         \n-<!-- END edit -->"},{"sha":"ade2f3acad59ba4d4c4ca5f5f5ce95981740f0d0","filename":"src/modules/todo/templates/base/list.tpl","status":"removed","additions":0,"deletions":77,"changes":77,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Flist.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Flist.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Flist.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,77 +0,0 @@\n-<!-- $Id$ -->\n-<!-- BEGIN page_header -->\n-<h1>{lang_action}</h1>\n-<center>\n-\t<table border=\"0\" cellspacing=\"2\" cellpadding=\"2\" width=\"98%\">\n-\t\t<tr>\n-\t\t\t<td colspan=\"9\" align=\"left\">\n-\t\t\t\t<table border=\"0\" width=\"100%\">\n-\t\t\t\t\t<tr>\n-\t\t\t\t\t{left}\n-\t\t\t\t\t\t<td align=\"center\">{total_matchs}</td>\n-\t\t\t\t\t{right}\n-\t\t\t\t\t</tr>\n-\t\t\t\t</table>\n-\t\t\t</td>\n-\t\t</tr>\n-\t\t<tr>\n-\t\t\t<td width=\"33%\" align=\"left\">\n-\t\t\t\t<form method=\"POST\" name=\"cat\" action=\"{cat_action}\">\n-\t\t\t\t{lang_category}&nbsp;&nbsp;<select name=\"cat_id\" onChange=\"this.form.submit();\">\n-\t\t\t\t<option value=\"0\">{lang_all}</option>\n-\t\t\t\t{categories}\n-\t\t\t\t</select>\n-\t\t\t\t<noscript><input type=\"submit\" name=\"cats\" value=\"{lang_select}\"></noscript>\n-\t\t\t\t</form>\n-\t\t\t</td >\n-\t\t\t<td width=\"33%\" align=\"center\"><form method=\"POST\" name=\"filter\" action=\"{filter_action}\">{filter_list}</form></td>\n-\t\t\t<td width=\"33%\" align=\"right\"><form method=\"POST\" name=\"query\" action=\"{search_action}\">{search_list}</form></td>\n-\t\t</tr>\n-\t</table>\n-<!-- END page_header -->\n-\n-<!-- BEGIN table_header -->\n-\t<table id=\"todo_list\">\n-\t\t<thead>\n-\t\t\t<tr>\n-\t\t\t\t<td width=\"5%\" align=\"right\">{sort_status}</td>\n-\t\t\t\t<td width=\"5%\" align=\"center\">{sort_urgency}</td>\n-\t\t\t\t<td>{sort_title}</td>\n-\t\t\t\t<td width=\"9%\" align=\"center\">{sort_sdate}</td>\n-\t\t\t\t<td width=\"9%\" align=\"center\">{sort_edate}</td>\n-\t\t\t\t<td width=\"15%\">{sort_owner}</td>\n-\t\t\t\t<td width=\"15%\">{sort_assigned}</td>\n-\t\t\t\t<td width=\"9%\" align=\"center\">{h_lang_sub}</td>\n-\t\t\t\t<td width=\"5%\" align=\"center\">{h_lang_view}</td>\n-\t\t\t\t<td width=\"5%\" align=\"center\">{h_lang_edit}</td>\n-\t\t\t</tr>\n-\t\t</thead>\n-\t\t<tbody>\n-<!-- END table_header -->\n-  \n-<!-- BEGIN todo_list -->\n-\t  <tr class=\"{tr_class}\">\n-\t    <td align=\"right\">{status}%</td>\n-\t    <td align=\"center\">{pri}</font></td>\n-\t    <td>{title}</td>\n-\t    <td align=\"center\">{datecreated}</td>\n-\t    <td align=\"center\">{datedue}</td>\n-\t    <td>{owner}</td>\n-\t    <td>{assigned}</td>\n-\t    <td align=\"center\">{subadd}</td>\n-\t    <td align=\"center\">{view}</td>\n-\t    <td align=\"center\">{edit}</td>\n-\t  </tr>\n-<!-- END todo_list -->\n-\n-<!-- BEGIN table_footer -->\n-\t\t</tbody>\n-\t</table>\n-<!-- END table_footer -->\n-\n-<!-- BEGIN page_footer -->\n-<div class=\"button_group\">\n-\t{add}\n-\t{matrixview}\n-</div>\n-<!-- END page_footer -->"},{"sha":"df3b6308f737479f2343406cbdeb51551a5bdb5f","filename":"src/modules/todo/templates/base/pref.tpl","status":"removed","additions":0,"deletions":22,"changes":22,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpref.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpref.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpref.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,22 +0,0 @@\n-<!-- BEGIN pref -->\n-<p><b>{title}:</b><hr><p>\n-<form action=\"{action_url}\" method=\"POST\">\n- <table border=\"0\" align=\"center\" width=\"90%\">\n-  {row}\n- </table>\n- <center><input type=\"submit\" name=\"submit\" value=\"{submit_lang}\"></center>\n-</form>\n-<!-- END pref -->\n-\n-<!-- BEGIN pref_colspan -->\n-  <tr bgcolor=\"{bg_color}\">\n-   <td colspan=\"2\">{text}</td>\n-  </tr>\n-<!-- END pref_colspan -->\n-\n-<!-- BEGIN pref_list -->\n-  <tr bgcolor=\"{bg_color}\">\n-   <td align=\"right\">{field}</td>\n-   <td align=\"center\">{data}</td>\n-  </tr>\n-<!-- END pref_list -->"},{"sha":"100ed8359eb5d443cf0fddbf225f7afb47fec2d0","filename":"src/modules/todo/templates/base/preference_acl.tpl","status":"removed","additions":0,"deletions":28,"changes":28,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_acl.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_acl.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_acl.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,28 +0,0 @@\n-<!-- $Id$ -->\n-{errors}\n-{title}\n-<table border=\"0\" align=\"center\" width=\"70%\">\n-\t<tr>\n-\t{nml}\n-\t\t<td width=\"40%\">\n-\t\t\t<div align=\"center\">\n-\t\t\t<form method=\"POST\" action=\"{action_url}\">\n-\t\t\t{common_hidden_vars}\n-\t\t\t<input type=\"text\" name=\"query\" value=\"{search_value}\">\n-\t\t\t<input type=\"submit\" name=\"search\" value=\"{search}\">\n-\t\t\t</form>\n-\t\t\t</div>\n-\t\t</td>\n-\t{nmr}\n-\t</tr>\n-</table>\n-<form method=\"POST\" action=\"{action_url}\">\n-<table border=\"0\" align=\"center\" width=\"50%\">\n-\t{row}\n-</table>\n-\t{common_hidden_vars_form}\n-\t<input type=\"hidden\" name=\"processed\" value=\"{processed}\">\n-\t<center><input type=\"submit\" name=\"submit\" value=\"{submit_lang}\"></center>\n-\t</form>\n-\n-"},{"sha":"7d38275b888fabca83d5c8082cf2be6a4f0f6379","filename":"src/modules/todo/templates/base/preference_acl_row.tpl","status":"removed","additions":0,"deletions":9,"changes":9,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_acl_row.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_acl_row.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_acl_row.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,9 +0,0 @@\n-<!-- $Id$ -->\n-<tr class=\"{row_class}\">\n-\t<td>{user}</td>\n-\t<td align=\"center\"><input type=\"checkbox\" name=\"{read}\" value=\"Y\"{read_selected}></td>\n-\t<td align=\"center\"><input type=\"checkbox\" name=\"{add}\" value=\"Y\"{add_selected}></td>\n-\t<td align=\"center\"><input type=\"checkbox\" name=\"{edit}\" value=\"Y\"{edit_selected}></td>\n-\t<td align=\"center\"><input type=\"checkbox\" name=\"{delete}\" value=\"Y\"{delete_selected}></td>\n-\t<td align=\"center\"><input type=\"checkbox\" name=\"{private}\" value=\"Y\"{private_selected}></td>\n-</tr>"},{"sha":"0fb4d5f685f7e324cd80568ffb198f1ea7adfbb6","filename":"src/modules/todo/templates/base/preference_colspan.tpl","status":"removed","additions":0,"deletions":9,"changes":9,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_colspan.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_colspan.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fpreference_colspan.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,9 +0,0 @@\n-<!-- $Id$ -->\n-<tr class=\"th\">\n-\t<td>{string}</td>\n-\t<td align=\"center\">{read_lang}</td>\n-\t<td align=\"center\">{add_lang}</td>\n-\t<td align=\"center\">{edit_lang}</td>\n-\t<td align=\"center\">{delete_lang}</td>\n-\t<td align=\"center\">{private_lang}</td>\n-</tr>"},{"sha":"c82b89f25bc593cdd2a93ea32ee4f42a46e84ec0","filename":"src/modules/todo/templates/base/view.tpl","status":"removed","additions":0,"deletions":72,"changes":72,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fview.tpl","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/21925c2e01c1a258820e13ea25438a067b5f6a0f/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fview.tpl","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Ftemplates%2Fbase%2Fview.tpl?ref=21925c2e01c1a258820e13ea25438a067b5f6a0f","patch":"@@ -1,72 +0,0 @@\n-<!-- $Id$ -->\n-<p>&nbsp;&nbsp;&nbsp;<b>{lang_todo_action}</b><br>\n-<hr noshade width=\"98%\" align=\"center\" size=\"1\">\n-<center>\n-{error}\n-<table width=\"90%\" border=\"0\" cellspacing=\"0\" cellpadding=\"2\">\n-\t<tr bgcolor=\"{row_on}\">\n-\t\t<td>{lang_title}:</td>\n-\t\t<td>{value_title}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_off}\">\n-\t\t<td valign=\"top\">{lang_descr}:</td>\n-\t\t<td>{value_descr}</td>\n-\t</tr>\n-\t<tr>\n-\t\t<td>&nbsp;</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_on}\">\n-\t\t<td>{lang_parent}:</td>\n-\t\t<td>{value_parent}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_off}\">\n-\t\t<td>{lang_category}:</td>\n-\t\t<td>{value_category}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_on}\">\n-\t\t<td>{lang_owner}:</td>\n-\t\t<td>{owner}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_off}\">\n-\t\t<td valign=\"top\">{lang_assigned}:</td>\n-\t\t<td>{assigned}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_on}\">\n-\t\t<td>{lang_access}:</td>\n-\t\t<td>{access}</td>\n-\t</tr>\n-\t<tr>\n-\t\t<td>&nbsp;</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_off}\">\n-\t\t<td>{lang_start_date}:</td>\n-\t\t<td>{value_start_date}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_on}\">\n-\t\t<td>{lang_end_date}:</td>\n-\t\t<td>{value_end_date}</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_off}\">\n-\t\t<td>{lang_completed}:</td>\n-\t\t<td>{value_completed} %</td>\n-\t</tr>\n-\t<tr bgcolor=\"{row_on}\">\n-\t\t<td>{lang_urgency}:</td>\n-\t\t<td>{value_urgency}</td>\n-\t</tr>\n-\t<tr>\n-\t\t<td align=\"left\">{button_edit}</td>\n-\t\t<td align=\"center\">{button_done}</td>\n-\t\t<td align=\"right\">{button_delete}</td>\n-\t</tr>\n-</table>\n-<p>\n-{history}\n-<table width=\"90%\" border=\"0\" cellspacing=\"2\" cellpadding=\"2\">\n-\t<tr>\n-\t\t<td height=\"50\">\n-\t\t\t<form method=\"POST\" action=\"{done_action}\">\n-\t\t\t<input type=\"submit\" name=\"done\" value=\"{lang_done}\">\n-\t\t\t</form></td>\n-\t</tr>\n-</table>"},{"sha":"3cb94312fe0400fde8e11e0beb532b0409d254f4","filename":"src/modules/todo/viewcontrollers/TodoViewController.php","status":"added","additions":565,"deletions":0,"changes":565,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fviewcontrollers%2FTodoViewController.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/src%2Fmodules%2Ftodo%2Fviewcontrollers%2FTodoViewController.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Ftodo%2Fviewcontrollers%2FTodoViewController.php?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -0,0 +1,565 @@\n+<?php\n+\n+namespace App\\modules\\todo\\viewcontrollers;\n+\n+use App\\modules\\phpgwapi\\services\\Settings;\n+use App\\modules\\phpgwapi\\security\\Acl;\n+use App\\modules\\phpgwapi\\helpers\\LegacyViewHelper;\n+use App\\modules\\phpgwapi\\helpers\\TwigHelper;\n+use App\\helpers\\ResponseHelper;\n+use Psr\\Http\\Message\\ResponseInterface as Response;\n+use Psr\\Http\\Message\\ServerRequestInterface as Request;\n+use Exception;\n+\n+class TodoViewController\n+{\n+\tprotected TwigHelper $twig;\n+\tprotected LegacyViewHelper $legacyView;\n+\n+\tprivate function getDatatableI18n(): array\n+\t{\n+\t\t$user = Settings::getInstance()->get('user');\n+\t\t$rowsPerPage = isset($user['preferences']['common']['maxmatchs']) && (int) $user['preferences']['common']['maxmatchs'] > 0\n+\t\t\t? (int) $user['preferences']['common']['maxmatchs']\n+\t\t\t: 10;\n+\n+\t\t$lengthmenu = [[], []];\n+\t\tfor ($i = 1; $i < 4; $i++)\n+\t\t{\n+\t\t\t$lengthmenu[0][] = $i * $rowsPerPage;\n+\t\t\t$lengthmenu[1][] = $i * $rowsPerPage;\n+\t\t}\n+\n+\t\treturn [\n+\t\t\t'datatable' => [\n+\t\t\t\t'emptyTable' => json_encode(lang('No data available in table')),\n+\t\t\t\t'info' => json_encode(lang('Showing _START_ to _END_ of _TOTAL_ entries')),\n+\t\t\t\t'infoEmpty' => json_encode(lang('Showing 0 to 0 of 0 entries')),\n+\t\t\t\t'infoFiltered' => json_encode(lang('(filtered from _MAX_ total entries)')),\n+\t\t\t\t'infoPostFix' => json_encode(''),\n+\t\t\t\t'thousands' => json_encode(','),\n+\t\t\t\t'lengthMenu' => json_encode(lang('Show _MENU_ entries')),\n+\t\t\t\t'loadingRecords' => json_encode(lang('Loading...')),\n+\t\t\t\t'processing' => json_encode(lang('Processing...')),\n+\t\t\t\t'search' => json_encode(lang('search')),\n+\t\t\t\t'zeroRecords' => json_encode(lang('No matching records found')),\n+\t\t\t\t'paginate' => json_encode([\n+\t\t\t\t\t'first' => lang('first'),\n+\t\t\t\t\t'last' => lang('last'),\n+\t\t\t\t\t'next' => lang('next'),\n+\t\t\t\t\t'previous' => lang('prev'),\n+\t\t\t\t]),\n+\t\t\t\t'aria' => json_encode([\n+\t\t\t\t\t'sortAscending' => lang(': activate to sort column ascending'),\n+\t\t\t\t\t'sortDescending' => lang(': activate to sort column descending'),\n+\t\t\t\t]),\n+\t\t\t\t'select' => json_encode([\n+\t\t\t\t\t'rows' => [\n+\t\t\t\t\t\t'0' => '',\n+\t\t\t\t\t\t'_' => '%d ' . lang('rows selected'),\n+\t\t\t\t\t],\n+\t\t\t\t]),\n+\t\t\t],\n+\t\t\t'lengthmenu' => ['_' => json_encode($lengthmenu)],\n+\t\t\t'lengthmenu_allrows' => ['_' => json_encode([-1, lang('all')])],\n+\t\t\t'csv_download' => ['_' => json_encode([\n+\t\t\t\t'show_button' => !empty($user['preferences']['common']['csv_download']),\n+\t\t\t\t'title' => lang('download visible data'),\n+\t\t\t])],\n+\t\t];\n+\t}\n+\n+\tprivate function formatDateForInput($timestamp, string $dateFormat): string\n+\t{\n+\t\t$ts = (int) $timestamp;\n+\t\tif ($ts <= 0)\n+\t\t{\n+\t\t\treturn '';\n+\t\t}\n+\n+\t\treturn date($dateFormat, $ts);\n+\t}\n+\n+\tprivate function buildMatrixHierarchyPrefix(int $level): string\n+\t{\n+\t\t$level = max(0, $level);\n+\t\tif ($level === 0)\n+\t\t{\n+\t\t\treturn '<span class=\"todo-matrix__node-prefix todo-matrix__node-prefix--root\"></span>';\n+\t\t}\n+\n+\t\t$spacer = str_repeat('&nbsp;&nbsp;&nbsp;', $level);\n+\t\treturn '<span class=\"todo-matrix__node-prefix\" aria-hidden=\"true\">' . $spacer . '&boxur;&nbsp;</span>';\n+\t}\n+\n+\tprivate function buildMatrixTitle(array $entry, string $bandClass = ''): string\n+\t{\n+\t\t$id = (int) ($entry['id'] ?? 0);\n+\t\t$level = max(0, (int) ($entry['level'] ?? 0));\n+\t\t$status = (int) ($entry['status'] ?? 0);\n+\t\t$status = max(0, min(100, $status));\n+\t\t$title = \t\\phpgw::strip_html((string) ($entry['title'] ?? ''));\n+\t\tif ($title === '')\n+\t\t{\n+\t\t\t$title = lang('Untitled');\n+\t\t}\n+\n+\t\t$prefix = $this->buildMatrixHierarchyPrefix($level);\n+\t\t$link = '<a href=\"' . \\phpgw::link('/todo/view/todos/' . $id) . '\">' . $title . '</a>';\n+\t\t$statusHtml = '<button type=\"button\" class=\"todo-matrix__status\" data-todo-id=\"' . $id . '\" data-status=\"' . $status . '\" title=\"' . lang('Completed') . ': ' . $status . '% - ' . lang('click to edit') . '\">'\n+\t\t\t. '<span class=\"todo-matrix__status-value\">' . $status . '%</span>'\n+\t\t\t. '<span class=\"todo-matrix__status-bar\" aria-hidden=\"true\"><span class=\"todo-matrix__status-fill\" style=\"width:' . $status . '%\"></span></span>'\n+\t\t\t. '</button>';\n+\t\t$bandClass = trim($bandClass);\n+\t\t$bandClassPart = $bandClass ? ' ' . $bandClass : '';\n+\n+\t\treturn '<span class=\"todo-matrix__node todo-matrix__node--level-' . $level . $bandClassPart . '\">' . $prefix . $link . $statusHtml . '</span>';\n+\t}\n+\n+\tprivate function getPeopleList(string $type): array\n+\t{\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$employees = $botodo->employee_list($type);\n+\t\t$options = [];\n+\n+\t\t$accounts = new \\App\\modules\\phpgwapi\\controllers\\Accounts\\Accounts();\n+\t\tforeach ((array) $employees as $employee)\n+\t\t{\n+\t\t\t$options[] = [\n+\t\t\t\t'id' => (int) ($employee->id ?? 0),\n+\t\t\t\t'name' => (string) $accounts->id2name((int) ($employee->id ?? 0)),\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn $options;\n+\t}\n+\n+\tprivate function getParentTodos(int $excludeId = 0): array\n+\t{\n+\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t$todos = $botodo->_list(0, 0, '', '', '', '', 0, 'all');\n+\t\t$excludedIds = [];\n+\t\tif ($excludeId > 0)\n+\t\t{\n+\t\t\t$excludedIds[] = $excludeId;\n+\t\t\t$descendants = (string) $botodo->sotodo->find_subs((string) $excludeId);\n+\t\t\tif ($descendants !== '')\n+\t\t\t{\n+\t\t\t\t$excludedIds = array_merge($excludedIds, array_filter(array_map('intval', explode(',', $descendants))));\n+\t\t\t}\n+\t\t\t$excludedIds = array_values(array_unique($excludedIds));\n+\t\t}\n+\n+\t\t$options = [\n+\t\t\t['id' => 0, 'title' => lang('None')],\n+\t\t];\n+\n+\t\tforeach ((array) $todos as $todo)\n+\t\t{\n+\t\t\t$id = (int) ($todo['id'] ?? 0);\n+\t\t\tif ($excludeId > 0 && in_array($id, $excludedIds, true))\n+\t\t\t{\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t$title = (string) ($todo['title'] ?? '');\n+\t\t\tif (!$title)\n+\t\t\t{\n+\t\t\t\t$descr = \\phpgw::strip_html((string) ($todo['descr'] ?? ''));\n+\t\t\t\t$title = trim(implode(' ', array_slice(explode(' ', $descr), 0, 4)) . ' ...');\n+\t\t\t}\n+\n+\t\t\t$level = (int) ($todo['level'] ?? 0);\n+\t\t\tif ($level > 0)\n+\t\t\t{\n+\t\t\t\t$title = str_repeat('-- ', $level) . $title;\n+\t\t\t}\n+\n+\t\t\t$options[] = [\n+\t\t\t\t'id' => $id,\n+\t\t\t\t'title' => $title,\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn $options;\n+\t}\n+\n+\tprivate function getCategories(): array\n+\t{\n+\t\t$cats = \\CreateObject('phpgwapi.categories', -1, 'todo', '.task');\n+\t\t$categories = $cats->return_sorted_array(0, false, '', '', '', true, 0, false);\n+\n+\t\t$options = [\n+\t\t\t[\n+\t\t\t\t'id' => 0,\n+\t\t\t\t'name' => lang('All')\n+\t\t\t]\n+\t\t];\n+\n+\t\tforeach ((array) $categories as $category)\n+\t\t{\n+\t\t\t$options[] = [\n+\t\t\t\t'id' => (int) ($category['id'] ?? 0),\n+\t\t\t\t'name' => (string) ($category['name'] ?? '')\n+\t\t\t];\n+\t\t}\n+\n+\t\treturn $options;\n+\t}\n+\n+\tprivate function getCsrfData(Request $request): array\n+\t{\n+\t\t$nameKey = 'csrf_name';\n+\t\t$valueKey = 'csrf_value';\n+\n+\t\treturn [\n+\t\t\t'name_key' => $nameKey,\n+\t\t\t'value_key' => $valueKey,\n+\t\t\t'name' => (string) ($request->getAttribute($nameKey) ?? ''),\n+\t\t\t'value' => (string) ($request->getAttribute($valueKey) ?? ''),\n+\t\t];\n+\t}\n+\n+\tpublic function __construct()\n+\t{\n+\t\t$this->legacyView = new LegacyViewHelper();\n+\t\t$this->twig = new TwigHelper('todo');\n+\t}\n+\n+\t/**\n+\t * GET|POST /todo/view/todos/matrix\n+\t */\n+\tpublic function matrix(Request $request, Response $response): Response\n+\t{\n+\t\ttry {\n+\t\t\t$body = (array) ($request->getParsedBody() ?: []);\n+\t\t\t$query = $request->getQueryParams();\n+\n+\t\t\t$month = isset($query['month']) ? (int) $query['month'] : (isset($body['month']) ? (int) $body['month'] : (int) date('n'));\n+\t\t\t$year = isset($query['year']) ? (int) $query['year'] : (isset($body['year']) ? (int) $body['year'] : (int) date('Y'));\n+\n+\t\t\tif ($month < 1 || $month > 12)\n+\t\t\t{\n+\t\t\t\t$month = (int) date('n');\n+\t\t\t}\n+\t\t\tif ($year < 1970 || $year > 2100)\n+\t\t\t{\n+\t\t\t\t$year = (int) date('Y');\n+\t\t\t}\n+\n+\t\t\t$colors = [\n+\t\t\t\t'#cc0033',\n+\t\t\t\t'#006600',\n+\t\t\t\t'#00ccff',\n+\t\t\t\t'#ff6600',\n+\t\t\t\t'#0000ff',\n+\t\t\t];\n+\n+\t\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t\t$matrix = \\CreateObject('phpgwapi.matrixview', $month, $year);\n+\t\t\t$entries = $botodo->_list(0, 0, '', '', '', '', '', 'all');\n+\n+\t\t\t$groupColors = [];\n+\t\t\t$groupBands = [];\n+\t\t\t$colorIndex = 0;\n+\t\t\tforeach ((array) $entries as $entry)\n+\t\t\t{\n+\t\t\t\tif ((int) ($entry['sdate_epoch'] ?? 0) <= 0 || (int) ($entry['edate_epoch'] ?? 0) <= 0)\n+\t\t\t\t{\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\n+\t\t\t\t$groupId = (int) ($entry['main'] ?? 0);\n+\t\t\t\tif ($groupId <= 0)\n+\t\t\t\t{\n+\t\t\t\t\t$groupId = (int) ($entry['id'] ?? 0);\n+\t\t\t\t}\n+\n+\t\t\t\tif (!isset($groupColors[$groupId]))\n+\t\t\t\t{\n+\t\t\t\t\t$groupColors[$groupId] = $colors[$colorIndex % count($colors)];\n+\t\t\t\t\t$groupBands[$groupId] = ($colorIndex % 2 === 0) ? 'todo-matrix__node--band-a' : 'todo-matrix__node--band-b';\n+\t\t\t\t\t$colorIndex++;\n+\t\t\t\t}\n+\n+\t\t\t\t$title = $this->buildMatrixTitle((array) $entry, (string) ($groupBands[$groupId] ?? ''));\n+\t\t\t\t$startd = date('Ymd', (int) $entry['sdate_epoch']);\n+\t\t\t\t$endd = date('Ymd', (int) $entry['edate_epoch']);\n+\t\t\t\t$matrix->setPeriod($title, $startd, $endd, $groupColors[$groupId]);\n+\t\t\t}\n+\n+\t\t\tob_start();\n+\t\t\t$matrix->out(\\phpgw::link('/todo/view/todos/matrix'));\n+\t\t\t$matrixHtml = (string) ob_get_clean();\n+\n+\t\t\t$componentHtml = $this->twig->render('@views/todo/matrix/todo_matrix.twig', [\n+\t\t\t\t'layout' => '@views/_bare.twig',\n+\t\t\t\t'matrix_html' => $matrixHtml,\n+\t\t\t\t'csrf' => $this->getCsrfData($request),\n+\t\t\t]);\n+\n+\t\t\t$html = $this->legacyView->render($componentHtml, ['todo', 'matrix']);\n+\t\t\t$response->getBody()->write($html);\n+\t\t\treturn $response->withHeader('Content-Type', 'text/html');\n+\t\t} catch (Exception $e) {\n+\t\t\tif (ob_get_level() > 0)\n+\t\t\t{\n+\t\t\t\tob_end_clean();\n+\t\t\t}\n+\n+\t\t\treturn ResponseHelper::sendErrorResponse(\n+\t\t\t\t['error' => 'Error loading todo matrix page: ' . $e->getMessage()],\n+\t\t\t\t500\n+\t\t\t);\n+\t\t}\n+\t}\n+\t/**\n+\t *\n+\t * @param string $app\n+\t * @param string $pkg will always look within template set, then fallback to $pkg\n+\t * @param string $name name of the javascript file to include\n+\t * @param bool $end_of_page\n+\t * @param array $config\n+\t * @return bool\n+\t */\n+\n+\tpublic static function add_javascript($app, $pkg, $name, $end_of_page = false, $config = array())\n+\t{\n+\n+\t\treturn \\phpgwapi_js::getInstance()->validate_file($pkg, str_replace('.js', '', $name), $app, $end_of_page, $config);\n+\t}\n+\n+\t/**\n+\t * GET /todo/view/todos\n+\t */\n+\tpublic function index(Request $request, Response $response): Response\n+\t{\n+\n+\t\ttry {\n+\t\t\t\\phpgw::import_class('phpgwapi.jquery');\n+\t\t\t\\phpgw::import_class('phpgwapi.css');\n+\t\t\t\\phpgw::import_class('phpgwapi.js');\n+\t\t\t\\phpgwapi_jquery::load_widget('core');\n+\t\t\t\\phpgwapi_jquery::load_widget('contextMenu');\n+\t\t\tself::add_javascript('phpgwapi', \"jquery\", 'common.js', false, array('combine' => true));\n+\t\t\tself::add_javascript('phpgwapi', 'DataTables2', 'datatables.min.js', false, array('combine' => true));\n+\t\t\tself::add_javascript('phpgwapi', 'DataTables2', 'plugins/dataTables.inputPaging.js', false, array('combine' => true));\n+\t\t\tself::add_javascript('phpgwapi', 'jquery', 'editable/jquery.jeditable.min.js', false, array('combine' => true));\n+\t\t\tself::add_javascript('phpgwapi', 'jquery', 'editable/jquery.dataTables.editable.min.js', false, array('combine' => true));\n+\t\t\t\\phpgwapi_css::getInstance()->add_external_file('phpgwapi/js/DataTables2/datatables.min.css');\n+\t\t\t\\phpgwapi_css::getInstance()->add_external_file('phpgwapi/js/DataTables2/plugins/dataTables.inputPaging.min.css');\n+\n+\n+\n+\t\t\t$query = $request->getQueryParams();\n+\t\t\t$selectedCat = isset($query['cat_id']) ? (int) $query['cat_id'] : 0;\n+\t\t\t$selectedFilter = isset($query['filter']) ? (string) $query['filter'] : 'none';\n+\t\t\t$search = isset($query['search']) ? (string) $query['search'] : '';\n+\n+\t\t\t$categories = array_map(static function (array $category) use ($selectedCat): array\n+\t\t\t{\n+\t\t\t\t$category['selected'] = ((int) ($category['id'] ?? 0) === $selectedCat) ? '1' : '0';\n+\t\t\t\treturn $category;\n+\t\t\t}, $this->getCategories());\n+\n+\t\t\t$filters = [\n+\t\t\t\t[\n+\t\t\t\t\t'id' => 'none',\n+\t\t\t\t\t'name' => lang('All'),\n+\t\t\t\t\t'selected' => $selectedFilter === 'none' ? '1' : '0',\n+\t\t\t\t],\n+\t\t\t\t[\n+\t\t\t\t\t'id' => 'private',\n+\t\t\t\t\t'name' => lang('Private'),\n+\t\t\t\t\t'selected' => $selectedFilter === 'private' ? '1' : '0',\n+\t\t\t\t],\n+\t\t\t];\n+\n+\t\t\t$componentHtml = $this->twig->render('@views/todo/index/todo_datatable.twig', [\n+\t\t\t\t'layout' => '@views/_bare.twig',\n+\t\t\t\t'categories' => $categories,\n+\t\t\t\t'filters' => $filters,\n+\t\t\t\t'acl_delete' => (bool) Acl::getInstance()->check('.todo', ACL_DELETE, 'todo'),\n+\t\t\t\t'search_query' => $search,\n+\t\t\t\t'jquery_phpgw_i18n' => $this->getDatatableI18n(),\n+\t\t\t\t'matrix_url' => \\phpgw::link('/todo/view/todos/matrix', [\n+\t\t\t\t\t'month' => date('m'),\n+\t\t\t\t\t'year' => date('Y'),\n+\t\t\t\t]),\n+\t\t\t]);\n+\n+\t\t\t$html = $this->legacyView->render(\n+\t\t\t\t$componentHtml,\n+\t\t\t\t['todo']\n+\t\t\t);\n+\n+\t\t\t$response->getBody()->write($html);\n+\t\t\treturn $response->withHeader('Content-Type', 'text/html');\n+\t\t} catch (Exception $e) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(\n+\t\t\t\t['error' => 'Error loading todo page: ' . $e->getMessage()],\n+\t\t\t\t500\n+\t\t\t);\n+\t\t}\n+\t}\n+\n+\t/**\n+\t * GET /todo/view/todos/{id}\n+\t */\n+\tpublic function view(Request $request, Response $response, array $args): Response\n+\t{\n+\t\ttry {\n+\t\t\t$id = (int) ($args['id'] ?? 0);\n+\t\t\tif ($id <= 0)\n+\t\t\t{\n+\t\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t\t}\n+\n+\t\t\t$componentHtml = $this->twig->render('@views/todo/view/todo_view.twig', [\n+\t\t\t\t'layout' => '@views/_bare.twig',\n+\t\t\t\t'todo_id' => $id,\n+\t\t\t\t'csrf' => $this->getCsrfData($request),\n+\t\t\t]);\n+\n+\t\t\t$html = $this->legacyView->render($componentHtml, ['todo', 'view'], 'todo');\n+\t\t\t$response->getBody()->write($html);\n+\t\t\treturn $response->withHeader('Content-Type', 'text/html');\n+\t\t} catch (Exception $e) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(\n+\t\t\t\t['error' => 'Error loading todo view page: ' . $e->getMessage()],\n+\t\t\t\t500\n+\t\t\t);\n+\t\t}\n+\t}\n+\n+\t/**\n+\t * GET /todo/view/todos/{id}/delete\n+\t */\n+\tpublic function delete(Request $request, Response $response, array $args): Response\n+\t{\n+\t\ttry {\n+\t\t\t$id = (int) ($args['id'] ?? 0);\n+\t\t\tif ($id <= 0)\n+\t\t\t{\n+\t\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t\t}\n+\n+\t\t\t$componentHtml = $this->twig->render('@views/todo/delete/todo_delete.twig', [\n+\t\t\t\t'layout' => '@views/_bare.twig',\n+\t\t\t\t'todo_id' => $id,\n+\t\t\t\t'csrf' => $this->getCsrfData($request),\n+\t\t\t]);\n+\n+\t\t\t$html = $this->legacyView->render($componentHtml, ['todo', 'delete'], 'todo');\n+\t\t\t$response->getBody()->write($html);\n+\t\t\treturn $response->withHeader('Content-Type', 'text/html');\n+\t\t} catch (Exception $e) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(\n+\t\t\t\t['error' => 'Error loading todo delete page: ' . $e->getMessage()],\n+\t\t\t\t500\n+\t\t\t);\n+\t\t}\n+\t}\n+\n+\t/**\n+\t * GET /todo/view/todos/add\n+\t */\n+\tpublic function add(Request $request, Response $response): Response\n+\t{\n+\t\ttry {\n+\t\t\t\\phpgw::import_class('phpgwapi.jquery');\n+\t\t\t\\phpgwapi_jquery::load_widget('select2');\n+\t\t\t\\phpgwapi_jquery::load_widget('datepicker');\n+\n+\t\t\t$query = $request->getQueryParams();\n+\t\t\t$userSettings = \\App\\modules\\phpgwapi\\services\\Settings::getInstance()->get('user');\n+\t\t\t$dateFormat = (string) ($userSettings['preferences']['common']['dateformat'] ?? 'Y-m-d');\n+\t\t\t$componentHtml = $this->twig->render('@views/todo/add/todo_add.twig', [\n+\t\t\t\t'layout' => '@views/_bare.twig',\n+\t\t\t\t'categories' => $this->getCategories(),\n+\t\t\t\t'parentTodos' => $this->getParentTodos(),\n+\t\t\t\t'users' => $this->getPeopleList('accounts'),\n+\t\t\t\t'groups' => $this->getPeopleList('groups'),\n+\t\t\t\t'csrf' => $this->getCsrfData($request),\n+\t\t\t\t'date_format' => $dateFormat,\n+\t\t\t\t'selected_cat_id' => isset($query['cat_id']) ? (int) $query['cat_id'] : 0,\n+\t\t\t\t'selected_parent_id' => isset($query['parent']) ? (int) $query['parent'] : 0,\n+\t\t\t]);\n+\n+\t\t\t$html = $this->legacyView->render($componentHtml, ['todo', 'add'], 'todo');\n+\t\t\t$response->getBody()->write($html);\n+\t\t\treturn $response->withHeader('Content-Type', 'text/html');\n+\t\t} catch (Exception $e) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(\n+\t\t\t\t['error' => 'Error loading todo add page: ' . $e->getMessage()],\n+\t\t\t\t500\n+\t\t\t);\n+\t\t}\n+\t}\n+\n+\t/**\n+\t * GET /todo/view/todos/{id}/edit\n+\t */\n+\tpublic function edit(Request $request, Response $response, array $args): Response\n+\t{\n+\t\ttry {\n+\t\t\t$id = (int) ($args['id'] ?? 0);\n+\t\t\tif ($id <= 0)\n+\t\t\t{\n+\t\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Missing todo ID'], 400);\n+\t\t\t}\n+\n+\t\t\t$botodo = \\CreateObject('todo.botodo', true);\n+\t\t\t$item = $botodo->read($id);\n+\t\t\tif (!is_array($item) || !count($item))\n+\t\t\t{\n+\t\t\t\treturn ResponseHelper::sendErrorResponse(['error' => 'Todo not found'], 404);\n+\t\t\t}\n+\n+\t\t\t\\phpgw::import_class('phpgwapi.jquery');\n+\t\t\t\\phpgwapi_jquery::load_widget('select2');\n+\t\t\t\\phpgwapi_jquery::load_widget('datepicker');\n+\n+\t\t\t$userSettings = \\App\\modules\\phpgwapi\\services\\Settings::getInstance()->get('user');\n+\t\t\t$dateFormat = (string) ($userSettings['preferences']['common']['dateformat'] ?? 'Y-m-d');\n+\n+\t\t\t$assignedIds = $botodo->format_assigned((string) ($item['assigned'] ?? ''));\n+\t\t\t$assignedGroupIds = $botodo->format_assigned((string) ($item['assigned_group'] ?? ''));\n+\n+\t\t\t$todo = [\n+\t\t\t\t'id' => $id,\n+\t\t\t\t'title' => (string) ($item['title'] ?? ''),\n+\t\t\t\t'descr' => (string) ($item['descr'] ?? ''),\n+\t\t\t\t'cat' => (int) ($item['cat'] ?? 0),\n+\t\t\t\t'parent' => (int) ($item['parent'] ?? 0),\n+\t\t\t\t'pri' => (int) ($item['pri'] ?? 2),\n+\t\t\t\t'status' => (int) ($item['status'] ?? 0),\n+\t\t\t\t'sdate' => $this->formatDateForInput($item['sdate'] ?? 0, $dateFormat),\n+\t\t\t\t'edate' => $this->formatDateForInput($item['edate'] ?? 0, $dateFormat),\n+\t\t\t\t'access_private' => (string) ($item['access'] ?? '') === 'private',\n+\t\t\t\t'assigned_ids' => is_array($assignedIds) ? $assignedIds : [],\n+\t\t\t\t'assigned_group_ids' => is_array($assignedGroupIds) ? $assignedGroupIds : [],\n+\t\t\t];\n+\n+\t\t\t$componentHtml = $this->twig->render('@views/todo/edit/todo_edit.twig', [\n+\t\t\t\t'layout' => '@views/_bare.twig',\n+\t\t\t\t'todo' => $todo,\n+\t\t\t\t'categories' => $this->getCategories(),\n+\t\t\t\t'parentTodos' => $this->getParentTodos($id),\n+\t\t\t\t'users' => $this->getPeopleList('accounts'),\n+\t\t\t\t'groups' => $this->getPeopleList('groups'),\n+\t\t\t\t'csrf' => $this->getCsrfData($request),\n+\t\t\t\t'date_format' => $dateFormat,\n+\t\t\t]);\n+\n+\t\t\t$html = $this->legacyView->render($componentHtml, ['todo', 'edit'], 'todo');\n+\t\t\t$response->getBody()->write($html);\n+\t\t\treturn $response->withHeader('Content-Type', 'text/html');\n+\t\t} catch (Exception $e) {\n+\t\t\treturn ResponseHelper::sendErrorResponse(\n+\t\t\t\t['error' => 'Error loading todo edit page: ' . $e->getMessage()],\n+\t\t\t\t500\n+\t\t\t);\n+\t\t}\n+\t}\n+}"},{"sha":"ffb8c567b031289bb5554f54e9faf045e7cd4491","filename":"swagger_spec/openapi.json","status":"modified","additions":803,"deletions":2,"changes":805,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/86424db2bf27231b3de07ffa5810bd64cbb522f6/swagger_spec%2Fopenapi.json","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/86424db2bf27231b3de07ffa5810bd64cbb522f6/swagger_spec%2Fopenapi.json","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/swagger_spec%2Fopenapi.json?ref=86424db2bf27231b3de07ffa5810bd64cbb522f6","patch":"@@ -8640,7 +8640,7 @@\n                 ],\n                 \"summary\": \"Create a new location\",\n                 \"description\": \"Creates a new location record with provided field values and custom attributes. Legacy alias \\/property\\/location\\/add is still supported for backward compatibility.\",\n-                \"operationId\": \"aadef3c5bbb5e9c38ffc667e0aa23a92\",\n+                \"operationId\": \"c5d0f21e32e72206d5253ca353ff1711\",\n                 \"requestBody\": {\n                     \"required\": true,\n                     \"content\": {\n@@ -9709,7 +9709,7 @@\n                 ],\n                 \"summary\": \"Update an existing location\",\n                 \"description\": \"Updates a location record with field changes, custom attribute modifications, and optional rename via location_code_original threading\",\n-                \"operationId\": \"b15d5b022a534fed78955d3ccf6e4a8b\",\n+                \"operationId\": \"4362891b158eb5cfa9b9ab662c9f161b\",\n                 \"parameters\": [\n                     {\n                         \"name\": \"location_code\",\n@@ -11404,6 +11404,616 @@\n                     }\n                 }\n             }\n+        },\n+        \"\\/todo\\/todos\": {\n+            \"get\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"List todo items\",\n+                \"description\": \"GET \\/todo\\/todos\",\n+                \"operationId\": \"1aaab10719ee532a05c66c69999fdf71\",\n+                \"parameters\": [\n+                    {\n+                        \"name\": \"start\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"integer\",\n+                            \"default\": 0\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"limit\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"integer\",\n+                            \"default\": 100\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"search\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"string\"\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"filter\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"string\",\n+                            \"default\": \"none\"\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"cat_id\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"integer\",\n+                            \"default\": 0\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"sort\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"string\"\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"dir\",\n+                        \"in\": \"query\",\n+                        \"schema\": {\n+                            \"type\": \"string\",\n+                            \"default\": \"ASC\",\n+                            \"enum\": [\n+                                \"ASC\",\n+                                \"DESC\"\n+                            ]\n+                        }\n+                    }\n+                ],\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"Todo list\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"total\": {\n+                                            \"type\": \"integer\"\n+                                        },\n+                                        \"items\": {\n+                                            \"type\": \"array\",\n+                                            \"items\": {\n+                                                \"$ref\": \"#\\/components\\/schemas\\/TodoItem\"\n+                                            }\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            },\n+            \"post\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"Create todo\",\n+                \"description\": \"POST \\/todo\\/todos\",\n+                \"operationId\": \"edffa6843bd693a6158c7b9de91f38aa\",\n+                \"requestBody\": {\n+                    \"required\": true,\n+                    \"content\": {\n+                        \"application\\/json\": {\n+                            \"schema\": {\n+                                \"$ref\": \"#\\/components\\/schemas\\/TodoUpsertRequest\"\n+                            }\n+                        }\n+                    }\n+                },\n+                \"responses\": {\n+                    \"201\": {\n+                        \"description\": \"Created\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"id\": {\n+                                            \"type\": \"integer\"\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"400\": {\n+                        \"description\": \"Validation error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            }\n+        },\n+        \"\\/todo\\/categories\": {\n+            \"get\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"List todo categories\",\n+                \"description\": \"GET \\/todo\\/categories\",\n+                \"operationId\": \"a2d1089f9cd02a1a0a18aebfec81df40\",\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"Category list\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"items\": {\n+                                            \"type\": \"array\",\n+                                            \"items\": {\n+                                                \"$ref\": \"#\\/components\\/schemas\\/TodoCategory\"\n+                                            }\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            }\n+        },\n+        \"\\/todo\\/todos\\/export\\/csv\": {\n+            \"get\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"Export todo list as CSV\",\n+                \"description\": \"GET \\/todo\\/todos\\/export\\/csv\",\n+                \"operationId\": \"2f2a3be82a3b7f637b96dc82c37c45dc\",\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"CSV file\"\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            }\n+        },\n+        \"\\/todo\\/todos\\/{id}\": {\n+            \"get\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"Get single todo\",\n+                \"description\": \"GET \\/todo\\/todos\\/{id}\",\n+                \"operationId\": \"6018b4c996401c3bed72e7c3859664d0\",\n+                \"parameters\": [\n+                    {\n+                        \"name\": \"id\",\n+                        \"in\": \"path\",\n+                        \"required\": true,\n+                        \"schema\": {\n+                            \"type\": \"integer\"\n+                        }\n+                    }\n+                ],\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"Todo item\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"item\": {\n+                                            \"type\": \"object\"\n+                                        },\n+                                        \"detail\": {\n+                                            \"$ref\": \"#\\/components\\/schemas\\/TodoDetail\"\n+                                        },\n+                                        \"history_html\": {\n+                                            \"type\": \"string\"\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"404\": {\n+                        \"description\": \"Todo not found\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            },\n+            \"put\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"Update todo\",\n+                \"description\": \"PUT \\/todo\\/todos\\/{id}\",\n+                \"operationId\": \"f19b56d46553a6094cd7efb5dcf732ed\",\n+                \"parameters\": [\n+                    {\n+                        \"name\": \"id\",\n+                        \"in\": \"path\",\n+                        \"required\": true,\n+                        \"schema\": {\n+                            \"type\": \"integer\"\n+                        }\n+                    }\n+                ],\n+                \"requestBody\": {\n+                    \"required\": true,\n+                    \"content\": {\n+                        \"application\\/json\": {\n+                            \"schema\": {\n+                                \"$ref\": \"#\\/components\\/schemas\\/TodoUpsertRequest\"\n+                            }\n+                        }\n+                    }\n+                },\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"Updated\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"id\": {\n+                                            \"type\": \"integer\"\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"400\": {\n+                        \"description\": \"Invalid request\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"404\": {\n+                        \"description\": \"Todo not found\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            },\n+            \"delete\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"Delete todo\",\n+                \"description\": \"DELETE \\/todo\\/todos\\/{id}\",\n+                \"operationId\": \"6c3f749bcb5f0d6153b0db5d60d07762\",\n+                \"parameters\": [\n+                    {\n+                        \"name\": \"id\",\n+                        \"in\": \"path\",\n+                        \"required\": true,\n+                        \"schema\": {\n+                            \"type\": \"integer\"\n+                        }\n+                    },\n+                    {\n+                        \"name\": \"subs\",\n+                        \"in\": \"query\",\n+                        \"required\": false,\n+                        \"schema\": {\n+                            \"type\": \"boolean\",\n+                            \"default\": false\n+                        }\n+                    }\n+                ],\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"Delete result\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"deleted\": {\n+                                            \"type\": \"boolean\"\n+                                        },\n+                                        \"subs\": {\n+                                            \"type\": \"boolean\"\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"403\": {\n+                        \"description\": \"Delete denied\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"404\": {\n+                        \"description\": \"Todo not found\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            }\n+        },\n+        \"\\/todo\\/todos\\/{id}\\/status\": {\n+            \"patch\": {\n+                \"tags\": [\n+                    \"Todo\"\n+                ],\n+                \"summary\": \"Update todo completion percentage\",\n+                \"description\": \"PATCH \\/todo\\/todos\\/{id}\\/status\",\n+                \"operationId\": \"59949f3960541815d3da46d44c312bec\",\n+                \"parameters\": [\n+                    {\n+                        \"name\": \"id\",\n+                        \"in\": \"path\",\n+                        \"required\": true,\n+                        \"schema\": {\n+                            \"type\": \"integer\"\n+                        }\n+                    }\n+                ],\n+                \"requestBody\": {\n+                    \"required\": true,\n+                    \"content\": {\n+                        \"application\\/json\": {\n+                            \"schema\": {\n+                                \"required\": [\n+                                    \"status\"\n+                                ],\n+                                \"properties\": {\n+                                    \"status\": {\n+                                        \"type\": \"integer\",\n+                                        \"maximum\": 100,\n+                                        \"minimum\": 0\n+                                    }\n+                                },\n+                                \"type\": \"object\"\n+                            }\n+                        }\n+                    }\n+                },\n+                \"responses\": {\n+                    \"200\": {\n+                        \"description\": \"Status updated\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"properties\": {\n+                                        \"id\": {\n+                                            \"type\": \"integer\"\n+                                        },\n+                                        \"status\": {\n+                                            \"type\": \"integer\",\n+                                            \"maximum\": 100,\n+                                            \"minimum\": 0\n+                                        }\n+                                    },\n+                                    \"type\": \"object\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"400\": {\n+                        \"description\": \"Invalid status value\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"404\": {\n+                        \"description\": \"Todo not found\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"401\": {\n+                        \"description\": \"Unauthorized\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    },\n+                    \"500\": {\n+                        \"description\": \"Internal server error\",\n+                        \"content\": {\n+                            \"application\\/json\": {\n+                                \"schema\": {\n+                                    \"$ref\": \"#\\/components\\/schemas\\/TodoErrorResponse\"\n+                                }\n+                            }\n+                        }\n+                    }\n+                }\n+            }\n         }\n     },\n     \"components\": {\n@@ -11982,6 +12592,10 @@\n                     },\n                     \"close_calendar_without_season\": {\n                         \"type\": \"boolean\"\n+                    },\n+                    \"allow_old_template\": {\n+                        \"description\": \"When true, users who explicitly selected the old template stay on it.\\nWhen false (default), the new client is forced for paths that have a\\nnew-client alternative (see phpgwapi StartPoint redirect map).\",\n+                        \"type\": \"boolean\"\n                     }\n                 },\n                 \"type\": \"object\"\n@@ -13233,6 +13847,189 @@\n                     }\n                 },\n                 \"type\": \"object\"\n+            },\n+            \"TodoItem\": {\n+                \"properties\": {\n+                    \"id\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"cat_id\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"title\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"level\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"status\": {\n+                        \"type\": \"integer\",\n+                        \"maximum\": 100,\n+                        \"minimum\": 0\n+                    },\n+                    \"pri\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"sdate\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"edate\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"owner\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"assigned\": {\n+                        \"type\": \"string\"\n+                    }\n+                },\n+                \"type\": \"object\"\n+            },\n+            \"TodoDetail\": {\n+                \"properties\": {\n+                    \"id\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"title\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"descr\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"category\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"parent\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"status\": {\n+                        \"type\": \"integer\",\n+                        \"maximum\": 100,\n+                        \"minimum\": 0\n+                    },\n+                    \"pri\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"access\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"owner\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"assigned\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"sdate\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"edate\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"has_subs\": {\n+                        \"type\": \"boolean\"\n+                    }\n+                },\n+                \"type\": \"object\"\n+            },\n+            \"TodoCategory\": {\n+                \"properties\": {\n+                    \"id\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"name\": {\n+                        \"type\": \"string\"\n+                    }\n+                },\n+                \"type\": \"object\"\n+            },\n+            \"TodoUpsertRequest\": {\n+                \"properties\": {\n+                    \"title\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"descr\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"cat\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"parent\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"pri\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"status\": {\n+                        \"type\": \"integer\",\n+                        \"maximum\": 100,\n+                        \"minimum\": 0\n+                    },\n+                    \"access\": {\n+                        \"type\": \"boolean\"\n+                    },\n+                    \"assigned\": {\n+                        \"oneOf\": [\n+                            {\n+                                \"type\": \"string\"\n+                            },\n+                            {\n+                                \"type\": \"array\",\n+                                \"items\": {\n+                                    \"type\": \"string\"\n+                                }\n+                            }\n+                        ]\n+                    },\n+                    \"assigned_group\": {\n+                        \"oneOf\": [\n+                            {\n+                                \"type\": \"string\"\n+                            },\n+                            {\n+                                \"type\": \"array\",\n+                                \"items\": {\n+                                    \"type\": \"string\"\n+                                }\n+                            }\n+                        ]\n+                    },\n+                    \"sday\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"smonth\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"syear\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"eday\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"emonth\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"eyear\": {\n+                        \"type\": \"integer\"\n+                    }\n+                },\n+                \"type\": \"object\"\n+            },\n+            \"TodoErrorResponse\": {\n+                \"properties\": {\n+                    \"error\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"reason\": {\n+                        \"type\": \"string\"\n+                    },\n+                    \"todo_id\": {\n+                        \"type\": \"integer\"\n+                    },\n+                    \"parent_id\": {\n+                        \"type\": \"integer\"\n+                    }\n+                },\n+                \"type\": \"object\"\n             }\n         },\n         \"securitySchemes\": {\n@@ -13308,6 +14105,10 @@\n             \"name\": \"Workorder\",\n             \"description\": \"REST API for workorder resources\"\n         },\n+        {\n+            \"name\": \"Todo\",\n+            \"description\": \"REST API for todo items\"\n+        },\n         {\n             \"name\": \"Relationships\",\n             \"description\": \"Relationships\""}]}