Compare commits

..
Author SHA1 Message Date
bence 5041258de0 Merge pull request 'pass scheme to fastcgi' (!70) from bugfix/pass-scheme-to-fastcgi into master
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #70
2023-09-28 14:55:37 +02:00
bence 47928d2d2b pass scheme to fastcgi
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 14:43:30 +02:00
bence 55cf2afde3 Merge pull request 'feature/update-to-ubuntu-2204' (!69) from feature/update-to-ubuntu-2204 into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #69
2023-09-28 14:42:43 +02:00
bence ed2b1c23ae update soko-web
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 14:30:59 +02:00
bence 6ef45b8b6a generate composer.lock 2023-09-28 14:30:58 +02:00
bence b4c359e81d update phpunit 2023-09-28 14:30:58 +02:00
bence 495a2fe910 update ubuntu to 22.04 and php to 8.1 2023-09-28 14:30:58 +02:00
bence 37ba0ec172 Merge pull request 'feature/create-working-docker-images' (!68) from feature/create-working-docker-images into master
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #68
2023-09-28 13:33:02 +02:00
bence 9ffde6bccb update readme
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 13:31:26 +02:00
bence 0ce1c4f28a update soko-web 2023-09-28 13:21:54 +02:00
bence 0d1e4a3d1c update docker-compose 2023-09-28 13:21:54 +02:00
bence cbf62d1c4a add docker release stage to pipeline 2023-09-28 13:21:54 +02:00
bence 5eeac18b4c use the new dockerfile in pipeline 2023-09-28 13:21:54 +02:00
bence c4dce94f5e delete deprecated dockerfiles 2023-09-28 13:21:54 +02:00
bence 147b7690ac add new dockerfile with dev and release stages 2023-09-28 13:21:54 +02:00
bence bdfa46b838 add entry point for dev docker 2023-09-28 13:21:54 +02:00
bence 27fe883f59 add entry point for release docker 2023-09-28 13:21:54 +02:00
bence 1718dfca9e install base database in MigrateDatabaseCommand 2023-09-28 13:21:54 +02:00
bence 6870d08c21 remove deprecated scripts 2023-09-28 13:21:54 +02:00
bence eba674bfbc add cron for db:maintain 2023-09-28 13:21:54 +02:00
bence 832170b1e1 add release generator script 2023-09-28 13:21:54 +02:00
bence 8dfea0993b add nodejs installer script 2023-09-28 13:21:54 +02:00
bence 6146641eed Merge pull request 'fix selectUpcomingAndRecent when used with other selects' (!67) from bugfix/upcoming-events-are-multiplicated into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #67
2023-08-06 22:09:33 +02:00
bence 7cb406cc49 fix selectUpcomingAndRecent when used with other selects
rvr-nextgen/pipeline/pr-master This commit looks good
2023-08-06 22:06:42 +02:00
bence 2d2cf9c6f0 Merge pull request 'make short new transaction url even shorter' (!66) from feature/change-short-new-transaction-url into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #66
2023-07-24 21:43:39 +02:00
bence ca069d0795 make short new transaction url even shorter
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 21:42:27 +02:00
bence 17d9437a50 Merge pull request 'fix select to avoid getting all events as upcoming/recent' (!65) from bugfix/fix-select-for-upcoming-and-recent-events into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #65
2023-07-24 02:12:15 +02:00
bence aed4548c7b fix select to avoid getting all events as upcoming/recent
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 02:11:27 +02:00
bence 0d6d152b82 Merge pull request 'create endpoints to get current event and current event new transaction' (!64) from feature/create-endpoint-for-currect-event-redirection into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #64
2023-07-24 02:08:11 +02:00
bence 7beec510a1 create endpoints to get current event and current event new transaction
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 02:00:59 +02:00
bence 22b849a1c8 Merge pull request 'select current user as payer by default for new transaction' (!63) from feature/default-payer-is-current-user into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #63
2023-07-24 01:31:22 +02:00
bence 492aa9fcef select current user as payer by default for new transaction
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-24 01:29:53 +02:00
bence b6da70e015 Merge pull request 'fix transaction edit header' (!62) from bugfix/fix-transaction-edit-header into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #62
2023-07-12 00:40:06 +02:00
bence afead7fd61 Merge pull request 'show balance for events' (!61) from feature/show-balance-for-events into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #61
2023-07-12 00:38:17 +02:00
bence 69acbf708b fix transaction edit header
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-12 00:32:55 +02:00
bence 08ebfae030 show balance for events
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-12 00:26:59 +02:00
bence a597839379 Merge pull request 'feature/RVRNEXT-45-show-recent-events-as-well' (!60) from feature/RVRNEXT-45-show-recent-events-as-well into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #60
2023-07-08 14:44:35 +02:00
bence e0fe6a8622 RVRNEXT-45 update soko-web to 0.13.1
rvr-nextgen/pipeline/pr-master This commit looks good
2023-07-08 14:42:09 +02:00
bence 5ebce653dc RVRNEXT-45 show recent events as well with upcoming events 2023-07-08 13:52:24 +02:00
bence 61eb2404a2 Merge pull request 'feature/RVRNEXT-43-split-transactions' (!59) from feature/RVRNEXT-43-split-transactions into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #59
2023-06-17 14:59:09 +02:00
bence e199dfccc7 RVRNEXT-43 update soko-web
rvr-nextgen/pipeline/pr-master This commit looks good
2023-06-17 14:35:00 +02:00
bence 1d0a2b09d5 RVRNEXT-43 adapt balance calculations to multiple payees 2023-06-17 13:47:05 +02:00
bence 1886257b63 RVRNEXT-43 adapt views to multiple payees 2023-06-17 13:46:43 +02:00
bence eda1415e86 RVRNEXT-43 implement multiple payees in transaction controller 2023-06-17 13:46:13 +02:00
bence 4e9c7f8c3c RVRNEXT-43 extend community member repository with count 2023-06-17 13:17:29 +02:00
bence eebf4aff00 RVRNEXT-43 add multi relation for transaction 2023-06-17 13:16:53 +02:00
bence 6dae33507f RVRNEXT-43 add repository for transaction payees 2023-06-17 13:15:54 +02:00
bence 96d5aa2219 RVRNEXT-43 add model for transaction payee 2023-06-17 13:14:43 +02:00
bence 6c326b89e2 RVRNEXT-43 add migration for transaction payees 2023-06-17 13:14:33 +02:00
bence 6c821d41b4 Merge pull request 'feature/RVRNEXT-39-delete-communities' (!58) from feature/RVRNEXT-39-delete-communities into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #58
2023-06-16 21:39:45 +02:00
bence ade02f4736 RVRNEXT-39 add endpoint for community delete
rvr-nextgen/pipeline/pr-master This commit looks good
2023-06-16 21:38:27 +02:00
bence bb5b510315 RVRNEXT-39 add delete button for communities 2023-06-16 21:38:27 +02:00
bence 1267cb3d95 RVRNEXT-39 implement community delete 2023-06-16 21:38:26 +02:00
bence c7641b85e7 RVRNEXT-39 make main currency nullable 2023-06-16 21:28:42 +02:00
bence ee4b8e9d65 Merge pull request 'bugfix/RVRNEXT-31-allow-special-characters-for-local-assets' (!57) from bugfix/RVRNEXT-31-allow-special-characters-for-local-assets into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #57
2023-05-28 22:31:21 +02:00
bence 58d4f982e9 RVRNEXT-31 remove hack for assets of tom-select
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 22:30:00 +02:00
bence ad06cfaa58 RVRNEXT-31 update soko-web 2023-05-28 22:30:00 +02:00
bence aa2e0e9b4a Merge pull request 'feature/RVRNEXT-22-add-pagination-template' (!56) from feature/RVRNEXT-22-add-pagination-template into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #56
2023-05-28 20:46:22 +02:00
bence d1c926541a RVRNEXT-22 use the new repository methods in controllers
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 20:34:07 +02:00
bence 9929e00a3f RVRNEXT-22 use Select::paginate in repositories 2023-05-28 20:34:07 +02:00
bence bf415a2a46 RVRNEXT-22 use pagination template 2023-05-28 20:34:00 +02:00
bence c0bfa5b334 RVRNEXT-22 add pagination template 2023-05-28 20:33:59 +02:00
bence e122248a97 Merge pull request 'fix sql statement for table transactions' (!55) from bugfix/fix-buggy-migration into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #55
2023-05-28 17:03:11 +02:00
bence d2bb1d4936 fix sql statement for table transactions
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 17:01:19 +02:00
bence b8a901b041 Merge pull request 'feature/RVRNEXT-11-events' (!54) from feature/RVRNEXT-11-events into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #54
2023-05-28 16:52:48 +02:00
bence ede5c24a5d RVRNEXT-11 fix archive in pipeline
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-28 16:48:38 +02:00
bence 0ae7040804 RVRNEXT-11 update soko-web to 0.12.1 2023-05-28 16:48:38 +02:00
bence f370ccba88 RVRNEXT-11 add upcoming events for user 2023-05-28 16:48:38 +02:00
bence a128168e7f RVRNEXT-11 fix select in community members form 2023-05-28 16:48:38 +02:00
bence 3e9c25f6fe RVRNEXT-11 style improvements 2023-05-28 16:48:38 +02:00
bence 352311a82e RVRNEXT-11 add custom title for main layout 2023-05-28 16:48:38 +02:00
bence c97d80a724 RVRNEXT-11 add upcoming events for community 2023-05-28 16:48:38 +02:00
bence 532dbfd8d6 RVRNEXT-11 add event for transactions 2023-05-28 16:48:37 +02:00
bence 49f3a371e5 RVRNEXT-11 add views for events 2023-05-28 16:48:37 +02:00
bence 2bcaf13ee3 RVRNEXT-11 add controller for events 2023-05-28 16:48:37 +02:00
bence 2fc5812eba RVRNEXT-11 add new endpoints for events 2023-05-28 11:29:36 +02:00
bence 63ca6e7557 RVRNEXT-11 add model and repository for events 2023-05-28 11:29:36 +02:00
bence 224cdc0eda RVRNEXT-11 add migrations for events 2023-05-28 11:29:35 +02:00
bence 42da9eb778 Merge pull request 'bugfix/fixes-for-password-reset' (!53) from bugfix/fixes-for-password-reset into master
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #53
2023-05-26 16:08:50 +02:00
bence d1dee27191 reset grecaptcha in case of error
rvr-nextgen/pipeline/pr-master This commit looks good
2023-05-08 18:48:43 +02:00
bence 726b1818d0 fix error message for password reset request 2023-05-08 18:48:12 +02:00
54 changed files with 1921 additions and 1066 deletions

