diff --git a/app/controllers/budgets_controller.rb b/app/controllers/budgets_controller.rb
new file mode 100644
index 0000000..db6621a
--- /dev/null
+++ b/app/controllers/budgets_controller.rb
@@ -0,0 +1,33 @@
+require 'csv'
+
+class BudgetsController < ApplicationController
+ skip_before_action :verify_authenticity_token
+
+ def process_budget
+ user = User.find(params[:user_id])
+ budget.process_budget
+
+ Turbo::StreamsChannel.broadcast_replace_to(
+ user,
+ target: "spend_forecast",
+ partial: "spend_forecasts/form",
+ locals: { user:, spend_forecast: budget.spend_forecast_from_budget}
+ )
+ end
+
+ private
+
+ def user
+ @user ||= User.find(params[:user_id])
+ end
+
+ def budget
+ @budget ||= Budget.new(
+ budget_contents: params[:budget_file_contents],
+ start_date: Date.parse(params[:start_date]),
+ end_date: Date.parse(params[:end_date]),
+ channel_daily_spend_limit: params[:channel_daily_spend_limit].to_i,
+ user:
+ )
+ end
+end
diff --git a/app/controllers/spend_forecasts_controller.rb b/app/controllers/spend_forecasts_controller.rb
index dac6059..4f71b49 100644
--- a/app/controllers/spend_forecasts_controller.rb
+++ b/app/controllers/spend_forecasts_controller.rb
@@ -24,6 +24,22 @@ def update
end
end
+ def update
+ if spend_forecast.update(spend_forecast_params)
+ redirect_to spend_forecast_path(spend_forecast)
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ def create
+ if spend_forecast.update(spend_forecast_params)
+ redirect_to spend_forecast_path(spend_forecast)
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
def show;end
def download_budget_csv
@@ -43,13 +59,6 @@ def set_spend_forecast
end
def spend_forecast_params
- temp_params = params.require(:spend_forecast).permit(:name, :start_date, :end_date)
- temp_params.merge!(budget: process_csv(params[:spend_forecast][:budget_file])) if params[:spend_forecast][:budget_file]
- temp_params
- end
-
- def process_csv(file)
- require 'csv'
- CSV.read(file.path, headers: true)
+ params.require(:spend_forecast).permit(:name, :start_date, :end_date, :channel_daily_spend_limit, :budget)
end
end
diff --git a/app/javascript/controllers/budget_file_controller.js b/app/javascript/controllers/budget_file_controller.js
new file mode 100644
index 0000000..f6049a2
--- /dev/null
+++ b/app/javascript/controllers/budget_file_controller.js
@@ -0,0 +1,43 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ["input", "form"];
+
+ connect() {
+ this.inputTarget.addEventListener("change", this.uploadFile.bind(this));
+ }
+
+ uploadFile() {
+ const file = this.inputTarget.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const fileContents = event.target.result;
+ this.sendFileContents(fileContents);
+ };
+ reader.readAsText(file);
+ }
+ }
+
+ sendFileContents(contents) {
+ const url = '/process_budget';
+ const formData = new FormData();
+ formData.append("budget_file_contents", contents);
+ formData.append("user_id", this.formTarget.dataset.budgetFileUserId);
+ formData.append("start_date", document.getElementById("start_date").value);
+ formData.append("end_date", document.getElementById("end_date").value);
+ formData.append("channel_daily_spend_limit", document.getElementById("channel_daily_spend_limit").value);
+
+ fetch(url, {
+ method: "POST",
+ body: formData,
+ headers: {
+ "Accept": "application/json"
+ }
+ });
+ }
+
+ deleteFile() {
+ this.inputTarget.value = "";
+ }
+}
diff --git a/app/models/budget.rb b/app/models/budget.rb
new file mode 100644
index 0000000..f222c36
--- /dev/null
+++ b/app/models/budget.rb
@@ -0,0 +1,31 @@
+class Budget
+ attr_reader :processed_budget, :rows_lost, :start_date, :end_date, :channel_daily_spend_limit, :rows_lost, :user
+
+ def initialize(budget_contents:, start_date:, end_date:, channel_daily_spend_limit:, user:)
+ @budget_contents = CSV.parse(budget_contents, headers: true)
+ @start_date = start_date
+ @end_date = end_date
+ @channel_daily_spend_limit = channel_daily_spend_limit.to_i
+ @user = user
+ end
+
+ def spend_forecast_from_budget
+ user.spend_forecasts.new(start_date:, end_date:, budget: processed_budget, channel_daily_spend_limit:, rows_lost:)
+ end
+
+ def process_budget
+ filtered_budget = @budget_contents.select do |row|
+ date = Date.parse(row['date'])
+ total_spending = row.to_h.values[1..-1].map(&:to_i).sum
+ date >= start_date && date <= end_date && total_spending < channel_daily_spend_limit
+ end
+
+ filtered_budget_array = filtered_budget.map(&:fields)
+ filtered_budget_array.unshift(@budget_contents.headers)
+ rows_lost = (@budget_contents.to_a - filtered_budget_array)
+ rows_lost.unshift(@budget_contents.headers) if rows_lost.any?
+
+ @processed_budget = filtered_budget_array
+ @rows_lost = rows_lost
+ end
+end
diff --git a/app/models/spend_forecast.rb b/app/models/spend_forecast.rb
index 7bdafe3..b914d2b 100644
--- a/app/models/spend_forecast.rb
+++ b/app/models/spend_forecast.rb
@@ -8,7 +8,7 @@ class SpendForecast < ApplicationRecord
validates :status, presence: true, inclusion: { in: STATUSES }
validate :end_date_after_start_date
- attr_accessor :budget_file
+ attr_accessor :budget_file, :rows_lost
enum status: STATUSES.index_by(&:to_sym)
diff --git a/app/views/spend_forecasts/_form.html.erb b/app/views/spend_forecasts/_form.html.erb
index 5d6ca74..1ca1b8f 100644
--- a/app/views/spend_forecasts/_form.html.erb
+++ b/app/views/spend_forecasts/_form.html.erb
@@ -1,5 +1,5 @@
-<%= form_for spend_forecast do |f| %>
- <% if @spend_forecast.errors.any? %>
+<%= form_for spend_forecast, id: 'spend_forecast', html: { data: { controller: "budget-file", budget_file_target: "form", budget_file_user_id: user.id } } do |f| %>
+ <% if spend_forecast.errors.any? %>
There were some errors with your submission:
@@ -18,21 +18,76 @@
<%= f.label :start_date, 'Start Date:', class: "block text-gray-700 text-sm font-bold mb-2" %>
- <%= f.date_field :start_date, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
+ <%= f.date_field :start_date, id: :start_date, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
<%= f.label :end_date, 'End Date:', class: "block text-gray-700 text-sm font-bold mb-2" %>
- <%= f.date_field :end_date, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
+ <%= f.date_field :end_date, id: :end_date, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
+
+ <%= f.label :channel_daily_spend_limit, 'Daily spend limit for a channel:', class: "block text-gray-700 text-sm font-bold mb-2" %>
+ <%= f.text_field :channel_daily_spend_limit, id: :channel_daily_spend_limit, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
+
+
<%= f.label :budget_file, 'Budget CSV:', class: "block text-gray-700 text-sm font-bold mb-2" %>
- <%= f.file_field :budget_file, accept: 'text/csv', class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
+ <%= f.file_field :budget_file, accept: 'text/csv', data: { target: "budget-file.input" }, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
+ <%= f.hidden_field :budget, value: f.object.budget.to_json %>
+
+ <% if f.object.budget.present? %>
+ Rows present in spend forecast:
+
+
+
+
+ <% f.object.budget.first.each do |header| %>
+ | <%= header.capitalize %> |
+ <% end %>
+
+
+
+ <% f.object.budget.drop(1).each do |entry| %>
+
+ <% entry.each do |value| %>
+ | <%= value %> |
+ <% end %>
+
+ <% end %>
+
+
+
+ <% end %>
+
+ <% if f.object.rows_lost.present? %>
+ Rows missing from spend forecast:
+
+
+
+
+ <% f.object.rows_lost.first.to_a.each do |header| %>
+ | <%= header.capitalize %> |
+ <% end %>
+
+
+
+ <% f.object.rows_lost.drop(1).each do |entry| %>
+
+ <% entry.each do |value| %>
+ | <%= value %> |
+ <% end %>
+
+ <% end %>
+
+
+
+ <% end %>
+
<%= f.submit 'Create Spend Forecast', class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" %>
- <%= link_to 'Download Budget CSV template', download_budget_template_csv_user_path(@spend_forecast.user, format: :csv), class: "bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline ml-4" %>
+ <%= link_to 'Download Budget CSV template', download_budget_template_csv_user_path(spend_forecast.user, format: :csv), class: "bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline ml-4" %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/spend_forecasts/edit.html.erb b/app/views/spend_forecasts/edit.html.erb
index 8839925..01e023a 100644
--- a/app/views/spend_forecasts/edit.html.erb
+++ b/app/views/spend_forecasts/edit.html.erb
@@ -1,6 +1,6 @@
Edit Spend Forecast
- <%= render 'form', spend_forecast: @spend_forecast %>
+ <%= render 'form', spend_forecast: @spend_forecast, user: current_user %>
diff --git a/app/views/spend_forecasts/new.html.erb b/app/views/spend_forecasts/new.html.erb
index 09f9390..ed97e90 100644
--- a/app/views/spend_forecasts/new.html.erb
+++ b/app/views/spend_forecasts/new.html.erb
@@ -1,6 +1,9 @@
Create New Spend Forecast
- <%= render 'form', spend_forecast: @spend_forecast %>
+
+ <%= render 'form', spend_forecast: @spend_forecast, user: current_user %>
+ <%= turbo_stream_from current_user %>
+
-
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 4a2b926..8c12e82 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -7,4 +7,6 @@
resources :users, only: [] do
get :download_budget_template_csv, on: :member
end
+
+ post :process_budget, to: 'budgets#process_budget'
end
diff --git a/db/migrate/20240517143647_add_limit_to_spend_forecasts.rb b/db/migrate/20240517143647_add_limit_to_spend_forecasts.rb
new file mode 100644
index 0000000..1e0b9a4
--- /dev/null
+++ b/db/migrate/20240517143647_add_limit_to_spend_forecasts.rb
@@ -0,0 +1,5 @@
+class AddLimitToSpendForecasts < ActiveRecord::Migration[7.1]
+ def change
+ add_column :spend_forecasts, :channel_daily_spend_limit, :decimal, default: 0
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e04595b..4549fda 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_05_17_084102) do
+ActiveRecord::Schema[7.1].define(version: 2024_05_17_143647) do
create_table "spend_forecasts", force: :cascade do |t|
t.string "name"
t.date "start_date"
@@ -20,6 +20,7 @@
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.decimal "channel_daily_spend_limit", default: "0.0"
t.index ["user_id"], name: "index_spend_forecasts_on_user_id"
end