Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/controllers/budgets_controller.rb
Original file line number Diff line number Diff line change
@@ -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
25 changes: 17 additions & 8 deletions app/controllers/spend_forecasts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
43 changes: 43 additions & 0 deletions app/javascript/controllers/budget_file_controller.js
Original file line number Diff line number Diff line change
@@ -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 = "";
}
}
31 changes: 31 additions & 0 deletions app/models/budget.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion app/models/spend_forecast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
69 changes: 62 additions & 7 deletions app/views/spend_forecasts/_form.html.erb
Original file line number Diff line number Diff line change
@@ -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? %>
<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">There were some errors with your submission:</strong>
<ul class="list-disc pl-5 mt-2">
Expand All @@ -18,21 +18,76 @@
<div class="flex space-x-4 mb-4">
<div class="w-1/2">
<%= 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" %>
</div>
<div class="w-1/2">
<%= 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" %>
</div>
</div>

<div class="mb-4">
<%= 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" %>
</div>

<div class="mb-4">
<%= 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" %>
</div>

<%= f.hidden_field :budget, value: f.object.budget.to_json %>

<% if f.object.budget.present? %>
<h1> Rows present in spend forecast: </h1>
<div class="overflow-x-auto">
<table class="min-w-full bg-white shadow-md rounded-lg overflow-hidden">
<thead>
<tr class="bg-gray-800 text-white">
<% f.object.budget.first.each do |header| %>
<th class="py-2 px-4 text-left"><%= header.capitalize %></th>
<% end %>
</tr>
</thead>
<tbody>
<% f.object.budget.drop(1).each do |entry| %>
<tr class="border-b border-gray-200">
<% entry.each do |value| %>
<td class="py-2 px-4"><%= value %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>

<% if f.object.rows_lost.present? %>
<h1> Rows missing from spend forecast: </h1>
<div class="overflow-x-auto">
<table class="min-w-full bg-white shadow-md rounded-lg overflow-hidden">
<thead>
<tr class="bg-gray-800 text-white">
<% f.object.rows_lost.first.to_a.each do |header| %>
<th class="py-2 px-4 text-left"><%= header.capitalize %></th>
<% end %>
</tr>
</thead>
<tbody>
<% f.object.rows_lost.drop(1).each do |entry| %>
<tr class="border-b border-gray-200">
<% entry.each do |value| %>
<td class="py-2 px-4"><%= value %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>

<div class="flex items-center justify-end mt-4">
<%= 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" %>
</div>
<% end %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/spend_forecasts/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Edit Spend Forecast</h1>
<div class="bg-white p-6 rounded-lg shadow-md">
<%= render 'form', spend_forecast: @spend_forecast %>
<%= render 'form', spend_forecast: @spend_forecast, user: current_user %>
</div>
</div>
7 changes: 5 additions & 2 deletions app/views/spend_forecasts/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<div class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Create New Spend Forecast</h1>
<div class="bg-white p-6 rounded-lg shadow-md">
<%= render 'form', spend_forecast: @spend_forecast %>
<div id="spend_forecast">
<%= render 'form', spend_forecast: @spend_forecast, user: current_user %>
<%= turbo_stream_from current_user %>
</div>
</div>
</div>
</div>
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
resources :users, only: [] do
get :download_budget_template_csv, on: :member
end

post :process_budget, to: 'budgets#process_budget'
end
5 changes: 5 additions & 0 deletions db/migrate/20240517143647_add_limit_to_spend_forecasts.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.