Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
CI / scan_ruby (push) Failing after 11m20s
CI / scan_js (push) Successful in 10m35s
CI / lint (push) Has been cancelled

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:
2026-08-17 23:01:48 +02:00
co-authored by Cursor
commit c4e5f289cf
258 changed files with 11293 additions and 0 deletions
@@ -0,0 +1,164 @@
class CreateSimplecrmSchema < ActiveRecord::Migration[8.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.string :first_name, null: false
t.string :last_name, null: false
t.string :role, null: false, default: "user"
t.boolean :active, null: false, default: true
t.string :password_reset_token
t.datetime :password_reset_sent_at
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :password_reset_token, unique: true
add_index :users, :role
create_table :products do |t|
t.string :name, null: false
t.string :code, null: false
t.boolean :active, null: false, default: true
t.integer :position, null: false, default: 0
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :products, :code, unique: true
add_index :products, :position
create_table :organizations do |t|
t.string :name, null: false
t.string :legal_name
t.string :organization_type, null: false, default: "altro"
t.string :sport
t.string :country, null: false, default: "Italia"
t.string :region
t.string :province
t.string :city
t.string :address
t.string :website
t.string :phone
t.string :email
t.string :vat_number
t.text :notes
t.string :status, null: false, default: "prospect"
t.string :lead_source
t.references :assigned_user, foreign_key: { to_table: :users }
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :organizations, :status
add_index :organizations, :lead_source
add_index :organizations, :organization_type
add_index :organizations, :email
add_index :organizations, :name
add_index :organizations, :city
add_index :organizations, :website
add_index :organizations, :sport
add_index :organizations, :country
add_index :organizations, :region
create_table :contacts do |t|
t.references :organization, null: false, foreign_key: true
t.string :first_name, null: false
t.string :last_name, null: false
t.string :role
t.string :email
t.string :phone
t.string :mobile
t.string :preferred_contact_method, default: "email"
t.text :notes
t.boolean :primary_contact, null: false, default: false
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :contacts, :email
add_index :contacts, :phone
add_index :contacts, :primary_contact
add_index :contacts, [:last_name, :first_name]
create_table :opportunities do |t|
t.references :organization, null: false, foreign_key: true
t.string :name, null: false
t.string :pipeline_stage, null: false, default: "to_contact"
t.decimal :estimated_value, precision: 12, scale: 2
t.integer :probability, default: 0
t.date :expected_close_date
t.string :product
t.references :assigned_user, foreign_key: { to_table: :users }
t.string :lost_reason
t.text :notes
t.datetime :won_at
t.datetime :lost_at
t.datetime :stage_changed_at
t.datetime :first_contacted_at
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :opportunities, :pipeline_stage
add_index :opportunities, :lost_reason
add_index :opportunities, :product
add_index :opportunities, :expected_close_date
add_index :opportunities, :stage_changed_at
create_table :activities do |t|
t.string :activity_type, null: false
t.string :subject, null: false
t.text :description
t.datetime :happened_at, null: false
t.references :user, null: false, foreign_key: true
t.references :organization, null: false, foreign_key: true
t.references :contact, foreign_key: true
t.references :opportunity, foreign_key: true
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :activities, :activity_type
add_index :activities, :happened_at
add_index :activities, [:organization_id, :happened_at]
create_table :tasks do |t|
t.string :title, null: false
t.text :description
t.references :organization, null: false, foreign_key: true
t.references :contact, foreign_key: true
t.references :opportunity, foreign_key: true
t.references :assigned_user, foreign_key: { to_table: :users }
t.datetime :due_at, null: false
t.datetime :completed_at
t.string :priority, null: false, default: "normal"
t.string :task_type, null: false, default: "generic"
t.string :status, null: false, default: "pending"
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :tasks, :due_at
add_index :tasks, :status
add_index :tasks, :priority
add_index :tasks, :task_type
add_index :tasks, [:status, :due_at]
create_table :sales_goals do |t|
t.string :name, null: false
t.string :metric, null: false
t.decimal :target_value, precision: 12, scale: 2, null: false
t.date :start_date, null: false
t.date :end_date, null: false
t.boolean :active, null: false, default: true
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :sales_goals, :metric
add_index :sales_goals, [:start_date, :end_date]
add_index :sales_goals, :active
end
end
+75
View File
@@ -0,0 +1,75 @@
class AddProjects < ActiveRecord::Migration[8.1]
def up
create_table :projects do |t|
t.string :name, null: false
t.string :code, null: false
t.text :description
t.boolean :active, null: false, default: true
t.integer :position, null: false, default: 0
t.string :color, default: "#0f172a"
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :projects, :code, unique: true
add_index :projects, :active
add_index :projects, :position
create_table :user_projects do |t|
t.references :user, null: false, foreign_key: true
t.references :project, null: false, foreign_key: true
t.boolean :enabled, null: false, default: true
t.timestamps
end
add_index :user_projects, [:user_id, :project_id], unique: true
add_index :user_projects, :enabled
create_table :organization_projects do |t|
t.references :organization, null: false, foreign_key: true
t.references :project, null: false, foreign_key: true
t.timestamps
end
add_index :organization_projects, [:organization_id, :project_id], unique: true, name: "idx_org_projects_unique"
add_reference :opportunities, :project, foreign_key: true
add_reference :sales_goals, :project, foreign_key: true
say_with_time "Backfill default projects" do
now = quote(Time.current)
execute <<~SQL.squish
INSERT INTO projects (name, code, description, active, position, color, created_at, updated_at) VALUES
('MatchLiveTV', 'matchlivetv', 'Streaming e diritti sportivi', TRUE, 0, '#0f172a', #{now}, #{now}),
('RiskMeter', 'riskmeter', 'Risk management', TRUE, 1, '#0369a1', #{now}, #{now}),
('Cardoo', 'cardoo', 'Cardoo', TRUE, 2, '#047857', #{now}, #{now})
SQL
project_id = select_value("SELECT id FROM projects WHERE code = 'matchlivetv'")
execute <<~SQL.squish
INSERT INTO organization_projects (organization_id, project_id, created_at, updated_at)
SELECT id, #{project_id}, #{now}, #{now} FROM organizations
ON CONFLICT (organization_id, project_id) DO NOTHING
SQL
execute "UPDATE opportunities SET project_id = #{project_id} WHERE project_id IS NULL"
execute "UPDATE sales_goals SET project_id = #{project_id} WHERE project_id IS NULL"
execute <<~SQL.squish
INSERT INTO user_projects (user_id, project_id, enabled, created_at, updated_at)
SELECT u.id, p.id, TRUE, #{now}, #{now}
FROM users u
CROSS JOIN projects p
WHERE u.role = 'user'
ON CONFLICT (user_id, project_id) DO NOTHING
SQL
end
end
def down
remove_reference :sales_goals, :project, foreign_key: true
remove_reference :opportunities, :project, foreign_key: true
drop_table :organization_projects
drop_table :user_projects
drop_table :projects
end
end
@@ -0,0 +1,5 @@
class AllowNullActivityUserId < ActiveRecord::Migration[8.1]
def change
change_column_null :activities, :user_id, true
end
end
@@ -0,0 +1,26 @@
class AddCampaignFieldsToOrganizationsAndOpportunities < ActiveRecord::Migration[8.1]
def change
change_table :organizations, bulk: true do |t|
t.integer :list_position
t.string :team_gender
t.string :streaming_status
t.text :commercial_fit
t.string :source_url
t.date :verified_at
end
add_index :organizations, :list_position
add_index :organizations, :team_gender
add_index :organizations, :streaming_status
change_table :opportunities, bulk: true do |t|
t.string :ab_variant
t.string :send_status
t.date :sent_on
t.string :outcome
t.boolean :demo_trial, default: false, null: false
t.boolean :converted, default: false, null: false
end
add_index :opportunities, :ab_variant
add_index :opportunities, :send_status
end
end
@@ -0,0 +1,57 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end
@@ -0,0 +1,79 @@
class CreateMailMarketing < ActiveRecord::Migration[8.1]
def change
create_table :mail_identities do |t|
t.string :name, null: false
t.string :from_name, null: false
t.string :from_email, null: false
t.string :reply_to
t.string :smtp_host, null: false
t.integer :smtp_port, null: false, default: 587
t.string :smtp_username
t.text :smtp_password
t.string :smtp_authentication, default: "plain", null: false
t.string :encryption, default: "starttls", null: false
t.boolean :verify_ssl, default: true, null: false
t.boolean :active, default: true, null: false
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :mail_identities, :from_email
add_index :mail_identities, :active
create_table :mail_templates do |t|
t.bigint :project_id
t.string :name, null: false
t.string :subject, null: false
t.text :body_html, null: false
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :mail_templates, :project_id
add_index :mail_templates, :name
create_table :mailings do |t|
t.bigint :project_id, null: false
t.bigint :mail_identity_id, null: false
t.bigint :mail_template_id
t.string :name, null: false
t.string :subject, null: false
t.text :body_html, null: false
t.string :audience, default: "to_send", null: false
t.string :status, default: "draft", null: false
t.integer :interval_seconds, default: 0, null: false
t.datetime :queued_at
t.datetime :completed_at
t.bigint :created_by_id
t.bigint :updated_by_id
t.timestamps
end
add_index :mailings, :project_id
add_index :mailings, :mail_identity_id
add_index :mailings, :status
create_table :mailing_recipients do |t|
t.bigint :mailing_id, null: false
t.bigint :organization_id, null: false
t.bigint :contact_id
t.string :email
t.string :status, default: "pending", null: false
t.string :skip_reason
t.text :error_message
t.string :rendered_subject
t.datetime :sent_at
t.timestamps
end
add_index :mailing_recipients, %i[mailing_id organization_id], unique: true, name: "idx_mailing_recipients_unique"
add_index :mailing_recipients, :status
add_index :mailing_recipients, :email
add_foreign_key :mail_templates, :projects
add_foreign_key :mailings, :projects
add_foreign_key :mailings, :mail_identities
add_foreign_key :mailings, :mail_templates
add_foreign_key :mailing_recipients, :mailings
add_foreign_key :mailing_recipients, :organizations
add_foreign_key :mailing_recipients, :contacts
end
end
@@ -0,0 +1,16 @@
class AddAbTestToMailings < ActiveRecord::Migration[8.1]
def change
change_table :mailings do |t|
t.boolean :ab_test, default: false, null: false
t.string :ab_assignment, default: "from_record", null: false
t.string :subject_b
t.text :body_html_b
t.bigint :mail_template_b_id
end
add_index :mailings, :ab_test
add_foreign_key :mailings, :mail_templates, column: :mail_template_b_id
add_column :mailing_recipients, :ab_variant, :string
add_index :mailing_recipients, :ab_variant
end
end