Compare commits
3 Commits
f17c95d999
...
76be77a2d5
| Author | SHA1 | Date |
|---|---|---|
|
|
76be77a2d5 | |
|
|
d13227e897 | |
|
|
4991d8d4b6 |
2
Gemfile
2
Gemfile
|
|
@ -38,7 +38,7 @@ gem "kamal", require: false
|
|||
gem "thruster", require: false
|
||||
|
||||
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
|
||||
# gem "image_processing", "~> 1.2"
|
||||
gem 'image_processing', '~> 1.12'
|
||||
|
||||
group :development, :test do
|
||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||
|
|
|
|||
10
Gemfile.lock
10
Gemfile.lock
|
|
@ -108,6 +108,7 @@ GEM
|
|||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
fugit (1.11.2)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
|
|
@ -115,6 +116,9 @@ GEM
|
|||
activesupport (>= 6.1)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.2.2)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
|
|
@ -152,6 +156,8 @@ GEM
|
|||
net-smtp
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.8.0)
|
||||
|
|
@ -269,6 +275,9 @@ GEM
|
|||
rubocop-performance (>= 1.24)
|
||||
rubocop-rails (>= 2.30)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.5)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
rubyzip (3.1.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.35.0)
|
||||
|
|
@ -340,6 +349,7 @@ DEPENDENCIES
|
|||
brakeman
|
||||
capybara
|
||||
debug
|
||||
image_processing (~> 1.12)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
before_action :set_assets_url
|
||||
def set_assets_url
|
||||
Rails.application.routes.default_url_options[:host] = "10.159.208.233:3000"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
class ExclusiveTraditionalRecordsController < ApplicationController
|
||||
before_action :set_exclusive_traditional_record, only: %i[ show edit update destroy ]
|
||||
|
||||
# GET /exclusive_traditional_records or /exclusive_traditional_records.json
|
||||
def index
|
||||
@exclusive_traditional_records = ExclusiveTraditionalRecord.all
|
||||
end
|
||||
|
||||
# GET /exclusive_traditional_records/1 or /exclusive_traditional_records/1.json
|
||||
def show
|
||||
end
|
||||
|
||||
# GET /exclusive_traditional_records/new
|
||||
def new
|
||||
@exclusive_traditional_record = ExclusiveTraditionalRecord.new
|
||||
end
|
||||
|
||||
# GET /exclusive_traditional_records/1/edit
|
||||
def edit
|
||||
end
|
||||
|
||||
# POST /exclusive_traditional_records or /exclusive_traditional_records.json
|
||||
def create
|
||||
@exclusive_traditional_record = ExclusiveTraditionalRecord.new(exclusive_traditional_record_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @exclusive_traditional_record.save
|
||||
format.html { redirect_to @exclusive_traditional_record, notice: "Exclusive traditional record was successfully created." }
|
||||
format.json { render :show, status: :created, location: @exclusive_traditional_record }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
format.json { render json: @exclusive_traditional_record.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /exclusive_traditional_records/1 or /exclusive_traditional_records/1.json
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @exclusive_traditional_record.update(exclusive_traditional_record_params)
|
||||
format.html { redirect_to @exclusive_traditional_record, notice: "Exclusive traditional record was successfully updated.", status: :see_other }
|
||||
format.json { render :show, status: :ok, location: @exclusive_traditional_record }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @exclusive_traditional_record.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /exclusive_traditional_records/1 or /exclusive_traditional_records/1.json
|
||||
def destroy
|
||||
@exclusive_traditional_record.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to exclusive_traditional_records_path, notice: "Exclusive traditional record was successfully destroyed.", status: :see_other }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_exclusive_traditional_record
|
||||
@exclusive_traditional_record = ExclusiveTraditionalRecord.find(params[:id])
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def exclusive_traditional_record_params
|
||||
params.require(:exclusive_traditional_record).permit(:name, :author, :description, :pdf_file)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
class ExpensesController < ApplicationController
|
||||
before_action :set_expense, only: %i[ show edit update destroy ]
|
||||
|
||||
# GET /expenses or /expenses.json
|
||||
def index
|
||||
@expenses = Expense.all
|
||||
@expenses = Expense.order(created_at: :desc)
|
||||
@total_expenses = Expense.sum(:amount)
|
||||
@daily_expenses = Expense.where(created_at: Time.current.all_day).sum(:amount)
|
||||
@weekly_expenses = Expense.where(created_at: Time.current.all_week).sum(:amount)
|
||||
@monthly_expenses = Expense.where(created_at: Time.current.all_month).sum(:amount)
|
||||
@daily_expense_items = Expense.where(created_at: Time.current.all_day)
|
||||
@weekly_expense_items = Expense.where(created_at: Time.current.all_week)
|
||||
@monthly_expense_items = Expense.where(created_at: Time.current.all_month)
|
||||
end
|
||||
|
||||
# GET /expenses/1 or /expenses/1.json
|
||||
def show
|
||||
@expense = Expense.find(params[:id])
|
||||
|
||||
# Totals
|
||||
@total_expenses = Expense.sum(:amount)
|
||||
@daily_expenses = Expense.where(created_at: Time.current.all_day).sum(:amount)
|
||||
@weekly_expenses = Expense.where(created_at: Time.current.all_week).sum(:amount)
|
||||
@monthly_expenses = Expense.where(created_at: Time.current.all_month).sum(:amount)
|
||||
|
||||
# Lists
|
||||
@daily_expense_items = Expense.where(created_at: Time.current.all_day)
|
||||
@weekly_expense_items = Expense.where(created_at: Time.current.all_week)
|
||||
@monthly_expense_items = Expense.where(created_at: Time.current.all_month)
|
||||
end
|
||||
|
||||
# GET /expenses/report
|
||||
def report
|
||||
set_summary
|
||||
render :report
|
||||
@daily_expense_items = Expense.where(date: Date.today.all_day)
|
||||
@weekly_expense_items = Expense.where(date: Date.today.beginning_of_week..Date.today.end_of_week)
|
||||
@monthly_expense_items = Expense.where(date: Date.today.beginning_of_month..Date.today.end_of_month)
|
||||
|
||||
@daily_expenses = @daily_expense_items.sum(:amount)
|
||||
@weekly_expenses = @weekly_expense_items.sum(:amount)
|
||||
@monthly_expenses = @monthly_expense_items.sum(:amount)
|
||||
|
||||
# For chart
|
||||
@chart_labels = @monthly_expense_items.pluck(:date).map { |d| d.strftime("%d %b") }
|
||||
@chart_data = @monthly_expense_items.pluck(:amount)
|
||||
end
|
||||
|
||||
# GET /expenses/new
|
||||
def new
|
||||
@expense = Expense.new
|
||||
end
|
||||
|
||||
# GET /expenses/1/edit
|
||||
def edit
|
||||
end
|
||||
|
||||
# POST /expenses or /expenses.json
|
||||
def create
|
||||
@expense = Expense.new(expense_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @expense.save
|
||||
format.html { redirect_to @expense, notice: "Expense was successfully created." }
|
||||
format.json { render :show, status: :created, location: @expense }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
format.json { render json: @expense.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /expenses/1 or /expenses/1.json
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @expense.update(expense_params)
|
||||
format.html { redirect_to @expense, notice: "Expense was successfully updated.", status: :see_other }
|
||||
format.json { render :show, status: :ok, location: @expense }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @expense.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /expenses/1 or /expenses/1.json
|
||||
def destroy
|
||||
@expense.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to expenses_path, notice: "Expense was successfully destroyed.", status: :see_other }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_expense
|
||||
@expense = Expense.find(params[:id])
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def expense_params
|
||||
params.require(:expense).permit(:amount, :date, :description)
|
||||
end
|
||||
def set_summary
|
||||
@total_expenses = Expense.sum(:amount)
|
||||
@daily_expenses = Expense.where(created_at: Time.current.all_day).sum(:amount)
|
||||
@weekly_expenses = Expense.where(created_at: Time.current.all_week).sum(:amount)
|
||||
@monthly_expenses = Expense.where(created_at: Time.current.all_month).sum(:amount)
|
||||
|
||||
@daily_expense_items = Expense.where(created_at: Time.current.all_day)
|
||||
@weekly_expense_items = Expense.where(created_at: Time.current.all_week)
|
||||
@monthly_expense_items = Expense.where(created_at: Time.current.all_month)
|
||||
|
||||
# Build chart data for the last 30 days (labels + totals per day)
|
||||
days_back = 29
|
||||
end_date = Date.current
|
||||
start_date = end_date - days_back
|
||||
labels = []
|
||||
data = []
|
||||
(start_date..end_date).each do |d|
|
||||
labels << d.strftime("%d %b")
|
||||
day_total = Expense.where(created_at: d.beginning_of_day..d.end_of_day).sum(:amount)
|
||||
data << day_total
|
||||
end
|
||||
@chart_labels = labels
|
||||
@chart_data = data
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
class IncomesController < ApplicationController
|
||||
before_action :set_income, only: %i[ show edit update destroy ]
|
||||
|
||||
# GET /incomes
|
||||
def index
|
||||
@incomes = Income.all
|
||||
end
|
||||
|
||||
# GET /incomes/report
|
||||
def report
|
||||
# @incomes = Income.all
|
||||
|
||||
# # Group incomes
|
||||
# @daily_income = @incomes.where(date: Date.today).sum(:amount)
|
||||
# @weekly_income = @incomes.where(date: Date.today.beginning_of_week..Date.today.end_of_week).sum(:amount)
|
||||
# @monthly_income = @incomes.where(date: Date.today.beginning_of_month..Date.today.end_of_month).sum(:amount)
|
||||
|
||||
# # List items
|
||||
# @daily_income_items = @incomes.where(date: Date.today)
|
||||
# @weekly_income_items = @incomes.where(date: Date.today.beginning_of_week..Date.today.end_of_week)
|
||||
# @monthly_income_items = @incomes.where(date: Date.today.beginning_of_month..Date.today.end_of_month)
|
||||
|
||||
# # Chart (Group by Month)
|
||||
# grouped = @incomes.group_by { |i| i.date.strftime("%b") }
|
||||
# @chart_labels = grouped.keys
|
||||
# @chart_data = grouped.values.map { |items| items.sum(&:amount) }
|
||||
@incomes = Income.all
|
||||
|
||||
# Daily / Weekly / Monthly totals using scopes
|
||||
@daily_income = Income.today.total_amount
|
||||
@weekly_income = Income.this_week.total_amount
|
||||
@monthly_income = Income.this_month.total_amount
|
||||
|
||||
# For charts
|
||||
@chart_labels = (0..6).map { |i| (Date.today - i).strftime("%d %b") }.reverse
|
||||
@chart_data = @chart_labels.map { |d| Income.where(created_at: d.to_date.all_day).sum(:amount) }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def new
|
||||
@income = Income.new
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@income = Income.new(income_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @income.save
|
||||
format.html { redirect_to @income, notice: "Income was successfully created." }
|
||||
format.json { render :show, status: :created, location: @income }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
format.json { render json: @income.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @income.update(income_params)
|
||||
format.html { redirect_to @income, notice: "Income was successfully updated.", status: :see_other }
|
||||
format.json { render :show, status: :ok, location: @income }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @income.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@income.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to incomes_path, notice: "Income was successfully destroyed.", status: :see_other }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_income
|
||||
@income = Income.find(params[:id])
|
||||
end
|
||||
|
||||
def income_params
|
||||
params.require(:income).permit(:title, :amount, :date, :category, :description)
|
||||
end
|
||||
end
|
||||
|
|
@ -2,17 +2,58 @@ class InstitutionsController < ApplicationController
|
|||
before_action :set_institution, only: %i[ show edit update destroy ]
|
||||
|
||||
|
||||
|
||||
# GET /institutions or /institutions.json
|
||||
def index
|
||||
@incomes = Income.all
|
||||
@institutions = Institution.all
|
||||
@programs = Program.all
|
||||
@ziyaras = Ziyara.all
|
||||
@students = Student.all
|
||||
@institution_count = Institution.count
|
||||
@program_count = Program.count
|
||||
@student_count = Student.count
|
||||
@ziyara_count = Ziyara.count
|
||||
|
||||
@institutions = Institution.all
|
||||
@programs = Program.all
|
||||
@ziyaras = Ziyara.all
|
||||
@exclusive_traditional_records = ExclusiveTraditionalRecord.all
|
||||
@exclusive_traditional_records_count = ExclusiveTraditionalRecord.count
|
||||
@expenses = Expense.all
|
||||
@expenses = Expense.order(created_at: :desc)
|
||||
|
||||
# Totals
|
||||
@total_expenses = Expense.sum(:amount)
|
||||
@daily_expenses = Expense.where(created_at: Time.current.all_day).sum(:amount)
|
||||
@weekly_expenses = Expense.where(created_at: Time.current.all_week).sum(:amount)
|
||||
@monthly_expenses = Expense.where(created_at: Time.current.all_month).sum(:amount)
|
||||
|
||||
# Expense lists (MUST be here)
|
||||
@daily_expense_items = Expense.where(created_at: Time.current.all_day)
|
||||
@weekly_expense_items = Expense.where(created_at: Time.current.all_week)
|
||||
@monthly_expense_items = Expense.where(created_at: Time.current.all_month)
|
||||
|
||||
|
||||
# index is a collection action — do not attempt to load a single institution here
|
||||
@students = Student.all
|
||||
|
||||
end
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_institution
|
||||
@institution = Institution.find_by(id: params[:institution_id])
|
||||
end
|
||||
|
||||
|
||||
# GET /institutions/1 or /institutions/1.json
|
||||
def show
|
||||
end
|
||||
|
||||
# GET /institutions/contact
|
||||
def contact
|
||||
end
|
||||
|
||||
# GET /institutions/new
|
||||
def new
|
||||
@institution = Institution.new
|
||||
|
|
@ -63,11 +104,13 @@ class InstitutionsController < ApplicationController
|
|||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_institution
|
||||
@institution = Institution.find(params.expect(:id))
|
||||
@institution = Institution.find(params[:id])
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def institution_params
|
||||
params.expect(institution: [ :name, :institution_type ])
|
||||
params.require(:institution).permit(:name, :institution_type, :place)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ class ProgramsController < ApplicationController
|
|||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_program
|
||||
@program = Program.find(params.expect(:id))
|
||||
@program = Program.find(params[:id])
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def program_params
|
||||
params.expect(program: [ :name, :date, :leadingPerson ])
|
||||
params.require(:program).permit(:name, :date, :leadingPerson, :description)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
class StudentsController < ApplicationController
|
||||
# set_institution will try to load an institution when institution_id is present
|
||||
before_action :set_institution
|
||||
# set_student will safely find a student either scoped to @institution or globally
|
||||
before_action :set_student, only: %i[ show edit update destroy ]
|
||||
# ensure nested behaviour for index/new/create (these expect an institution)
|
||||
before_action :ensure_institution_for_nested_actions, only: %i[index new create]
|
||||
|
||||
# GET /students or /students.json
|
||||
def index
|
||||
@students = @institution.students
|
||||
if @institution
|
||||
@students = @institution.students
|
||||
else
|
||||
# show all students when not nested
|
||||
@students = Student.all
|
||||
end
|
||||
end
|
||||
|
||||
# GET /students/1 or /students/1.json
|
||||
|
|
@ -21,14 +30,28 @@ class StudentsController < ApplicationController
|
|||
end
|
||||
|
||||
# POST /students or /students.json
|
||||
# def create
|
||||
# @student = @institution.students.build(student_params)
|
||||
# if @student.save
|
||||
# redirect_to institution_students_path(@institution), notice: 'Student was successfully created.'
|
||||
# else
|
||||
# render :new, status: :unprocessable_entity
|
||||
# end
|
||||
# end
|
||||
def create
|
||||
@student = @institution.students.build(student_params)
|
||||
if @student.save
|
||||
redirect_to institution_students_path(@institution), notice: 'Student was successfully created.'
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
@student = @institution.students.new(student_params)
|
||||
|
||||
if @student.save
|
||||
respond_to do |format|
|
||||
format.js # creates -> create.js.erb
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.js { render :error }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# PATCH/PUT /students/1 or /students/1.json
|
||||
def update
|
||||
|
|
@ -48,15 +71,33 @@ class StudentsController < ApplicationController
|
|||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_institution
|
||||
return unless params[:institution_id].present?
|
||||
@institution = Institution.find(params[:institution_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# Let other parts handle missing institution (redirects in actions)
|
||||
@institution = nil
|
||||
end
|
||||
|
||||
def set_student
|
||||
@student = @institution.students.find(params[:id])
|
||||
if @institution.present?
|
||||
@student = @institution.students.find(params[:id])
|
||||
else
|
||||
# fallback to global lookup when not nested
|
||||
@student = Student.find(params[:id]) if params[:id].present?
|
||||
@institution = @student.institution if @student.present?
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# will raise in actions if record missing
|
||||
raise
|
||||
end
|
||||
|
||||
def ensure_institution_for_nested_actions
|
||||
return if @institution.present?
|
||||
redirect_to institutions_path, alert: 'Please select an Institution first.'
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def student_params
|
||||
params.require(:student).permit(:first_name, :last_name, :email, :institution_id)
|
||||
params.require(:student).permit(:first_name, :last_name, :email, :institution_id, :place, :phone_number, :age, :photo)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ class ZiyarasController < ApplicationController
|
|||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_ziyara
|
||||
@ziyara = Ziyara.find(params.expect(:id))
|
||||
@ziyara = Ziyara.find(params[:id])
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def ziyara_params
|
||||
params.expect(ziyara: [ :name ])
|
||||
params.require(:ziyara).permit(:name, :date, :location, :description)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
module ExclusiveTraditionalRecordsHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module ExpensesHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module IncomesHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
class ExclusiveTraditionalRecord < ApplicationRecord
|
||||
has_one_attached :pdf_file
|
||||
end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
class Expense < ApplicationRecord
|
||||
scope :today, -> { where(created_at: Time.current.all_day) }
|
||||
scope :this_week, -> { where(created_at: Time.current.all_week) }
|
||||
scope :this_month, -> { where(created_at: Time.current.all_month) }
|
||||
|
||||
validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :date, presence: true
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
class Income < ApplicationRecord
|
||||
# Today’s income
|
||||
scope :today, -> { where("created_at >= ?", Time.zone.now.beginning_of_day) }
|
||||
|
||||
# This week income
|
||||
scope :this_week, -> { where(created_at: Time.zone.now.beginning_of_week..Time.zone.now.end_of_week) }
|
||||
|
||||
# This month income
|
||||
scope :this_month, -> { where(created_at: Time.zone.now.beginning_of_month..Time.zone.now.end_of_month) }
|
||||
|
||||
# Optional: total amount
|
||||
scope :total_amount, -> { sum(:amount) }
|
||||
end
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
class Student < ApplicationRecord
|
||||
belongs_to :institution
|
||||
has_one_attached :photo
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<div id="<%= dom_id exclusive_traditional_record %>">
|
||||
<p>
|
||||
<strong>Name:</strong>
|
||||
<%= exclusive_traditional_record.name %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Author:</strong>
|
||||
<%= exclusive_traditional_record.author %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Description:</strong>
|
||||
<%= exclusive_traditional_record.description %>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<% if exclusive_traditional_record.pdf_file.attached? %>
|
||||
<strong>PDF File:</strong>
|
||||
<%= link_to exclusive_traditional_record.pdf_file.filename.to_s, rails_blob_path(exclusive_traditional_record.pdf_file, disposition: "attachment") %>
|
||||
<% else %>
|
||||
<em>No PDF file uploaded.</em>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
json.extract! exclusive_traditional_record, :id, :name, :author, :description, :created_at, :updated_at
|
||||
json.url exclusive_traditional_record_url(exclusive_traditional_record, format: :json)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<%= form_with(model: exclusive_traditional_record) do |form| %>
|
||||
<% if exclusive_traditional_record.errors.any? %>
|
||||
<div style="color: red">
|
||||
<h2><%= pluralize(exclusive_traditional_record.errors.count, "error") %> prohibited this exclusive_traditional_record from being saved:</h2>
|
||||
|
||||
<ul>
|
||||
<% exclusive_traditional_record.errors.each do |error| %>
|
||||
<li><%= error.full_message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, style: "display: block" %>
|
||||
<%= form.text_field :name %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :author, style: "display: block" %>
|
||||
<%= form.text_field :author %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :description, style: "display: block" %>
|
||||
<%= form.textarea :description %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :pdf_file, "Upload PDF", style: "display: block" %>
|
||||
<%= form.file_field :pdf_file %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<% content_for :title, "Editing exclusive traditional record" %>
|
||||
|
||||
<h1>Editing exclusive traditional record</h1>
|
||||
|
||||
<%= render "form", exclusive_traditional_record: @exclusive_traditional_record %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Show this exclusive traditional record", @exclusive_traditional_record %> |
|
||||
<%= link_to "Back to exclusive traditional records", exclusive_traditional_records_path %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<% content_for :title, "Exclusive traditional records" %>
|
||||
|
||||
<h1>Exclusive traditional records</h1>
|
||||
|
||||
<div id="exclusive_traditional_records">
|
||||
<% @exclusive_traditional_records.each do |exclusive_traditional_record| %>
|
||||
<%= render exclusive_traditional_record %>
|
||||
<p>
|
||||
<%= link_to "Show this exclusive traditional record", exclusive_traditional_record %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to "New exclusive traditional record", new_exclusive_traditional_record_path %>
|
||||
|
|
@ -0,0 +1 @@
|
|||
json.array! @exclusive_traditional_records, partial: "exclusive_traditional_records/exclusive_traditional_record", as: :exclusive_traditional_record
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<% content_for :title, "New exclusive traditional record" %>
|
||||
|
||||
<h1>New exclusive traditional record</h1>
|
||||
|
||||
<%= render "form", exclusive_traditional_record: @exclusive_traditional_record %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Back to exclusive traditional records", exclusive_traditional_records_path %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<%= render @exclusive_traditional_record %>
|
||||
|
||||
<div>
|
||||
<%= link_to "Edit this exclusive traditional record", edit_exclusive_traditional_record_path(@exclusive_traditional_record) %> |
|
||||
<%= link_to "Back to exclusive traditional records", exclusive_traditional_records_path %>
|
||||
|
||||
<%= button_to "Destroy this exclusive traditional record", @exclusive_traditional_record, method: :delete %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1 @@
|
|||
json.partial! "exclusive_traditional_records/exclusive_traditional_record", exclusive_traditional_record: @exclusive_traditional_record
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<div id="<%= dom_id expense %>">
|
||||
<p>
|
||||
<strong>Amount:</strong>
|
||||
<%= expense.amount %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Date:</strong>
|
||||
<%= expense.date %>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
json.extract! expense, :id, :amount, :date, :created_at, :updated_at
|
||||
json.url expense_url(expense, format: :json)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<%= form_with(model: expense) do |form| %>
|
||||
<% if expense.errors.any? %>
|
||||
<div style="color: red">
|
||||
<h2><%= pluralize(expense.errors.count, "error") %> prohibited this expense from being saved:</h2>
|
||||
|
||||
<ul>
|
||||
<% expense.errors.each do |error| %>
|
||||
<li><%= error.full_message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :amount, style: "display: block" %>
|
||||
<%= form.text_field :amount %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :date, style: "display: block" %>
|
||||
<%= form.date_field :date %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<% content_for :title, "Editing expense" %>
|
||||
|
||||
<h1>Editing expense</h1>
|
||||
|
||||
<%= render "form", expense: @expense %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Show this expense", @expense %> |
|
||||
<%= link_to "Back to expenses", expenses_path %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<% content_for :title, "Expenses" %>
|
||||
|
||||
<h1>Expenses</h1>
|
||||
<div class="text-end mb-3">
|
||||
<%= link_to "📊 Expense Report", expense_report_path, class: "btn btn-outline-primary" %>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
/* Container for all cards */
|
||||
#expenses {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Individual expense card */
|
||||
.expense-card-item {
|
||||
background: linear-gradient(135deg, #f0f8ff, #e6f7ff); /* soft gradient */
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
width: 250px;
|
||||
padding: 20px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Hover effect for card */
|
||||
.expense-card-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Amount styling */
|
||||
.expense-card-item .amount {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Date styling */
|
||||
.expense-card-item .date {
|
||||
font-size: 0.9rem;
|
||||
color: #555555;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Show link styling */
|
||||
.expense-card-item .show-link {
|
||||
align-self: flex-start;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.expense-card-item .show-link:hover {
|
||||
background-color: #1e7e34;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.btn-expense {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #6a11cb, #2575fc);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn-expense:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
background: linear-gradient(135deg, #2575fc, #6a11cb);
|
||||
}
|
||||
</style>
|
||||
<%= link_to "New Expense", new_expense_path, class: "btn-expense" %>
|
||||
|
||||
|
||||
<div id="expenses">
|
||||
<% @expenses.each do |expense| %>
|
||||
<div class="expense-card-item">
|
||||
<div class="amount"><%= number_to_currency(expense.amount) %></div>
|
||||
<div class="date"><%= expense.created_at.strftime("%d %b %Y") %></div>
|
||||
<%= link_to "Show this expense", expense, class: "show-link" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
json.array! @expenses, partial: "expenses/expense", as: :expense
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<% content_for :title, "New expense" %>
|
||||
|
||||
<h1>New expense</h1>
|
||||
|
||||
<%= render "form", expense: @expense %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Back to expenses", expenses_path %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
<% content_for :title, "Expense Report" %>
|
||||
|
||||
<!-- Minimal & Clean CSS (small and tidy) -->
|
||||
<style>
|
||||
:root{
|
||||
--card-bg: #ffffff;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb; /* blue */
|
||||
--radius: 10px;
|
||||
--shadow: 0 6px 20px rgba(16,24,40,0.06);
|
||||
--gap: 18px;
|
||||
font-family: "Inter", "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
||||
}
|
||||
|
||||
.er-container {
|
||||
max-width: 920px;
|
||||
margin: 28px auto;
|
||||
padding: 18px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.er-header {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
margin-bottom: var(--gap);
|
||||
}
|
||||
|
||||
.er-title {
|
||||
color: #fff;
|
||||
font-size:1.4rem;
|
||||
font-weight:600;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.er-actions { display:flex; gap:10px; align-items:center; }
|
||||
|
||||
.btn-simple {
|
||||
background: var(--accent);
|
||||
color:#fff;
|
||||
padding:8px 12px;
|
||||
border-radius:8px;
|
||||
text-decoration:none;
|
||||
font-weight:600;
|
||||
font-size:0.95rem;
|
||||
box-shadow: 0 6px 16px rgba(37,99,235,0.12);
|
||||
transition: transform .12s ease, box-shadow .12s ease;
|
||||
}
|
||||
.btn-simple:hover { transform: translateY(-3px); box-shadow: 0 10px 26px rgba(37,99,235,0.16); }
|
||||
|
||||
/* summary cards row */
|
||||
.summary-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: #87ceeb; /* 🌤️ Sky Blue */
|
||||
border: 1px solid var(--border);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: var(--shadowG3);
|
||||
transition: .25s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-6px) scale(1.02);
|
||||
box-shadow: 0 12px 36px rgba(168,85,247,0.75);
|
||||
}
|
||||
|
||||
.card small {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card .value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* chart wrapper */
|
||||
.chart-wrap {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding:12px;
|
||||
margin-bottom: var(--gap);
|
||||
border: 1px solid rgba(15,23,42,0.04);
|
||||
}
|
||||
|
||||
/* tables */
|
||||
.table {
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
margin-bottom:20px;
|
||||
background: transparent;
|
||||
}
|
||||
.table thead th {
|
||||
text-align:left; font-size:0.9rem; color:var(--muted); padding:10px 12px;
|
||||
border-bottom: 1px solid rgba(15,23,42,0.06);
|
||||
}
|
||||
.table tbody tr {
|
||||
background: #fff; border-radius:8px;
|
||||
}
|
||||
.table td {
|
||||
padding:11px 12px;
|
||||
font-size:0.95rem;
|
||||
vertical-align:middle;
|
||||
border-bottom: 1px solid rgba(15,23,42,0.03);
|
||||
}
|
||||
.table tfoot th { padding:10px 12px; text-align:left; font-weight:700; border-top:1px solid rgba(15,23,42,0.06); }
|
||||
|
||||
/* responsive: stack cards on small screens */
|
||||
@media (max-width:720px){
|
||||
.summary-row { flex-direction:column; }
|
||||
.er-header { flex-direction:column; align-items:flex-start; gap:10px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="er-container">
|
||||
<div class="er-header">
|
||||
<h1 class="er-title">EXPENSE REPORT</h1>
|
||||
|
||||
<div class="er-actions">
|
||||
<%= link_to "New Expense", new_expense_path, class: "btn-simple" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="summary-row">
|
||||
<div class="card">
|
||||
<small>Today's Expenses</small>
|
||||
<div class="value"><%= number_to_currency(@daily_expenses || 0, unit: "₹") %></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<small>This Week</small>
|
||||
<div class="value"><%= number_to_currency(@weekly_expenses || 0, unit: "₹") %></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<small>This Month</small>
|
||||
<div class="value"><%= number_to_currency(@monthly_expenses || 0, unit: "₹") %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="chart-wrap">
|
||||
<canvas id="expensesChart" height="120"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Today's table -->
|
||||
<section>
|
||||
<h3 style="margin:0 0 8px 0; font-size:1rem; color:#374151;">Today's Expenses</h3>
|
||||
<table class="table" aria-describedby="today-expenses">
|
||||
<thead>
|
||||
<tr><th>Amount</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% (@daily_expense_items || []).each do |expense| %>
|
||||
<tr>
|
||||
<td><%= number_to_currency(expense.amount, unit: "₹") %></td>
|
||||
<td><%= expense.created_at.strftime("%d %b %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% if (@daily_expense_items || []).empty? %>
|
||||
<tr><td colspan="2" style="padding:12px;color:var(--muted)">No expenses for today.</td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><th>Total</th><th><%= number_to_currency(@daily_expenses || 0, unit: "₹") %></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Week table -->
|
||||
<section>
|
||||
<h3 style="margin:0 0 8px 0; font-size:1rem; color:#374151;">This Week's Expenses</h3>
|
||||
<table class="table" aria-describedby="week-expenses">
|
||||
<thead>
|
||||
<tr><th>Amount</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% (@weekly_expense_items || []).each do |expense| %>
|
||||
<tr>
|
||||
<td><%= number_to_currency(expense.amount, unit: "₹") %></td>
|
||||
<td><%= expense.created_at.strftime("%d %b %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% if (@weekly_expense_items || []).empty? %>
|
||||
<tr><td colspan="2" style="padding:12px;color:var(--muted)">No expenses for this week.</td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><th>Total</th><th><%= number_to_currency(@weekly_expenses || 0, unit: "₹") %></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Month table -->
|
||||
<section>
|
||||
<h3 style="margin:0 0 8px 0; font-size:1rem; color:#374151;">This Month's Expenses</h3>
|
||||
<table class="table" aria-describedby="month-expenses">
|
||||
<thead>
|
||||
<tr><th>Amount</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% (@monthly_expense_items || []).each do |expense| %>
|
||||
<tr>
|
||||
<td><%= number_to_currency(expense.amount, unit: "₹") %></td>
|
||||
<td><%= expense.created_at.strftime("%d %b %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% if (@monthly_expense_items || []).empty? %>
|
||||
<tr><td colspan="2" style="padding:12px;color:var(--muted)">No expenses for this month.</td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><th>Total</th><th><%= number_to_currency(@monthly_expenses || 0, unit: "₹") %></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Chart.js (rounded line + subtle gradient) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const labels = <%= raw((@chart_labels || []).to_json) %>;
|
||||
const data = <%= raw((@chart_data || []).to_json) %>;
|
||||
|
||||
const ctx = document.getElementById('expensesChart').getContext('2d');
|
||||
const gradient = ctx.createLinearGradient(0,0,0,120);
|
||||
gradient.addColorStop(0, 'rgba(37,99,235,0.18)');
|
||||
gradient.addColorStop(1, 'rgba(37,99,235,0)');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Expenses',
|
||||
data: data,
|
||||
fill: true,
|
||||
backgroundColor: gradient,
|
||||
borderColor: 'rgba(37,99,235,1)',
|
||||
tension: 0.42,
|
||||
borderWidth: 2,
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: 'rgba(37,99,235,1)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { ticks: { color: '#374151' } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(v){ return '₹' + v; },
|
||||
color: '#374151'
|
||||
},
|
||||
grid: { color: 'rgba(15,23,42,0.04)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(ctx){ return '₹' + ctx.formattedValue; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<% content_for :title, "Expense Report" %>
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<%= render @expense %>
|
||||
|
||||
<div>
|
||||
<%= link_to "Edit this expense", edit_expense_path(@expense) %> |
|
||||
<%= link_to "Back to expenses", expenses_path %>
|
||||
|
||||
<%= button_to "Destroy this expense", @expense, method: :delete %>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
json.partial! "expenses/expense", expense: @expense
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<%= form_with(model: income) do |form| %>
|
||||
<% if income.errors.any? %>
|
||||
<div style="color: red">
|
||||
<h2><%= pluralize(income.errors.count, "error") %> prohibited this income from being saved:</h2>
|
||||
|
||||
<ul>
|
||||
<% income.errors.each do |error| %>
|
||||
<li><%= error.full_message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :title, style: "display: block" %>
|
||||
<%= form.text_field :title %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :amount, style: "display: block" %>
|
||||
<%= form.text_field :amount %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :date, style: "display: block" %>
|
||||
<%= form.date_field :date %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :category, style: "display: block" %>
|
||||
<%= form.text_field :category %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :description, style: "display: block" %>
|
||||
<%= form.textarea :description %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<div id="<%= dom_id income %>">
|
||||
<p>
|
||||
<strong>Title:</strong>
|
||||
<%= income.title %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Amount:</strong>
|
||||
<%= income.amount %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Date:</strong>
|
||||
<%= income.date %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Category:</strong>
|
||||
<%= income.category %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Description:</strong>
|
||||
<%= income.description %>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
json.extract! income, :id, :title, :amount, :date, :category, :description, :created_at, :updated_at
|
||||
json.url income_url(income, format: :json)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<% content_for :title, "Editing income" %>
|
||||
|
||||
<h1>Editing income</h1>
|
||||
|
||||
<%= render "form", income: @income %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Show this income", @income %> |
|
||||
<%= link_to "Back to incomes", incomes_path %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<% content_for :title, "Incomes" %>
|
||||
|
||||
<h1>Incomes</h1>
|
||||
|
||||
<div class="text-end mb-3">
|
||||
<%= link_to "📊 Income Report", income_report_path, class: "btn btn-outline-primary" %>
|
||||
</div>
|
||||
|
||||
<%= link_to "New income", new_income_path, class:"btn-income" %>
|
||||
|
||||
|
||||
<div id="income">
|
||||
<% @incomes.each do |income| %>
|
||||
<div class="income-card-item">
|
||||
<div class="amount"><%= number_to_currency(income.amount) %></div>
|
||||
<div class="date"><%= income.created_at.strftime("%d %b %Y") %></div>
|
||||
<%= link_to "Show this income", income, class: "show-link" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Container for all cards */
|
||||
#income {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Individual income card */
|
||||
.income-card-item {
|
||||
background: linear-gradient(135deg, #f0f8ff, #e6f7ff); /* soft gradient */
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
width: 250px;
|
||||
padding: 20px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Hover effect for card */
|
||||
.income-card-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Amount styling */
|
||||
.income-card-item .amount {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Date styling */
|
||||
.income-card-item .date {
|
||||
font-size: 0.9rem;
|
||||
color: #555555;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Show link styling */
|
||||
.income-card-item .show-link {
|
||||
align-self: flex-start;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.income-card-item .show-link:hover {
|
||||
background-color: #1e7e34;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.btn-income {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #6a11cb, #2575fc);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn-income:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
background: linear-gradient(135deg, #2575fc, #6a11cb);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1 @@
|
|||
json.array! @incomes, partial: "incomes/income", as: :income
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<% content_for :title, "New income" %>
|
||||
|
||||
<h1>New income</h1>
|
||||
|
||||
<%= render "form", income: @income %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Back to incomes", incomes_path %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
<% content_for :title, "Income Report" %>
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--card-bg: #ffffff;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb;
|
||||
--radius: 10px;
|
||||
--shadow: 0 6px 20px rgba(16,24,40,0.06);
|
||||
--gap: 18px;
|
||||
font-family: "Inter", "Poppins", system-ui;
|
||||
}
|
||||
|
||||
.er-container {
|
||||
max-width: 920px;
|
||||
margin: 28px auto;
|
||||
padding: 18px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.er-header {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
margin-bottom: var(--gap);
|
||||
}
|
||||
|
||||
.er-title {
|
||||
color: #fff;
|
||||
font-size:1.4rem;
|
||||
font-weight:600;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.er-actions { display:flex; gap:10px; align-items:center; }
|
||||
|
||||
.btn-simple {
|
||||
background: var(--accent);
|
||||
color:#fff;
|
||||
padding:8px 12px;
|
||||
border-radius:8px;
|
||||
text-decoration:none;
|
||||
font-weight:600;
|
||||
font-size:0.95rem;
|
||||
box-shadow: 0 6px 16px rgba(37,99,235,0.12);
|
||||
transition: transform .12s ease, box-shadow .12s ease;
|
||||
}
|
||||
.btn-simple:hover { transform: translateY(-3px); box-shadow: 0 10px 26px rgba(37,99,235,0.16); }
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: #87ceeb;
|
||||
border: 1px solid var(--border);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: var(--shadowG3);
|
||||
transition: .25s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-6px) scale(1.02);
|
||||
box-shadow: 0 12px 36px rgba(168,85,247,0.75);
|
||||
}
|
||||
|
||||
.card small {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card .value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding:12px;
|
||||
margin-bottom: var(--gap);
|
||||
border: 1px solid rgba(15,23,42,0.04);
|
||||
}
|
||||
|
||||
.table {
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
margin-bottom:20px;
|
||||
background: transparent;
|
||||
}
|
||||
.table thead th {
|
||||
text-align:left; font-size:0.9rem; color:var(--muted); padding:10px 12px;
|
||||
border-bottom: 1px solid rgba(15,23,42,0.06);
|
||||
}
|
||||
.table tbody tr { background: #fff; border-radius:8px; }
|
||||
.table td {
|
||||
padding:11px 12px;
|
||||
font-size:0.95rem;
|
||||
vertical-align:middle;
|
||||
border-bottom: 1px solid rgba(15,23,42,0.03);
|
||||
}
|
||||
.table tfoot th {
|
||||
padding:10px 12px;
|
||||
text-align:left;
|
||||
font-weight:700;
|
||||
border-top:1px solid rgba(15,23,42,0.06);
|
||||
}
|
||||
|
||||
@media (max-width:720px){
|
||||
.summary-row { flex-direction:column; }
|
||||
.er-header { flex-direction:column; align-items:flex-start; gap:10px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="er-container">
|
||||
<div class="er-header">
|
||||
<h1 class="er-title">INCOME REPORT</h1>
|
||||
|
||||
<div class="er-actions">
|
||||
<%= link_to "New Income", new_income_path, class: "btn-simple" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="summary-row">
|
||||
<div class="card">
|
||||
<small>Today's Income</small>
|
||||
<div class="value"><%= number_to_currency(@daily_income || 0, unit: "₹") %></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<small>This Week</small>
|
||||
<div class="value"><%= number_to_currency(@weekly_income || 0, unit: "₹") %></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<small>This Month</small>
|
||||
<div class="value"><%= number_to_currency(@monthly_income || 0, unit: "₹") %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="chart-wrap">
|
||||
<canvas id="incomeChart" height="120"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Today's table -->
|
||||
<section>
|
||||
<h3 style="margin:0 0 8px 0; font-size:1rem; color:#374151;">Today's Income</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Amount</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% (@daily_income_items || []).each do |income| %>
|
||||
<tr>
|
||||
<td><%= number_to_currency(income.amount, unit: "₹") %></td>
|
||||
<td><%= income.created_at.strftime("%d %b %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% if (@daily_income_items || []).empty? %>
|
||||
<tr><td colspan="2" style="padding:12px;color:var(--muted)">No income for today.</td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><th>Total</th><th><%= number_to_currency(@daily_income || 0, unit: "₹") %></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Week table -->
|
||||
<section>
|
||||
<h3 style="margin:0 0 8px 0; font-size:1rem; color:#374151;">This Week's Income</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Amount</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% (@weekly_income_items || []).each do |income| %>
|
||||
<tr>
|
||||
<td><%= number_to_currency(income.amount, unit: "₹") %></td>
|
||||
<td><%= income.created_at.strftime("%d %b %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% if (@weekly_income_items || []).empty? %>
|
||||
<tr><td colspan="2" style="padding:12px;color:var(--muted)">No income for this week.</td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><th>Total</th><th><%= number_to_currency(@weekly_income || 0, unit: "₹") %></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Month table -->
|
||||
<section>
|
||||
<h3 style="margin:0 0 8px 0; font-size:1rem; color:#374151;">This Month's Income</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Amount</th><th>Date</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% (@monthly_income_items || []).each do |income| %>
|
||||
<tr>
|
||||
<td><%= number_to_currency(income.amount, unit: "₹") %></td>
|
||||
<td><%= income.created_at.strftime("%d %b %Y") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% if (@monthly_income_items || []).empty? %>
|
||||
<tr><td colspan="2" style="padding:12px;color:var(--muted)">No income for this month.</td></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><th>Total</th><th><%= number_to_currency(@monthly_income || 0, unit: "₹") %></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const labels = <%= raw((@chart_labels || []).to_json) %>;
|
||||
const data = <%= raw((@chart_data || []).to_json) %>;
|
||||
|
||||
const ctx = document.getElementById('incomeChart').getContext('2d');
|
||||
const gradient = ctx.createLinearGradient(0,0,0,120);
|
||||
gradient.addColorStop(0, 'rgba(37,99,235,0.18)');
|
||||
gradient.addColorStop(1, 'rgba(37,99,235,0)');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Income',
|
||||
data: data,
|
||||
fill: true,
|
||||
backgroundColor: gradient,
|
||||
borderColor: 'rgba(37,99,235,1)',
|
||||
tension: 0.42,
|
||||
borderWidth: 2,
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: 'rgba(37,99,235,1)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { ticks: { color: '#374151' } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { callback: v => '₹' + v, color: '#374151' },
|
||||
grid: { color: 'rgba(15,23,42,0.04)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { callbacks: { label: ctx => '₹' + ctx.formattedValue } }
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<%= render @income %>
|
||||
|
||||
<div>
|
||||
<%= link_to "Edit this income", edit_income_path(@income) %> |
|
||||
<%= link_to "Back to incomes", incomes_path %>
|
||||
|
||||
<%= button_to "Destroy this income", @income, method: :delete %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1 @@
|
|||
json.partial! "incomes/income", income: @income
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<h2>Contact Us</h2>
|
||||
<p>If you have any questions or need assistance, please feel free to reach out to us:</p>
|
||||
<ul>
|
||||
<li>Email: <a href="mailto:info@badarmadeena.org">info@badarmadeena.org</a></li>
|
||||
<li>Phone: +91 9747111705</li>
|
||||
<li>Phone: +91 8592080609</li>
|
||||
<li>Website: <a href="https://badarmadeena.org">badarmadeena.org</a></li>
|
||||
<li>Address: Badar Madeena Educational Trust, Kerala, India</li>
|
||||
|
||||
</ul>
|
||||
|
|
@ -1,230 +1,253 @@
|
|||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* ---------------- Body & Layout ---------------- */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Poppins", sans-serif;
|
||||
/* Dark gradient background */
|
||||
background: linear-gradient(135deg, #0f1724, #1e293b); /* deep navy/charcoal */
|
||||
color: #e6eef6;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #e3f2fd, #e8f5e9);
|
||||
font-family: "Poppins", sans-serif;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.page-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between; /* space out header and content */
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 20px;
|
||||
max-width: 1900px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* center vertically */
|
||||
}
|
||||
|
||||
.text-center.my-5 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
}
|
||||
/* ---------------- Header ---------------- */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-weight: 700;
|
||||
color: #1e88e5;
|
||||
margin-bottom: 0px;
|
||||
font-size: 3rem;
|
||||
background: linear-gradient(90deg, #1e88e5, #43a047, #1e88e5);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient-move 3s linear infinite;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@keyframes gradient-move {
|
||||
0% {
|
||||
background-position: 0% center;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
0% { background-position: 0% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.divider {
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: linear-gradient(to right, #1e88e5, #43a047);
|
||||
margin: 15px auto;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 70px;
|
||||
height: 4px;
|
||||
background: linear-gradient(to right, #1e88e5, #43a047);
|
||||
margin: 15px auto;
|
||||
border-radius: 3px;
|
||||
}
|
||||
/* ---------------- Stats Cards ---------------- */
|
||||
.stats-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Card Styles */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transition: 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
.stat-card {
|
||||
flex: 0 0 auto;
|
||||
width: 140px; /* compact cards */
|
||||
margin: 6px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 18px rgba(2,6,23,0.6);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 22px rgba(2,6,23,0.7);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #1565c0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.stat-label {
|
||||
font-weight: 600;
|
||||
color: rgba(230,238,246,0.8);
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
border-radius: 15px;
|
||||
background: #ffffff;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 1.4rem; /* smaller count */
|
||||
font-weight: 700;
|
||||
color: #7dd3fc; /* soft cyan */
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
background: #f9f9f9;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
/* ---------------- Glass Cards ---------------- */
|
||||
.glass-card {
|
||||
background: rgba(255,255,255,0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
padding: 25px;
|
||||
transition: 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #0d47a1;
|
||||
}
|
||||
.glass-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
color: #37474f;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
/* ---------------- Section Titles ---------------- */
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
border-top: 1px solid #e3f2fd;
|
||||
padding-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
/* ---------------- Item Cards ---------------- */
|
||||
.item-card {
|
||||
border-radius: 15px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 15px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn-custom {
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
padding: 7px 18px;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
.item-card:hover {
|
||||
transform: scale(1.03);
|
||||
background: #f9f9f9;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn-primary-custom {
|
||||
background-color: #1e88e5;
|
||||
color: #fff;
|
||||
}
|
||||
.item-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #0d47a1;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.btn-primary-custom:hover {
|
||||
background-color: #1565c0;
|
||||
}
|
||||
.item-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #37474f;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-success-custom {
|
||||
background-color: #43a047;
|
||||
color: #fff;
|
||||
}
|
||||
.item-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn-success-custom:hover {
|
||||
background-color: #2e7d32;
|
||||
}
|
||||
/* ---------------- Buttons ---------------- */
|
||||
.btn-custom {
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
padding: 7px 18px;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
backdrop-filter: blur(8px);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.btn-primary-custom { background-color: #1e88e5; color: #fff; }
|
||||
.btn-primary-custom:hover { background-color: #1565c0; }
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
/* --- Ziyara Section Styling --- */
|
||||
.btn-success-custom { background-color: #43a047; color: #fff; }
|
||||
.btn-success-custom:hover { background-color: #2e7d32; }
|
||||
|
||||
/* ---------------- Ziyara Section ---------------- */
|
||||
.ziyara-card {
|
||||
border-top: 5px solid #fbc02d;
|
||||
background: rgba(255, 255, 240, 0.95);
|
||||
background: rgba(255,255,240,0.95);
|
||||
}
|
||||
|
||||
.ziyara-title {
|
||||
color: #f9a825;
|
||||
background: linear-gradient(90deg, #fdd835, #fbc02d, #f57f17);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient-move 4s linear infinite;
|
||||
}
|
||||
|
||||
.ziyara-item {
|
||||
border-left: 4px solid #fbc02d;
|
||||
transition: all 0.3s ease;
|
||||
/* ---------------- Responsive ---------------- */
|
||||
@media (max-width: 992px) {
|
||||
.stats-row { flex-direction: column; align-items: center; }
|
||||
}
|
||||
|
||||
.ziyara-item:hover {
|
||||
background: #fff8e1;
|
||||
box-shadow: 0 8px 20px rgba(251, 192, 45, 0.25);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="container text-center">
|
||||
<!-- Logo & Header -->
|
||||
<div class="page-header mb-4 d-flex align-items-center justify-content-center flex-wrap">
|
||||
<%= image_tag("unnamed.jpg", alt: "Logo", class: "logo-img me-3") %>
|
||||
<h1>BADAR MADEENA CULTURAL CENTER
|
||||
AND CHARITY TRUST</h1>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="page-header d-flex flex-column align-items-center">
|
||||
<%= image_tag("unnamed.jpg", alt: "Logo", class: "logo-img") %>
|
||||
<h1>BADAR MADEENA CULTURAL CENTER AND CHARITY TRUST</h1>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">INSTITUTION</div>
|
||||
<div class="stat-value"><%= @institution_count || 0 %></div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">PROGRAMS</div>
|
||||
<div class="stat-value"><%= @program_count || 0 %></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">STUDENTS</div>
|
||||
<div class="stat-value"><%= @student_count || 0 %></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">ZIYARA</div>
|
||||
<div class="stat-value"><%= @ziyara_count || 0 %></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">TRADITIONAL RECORDS</div>
|
||||
<div class="stat-value"><%= @exclusive_traditional_records_count || 0 %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Two Sections Side by Side -->
|
||||
<div class="row g-4 justify-content-center align-items-stretch">
|
||||
<!-- Institutions Section -->
|
||||
|
||||
<!-- Two Column Sections -->
|
||||
<div class="row g-4">
|
||||
<!-- Institutions -->
|
||||
<div class="col-lg-6 d-flex">
|
||||
<div class="glass-card p-4 w-100">
|
||||
<div class="glass-card">
|
||||
<h3 class="section-title text-primary">🏫 Institutions</h3>
|
||||
<% if @institutions.present? %>
|
||||
<div class="row g-3">
|
||||
<% @institutions.each do |institution| %>
|
||||
<div class="col-12">
|
||||
<div class="item-card p-3 shadow-sm">
|
||||
<div class="item-card">
|
||||
<div class="item-title"><%= institution.name %></div>
|
||||
<% if institution.respond_to?(:description) %>
|
||||
<p class="item-desc mb-2"><%= institution.description.truncate(100) %></p>
|
||||
<p class="item-desc"><%= institution.description.truncate(100) %></p>
|
||||
<% end %>
|
||||
<div class="item-footer">
|
||||
<%= link_to "View", institution, class: "btn btn-primary-custom btn-sm btn-custom" %>
|
||||
</div>
|
||||
<div class="item-footer d-flex justify-content-end gap-2">
|
||||
<%= link_to "Students", institution_students_path(institution), class: "btn btn-success-custom btn-sm btn-custom" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -233,29 +256,28 @@
|
|||
<p class="item-desc text-center">No institutions added yet.</p>
|
||||
<% end %>
|
||||
<div class="text-center mt-3">
|
||||
<%= link_to "➕ New Institution", new_institution_path, class: "btn btn-success-custom btn-custom" %>
|
||||
<%= link_to "➕ New Institution", new_institution_path, class: "btn btn-success-custom btn-custom me-2" %>
|
||||
<%= link_to "📚 All Students", students_path, class: "btn btn-primary-custom btn-custom" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Programs Section -->
|
||||
<!-- Programs -->
|
||||
<div class="col-lg-6 d-flex">
|
||||
<div class="glass-card p-4 w-100">
|
||||
<div class="glass-card">
|
||||
<h3 class="section-title text-success">🎓 Programs</h3>
|
||||
<% if @programs.present? %>
|
||||
<div class="row g-3">
|
||||
<% @programs.each do |program| %>
|
||||
<div class="col-12">
|
||||
<div class="item-card p-3 shadow-sm">
|
||||
<div class="item-card">
|
||||
<div class="item-title"><%= program.name %></div>
|
||||
<p class="item-desc mb-2">
|
||||
<% if program.respond_to?(:date) %>
|
||||
<strong>Date:</strong> <%= program.date %><br>
|
||||
<% end %>
|
||||
<% if program.respond_to?(:leadingPerson) %>
|
||||
<strong>Lead:</strong> <%= program.leadingPerson %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% if program.respond_to?(:date) %>
|
||||
<p class="item-desc"><strong>Date:</strong> <%= program.date %></p>
|
||||
<% end %>
|
||||
<% if program.respond_to?(:leadingPerson) %>
|
||||
<p class="item-desc"><strong>Lead:</strong> <%= program.leadingPerson %></p>
|
||||
<% end %>
|
||||
<div class="item-footer">
|
||||
<%= link_to "View", program, class: "btn btn-success-custom btn-sm btn-custom" %>
|
||||
</div>
|
||||
|
|
@ -273,43 +295,226 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Ziyara Section -->
|
||||
<div class="row mt-4 justify-content-center">
|
||||
<div class="col-lg-10 d-flex">
|
||||
<div class="glass-card ziyara-card p-4 w-100">
|
||||
<h3 class="section-title ziyara-title">🕌 Ziyara</h3>
|
||||
|
||||
<% if @ziyaras.present? %>
|
||||
<div class="row g-3">
|
||||
<% @ziyaras.each do |ziyara| %>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="item-card ziyara-item p-3 shadow-sm">
|
||||
<div class="item-title"><%= ziyara.name %></div>
|
||||
<% if ziyara.respond_to?(:location) %>
|
||||
<p class="item-desc mb-1"><strong>📍 Location:</strong> <%= ziyara.location %></p>
|
||||
<% end %>
|
||||
<% if ziyara.respond_to?(:date) %>
|
||||
<p class="item-desc mb-1"><strong>📅 Date:</strong> <%= ziyara.date %></p>
|
||||
<% end %>
|
||||
<div class="item-footer">
|
||||
<%= link_to "View", ziyara, class: "btn btn-primary-custom btn-sm btn-custom" %>
|
||||
<!-- Ziyara & Exclusive Traditional Records Side-by-side -->
|
||||
<div class="row mt-4 g-4 justify-content-center">
|
||||
<div class="col-lg-5 d-flex">
|
||||
<div class="glass-card ziyara-card w-100">
|
||||
<h3 class="section-title ziyara-title">🕌 Ziyara</h3>
|
||||
<% if @ziyaras.present? %>
|
||||
<div class="row g-3">
|
||||
<% @ziyaras.each do |ziyara| %>
|
||||
<div class="col-12">
|
||||
<div class="item-card">
|
||||
<div class="item-title"><%= ziyara.name %></div>
|
||||
<% if ziyara.respond_to?(:location) %>
|
||||
<p class="item-desc"><strong>📍 Location:</strong> <%= ziyara.location %></p>
|
||||
<% end %>
|
||||
<% if ziyara.respond_to?(:date) %>
|
||||
<p class="item-desc"><strong>📅 Date:</strong> <%= ziyara.date %></p>
|
||||
<% end %>
|
||||
<div class="item-footer">
|
||||
<%= link_to "View", ziyara, class: "btn btn-primary-custom btn-sm btn-custom" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="item-desc text-center">No Ziyara records yet.</p>
|
||||
<% end %>
|
||||
<div class="text-center mt-3">
|
||||
<%= link_to "➕ New Ziyara", new_ziyara_path, class: "btn btn-success-custom btn-custom" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5 d-flex">
|
||||
<div class="glass-card w-100">
|
||||
<h3 class="section-title text-secondary">📜 Exclusive Traditional Records</h3>
|
||||
<% if @exclusive_traditional_records.present? %>
|
||||
<div class="row g-3">
|
||||
<% @exclusive_traditional_records.each do |record| %>
|
||||
<div class="col-12">
|
||||
<div class="item-card">
|
||||
<div class="item-title"><%= record.name if record.respond_to?(:name) %></div>
|
||||
<% if record.respond_to?(:description) %>
|
||||
<p class="item-desc"><%= record.description.truncate(100) %></p>
|
||||
<% end %>
|
||||
<div class="item-footer">
|
||||
<%= link_to "View", record, class: "btn btn-primary-custom btn-sm btn-custom" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="item-desc text-center">No exclusive traditional records yet.</p>
|
||||
<% end %>
|
||||
<div class="text-center mt-3">
|
||||
<%= link_to "➕ New Record", new_exclusive_traditional_record_path, class: "btn btn-success-custom btn-custom" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% content_for :title, "Expenses" %>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
/* Main card container */
|
||||
.expense-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
padding: 20px 25px;
|
||||
max-width: 420px;
|
||||
margin: 30px auto;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hover effect */
|
||||
.expense-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Card header */
|
||||
.expense-card h1 {
|
||||
font-size: 1.7rem;
|
||||
margin-bottom: 15px;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Flash notice */
|
||||
.expense-card .flash-notice {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.expense-card .btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 10px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* Button hover */
|
||||
.expense-card .btn:hover {
|
||||
background-color: #0056b3;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Main card container */
|
||||
.income-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
padding: 20px 25px;
|
||||
max-width: 420px;
|
||||
margin: 30px auto;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hover effect */
|
||||
.income-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Card header */
|
||||
.income-card h1 {
|
||||
font-size: 1.7rem;
|
||||
margin-bottom: 15px;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Flash notice */
|
||||
.income-card .flash-notice {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.income-card .btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 10px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* Button hover */
|
||||
.income-card .btn:hover {
|
||||
background-color: #0056b3;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="row mt-4 g-4 justify-content-center">
|
||||
<!-- Expense Card -->
|
||||
<div class="col-lg-5 d-flex">
|
||||
<div class="expense-card w-100">
|
||||
<% if notice.present? %>
|
||||
<div class="flash-notice">
|
||||
<%= notice %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="item-desc text-center">No Ziyara records yet.</p>
|
||||
<% end %>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<%= link_to "➕ New Ziyara", new_ziyara_path, class: "btn btn-success-custom btn-custom" %>
|
||||
</div>
|
||||
<h1>📊 Expenses</h1>
|
||||
<%= link_to "Expenses List", expenses_path, class: "btn" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Income Card -->
|
||||
<div class="col-lg-5 d-flex">
|
||||
<div class="income-card w-100">
|
||||
<% if notice.present? %>
|
||||
<div class="flash-notice">
|
||||
<%= notice %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h1>💰 Incomes</h1>
|
||||
<%= link_to "Incomes List", incomes_path, class: "btn" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div> <!-- container -->
|
||||
</div> <!-- page-wrapper -->
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
<div class="card bg-white shadow p-4 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-3 text-primary"><%= @institution.name %></h2>
|
||||
<p class="card-text mb-2"><strong>ID:</strong> <%= @institution.id %></p>
|
||||
<%# Add more institution details here if needed %>
|
||||
<div class="mb-3">
|
||||
<%= link_to "Edit Institution", edit_institution_path(@institution), class: "btn btn-secondary me-2" %>
|
||||
|
|
@ -29,7 +28,6 @@
|
|||
<div class="card-body">
|
||||
<h5 class="card-title text-info"><%= student.first_name %> <%= student.last_name %></h5>
|
||||
<p class="card-text mb-1"><strong>Email:</strong> <%= student.email %></p>
|
||||
<p class="card-text mb-1"><strong>ID:</strong> <%= student.id %></p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0 d-flex justify-content-end">
|
||||
<%= link_to "Show", institution_student_path(@institution, student), class: "btn btn-primary btn-sm me-2" %>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,14 @@
|
|||
|
||||
<!-- Bootstrap CSS for modern UI -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #181a1b !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #23272b !important;
|
||||
color: #e0e0e0 !important;
|
||||
|
|
@ -73,7 +76,7 @@
|
|||
<!-- Logo & Brand -->
|
||||
<%= link_to root_path, class: "navbar-brand d-flex align-items-center" do %>
|
||||
<%= image_tag("unnamed.jpg", alt: "Logo", class: "nav-logo me-2") %>
|
||||
<span>Badar Madeena Trust</span>
|
||||
<span>Badar Madeena pullarikode</span>
|
||||
<% end %>
|
||||
|
||||
<!-- Toggler (for mobile) -->
|
||||
|
|
@ -98,7 +101,13 @@
|
|||
<%= link_to "🕌 Ziyara", ziyaras_path, class: "nav-link" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "📞 Contact", "#", class: "nav-link" %>
|
||||
<%= link_to "👩🎓 Students", students_path, class: "nav-link" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "📊 Expenses", expenses_path, class: "nav-link" %>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<%= link_to "📞 Contact", contact_path, class: "nav-link" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -117,7 +126,7 @@
|
|||
|
||||
.glass-navbar .navbar-brand {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.9rem;
|
||||
color: #1e88e5;
|
||||
background: linear-gradient(90deg, #1e88e5, #43a047);
|
||||
-webkit-background-clip: text;
|
||||
|
|
@ -136,7 +145,7 @@
|
|||
|
||||
.navbar-nav .nav-link {
|
||||
font-weight: 600;
|
||||
color: #2c3e50 !important;
|
||||
color: #91e59b !important;
|
||||
padding: 8px 18px;
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s ease;
|
||||
|
|
@ -179,10 +188,11 @@
|
|||
<% if notice %>
|
||||
<div class="alert alert-success"><%= notice %></div>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
<% if alert %>
|
||||
<div class="alert alert-danger"><%= alert %></div>
|
||||
<% end %>
|
||||
<%= yield %>
|
||||
</div>
|
||||
<footer class="footer text-center mt-5 mb-3 text-muted">
|
||||
© <%= Time.current.year %> Badar Madeena
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<% institution = @institution || student.institution %>
|
||||
<% form_url = student.persisted? ? institution_student_path(institution, student) : institution_students_path(institution) %>
|
||||
<%= form_with(model: student, url: form_url) do |form| %>
|
||||
<%= form_with(model: student, url: form_url, remote: true) do |form| %>
|
||||
<% if student.errors.any? %>
|
||||
<div style="color: red">
|
||||
<h2><%= pluralize(student.errors.count, "error") %> prohibited this student from being saved:</h2>
|
||||
|
|
@ -43,11 +43,10 @@
|
|||
<%= form.number_field :age %>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<%= form.label :institution_id, style: "display: block" %>
|
||||
<%= form.text_field :institution_id %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= form.label :photo, "Upload Photo", style: "display: block" %>
|
||||
<%= form.file_field :photo, accept: "image/*" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit %>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,123 @@
|
|||
<div id="<%= dom_id student %>">
|
||||
<p>
|
||||
<strong>First name:</strong>
|
||||
<%= student.first_name %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Last name:</strong>
|
||||
<%= student.last_name %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Email:</strong>
|
||||
<%= student.email %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Institution:</strong>
|
||||
<%= student.institution_id %>
|
||||
</p>
|
||||
<p>
|
||||
<strong>phone_number:</strong>
|
||||
<%= student.phone_number %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Place:</strong>
|
||||
<%= student.place %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Age:</strong>
|
||||
<%= student.age %>
|
||||
</p>
|
||||
|
||||
<div class="student-card" id="<%= dom_id student %>">
|
||||
<div class="student-header">
|
||||
<div class="student-photo">
|
||||
<% if student.photo.attached? %>
|
||||
<%# Fallback for image processing errors %>
|
||||
<% begin %>
|
||||
<%= image_tag url_for(student.photo.variant(resize_to_limit: [120, 120]).processed), alt: student.first_name %>
|
||||
<% rescue => e %>
|
||||
<%= image_tag url_for(student.photo), alt: student.first_name %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="placeholder">No Photo</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="student-name">
|
||||
<h3><%= student.first_name %> <%= student.last_name %></h3>
|
||||
<% if student.institution.present? %>
|
||||
<%= link_to student.institution.name, institution_path(student.institution), class: "badge badge-primary" %>
|
||||
<% else %>
|
||||
<span class="badge badge-secondary">No institution</span>
|
||||
<% end %>
|
||||
<p class="student-email"><%= student.email %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="student-info">
|
||||
<div class="info-item"><strong>Phone:</strong> <%= student.phone_number %></div>
|
||||
<div class="info-item"><strong>Place:</strong> <%= student.place %></div>
|
||||
<div class="info-item"><strong>Age:</strong> <%= student.age %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.student-card {
|
||||
background: linear-gradient(135deg, #ffffff, #f9fafb);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.student-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 32px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.student-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.student-photo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.student-photo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.student-name h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.student-name .badge {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.student-name .student-email {
|
||||
font-size: 0.9rem;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.student-info {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.badge-primary { background: #2563eb; }
|
||||
.badge-secondary { background: #6b7280; }
|
||||
|
||||
.placeholder {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,10 @@
|
|||
json.extract! student, :id, :first_name, :last_name, :email, :institution_id, :created_at, :updated_at
|
||||
json.url student_url(student, format: :json)
|
||||
// list refresh ചെയ്യുക
|
||||
document.getElementById("students-list").innerHTML = "<%= j(render @students) %>";
|
||||
|
||||
// form clear ചെയ്യുക
|
||||
document.getElementById("student-form").innerHTML = "<%= j(render 'form', student: Student.new) %>";
|
||||
|
||||
// success alert
|
||||
alert("Student added successfully (without page reload!)");
|
||||
|
|
|
|||
|
|
@ -1,16 +1,129 @@
|
|||
<p style="color: green"><%= notice %></p>
|
||||
|
||||
<p class="text-success text-center"><%= notice %></p>
|
||||
<% content_for :title, "Students" %>
|
||||
|
||||
<h1>Students</h1>
|
||||
<style>
|
||||
.students-container {
|
||||
max-width: 900px;
|
||||
margin: 25px auto;
|
||||
padding: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
<div id="students">
|
||||
<% @students.each do |student| %>
|
||||
<%= render student %>
|
||||
<p>
|
||||
<%= link_to "Show this student", institution_student_path(@institution, student) %>
|
||||
</p>
|
||||
<% end %>
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
padding: 10px 14px;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(37,99,235,0.25);
|
||||
font-weight: 600;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #1e4fc4;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.student-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.06);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.student-info h3 {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.student-info p {
|
||||
margin: 3px 0 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
.btn-small:hover {
|
||||
background: #0a8d63;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.badge-primary { background: #2563eb; }
|
||||
.badge-secondary { background: #6b7280; }
|
||||
</style>
|
||||
|
||||
<div class="students-container">
|
||||
<div class="page-header">
|
||||
<h1>Students</h1>
|
||||
<% if @institution.present? %>
|
||||
<%= link_to "➕ New Student", new_institution_student_path(@institution), class: "btn-primary" %>
|
||||
<% else %>
|
||||
<%= link_to "➕ New Student", new_institution_path, class: "btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="students-list">
|
||||
<% @students.each do |student| %>
|
||||
<div class="student-card">
|
||||
<div class="student-info">
|
||||
<h3><%= student.first_name %> <%= student.last_name %></h3>
|
||||
<p>Place: <%= student.place %></p>
|
||||
<p>
|
||||
Institution:
|
||||
<% if student.institution.present? %>
|
||||
<%= link_to student.institution.name, institution_path(student.institution), class: "badge badge-primary" %>
|
||||
<% else %>
|
||||
<span class="badge badge-secondary">No institution</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<%# view path: prefer nested if current page is nested, else use student's institution if present %>
|
||||
<% view_path = if @institution.present?
|
||||
institution_student_path(@institution, student)
|
||||
elsif student.institution.present?
|
||||
institution_student_path(student.institution, student)
|
||||
else
|
||||
student_path(student)
|
||||
end %>
|
||||
<%= link_to "View →", view_path, class: "btn-small" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @students.empty? %>
|
||||
<p class="text-center" style="color:#6b7280;">No students found.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= link_to "New student", new_institution_student_path(@institution) %>
|
||||
|
|
|
|||
|
|
@ -29,3 +29,10 @@
|
|||
|
||||
en:
|
||||
hello: "Hello world"
|
||||
number:
|
||||
currency:
|
||||
format:
|
||||
unit: "₹"
|
||||
separator: "."
|
||||
delimiter: ","
|
||||
format: "%u%n"
|
||||
|
|
@ -1,11 +1,37 @@
|
|||
Rails.application.routes.draw do
|
||||
|
||||
resources :students
|
||||
resources :incomes do
|
||||
collection do
|
||||
get 'report'
|
||||
end
|
||||
end
|
||||
get 'incomes/report', to: 'incomes#report', as: :income_report
|
||||
resources :expenses do
|
||||
collection do
|
||||
get 'report'
|
||||
end
|
||||
end
|
||||
get 'expenses/report', to: 'expenses#report', as: :expense_report
|
||||
resources :exclusive_traditional_records
|
||||
resources :ziyaras
|
||||
resources :programs
|
||||
resources :institutions do
|
||||
resources :students
|
||||
end
|
||||
resources :institutions do
|
||||
resources :students
|
||||
end
|
||||
|
||||
get "up" => "rails/health#show", as: :rails_health_check
|
||||
get 'contact', to: 'institutions#contact'
|
||||
get 'institutions', to: 'institutions#index'
|
||||
get 'programs', to: 'programs#index'
|
||||
get 'ziyaras', to: 'ziyaras#index'
|
||||
get 'students', to: 'students#index'
|
||||
get 'expenses', to: 'expenses#index'
|
||||
|
||||
|
||||
root "institutions#index"
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
class CreateExclusiveTraditionalRecords < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :exclusive_traditional_records do |t|
|
||||
t.string :name
|
||||
t.string :author
|
||||
t.text :description
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
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,10 @@
|
|||
class CreateExpenses < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :expenses do |t|
|
||||
t.decimal :amount
|
||||
t.date :date
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
class CreateIncomes < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :incomes do |t|
|
||||
t.string :title
|
||||
t.decimal :amount
|
||||
t.date :date
|
||||
t.string :category
|
||||
t.text :description
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -10,7 +10,60 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_10_18_034921) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_10_22_133611) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_blobs", force: :cascade 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"
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "exclusive_traditional_records", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "author"
|
||||
t.text "description"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "expenses", force: :cascade do |t|
|
||||
t.decimal "amount"
|
||||
t.date "date"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "incomes", force: :cascade do |t|
|
||||
t.string "title"
|
||||
t.decimal "amount"
|
||||
t.date "date"
|
||||
t.string "category"
|
||||
t.text "description"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "institutions", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "institution_type"
|
||||
|
|
@ -18,6 +71,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_18_034921) do
|
|||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "models", force: :cascade do |t|
|
||||
t.string "Income"
|
||||
t.string "title"
|
||||
t.decimal "amount"
|
||||
t.date "date"
|
||||
t.string "category"
|
||||
t.text "description"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "programs", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.date "date"
|
||||
|
|
@ -45,5 +109,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_18_034921) do
|
|||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "students", "institutions"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
require "test_helper"
|
||||
|
||||
class ExclusiveTraditionalRecordsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@exclusive_traditional_record = exclusive_traditional_records(:one)
|
||||
end
|
||||
|
||||
test "should get index" do
|
||||
get exclusive_traditional_records_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get new" do
|
||||
get new_exclusive_traditional_record_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create exclusive_traditional_record" do
|
||||
assert_difference("ExclusiveTraditionalRecord.count") do
|
||||
post exclusive_traditional_records_url, params: { exclusive_traditional_record: { author: @exclusive_traditional_record.author, description: @exclusive_traditional_record.description, name: @exclusive_traditional_record.name } }
|
||||
end
|
||||
|
||||
assert_redirected_to exclusive_traditional_record_url(ExclusiveTraditionalRecord.last)
|
||||
end
|
||||
|
||||
test "should show exclusive_traditional_record" do
|
||||
get exclusive_traditional_record_url(@exclusive_traditional_record)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get edit" do
|
||||
get edit_exclusive_traditional_record_url(@exclusive_traditional_record)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should update exclusive_traditional_record" do
|
||||
patch exclusive_traditional_record_url(@exclusive_traditional_record), params: { exclusive_traditional_record: { author: @exclusive_traditional_record.author, description: @exclusive_traditional_record.description, name: @exclusive_traditional_record.name } }
|
||||
assert_redirected_to exclusive_traditional_record_url(@exclusive_traditional_record)
|
||||
end
|
||||
|
||||
test "should destroy exclusive_traditional_record" do
|
||||
assert_difference("ExclusiveTraditionalRecord.count", -1) do
|
||||
delete exclusive_traditional_record_url(@exclusive_traditional_record)
|
||||
end
|
||||
|
||||
assert_redirected_to exclusive_traditional_records_url
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
require "test_helper"
|
||||
|
||||
class ExpensesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@expense = expenses(:one)
|
||||
end
|
||||
|
||||
test "should get index" do
|
||||
get expenses_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get new" do
|
||||
get new_expense_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create expense" do
|
||||
assert_difference("Expense.count") do
|
||||
post expenses_url, params: { expense: { amount: @expense.amount, date: @expense.date } }
|
||||
end
|
||||
|
||||
assert_redirected_to expense_url(Expense.last)
|
||||
end
|
||||
|
||||
test "should show expense" do
|
||||
get expense_url(@expense)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get edit" do
|
||||
get edit_expense_url(@expense)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should update expense" do
|
||||
patch expense_url(@expense), params: { expense: { amount: @expense.amount, date: @expense.date } }
|
||||
assert_redirected_to expense_url(@expense)
|
||||
end
|
||||
|
||||
test "should destroy expense" do
|
||||
assert_difference("Expense.count", -1) do
|
||||
delete expense_url(@expense)
|
||||
end
|
||||
|
||||
assert_redirected_to expenses_url
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
require "test_helper"
|
||||
|
||||
class IncomesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@income = incomes(:one)
|
||||
end
|
||||
|
||||
test "should get index" do
|
||||
get incomes_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get new" do
|
||||
get new_income_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create income" do
|
||||
assert_difference("Income.count") do
|
||||
post incomes_url, params: { income: { amount: @income.amount, category: @income.category, date: @income.date, description: @income.description, title: @income.title } }
|
||||
end
|
||||
|
||||
assert_redirected_to income_url(Income.last)
|
||||
end
|
||||
|
||||
test "should show income" do
|
||||
get income_url(@income)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get edit" do
|
||||
get edit_income_url(@income)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should update income" do
|
||||
patch income_url(@income), params: { income: { amount: @income.amount, category: @income.category, date: @income.date, description: @income.description, title: @income.title } }
|
||||
assert_redirected_to income_url(@income)
|
||||
end
|
||||
|
||||
test "should destroy income" do
|
||||
assert_difference("Income.count", -1) do
|
||||
delete income_url(@income)
|
||||
end
|
||||
|
||||
assert_redirected_to incomes_url
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
name: MyString
|
||||
author: MyString
|
||||
description: MyText
|
||||
|
||||
two:
|
||||
name: MyString
|
||||
author: MyString
|
||||
description: MyText
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
amount: 9.99
|
||||
date: 2025-10-22
|
||||
|
||||
two:
|
||||
amount: 9.99
|
||||
date: 2025-10-22
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
title: MyString
|
||||
amount: 9.99
|
||||
date: 2025-10-22
|
||||
category: MyString
|
||||
description: MyText
|
||||
|
||||
two:
|
||||
title: MyString
|
||||
amount: 9.99
|
||||
date: 2025-10-22
|
||||
category: MyString
|
||||
description: MyText
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class ExclusiveTraditionalRecordTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class ExpenseTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class IncomeTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
require "application_system_test_case"
|
||||
|
||||
class ExclusiveTraditionalRecordsTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
@exclusive_traditional_record = exclusive_traditional_records(:one)
|
||||
end
|
||||
|
||||
test "visiting the index" do
|
||||
visit exclusive_traditional_records_url
|
||||
assert_selector "h1", text: "Exclusive traditional records"
|
||||
end
|
||||
|
||||
test "should create exclusive traditional record" do
|
||||
visit exclusive_traditional_records_url
|
||||
click_on "New exclusive traditional record"
|
||||
|
||||
fill_in "Author", with: @exclusive_traditional_record.author
|
||||
fill_in "Description", with: @exclusive_traditional_record.description
|
||||
fill_in "Name", with: @exclusive_traditional_record.name
|
||||
click_on "Create Exclusive traditional record"
|
||||
|
||||
assert_text "Exclusive traditional record was successfully created"
|
||||
click_on "Back"
|
||||
end
|
||||
|
||||
test "should update Exclusive traditional record" do
|
||||
visit exclusive_traditional_record_url(@exclusive_traditional_record)
|
||||
click_on "Edit this exclusive traditional record", match: :first
|
||||
|
||||
fill_in "Author", with: @exclusive_traditional_record.author
|
||||
fill_in "Description", with: @exclusive_traditional_record.description
|
||||
fill_in "Name", with: @exclusive_traditional_record.name
|
||||
click_on "Update Exclusive traditional record"
|
||||
|
||||
assert_text "Exclusive traditional record was successfully updated"
|
||||
click_on "Back"
|
||||
end
|
||||
|
||||
test "should destroy Exclusive traditional record" do
|
||||
visit exclusive_traditional_record_url(@exclusive_traditional_record)
|
||||
click_on "Destroy this exclusive traditional record", match: :first
|
||||
|
||||
assert_text "Exclusive traditional record was successfully destroyed"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
require "application_system_test_case"
|
||||
|
||||
class ExpensesTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
@expense = expenses(:one)
|
||||
end
|
||||
|
||||
test "visiting the index" do
|
||||
visit expenses_url
|
||||
assert_selector "h1", text: "Expenses"
|
||||
end
|
||||
|
||||
test "should create expense" do
|
||||
visit expenses_url
|
||||
click_on "New expense"
|
||||
|
||||
fill_in "Amount", with: @expense.amount
|
||||
fill_in "Date", with: @expense.date
|
||||
click_on "Create Expense"
|
||||
|
||||
assert_text "Expense was successfully created"
|
||||
click_on "Back"
|
||||
end
|
||||
|
||||
test "should update Expense" do
|
||||
visit expense_url(@expense)
|
||||
click_on "Edit this expense", match: :first
|
||||
|
||||
fill_in "Amount", with: @expense.amount
|
||||
fill_in "Date", with: @expense.date
|
||||
click_on "Update Expense"
|
||||
|
||||
assert_text "Expense was successfully updated"
|
||||
click_on "Back"
|
||||
end
|
||||
|
||||
test "should destroy Expense" do
|
||||
visit expense_url(@expense)
|
||||
click_on "Destroy this expense", match: :first
|
||||
|
||||
assert_text "Expense was successfully destroyed"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
require "application_system_test_case"
|
||||
|
||||
class IncomesTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
@income = incomes(:one)
|
||||
end
|
||||
|
||||
test "visiting the index" do
|
||||
visit incomes_url
|
||||
assert_selector "h1", text: "Incomes"
|
||||
end
|
||||
|
||||
test "should create income" do
|
||||
visit incomes_url
|
||||
click_on "New income"
|
||||
|
||||
fill_in "Amount", with: @income.amount
|
||||
fill_in "Category", with: @income.category
|
||||
fill_in "Date", with: @income.date
|
||||
fill_in "Description", with: @income.description
|
||||
fill_in "Title", with: @income.title
|
||||
click_on "Create Income"
|
||||
|
||||
assert_text "Income was successfully created"
|
||||
click_on "Back"
|
||||
end
|
||||
|
||||
test "should update Income" do
|
||||
visit income_url(@income)
|
||||
click_on "Edit this income", match: :first
|
||||
|
||||
fill_in "Amount", with: @income.amount
|
||||
fill_in "Category", with: @income.category
|
||||
fill_in "Date", with: @income.date
|
||||
fill_in "Description", with: @income.description
|
||||
fill_in "Title", with: @income.title
|
||||
click_on "Update Income"
|
||||
|
||||
assert_text "Income was successfully updated"
|
||||
click_on "Back"
|
||||
end
|
||||
|
||||
test "should destroy Income" do
|
||||
visit income_url(@income)
|
||||
click_on "Destroy this income", match: :first
|
||||
|
||||
assert_text "Income was successfully destroyed"
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue