Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
class ActivitiesController < ApplicationController
|
||||
before_action :set_organization, only: %i[create]
|
||||
|
||||
def create
|
||||
@activity = @organization.activities.build(activity_params)
|
||||
@activity.user = current_user
|
||||
@activity.happened_at ||= Time.current
|
||||
|
||||
if @activity.save
|
||||
maybe_update_pipeline_from_activity!
|
||||
redirect_to @organization, notice: "Attività registrata."
|
||||
else
|
||||
redirect_to @organization, alert: @activity.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_organization
|
||||
@organization = Organization.find(params[:organization_id])
|
||||
end
|
||||
|
||||
def activity_params
|
||||
params.require(:activity).permit(
|
||||
:activity_type, :subject, :description, :happened_at, :contact_id, :opportunity_id
|
||||
)
|
||||
end
|
||||
|
||||
def maybe_update_pipeline_from_activity!
|
||||
opportunity = @activity.opportunity || @organization.opportunities.open_stage.order(updated_at: :desc).first
|
||||
return unless opportunity
|
||||
|
||||
stage_map = {
|
||||
"email_sent" => "contacted",
|
||||
"email_received" => "replied",
|
||||
"call" => "contacted",
|
||||
"demo" => "demo_trial",
|
||||
"trial_started" => "demo_trial",
|
||||
"first_use" => "first_use",
|
||||
"proposal_sent" => "proposal",
|
||||
"won" => "won",
|
||||
"lost" => "lost"
|
||||
}
|
||||
target = stage_map[@activity.activity_type]
|
||||
return unless target
|
||||
return if opportunity.won? || opportunity.lost?
|
||||
return if Catalog::PIPELINE_ORDER.index(opportunity.pipeline_stage).to_i >= Catalog::PIPELINE_ORDER.index(target).to_i
|
||||
|
||||
attrs = { pipeline_stage: target }
|
||||
if target == "lost"
|
||||
attrs[:lost_reason] = params[:lost_reason].presence || "other"
|
||||
end
|
||||
opportunity.move_to_stage!(target, lost_reason: attrs[:lost_reason], user: current_user)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class AdminController < ApplicationController
|
||||
before_action :require_admin
|
||||
|
||||
def show
|
||||
@page_title = "Impostazioni"
|
||||
@users_count = User.count
|
||||
@active_users_count = User.active.count
|
||||
@projects_count = Project.count
|
||||
@mail_identities_count = MailIdentity.count
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include Authentication
|
||||
include ProjectScoping
|
||||
include Pagy::Backend
|
||||
|
||||
allow_browser versions: :modern
|
||||
before_action :set_current_user
|
||||
|
||||
helper_method :page_title
|
||||
|
||||
private
|
||||
|
||||
def page_title
|
||||
@page_title || Rails.application.config.x.app_name
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :require_login
|
||||
helper_method :current_user, :logged_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
@current_user ||= session[:user_id] ? User.find_by(id: session[:user_id]) : nil
|
||||
end
|
||||
|
||||
def logged_in?
|
||||
current_user.present? && current_user.active?
|
||||
end
|
||||
|
||||
def require_login
|
||||
if current_user.present? && !current_user.active?
|
||||
logout
|
||||
redirect_to login_path, alert: "Il tuo account è stato disabilitato."
|
||||
return
|
||||
end
|
||||
return if logged_in?
|
||||
|
||||
redirect_to login_path, alert: "Effettua l'accesso per continuare."
|
||||
end
|
||||
|
||||
def require_admin
|
||||
return if current_user&.admin? && current_user.active?
|
||||
|
||||
redirect_to root_path, alert: "Accesso riservato agli amministratori."
|
||||
end
|
||||
|
||||
def login_as(user)
|
||||
reset_session
|
||||
session[:user_id] = user.id
|
||||
@current_user = user
|
||||
Current.user = user
|
||||
end
|
||||
|
||||
def logout
|
||||
reset_session
|
||||
@current_user = nil
|
||||
Current.user = nil
|
||||
end
|
||||
|
||||
def set_current_user
|
||||
Current.user = current_user
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
module ProjectScoping
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
helper_method :current_project, :available_projects, :in_project_space?
|
||||
before_action :set_current_project, if: :logged_in?
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
return {} unless respond_to?(:request) && request&.path&.start_with?("/p/") && current_project
|
||||
|
||||
{ project_code: current_project.code }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def available_projects
|
||||
@available_projects ||= if current_user&.admin?
|
||||
Project.active.ordered
|
||||
elsif current_user
|
||||
current_user.accessible_projects
|
||||
else
|
||||
Project.none
|
||||
end
|
||||
end
|
||||
|
||||
def current_project
|
||||
Current.project
|
||||
end
|
||||
|
||||
def in_project_space?
|
||||
current_project.present? && request.path.start_with?("/p/")
|
||||
end
|
||||
|
||||
def set_current_project
|
||||
code = params[:project_code].to_s.presence
|
||||
unless code
|
||||
Current.project = nil
|
||||
return
|
||||
end
|
||||
|
||||
project = available_projects.find_by(code: code)
|
||||
if project.nil?
|
||||
redirect_to root_path, alert: "Non hai accesso a questo progetto."
|
||||
return
|
||||
end
|
||||
|
||||
Current.project = project
|
||||
session[:last_project_code] = project.code
|
||||
end
|
||||
|
||||
def require_current_project!
|
||||
return if current_project
|
||||
|
||||
redirect_to root_path, alert: "Seleziona un progetto per continuare."
|
||||
end
|
||||
|
||||
def organizations_for_current_project
|
||||
Organization.for_project(current_project)
|
||||
end
|
||||
|
||||
def opportunities_for_current_project
|
||||
Opportunity.for_project(current_project)
|
||||
end
|
||||
|
||||
def tasks_for_current_project
|
||||
Task.for_project(current_project)
|
||||
end
|
||||
|
||||
def project_home_path_for(project)
|
||||
project_root_path(project_code: project.code)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
class ContactsController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
before_action :set_contact, only: %i[show edit update destroy]
|
||||
before_action :load_organizations, only: %i[new create edit update]
|
||||
|
||||
def index
|
||||
@page_title = "Contatti"
|
||||
scope = Contact.joins(:organization)
|
||||
.merge(organizations_for_current_project)
|
||||
.includes(:organization)
|
||||
.primary_first
|
||||
scope = scope.search(params[:q]) if params[:q].present?
|
||||
scope = scope.where(organization_id: params[:organization_id]) if params[:organization_id].present?
|
||||
@pagy, @contacts = pagy(scope, items: 30)
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.csv { send_data CsvExport.contacts(scope), filename: "contacts-#{Date.current}.csv" }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@page_title = @contact.full_name
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuovo contatto"
|
||||
@contact = Contact.new(organization_id: params[:organization_id], primary_contact: params[:primary].present?)
|
||||
end
|
||||
|
||||
def create
|
||||
@contact = Contact.new(contact_params)
|
||||
if @contact.save
|
||||
redirect_to @contact.organization, notice: "Contatto creato."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica #{@contact.full_name}"
|
||||
end
|
||||
|
||||
def update
|
||||
if @contact.update(contact_params)
|
||||
redirect_to @contact.organization, notice: "Contatto aggiornato."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
org = @contact.organization
|
||||
@contact.destroy!
|
||||
redirect_to org, notice: "Contatto eliminato."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_contact
|
||||
@contact = Contact.find(params[:id])
|
||||
end
|
||||
|
||||
def load_organizations
|
||||
@organizations = organizations_for_current_project.order(:name)
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(
|
||||
:organization_id, :first_name, :last_name, :role, :email, :phone, :mobile,
|
||||
:preferred_contact_method, :notes, :primary_contact
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class DashboardController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
def show
|
||||
@page_title = "Dashboard"
|
||||
@goal = SalesGoal.current(current_project).order(created_at: :desc).first
|
||||
opp_scope = opportunities_for_current_project
|
||||
@metrics = Dashboard::Metrics.new(scope: opp_scope, project: current_project)
|
||||
task_scope = tasks_for_current_project
|
||||
@overdue_tasks = task_scope.overdue.includes(:organization, :contact, :assigned_user).ordered.limit(10)
|
||||
@today_tasks = task_scope.due_today.includes(:organization, :contact, :assigned_user).ordered.limit(10)
|
||||
@upcoming_tasks = task_scope.upcoming.includes(:organization, :contact, :assigned_user).ordered.limit(10)
|
||||
@attention = @metrics.attention_items
|
||||
@funnel = @metrics.funnel_steps
|
||||
@stage_counts = @metrics.stage_counts
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
@page_title = "I miei progetti"
|
||||
@projects = available_projects
|
||||
|
||||
if !current_user.admin? && @projects.one?
|
||||
redirect_to project_root_path(project_code: @projects.first.code) and return
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
class ImportsController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
def new
|
||||
@page_title = "Import CSV"
|
||||
end
|
||||
|
||||
def create
|
||||
unless params[:file].present?
|
||||
redirect_to new_import_path, alert: "Seleziona un file CSV." and return
|
||||
end
|
||||
|
||||
importer = CsvImport::Organizations.new(file: params[:file], user: current_user, project: current_project)
|
||||
|
||||
if params[:preview].present?
|
||||
@preview = importer.preview
|
||||
@page_title = "Anteprima import"
|
||||
render :preview and return
|
||||
end
|
||||
|
||||
@result = importer.import!
|
||||
@page_title = "Risultato import"
|
||||
render :result
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,63 @@
|
||||
class MailIdentitiesController < ApplicationController
|
||||
before_action :require_admin
|
||||
before_action :set_identity, only: %i[edit update destroy]
|
||||
|
||||
def index
|
||||
@page_title = "Account email"
|
||||
@mail_identities = MailIdentity.ordered
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuovo account email"
|
||||
@mail_identity = MailIdentity.new(smtp_port: 587, encryption: "starttls", smtp_authentication: "plain", active: true, verify_ssl: true)
|
||||
end
|
||||
|
||||
def create
|
||||
@mail_identity = MailIdentity.new(identity_params)
|
||||
if @mail_identity.save
|
||||
redirect_to mail_identities_path, notice: "Account email creato. Da qui partono gli invii."
|
||||
else
|
||||
@page_title = "Nuovo account email"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica account email"
|
||||
end
|
||||
|
||||
def update
|
||||
attrs = identity_params
|
||||
attrs.delete(:smtp_password) if attrs[:smtp_password].blank?
|
||||
if @mail_identity.update(attrs)
|
||||
redirect_to mail_identities_path, notice: "Account email aggiornato."
|
||||
else
|
||||
@page_title = "Modifica account email"
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @mail_identity.mailings.exists?
|
||||
redirect_to mail_identities_path, alert: "Non puoi eliminare un account già usato in un invio. Disattivalo."
|
||||
return
|
||||
end
|
||||
|
||||
@mail_identity.destroy!
|
||||
redirect_to mail_identities_path, notice: "Account email eliminato."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_identity
|
||||
@mail_identity = MailIdentity.find(params[:id])
|
||||
end
|
||||
|
||||
def identity_params
|
||||
params.require(:mail_identity).permit(
|
||||
:name, :from_name, :from_email, :reply_to, :smtp_host, :smtp_port,
|
||||
:smtp_username, :smtp_password, :smtp_authentication, :encryption,
|
||||
:verify_ssl, :active
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class MailImagesController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
MAX_BYTES = 5.megabytes
|
||||
ALLOWED_TYPES = %w[image/jpeg image/png image/gif image/webp].freeze
|
||||
|
||||
def create
|
||||
file = params[:file]
|
||||
unless file.respond_to?(:content_type)
|
||||
return render json: { error: "Seleziona un'immagine." }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
content_type = Marcel::MimeType.for(file, name: file.original_filename, declared_type: file.content_type)
|
||||
unless ALLOWED_TYPES.include?(content_type)
|
||||
return render json: { error: "Formato non valido. Usa JPG, PNG, GIF o WebP." }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if file.size > MAX_BYTES
|
||||
return render json: { error: "L'immagine supera i 5 MB." }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: file,
|
||||
filename: file.original_filename.presence || "immagine",
|
||||
content_type: content_type
|
||||
)
|
||||
|
||||
render json: { url: url_for(blob) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
class MailTemplatesController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
before_action :set_template, only: %i[edit update destroy]
|
||||
|
||||
def index
|
||||
@page_title = "Template email"
|
||||
@mail_templates = MailTemplate.for_project(current_project)
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuovo template"
|
||||
@mail_template = MailTemplate.new(project: current_project, body_html: default_html, subject: "MatchLiveTV per {{societa}}")
|
||||
end
|
||||
|
||||
def create
|
||||
@mail_template = MailTemplate.new(template_params)
|
||||
@mail_template.project ||= current_project
|
||||
if @mail_template.save
|
||||
redirect_to mail_templates_path, notice: "Template salvato."
|
||||
else
|
||||
@page_title = "Nuovo template"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica template"
|
||||
end
|
||||
|
||||
def update
|
||||
if @mail_template.update(template_params)
|
||||
redirect_to mail_templates_path, notice: "Template aggiornato."
|
||||
else
|
||||
@page_title = "Modifica template"
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @mail_template.mailings.exists? || @mail_template.mailings_as_b.exists?
|
||||
redirect_to mail_templates_path, alert: "Template già usato in un invio: non si elimina, puoi solo modificarlo."
|
||||
return
|
||||
end
|
||||
|
||||
@mail_template.destroy!
|
||||
redirect_to mail_templates_path, notice: "Template eliminato."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_template
|
||||
@mail_template = MailTemplate.for_project(current_project).find(params[:id])
|
||||
end
|
||||
|
||||
def template_params
|
||||
params.require(:mail_template).permit(:name, :subject, :body_html)
|
||||
end
|
||||
|
||||
def default_html
|
||||
<<~HTML
|
||||
<p>Ciao {{contatto_nome}},</p>
|
||||
<p>scriviamo a <strong>{{societa}}</strong> ({{regione}}) per presentarti MatchLiveTV.</p>
|
||||
<p>Possiamo aiutarvi a trasmettere le giovanili in modo semplice.</p>
|
||||
<p>A presto,<br>Il team MatchLiveTV</p>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
class MailingsController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
before_action :set_mailing, only: %i[show edit update destroy queue test_send refresh_recipients update_recipients preview]
|
||||
before_action :load_form_collections, only: %i[new create edit update]
|
||||
|
||||
def index
|
||||
@page_title = "Email"
|
||||
@mailings = Mailing.for_project(current_project).includes(:mail_identity).recent
|
||||
@mail_identities_count = MailIdentity.active.count
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuovo invio"
|
||||
template = MailTemplate.for_project(current_project).order(:name).first
|
||||
identity = MailIdentity.active.ordered.first
|
||||
@mailing = Mailing.new(
|
||||
project: current_project,
|
||||
mail_template: template,
|
||||
mail_identity: identity,
|
||||
audience: "to_send",
|
||||
interval_seconds: 0,
|
||||
ab_assignment: "from_record",
|
||||
name: "Invio #{l(Time.zone.today)}",
|
||||
subject: template&.subject,
|
||||
body_html: template&.body_html
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@mailing = Mailing.new(mailing_params)
|
||||
@mailing.project = current_project
|
||||
apply_template_if_needed
|
||||
if @mailing.save
|
||||
@mailing.rebuild_recipients!
|
||||
redirect_to @mailing, notice: "Bozza creata. Controlla i destinatari e poi invia."
|
||||
else
|
||||
@page_title = "Nuovo invio"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@page_title = @mailing.name
|
||||
@recipients = @mailing.mailing_recipients.includes(:organization, :contact).ordered
|
||||
@preview_recipient = preview_recipient
|
||||
@preview_variant = preview_variant
|
||||
context = @preview_recipient&.merge_context || { project: current_project, ab_variant: @preview_variant }
|
||||
@preview_html = MailMerge.render(@mailing.body_for(@preview_variant), **context)
|
||||
@preview_subject = MailMerge.render(@mailing.subject_for(@preview_variant), **context)
|
||||
end
|
||||
|
||||
def edit
|
||||
unless @mailing.editable?
|
||||
redirect_to @mailing, alert: "Questo invio non è più modificabile."
|
||||
return
|
||||
end
|
||||
@page_title = "Modifica invio"
|
||||
end
|
||||
|
||||
def update
|
||||
unless @mailing.editable?
|
||||
redirect_to @mailing, alert: "Questo invio non è più modificabile."
|
||||
return
|
||||
end
|
||||
|
||||
@mailing.assign_attributes(mailing_params)
|
||||
apply_template_if_needed
|
||||
if @mailing.save
|
||||
rebuild_recipients_if_needed
|
||||
redirect_to @mailing, notice: "Invio aggiornato."
|
||||
else
|
||||
@page_title = "Modifica invio"
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @mailing.draft?
|
||||
redirect_to mailings_path, alert: "Puoi eliminare solo le bozze."
|
||||
return
|
||||
end
|
||||
|
||||
@mailing.destroy!
|
||||
redirect_to mailings_path, notice: "Invio eliminato."
|
||||
end
|
||||
|
||||
def refresh_recipients
|
||||
unless @mailing.editable?
|
||||
redirect_to @mailing, alert: "Destinatari bloccati: l'invio è già partito."
|
||||
return
|
||||
end
|
||||
|
||||
@mailing.rebuild_recipients!
|
||||
redirect_to @mailing, notice: "Lista destinatari aggiornata."
|
||||
end
|
||||
|
||||
def update_recipients
|
||||
unless @mailing.editable?
|
||||
redirect_to @mailing, alert: "Non puoi più cambiare i destinatari."
|
||||
return
|
||||
end
|
||||
|
||||
selected = Array(params[:pending_ids]).map(&:to_i)
|
||||
@mailing.mailing_recipients.where.not(status: %w[sent queued]).find_each do |recipient|
|
||||
if selected.include?(recipient.id) && recipient.email_ok?
|
||||
recipient.update!(status: "pending", skip_reason: nil)
|
||||
else
|
||||
reason = recipient.email_ok? ? "escluso dal check" : (recipient.skip_reason.presence || "email non valida")
|
||||
recipient.update!(status: "skipped", skip_reason: reason)
|
||||
end
|
||||
end
|
||||
redirect_to @mailing, notice: "Selezione destinatari salvata."
|
||||
end
|
||||
|
||||
def queue
|
||||
unless @mailing.draft? || @mailing.sending?
|
||||
redirect_to @mailing, alert: "Questo invio è già stato concluso."
|
||||
return
|
||||
end
|
||||
|
||||
@mailing.queue_send!
|
||||
redirect_to @mailing, notice: "Invio avviato. Le email partono in background#{@mailing.interval_seconds.positive? ? " con pausa di #{@mailing.interval_seconds}s" : ""}."
|
||||
rescue StandardError => e
|
||||
redirect_to @mailing, alert: e.message
|
||||
end
|
||||
|
||||
def test_send
|
||||
recipient = preview_recipient
|
||||
unless recipient&.email_ok?
|
||||
redirect_to @mailing, alert: "Nessun destinatario valido da usare come dati di prova."
|
||||
return
|
||||
end
|
||||
|
||||
variant = preview_variant
|
||||
html = MailMerge.render(@mailing.body_for(variant), **recipient.merge_context.merge(ab_variant: variant))
|
||||
subject = "[TEST#{@mailing.ab_test? ? " #{variant}" : ""}] #{MailMerge.render(@mailing.subject_for(variant), **recipient.merge_context.merge(ab_variant: variant))}"
|
||||
test_recipient = recipient.dup
|
||||
test_recipient.email = current_user.email
|
||||
CampaignMailer.outreach(test_recipient, html: html, subject: subject).deliver_now
|
||||
redirect_to mailing_path(@mailing, recipient_id: recipient.id, variant: variant), notice: "Email di prova (#{@mailing.ab_test? ? "variante #{variant}" : "unica"}) inviata a #{current_user.email}."
|
||||
rescue StandardError => e
|
||||
redirect_to @mailing, alert: "Invio di prova non riuscito: #{e.message}"
|
||||
end
|
||||
|
||||
def preview
|
||||
redirect_to mailing_path(@mailing, recipient_id: params[:recipient_id])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_mailing
|
||||
@mailing = Mailing.for_project(current_project).find(params[:id])
|
||||
end
|
||||
|
||||
def load_form_collections
|
||||
@mail_identities = MailIdentity.active.ordered
|
||||
@mail_templates = MailTemplate.for_project(current_project)
|
||||
end
|
||||
|
||||
def mailing_params
|
||||
params.require(:mailing).permit(
|
||||
:name, :mail_identity_id, :mail_template_id, :mail_template_b_id, :audience,
|
||||
:subject, :body_html, :subject_b, :body_html_b, :ab_test, :ab_assignment,
|
||||
:interval_seconds, files: []
|
||||
)
|
||||
end
|
||||
|
||||
def apply_template_if_needed
|
||||
copy_template(params.dig(:mailing, :mail_template_id), :subject, :body_html, params[:use_template_content])
|
||||
copy_template(params.dig(:mailing, :mail_template_b_id), :subject_b, :body_html_b, params[:use_template_b_content])
|
||||
end
|
||||
|
||||
def copy_template(template_id, subject_attr, body_attr, flag)
|
||||
return if template_id.blank?
|
||||
return unless ActiveModel::Type::Boolean.new.cast(flag)
|
||||
|
||||
template = MailTemplate.for_project(current_project).find_by(id: template_id)
|
||||
return unless template
|
||||
|
||||
@mailing.public_send("#{subject_attr}=", template.subject)
|
||||
@mailing.public_send("#{body_attr}=", template.body_html)
|
||||
end
|
||||
|
||||
def rebuild_recipients_if_needed
|
||||
return unless @mailing.editable?
|
||||
return unless @mailing.saved_change_to_audience? || @mailing.saved_change_to_ab_test? || @mailing.saved_change_to_ab_assignment?
|
||||
|
||||
@mailing.rebuild_recipients!
|
||||
end
|
||||
|
||||
def preview_recipient
|
||||
if params[:recipient_id].present?
|
||||
@mailing.mailing_recipients.find_by(id: params[:recipient_id])
|
||||
else
|
||||
@mailing.mailing_recipients.pending.ordered.first || @mailing.mailing_recipients.ordered.first
|
||||
end
|
||||
end
|
||||
|
||||
def preview_variant
|
||||
requested = params[:variant].to_s.upcase
|
||||
return requested if requested.in?(%w[A B])
|
||||
return "A" unless @mailing.ab_test?
|
||||
|
||||
preview_recipient&.ab_variant.presence_in(%w[A B]) || "A"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
class OpportunitiesController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
before_action :set_opportunity, only: %i[show edit update destroy update_stage]
|
||||
before_action :load_form_data, only: %i[new create edit update]
|
||||
|
||||
def index
|
||||
@page_title = "Opportunità"
|
||||
scope = opportunities_for_current_project.includes(:organization, :assigned_user, :project).order(updated_at: :desc)
|
||||
scope = scope.where(pipeline_stage: params[:pipeline_stage]) if params[:pipeline_stage].present?
|
||||
scope = scope.where(assigned_user_id: params[:owner]) if params[:owner].present?
|
||||
@pagy, @opportunities = pagy(scope, items: 30)
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.csv { send_data CsvExport.opportunities(scope), filename: "opportunities-#{Date.current}.csv" }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
redirect_to @opportunity.organization
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuova opportunità"
|
||||
@opportunity = Opportunity.new(
|
||||
organization_id: params[:organization_id],
|
||||
project: current_project,
|
||||
assigned_user: current_user,
|
||||
pipeline_stage: "to_contact",
|
||||
probability: 5
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@opportunity = Opportunity.new(opportunity_params)
|
||||
@opportunity.project ||= current_project
|
||||
if @opportunity.save
|
||||
redirect_to @opportunity.organization, notice: "Opportunità creata."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica opportunità"
|
||||
end
|
||||
|
||||
def update
|
||||
if @opportunity.update(opportunity_params)
|
||||
redirect_to @opportunity.organization, notice: "Opportunità aggiornata."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
org = @opportunity.organization
|
||||
@opportunity.destroy!
|
||||
redirect_to org, notice: "Opportunità eliminata."
|
||||
end
|
||||
|
||||
def update_stage
|
||||
stage_params = params.permit(:pipeline_stage, :lost_reason, :notes)
|
||||
new_stage = stage_params[:pipeline_stage].to_s
|
||||
unless Catalog::PIPELINE_STAGES.key?(new_stage)
|
||||
redirect_back fallback_location: pipeline_path, alert: "Stage non valido." and return
|
||||
end
|
||||
|
||||
begin
|
||||
@opportunity.move_to_stage!(
|
||||
new_stage,
|
||||
lost_reason: stage_params[:lost_reason],
|
||||
notes: stage_params[:notes],
|
||||
user: current_user
|
||||
)
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.replace("flash", partial: "shared/flash", locals: { notice: "Stage aggiornato." }) }
|
||||
format.html { redirect_back fallback_location: pipeline_path, notice: "Stage aggiornato." }
|
||||
format.json { render json: { ok: true, stage: @opportunity.pipeline_stage } }
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
respond_to do |format|
|
||||
format.html { redirect_back fallback_location: pipeline_path, alert: e.record.errors.full_messages.to_sentence }
|
||||
format.json { render json: { ok: false, errors: e.record.errors.full_messages }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_opportunity
|
||||
@opportunity = Opportunity.find(params[:id])
|
||||
end
|
||||
|
||||
def load_form_data
|
||||
@organizations = organizations_for_current_project.order(:name)
|
||||
@users = User.active.order(:first_name)
|
||||
@products = Product.active.ordered
|
||||
@projects = available_projects
|
||||
end
|
||||
|
||||
def opportunity_params
|
||||
params.require(:opportunity).permit(
|
||||
:organization_id, :project_id, :name, :pipeline_stage, :estimated_value, :probability,
|
||||
:expected_close_date, :product, :assigned_user_id, :lost_reason, :notes,
|
||||
:ab_variant, :send_status, :sent_on, :outcome, :demo_trial, :converted
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
class OrganizationsController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
before_action :set_organization, only: %i[show edit update destroy]
|
||||
before_action :load_form_collections, only: %i[new create edit update]
|
||||
|
||||
def index
|
||||
@page_title = "Organizzazioni"
|
||||
scope = organizations_for_current_project.includes(:assigned_user, :contacts, :opportunities, :tasks, :projects)
|
||||
scope = scope.search(params[:q]) if params[:q].present?
|
||||
scope = apply_filters(scope)
|
||||
scope = apply_sort(scope)
|
||||
@pagy, @organizations = pagy(scope, items: 50)
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.csv { send_data CsvExport.organizations(scope), filename: "organizations-#{Date.current}.csv" }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
authorize_organization!(@organization)
|
||||
@page_title = @organization.name
|
||||
@contacts = @organization.contacts.primary_first
|
||||
@opportunities = @organization.opportunities.for_project(current_project).includes(:assigned_user, :project).order(updated_at: :desc)
|
||||
@pending_tasks = @organization.tasks.pending.ordered
|
||||
@completed_tasks = @organization.tasks.completed.order(completed_at: :desc).limit(10)
|
||||
@activities = @organization.activities.includes(:user, :contact, :opportunity).recent_first
|
||||
@next_task = @organization.next_pending_task
|
||||
@activity = @organization.activities.build(happened_at: Time.current, user: current_user)
|
||||
@users = User.active.order(:first_name)
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuova organizzazione"
|
||||
@organization = Organization.new(assigned_user: current_user, country: "Italia", status: "prospect")
|
||||
@organization.project_ids = [current_project.id] if current_project
|
||||
end
|
||||
|
||||
def create
|
||||
@organization = Organization.new(organization_params)
|
||||
if @organization.save
|
||||
redirect_to @organization, notice: "Organizzazione creata."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize_organization!(@organization)
|
||||
@page_title = "Modifica #{@organization.name}"
|
||||
end
|
||||
|
||||
def update
|
||||
authorize_organization!(@organization)
|
||||
if @organization.update(organization_params)
|
||||
redirect_to @organization, notice: "Organizzazione aggiornata."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize_organization!(@organization)
|
||||
@organization.destroy!
|
||||
redirect_to organizations_path, notice: "Organizzazione eliminata."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_organization
|
||||
@organization = Organization.find(params[:id])
|
||||
end
|
||||
|
||||
def authorize_organization!(org)
|
||||
return if current_user.admin?
|
||||
return if org.projects.merge(available_projects).exists?
|
||||
|
||||
redirect_to organizations_path, alert: "Organizzazione non disponibile per i tuoi progetti." and return
|
||||
end
|
||||
|
||||
def load_form_collections
|
||||
@users = User.active.order(:first_name)
|
||||
@projects = available_projects
|
||||
end
|
||||
|
||||
def organization_params
|
||||
params.require(:organization).permit(
|
||||
:name, :legal_name, :organization_type, :sport, :country, :region, :province, :city,
|
||||
:address, :website, :source_url, :phone, :email, :vat_number, :notes, :status, :lead_source,
|
||||
:assigned_user_id, :list_position, :team_gender, :streaming_status, :commercial_fit, :verified_at,
|
||||
project_ids: []
|
||||
)
|
||||
end
|
||||
|
||||
def apply_filters(scope)
|
||||
scope = scope.where(status: params[:status]) if params[:status].present?
|
||||
scope = scope.where(lead_source: params[:lead_source]) if params[:lead_source].present?
|
||||
scope = scope.where(organization_type: params[:organization_type]) if params[:organization_type].present?
|
||||
scope = scope.where(sport: params[:sport]) if params[:sport].present?
|
||||
scope = scope.where(country: params[:country]) if params[:country].present?
|
||||
scope = scope.where(region: params[:region]) if params[:region].present?
|
||||
scope = scope.where(assigned_user_id: params[:owner]) if params[:owner].present?
|
||||
scope = scope.where(team_gender: params[:team_gender]) if params[:team_gender].present?
|
||||
scope = scope.where(streaming_status: params[:streaming_status]) if params[:streaming_status].present?
|
||||
|
||||
if params[:customer] == "yes"
|
||||
scope = scope.customers
|
||||
elsif params[:customer] == "no"
|
||||
scope = scope.where.not(status: %w[active_customer inactive_customer])
|
||||
end
|
||||
|
||||
if params[:pipeline_stage].present?
|
||||
scope = scope.joins(:opportunities).where(opportunities: { pipeline_stage: params[:pipeline_stage], project_id: current_project.id }).distinct
|
||||
end
|
||||
|
||||
if params[:ab_variant].present?
|
||||
scope = scope.joins(:opportunities).where(opportunities: { ab_variant: params[:ab_variant], project_id: current_project.id }).distinct
|
||||
end
|
||||
|
||||
if params[:overdue_tasks] == "1"
|
||||
scope = scope.joins(:tasks).merge(Task.overdue).distinct
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
def apply_sort(scope)
|
||||
case params[:sort]
|
||||
when "name"
|
||||
scope.order(:name)
|
||||
when "updated"
|
||||
scope.order(updated_at: :desc)
|
||||
when "created"
|
||||
scope.order(created_at: :desc)
|
||||
when "lista"
|
||||
scope.order(Arel.sql("organizations.list_position ASC NULLS LAST, organizations.name ASC"))
|
||||
else
|
||||
if current_project&.code == "matchlivetv"
|
||||
scope.order(Arel.sql("organizations.list_position ASC NULLS LAST, organizations.name ASC"))
|
||||
else
|
||||
scope.order(updated_at: :desc)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
class PasswordResetsController < ApplicationController
|
||||
skip_before_action :require_login
|
||||
|
||||
def new; end
|
||||
|
||||
def create
|
||||
user = User.active.find_by(email: params[:email].to_s.downcase.strip)
|
||||
if user
|
||||
user.generate_password_reset_token!
|
||||
PasswordMailer.reset(user).deliver_later
|
||||
end
|
||||
redirect_to login_path, notice: "Se l'email esiste, riceverai le istruzioni per il reset."
|
||||
end
|
||||
|
||||
def edit
|
||||
@user = User.find_by(password_reset_token: params[:token])
|
||||
return if @user&.password_reset_token_valid?
|
||||
|
||||
redirect_to new_password_reset_path, alert: "Link di reset non valido o scaduto."
|
||||
end
|
||||
|
||||
def update
|
||||
@user = User.find_by(password_reset_token: params[:token])
|
||||
unless @user&.password_reset_token_valid?
|
||||
redirect_to new_password_reset_path, alert: "Link di reset non valido o scaduto." and return
|
||||
end
|
||||
|
||||
if @user.update(password_params)
|
||||
@user.clear_password_reset_token!
|
||||
redirect_to login_path, notice: "Password aggiornata. Ora puoi accedere."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class PasswordsController < ApplicationController
|
||||
def edit
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def update
|
||||
@user = current_user
|
||||
if @user.authenticate(params[:current_password]) && @user.update(password_params)
|
||||
redirect_to root_path, notice: "Password aggiornata."
|
||||
else
|
||||
flash.now[:alert] = "Impossibile aggiornare la password. Verifica i dati inseriti."
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class PipelineController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
def show
|
||||
@page_title = "Pipeline"
|
||||
@opportunities_by_stage = Catalog::PIPELINE_ORDER.index_with do |stage|
|
||||
opportunities_for_current_project.where(pipeline_stage: stage)
|
||||
.includes(:organization, :assigned_user, :tasks, organization: :contacts)
|
||||
.order(updated_at: :desc)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
class ProjectsController < ApplicationController
|
||||
before_action :require_admin
|
||||
before_action :set_project, only: %i[edit update destroy]
|
||||
skip_before_action :set_current_project, only: %i[create], raise: false
|
||||
|
||||
def create
|
||||
@project = Project.new(project_params)
|
||||
if @project.save
|
||||
redirect_to settings_path(project_code: @project.code), notice: "Progetto creato. Sei nell'istanza #{@project.name}."
|
||||
else
|
||||
redirect_to root_path(new_project: 1), alert: @project.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica progetto"
|
||||
end
|
||||
|
||||
def update
|
||||
if @project.update(project_params)
|
||||
redirect_to(current_project ? settings_path : root_path, notice: "Progetto aggiornato.")
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @project.opportunities.exists?
|
||||
redirect_to(current_project ? settings_path : root_path, alert: "Impossibile eliminare: ci sono opportunità collegate.")
|
||||
else
|
||||
@project.destroy!
|
||||
redirect_to root_path, notice: "Progetto eliminato."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_project
|
||||
@project = Project.find(params[:id])
|
||||
end
|
||||
|
||||
def project_params
|
||||
params.require(:project).permit(:name, :code, :description, :active, :position, :color)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class ReportsController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
def index
|
||||
@page_title = "Report"
|
||||
@reports = Reports::Builder.new(project: current_project)
|
||||
@funnel = @reports.funnel
|
||||
@lead_sources = @reports.lead_sources
|
||||
@won_lost = @reports.won_lost_by_month
|
||||
@lost_reasons = @reports.lost_reasons
|
||||
@sales_owners = @reports.sales_owners
|
||||
@revenue = @reports.revenue_by_month
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
class SearchController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
def show
|
||||
@page_title = "Ricerca"
|
||||
@query = params[:q].to_s.strip
|
||||
if @query.present?
|
||||
@organizations = organizations_for_current_project.search(@query).includes(:assigned_user).limit(20)
|
||||
@contacts = Contact.joins(:organization).merge(organizations_for_current_project).search(@query).includes(:organization).limit(20)
|
||||
@opportunities = opportunities_for_current_project.joins(:organization)
|
||||
.where("opportunities.name ILIKE :q OR organizations.name ILIKE :q", q: "%#{ActiveRecord::Base.sanitize_sql_like(@query)}%")
|
||||
.includes(:organization)
|
||||
.limit(20)
|
||||
else
|
||||
@organizations = []
|
||||
@contacts = []
|
||||
@opportunities = []
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
class SessionsController < ApplicationController
|
||||
skip_before_action :require_login, only: %i[new create]
|
||||
skip_before_action :set_current_project, only: %i[new create], raise: false
|
||||
|
||||
def new
|
||||
redirect_to after_login_path if logged_in?
|
||||
end
|
||||
|
||||
def create
|
||||
user = User.active.find_by(email: params[:email].to_s.downcase.strip)
|
||||
|
||||
if user&.authenticate(params[:password])
|
||||
login_as(user)
|
||||
redirect_to after_login_path, notice: "Bentornato, #{user.first_name}!"
|
||||
else
|
||||
flash.now[:alert] = "Email o password non validi."
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
logout
|
||||
redirect_to login_path, notice: "Disconnesso correttamente."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_login_path
|
||||
projects = if current_user.admin?
|
||||
Project.active.ordered
|
||||
else
|
||||
current_user.accessible_projects
|
||||
end
|
||||
|
||||
if projects.one?
|
||||
project_root_path(project_code: projects.first.code)
|
||||
elsif (code = session[:last_project_code]) && projects.exists?(code: code)
|
||||
project_root_path(project_code: code)
|
||||
else
|
||||
root_path
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
class SettingsController < ApplicationController
|
||||
def show
|
||||
@page_title = "Impostazioni"
|
||||
@goal = SalesGoal.current(current_project).order(created_at: :desc).first || SalesGoal.new(project: current_project)
|
||||
@goals = SalesGoal.for_project(current_project).order(start_date: :desc)
|
||||
@products = Product.ordered
|
||||
@projects = Project.ordered
|
||||
end
|
||||
|
||||
def update_goal
|
||||
@goal = params[:id].present? ? SalesGoal.find(params[:id]) : SalesGoal.new
|
||||
attrs = goal_params.merge(active: true)
|
||||
attrs[:project_id] ||= current_project&.id
|
||||
if @goal.update(attrs)
|
||||
if params[:make_current] == "1" && @goal.project_id.present?
|
||||
SalesGoal.where(project_id: @goal.project_id).where.not(id: @goal.id).update_all(active: false)
|
||||
end
|
||||
redirect_to settings_path, notice: "Obiettivo aggiornato."
|
||||
else
|
||||
redirect_to settings_path, alert: @goal.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def goal_params
|
||||
params.require(:sales_goal).permit(:name, :metric, :target_value, :start_date, :end_date, :active, :project_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
class TasksController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
before_action :set_task, only: %i[show edit update destroy complete]
|
||||
before_action :load_form_data, only: %i[new create edit update]
|
||||
|
||||
def index
|
||||
@page_title = "Task"
|
||||
tasks = tasks_for_current_project
|
||||
@overdue_tasks = tasks.overdue.includes(:organization, :contact, :assigned_user).ordered
|
||||
@today_tasks = tasks.due_today.includes(:organization, :contact, :assigned_user).ordered
|
||||
@upcoming_tasks = tasks.upcoming(30).includes(:organization, :contact, :assigned_user).ordered
|
||||
@completed_tasks = tasks.completed.includes(:organization).order(completed_at: :desc).limit(20)
|
||||
end
|
||||
|
||||
def show
|
||||
redirect_to @task.organization
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuovo task"
|
||||
@task = Task.new(
|
||||
organization_id: params[:organization_id],
|
||||
opportunity_id: params[:opportunity_id],
|
||||
contact_id: params[:contact_id],
|
||||
assigned_user: current_user,
|
||||
due_at: 1.day.from_now.change(hour: 10),
|
||||
priority: "normal",
|
||||
task_type: params[:task_type].presence || "follow_up"
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@task = Task.new(task_params)
|
||||
if @task.save
|
||||
redirect_to(@task.organization || tasks_path, notice: "Task creato.")
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica task"
|
||||
end
|
||||
|
||||
def update
|
||||
if @task.update(task_params)
|
||||
redirect_to @task.organization, notice: "Task aggiornato."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
org = @task.organization
|
||||
@task.destroy!
|
||||
redirect_to org, notice: "Task eliminato."
|
||||
end
|
||||
|
||||
def complete
|
||||
if @task.complete!(user: current_user)
|
||||
redirect_back fallback_location: today_path, notice: "Task completato."
|
||||
else
|
||||
redirect_back fallback_location: today_path, alert: "Il task non può essere completato."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_task
|
||||
@task = Task.find(params[:id])
|
||||
end
|
||||
|
||||
def load_form_data
|
||||
@organizations = organizations_for_current_project.order(:name)
|
||||
@users = User.active.order(:first_name)
|
||||
end
|
||||
|
||||
def task_params
|
||||
params.require(:task).permit(
|
||||
:title, :description, :organization_id, :contact_id, :opportunity_id,
|
||||
:assigned_user_id, :due_at, :priority, :task_type, :status
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
class TodayController < ApplicationController
|
||||
before_action :require_current_project!
|
||||
|
||||
def show
|
||||
@page_title = "Oggi"
|
||||
tasks = tasks_for_current_project
|
||||
@overdue_tasks = tasks.overdue.includes(:organization, :contact, :opportunity, :assigned_user).ordered
|
||||
@today_tasks = tasks.due_today.includes(:organization, :contact, :opportunity, :assigned_user).ordered
|
||||
@upcoming_tasks = tasks.upcoming.includes(:organization, :contact, :opportunity, :assigned_user).ordered
|
||||
@stalled = opportunities_for_current_project.open_stage
|
||||
.where("stage_changed_at < ? OR (stage_changed_at IS NULL AND opportunities.created_at < ?)", 7.days.ago, 7.days.ago)
|
||||
.includes(:organization, :assigned_user, :tasks)
|
||||
@new_prospects = organizations_for_current_project.prospects
|
||||
.left_joins(:opportunities)
|
||||
.where("opportunities.id IS NULL OR (opportunities.project_id = ? AND opportunities.pipeline_stage = ?)", current_project.id, "to_contact")
|
||||
.includes(:contacts, :assigned_user, :opportunities)
|
||||
.distinct
|
||||
.limit(20)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
class UsersController < ApplicationController
|
||||
before_action :require_admin
|
||||
before_action :set_user, only: %i[edit update destroy]
|
||||
before_action :load_projects, only: %i[new create edit update]
|
||||
|
||||
def index
|
||||
@page_title = "Utenti"
|
||||
@users = User.includes(user_projects: :project).order(:first_name, :last_name)
|
||||
end
|
||||
|
||||
def new
|
||||
@page_title = "Nuovo utente"
|
||||
@user = User.new(role: "user", active: true)
|
||||
end
|
||||
|
||||
def create
|
||||
@page_title = "Nuovo utente"
|
||||
@user = User.new(user_params)
|
||||
if @user.save
|
||||
sync_user_projects!(@user, params[:project_ids]) unless @user.admin?
|
||||
redirect_to users_path, notice: "Utente creato."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_title = "Modifica utente"
|
||||
end
|
||||
|
||||
def update
|
||||
@page_title = "Modifica utente"
|
||||
attrs = user_params
|
||||
attrs.delete(:password) if attrs[:password].blank?
|
||||
attrs.delete(:password_confirmation) if attrs[:password].blank?
|
||||
attrs[:active] = ActiveModel::Type::Boolean.new.cast(attrs[:active]) if attrs.key?(:active)
|
||||
|
||||
if @user.update(attrs)
|
||||
sync_user_projects!(@user, params[:project_ids]) if !@user.admin? && params.key?(:project_ids)
|
||||
notice = if @user.saved_change_to_active?
|
||||
@user.active? ? "#{@user.full_name} riabilitato." : "#{@user.full_name} disabilitato."
|
||||
else
|
||||
"Utente aggiornato."
|
||||
end
|
||||
redirect_to users_path, notice: notice
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @user == current_user
|
||||
redirect_to users_path, alert: "Non puoi eliminare il tuo account." and return
|
||||
end
|
||||
|
||||
unless @user.can_be_destroyed?
|
||||
redirect_to users_path, alert: "Non puoi eliminare l'unico amministratore attivo." and return
|
||||
end
|
||||
|
||||
name = @user.full_name
|
||||
@user.destroy!
|
||||
redirect_to users_path, notice: "#{name} eliminato."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def load_projects
|
||||
@projects = Project.ordered
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :first_name, :last_name, :role, :password, :password_confirmation, :active)
|
||||
end
|
||||
|
||||
def sync_user_projects!(user, selected_ids)
|
||||
selected_ids = Array(selected_ids).map(&:presence).compact.map(&:to_i)
|
||||
Project.find_each do |project|
|
||||
up = user.user_projects.find_or_initialize_by(project: project)
|
||||
up.enabled = selected_ids.include?(project.id)
|
||||
up.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user