No files matched your search

Vendored
+49 -5
View File
@@ -13,8 +13,9 @@ pipeline {
}
agent {
dockerfile {
filename 'docker/Dockerfile-test'
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true
}
}
@@ -26,8 +27,9 @@ pipeline {
stage('Unit Testing') {
agent {
dockerfile {
filename 'docker/Dockerfile-test'
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true
}
}
@@ -35,7 +37,7 @@ pipeline {
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
}
post {
success {
always {
archiveArtifacts 'unit_test_results.xml'
}
}
@@ -44,8 +46,9 @@ pipeline {
stage('Static Code Analysis') {
agent {
dockerfile {
filename 'docker/Dockerfile-test'
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true
}
}
@@ -53,10 +56,51 @@ pipeline {
sh 'php vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
}
post {
success {
always {
archiveArtifacts 'static_code_analysis_results.json'
}
}
}
stage('Prepare Docker release') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
npm_config_cache="${WORKSPACE}/.npm"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true
}
}
steps {
script {
sh script: 'git clean -ffdx', label: 'Clean repository'
env.VERSION = sh(script: 'git describe --tags --always --match "Release_*" HEAD', returnStdout: true).trim()
sh script: 'docker/scripts/release.sh', label: 'Release script'
sh script: "rm -rf ${env.COMPOSER_HOME} ${env.npm_config_cache}"
}
}
}
stage('Release Docker image') {
steps {
script {
withDockerRegistry([credentialsId: 'gitea-system-user', url: 'https://git.esoko.eu/']) {
sh script: 'docker buildx create --use --bootstrap --platform=linux/arm64,linux/amd64 --name multi-platform-builder'
sh script: """docker buildx build \
--platform linux/amd64,linux/arm64 \
-f docker/Dockerfile \
--target rvr_release \
-t git.esoko.eu/esoko/rvr:${env.VERSION} \
--push \
.""",
label: 'Build Docker image'
}
}
}
}
}
}
+59 -59
View File
@@ -2,81 +2,81 @@
[![Build Status](https://ci.esoko.eu/job/rvr-nextgen/job/master/badge/icon)](https://ci.esoko.eu/job/rvr-nextgen/job/master/)
This is the RVR Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
This is the RVR Application project.
## Installation
### Clone the Git repository
### Set environment variables
The first step is obviously cloning the repository to your machine:
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment. Check `.env.example` for reference.
**Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
### Docker Compose
Create a `docker-compose.yml` file. The example code below assumes that `.env` is placed in the same folder.
```yml
version: '3'
services:
app:
image: git.esoko.eu/esoko/rvr:latest
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
volumes:
- .env:/var/www/rvr/.env
mariadb:
image: mariadb:10.3
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'rvr'
MYSQL_USER: 'rvr'
MYSQL_PASSWORD: 'rvr'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
volumes:
mysql:
```
git clone https://git.esoko.eu/esoko/rvr-nextgen.git
```
All the commands listed here should be executed from the repository root.
### Setup Docker stack (recommended)
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
All you have to do is executing the following command:
```
Execute the following command:
```bash
docker compose up -d
```
Attach shell to the container of `rvr-nextgen-app`:
```
docker exec -it rvr-nextgen-app bash
```
All of the following commands should be executed there.
### Manual setup (alternative)
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
### Initialize project
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
```
composer create-project
```
### Set environment variables
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
```
0 * * * * /path/to/your/installation/rvr db:maintain >>/var/log/cron-rvr.log 2>&1
```
### Finalize installation
After you followed the above steps, execute the following command:
```
scripts/install.sh
```
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **rvr@rvr.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
**And you are done!** The application is ready to use. You can create the first administrative user with the following command after attaching to the `app` container:
```
./rvr user:add EMAIL PASSWORD admin
```
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
## Development
### Set environment variables
`.env.example` should be copied to `.env` into the repo root. Only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. **`DEV=1` should be set for development!**
### Docker Compose
Execute the following command from the repo root:
```bash
docker compose up -d
```
**And you are done!** You can reach the application on http://localhost. The mails that are sent by the application can be found on http://localhost:8080. If needed, the database server can be directly reached on localhost:3306, or you can use Adminer web interface on http://localhost:9090
You might have to attach to the `app` container, e.g. for creating users, `composer update`, etc.
---
+2 -2
View File
@@ -10,11 +10,11 @@
}
],
"require": {
"esoko/soko-web": "0.11",
"esoko/soko-web": "0.15",
"firebase/php-jwt": "^6.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"phpunit/phpunit": "^10.3",
"phpstan/phpstan": "^1.10"
},
"autoload": {
Generated
+381 -583
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,13 @@
CREATE TABLE `events` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`slug` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
`community_id` int(10) unsigned NOT NULL,
`start` timestamp NOT NULL DEFAULT current_timestamp(),
`end` timestamp NOT NULL DEFAULT current_timestamp(),
`title` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`),
KEY `community_id` (`community_id`),
UNIQUE KEY `slug` (`slug`),
CONSTRAINT `events_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,4 @@
ALTER TABLE `transactions`
ADD `event_id` int(10) unsigned NULL AFTER `community_id`,
ADD KEY `event_id` (`event_id`),
ADD CONSTRAINT `transactions_event_id` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`);
@@ -0,0 +1,11 @@
CREATE TABLE `transaction_payees` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_for_transaction` (`transaction_id`, `user_id`),
KEY `transaction_id` (`transaction_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `transaction_payees_transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`id`),
CONSTRAINT `transaction_payees_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+14 -2
View File
@@ -2,12 +2,17 @@ version: '3'
services:
app:
build:
context: ./docker
dockerfile: Dockerfile-app
context: .
dockerfile: docker/Dockerfile
target: rvr_dev
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
volumes:
- .:/var/www/rvr
working_dir: /var/www/rvr
mariadb:
image: mariadb:10.3
ports:
@@ -19,6 +24,13 @@ services:
MYSQL_DATABASE: 'rvr'
MYSQL_USER: 'rvr'
MYSQL_PASSWORD: 'rvr'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer:4.8.1-standalone
ports:
+44
View File
@@ -0,0 +1,44 @@
FROM ubuntu:22.04 AS rvr_base
ENV DEBIAN_FRONTEND noninteractive
RUN apt update --fix-missing && apt install -y sudo curl git unzip mariadb-client nginx \
php-apcu php8.1-cli php8.1-curl php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-zip php8.1-xml
RUN mkdir -p /run/php
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default
COPY docker/scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
COPY docker/scripts/install-nodejs.sh install-nodejs.sh
RUN ./install-nodejs.sh
RUN npm install -g uglify-js clean-css-cli svgo yarn
FROM rvr_base AS rvr_dev
RUN apt update --fix-missing && apt install -y php-xdebug
RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_autostart = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_connect_back = 1" >> /etc/php/8.1/mods-available/xdebug.ini
EXPOSE 80
EXPOSE 5000
EXPOSE 8090
EXPOSE 9229
ENTRYPOINT docker/scripts/entry-point-dev.sh
FROM rvr_base AS rvr_release
RUN apt update --fix-missing && apt install -y cron
WORKDIR /var/www/rvr
COPY ./ /var/www/rvr
RUN rm -rf /var/www/rvr/.git
EXPOSE 80
EXPOSE 8090
ENTRYPOINT docker/scripts/entry-point.sh
-30
View File
@@ -1,30 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git unzip mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php7.4-xml
# Configure Nginx with PHP
RUN mkdir -p /run/php
COPY configs/nginx.conf /etc/nginx/sites-available/default
RUN echo "xdebug.remote_enable = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_autostart = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.ini
# Install Composer
COPY scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
RUN npm install -g uglify-js clean-css-cli svgo yarn
EXPOSE 80
VOLUME /var/www/rvr
WORKDIR /var/www/rvr
ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'
-6
View File
@@ -1,6 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && apt install -y curl git unzip php7.4-cli php7.4-mbstring php7.4-xml
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+2 -1
View File
@@ -14,7 +14,8 @@ server {
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param REQUEST_SCHEME $forwarded_scheme;
}
location ~ /\.ht {
+1
View File
@@ -0,0 +1 @@
0 * * * * /var/www/rvr/rvr db:maintain
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
if [ -f .env ]; then
composer install
else
composer create-project
fi
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Migrating DB..."
./rvr db:migrate
echo "Set runner user based on owner of .env..."
USER_UID=$(stat -c "%u" .env)
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID rvr
useradd --uid $USER_UID --gid $USER_GID rvr
sed -i -e "s/^user = .*$/user = rvr/g" -e "s/^group = .*$/group = rvr/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
wait -n
exit $?
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -e
echo "Migrating DB..."
./rvr db:migrate
echo "Installing crontab..."
/usr/bin/crontab docker/scripts/cron
echo "Set runner user based on owner of .env..."
USER_UID=$(stat -c "%u" .env)
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID rvr
useradd --uid $USER_UID --gid $USER_GID rvr
chown -R rvr:rvr cache
sed -i -e "s/^user = .*$/user = rvr/g" -e "s/^group = .*$/group = rvr/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/cron -f &
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
wait -n
exit $?
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -e
apt update
apt install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
apt update
apt install -y nodejs
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
composer create-project --no-dev
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" app.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" app.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" app.php
echo "Minifying JS, CSS and SVG files..."
find public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
echo "Linking view files..."
./rvr view:link
rm .env
+17 -4
View File
@@ -108,6 +108,10 @@ p.small, span.small, td.small {
font-size: 14px;
}
p.big, span.big, td.big {
font-size: 18px;
}
.red {
color: #a80908;
}
@@ -255,11 +259,11 @@ button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button
}
button.yellow, a.button.yellow {
background-color: #e8a349;
background-color: #daa520;
}
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
background-color: #c37713;
background-color: #b8860b;
}
button.green, a.button.green {
@@ -397,7 +401,7 @@ header>p {
}
header>p>span {
padding-left: 6px;
padding-left: 8px;
}
header>p>span>a:link, header>p>span>a:visited {
@@ -410,7 +414,7 @@ header>p>span>a:hover, header>p>span>a:focus {
header>p>span:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
padding-right: 8px;
}
main {
@@ -537,6 +541,15 @@ p.formLabel {
font-size: 14px;
}
span.label {
border-radius: 3px;
background-color: #555555;
color: #ffffff;
padding: 2px 4px;
font-weight: bold;
font-size: 13px;
}
@media screen and (max-width: 424px) {
div.gridContainer {
grid-template-columns: auto;
@@ -0,0 +1,30 @@
(function () {
const element = document.getElementById('transactionForm').elements['event_id'];
const select = new TomSelect(element, {
valueField: 'value',
labelField: 'label',
searchField: 'label',
loadThrottle: 300,
load: function (query, callback) {
var self = this;
RVR.httpRequest('GET', searchEventUrl.replace('QUERY', encodeURIComponent(query)), function () {
self.clearOptions();
callback(this.response.results);
});
},
});
select.on('change', function (value) {
this.clearOptions();
});
select.on('blur', function (value) {
this.clearOptions();
});
select.on('type', function (value) {
if (value === '') {
this.clearOptions();
}
});
})();
+3
View File
@@ -64,6 +64,9 @@ var RVR = {
formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText;
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
return;
}
-162
View File
@@ -1,162 +0,0 @@
#!/usr/bin/python3
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
import sys
import os
import subprocess
import re
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
if len(sys.argv) < 4:
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
exit(1)
REPO = os.path.abspath(sys.argv[1])
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
class Worktree:
def __init__(self, path, branch, revision, version):
self.path = path
self.branch = branch
self.revision = revision
self.version = version
self.newRevision = None
self.newVersion = None
def getDataForWorktrees():
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
blocks = ret.split("\n\n")
worktrees = []
for block in blocks:
matches = re.search(WORKTREE_REGEX, block)
if matches:
path = matches.group(1)
revision = matches.group(2)
branch = matches.group(3)
version = getVersion(revision)
worktrees.append(Worktree(path, branch, revision, version))
return worktrees
def findWorktree(path):
for worktree in worktrees:
if worktree.path == path:
return worktree
return None
def getVersion(branch):
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag():
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
def cleanWorktree(worktreePath):
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
def updateAppInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
def updateAppVersionInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
worktrees = getDataForWorktrees()
updateRepoFromRemote()
print("Repo is updated from origin")
print("----------------------------------------------")
print("----------------------------------------------")
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
if developmentWorktree.revision != developmentWorktree.newRevision:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
print("----------------------------------------------")
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
cleanWorktree(developmentWorktree.path)
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
updateAppInWorktree(developmentWorktree.path)
updateAppVersionInWorktree(developmentWorktree.path)
print("RVR is updated in " + developmentWorktree.path)
elif developmentWorktree.version != developmentWorktree.newVersion:
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
updateAppVersionInWorktree(developmentWorktree.path)
print("RVR version is updated in " + developmentWorktree.path)
else:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
productionWorktree.newVersion = getLatestReleaseTag()
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
print("PRODUCTION (" + productionWorktree.path + ")")
print(productionWorktree.revision + " = " + productionWorktree.version)
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
if productionWorktree.revision != productionWorktree.newRevision:
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
cleanWorktree(productionWorktree.path)
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
updateAppInWorktree(productionWorktree.path)
updateAppVersionInWorktree(productionWorktree.path)
print("RVR is updated in " + productionWorktree.path)
else:
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -f ${ROOT_DIR}/installed ]; then
echo "RVR is already installed! To force reinstall, delete file 'installed' from the root directory!"
exit 1
fi
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing RVR DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/rvr.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./rvr db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./rvr view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./rvr user:add rvr@rvr.dev 123456 admin)
fi
touch ${ROOT_DIR}/installed
-11
View File
@@ -1,11 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
cd ${ROOT_DIR}
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" app.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" app.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" app.php
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
(cd ${ROOT_DIR} && composer install --no-dev)
else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./rvr db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./rvr view:link)
fi
+20 -11
View File
@@ -19,6 +19,8 @@ class MigrateDatabaseCommand extends Command
{
$db = \Container::$dbConnection;
$this->createBaseDb();
$db->startTransaction();
$success = [];
@@ -62,10 +64,8 @@ class MigrateDatabaseCommand extends Command
return 0;
}
private function readDir(string $type): array
private function createBaseDb()
{
$done = [];
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
@@ -73,16 +73,25 @@ class MigrateDatabaseCommand extends Command
->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) {
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
return;
}
$result = $select->execute();
\Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/rvr.sql'));
}
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
private function readDir(string $type): array
{
$done = [];
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
$result = $select->execute();
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
$path = ROOT . '/database/migrations/' . $type;
+53 -23
View File
@@ -11,6 +11,7 @@ use RVR\Repository\CommunityRepository;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CurrencyExchangeRateRepository;
use RVR\Repository\CurrencyRepository;
use RVR\Repository\EventRepository;
use RVR\Repository\TransactionRepository;
use RVR\Repository\UserRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
@@ -32,6 +33,8 @@ class CommunityController implements IAuthenticationRequired
private TransactionRepository $transactionRepository;
private EventRepository $eventRepository;
public function __construct()
{
$this->userRepository = new UserRepository();
@@ -40,6 +43,7 @@ class CommunityController implements IAuthenticationRequired
$this->currencyRepository = new CurrencyRepository();
$this->currencyExchangeRatesRepository = new CurrencyExchangeRateRepository();
$this->transactionRepository = new TransactionRepository();
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
@@ -55,32 +59,21 @@ class CommunityController implements IAuthenticationRequired
\Container::$persistentDataManager->loadRelationsFromDb($community, false, ['main_currency']);
$balanceCalculator = new BalanceCalculator($community);
$debts = $balanceCalculator->calculate();
$debtItems = [];
$debtBalance = 0.0;
$outstandingItems = [];
$outstandingBalance = 0.0;
foreach ($debts as $debt) {
if ($debt['payer']->getId() === \Container::$request->user()->getUniqueId()) {
$debtBalance += $debt['amount'];
$debtItems[] = $debt;
}
if ($debt['payee']->getId() === \Container::$request->user()->getUniqueId()) {
$outstandingBalance += $debt['amount'];
$outstandingItems[] = $debt;
}
}
$balance = $outstandingBalance - $debtBalance;
/**
* @var User $user
*/
$user = \Container::$request->user();
$balanceCalculator = new BalanceCalculator($community, $user);
$balance = $balanceCalculator->calculate();
return new HtmlContent('communities/community', [
'community' => $community,
'upcomingEvents' => [],
'debtItems' => $debtItems,
'debtBalance' => $debtBalance,
'outstandingItems' => $outstandingItems,
'outstandingBalance' => $outstandingBalance,
'balance' => $balance,
'upcomingAndRecentEvents' => iterator_to_array($this->eventRepository->getUpcomingAndRecentByCommunity($community, new DateTime(), 30, 3)),
'debtItems' => $balance['debtItems'],
'debtBalance' => $balance['debtBalance'],
'outstandingItems' => $balance['outstandingItems'],
'outstandingBalance' => $balance['outstandingBalance'],
'balance' => $balance['absoluteBalance'],
'editPermission' => $ownCommunityMember->getOwner()
]);
}
@@ -172,6 +165,43 @@ class CommunityController implements IAuthenticationRequired
]);
}
public function deleteCommunity(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
return null;
}
if ($this->transactionRepository->countAllByCommunity($community) > 0) {
return new JsonContent([
'error' => ['errorText' => 'There are transactions for this community!']
]);
}
if ($this->eventRepository->countAllByCommunity($community) > 0) {
return new JsonContent([
'error' => ['errorText' => 'There are events for this community!']
]);
}
foreach ($this->communityMemberRepository->getAllByCommunity($community) as $communityMember) {
\Container::$persistentDataManager->deleteFromDb($communityMember);
}
$community->setMainCurrencyId(null);
\Container::$persistentDataManager->saveToDb($community);
foreach ($this->currencyRepository->getAllByCommunity($community) as $currency) {
foreach ($this->currencyExchangeRatesRepository->getAllByCurrency($currency) as $currencyExchangeRate) {
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
}
\Container::$persistentDataManager->deleteFromDb($currency);
}
\Container::$persistentDataManager->deleteFromDb($community);
return new JsonContent(['success' => true]);
}
public function getMembersEdit(): ?IContent
{
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
+193
View File
@@ -0,0 +1,193 @@
<?php namespace RVR\Controller;
use Container;
use DateTime;
use RVR\Finance\BalanceCalculator;
use RVR\Finance\ExchangeRateCalculator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CommunityRepository;
use RVR\Repository\EventRepository;
use RVR\Repository\TransactionRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
class EventController implements IAuthenticationRequired, ISecured
{
private CommunityRepository $communityRepository;
private CommunityMemberRepository $communityMemberRepository;
private EventRepository $eventRepository;
private TransactionRepository $transactionRepository;
private ?Community $community;
private ?CommunityMember $ownCommunityMember;
public function __construct()
{
$this->communityRepository = new CommunityRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
$this->eventRepository = new EventRepository();
$this->transactionRepository = new TransactionRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function authorize(): bool
{
$communitySlug = \Container::$request->query('communitySlug');
$this->community = $this->communityRepository->getBySlug($communitySlug);
if ($this->community === null) {
return false;
}
/**
* @var User $user
*/
$user = \Container::$request->user();
$this->ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($this->community, $user);
if ($this->ownCommunityMember === null) {
return false;
}
return true;
}
public function getEvents(): IContent
{
$itemsPerPage = 10;
$numberOfEvents = $this->eventRepository->countAllByCommunity($this->community);
$currentPage = Container::$request->query('page') ?: 1;
$events = $this->eventRepository->getPagedByCommunity(
$this->community,
$currentPage,
$itemsPerPage
);
return new HtmlContent('events/events', [
'community' => $this->community,
'pages' => ceil($numberOfEvents / $itemsPerPage),
'currentPage' => $currentPage,
'numberOfEvents' => $numberOfEvents,
'events' => $events
]);
}
public function searchEvent(): IContent
{
$events = iterator_to_array($this->eventRepository->searchByTitle($this->community, Container::$request->query('q')));
$results = [];
foreach ($events as $event) {
$results[] = ['value' => $event->getId(), 'label' => $event->getTitle()];
}
return new JsonContent([
'results' => $results
]);
}
public function getEvent(): ?IContent
{
$event = $this->eventRepository->getBySlug(Container::$request->query('eventSlug'));
if (!$event) {
return null;
}
Container::$persistentDataManager->loadRelationsFromDb($this->community, true, ['main_currency']);
/**
* @var User $user
*/
$user = \Container::$request->user();
$balanceCalculator = new BalanceCalculator($this->community, $user, $event);
$balance = $balanceCalculator->calculate();
return new HtmlContent('events/event', [
'community' => $this->community,
'event' => $event,
'totalCost' => $this->sumTransactions($event),
'debtItems' => $balance['debtItems'],
'debtBalance' => $balance['debtBalance'],
'outstandingItems' => $balance['outstandingItems'],
'outstandingBalance' => $balance['outstandingBalance'],
'balance' => $balance['absoluteBalance'],
]);
}
public function getEventEdit(): ?IContent
{
$eventSlug = Container::$request->query('eventSlug');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
if ($event === null) {
return null;
}
} else {
$event = null;
}
return new HtmlContent('events/event_edit', [
'community' => $this->community,
'event' => $event
]);
}
public function saveEvent(): ?IContent
{
$eventSlug = Container::$request->query('eventSlug');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
} else {
$event = new Event();
$event->setCommunity($this->community);
}
$event->setTitle(Container::$request->post('title'));
$event->setDescription(Container::$request->post('description'));
$event->setStartDate(new DateTime(Container::$request->post('start')));
$event->setEndDate(new DateTime(Container::$request->post('end')));
Container::$persistentDataManager->saveToDb($event);
return new JsonContent([
'redirect' => ['target' => \Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $this->community->getSlug(), 'eventSlug' => $event->getSlug()])]
]);
}
public function deleteEvent(): IContent
{
$event = $this->eventRepository->getBySlug(Container::$request->query('eventSlug'));
foreach ($this->transactionRepository->getAllByEvent($event) as $transaction) {
$transaction->setEventId(null);
Container::$persistentDataManager->saveToDb($transaction);
}
Container::$persistentDataManager->deleteFromDb($event);
return new JsonContent(['success' => true]);
}
private function sumTransactions(Event $event): float
{
$exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
$transactions = $this->transactionRepository->getAllByEvent($event, true, ['currency']);
$sum = 0.0;
foreach ($transactions as $transaction) {
$sum += $exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
}
return $sum;
}
}
@@ -0,0 +1,70 @@
<?php namespace RVR\Controller;
use DateTime;
use Container;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\Redirect;
class EventRedirectController implements IAuthenticationRequired
{
private EventRepository $eventRepository;
public function __construct()
{
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function getEvent()
{
$currentEvent = $this->getCurrentEvent();
if ($currentEvent === null) {
return new HtmlContent('event_redirect/no_event');
}
return new Redirect(
\Container::$routeCollection->getRoute('community.event')
->generateLink([
'communitySlug' => $currentEvent->getCommunity()->getSlug(),
'eventSlug' => $currentEvent->getSlug()
]),
IRedirect::TEMPORARY
);
}
public function getEventNewTransaction()
{
$currentEvent = $this->getCurrentEvent();
if ($currentEvent === null) {
return new HtmlContent('event_redirect/no_event');
}
return new Redirect(
\Container::$routeCollection->getRoute('community.transactions.new')
->generateLink([
'communitySlug' => $currentEvent->getCommunity()->getSlug(),
'event' => $currentEvent->getSlug()
]),
IRedirect::TEMPORARY
);
}
private function getCurrentEvent(): ?Event
{
/**
* @var User $user
*/
$user = Container::$request->user();
return $this->eventRepository->getCurrentByUser($user, new DateTime(), 30, true, ['community']);
}
}
+6 -1
View File
@@ -1,8 +1,10 @@
<?php namespace RVR\Controller;
use DateTime;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
@@ -11,9 +13,12 @@ class HomeController implements IAuthenticationRequired
{
private CommunityMemberRepository $communityMemberRepository;
private EventRepository $eventRepository;
public function __construct()
{
$this->communityMemberRepository = new CommunityMemberRepository();
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
@@ -39,7 +44,7 @@ class HomeController implements IAuthenticationRequired
return new HtmlContent('home', [
'communities' => $communities,
'upcomingEvents' => []
'upcomingAndRecentEvents' => iterator_to_array($this->eventRepository->getUpcomingAndRecentByUser($user, new DateTime(), 30, 3, true, ['community']))
]);
}
}
+1 -1
View File
@@ -196,7 +196,7 @@ class LoginController
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address / username.'
'errorText' => 'No user found with the given email address.'
]
]);
}
+94 -18
View File
@@ -6,11 +6,14 @@ use RVR\Finance\ExchangeRateCalculator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\TransactionPayee;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CommunityRepository;
use RVR\Repository\CurrencyRepository;
use RVR\Repository\TransactionRepository;
use RVR\Repository\TransactionPayeeRepository;
use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Response\IContent;
@@ -27,9 +30,13 @@ class TransactionController implements IAuthenticationRequired, ISecured
private TransactionRepository $transactionRepository;
private Community $community;
private TransactionPayeeRepository $transactionPayeeRepository;
private CommunityMember $ownCommunityMember;
private EventRepository $eventRepository;
private ?Community $community;
private ?CommunityMember $ownCommunityMember;
public function __construct()
{
@@ -37,6 +44,8 @@ class TransactionController implements IAuthenticationRequired, ISecured
$this->communityMemberRepository = new CommunityMemberRepository();
$this->currencyRepository = new CurrencyRepository();
$this->transactionRepository = new TransactionRepository();
$this->transactionPayeeRepository = new TransactionPayeeRepository();
$this->eventRepository = new EventRepository();
}
public function isAuthenticationRequired(): bool
@@ -69,25 +78,46 @@ class TransactionController implements IAuthenticationRequired, ISecured
Container::$persistentDataManager->loadRelationsFromDb($this->community, true, ['main_currency']);
$exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
$eventSlug = Container::$request->query('event');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
} else {
$event = null;
}
$itemsPerPage = 50;
$numberOfTransactions = $this->transactionRepository->countAllByCommunity($this->community);
$pages = ceil($numberOfTransactions / $itemsPerPage);
$currentPage = Container::$request->query('page') ?: 0;
$transactions = $this->transactionRepository->getPagedByCommunity(
$this->community,
$currentPage * $itemsPerPage,
$itemsPerPage,
true,
['currency', 'payer_user', 'payee_user']
);
$numberOfTransactions = $event ?
$this->transactionRepository->countAllByEvent($event) :
$this->transactionRepository->countAllByCommunity($this->community);
$currentPage = Container::$request->query('page') ?: 1;
$transactions = $event ?
$this->transactionRepository->getPagedByEvent(
$event,
$currentPage,
$itemsPerPage,
true,
['currency', 'payer_user']
) :
$this->transactionRepository->getPagedByCommunity(
$this->community,
$currentPage,
$itemsPerPage,
true,
['event', 'currency', 'payer_user']
);
$transactions = iterator_to_array($transactions);
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees', true, ['user']);
return new HtmlContent('communities/transactions', [
'community' => $this->community,
'event' => $event,
'exchangeRateCalculator' => $exchangeRateCalculator,
'pages' => $pages,
'pages' => ceil($numberOfTransactions / $itemsPerPage),
'currentPage' => $currentPage,
'numberOfTransactions' => $numberOfTransactions,
'transactions' => $transactions
'transactions' => $transactions,
'members' => $this->getMembers($this->community)
]);
}
@@ -99,13 +129,28 @@ class TransactionController implements IAuthenticationRequired, ISecured
if ($transaction === null) {
return null;
}
Container::$persistentDataManager->loadRelationsFromDb($transaction, false, ['event']);
$event = $transaction->getEvent();
$payeeUserIds = [];
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
$payeeUserIds[] = $payee->getUserId();
}
} else {
$transaction = null;
$eventSlug = Container::$request->query('event');
if ($eventSlug) {
$event = $this->eventRepository->getBySlug($eventSlug);
} else {
$event = null;
}
$payeeUserIds = [];
}
return new HtmlContent('communities/transaction_edit', [
'community' => $this->community,
'transaction' => $transaction,
'payeeUserIds' => $payeeUserIds,
'event' => $event,
'members' => $this->getMembers($this->community),
'currencies' => $this->getCurrencies($this->community)
]);
@@ -121,20 +166,51 @@ class TransactionController implements IAuthenticationRequired, ISecured
$transaction->setCommunity($this->community);
}
$transaction->setEventId(Container::$request->post('event_id') ?: null);
$transaction->setCurrencyId(Container::$request->post('currency_id'));
$transaction->setPayerUserId(Container::$request->post('payer_user_id'));
$transaction->setPayeeUserId(Container::$request->post('payee_user_id') ?: null);
$transaction->setDescription(Container::$request->post('description'));
$transaction->setSum(Container::$request->post('sum'));
$transaction->setTimeDate(new DateTime(Container::$request->post('time')));
Container::$persistentDataManager->saveToDb($transaction);
$payeeUserIds = array_unique(Container::$request->post('payee_user_ids'));
if (count($payeeUserIds) === $this->communityMemberRepository->countAllByCommunity($this->community)) {
$payeeUserIds = [];
}
$currentPayees = [];
foreach ($payeeUserIds as $payeeUserId) {
$payee = new TransactionPayee();
$payee->setTransaction($transaction);
$payee->setUserId((int)$payeeUserId);
$currentPayees[(int)$payeeUserId] = $payee;
}
$existingPayees = [];
if ($transactionId) {
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
$existingPayees[$payee->getUserId()] = $payee;
}
}
foreach (array_diff_key($currentPayees, $existingPayees) as $newPayee) {
Container::$persistentDataManager->saveToDb($newPayee);
}
foreach (array_diff_key($existingPayees, $currentPayees) as $deletedPayee) {
Container::$persistentDataManager->deleteFromDb($deletedPayee);
}
return new JsonContent(['success' => true]);
}
public function deleteTransaction(): IContent
{
$transaction = $this->transactionRepository->getById(Container::$request->query('transactionId'));
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
Container::$persistentDataManager->deleteFromDb($payee);
}
Container::$persistentDataManager->deleteFromDb($transaction);
return new JsonContent(['success' => true]);
@@ -143,7 +219,7 @@ class TransactionController implements IAuthenticationRequired, ISecured
private function getMembers(Community $community): array
{
$members = iterator_to_array($this->communityMemberRepository->getAllByCommunity($community, true, ['user']));
usort($members, function($a, $b) {
usort($members, function ($a, $b) {
return strnatcmp($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName());
});
return $members;
@@ -152,10 +228,10 @@ class TransactionController implements IAuthenticationRequired, ISecured
private function getCurrencies(Community $community): array
{
$currencies = iterator_to_array($this->currencyRepository->getAllByCommunity($community));
usort($currencies, function($a, $b) {
usort($currencies, function ($a, $b) {
return strnatcmp($a->getCode(), $b->getCode());
});
usort($currencies, function($a, $b) use ($community) {
usort($currencies, function ($a, $b) use ($community) {
return (int)($b->getId() === $community->getMainCurrencyId()) - (int)($a->getId() === $community->getMainCurrencyId());
});
return $currencies;
+55 -9
View File
@@ -1,6 +1,9 @@
<?php namespace RVR\Finance;
use Container;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\TransactionRepository;
@@ -8,6 +11,10 @@ class BalanceCalculator
{
private Community $community;
private User $user;
private ?Event $event;
private TransactionRepository $transactionRepository;
private CommunityMemberRepository $communityMemberRepository;
@@ -18,9 +25,13 @@ class BalanceCalculator
private array $payments;
public function __construct(Community $community)
private array $actualDebts;
public function __construct(Community $community, User $user, ?Event $event = null)
{
$this->community = $community;
$this->user = $user;
$this->event = $event;
$this->transactionRepository = new TransactionRepository();
$this->communityMemberRepository = new CommunityMemberRepository();
$this->exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
@@ -31,7 +42,8 @@ class BalanceCalculator
$this->collectMembers();
$this->createPaymentsMatrix();
$this->sumTransactions();
return $this->calculateActualDebts();
$this->calculateActualDebts();
return $this->calculateBalanceForUser();
}
private function collectMembers(): void
@@ -56,13 +68,22 @@ class BalanceCalculator
private function sumTransactions(): void
{
$membersCount = count($this->members);
$transactions = $this->transactionRepository->getAllByCommunity($this->community, true, ['currency']);
if ($this->event !== null) {
$transactions = iterator_to_array($this->transactionRepository->getAllByEvent($this->event, true, ['currency']));
} else {
$transactions = iterator_to_array($this->transactionRepository->getAllByCommunity($this->community, true, ['currency']));
}
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees');
foreach ($transactions as $transaction) {
$sum = $this->exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
$payees = $transaction->getPayees();
$payeeCount = count($payees);
if ($transaction->getPayeeUserId()) {
$this->payments[$transaction->getPayerUserId()][$transaction->getPayeeUserId()] += $sum;
if ($payeeCount > 0) {
foreach ($payees as $payee) {
$this->payments[$transaction->getPayerUserId()][$payee->getUserId()] += $sum / $payeeCount;
}
} else {
foreach ($this->members as $payeeUserId => $member) {
$this->payments[$transaction->getPayerUserId()][$payeeUserId] += $sum / $membersCount;
@@ -71,20 +92,45 @@ class BalanceCalculator
}
}
private function calculateActualDebts(): array
private function calculateActualDebts(): void
{
$actualDebts = [];
$this->actualDebts = [];
foreach ($this->payments as $payerUserId => $paymentsOfPayer) {
foreach ($paymentsOfPayer as $payeeUserId => $sum) {
$actualDebt = $this->payments[$payeeUserId][$payerUserId] - $sum;
if (round($actualDebt, $this->community->getMainCurrency()->getRoundDigits()) > 0.0) {
$actualDebts[] = ['payer' => $this->members[$payerUserId], 'payee' => $this->members[$payeeUserId], 'amount' => $actualDebt];
$this->actualDebts[] = ['payer' => $this->members[$payerUserId], 'payee' => $this->members[$payeeUserId], 'amount' => $actualDebt];
}
}
}
}
return $actualDebts;
private function calculateBalanceForUser(): array
{
$debtItems = [];
$debtBalance = 0.0;
$outstandingItems = [];
$outstandingBalance = 0.0;
foreach ($this->actualDebts as $debt) {
if ($debt['payer']->getId() === $this->user->getId()) {
$debtBalance += $debt['amount'];
$debtItems[] = $debt;
}
if ($debt['payee']->getId() === $this->user->getId()) {
$outstandingBalance += $debt['amount'];
$outstandingItems[] = $debt;
}
}
$absoluteBalance = $outstandingBalance - $debtBalance;
return [
'absoluteBalance' => $absoluteBalance,
'debtItems' => $debtItems,
'debtBalance' => $debtBalance,
'outstandingItems' => $outstandingItems,
'outstandingBalance' => $outstandingBalance
];
}
}
+2 -2
View File
@@ -33,12 +33,12 @@ class Community extends ModelWithSlug
$this->currency = $currency;
}
public function setMainCurrency(Currency $mainCurrency): void
public function setMainCurrency(?Currency $mainCurrency): void
{
$this->mainCurrency = $mainCurrency;
}
public function setMainCurrencyId(int $mainCurrencyId): void
public function setMainCurrencyId(?int $mainCurrencyId): void
{
$this->mainCurrencyId = $mainCurrencyId;
}
+107
View File
@@ -0,0 +1,107 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\ModelWithSlug;
class Event extends ModelWithSlug
{
protected static string $table = 'events';
protected static array $fields = ['community_id', 'start', 'end', 'title', 'description'];
protected static array $relations = ['community' => Community::class];
protected static string $slugSource = 'title';
private ?Community $community = null;
private int $communityId;
private DateTime $start;
private DateTime $end;
private string $title = '';
private string $description = '';
public function setCommunity(Community $community): void
{
$this->community = $community;
}
public function setCommunityId(int $communityId): void
{
$this->communityId = $communityId;
}
public function setStartDate(DateTime $start): void
{
$this->start = $start;
}
public function setStart(string $start): void
{
$this->start = new DateTime($start);
}
public function setEndDate(DateTime $end): void
{
$this->end = $end;
}
public function setEnd(string $end): void
{
$this->end = new DateTime($end);
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getCommunity(): ?Community
{
return $this->community;
}
public function getCommunityId(): int
{
return $this->communityId;
}
public function getStartDate(): DateTime
{
return $this->start;
}
public function getStart(): string
{
return $this->start->format('Y-m-d H:i:s');
}
public function getEndDate(): DateTime
{
return $this->end;
}
public function getEnd(): string
{
return $this->end->format('Y-m-d H:i:s');
}
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
}
+42 -1
View File
@@ -7,19 +7,28 @@ class Transaction extends Model
{
protected static string $table = 'transactions';
protected static array $fields = ['community_id', 'currency_id', 'payer_user_id', 'payee_user_id', 'description', 'sum', 'time'];
protected static array $fields = ['community_id', 'event_id', 'currency_id', 'payer_user_id', 'payee_user_id', 'description', 'sum', 'time'];
protected static array $relations = [
'community' => Community::class,
'event' => Event::class,
'currency' => Currency::class,
'payer_user' => User::class,
'payee_user' => User::class
];
protected static array $multiRelations = [
'payees' => [TransactionPayee::class, 'transaction']
];
private ?Community $community = null;
private int $communityId;
private ?Event $event = null;
private ?int $eventId = null;
private ?Currency $currency = null;
private int $currencyId;
@@ -32,6 +41,8 @@ class Transaction extends Model
private ?int $payeeUserId = null;
private ?array $payees = null;
private string $description = '';
private float $sum = 0.0;
@@ -48,6 +59,16 @@ class Transaction extends Model
$this->communityId = $communityId;
}
public function setEvent(?Event $event): void
{
$this->event = $event;
}
public function setEventId(?int $eventId): void
{
$this->eventId = $eventId;
}
public function setCurrency(Currency $currency): void
{
$this->currency = $currency;
@@ -78,6 +99,11 @@ class Transaction extends Model
$this->payeeUserId = $payeeUserId;
}
public function setPayees(array $payees): void
{
$this->payees = $payees;
}
public function setDescription(string $description): void
{
$this->description = $description;
@@ -108,6 +134,16 @@ class Transaction extends Model
return $this->communityId;
}
public function getEvent(): ?Event
{
return $this->event;
}
public function getEventId(): ?int
{
return $this->eventId;
}
public function getCurrency(): ?Currency
{
return $this->currency;
@@ -138,6 +174,11 @@ class Transaction extends Model
return $this->payeeUserId;
}
public function getPayees(): ?array
{
return $this->payees;
}
public function getDescription(): string
{
return $this->description;
@@ -0,0 +1,60 @@
<?php namespace RVR\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class TransactionPayee extends Model
{
protected static string $table = 'transaction_payees';
protected static array $fields = ['transaction_id', 'user_id'];
protected static array $relations = ['transaction' => Transaction::class, 'user' => User::class];
private ?Transaction $transaction = null;
private ?int $transactionId = null;
private ?User $user = null;
private ?int $userId = null;
public function setTransaction(Transaction $transaction): void
{
$this->transaction = $transaction;
}
public function setTransactionId(int $transactionId): void
{
$this->transactionId = $transactionId;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function getTransaction(): ?Transaction
{
return $this->transaction;
}
public function getTransactionId(): ?int
{
return $this->transactionId;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
}
+13 -2
View File
@@ -15,12 +15,16 @@ class CommunityMemberRepository
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
$select = $this->selectAllByCommunity($community);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
return $this->selectAllByCommunity($community)->count();
}
public function getAllByUser(User $user, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(\Container::$dbConnection);
@@ -37,4 +41,11 @@ class CommunityMemberRepository
return \Container::$persistentDataManager->selectFromDb($select, CommunityMember::class);
}
private function selectAllByCommunity(Community $community): Select
{
$select = new Select(\Container::$dbConnection, CommunityMember::getTable());
$select->where('community_id', '=', $community->getId());
return $select;
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php namespace RVR\Repository;
use Container;
use DateTime;
use DateInterval;
use Generator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\User;
use SokoWeb\Database\Query\Select;
class EventRepository
{
public function getById(int $id): ?Event
{
return Container::$persistentDataManager->selectFromDbById($id, Event::class);
}
public function getBySlug(string $slug): ?Event
{
return \Container::$persistentDataManager->selectFromDbBySlug($slug, Event::class);
}
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
return $select->count();
}
public function getUpcomingAndRecentByCommunity(Community $community, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC');
$select->limit($limit, 0);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function getUpcomingAndRecentByUser(User $user, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByUser($select, $user);
$this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC');
$select->limit($limit, 0);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function getCurrentByUser(User $user, DateTime $from, int $days, bool $useRelations = false, array $withRelations = []): ?Event
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByUser($select, $user);
$this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC');
$select->limit(1, 0);
return Container::$persistentDataManager->selectFromDb($select, Event::class, $useRelations, $withRelations);
}
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$select->orderBy('start', 'DESC');
$select->paginate($page, $itemsPerPage);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
}
public function searchByTitle(Community $community, string $title): Generator
{
$select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$select->where('title', 'LIKE', '%' . $title . '%');
$select->orderBy('start', 'DESC');
$select->limit(10);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class);
}
private function selectAllByCommunity(Select $select, Community $community): void
{
$select->where('community_id', '=', $community->getId());
}
private function selectAllByUser(Select $select, User $user): void
{
$select->innerJoin('communities', ['communities', 'id'], '=', ['events', 'community_id']);
$select->innerJoin('community_members', ['communities', 'id'], '=', ['community_members', 'community_id']);
$select->where(['community_members', 'user_id'], '=', $user->getId());
}
private function selectUpcomingAndRecent(Select $select, DateTime $from, int $days): void
{
$select->where(function (Select $select) use ($from, $days) {
$select->where(function (Select $select) use ($from, $days) {
$select->where('start', '<', (clone $from)->add(DateInterval::createFromDateString("$days days"))->format('Y-m-d H:i:s'));
$select->where('end', '>', $from->format('Y-m-d H:i:s'));
});
$select->orWhere(function (Select $select) use ($from, $days) {
$select->where('end', '>', (clone $from)->sub(DateInterval::createFromDateString("$days days"))->format('Y-m-d H:i:s'));
$select->where('start', '<', $from->format('Y-m-d H:i:s'));
});
});
}
}
@@ -0,0 +1,23 @@
<?php namespace RVR\Repository;
use Container;
use Generator;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\TransactionPayee;
use SokoWeb\Database\Query\Select;
class TransactionPayeeRepository
{
public function getById(int $id): ?TransactionPayee
{
return Container::$persistentDataManager->selectFromDbById($id, TransactionPayee::class);
}
public function getAllByTransaction(Transaction $transaction, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection);
$select->where('transaction_id', '=', $transaction->getId());
yield from Container::$persistentDataManager->selectMultipleFromDb($select, TransactionPayee::class, $useRelations, $withRelations);
}
}
+32 -4
View File
@@ -4,6 +4,7 @@ use Container;
use Generator;
use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\Currency;
use RVR\PersistentData\Model\Event;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\User;
use SokoWeb\Database\Query\Select;
@@ -22,11 +23,23 @@ class TransactionRepository
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
public function getAllByEvent(Event $event, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByEvent($event);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
public function countAllByCommunity(Community $community): int
{
return $this->selectAllByCommunity($community)->count();
}
public function countAllByEvent(Event $event): int
{
return $this->selectAllByEvent($event)->count();
}
public function isAnyForUser(User $user): bool
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
@@ -53,12 +66,20 @@ class TransactionRepository
return $select->count() > 0;
}
public function getPagedByCommunity(Community $community, int $start, int $limit, bool $useRelations = false, array $withRelations = []): Generator
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection);
$select->where('community_id', '=', $community->getId());
$select = $this->selectAllByCommunity($community);
$select->orderBy('time', 'DESC');
$select->limit($limit, $start);
$select->paginate($page, $itemsPerPage);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
public function getPagedByEvent(Event $event, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->selectAllByEvent($event);
$select->orderBy('time', 'DESC');
$select->paginate($page, $itemsPerPage);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
}
@@ -69,4 +90,11 @@ class TransactionRepository
$select->where('community_id', '=', $community->getId());
return $select;
}
private function selectAllByEvent(Event $event)
{
$select = new Select(Container::$dbConnection, Transaction::getTable());
$select->where('event_id', '=', $event->getId());
return $select;
}
}
+10 -6
View File
@@ -12,14 +12,18 @@
<div class="gridContainer marginTop">
<div>
<h3 class="marginBottom">Upcoming events</h3>
<?php if (count($upcomingEvents) > 0): ?>
<?php foreach ($upcomingEvents as $event): ?>
<!-- todo -->
<h3 class="marginBottom">Upcoming and recent events</h3>
<?php if (count($upcomingAndRecentEvents) > 0): ?>
<?php foreach ($upcomingAndRecentEvents as $event): ?>
<p>
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a>
<span class="small"><?= $event->getStartDate()->format('Y-m-d') ?> – <?= $event->getEndDate()->format('Y-m-d') ?></span>
</p>
<?php endforeach; ?>
<?php else: ?>
<p>There is no upcoming event.</p>
<p>There is no event to show.</p>
<?php endif; ?>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">All events</a> | <a href="<?= Container::$routeCollection->getRoute('community.events.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New event</a></p>
</div>
<div>
<?php
@@ -27,7 +31,7 @@
$mainCurrencyRoundDigits = $community->getMainCurrency()->getRoundDigits();
?>
<h3 class="marginBottom">Finances</h3>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Transactions</a>
<p><a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Transactions</a> | <a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New transaction</a></p>
<table class="fullWidth marginTop">
<tr>
<td class="bold">You owe</td>
+7 -1
View File
@@ -25,9 +25,15 @@
<input type="number" class="text big fullWidth" name="main_currency_round_digits" min="0" max="9" required>
<?php endif; ?>
<p id="communityFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<div class="right marginTop" style="font-size: 0;">
<button type="submit" name="submit_button"><?= isset($community) ? '<i class="fa-regular fa-floppy-disk"></i> Save' : '<i class="fa-regular fa-plus"></i> Create' ?></button>
<?php if (isset($community)): ?>
<button type="submit" form="deleteCommunity" name="submit_button" data-confirmation="Are you sure you want to delete this community?" class="red marginLeft"><i class="fa-regular fa-trash-can"></i> Delete</button>
<?php endif; ?>
</div>
</form>
<?php if (isset($community)): ?>
<form id="deleteCommunity" action="<?= Container::$routeCollection->getRoute('community.delete-action')->generateLink(['communitySlug' => $community->getSlug()]) ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>"></form>
<?php endif; ?>
</div>
@endsection
+3 -3
View File
@@ -1,5 +1,5 @@
@css(/static/node_modules/tom-select/dist/css/tom-select.min.css)
@js(/static/node_modules/tom-select/dist/js/tom-select.base.min.js)
@css(node_modules/tom-select/dist/css/tom-select.min.css)
@js(node_modules/tom-select/dist/js/tom-select.base.min.js)
@js(js/communities/community_members.js)
@extends(templates/layout_normal)
@@ -41,7 +41,7 @@
<tr>
<td>
<form id="newMember" action="<?= Container::$routeCollection->getRoute('community.members.new-action')->generateLink(['communitySlug' => $community->getSlug()]) ?>" method="post" data-reload-on-success="true" data-observe-inputs="user_id"></form>
<select type="text" form="newMember" name="user_id">
<select form="newMember" name="user_id" required></select>
</td>
<td style="text-align: center;">
<input type="checkbox" form="newMember" name="owner" />
+37 -9
View File
@@ -1,9 +1,17 @@
@css(node_modules/tom-select/dist/css/tom-select.min.css)
@js(node_modules/tom-select/dist/js/tom-select.base.min.js)
@js(js/communities/transaction.js)
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Transactions</a> »
<?php if (isset($event)): ?>
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a> »
<?php endif; ?>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">Transactions</a> »
<?php if (isset($transaction)): ?>
Edit transaction
<?php else: ?>
@@ -16,21 +24,35 @@
Container::$routeCollection->getRoute('community.transactions.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'transactionId' => $transaction->getId()]) :
Container::$routeCollection->getRoute('community.transactions.new-action')->generateLink(['communitySlug' => $community->getSlug()]);
?>
<form id="transactionForm" action="<?= $formAction ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug()]) ?>">
<p class="formLabel">Payer</p>
<form id="transactionForm" action="<?= $formAction ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">
<p class="formLabel">Event</p>
<select name="event_id">
<option value="">[none]</option>
<?php if (isset($event)): ?>
<option value="<?= $event->getId() ?>" selected><?= $event->getTitle() ?></option>
<?php endif; ?>
</select>
<p class="formLabel marginTop">Payer</p>
<select class="big fullWidth" name="payer_user_id" required>
<option value="" hidden></option>
<?php foreach ($members as $member): ?>
<option value="<?= $member->getUser()->getId() ?>" <?= isset($transaction) && $transaction->getPayerUserId() === $member->getUser()->getId() ? 'selected' : '' ?>><?= $member->getUser()->getDisplayName() ?></option>
<option value="<?= $member->getUser()->getId() ?>"
<?= isset($transaction) ?
($transaction->getPayerUserId() === $member->getUser()->getId() ? 'selected' : '') :
(Container::$request->user()->getUniqueId() == $member->getUser()->getId() ? 'selected' : '') ?>>
<?= $member->getUser()->getDisplayName() ?>
</option>
<?php endforeach; ?>
</select>
<p class="formLabel marginTop">Payee</p>
<select class="big fullWidth" name="payee_user_id">
<option value="">[common]</option>
<p class="formLabel marginTop">Payee(s)</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-gap: 10px;">
<?php foreach ($members as $member): ?>
<option value="<?= $member->getUser()->getId() ?>" <?= isset($transaction) && $transaction->getPayeeUserId() === $member->getUser()->getId() ? 'selected' : '' ?>><?= $member->getUser()->getDisplayName() ?></option>
<div style="text-align: center;">
<input id="payee_<?= $member->getUserId() ?>" type="checkbox" name="payee_user_ids[]" value="<?= $member->getUserId() ?>" <?= !isset($transaction) || count($payeeUserIds) === 0 || in_array($member->getUserId(), $payeeUserIds) ? 'checked' : '' ?>><!--
--><label for="payee_<?= $member->getUserId() ?>"><?= $member->getUser()->getDisplayName() ?></label>
</div>
<?php endforeach; ?>
</select>
</div>
<p class="formLabel marginTop">Description</p>
<input type="text" class="text big fullWidth" name="description" value="<?= isset($transaction) ? $transaction->getDescription() : '' ?>" required>
<p class="formLabel marginTop">Sum</p>
@@ -57,3 +79,9 @@
<?php endif; ?>
</div>
@endsection
@section(pageScript)
<script>
var searchEventUrl = '<?= Container::$routeCollection->getRoute('community.events.search')->generateLink(['communitySlug' => $community->getSlug(), 'q' => 'QUERY']) ?>';
</script>
@endsection
+39 -29
View File
@@ -3,34 +3,56 @@
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<?php if (isset($event)): ?>
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a> »
<?php endif; ?>
Transactions
</h2>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New transaction</a></p>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">New transaction</a></p>
<?php if ($numberOfTransactions > 0): ?>
<?php
$paginationRouteId = 'community.transactions';
$paginationRouteParams = ['communitySlug' => $community->getSlug()];
?>
<?php if ($pages > 1): ?>
<p class="paginateContainer marginTop">
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => 0]) ?>">«</a>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => max(0, $currentPage - 1)]) ?>">‹</a>
<?php for ($i = 0; $i < $pages; $i++): ?>
<?php if ($currentPage == $i): ?>
<span class="selected"><?= $i + 1 ?></span>
<?php else: ?>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => $i]) ?>"><?= $i + 1 ?></a>
<?php endif; ?>
<?php endfor; ?>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => min($pages - 1, $currentPage + 1)]) ?>">›</a>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => $pages - 1]) ?>">»</a>
</p>
@include(templates/pagination)
<?php endif; ?>
<?php foreach ($transactions as $transaction): ?>
<a class="block" href="<?= Container::$routeCollection->getRoute('community.transactions.edit')->generateLink(['communitySlug' => $community->getSlug(), 'transactionId' => $transaction->getId()]) ?>">
<div class="box transaction">
<div>
<?php if ($transaction->getEvent()): ?>
<p><span class="label"><?= $transaction->getEvent()->getTitle() ?></span></p>
<?php endif; ?>
<p style="font-weight: bold;"><?= $transaction->getDescription() ?></p>
<p class="small"><?= $transaction->getPayerUser()->getDisplayName() ?> <i class="fa-solid fa-caret-right"></i> <?= $transaction->getPayeeUser() ? $transaction->getPayeeUser()->getDisplayName() : '[common]' ?></p>
<p class="small">
<?= $transaction->getPayerUser()->getDisplayName() ?>
<i class="fa-solid fa-caret-right"></i>
<?php foreach ($members as $member): ?>
<?php
if (count($transaction->getPayees()) > 0) {
$found = false;
foreach ($transaction->getPayees() as $payee) {
if ($member->getUserId() === $payee->getUserId()) {
$found = true;
}
}
} else {
$found = true;
}
?>
<?php if ($found): ?>
<?= $member->getUser()->getDisplayName() ?>
<?php else: ?>
<span class="gray" style="text-decoration: line-through;"><?= $member->getUser()->getDisplayName() ?></span>
<?php endif; ?>
<?php endforeach; ?>
</p>
<p class="small"><?= $transaction->getTimeDate()->format('Y-m-d H:i') ?></p>
</div>
<div style="text-align: right;">
@@ -44,22 +66,10 @@
<?php endforeach; ?>
<?php if ($pages > 1): ?>
<p class="paginateContainer marginTop">
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => 0]) ?>">«</a>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => max(0, $currentPage - 1)]) ?>">‹</a>
<?php for ($i = 0; $i < $pages; $i++): ?>
<?php if ($currentPage == $i): ?>
<span class="selected"><?= $i + 1 ?></span>
<?php else: ?>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => $i]) ?>"><?= $i + 1 ?></a>
<?php endif; ?>
<?php endfor; ?>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => min($pages - 1, $currentPage + 1)]) ?>">›</a>
<a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'page' => $pages - 1]) ?>">»</a>
</p>
@include(templates/pagination)
<?php endif; ?>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New transaction</a></p>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug(), 'event' => isset($event) ? $event->getSlug() : null]) ?>">New transaction</a></p>
<?php else: ?>
<div class="box">
<p>There are no transactions yet.</p>
+8
View File
@@ -0,0 +1,8 @@
@extends(templates/layout_normal)
@section(main)
<h2>No event found</h2>
<div class="box compactBox">
<p class="error justify">No upcoming or recent event was found. <a href="<?= Container::$routeCollection->getRoute('home')->generateLink() ?>" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p>
</div>
@endsection
+54
View File
@@ -0,0 +1,54 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<?= $event->getTitle() ?>
<span class="small">[<a href="<?= Container::$routeCollection->getRoute('community.event.edit')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>">edit</a>]</span>
</h2>
<div class="gridContainer marginTop">
<div>
<?php
$mainCurrencyCode = $community->getMainCurrency()->getCode();
$mainCurrencyRoundDigits = $community->getMainCurrency()->getRoundDigits();
?>
<h3 class="marginBottom">Finances</h3>
<p><a href="<?= Container::$routeCollection->getRoute('community.transactions')->generateLink(['communitySlug' => $community->getSlug(), 'event' => $event->getSlug()]) ?>">Transactions</a> | <a href="<?= Container::$routeCollection->getRoute('community.transactions.new')->generateLink(['communitySlug' => $community->getSlug(), 'event' => $event->getSlug()]) ?>">New transaction</a></p>
<table class="fullWidth marginTop">
<tr>
<td class="bold">Total cost</td>
<td class="mono" style="text-align: right;"><?= number_format($totalCost, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
</table>
<table class="fullWidth marginTop">
<tr>
<td class="bold">You owe*</td>
<td class="mono red" style="text-align: right;"><?= number_format($debtBalance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php foreach ($debtItems as $item): ?>
<tr>
<td class="small"><?= $item['payee']->getUser()->getDisplayName() ?></td>
<td class="small mono red" style="text-align: right;"><?= number_format($item['amount'], $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td class="bold">You're owed*</td>
<td class="mono green" style="text-align: right;"><?= number_format($outstandingBalance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php foreach ($outstandingItems as $item): ?>
<tr>
<td class="small"><?= $item['payer']->getUser()->getDisplayName() ?></td>
<td class="small mono green" style="text-align: right;"><?= number_format($item['amount'], $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td class="bold">Your balance*</td>
<td class="mono <?= $balance < 0 ? 'red' : ($balance > 0 ? 'green' : '') ?>" style="text-align: right;;"><?= number_format($balance, $mainCurrencyRoundDigits) ?> <?= $mainCurrencyCode ?></td>
</tr>
</table>
<p class="small right">* Virtual balance only for this event. Check <a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>">community</a> finances for your real balance.</p>
</div>
</div>
@endsection
+40
View File
@@ -0,0 +1,40 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
<a href="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>">Events</a> »
<?php if (isset($event)): ?>
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a> » Edit event
<?php else: ?>
New event
<?php endif; ?>
</h2>
<div class="box compactBox">
<?php
$formAction = isset($event) ?
Container::$routeCollection->getRoute('community.event.edit-action')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) :
Container::$routeCollection->getRoute('community.events.new-action')->generateLink(['communitySlug' => $community->getSlug()]);
?>
<form id="eventForm" action="<?= $formAction ?>" method="post" data-redirect-on-success="true">
<p class="formLabel">Title</p>
<input type="text" class="text big fullWidth" name="title" value="<?= isset($event) ? $event->getTitle() : '' ?>" required>
<p class="formLabel marginTop">Description</p>
<textarea class="text big fullWidth" name="description" rows="3"><?= isset($event) ? $event->getDescription() : '' ?></textarea>
<p class="formLabel marginTop">Start</p>
<input type="datetime-local" class="text big fullWidth" name="start" value="<?= isset($event) ? $event->getStartDate()->format('Y-m-d\TH:i') : '' ?>" required>
<p class="formLabel marginTop">End</p>
<input type="datetime-local" class="text big fullWidth" name="end" value="<?= isset($event) ? $event->getEndDate()->format('Y-m-d\TH:i') : '' ?>" required>
<p class="formError justify marginTop"></p>
<div class="right marginTop" style="font-size: 0;">
<button type="submit" name="submit_button"><?= isset($event) ? '<i class="fa-regular fa-floppy-disk"></i> Save' : '<i class="fa-regular fa-plus"></i> Create' ?></button>
<?php if (isset($event)): ?>
<button type="submit" form="deleteEvent" name="submit_button" data-confirmation="Are you sure you want to delete this event? All linked transactions will be unlinked!" class="red marginLeft"><i class="fa-regular fa-trash-can"></i> Delete</button>
<?php endif; ?>
</div>
</form>
<?php if (isset($event)): ?>
<form id="deleteEvent" action="<?= Container::$routeCollection->getRoute('community.event.delete-action')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>" method="post" data-redirect-on-success="<?= Container::$routeCollection->getRoute('community.events')->generateLink(['communitySlug' => $community->getSlug()]) ?>"></form>
<?php endif; ?>
</div>
@endsection
+41
View File
@@ -0,0 +1,41 @@
@extends(templates/layout_normal)
@section(main)
<h2>
<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()]) ?>"><?= $community->getName() ?></a> »
Events
</h2>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.events.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New event</a></p>
<?php if ($numberOfEvents > 0): ?>
<?php
$paginationRouteId = 'community.events';
$paginationRouteParams = ['communitySlug' => $community->getSlug()];
?>
<?php if ($pages > 1): ?>
@include(templates/pagination)
<?php endif; ?>
<?php foreach ($events as $event): ?>
<a class="block" href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $community->getSlug(), 'eventSlug' => $event->getSlug()]) ?>">
<div class="box">
<p style="font-weight: bold;"><?= $event->getTitle() ?></p>
<p class="small"><?= $event->getStartDate()->format('Y-m-d') ?> – <?= $event->getEndDate()->format('Y-m-d') ?></p>
<p class="small"><?= $event->getDescription() ?></p>
</div>
</a>
<?php endforeach; ?>
<?php if ($pages > 1): ?>
@include(templates/pagination)
<?php endif; ?>
<p class="marginTop"><a href="<?= Container::$routeCollection->getRoute('community.events.new')->generateLink(['communitySlug' => $community->getSlug()]) ?>">New event</a></p>
<?php else: ?>
<div class="box">
<p>There are no events yet.</p>
</div>
<?php endif; ?>
@endsection
+11 -5
View File
@@ -16,13 +16,19 @@
<?php endif; ?>
</div>
<div>
<h3 class="marginBottom">Upcoming events</h3>
<?php if (count($upcomingEvents) > 0): ?>
<?php foreach ($upcomingEvents as $event): ?>
<!-- todo -->
<h3 class="marginBottom">Upcoming and recent events</h3>
<?php if (count($upcomingAndRecentEvents) > 0): ?>
<?php foreach ($upcomingAndRecentEvents as $event): ?>
<p>
<a href="<?= Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $event->getCommunity()->getSlug(), 'eventSlug' => $event->getSlug()]) ?>"><?= $event->getTitle() ?></a>
<span class="small">
(<a href="<?= Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $event->getCommunity()->getSlug()]) ?>"><?= $event->getCommunity()->getName() ?></a>)
<?= $event->getStartDate()->format('Y-m-d') ?> – <?= $event->getEndDate()->format('Y-m-d') ?>
</span>
</p>
<?php endforeach; ?>
<?php else: ?>
<p>There is no upcoming event.</p>
<p>There is no event to show.</p>
<?php endif; ?>
</div>
</div>
+31
View File
@@ -0,0 +1,31 @@
<p class="paginateContainer marginTop">
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => 1])) ?>">«</a>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => max(1, $currentPage - 1)])) ?>">‹</a>
<?php
$maxPages = 7;
if ($pages <= $maxPages) {
$start = 1;
$end = $pages;
} else {
$maxAdditionalPages = $maxPages - 1;
$maxAdditionalPagesHalf = ceil($maxAdditionalPages / 2);
$start = $currentPage - $maxAdditionalPagesHalf > 1 ?
($currentPage - $maxAdditionalPagesHalf < $pages - $maxAdditionalPages ?
$currentPage - $maxAdditionalPagesHalf :
$pages - $maxAdditionalPages) :
1;
$end = $start + $maxAdditionalPages < $pages ?
$start + $maxAdditionalPages :
$pages;
}
?>
<?php for ($i = $start; $i <= $end; $i++): ?>
<?php if ($currentPage == $i): ?>
<span class="selected"><?= $i ?></span>
<?php else: ?>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => $i])) ?>"><?= $i ?></a>
<?php endif; ?>
<?php endfor; ?>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => min($pages, $currentPage + 1)])) ?>">›</a>
<a href="<?= Container::$routeCollection->getRoute($paginationRouteId)->generateLink(array_merge($paginationRouteParams, ['page' => $pages])) ?>">»</a>
</p>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="description" content="<?= $_ENV['APP_NAME'] ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#212f4d">
<title><?= $_ENV['APP_NAME'] ?></title>
<title><?= isset($title) ? $title . ' – ' : '' ?><?= $_ENV['APP_NAME'] ?></title>
<?php if (preg_match('/^(http(s)?:)?\/\//', $_ENV['STATIC_ROOT']) === 1): ?>
<link href="<?= $_ENV['STATIC_ROOT'] ?>" rel="preconnect">
<?php endif; ?>
+19
View File
@@ -13,6 +13,8 @@ use RVR\Controller\UserController;
use RVR\Controller\UserSearchController;
use RVR\Controller\CommunityController;
use RVR\Controller\TransactionController;
use RVR\Controller\EventRedirectController;
use RVR\Controller\EventController;
use RVR\Repository\UserRepository;
require 'app.php';
@@ -61,6 +63,10 @@ Container::$routeCollection->group('account', function (RouteCollection $routeCo
$routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [UserController::class, 'authenticateWithGoogle']);
});
Container::$routeCollection->get('searchUser', 'searchUser', [UserSearchController::class, 'searchUser']);
Container::$routeCollection->group('now', function (RouteCollection $routeCollection) {
$routeCollection->get('now.event', '', [EventRedirectController::class, 'getEvent']);
$routeCollection->get('now.transactions.new', 'transaction', [EventRedirectController::class, 'getEventNewTransaction']);
});
Container::$routeCollection->group('communities', function (RouteCollection $routeCollection) {
$routeCollection->get('community.new', 'new', [CommunityController::class, 'getCommunityNew']);
$routeCollection->post('community.new-action', 'new', [CommunityController::class, 'saveCommunity']);
@@ -69,6 +75,7 @@ Container::$routeCollection->group('communities', function (RouteCollection $rou
$routeCollection->get('community.settings', 'settings', [CommunityController::class, 'getCommunitySettings']);
$routeCollection->get('community.edit', 'edit', [CommunityController::class, 'getCommunityEdit']);
$routeCollection->post('community.edit-action', 'edit', [CommunityController::class, 'saveCommunity']);
$routeCollection->post('community.delete-action', 'delete', [CommunityController::class, 'deleteCommunity']);
$routeCollection->get('community.members', 'members', [CommunityController::class, 'getMembersEdit']);
$routeCollection->post('community.members.new-action', 'members/new', [CommunityController::class, 'saveMember']);
$routeCollection->post('community.members.edit-action', 'members/edit', [CommunityController::class, 'saveMember']);
@@ -91,6 +98,18 @@ Container::$routeCollection->group('communities', function (RouteCollection $rou
$routeCollection->post('community.transactions.edit-action', '{transactionId}', [TransactionController::class, 'saveTransaction']);
$routeCollection->post('community.transactions.delete-action', '{transactionId}/delete', [TransactionController::class, 'deleteTransaction']);
});
$routeCollection->group('events', function (RouteCollection $routeCollection) {
$routeCollection->get('community.events', '', [EventController::class, 'getEvents']);
$routeCollection->get('community.events.new', 'new', [EventController::class, 'getEventEdit']);
$routeCollection->post('community.events.new-action', 'new', [EventController::class, 'saveEvent']);
$routeCollection->get('community.events.search', 'search', [EventController::class, 'searchEvent']);
$routeCollection->group('{eventSlug}', function (RouteCollection $routeCollection) {
$routeCollection->get('community.event', '', [EventController::class, 'getEvent']);
$routeCollection->get('community.event.edit', 'edit', [EventController::class, 'getEventEdit']);
$routeCollection->post('community.event.edit-action', 'edit', [EventController::class, 'saveEvent']);
$routeCollection->post('community.event.delete-action', 'delete', [EventController::class, 'deleteEvent']);
});
});
});
});