diff --git a/README.md b/README.md index 7f37ce4..b6babc2 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,203 @@ [![Deploy app](https://github.com/Adacis/whereami/actions/workflows/release.yml/badge.svg)](https://github.com/Adacis/whereami/actions/workflows/release.yml) -# whereami -Is a simple application to locate everybody in your company +# Where Am I ? +
+ + Logo + Logo dark + + +

Where Am I ? (WAI)

+ +

+ Where am I is a management application allowing users to record and see who and what everyone is working on in a calendar and table. The application is hosted on Nextcloud. +
+
+

+
+ + +## Tech Stack + +**Client:** JS + +**Server:** PHP Symphony + +**Database:** MySQL + +## Installation + +### Prerequisites + +The Git command must be installed on the machine, or if you're on Windows or MacOS, you can use the Github Desktop application: https://desktop.github.com/ + +Once git is installed on your machine, clone the WhereAmI project (https://github.com/Adacis/whereami.git) to a directory on your machine. + +Install Docker (Windows, MacOS or Linux): https://www.docker.com/products/docker-desktop +or from the command line (e.g. Ubuntu): +https://docs.docker.com/engine/install/ubuntu/ + +After installing Docker, create the “nextcloud” network: +```sh + docker network create nextcloud + ``` + + + +### Installing the Nextcloud database + +This part of the installation will enable us to install a MySQL 8.3.0 database, which will be the database for our Nextcloud application. + +**Step 1**: +Go to the directory where the project was cloned in the prerequisites and then to the “whereami-docker” folder: + +```sh +cd whereami-docker/ +``` + +**Step 2**: +Run the docker command to create the MySQL database container: +```sh +docker compose up -d +``` + +**Step 3**: +Check that the container has launched with the command : +```sh +docker ps +``` + +A container named “nextcloud-db” should be present in the list. If not, you can retrieve the container's logs with the command +docker logs nextcloud-db + +**Step 4**: +Check that the database is accessible with a tool like Dbeaver (https://dbeaver.io/). +The identifiers are as follows: +``` +Server Host: localhost +Port: 3306 +Username: nextcloud +Password: nextcloud +``` + +### Installing the Nextcloud application + +This part requires you to have installed the database beforehand. + +**Step 1**: +Create a “nextcloud_data” folder on your machine: +```sh +mkdir /Users/miage/Documents/nextcloud_data +``` + +**Step 2**: +Launch the nextcloud container, which loads the nextcloud:28.0.5 image. +(Remember to change the data folder /Users/miage/Documents/nextcloud_data to the one you created in step 1) +```sh +docker run -d --name nextcloud -p 8001:80 -v /Users/miage/Documents/nextcloud_data:/var/www/html --network nextcloud nextcloud:28.0.5 +``` + +Note that the container will be on port 8001 of the machine, so it's possible to modify it if necessary. + +**Step 3**: +Check that the container has launched with the command : +```sh +docker ps +``` + +A container named “nextcloud” should be present in the list. If not, you can retrieve the container's logs with the command : +```sh +docker logs nextcloud +``` + + +**Step 4**: +Go to Nextcloud at http://localhost:8001 +Enter the credentials for the Nextcloud administrator account you want, e.g. : +``` +Username : adacis +Password : admin +``` + +In the “Configure database” section, select the “MySQL/MariaDB” box and enter the following information: +``` +Database user: nextcloud +Database password: nextcloud +Database name: nextcloud-db +Database host: nextcloud-db:3306 +``` + + + +Then click on Install. +You now have an instance of Nextcloud 28.0.5. + +### Installing the WhereAmI application + +**Step 1** : +Take the project already cloned in a directory, and copy it into the nextcloud_data/apps/ folder created during Nextcloud installation with the command +```sh +cp -r [Directory containing cloned project]/whereami [Path to nextcloud_data folder]/nextcloud_data/apps/ +``` + +**Step 2** : +- Log on to Nextcloud with an administrator account +- Click on your profile photo in the top right-hand corner of the screen +- Select “Applications +- Search for the “Where am I?” application and click on Activate + + +You now have the Where Am I application on Nextcloud. To use this application, please refer to the user manual. + + +## Deployment (only in dev mode) + +### First time deployment +To deploy this project run the following commands in the order : +- Start the Docker Bash Terminal for Nextcloud +```bash +docker exec -it nextcloud bash +``` +- Navigate to the correct folder +```bash +cd apps/whereami/ +``` +- Install and update NPM if you don't have it +```bash +apt-get update && apt-get install -y npm +``` +- Initialise the project +```bash +make npm-init +``` +- Build the project +```bash +make build-js +``` + +### Build +- Start the Docker Bash Terminal +```bash +docker exec -it nextcloud bash +``` +- Place yourself in the correct folder +```bash +cd apps/whereami/ +``` +- Build the project +```bash +make build-js +``` +## Features +- **Event Assignment using the Calendar feature**: Users can register themselves, record their whereabouts and time slots for specific contracts as well as their work status, which are then converted into full or half work days. +- **Employees Tab**: A summary table of all users, their locations, and their status for the selected date range. +- **Location Tab**: A summary table showing the number of users by location for the selected date range. +- **Last Seen Tab**: A summary table of all the instances where users have crossed paths with other users within the selected date range. +- **Contract Tab**: A summary table listing all users with their contracts and the total hours worked, rounded to the nearest half day, for the selected date range. +- **Summary Tab**: A summary table showing the locations and the time spent at each location by the user for the selected month. +- **Hover Summary Functionality**: Provide brief information when hovering over specific dates. + +## Authors +- [@Benjamin Aimard](https://github.com/baimard) diff --git a/appinfo/info.xml b/appinfo/info.xml index 414fc8e..88a3165 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -4,7 +4,7 @@ Where am I ? - 0.0.36 + 0.0.37 agpl ADACIS Whereami @@ -12,7 +12,7 @@ organization https://github.com/Adacis/whereami/issues - + OCA\Whereami\Settings\WhereamiAdmin diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index ceb873d..dc8b0ed 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -86,7 +86,7 @@ public function getNavigationLink() public function getContracts(String $dtStart, String $dtEnd) { // Fetch data from the database - $result = $this->myDb->getContracts($dtStart, $dtEnd); + $result = $this->myDb->getContracts($dtStart, $dtEnd, $this->userId); // Initialize arrays to store contracts and users per contract $contracts = []; diff --git a/lib/Db/Bdd.php b/lib/Db/Bdd.php index 2c5f87e..c5a2811 100755 --- a/lib/Db/Bdd.php +++ b/lib/Db/Bdd.php @@ -12,11 +12,29 @@ class Bdd private String $tableprefix; private $logger; + private $grpWhereAmI = 'whereami_global'; + + public function __construct(IDbConnection $db, LoggerInterface $log) { $this->pdo = $db; $this->tableprefix = '*PREFIX*' . "whereami_"; $this->logger = $log; + $this->initializeGroupWhereAmI(); + } + + /** + * Insert the group whereami_global in the database if it does not exist + */ + public function initializeGroupWhereAmI() + { + + $sql = "SELECT * FROM `*PREFIX*groups` WHERE `gId` = ?"; + + if (empty($this->execSQLNoJsonReturn($sql, array($this->grpWhereAmI)))) { + $sql = "INSERT INTO `*PREFIX*groups` (`gId`,`displayname`) VALUES (?,?)"; + $this->execSQLNoData($sql, array($this->grpWhereAmI, $this->grpWhereAmI)); + } } public function listUID() @@ -180,14 +198,21 @@ public function getIconsInPrefixList($person, $label) * Retrieve activity reports for contracts within a specified date range. * * This method fetches activity reports including the number of contracts (`nb_contract`), - * associated usernames (`username`), aggregated activity values (`activity_report_value`), + * associated usernames (`uid`), aggregated activity values (`activity_report_value`), * and corresponding dates (`activity_report_date`) for a given time period. + * The results are filtered based on the user's group membership. If the user belongs + * to the "WhereAmI" global group, they will see all available data for every user. Otherwise, + * they will only see their own data. * + * @see Bdd::$grpWhereAmI Constant that holds the value for the "WhereAmI" global group. * @param string $dtStart The start date of the date range (YYYY-MM-DD format). * @param string $dtEnd The end date of the date range (YYYY-MM-DD format). + * @param string $uid The ID of the current user. * @return array Fetched database results containing activity report details. */ - public function getContracts($dtStart, $dtEnd){ + public function getContracts($dtStart, $dtEnd, $uid){ + $isGlobalUser = $this->isGrpWanted($uid, $this->grpWhereAmI); + $sql = "SELECT REGEXP_SUBSTR(value, '\\\\b[Dd]\\\\d{5}\\\\b') as nb_contract, uid, @@ -221,9 +246,11 @@ public function getContracts($dtStart, $dtEnd){ AND FROM_UNIXTIME(oc.firstoccurence) BETWEEN ? AND ? AND oc.deleted_at IS NULL ) sr + WHERE + (? = TRUE) OR ? = uid GROUP BY value, uid, first_occurence, last_occurence"; - return $this->execSQLNoJsonReturn($sql, array($dtStart, $dtEnd)); + return $this->execSQLNoJsonReturn($sql, array($dtStart, $dtEnd, $isGlobalUser, $uid)); } @@ -249,6 +276,16 @@ private function getQuadri($name) return strtoupper($quadri); } + /** + * Return true if the user is in the group wanted + * @userId : current user id + */ + public function isGrpWanted($userId,$grp) + { + $sql = "SELECT * FROM `*PREFIX*group_user` WHERE `gid` = ? AND `uid` = ?"; + return !empty($this->execSQLNoJsonReturn($sql, array($grp, $userId))); + } + /** * @sql * @array() //prepare statement diff --git a/src/js/class/ListEvents.js b/src/js/class/ListEvents.js index 5afb664..24be636 100644 --- a/src/js/class/ListEvents.js +++ b/src/js/class/ListEvents.js @@ -102,6 +102,7 @@ export class ListEvents { e.place2 = e.place } + if (placeIsExcluded) { isSomeoneThere = true isSomeoneThere2 = true @@ -118,16 +119,10 @@ export class ListEvents { } } - if (e.place === this.id) { title += e.nextcloud_users + '\n' res += 1 found = true - } - if (e.place2 === this.id) { - title2 += e.nextcloud_users + '\n' - res2 += 1 - found2 = true - } + } }) @@ -140,10 +135,8 @@ export class ListEvents { let div1 = this.createDiv(res, title, found, isSomeoneThere, from) div.appendChild(div1) if (title !== title2) { - let div2 = this.createDiv(res2, title2, found2, isSomeoneThere2, from) div1.classList.add('cell-base-two') - div2.classList.add('cell-base-two') - div.appendChild(div2) + div.appendChild(div1) } else div1.classList.add('cell-base-alone') diff --git a/src/js/module/datatables.js b/src/js/module/datatables.js index 3fa1aad..688447f 100644 --- a/src/js/module/datatables.js +++ b/src/js/module/datatables.js @@ -98,84 +98,71 @@ function getAllDatesFromContract (informations) { * } */ export function newTableContracts(response) { - const res = JSON.parse(response); - - const table = document.createElement('table'); - table.setAttribute('id', 'contracts'); - table.setAttribute('class', 'table table-striped'); - - let thead = document.createElement('thead'); - let tbody = document.createElement('tbody'); - - // Create a headline with contracts names - const headLine = document.createElement('tr'); - headLine.appendChild(newCell('th', 'Contracts')); // Header for contracts column - - - // Add each contracts name to the headline - Object.keys(res.userByContract).forEach(contractKey => { - headLine.appendChild(newCell('th', contractKey)); - }); - - thead.appendChild(headLine); - - // Create an array to hold all contracts for each employee - const employeeContracts = {}; - - // Iterate over each contract - Object.keys(res.contracts).forEach(contractKey => { - const contractData = res.contracts[contractKey]; - const userName = Object.keys(contractData)[0]; - // Iterate over each employee - Object.keys(res.userByContract).forEach(contractKey => { - const contractCount = res.userByContract[contractKey][userName] || 0; - // Add contract value to employee's contract array - if (!employeeContracts[contractKey]) { - employeeContracts[contractKey] = []; - } - //If employeeContractcs[contractKey] not contains userName, add it - if (!employeeContracts[contractKey].some(e => e.user === userName)){ - employeeContracts[contractKey].push({ - user: userName, - count: contractCount - }); - } - }); - }); - - let userPresent = [] - - // Create rows for each contract - Object.keys(employeeContracts).forEach(contractKey => { - const contractData = res.contracts[contractKey]; - const userName = Object.keys(contractData)[0]; - const contractRow = document.createElement('tr'); - - if (!userPresent.includes(userName)) { - contractRow.appendChild(newCell('td', userName)); // Add employee in the first row - // Add each time the employee was present for each contract - for (const key in employeeContracts) { - for(let i = 0; i < employeeContracts[key].length; i++){ - if(employeeContracts[key][i].user === userName) { - contractRow.appendChild(newCell('td', employeeContracts[key][i].count, '', getAllDatesFromContract(res.contracts[key][userName]))); - } - } - - } - - tbody.appendChild(contractRow); - userPresent.push(userName); - } + const res = JSON.parse(response); + + const table = document.createElement('table'); + table.setAttribute('id', 'contracts'); + table.setAttribute('class', 'table table-striped'); + + let thead = document.createElement('thead'); + let tbody = document.createElement('tbody'); + + // Create a headline with contracts names + const headLine = document.createElement('tr'); + headLine.appendChild(newCell('th', 'Contracts')); // Header for contracts column + + // Add each contracts name to the headline + Object.keys(res.userByContract).forEach(contractKey => { + headLine.appendChild(newCell('th', contractKey)); + }); + + thead.appendChild(headLine); + + // Create an array to hold all contracts for each employee + const employeeContracts = {}; + + // Iterate over each contract + Object.keys(res.contracts).forEach(contractKey => { + const contractData = res.contracts[contractKey]; + // Iterate over each user within the contract + Object.keys(contractData).forEach(userName => { + const contractCount = res.userByContract[contractKey][userName] || 0; + // Initialize user in employeeContracts if not already present + if (!employeeContracts[userName]) { + employeeContracts[userName] = {}; + } + // Add contract value to employee's contract array + employeeContracts[userName][contractKey] = contractCount; }); + }); + + // Track which users have been added to the table + let userPresent = []; + + // Create rows for each user + Object.keys(employeeContracts).forEach(userName => { + if (!userPresent.includes(userName)) { + const userContracts = employeeContracts[userName]; + const contractRow = document.createElement('tr'); + + contractRow.appendChild(newCell('td', userName)); // Add employee name in the first column + // Add each contract count for the user + Object.keys(res.userByContract).forEach(contractKey => { + const contractCount = userContracts[contractKey] || 0; + contractRow.appendChild(newCell('td', contractCount, '', getAllDatesFromContract(res.contracts[contractKey][userName]))); + }); + + tbody.appendChild(contractRow); + userPresent.push(userName); + } + }); - table.appendChild(thead); - table.appendChild(tbody); - document.getElementById('myapp').innerHTML = ''; - document.getElementById('myapp').appendChild(table); + table.appendChild(thead); + table.appendChild(tbody); + document.getElementById('myapp').innerHTML = ''; + document.getElementById('myapp').appendChild(table); } - - export function newTableSeen(response, dtStart, dtEnd) { const res = JSON.parse(response) var totalPeople = 1 diff --git a/templates/navigation/index.php b/templates/navigation/index.php index 27ac4cf..474317d 100644 --- a/templates/navigation/index.php +++ b/templates/navigation/index.php @@ -1,5 +1,5 @@