diff --git a/Gemfile b/Gemfile index b6a5c6d8..5b7828e0 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem 'faraday' gem 'image_processing', '>= 1.2' gem 'jbuilder' gem 'jsbundling-rails' +gem 'money-rails', '~> 1.12' gem 'puma', '~> 6.0' gem 'simple_calendar' gem 'sprockets-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 25e3e9ee..83cf7978 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -167,6 +167,15 @@ GEM mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.24.1) + monetize (1.13.0) + money (~> 6.12) + money (6.19.0) + i18n (>= 0.6.4, <= 2) + money-rails (1.15.0) + activesupport (>= 3.0) + monetize (~> 1.9) + money (~> 6.13) + railties (>= 3.0) msgpack (1.7.2) mutex_m (0.2.0) net-http (0.4.1) @@ -342,6 +351,7 @@ DEPENDENCIES image_processing (>= 1.2) jbuilder jsbundling-rails + money-rails (~> 1.12) puma (~> 6.0) rails (~> 7.1.3.1) rspec-rails diff --git a/README.md b/README.md index 9d1372e7..9bb61c46 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@
Usuários administrativos podem ser cadastrados no sistema e usuários proprietários e moradores podem ser convidados por e-mail a se registrar.
+:trophy:Usuários administrativos podem ser cadastrados no sistema e usuários proprietários e moradores podem ser convidados por e-mail a se registrar;
+ +:trophy:Apenas usuários administrativos categorizados como super no momento da criação podem cadastrar outros usuários administrativos, cadastrar um condomínio com endereço e associar um usuário ou mais usuários administrativos àquele condomínio;
+ +:trophy:Usuários administrativos regulares podem cadastrar torres, tipos de unidade, unidade de um condomínio, andares, áreas comuns e moradores;
+ +:trophy:A fração ideal de cada unidade é gerada automaticamente com base no tamanho de cada uma e a quantidade de unidades em um condomínio;
+ +:trophy:Tanto administradores quanto moradores podem ver a página de listatrophy e detalhes de condomínio. Tendo sua exibição alterada para cada tipo de usuário;
+ +:trophy:Moradores podem fazer uma reserva de área comum a partir de um calendário de reservas, bem como cancelar essa reserva. Gerando ou cancelando cobrança de taxa de uso dessa reserva na aplicação PagueAluguel;
+ +:trophy:Moradores podem consultar suas faturas e enviar comprovantes de pagamento que serão comunicados através da aplicação PagueAluguel;
-Apenas usuários administrativos categorizados como super no momento da criação podem cadastrar outros usuários administrativos, cadastrar um condomínio com endereço e associar um usuário ou mais usuários administrativos àquele condomínio.
+:trophy:Administradores podem registrar entrada de visitantes no condomínio e visualizar uma lista com o histórico de visitas;
+ +:trophy:Administradores podem criar avisos para serem mostrados em um mural na tela de detalhes de um condomínio.
-Usuários administrativos regulares podem cadastrar torres, tipos de unidade, unidade de um condomínio, andares, áreas comuns e moradores.
-Tanto administradores quanto moradores podem ver a página de listagem e detalhes de condomínio. Tendo sua exibição alterada para cada tipo de usuário.
-Moradores podem fazer uma reserva de área comum, bem como cancelar essa reserva.
- + ## Endpoints da API @@ -124,7 +137,7 @@ Retorna erro 404 caso não exista um condomínio cadastrado com esse id. Exemplo de resposta: ```json { - "name": "Condominio Residencial Paineiras", + "name": "Condomínio Residencial Paineiras", "registration_number": "62.810.952/2718-22", "address": { "public_place": "Travessa João Edimar", @@ -290,7 +303,7 @@ Possíveis respostas ``` Retorna 404 se não existe um proprietário com o CPF informado na aplicação CondoMínios, ou se existe, mas não possui nenhuma unidade como propriedade; Retorna 412 se o CPF não for válido para consulta. -Retorna 200 se o CPF é de um proprietário de alguma unidade e o seguinte JSON +Retorna 200 se o CPF é de um proprietário de alguma unidade e o seguinte JSON. ``` ```json @@ -371,7 +384,7 @@ Exemplo de Resposta: ``` -caso não existam áreas comuns cadastradas para o condomínio informado retorna o `id` do condomínio e um array vazio.
+Caso não existam áreas comuns cadastradas para o condomínio informado retorna o `id` do condomínio e um array vazio.
Retorna erro `404` caso o condomínio informado não esteja cadastrado.
@@ -384,17 +397,20 @@ Exemplo de Resposta: Exemplo de Resposta: ```json { - "name": "Piscina", - "description": "Para adultos e crianças", - "max_occupancy": 20, - "rules": "Só pode ser usada até 22h", - "condo_id": 1 + "name": "Piscina", + "description": "Para adultos e crianças", + "max_occupancy": 20, + "rules": "Só pode ser usada até 22h", + "condo_id": 1 } ```Retorna erro `404` caso a área comum informada não esteja cadastrada para o condomínio informado.
+ + + ## Instalação e Execução @@ -403,10 +419,14 @@ Exemplo de Resposta:Você vai precisar da versão 3.2.2 do Ruby, libvips e uma versão atual de NodeJS com Yarn instalado. Recomendamos sempre a instalação das versões LTS (Long Term Support).
-#### Instalação do [libvips](https://github.com/libvips/libvips/wiki/Build-for-Ubuntu) no ubuntu: +Instalação do [libvips](https://github.com/libvips/libvips/wiki/Build-for-Ubuntu) com o apt-get: ```sh sudo apt install libvips ``` +Instalação do rails +``` +gem install rails +``` @@ -420,6 +440,10 @@ Entre na pasta do projeto: ```sh cd condominions ``` +Instale Bundle: +```sh +bundle install +``` Instale as dependências: ```sh bin/setup @@ -429,14 +453,27 @@ Para popular o banco de dados: rails db:seed ``` - - + + ### Execução de Testes + Para rodar os testes, execute: ```sh rake spec ``` +### Testando E-mails + +Caso queria testar emails, você pode fazê-lo instalando a gem [MailCatcher](https://mailcatcher.me/): +```sh +gem install mailcatcher +``` +Para executar o MailCatcher: +```sh +mailcatcher +``` +Agora você pode acessá-lo através da rota http://localhost:1080/ + ### Execução da Aplicação @@ -446,40 +483,39 @@ bin/dev ``` Agora é possível acessar a aplicação a partir da rota http://localhost:3000/ +**Integração com o PagueAluguel**: Essa aplicação foi construída para ser integrada com o [PagueAluguel](https://github.com/TreinaDev/pague-aluguel). Com ambas as aplicações rodando, você poderá utilizá-la com todas as suas funcionalidades! + ## Estrutura do Banco de Dados -![Estrutura do banco de dados](https://i.imgur.com/emiKwf5.png) +![Estrutura do banco de dados](https://github.com/user-attachments/assets/ee40045d-5e22-4404-96c8-be0c573b4fd6) - + -## Banco de Dados Iniciais +## Seeds de Usuários -Dados inseridos no seeds +Esses usuários são gerados nas seeds e você pode utilizá-los para testar a aplicação. >Administradores ->>|Nome Completo|CPF|E-mail|Senha| ->>| :--------: | :--------: |:--------: | :--------: | ->>|Murilo Pereira Rocha|745.808.535-55|adm@teste.com|teste123| - ->Endereços ->>|Rua|Número|Bairro|Cidade|Estado|CEP|ID| ->>| :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | :--------: | ->>|Travessa João Edimar|29|João Eduardo II|Rio Branco|AC|69911-520| 1 | +>>|Nome Completo|E-mail|Senha| +>>| :--------: | :--------: |:--------: | +>>|Ednaldo Pereira|adm@teste.com|teste123| +>>|Adroaldo Silva Santos|adm@teste.com|teste123| ->Condominions ->>|Nome do condomínio|CNPJ|ID do endereço| ->>| :--------: | :--------: | :--------: | ->>|Condominio Residencial Paineiras|62.810.952/2718-22| 1 | +>Residentes +>>|Nome Completo|E-mail|Senha| +>>| :--------: | :--------: |:--------: | +>>|Marina Santos Oliveira|marina@email.com|teste123| +>>|Rafael Souza Lima|rafael@email.com|teste123| - + ## Desenvolvedores @@ -508,4 +544,4 @@ Dados inseridos no seeds [Ruby-url]: https://www.ruby-lang.org/en/news/2023/03/30/ruby-3-2-2-released/ [RubyOnRails.com]: https://img.shields.io/static/v1?label=Ruby%20On%20Rails&message=7.1.3.1&color=red&style=for-the-badge&logo=rubyonrails [RubyOnRails-url]: https://rubyonrails.org/2023/11/10/Rails-7-1-2-has-been-released -[EstruturaDoBancoDeDados-URL]: https://i.imgur.com/emiKwf5.png \ No newline at end of file +[EstruturaDoBancoDeDados-URL]: https://i.imgur.com/emiKwf5.png diff --git a/app/assets/stylesheets/style.scss b/app/assets/stylesheets/style.scss index 80417b02..d984cfd7 100644 --- a/app/assets/stylesheets/style.scss +++ b/app/assets/stylesheets/style.scss @@ -1,7 +1,3 @@ -body { - margin-bottom: 4rem; -} - // Colors .bg-light-gray { @@ -12,6 +8,10 @@ body { background-color: #FDE879; } +.bg-blue-cyan { + background-color: #1985A1 +} + .bg-medium-blue-lighter { background-color: #577998; } @@ -20,6 +20,10 @@ body { background-color: #4c677f; } +.bg-medium-blue-dark { + background-color: #3f556f; +} + // Images .user-image-200 { width: 200px; diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb index 2176cf64..9b31cf50 100644 --- a/app/controllers/announcements_controller.rb +++ b/app/controllers/announcements_controller.rb @@ -54,7 +54,7 @@ def set_breadcrumbs_for_action def set_breadcrumbs_for_details add_breadcrumb @announcement.condo.name, condo_path(@announcement.condo) - add_breadcrumb @announcement.title, common_area_path(@announcement) + add_breadcrumb @announcement.title, @announcement end def set_announcement diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e1b08c50..f7a0e0bf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,6 +25,12 @@ def authorize_super_manager not_authorized_redirect unless current_manager.is_super end + def authorize_condo_manager_superintendent(condo) + return if authorize_manager(condo) || (authorize_resident(condo) && current_resident&.superintendent) + + not_authorized_redirect + end + def authorize_condo_manager(condo) not_authorized_redirect unless authorize_manager(condo) end @@ -44,7 +50,7 @@ def authorize_manager(condo) end def authorize_resident(condo) - resident_signed_in? && condo.residents.include?(current_resident) + resident_signed_in? && condo && condo.residents.include?(current_resident) end def not_authorized_redirect diff --git a/app/controllers/fines_controller.rb b/app/controllers/fines_controller.rb new file mode 100644 index 00000000..939fb9ce --- /dev/null +++ b/app/controllers/fines_controller.rb @@ -0,0 +1,85 @@ +class FinesController < ApplicationController + before_action :set_condo, only: %i[new create] + before_action :authorize_superintendent, only: %i[new create] + before_action :set_breadcrumbs_for_register, only: %i[new create] + + def new + @fine = SingleCharge.new(condo: @condo) + end + + def create + @fine = SingleCharge.new(fine_params) + + if @fine.valid? + return redirect_to @condo if post_response + + flash.now.alert = t('alerts.single_charge.server_error') + else + flash.now.alert = t('alerts.single_charge.fine_not_created') + end + render 'new', status: :unprocessable_entity + end + + private + + def set_breadcrumbs_for_register + add_breadcrumb @condo.name.to_s, @condo + add_breadcrumb I18n.t('breadcrumb.fine.new') + end + + def authorize_superintendent + return if resident_signed_in? && @condo.superintendent && @condo.superintendent.tenant == current_resident + + redirect_to root_path + end + + def set_condo + @condo = Condo.find params[:condo_id] + end + + def find_tower_and_floor + return unless params['single_charge'] + + tower = Tower.find_by(id: params['single_charge']['tower_id']) + return tower.floors[params['single_charge']['floor'].to_i - 1] if tower + + nil + end + + def find_unit_id + floor = find_tower_and_floor + + return floor.units[params['single_charge']['unit'].to_i - 1].id if floor + + nil + end + + def fine_params + params.require(:single_charge) + .permit(:value, :description) + .merge({ charge_type: :fine, condo: @condo, unit_id: find_unit_id }) + end + + def single_charge_params + { single_charge: { + description: @fine.description, + value_cents: @fine.value_cents, + charge_type: @fine.charge_type, + issue_date: Time.zone.today, + condo_id: @fine.condo.id, + common_area_id: nil, + unit_id: @fine.unit.id + } } + end + + def post_response + request = Faraday.new(url: Rails.configuration.api['base_url'].to_s) + .post('/api/v1/single_charges/', single_charge_params.to_json, + 'Content-Type' => 'application/json') + return flash.notice = "Multa lançada com sucesso para a #{@fine.unit.print_identifier}" if request.success? + + nil + rescue Faraday::ConnectionFailed + nil + end +end diff --git a/app/controllers/residents/passwords_controller.rb b/app/controllers/residents/passwords_controller.rb new file mode 100644 index 00000000..7e480021 --- /dev/null +++ b/app/controllers/residents/passwords_controller.rb @@ -0,0 +1,23 @@ +module Residents + class PasswordsController < Devise::PasswordsController + def edit + add_breadcrumb I18n.t('breadcrumb.resident.edit_password') + self.resource = find_resource_by_token + + return redirect_to root_path, alert: t('alerts.resident.invalid_token') unless valid_token? + + sign_out(current_manager || current_resident) if anyone_signed_in? + super + end + + private + + def find_resource_by_token + resource_class.with_reset_password_token(params[:reset_password_token]) + end + + def valid_token? + resource.present? && resource.reset_password_period_valid? + end + end +end diff --git a/app/controllers/residents_controller.rb b/app/controllers/residents_controller.rb index 089013ff..eac75cc1 100644 --- a/app/controllers/residents_controller.rb +++ b/app/controllers/residents_controller.rb @@ -1,7 +1,9 @@ class ResidentsController < ApplicationController - before_action :authenticate_manager!, only: %i[new create find_towers show] + before_action :authenticate_manager!, only: %i[new create show] before_action :set_resident, only: %i[update edit_photo update_photo show] before_action :authenticate_resident!, only: %i[update edit_photo update_photo] + before_action :set_condo, only: %i[find_towers] + before_action -> { authorize_condo_manager_superintendent(@condo) }, only: %i[find_towers] def show add_breadcrumb I18n.t('breadcrumb.resident.show') @@ -46,13 +48,12 @@ def update end def find_towers - condo = Condo.find_by(id: params[:id]) - return render status: :not_found, json: [] unless condo + return render status: :not_found, json: [] unless @condo - towers = condo.towers + towers = @condo.towers return render status: :not_found, json: [] if towers.empty? - render json: condo.towers.to_json(only: %i[id name units_per_floor floor_quantity]) + render json: @condo.towers.to_json(only: %i[id name units_per_floor floor_quantity]) end def confirm @@ -93,6 +94,10 @@ def find_unit_id private + def set_condo + @condo = Condo.find_by(id: params[:id]) + end + def authenticate_resident! return redirect_to root_path if manager_signed_in? diff --git a/app/controllers/tenants_controller.rb b/app/controllers/tenants_controller.rb index 77f72b91..af8c6921 100644 --- a/app/controllers/tenants_controller.rb +++ b/app/controllers/tenants_controller.rb @@ -8,7 +8,7 @@ def new; end def create return unless update_resident_for_valid_unit - @resident.mail_not_confirmed! && send_email if @resident.residence_registration_pending? + return @resident.mail_not_confirmed! && send_email if @resident.residence_registration_pending? redirect_to @resident, notice: t('notices.tenant.updated') end @@ -29,6 +29,7 @@ def send_email random_password = SecureRandom.alphanumeric 8 @resident.update password: random_password @resident.send_invitation random_password + redirect_to @resident, notice: t('notices.resident.send_email') end def update_resident_for_valid_unit diff --git a/app/controllers/visitor_entries_controller.rb b/app/controllers/visitor_entries_controller.rb index bf18624f..93620c0a 100644 --- a/app/controllers/visitor_entries_controller.rb +++ b/app/controllers/visitor_entries_controller.rb @@ -10,7 +10,7 @@ def index @result = [] params.permit(:full_name, :visit_date, :identity_number).each do |key, value| - key = 'created_at' if key == 'visit_date' + key = 'database_datetime' if key == 'visit_date' @result << find_visitor_entries(key, value) if value.present? end diff --git a/app/javascript/controllers/fines_controller.js b/app/javascript/controllers/fines_controller.js new file mode 100644 index 00000000..d07c9f34 --- /dev/null +++ b/app/javascript/controllers/fines_controller.js @@ -0,0 +1,40 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ['condo', 'tower', 'floor', 'unit'] + + searchTowers(condoId) { + fetch(`${window.origin}/residents/find_towers?id=${condoId}`) + .then((response) => { + return response.json() + }) + .then((towers) => { + this.towerTarget.innerHTML = "" + towers.forEach(tower => { + this.towerTarget.options.add(new Option(tower.name, tower.id)) + }); + this.towers = towers + this.changeTower() + }) + .catch(() => { console.log('Towers not found') }) + } + + connect() { + this.searchTowers(this.element.getAttribute('condo-id')) + } + + changeTower() { + let tower = this.towers[this.towerTarget.selectedIndex] + console.log(tower) + this.unitTarget.innerHTML = "" + this.floorTarget.innerHTML = "" + + for (let floor = 1; floor <= tower.floor_quantity; floor++) { + this.floorTarget.options.add(new Option(`${floor}`, `${floor}`)) + } + + for (let unit = 1; unit <= tower.units_per_floor; unit++) { + this.unitTarget.options.add(new Option(`${unit}`, `${unit}`)) + } + } +} \ No newline at end of file diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 486e14af..8c6c7bb1 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -20,3 +20,6 @@ application.register("resident", ResidentController) import UnitsController from "./units_controller" application.register("units", UnitsController) + +import FinesController from "./fines_controller" +application.register("fines", FinesController) diff --git a/app/models/single_charge.rb b/app/models/single_charge.rb new file mode 100644 index 00000000..b6d768f4 --- /dev/null +++ b/app/models/single_charge.rb @@ -0,0 +1,22 @@ +class SingleCharge < ApplicationRecord + belongs_to :condo + belongs_to :unit + belongs_to :common_area, optional: true + + validates :value_cents, :charge_type, presence: true + validates :description, presence: true, if: -> { charge_type == 'fine' } + + validate :unit_valid? + + enum charge_type: { fine: 0, common_area_fee: 1 } + + monetize :value_cents, as: :value, with_model_currency: :currency + + private + + def unit_valid? + return true if unit&.owner + + errors.add(:unit, 'não possui um proprietário.') + end +end diff --git a/app/models/tower.rb b/app/models/tower.rb index 04a0d2dc..c487473c 100644 --- a/app/models/tower.rb +++ b/app/models/tower.rb @@ -8,7 +8,7 @@ class Tower < ApplicationRecord validates :name, :floor_quantity, :units_per_floor, presence: true validates :floor_quantity, :units_per_floor, numericality: { - greater_than: 0, only_integer: true + greater_than: 0, only_integer: true, less_than: 200 } after_create :generate_floors diff --git a/app/models/unit.rb b/app/models/unit.rb index 5cae10e7..14edfee7 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -15,6 +15,10 @@ def short_identifier "#{floor.identifier}#{floor.units.index(self) + 1}" end + def identifier + floor.units.index(self) + 1 + end + def tower_identifier "#{floor.tower.name} - #{short_identifier}" end diff --git a/app/models/visitor_entry.rb b/app/models/visitor_entry.rb index cd2562da..69eef811 100644 --- a/app/models/visitor_entry.rb +++ b/app/models/visitor_entry.rb @@ -8,4 +8,12 @@ class VisitorEntry < ApplicationRecord validates :identity_number, length: { in: 5..10 } validates :identity_number, format: { with: ID_REGEX, message: I18n.t('alerts.visitor_entry.only_numbers_and_letters') } + + before_save :set_database_datetime + + private + + def set_database_datetime + self.database_datetime ||= (DateTime.now + Time.zone.utc_offset.seconds) + end end diff --git a/app/views/announcements/show.html.erb b/app/views/announcements/show.html.erb index 59b26eee..f2471e2e 100644 --- a/app/views/announcements/show.html.erb +++ b/app/views/announcements/show.html.erb @@ -13,8 +13,10 @@Atualizado em: <%= I18n.l(@announcement.updated_at, format: :custom) %>
-