Initial commit (history cleared)
CI / test (3.4.1) (push) Has been cancelled

This commit is contained in:
2026-04-29 11:21:39 +01:00
commit 298610b5f6
277 changed files with 30877 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
<form action="" id="analytics_search">
Search by IP: <input type="text" id="ip" name="ip"><br />
<input type="checkbox" value="" id="field_ip_address" name="field[ip_address]"> IP Address<br />
<input type="checkbox" value="" id="field_referrer" name="field[referrer]"> Referrer<br />
<input type="checkbox" value="" id="field_user_agent" name="field[user_agent]"> User Agent
</form>
<div id="dt_example" class="example_alt_pagination">
<table class="table table-striped table-hover table-bordered pull-left <%= "custom" if params[:field] %>" id="data-table">
<thead>
<tr>
<%
count = (params[:field] ? (custom_fields.count+1) : 6)
count.times do %>
<th>&nbsp;</th>
<% end %>
</tr>
</thead>
<tbody>
<% @analytics.each do |a|%>
<tr>
<% a.attributes.each do |k,v| %>
<td><%= v %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<div id="editAcct" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel1" aria-hidden="true">
</div>
<div class="clearfix">
</div>
</div>
</div>
<%= javascript_include_tag "jquery.dataTables.min.js"%>
<script type="text/javascript">
function dataTablePagination(){
$('#data-table').dataTable({
"sPaginationType": "full_numbers"
});
};
$(document).ready(dataTablePagination());
</script>
+70
View File
@@ -0,0 +1,70 @@
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Success Alert -->
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">User information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<!-- Error Alert -->
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Something went wrong. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<!-- User Management Card -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-people-fill text-primary"></i> Manage Users
</h4>
</div>
<div id="userDataTable" class="card-body p-0">
<!-- Loading state -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading users...</span>
</div>
<p class="text-muted mt-3">Loading user data...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%= javascript_include_tag "jquery.dataTables.min.js" %>
<script type="text/javascript">
function makeActive() {
$('li[id="admin"]').addClass('active');
}
function loadTable() {
$("#userDataTable").load("/admin/" + <%= params[:admin_id] %> + "/get_all_users");
}
$(document).ready(function() {
makeActive();
loadTable();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
+63
View File
@@ -0,0 +1,63 @@
<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-people-fill text-primary"></i> Manage Users
</h2>
<p class="text-muted">View and manage all system users</p>
</div>
</div>
<!-- Users Table -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<div id="dt_example" class="example_alt_pagination">
<table class="table table-striped table-hover table-bordered" id="data-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Admin User</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% @users.each do |u| %>
<tr>
<td style="word-wrap:break-word;">
<%= "#{u.first_name} #{u.last_name}" %>
</td>
<td>
<%= u.email %>
</td>
<td class="text-center">
<%= u.admin ? '<i class="bi bi-check-circle-fill text-success" title="Admin"></i>'.html_safe : '<i class="bi bi-dash-circle text-muted" title="Not Admin"></i>'.html_safe %>
</td>
<td>
<%= link_to admin_get_user_path(u.id), class: "btn btn-sm btn-outline-primary" do %>
<i class="bi bi-pencil"></i> Edit
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function dataTablePagination(){
$('#data-table').dataTable({
"sPaginationType": "full_numbers"
});
};
$(document).ready(dataTablePagination());
</script>
+55
View File
@@ -0,0 +1,55 @@
<div class="container-fluid">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm mt-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h4 class="mb-0">
<i class="bi bi-person-gear text-primary"></i> Edit User Account
</h4>
<%= link_to "Back to Users", admin_get_all_users_path(current_user.id), class: "btn btn-outline-secondary btn-sm" %>
</div>
<div class="card-body p-4">
<%= form_for @user, url: admin_update_user_path(params[:admin_id]), method: :patch do |f| %>
<div class="mb-3">
<%= f.label :email, nil, {:class => "form-label"}%>
<%= f.text_field :email, {:class => "form-control"}%>
</div>
<div class="mb-3">
<%= f.label :first_name, nil, {:class => "form-label"}%>
<%= f.text_field :first_name, {:class => "form-control"} %>
</div>
<div class="mb-3">
<%= f.label :last_name, nil, {:class => "form-label"}%>
<%= f.text_field :last_name, {:class => "form-control"} %>
</div>
<div class="mb-3">
<%= f.label :password, "Password (leave blank to keep current)", {:class => "form-label"}%>
<%= f.password_field :password, {:class => "form-control", :placeholder => "Enter new password"}%>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, nil, {:class => "form-label"}%>
<%= f.password_field :password_confirmation, {:class => "form-control", :placeholder => "Confirm new password"} %>
</div>
<div class="mb-4">
<%= f.label :admin, "Administrator", {:class => "form-label"}%>
<%= f.select(:admin, @admin_select, {}, {:class => "form-select"}) %>
</div>
<div class="d-flex justify-content-between">
<%= link_to "Delete User", admin_delete_user_path(params[:admin_id]), method: :post, data: { confirm: "Are you sure you want to delete this user?" }, class: "btn btn-danger" %>
<div>
<%= link_to "Cancel", admin_get_all_users_path(current_user.id), class: "btn btn-secondary me-2" %>
<%= f.submit "Save Changes", class: "btn btn-primary" %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
+282
View File
@@ -0,0 +1,282 @@
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-file-earmark-medical text-primary"></i> Benefit Forms
</h2>
<p class="text-muted">Download benefit documents and upload completed forms</p>
</div>
</div>
<!-- Download Forms Section -->
<div class="row g-3 mb-4">
<!-- Health Insurance Card -->
<div class="col-lg-6">
<div class="card shadow-sm h-100 hover-card">
<div class="card-body text-center p-4">
<div class="mb-3">
<i class="bi bi-heart-pulse-fill" style="font-size: 3rem; color: var(--rg-primary);"></i>
</div>
<h4 class="card-title mb-3">Health Insurance</h4>
<p class="text-muted mb-4">Download your health insurance benefit forms and information</p>
<%= link_to download_path(type: "File", name: "public/docs/Health_n_Stuff.pdf"), class: "btn btn-primary btn-lg" do %>
<i class="bi bi-file-earmark-pdf"></i> Download PDF
<% end %>
</div>
</div>
</div>
<!-- Dental Insurance Card -->
<div class="col-lg-6">
<div class="card shadow-sm h-100 hover-card">
<div class="card-body text-center p-4">
<div class="mb-3">
<i class="bi bi-emoji-smile-fill" style="font-size: 3rem; color: var(--rg-success);"></i>
</div>
<h4 class="card-title mb-3">Dental Insurance</h4>
<p class="text-muted mb-4">Download your dental insurance benefit forms and information</p>
<%= link_to download_path(type: "File", name: "public/docs/Dental_n_Stuff.pdf"), class: "btn btn-success btn-lg" do %>
<i class="bi bi-file-earmark-pdf"></i> Download PDF
<% end %>
</div>
</div>
</div>
</div>
<!-- Upload Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-cloud-upload text-primary"></i> Upload Completed Forms
</h4>
</div>
<div class="card-body p-4">
<%= form_for @benefits, url: upload_path, html: { multipart: true, id: "fi", class: "needs-validation" } do |f| %>
<%= hidden_field "benefits", "backup", value: false %>
<div class="row g-3">
<div class="col-12">
<div class="upload-area p-4 text-center border rounded" style="border: 2px dashed #dee2e6; background: var(--rg-light); transition: all 0.3s;">
<i class="bi bi-cloud-arrow-up" style="font-size: 3rem; color: var(--rg-secondary);"></i>
<h5 class="mt-3 mb-2">Select File to Upload</h5>
<p class="text-muted mb-3">Choose a file from your computer</p>
<div class="file-input-wrapper">
<label for="benefits_upload" class="btn btn-primary mb-3" style="cursor: pointer;">
<i class="bi bi-folder2-open"></i> Choose File
</label>
<%= f.file_field :upload, class: "d-none", id: "benefits_upload" %>
</div>
<div class="selected-file mt-3">
<span class="filename text-muted">
<i class="bi bi-file-earmark"></i> No file selected
</span>
</div>
</div>
</div>
<div class="col-12">
<div class="d-flex gap-2">
<button id="start_upload" type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-upload"></i> Upload File
</button>
<button type="button" class="btn btn-outline-secondary btn-lg" onclick="document.getElementById('fi').reset(); $('.filename').html('<i class=\'bi bi-file-earmark\'></i> No file selected');">
<i class="bi bi-x-circle"></i> Cancel
</button>
</div>
</div>
<div class="col-12">
<!-- Progress Bar -->
<div class="progress" style="height: 25px; display: none;" id="upload-progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" role="progressbar" style="width: 0%;" id="progress-bar">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<div class="col-12">
<!-- Files Table -->
<table class="table table-hover d-none" id="files-table">
<thead class="table-light">
<tr>
<th>File Name</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody class="files" data-toggle="modal-gallery" data-target="#modal-gallery"></tbody>
</table>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Uploaded Files Section -->
<% if @uploaded_files.any? %>
<div class="row mt-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-folder2-open text-success"></i> Uploaded Files
</h4>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th><i class="bi bi-file-earmark"></i> File Name</th>
<th><i class="bi bi-hdd"></i> Size</th>
<th><i class="bi bi-clock"></i> Uploaded</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @uploaded_files.each do |file| %>
<tr>
<td>
<% icon_class = case File.extname(file[:name]).downcase
when '.pdf' then 'bi-file-earmark-pdf text-danger'
when '.doc', '.docx' then 'bi-file-earmark-word text-primary'
when '.jpg', '.jpeg', '.png' then 'bi-file-earmark-image text-success'
else 'bi-file-earmark'
end %>
<i class="bi <%= icon_class %> me-2"></i>
<strong><%= file[:name] %></strong>
</td>
<td><%= number_to_human_size(file[:size]) %></td>
<td><%= file[:modified].strftime("%b %d, %Y at %I:%M %p") %></td>
<td>
<%= link_to download_path(type: "File", name: "public/data/#{file[:name]}"), class: "btn btn-sm btn-outline-primary" do %>
<i class="bi bi-download"></i> Download
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<% end %>
<!-- Info Box -->
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Important Information</h5>
<ul class="mb-0 ps-3">
<li>Download benefit forms, fill them out completely, and upload them back</li>
<li>Accepted file formats: PDF, DOC, DOCX, JPG, PNG</li>
<li>Maximum file size: 10MB</li>
<li>All uploaded documents are securely stored</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive() {
$('li[id="benefit_forms"]').addClass('active');
}
$(document).ready(function() {
makeActive();
// File input change handler
$("#benefits_upload").change(function() {
var fileName = $(this).val();
if (fileName) {
// Extract just the filename from the full path
fileName = fileName.split('\\').pop().split('/').pop();
$(".filename").html('<i class="bi bi-file-earmark-check-fill text-success"></i> ' + fileName);
// Highlight the upload area
$(".upload-area").css({
'border-color': 'var(--rg-success)',
'background': 'rgba(6, 214, 160, 0.05)'
});
} else {
$(".filename").html('<i class="bi bi-file-earmark"></i> No file selected');
$(".upload-area").css({
'border-color': '#dee2e6',
'background': 'var(--rg-light)'
});
}
});
// Form submission handler (for demonstration)
$("#fi").submit(function(e) {
var fileName = $("#benefits_upload").val();
if (!fileName) {
e.preventDefault();
alert('Please select a file to upload');
return false;
}
// Show progress bar
$("#upload-progress").show();
// Simulate upload progress (in real implementation, this would be handled by the server)
var progress = 0;
var interval = setInterval(function() {
progress += 10;
$("#progress-bar").css('width', progress + '%');
$("#progress-text").text(progress + '%');
if (progress >= 100) {
clearInterval(interval);
}
}, 200);
});
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
<style>
.hover-card {
transition: all 0.3s ease;
}
.hover-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
}
.upload-area:hover {
border-color: var(--rg-primary) !important;
background: rgba(230, 57, 70, 0.03) !important;
}
.file-input-wrapper input[type="file"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
#progress-bar {
font-weight: 600;
line-height: 25px;
}
</style>
+94
View File
@@ -0,0 +1,94 @@
<div class="p-4">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Year</th>
<th class="text-end">Visitors</th>
<th class="text-end">Orders</th>
<th class="text-end">Income</th>
<th class="text-end">Expenses</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>2007</strong></td>
<td class="text-end">300</td>
<td class="text-end">800</td>
<td class="text-end">900</td>
<td class="text-end">300</td>
</tr>
<tr>
<td><strong>2008</strong></td>
<td class="text-end">1,170</td>
<td class="text-end">860</td>
<td class="text-end">1,220</td>
<td class="text-end">564</td>
</tr>
<tr>
<td><strong>2009</strong></td>
<td class="text-end">260</td>
<td class="text-end">1,120</td>
<td class="text-end">2,870</td>
<td class="text-end">2,340</td>
</tr>
<tr>
<td><strong>2010</strong></td>
<td class="text-end">1,030</td>
<td class="text-end">540</td>
<td class="text-end">3,430</td>
<td class="text-end">1,200</td>
</tr>
<tr>
<td><strong>2011</strong></td>
<td class="text-end">200</td>
<td class="text-end">700</td>
<td class="text-end">1,700</td>
<td class="text-end">770</td>
</tr>
<tr>
<td><strong>2012</strong></td>
<td class="text-end">1,170</td>
<td class="text-end">2,160</td>
<td class="text-end">3,920</td>
<td class="text-end">800</td>
</tr>
</tbody>
</table>
</div>
<div class="row mt-4 g-3">
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #b5799e;">
<div class="card-body">
<div class="text-muted small mb-1">Total Visitors</div>
<h3 class="mb-0" style="color: #b5799e;">3,130</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Total Orders</div>
<h3 class="mb-0" style="color: #579da9;">6,180</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Total Income</div>
<h3 class="mb-0" style="color: #e26666;">14,040</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Total Expenses</div>
<h3 class="mb-0" style="color: #1e825e;">5,174</h3>
</div>
</div>
</div>
</div>
</div>
+90
View File
@@ -0,0 +1,90 @@
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h4 class="mb-0">
<i class="bi bi-graph-up text-primary"></i> Current Statistics
</h4>
<!-- Chart Type Toggle -->
<div class="btn-group" role="group" aria-label="Chart type selection">
<button id="change_to_bar_graph" class="btn btn-outline-primary btn-sm" title="Bar Graph View" aria-label="Switch to bar graph">
<i class="bi bi-bar-chart-fill"></i> Bar Graph
</button>
<button id="change_to_pie_charts" class="btn btn-outline-primary btn-sm" title="Pie Charts View" aria-label="Switch to pie charts">
<i class="bi bi-pie-chart-fill"></i> Pie Charts
</button>
</div>
</div>
<div id="charts_body" class="card-body p-4">
<!-- Charts will load here dynamically -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading charts...</span>
</div>
<p class="text-muted mt-3">Loading statistics...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive() {
$('li[id="home"]').addClass('active');
}
$("#change_to_bar_graph").click(function(event) {
event.preventDefault();
// Add loading state
$("#charts_body").html('<div class="text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="text-muted mt-3">Loading bar graph...</p></div>');
// Remove active state from other button
$("#change_to_pie_charts").removeClass('active');
$(this).addClass('active');
// Load new content
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "bar_graph").inspect %>);
});
$("#change_to_pie_charts").click(function(event) {
event.preventDefault();
// Add loading state
$("#charts_body").html('<div class="text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="text-muted mt-3">Loading pie charts...</p></div>');
// Remove active state from other button
$("#change_to_bar_graph").removeClass('active');
$(this).addClass('active');
// Load new content
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>);
});
$(document).ready(function() {
makeActive();
// Mark pie charts as default active view
$("#change_to_pie_charts").addClass('active');
// Load default view
$("#charts_body").load(<%= sanitize change_graph_dashboard_index_path(:graph => "pie_charts").inspect %>);
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
<style>
/* Active button state for chart toggles */
#change_to_bar_graph.active,
#change_to_pie_charts.active {
background-color: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
</style>
+275
View File
@@ -0,0 +1,275 @@
<% if @user.paid_time_off %>
<!-- Begin easy pie charts container -->
<div class="easy-pie-charts-container">
<div class="pie-chart">
<div class="chart1" data-percent="100">
<%= @user.paid_time_off.pto_days_remaining %>
</div>
<h5 class="name">
Available PTO
</h5>
</div>
<div class="pie-chart">
<div class="chart2" data-percent="<%= @user.paid_time_off.sick_days_taken_percentage %>">
<%= @user.paid_time_off.sick_days_taken %>
</div>
<h5 class="name">
Sick Days Taken
</h5>
</div>
<div class="pie-chart">
<div class="chart3" data-percent="100">
<%= @user.work_info.income %>
</div>
<h5 class="name">
Income
</h5>
</div>
<div class="pie-chart">
<div class="chart4" data-percent="100">
<%= @user.performance.last.score %>
</div>
<h5 class="name">
Performance Score
</h5>
</div>
<div class="pie-chart">
<div class="chart5" data-percent="91">
<%= @user.retirement.total %>
</div>
<h5 class="name">
401k
</h5>
</div>
<div class="clearfix">
</div>
</div>
<!-- End easy pie charts container -->
<% end %>
<script type="text/javascript">
// Store timeout IDs so we can clear them on page navigation
var chartTimeouts = [];
function pieChartHome() {
// Clear any existing timeouts first
chartTimeouts.forEach(function(id) { clearTimeout(id); });
chartTimeouts = [];
$(function () {
// Only initialize if chart elements exist
if ($('.chart1').length === 0) return;
//create instance
$('.chart1').easyPieChart({
animate: 2000,
barColor: '#e26666',
trackColor: '#dddddd',
scaleColor: '#e26666',
size: 160,
lineWidth: 7,
});
//update instance after 5 sec
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(50);
}
}, 5000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(70);
}
}, 10000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(30);
}
}, 15000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(90);
}
}, 19000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart1').length && $('.chart1').data('easyPieChart')) {
$('.chart1').data('easyPieChart').update(40);
}
}, 32000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart2').length === 0) return;
//create instance
$('.chart2').easyPieChart({
animate: 2000,
barColor: '#b5799e',
trackColor: '#dddddd',
scaleColor: '#b5799e',
size: 160,
lineWidth: 7,
});
//update instance after 5 sec
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(90);
}
}, 10000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(40);
}
}, 18000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(70);
}
}, 28000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(50);
}
}, 32000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart2').length && $('.chart2').data('easyPieChart')) {
$('.chart2').data('easyPieChart').update(80);
}
}, 40000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart3').length === 0) return;
//create instance
$('.chart3').easyPieChart({
animate: 2000,
barColor: '#579da9',
trackColor: '#dddddd',
scaleColor: '#579da9',
size: 160,
lineWidth: 7,
});
//update instance after 5 sec
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(20);
}
}, 9000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(59);
}
}, 20000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(38);
}
}, 35000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(79);
}
}, 49000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart3').length && $('.chart3').data('easyPieChart')) {
$('.chart3').data('easyPieChart').update(96);
}
}, 52000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart4').length === 0) return;
//create instance
$('.chart4').easyPieChart({
animate: 2000,
barColor: '#dba26b',
trackColor: '#dddddd',
scaleColor: '#dba26b',
size: 160,
lineWidth: 7,
});
//update instance after 5 sec
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(40);
}
}, 6000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(67);
}
}, 14000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(43);
}
}, 23000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(80);
}
}, 36000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart4').length && $('.chart4').data('easyPieChart')) {
$('.chart4').data('easyPieChart').update(66);
}
}, 41000));
});
$(function () {
// Only initialize if chart elements exist
if ($('.chart5').length === 0) return;
//create instance
$('.chart5').easyPieChart({
animate: 3000,
barColor: '#1e825e',
trackColor: '#dddddd',
scaleColor: '#1e825e',
size: 160,
lineWidth: 7,
});
//update instance after 5 sec
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(30);
}
}, 9000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(87);
}
}, 19000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(28);
}
}, 27000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(69);
}
}, 39000));
chartTimeouts.push(setTimeout(function () {
if ($('.chart5').length && $('.chart5').data('easyPieChart')) {
$('.chart5').data('easyPieChart').update(99);
}
}, 47000));
});
}
$(document).ready(pieChartHome);
// Clear timeouts when navigating away with Turbolinks
$(document).on('turbolinks:before-visit', function() {
chartTimeouts.forEach(function(id) { clearTimeout(id); });
chartTimeouts = [];
});
</script>
+445
View File
@@ -0,0 +1,445 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RailsGoat - OWASP Security Training</title>
<%#= csrf_meta_tags %> <!-- <~ What is this for? I hear it helps w/ JS and Sea-surfing.....whatevz -->
<!-- VULNERABILITY A03:2025 - Software Supply Chain Failures
Missing Subresource Integrity (SRI) checks on CDN assets
If the CDN is compromised, malicious code can be injected without detection
SECURE: Should include integrity="sha384-..." crossorigin="anonymous"
See: /tutorials/supply_chain for exploitation details
-->
<!-- Load jQuery FIRST - other scripts depend on it -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- Bootstrap CSS and Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- Rails assets - loaded AFTER jQuery -->
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %>
<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
<!-- Bootstrap JS - loaded last -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- FullCalendar and dependencies for PTO page -->
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.js"></script>
<!-- Modern Design System -->
<style>
:root {
--rg-primary: #e63946;
--rg-primary-dark: #d62828;
--rg-secondary: #457b9d;
--rg-secondary-dark: #1d3557;
--rg-success: #06d6a0;
--rg-warning: #ffb703;
--rg-danger: #e63946;
--rg-light: #f8f9fa;
--rg-dark: #1d3557;
--rg-sidebar-width: 250px;
--rg-header-height: 60px;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--rg-light);
min-height: 100vh;
}
/* Modern Header */
.rg-header {
background: white;
border-bottom: none;
height: var(--rg-header-height);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
overflow: visible;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
}
.rg-header .container-fluid {
padding-left: 2rem;
padding-right: 2rem;
}
.rg-header .col-auto:first-child {
padding-left: 0;
margin-left: 0;
}
.rg-brand {
font-size: 1.5rem;
font-weight: 700;
color: var(--rg-primary);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
line-height: 1;
}
.rg-brand:hover {
color: var(--rg-primary-dark);
}
.rg-brand i {
font-size: 1.75rem;
line-height: 1;
display: inline-block;
min-width: 1.75rem;
text-align: center;
}
.rg-header .row {
margin-left: 0;
margin-right: 0;
}
/* Fix alignment of button_to forms in header */
.rg-header form {
margin: 0;
padding: 0;
display: inline-block;
}
/* Modern Sidebar */
.rg-sidebar {
position: fixed;
top: var(--rg-header-height);
left: 0;
bottom: 0;
width: var(--rg-sidebar-width);
background: var(--rg-dark);
color: white;
overflow-y: auto;
transition: transform 0.3s ease;
box-shadow: 2px 0 12px rgba(0,0,0,0.1);
}
.rg-sidebar-nav {
list-style: none;
padding: 1rem 0;
margin: 0;
}
.rg-sidebar-nav li a {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
margin: 0.25rem 0.75rem;
color: rgba(255,255,255,0.8);
text-decoration: none;
transition: all 0.2s;
border-radius: 0.75rem;
}
.rg-sidebar-nav li a:hover,
.rg-sidebar-nav li a.active {
background: rgba(255,255,255,0.15);
color: white;
transform: translateX(4px);
}
.rg-sidebar-nav li a i {
font-size: 1.25rem;
margin-right: 0.75rem;
width: 24px;
text-align: center;
}
/* Main Content */
.rg-main {
margin-top: var(--rg-header-height);
margin-left: var(--rg-sidebar-width);
padding: 2rem;
min-height: calc(100vh - var(--rg-header-height));
}
.rg-main.no-sidebar {
margin-left: 0;
}
/* Modern Cards */
.rg-card, .card {
background: white;
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 1.5rem;
border: none;
overflow: hidden;
}
.rg-card:hover, .card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
transition: box-shadow 0.3s ease;
}
/* Modern Buttons */
.btn {
border-radius: 0.75rem;
padding: 0.5rem 1.25rem;
font-weight: 500;
transition: all 0.2s ease;
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.btn-sm {
border-radius: 0.625rem;
padding: 0.375rem 1rem;
font-size: 0.875rem;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.btn-primary {
background: linear-gradient(135deg, var(--rg-primary) 0%, var(--rg-primary-dark) 100%);
border-color: transparent;
color: white;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--rg-primary-dark) 0%, #c11f2b 100%);
border-color: transparent;
color: white;
}
.btn-outline-primary {
border: 2px solid var(--rg-primary);
color: var(--rg-primary);
background: white;
}
.btn-outline-primary:hover {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.btn-warning {
background: linear-gradient(135deg, #ffb703 0%, #fb8500 100%);
color: white;
border: none;
}
.btn-warning:hover {
background: linear-gradient(135deg, #fb8500 0%, #e85d04 100%);
color: white;
}
.btn-outline-secondary {
border: 2px solid #dee2e6;
color: #6c757d;
background: white;
}
.btn-outline-secondary:hover {
background: #f8f9fa;
border-color: #adb5bd;
color: #495057;
}
/* Modern Alerts */
.alert {
border-radius: 0.875rem;
border: none;
padding: 1rem 1.25rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
/* Modern Tables */
.table {
border-radius: 0.75rem;
overflow: hidden;
}
.table thead th {
background: var(--rg-light);
font-weight: 600;
border-bottom: 2px solid #dee2e6;
padding: 1rem;
}
.table tbody tr {
transition: background-color 0.2s ease;
}
.table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
/* Modern Badges */
.badge {
border-radius: 0.5rem;
padding: 0.35rem 0.65rem;
font-weight: 500;
}
/* Modern Dropdowns */
.dropdown-menu {
border-radius: 0.75rem;
border: none;
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
padding: 0.5rem;
}
.dropdown-item {
border-radius: 0.5rem;
padding: 0.5rem 1rem;
transition: all 0.2s ease;
}
.dropdown-item:hover {
background-color: rgba(230, 57, 70, 0.08);
transform: translateX(2px);
}
/* Modern Widget Styling */
.widget {
background: white;
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 1.5rem;
border: none;
overflow: hidden;
}
.widget-header {
background: var(--rg-light);
border-bottom: none;
padding: 1.25rem 1.5rem;
border-radius: 1rem 1rem 0 0;
}
.widget-header .title {
font-weight: 600;
color: var(--rg-dark);
font-size: 1.1rem;
}
.widget-body {
padding: 1.5rem;
}
/* Modern Login Page */
.rg-login-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--rg-secondary-dark) 0%, var(--rg-secondary) 100%);
position: relative;
}
.rg-login-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 20% 50%, rgba(230, 57, 70, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(69, 123, 157, 0.1) 0%, transparent 50%);
}
.rg-login-card {
background: white;
border-radius: 1.5rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 3rem;
width: 100%;
max-width: 450px;
position: relative;
z-index: 1;
}
.form-control, .form-select {
border-radius: 0.75rem;
border: 2px solid #e9ecef;
padding: 0.75rem 1rem;
transition: all 0.2s ease;
}
.form-control:focus, .form-select:focus {
border-color: var(--rg-primary);
box-shadow: 0 0 0 3px rgba(230, 57, 70, 0.1);
}
.input-group-text {
border-radius: 0.75rem 0 0 0.75rem;
border: 2px solid #e9ecef;
border-right: none;
background: #f8f9fa;
}
.input-group .form-control {
border-radius: 0 0.75rem 0.75rem 0;
}
.rg-login-header {
text-align: center;
margin-bottom: 2rem;
}
.rg-login-logo {
font-size: 3rem;
color: var(--rg-primary);
margin-bottom: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.rg-sidebar {
transform: translateX(-100%);
}
.rg-sidebar.show {
transform: translateX(0);
}
.rg-main {
margin-left: 0;
}
}
/* VULNERABILITY: XSS via cookie font-size */
<%
if cookies[:font]
%>
body { font-size:<%= raw cookies[:font] %> !important;}
<%
end
%>
</style>
</head>
<body>
<%= render "layouts/shared/header" %>
<%= render "layouts/shared/sidebar" %>
<main class="rg-main <%= 'no-sidebar' unless current_user %>">
<%= render "layouts/shared/messages" %>
<%= yield %>
</main>
<%= render "layouts/shared/footer" %>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
<% if current_user %>
<footer class="border-top mt-5 py-4 text-center text-muted bg-white">
<div class="container">
<div class="row">
<div class="col-md-12">
<p class="mb-1">
<i class="bi bi-shield-check"></i>
&copy; <%= Date.current.year %> The Open Worldwide Application Security Project - OWASP
</p>
<p class="small mb-0">
<a href="https://owasp.org" target="_blank" class="text-decoration-none me-3">
<i class="bi bi-globe"></i> OWASP.org
</a>
<a href="https://github.com/OWASP/railsgoat" target="_blank" class="text-decoration-none me-3">
<i class="bi bi-github"></i> GitHub
</a>
<a href="https://github.com/OWASP/railsgoat/wiki" target="_blank" class="text-decoration-none">
<i class="bi bi-book"></i> Documentation
</a>
</p>
</div>
</div>
</div>
</footer>
<% end %>
<!-- Scroll to Top Button -->
<button id="scrollTopBtn" class="btn btn-primary rounded-circle position-fixed bottom-0 end-0 m-4" style="width: 48px; height: 48px; display: none; z-index: 1000;" title="Scroll to top">
<i class="bi bi-arrow-up"></i>
</button>
<script>
// Modern scroll-to-top without jQuery
(function() {
const scrollBtn = document.getElementById('scrollTopBtn');
if (scrollBtn) {
// Show/hide button based on scroll position
window.addEventListener('scroll', function() {
if (window.pageYOffset > 300) {
scrollBtn.style.display = 'block';
} else {
scrollBtn.style.display = 'none';
}
});
// Scroll to top on click
scrollBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
})();
</script>
+115
View File
@@ -0,0 +1,115 @@
<% if current_user %>
<!-- Authenticated Header -->
<header class="rg-header">
<div class="container-fluid h-100">
<div class="row h-100 align-items-center">
<div class="col-auto">
<a href="<%= home_dashboard_index_path %>" class="rg-brand">
<i class="bi bi-shield-fill-exclamation"></i> RailsGoat
</a>
</div>
<div class="col"></div>
<div class="col-auto">
<div class="d-flex align-items-center gap-3">
<!-- Font Size Controls -->
<div class="btn-group btn-group-sm" role="group" aria-label="Font size controls">
<a href="/dashboard/home?font=8pt" class="btn btn-outline-secondary" style="font-size: 10pt;" title="Small font" aria-label="Small font" data-turbolinks="false">
<i class="bi bi-type"></i>
</a>
<a href="/dashboard/home?font=200%25" class="btn btn-outline-secondary" style="font-size: 14pt;" title="Large font" aria-label="Large font" data-turbolinks="false">
<i class="bi bi-type"></i>
</a>
</div>
<!-- Tutorial Link -->
<%= button_to "https://github.com/OWASP/railsgoat/wiki", {
method: "get",
class: "btn btn-sm btn-outline-primary",
onclick: "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank'); return false;"
} do %>
<i class="bi bi-book"></i> Tutorials
<% end %>
<!-- User Dropdown -->
<div class="dropdown">
<button class="btn btn-link text-decoration-none dropdown-toggle d-flex align-items-center gap-2" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
<i class="bi bi-person-fill text-white"></i>
</div>
<!--
VULNERABILITY: XSS via html_safe
I'm going to use HTML safe because we had some weird stuff
going on with funny chars and jquery, plus it says safe so I'm guessing
nothing bad will happen
-->
<span class="text-dark"><%= current_user.first_name.html_safe %></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<%= link_to user_account_settings_path(user_id: current_user.id), class: "dropdown-item" do %>
<i class="bi bi-gear"></i> Account Settings
<% end %>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<%= link_to logout_path, class: "dropdown-item text-danger" do %>
<i class="bi bi-box-arrow-right"></i> Logout
<% end %>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</header>
<% else %>
<!-- Unauthenticated Header -->
<header class="rg-header">
<div class="container-fluid h-100">
<div class="row h-100 align-items-center">
<div class="col-auto">
<a href="<%= login_path %>" class="rg-brand">
<i class="bi bi-shield-fill-exclamation"></i> RailsGoat
</a>
</div>
<div class="col"></div>
<div class="col-auto">
<div class="d-flex align-items-center gap-2">
<%= link_to credentials_tutorials_path, class: "btn btn-sm btn-warning" do %>
<i class="bi bi-key"></i> Demo Credentials
<% end %>
<%= button_to "https://github.com/OWASP/railsgoat/wiki", {
method: "get",
class: "btn btn-sm btn-outline-primary",
onclick: "window.open('https://github.com/OWASP/railsgoat/wiki', '_blank'); return false;"
} do %>
<i class="bi bi-book"></i> Tutorials
<% end %>
<%= button_to signup_path, {
class: "btn btn-sm btn-primary",
method: "get"
} do %>
<i class="bi bi-person-plus"></i> Sign Up
<% end %>
<%= button_to login_path, {
class: "btn btn-sm btn-outline-primary",
method: "get"
} do %>
<i class="bi bi-box-arrow-in-right"></i> Login
<% end %>
</div>
</div>
</div>
</div>
</header>
<% end %>
+38
View File
@@ -0,0 +1,38 @@
<% flash.each do |name, msg| %>
<% name = name.to_sym %>
<%
alert_class = case name
when :error, :alert
'alert-danger'
when :success, :notice
'alert-success'
when :info
'alert-info'
when :warning
'alert-warning'
else
'alert-secondary'
end
icon_class = case name
when :error, :alert
'bi-exclamation-circle-fill'
when :success, :notice
'bi-check-circle-fill'
when :info
'bi-info-circle-fill'
when :warning
'bi-exclamation-triangle-fill'
else
'bi-bell-fill'
end
%>
<div class="alert <%= alert_class %> alert-dismissible show d-flex align-items-center" role="alert">
<i class="bi <%= icon_class %> me-2"></i>
<div class="flex-grow-1">
<%= msg %>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<% end %>
+90
View File
@@ -0,0 +1,90 @@
<% if current_user %>
<nav class="rg-sidebar">
<ul class="rg-sidebar-nav">
<li>
<%= link_to home_dashboard_index_path, class: "#{controller_name == 'dashboard' ? 'active' : ''}" do %>
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
<% end %>
</li>
<% if is_admin? %>
<li class="mt-3">
<div class="px-4 py-2 text-white-50 text-uppercase small fw-bold">Admin</div>
</li>
<li>
<%= link_to admin_dashboard_path(admin_id: "1"), class: "#{controller_name == 'admin' && action_name == 'dashboard' ? 'active' : ''}" do %>
<i class="bi bi-people"></i>
<span>Manage Users</span>
<% end %>
</li>
<li>
<%= link_to admin_analytics_path(admin_id: "1"), class: "#{controller_name == 'admin' && action_name == 'analytics' ? 'active' : ''}" do %>
<i class="bi bi-graph-up"></i>
<span>View Analytics</span>
<% end %>
</li>
<% end %>
<li class="mt-3">
<div class="px-4 py-2 text-white-50 text-uppercase small fw-bold">Employee</div>
</li>
<li>
<%= link_to user_benefit_forms_path(user_id: current_user.id), class: "#{controller_name == 'benefit_forms' ? 'active' : ''}" do %>
<i class="bi bi-file-earmark-text"></i>
<span>Benefit Forms</span>
<% end %>
</li>
<li>
<%= link_to user_retirement_index_path(user_id: current_user.id), class: "#{controller_name == 'retirement' ? 'active' : ''}" do %>
<i class="bi bi-piggy-bank"></i>
<span>401k Info</span>
<% end %>
</li>
<li>
<%= link_to user_paid_time_off_index_path(user_id: current_user.id), class: "#{controller_name == 'paid_time_off' ? 'active' : ''}" do %>
<i class="bi bi-calendar-check"></i>
<span>PTO</span>
<% end %>
</li>
<li>
<%= link_to user_work_info_index_path(user_id: current_user.id), class: "#{controller_name == 'work_info' ? 'active' : ''}" do %>
<i class="bi bi-briefcase"></i>
<span>Work Info</span>
<% end %>
</li>
<li>
<%= link_to user_performance_index_path(user_id: current_user.id), class: "#{controller_name == 'performance' ? 'active' : ''}" do %>
<i class="bi bi-bar-chart"></i>
<span>Performance</span>
<% end %>
</li>
<li>
<%= link_to user_messages_path(user_id: current_user.id), class: "#{controller_name == 'messages' ? 'active' : ''}" do %>
<i class="bi bi-envelope"></i>
<span>Messages</span>
<% end %>
</li>
<li>
<%= link_to user_pay_index_path(user_id: current_user.id), class: "#{controller_name == 'pay' ? 'active' : ''}" do %>
<i class="bi bi-credit-card"></i>
<span>Pay</span>
<% end %>
</li>
<li class="mt-4 pt-4 border-top border-secondary">
<div class="px-4 py-2 text-white-50 small">
<i class="bi bi-shield-exclamation"></i>
OWASP RailsGoat <%= Rails::VERSION::STRING %>
</div>
</li>
</ul>
</nav>
<% end %>
+330
View File
@@ -0,0 +1,330 @@
<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-envelope-fill text-primary"></i> Messages
</h2>
<p class="text-muted">Inbox for <%= current_user.full_name %></p>
</div>
</div>
<div class="row g-3">
<!-- Messages Inbox -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-inbox text-primary"></i> Inbox
</h4>
<p class="text-muted mb-0 small mt-1">Your received messages</p>
</div>
<div class="card-body p-0">
<% if @messages.any? %>
<div class="messages-list">
<% @messages.each do |message| %>
<div class="message-item">
<div class="message-avatar">
<div class="avatar-circle">
<i class="bi bi-person-fill"></i>
</div>
</div>
<div class="message-content">
<div class="message-header">
<div class="message-from">
<strong><%= message.creator_name %></strong>
</div>
<div class="message-date">
<i class="bi bi-calendar3 me-1"></i>
<%= message.created_at.strftime("%b %d, %Y") %>
</div>
</div>
<div class="message-text">
<%= message.message %>
</div>
<div class="message-actions">
<%= link_to user_message_path(:id => message.id), class: "btn btn-sm btn-outline-primary" do %>
<i class="bi bi-eye"></i> Details
<% end %>
<%= link_to user_message_path(:id => message.id), method: 'delete', data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-outline-danger" do %>
<i class="bi bi-trash"></i> Delete
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="empty-state">
<i class="bi bi-inbox"></i>
<h5>No Messages Yet</h5>
<p class="text-muted">Your inbox is empty. Send a message to get started!</p>
</div>
<% end %>
</div>
</div>
</div>
<!-- Send Message Form -->
<div class="col-lg-4">
<div class="card shadow-sm sticky-top" style="top: 80px; border-left: 4px solid var(--rg-success);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.05), rgba(30, 130, 94, 0.05));">
<h4 class="mb-0">
<i class="bi bi-send text-success"></i> Send Message
</h4>
<p class="text-muted mb-0 small mt-1">Compose a new message</p>
</div>
<div class="card-body p-4">
<!-- Alert Messages -->
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Success!</strong>
<p class="mb-0 small">Message sent successfully.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Error!</strong>
<p class="mb-0 small">Failed to send message.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<%= form_for @message, url: user_messages_path, method: :post, html: { id: "send_message" } do |f| %>
<%= f.hidden_field :creator_id, value: current_user.id %>
<%= f.hidden_field :read, value: '0' %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-person-circle text-success me-2"></i>To
</label>
<%= f.select(:receiver_id,
options_from_collection_for_select(User.all, :id, :full_name),
{ prompt: "Select a recipient..." },
{ class: "form-control form-control-lg", id: "message_receiver_id" }) %>
<small class="text-muted">Select message recipient</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-chat-left-text text-success me-2"></i>Message
</label>
<%= f.text_area :message,
class: "form-control form-control-lg",
rows: 6,
placeholder: "Type your message here...",
style: "resize: vertical;" %>
<small class="text-muted">Write your message content</small>
</div>
<div class="d-grid">
<%= f.submit "Send Message",
id: 'submit_button',
class: "btn btn-success btn-lg" %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-success me-1"></i>
<strong>Tip:</strong> Messages are delivered instantly
</small>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive(){
$('li[id="messages"]').addClass('active');
}
$(document).ready(function() {
makeActive();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
// Form submission with AJAX
$("#submit_button").click(function(event) {
event.preventDefault();
var valuesToSubmit = $("#send_message").serialize();
$.ajax({
url: <%= "/users/#{current_user.id}/messages.json".inspect.html_safe %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
if (response.msg == "failure") {
$('#failure').show(500).delay(2000).fadeOut();
} else {
$('#success').show(500).delay(2000).fadeOut();
// Clear form on success
$('#send_message')[0].reset();
// Reload page after delay to show new message
setTimeout(function() {
location.reload();
}, 2500);
}
},
error: function(event) {
$('#failure').show(500).delay(2000).fadeOut();
}
});
});
</script>
<style>
/* Messages List Styling */
.messages-list {
display: flex;
flex-direction: column;
}
.message-item {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-bottom: 1px solid #e9ecef;
transition: background-color 0.2s ease;
}
.message-item:hover {
background-color: rgba(230, 57, 70, 0.03);
}
.message-item:last-child {
border-bottom: none;
}
.message-avatar {
flex-shrink: 0;
}
.avatar-circle {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, var(--rg-primary) 0%, var(--rg-primary-dark) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
box-shadow: 0 2px 8px rgba(230, 57, 70, 0.2);
}
.message-content {
flex: 1;
min-width: 0;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.message-from {
font-size: 1.1rem;
color: var(--rg-dark);
}
.message-date {
font-size: 0.875rem;
color: #6c757d;
white-space: nowrap;
}
.message-text {
color: #495057;
margin-bottom: 1rem;
line-height: 1.6;
word-wrap: break-word;
}
.message-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6c757d;
}
.empty-state i {
font-size: 4rem;
opacity: 0.3;
margin-bottom: 1rem;
}
.empty-state h5 {
margin-bottom: 0.5rem;
color: #495057;
}
/* Sticky Form */
@media (min-width: 992px) {
.sticky-top {
position: sticky;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.message-item {
flex-direction: column;
text-align: center;
}
.message-header {
flex-direction: column;
text-align: center;
}
.message-actions {
justify-content: center;
}
}
/* Fix dropdown height and padding to prevent text cutoff */
#message_receiver_id {
min-height: 48px !important;
line-height: 1.5 !important;
padding: 0.5rem 0.75rem !important;
vertical-align: middle !important;
box-sizing: border-box !important;
}
/* Ensure dropdown options have proper spacing too */
#message_receiver_id option {
padding: 0.5rem 0.75rem !important;
line-height: 1.5 !important;
}
/* Ensure dropdown has proper focus styling */
#message_receiver_id:focus {
border-color: var(--rg-success);
box-shadow: 0 0 0 3px rgba(6, 214, 160, 0.1);
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span12">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe088;"></span> Messages for <%= current_user.full_name %>
</div>
</div>
<div class="widget-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:7%">From:</th>
<th style="width:6%">Date</th>
<th style="width:30%">Message</th>
<th style="width:6%">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td><%= @message.creator_name %></td>
<td><%= @message.created_at.to_date %></td>
<td><%= @message.message %></td>
<td><%= link_to "Delete", user_message_path, {:id => "@message.id", :method => 'delete', :class => "btn btn-danger pull-left"}%></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive(){
$('li[id="messages"]').addClass('active');
};
$(document).ready(function () {
makeActive()
});
</script>
+297
View File
@@ -0,0 +1,297 @@
<div class="container-fluid">
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<!-- Calendar and Schedule Form Row -->
<div class="row g-3">
<!-- PTO Calendar -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-calendar3 text-primary"></i> PTO Calendar
</h4>
</div>
<div id="calendarDiv" class="card-body">
<div id='calendar'></div>
</div>
</div>
</div>
<!-- Schedule PTO Form -->
<div class="col-lg-6">
<div class="card shadow-sm" style="border-left: 4px solid var(--rg-primary);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(230, 57, 70, 0.05), rgba(214, 40, 40, 0.05));">
<h4 class="mb-0">
<i class="bi bi-calendar-plus text-primary"></i> Schedule PTO
</h4>
<p class="text-muted mb-0 small mt-1">Plan your time away from work</p>
</div>
<div id="scheduleDiv" class="card-body p-4">
<%= form_for @schedule, url: "#", html: { id: "cal_update" } do |s| %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-tag-fill text-primary me-2"></i>Event Name
</label>
<%= s.text_field :event_name, {
placeholder: "e.g., Summer Vacation, Personal Day",
class: "form-control form-control-lg"
} %>
</div>
<%= s.text_field :event_type, type: "hidden", value: "pto" %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-chat-left-text-fill text-primary me-2"></i>Event Description
</label>
<%= s.text_field :event_desc, {
placeholder: "e.g., Family trip to Hawaii, Medical appointment",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Optional: Add details about your time off</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold" for="date_range1">
<i class="bi bi-calendar-event-fill text-primary me-2"></i>Event Dates
</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-white">
<i class="bi bi-calendar-range text-primary"></i>
</span>
<input type="text" name="date_range1" id="date_range1" class="form-control date_picker" placeholder="Click to select date range"/>
</div>
<small class="text-muted">Choose the start and end dates for your PTO</small>
</div>
<div class="d-grid">
<%= s.submit "Schedule PTO", {
id: 'cal_update_submit',
class: "btn btn-primary btn-lg"
} %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-primary me-1"></i>
<strong>Tip:</strong> Your PTO request will appear on the calendar after submission
</small>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Sick Days Stats -->
<div class="row mt-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-bandaid text-primary"></i> Sick Days
</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Days Earned</div>
<h3 class="mb-0" style="color: #579da9;"><%= @pto.sick_days_earned %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Days Taken</div>
<h3 class="mb-0" style="color: #e26666;"><%= @pto.sick_days_taken %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Days Remaining</div>
<h3 class="mb-0" style="color: #1e825e;"><%= @pto.sick_days_remaining %></h3>
</div>
</div>
</div>
</div>
<div class="text-center text-muted mt-3 small">
<i class="bi bi-info-circle"></i> As of today: <%= Date.today.strftime("%B %d, %Y") %>
</div>
</div>
</div>
</div>
</div>
<!-- PTO Stats -->
<div class="row mt-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-umbrella-fill text-primary"></i> Paid Time Off
</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Days Earned</div>
<h3 class="mb-0" style="color: #579da9;"><%= @pto.pto_earned %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Days Taken</div>
<h3 class="mb-0" style="color: #e26666;"><%= @pto.pto_taken %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Days Remaining</div>
<h3 class="mb-0" style="color: #1e825e;"><%= @pto.pto_days_remaining %></h3>
</div>
</div>
</div>
</div>
<div class="text-center text-muted mt-3 small">
<i class="bi bi-info-circle"></i> As of today: <%= Date.today.strftime("%B %d, %Y") %>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive() {
$('li[id="pto"]').addClass('active');
}
$(document).ready(function() {
makeActive();
// Initialize FullCalendar
$('#calendar').fullCalendar({
events: <%= get_pto_schedule_schedule_index_path(:format => "json").inspect.html_safe %>,
height: 'auto',
contentHeight: 'auto',
aspectRatio: 1.5
});
// Initialize date range picker
$('.date_picker').daterangepicker({
opens: 'right',
locale: {
format: 'MM/DD/YYYY'
}
});
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
// Form submission
$("#cal_update_submit").click(function(event) {
event.preventDefault();
var valuesToSubmit = $("#cal_update").serialize();
$.ajax({
url: "/schedule.json",
data: valuesToSubmit,
type: "POST",
success: function(response) {
if (response.msg == "failure") {
$('#failure').show(500).delay(1500).fadeOut();
} else {
$('#success').show(500).delay(1500).fadeOut();
$('#calendar').fullCalendar('refetchEvents');
// Clear form
$('#cal_update')[0].reset();
}
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
</script>
<style>
/* FullCalendar modern styling */
#calendar {
border-radius: 0.5rem;
}
.fc-toolbar {
background: var(--rg-light);
padding: 1rem;
border-radius: 0.5rem 0.5rem 0 0;
}
.fc-button {
background: var(--rg-primary) !important;
border-color: var(--rg-primary) !important;
border-radius: 0.5rem !important;
text-transform: none !important;
padding: 0.375rem 0.75rem !important;
}
.fc-button:hover {
background: var(--rg-primary-dark) !important;
border-color: var(--rg-primary-dark) !important;
}
.fc-day-header {
background: var(--rg-light);
padding: 0.75rem;
font-weight: 600;
}
.fc-event {
background: var(--rg-primary);
border-color: var(--rg-primary);
border-radius: 0.375rem;
padding: 0.25rem 0.5rem;
}
.fc-today {
background: rgba(230, 57, 70, 0.05) !important;
}
</style>
@@ -0,0 +1,60 @@
<div class="rg-login-wrapper">
<div class="rg-login-card">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-key-fill"></i>
</div>
<h2 class="mb-1">Reset Password</h2>
<p class="text-muted mb-0">We'll send you a reset link</p>
</div>
<%= form_tag "forgot_password", html: { class: "needs-validation", novalidate: true } do %>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<%= text_field_tag :email, params[:email], {
class: "form-control",
id: "email",
placeholder: "you@example.com",
required: true,
autofocus: true,
type: "email"
} %>
</div>
<div class="form-text">Enter the email address associated with your account</div>
</div>
<div class="d-grid gap-2">
<%= submit_tag "Send Reset Link", class: "btn btn-primary btn-lg" %>
</div>
<hr class="my-4">
<div class="text-center">
<p class="text-muted mb-2">Remember your password?</p>
<%= link_to login_path, class: "btn btn-outline-primary" do %>
<i class="bi bi-arrow-left"></i> Back to Login
<% end %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border: 2px solid rgba(69, 123, 157, 0.3);">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2 mt-1" style="font-size: 1.25rem; color: var(--rg-secondary);"></i>
<div class="small">
<strong class="d-block mb-1">Password Reset Help</strong>
If you don't receive an email within a few minutes, check your spam folder or contact support.
</div>
</div>
</div>
</div>
</div>
<style>
/* Override main content styling for password reset page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
@@ -0,0 +1,75 @@
<div class="rg-login-wrapper">
<div class="rg-login-card">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-shield-lock-fill"></i>
</div>
<h2 class="mb-1">Create New Password</h2>
<p class="text-muted mb-0">Choose a strong, unique password</p>
</div>
<!-- TODO: This form is just a placeholder with no working functionality -->
<%= form_tag "password_resets", html: { class: "needs-validation", novalidate: true } do %>
<%= hidden_field_tag 'user', Base64.encode64(Marshal.dump(@user)) %>
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<%= password_field_tag :password, params[:password], {
class: "form-control",
id: "password",
placeholder: "Enter new password",
required: true,
autofocus: true,
minlength: 6
} %>
</div>
<div class="form-text">Password must be at least 6 characters long</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirm New Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
<%= password_field_tag :confirm_password, params[:confirm_password], {
class: "form-control",
id: "confirm_password",
placeholder: "Re-enter new password",
required: true
} %>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<%= submit_tag "Create New Password", class: "btn btn-primary btn-lg" %>
</div>
<hr class="my-4">
<div class="text-center">
<%= link_to login_path, class: "btn btn-outline-secondary" do %>
<i class="bi bi-arrow-left"></i> Back to Login
<% end %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.1), rgba(17, 138, 178, 0.1)); border: 2px solid rgba(6, 214, 160, 0.3);">
<div class="d-flex align-items-start">
<i class="bi bi-shield-check me-2 mt-1" style="font-size: 1.25rem; color: var(--rg-success);"></i>
<div class="small">
<strong class="d-block mb-1">Password Security Tips</strong>
Use a mix of letters, numbers, and symbols. Avoid common words or personal information.
</div>
</div>
</div>
</div>
</div>
<style>
/* Override main content styling for password reset page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+441
View File
@@ -0,0 +1,441 @@
<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-bank text-primary"></i> Direct Deposit & Pay
</h2>
<p class="text-muted">Manage your direct deposit accounts and payment settings</p>
</div>
</div>
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<div class="row g-4">
<!-- Left Column - Forms -->
<div class="col-lg-5">
<!-- Add Direct Deposit Form -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="bi bi-plus-circle text-success me-2"></i>Add Direct Deposit
</h5>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "bank_info_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-bank2 text-success me-2"></i>Bank Account Number
</label>
<%= text_field_tag :bank_account_num, params[:bank_account_num], {
placeholder: "Enter account number",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Your bank account number</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-diagram-3 text-success me-2"></i>Bank Routing Number
</label>
<%= text_field_tag :bank_routing_num, params[:bank_routing_num], {
placeholder: "9-digit routing number",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Usually found at the bottom of checks</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-percent text-success me-2"></i>Percentage of Deposit
</label>
<%= text_field_tag :dd_percent, params[:dd_percent], {
placeholder: "e.g., 100",
class: "form-control form-control-lg"
} %>
<small class="text-muted">What percentage to deposit (1-100)</small>
</div>
<div class="d-grid">
<%= submit_tag "Add Account", {
id: "dd_form_btn",
class: "btn btn-success btn-lg"
} %>
</div>
<% end %>
</div>
</div>
<!-- Decrypt Form -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="bi bi-unlock text-warning me-2"></i>Decrypt Account
</h5>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "decrypt_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-key-fill text-warning me-2"></i>Encrypted Account Number
</label>
<%= text_field_tag :value_to_decrypt, params[:value_to_decrypt], {
placeholder: "Paste encrypted value",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Copy from the table to the right</small>
</div>
<div class="d-grid">
<%= submit_tag "Decrypt", {
id: "decrypt_btn",
class: "btn btn-warning btn-lg"
} %>
</div>
<% end %>
</div>
</div>
</div>
<!-- Right Column - Accounts Table -->
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-list-ul text-primary me-2"></i>Your Accounts
</h5>
<button type="button" class="btn btn-sm btn-outline-secondary" id="encrypted_acct_question">
<i class="bi bi-question-circle me-1"></i> Why Encrypted?
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="data_table">
<thead class="table-light">
<tr>
<th>Account Number</th>
<th>Routing Number</th>
<th>Deposit %</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- DataTable will populate this -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<%= javascript_include_tag "jquery.dataTables.min.js" %>
<script type="text/javascript">
/*
buildDeleteLink accepts a direct deposit ID and builds a link that enables
a user to delete that direct deposit entry
*/
function buildDeleteLink(dd_id){
var link = '<a href="/users/' + '<%= current_user.id %>' + '/pay/'+ dd_id + '" data-method="delete" rel="nofollow" class="btn btn-sm btn-outline-danger delete-row">' +
'<i class="bi bi-trash"></i> Delete</a>'
return link
};
/*
parseDirectDepositInfo accepts the response object and parses the JSON response, then
populates the direct deposit data table.
*/
function parseDirectDepostInfo(response){
var msg = jQuery.parseJSON(JSON.stringify(response));
var table = $('#data_table').DataTable();
$.each(msg.user, function(index, val){
table.row.add( [
'<code class="text-monospace">' + val.bank_account_num + '</code>',
'<span class="badge bg-light text-dark">' + val.bank_routing_num + '</span>',
'<span class="badge bg-success">' + val.percent_of_deposit + '%</span>',
buildDeleteLink(val.id)
] );
});
table.draw();
};
/*
createDataTable initializes the dd table as a datatable
*/
function createDataTable(){
// Check if DataTable is already initialized
if ($.fn.DataTable.isDataTable('#data_table')) {
$('#data_table').DataTable().destroy();
}
$('#data_table').DataTable({
"sPaginationType": "full_numbers",
"language": {
"emptyTable": "No direct deposit accounts configured yet"
},
"autoWidth": false,
"searching": true,
"ordering": true,
"columns": [
{ "title": "Account Number" },
{ "title": "Routing Number" },
{ "title": "Deposit %" },
{ "title": "Actions", "orderable": false }
]
});
};
/*
populateTable will first clear the existing dd table, then call the appropriate
endpoint to retrieve direct deposit entries and finally, provide parseDirectDepositInfo
with the response from the endpoint in order to populate the data table.
*/
function populateTable() {
var table = $('#data_table').DataTable();
table.clear();
$.ajax({
url: <%= sanitize(user_pay_path(:format => "json", user_id: current_user.id, id: current_user.id).inspect) %>,
type: "GET",
success: function(response) {
parseDirectDepostInfo(response);
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
};
/*
This function doesn't really work right now but is supposed to offer the user a
"delete confirmation" message
*/
$(document).on('click', '.delete-row', function (e) {
var conf = confirm('Are you sure you want to delete this account?');
if (!conf) {
e.preventDefault();
return false;
}
});
/*
decryptShow parses the json response from the application and then renders
a decrypted version of the user's account number
*/
function decryptShow(response){
var msg = jQuery.parseJSON(JSON.stringify(response));
// Modern alert using Bootstrap modal-like appearance
var alertHtml = '<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 100px; left: 50%; transform: translateX(-50%); z-index: 9999; min-width: 400px; box-shadow: 0 4px 16px rgba(0,0,0,0.2);">' +
'<div class="d-flex align-items-center">' +
'<i class="bi bi-unlock-fill me-3" style="font-size: 2rem;"></i>' +
'<div>' +
'<h5 class="alert-heading mb-1">Decrypted Account Number</h5>' +
'<p class="mb-0"><strong style="font-size: 1.2rem; font-family: monospace;">' + msg.account_num + '</strong></p>' +
'</div>' +
'</div>' +
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' +
'</div>';
$('body').append(alertHtml);
// Auto-remove after 5 seconds
setTimeout(function() {
$('.alert').fadeOut(function() {
$(this).remove();
});
}, 5000);
};
/*
This function overrides the decrypt buttons (submit button's) native behavior,
allowing an ajax call to be made with the decrypt_form's inputs which is decrypted
server side with a JSON response containing the decrypted value. The decrypted value is
then passed to decryptShow();
*/
$("#decrypt_btn").click(function(event){
var valuesToSubmit = $("#decrypt_form").serialize();
event.preventDefault();
$.ajax({
url: <%= sanitize(decrypted_bank_acct_num_user_pay_index_path(:format => "json", user_id: current_user.id).inspect) %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
decryptShow(response);
$('#decrypt_form')[0].reset();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
/*
This function overrides the dd_form_btn's native behavior in order to submit an ajax request
that updates the user's direct deposit information. Upon success, the populateTable() function
is called in order to update the dataTable on the page to reflect the latest entry.
*/
$("#dd_form_btn").click(function(event) {
var valuesToSubmit = $("#bank_info_form").serialize();
event.preventDefault();
$.ajax({
url: <%= sanitize(update_dd_info_user_pay_index_path(:format => "json").inspect) %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
$('#bank_info_form')[0].reset();
populateTable();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
$("#encrypted_acct_question").click(function(event) {
event.preventDefault();
// Create modern Bootstrap modal-like alert
var modalHtml = '<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">' +
'<div class="modal-dialog modal-dialog-centered">' +
'<div class="modal-content">' +
'<div class="modal-header" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border-left: 4px solid var(--rg-secondary);">' +
'<h5 class="modal-title"><i class="bi bi-shield-lock-fill text-primary me-2"></i>Why Are Account Numbers Encrypted?</h5>' +
'<button type="button" class="btn-close" onclick="$(this).closest(\'.modal\').remove();"></button>' +
'</div>' +
'<div class="modal-body">' +
'<p class="mb-3"><i class="bi bi-check-circle-fill text-success me-2"></i><strong>For your safety</strong>, your account number is stored encrypted in our database and presented to you in an encrypted form.</p>' +
'<p class="mb-0"><i class="bi bi-unlock-fill text-warning me-2"></i><strong>For your convenience</strong>, you can decrypt your bank account number at any time using our conveniently located decryption function.</p>' +
'</div>' +
'<div class="modal-footer">' +
'<button type="button" class="btn btn-primary" onclick="$(this).closest(\'.modal\').remove();">Got It</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
$('body').append(modalHtml);
});
/*
Make the sidebar element "Pay" active.
*/
function makeActive(){
$('li[id="pay"]').addClass('active');
};
/*
Initialize page - called on both ready and turbolinks:load
*/
function initializePage() {
makeActive();
createDataTable();
populateTable();
}
// Handle normal page loads
$(document).ready(function() {
initializePage();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
initializePage();
});
</script>
<style>
/* DataTables styling adjustments */
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.375rem 0.75rem;
margin: 0 0.125rem;
border-radius: 0.5rem;
border: 1px solid #dee2e6;
background: white;
color: var(--rg-dark);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.dataTables_filter input {
border-radius: 0.75rem;
border: 2px solid #e9ecef;
padding: 0.5rem 1rem;
}
.dataTables_filter input:focus {
border-color: var(--rg-primary);
outline: none;
box-shadow: 0 0 0 3px rgba(230, 57, 70, 0.1);
}
.text-monospace {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
/* Table hover effect */
#data_table tbody tr {
transition: background-color 0.2s ease;
}
#data_table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
/* Override focus colors for specific forms */
#bank_info_form .form-control:focus {
border-color: var(--rg-success);
box-shadow: 0 0 0 3px rgba(6, 214, 160, 0.1);
}
#decrypt_form .form-control:focus {
border-color: var(--rg-warning);
box-shadow: 0 0 0 3px rgba(255, 183, 3, 0.1);
}
</style>
+347
View File
@@ -0,0 +1,347 @@
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-graph-up-arrow text-primary"></i> Performance Reviews
</h2>
<p class="text-muted">Track your performance history and feedback</p>
</div>
</div>
<!-- Performance Summary Stats -->
<div class="row g-3 mb-4">
<%
total_reviews = @perf.count
avg_score = @perf.any? ? (@perf.sum(&:score).to_f / total_reviews).round(1) : 0
latest_score = @perf.last&.score || 0
highest_score = @perf.any? ? @perf.max_by(&:score).score : 0
%>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #579da9;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-star-fill" style="font-size: 2.5rem; color: #579da9;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Average Score
</h6>
<h2 class="mb-0" style="color: #579da9; font-weight: 700; font-size: 2.5rem;">
<%= avg_score %>
</h2>
<p class="text-muted mt-2 mb-0 small">Out of 5.0</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid var(--rg-primary);">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-trophy-fill" style="font-size: 2.5rem; color: var(--rg-primary);"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Highest Score
</h6>
<h2 class="mb-0" style="color: var(--rg-primary); font-weight: 700; font-size: 2.5rem;">
<%= highest_score %>
</h2>
<p class="text-muted mt-2 mb-0 small">Best performance</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #1e825e;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-calendar-check-fill" style="font-size: 2.5rem; color: #1e825e;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Latest Score
</h6>
<h2 class="mb-0" style="color: #1e825e; font-weight: 700; font-size: 2.5rem;">
<%= latest_score %>
</h2>
<p class="text-muted mt-2 mb-0 small">Most recent review</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #b5799e;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-file-earmark-text-fill" style="font-size: 2.5rem; color: #b5799e;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Total Reviews
</h6>
<h2 class="mb-0" style="color: #b5799e; font-weight: 700; font-size: 2.5rem;">
<%= total_reviews %>
</h2>
<p class="text-muted mt-2 mb-0 small">Performance evaluations</p>
</div>
</div>
</div>
</div>
<!-- Performance Timeline -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-graph-up text-primary"></i> Performance Trend
</h4>
<p class="text-muted mb-0 small mt-1">Your performance scores over time</p>
</div>
<div class="card-body p-4">
<% if @perf.any? %>
<div class="performance-timeline">
<% @perf.each_with_index do |p, index| %>
<%
score_percentage = (p.score.to_f / 5.0) * 100
score_color = case p.score
when 5 then '#1e825e'
when 4 then '#579da9'
when 3 then '#ffb703'
else '#e26666'
end
%>
<div class="timeline-item" style="animation-delay: <%= index * 0.1 %>s;">
<div class="timeline-marker">
<div class="timeline-date">
<small class="text-muted"><%= p.date_submitted %></small>
</div>
<div class="timeline-dot" style="background-color: <%= score_color %>;">
<span style="font-weight: 700; color: white; font-size: 0.9rem;"><%= p.score %></span>
</div>
<div class="timeline-reviewer">
<small class="text-muted"><%= p.reviewer_name %></small>
</div>
</div>
<div class="timeline-content">
<div class="progress" style="height: 30px; border-radius: 15px;">
<div class="progress-bar" role="progressbar"
style="width: <%= score_percentage %>%; background-color: <%= score_color %>; font-weight: 600; font-size: 1rem;"
aria-valuenow="<%= p.score %>" aria-valuemin="0" aria-valuemax="5">
<%= p.score %> / 5 - <%= p.comments %>
</div>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center text-muted py-5">
<i class="bi bi-graph-up" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3 mb-0">No performance data to display</p>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Performance History Table -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-table text-primary"></i> Performance History
</h4>
<p class="text-muted mb-0 small mt-1">Detailed review feedback and comments</p>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 20%;">
<i class="bi bi-person-badge me-2"></i>Reviewer
</th>
<th style="width: 15%;">
<i class="bi bi-calendar-event me-2"></i>Date
</th>
<th style="width: 10%;">
<i class="bi bi-star me-2"></i>Score
</th>
<th style="width: 55%;">
<i class="bi bi-chat-left-text me-2"></i>Comments
</th>
</tr>
</thead>
<tbody>
<% if @perf.any? %>
<% @perf.each do |p| %>
<tr>
<td class="fw-semibold">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary bg-opacity-10 p-2 me-2">
<i class="bi bi-person-fill text-primary"></i>
</div>
<%= p.reviewer_name %>
</div>
</td>
<td>
<span class="badge bg-light text-dark">
<i class="bi bi-calendar-date me-1"></i><%= p.date_submitted %>
</span>
</td>
<td>
<%
score_color = case p.score
when 5 then 'success'
when 4 then 'primary'
when 3 then 'warning'
else 'danger'
end
%>
<span class="badge bg-<%= score_color %>" style="font-size: 1rem; padding: 0.5rem 0.75rem;">
<%= p.score %> / 5
</span>
</td>
<td>
<div class="text-muted">
<%= p.comments %>
</div>
</td>
</tr>
<% end %>
<% else %>
<tr>
<td colspan="4" class="text-center text-muted py-5">
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3 mb-0">No performance reviews available yet</p>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive(){
$('li[id="performance"]').addClass('active');
}
$(document).ready(function() {
makeActive();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
<style>
.hover-stat-card {
transition: all 0.3s ease;
}
.hover-stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
}
.hover-stat-card h2 {
transition: transform 0.3s ease;
}
.hover-stat-card:hover h2 {
transform: scale(1.05);
}
.table tbody tr {
transition: background-color 0.2s ease;
}
.table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
/* Performance Timeline Styles */
.performance-timeline {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.timeline-item {
display: flex;
align-items: center;
gap: 1.5rem;
opacity: 0;
animation: fadeInUp 0.6s ease forwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.timeline-marker {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
gap: 0.5rem;
}
.timeline-dot {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: transform 0.3s ease;
}
.timeline-item:hover .timeline-dot {
transform: scale(1.15);
}
.timeline-content {
flex: 1;
}
.timeline-date, .timeline-reviewer {
text-align: center;
white-space: nowrap;
}
.progress {
transition: transform 0.3s ease;
}
.timeline-item:hover .progress {
transform: translateX(5px);
}
@media (max-width: 768px) {
.timeline-item {
flex-direction: column;
text-align: center;
}
.timeline-marker {
min-width: auto;
}
}
</style>
+181
View File
@@ -0,0 +1,181 @@
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-piggy-bank-fill text-primary"></i> 401(k) Retirement Plan
</h2>
<p class="text-muted">Your retirement savings summary and employee benefits</p>
</div>
</div>
<!-- Contribution Stats -->
<div class="row g-3 mb-4">
<!-- Employee Contribution -->
<div class="col-lg-4">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #579da9;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-person-fill-check" style="font-size: 3rem; color: #579da9;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Employee Contribution
</h6>
<h2 class="mb-0" style="color: #579da9; font-weight: 700; font-size: 2.5rem;">
<%= @info.employee_contrib %>
</h2>
<p class="text-muted mt-2 mb-0 small">Your contributions to date</p>
</div>
</div>
</div>
<!-- Employer Contribution -->
<div class="col-lg-4">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid #1e825e;">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-building-fill-check" style="font-size: 3rem; color: #1e825e;"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Employer Contribution
</h6>
<h2 class="mb-0" style="color: #1e825e; font-weight: 700; font-size: 2.5rem;">
<%= @info.employer_contrib %>
</h2>
<p class="text-muted mt-2 mb-0 small">MetaCorp matching funds</p>
</div>
</div>
</div>
<!-- Total Contribution -->
<div class="col-lg-4">
<div class="card shadow-sm text-center hover-stat-card" style="border-top: 4px solid var(--rg-primary); background: linear-gradient(135deg, rgba(230, 57, 70, 0.03), rgba(214, 40, 40, 0.03));">
<div class="card-body p-4">
<div class="mb-3">
<i class="bi bi-cash-stack" style="font-size: 3rem; color: var(--rg-primary);"></i>
</div>
<h6 class="text-muted text-uppercase mb-2" style="font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;">
Total Contribution
</h6>
<h2 class="mb-0" style="color: var(--rg-primary); font-weight: 700; font-size: 2.5rem;">
<%= @info.total %>
</h2>
<p class="text-muted mt-2 mb-0 small">Combined retirement savings</p>
</div>
</div>
</div>
</div>
<!-- Employee Services Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm" style="border-left: 5px solid var(--rg-secondary);">
<div class="card-body p-4">
<div class="row align-items-center">
<div class="col-md-2 text-center mb-3 mb-md-0">
<i class="bi bi-person-workspace" style="font-size: 4rem; color: var(--rg-secondary);"></i>
</div>
<div class="col-md-10">
<h4 class="mb-3">
<i class="bi bi-star-fill text-warning"></i> Employee Retirement Services
</h4>
<p class="mb-3" style="line-height: 1.7;">
Saving for retirement can be difficult. Choosing the plan that is right for you is incredibly important.
MetaCorp understands this and offers <strong>free one-on-one interaction with a savings counselor</strong>.
This service is available weekly, Monday through Wednesday.
</p>
<div class="alert alert-info mb-3" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-calendar-check me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>How to Sign Up:</strong><br>
Contact your department's designated finance lead to schedule your consultation.
</div>
</div>
</div>
<div class="p-3 rounded" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border-left: 4px solid var(--rg-secondary);">
<i class="bi bi-heart-fill text-danger"></i>
<strong>MetaCorp is dedicated to its employees</strong> - This service is just one way of showing it!
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Additional Info Cards -->
<div class="row mt-3 g-3">
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid #579da9;">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-graph-up-arrow text-primary"></i> Investment Options
</h6>
<p class="card-text small text-muted">
Choose from a variety of investment funds to match your risk tolerance and retirement goals.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid #1e825e;">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-percent text-success"></i> Employer Matching
</h6>
<p class="card-text small text-muted">
MetaCorp matches your contributions up to 6% of your salary to maximize your retirement savings.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid var(--rg-warning);">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-shield-check text-warning"></i> Tax Advantages
</h6>
<p class="card-text small text-muted">
Contributions are pre-tax, reducing your taxable income while building your retirement nest egg.
</p>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive() {
$('li[id="retirement"]').addClass('active');
}
$(document).ready(makeActive);
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
</script>
<style>
.hover-stat-card {
transition: all 0.3s ease;
}
.hover-stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
}
.hover-stat-card h2 {
transition: transform 0.3s ease;
}
.hover-stat-card:hover h2 {
transform: scale(1.05);
}
</style>
+102
View File
@@ -0,0 +1,102 @@
<div class="rg-login-wrapper">
<div class="rg-login-card">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-shield-fill-exclamation"></i>
</div>
<h2 class="mb-1">MetaCorp</h2>
<p class="text-muted mb-0">A GoatGroup Company</p>
</div>
<%= form_tag "sessions", class: "needs-validation", novalidate: true do %>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<%= text_field_tag :email, params[:email], {
class: "form-control",
id: "email",
placeholder: "you@example.com",
required: true,
autofocus: true
} %>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<%= password_field_tag :password, nil, {
class: "form-control",
id: "password",
placeholder: "Enter your password",
required: true
} %>
</div>
</div>
<%= hidden_field_tag :url, @url %>
<div class="mb-3 form-check">
<%= check_box_tag :remember_me, 1, params[:remember_me], {
id: "remember_me",
class: "form-check-input"
} %>
<label class="form-check-label" for="remember_me">
Remember me
</label>
</div>
<div class="d-grid gap-2">
<%= submit_tag "Login", class: "btn btn-primary btn-lg" %>
</div>
<div class="text-center mt-3">
<%= link_to "Forgot Password?", forgot_password_path, class: "text-decoration-none" %>
</div>
<hr class="my-4">
<div class="text-center">
<p class="text-muted mb-2">Don't have an account?</p>
<%= link_to "Sign up now", signup_path, class: "btn btn-outline-primary" %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 152, 0, 0.1)); border: 2px solid rgba(255, 193, 7, 0.3); backdrop-filter: blur(10px);">
<div class="d-flex align-items-start">
<i class="bi bi-exclamation-triangle-fill text-warning me-2 mt-1" style="font-size: 1.25rem;"></i>
<div class="small">
<strong class="d-block mb-1">Security Training Environment</strong>
This is an intentionally vulnerable application for educational purposes.
<a href="https://github.com/OWASP/railsgoat/wiki" target="_blank" class="text-warning fw-semibold text-decoration-none">Learn more →</a>
</div>
</div>
</div>
</div>
</div>
<!-- VULNERABILITY: XSS via URL hash parameter -->
<script>
// support for multiple languages coming soon!
try {
var hashParam = location.hash.split("#")[1];
if (hashParam) {
var paramName = hashParam.split('=')[0];
var paramValue = decodeURIComponent(hashParam.split('=')[1]);
// VULNERABLE: Directly writing user input to DOM
document.write("<div class='alert alert-info mt-3'>" + paramValue + "</div>");
}
} catch(err) {
// Silently fail
}
</script>
<style>
/* Override main content styling for login page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<div class="container mt-4">
<div class="card">
<div class="card-header bg-warning text-dark d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="bi bi-key"></i> Application Credentials (Spoiler)
</h4>
<%= link_to root_path, class: "btn btn-sm btn-outline-dark" do %>
<i class="bi bi-x-lg"></i> Close
<% end %>
</div>
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<strong>Warning:</strong> This is a spoiler. Are you sure you want to see the credentials?
</div>
<div id="creds_hidden" style="display:none">
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>Email</th>
<th>Password</th>
<th>API Key</th>
</tr>
</thead>
<tbody>
<tr>
<td style="word-wrap:break-word;">admin@metacorp.com</td>
<td>admin1234</td>
<td>1-01de24d75cffaa66db205278d1cf900bf087a737</td>
</tr>
<tr>
<td style="word-wrap:break-word;">jmmastey@metacorp.com</td>
<td>railsgoat!</td>
<td>2-050ddd40584978fe9e82840b8b95abb98e4786dc</td>
</tr>
<tr>
<td style="word-wrap:break-word;">jim@metacorp.com</td>
<td>alohaowasp</td>
<td>3-eaa9b4d748d6a8c6a38e24ac1cc2204ebc3541c1</td>
</tr>
<tr>
<td style="word-wrap:break-word;">mike@metacorp.com</td>
<td>motocross1445</td>
<td>4-c809b3d11d272cff8cab1da9e4cdf61137f29d2</td>
</tr>
<tr>
<td style="word-wrap:break-word;">ken@metacorp.com</td>
<td>citrusblend</td>
<td>5-4af604a848ca212cfa3935352aabe9522cf89fdc</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center mt-3">
<button id="understood" class="btn btn-primary">
<i class="bi bi-eye"></i> I understand - Show Credentials
</button>
</div>
</div>
<div class="card-footer text-center">
<%= link_to root_path, class: "btn btn-secondary" do %>
<i class="bi bi-arrow-left"></i> Back to Home
<% end %>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
$('#understood').click(function() {
$("#creds_hidden").show();
$(this).hide();
});
});
</script>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<h1>Need help logging in?</h1>
<p>
A password reset was requested for your user account.<br>
<br>
To reset your MetaCorp password, simply click on the
following link and follow the instructions:<br>
<br>
<%= link_to "Click here to reset your password", @url %><br>
<br>
If you don't want to change your password, you can ignore this email.
</p>
<p>Thanks, and have a great day!</p>
</body>
</html>
@@ -0,0 +1,13 @@
Need help logging in?
==========================================================
A password reset was requested for your user account.
To reset your MetaCorp password, simply copy the
following link and follow the instructions:
<%= @url %>
If you don't want to change your password, you can ignore this email.
Thanks, and have a great day!
+97
View File
@@ -0,0 +1,97 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div id="success" style="display: none;" class="alert alert-block alert-success fade in">
<h4 class="alert-heading">
Success!
</h4>
<p>
Information successfully updated.
</p>
</div>
</div>
<div id="failure" style="display: none;" class="alert alert-block alert-error fade in">
<h4 class="alert-heading">
Error!
</h4>
<p>
Failed to update.
</p>
</div>
<div class="row-fluid">
<div class="span6">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe090;"></span> Profile Settings
<span class="mini-title">
Edit your account details
</span>
</div>
</div>
<div class="widget-body">
<%= form_for @user, :html => {:id => "account_edit"} do |f|%>
<%= f.hidden_field :id %>
<div class="control-group">
<%= f.label :email, nil, {:class => "control-label"}%>
<%= f.text_field :email, {:class => "span12"}%>
</div>
<div class="control-group">
<%= f.label :first_name, nil, {:class => "control-label"}%>
<%= f.text_field :first_name, {:class => "span12"} %>
</div>
<div>
<%= f.label :last_name, nil, {:class => "control-label"}%>
<%= f.text_field :last_name, {:class => "span12"} %>
</div>
<div class="control-group">
<%= f.label :password, nil, {:class => "control-label"}%>
<%= f.password_field :password, {:class => "span12", :placeholder => "Enter Password"}%>
</div>
<div class="control-group">
<%= f.label :password_confirmation, nil, {:class => "control-label"}%>
<%= f.password_field :password_confirmation, {:class => "span12", :placeholder => "Enter Password"} %>
</div>
<div class="form-actions no-margin">
<%= f.submit "Submit", {:id => 'submit_button', :class => "btn btn-info pull-right"} %>
</div>
<div class="clearfix">
</div>
<% end %>
</div>
</div>
</div>
</div>
<%= javascript_include_tag ('validation.js')%>
<script type="text/javascript">
$("#submit_button").click(function(event) {
var valuesToSubmit = $("#account_edit").serialize();
event.preventDefault();
$.ajax({
url: <%= "/users/#{current_user.id}.json".inspect.html_safe %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
if (response.msg == "failure") {
$('#failure').show(500).delay(1500).fadeOut();
} else {
$('#success').show(500).delay(1500).fadeOut();
}
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
</script>
+119
View File
@@ -0,0 +1,119 @@
<div class="rg-login-wrapper">
<div class="rg-login-card" style="max-width: 500px;">
<div class="rg-login-header">
<div class="rg-login-logo">
<i class="bi bi-person-plus-fill"></i>
</div>
<h2 class="mb-1">Create Account</h2>
<p class="text-muted mb-0">Join the MetaCorp team</p>
</div>
<%= form_for @user, html: { id: "account_edit", class: "needs-validation", novalidate: true } do |f| %>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<%= f.text_field :email, {
class: "form-control",
id: "email",
placeholder: "you@example.com",
required: true,
autofocus: true
} %>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="first_name" class="form-label">First Name</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<%= f.text_field :first_name, {
class: "form-control",
id: "first_name",
placeholder: "First Name",
required: true
} %>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="last_name" class="form-label">Last Name</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<%= f.text_field :last_name, {
class: "form-control",
id: "last_name",
placeholder: "Last Name",
required: true
} %>
</div>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<%= f.password_field :password, {
class: "form-control",
id: "password",
placeholder: "At least 6 characters",
required: true,
minlength: 6
} %>
</div>
<div class="form-text">Password must be at least 6 characters long</div>
</div>
<div class="mb-3">
<label for="password_confirmation" class="form-label">Confirm Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
<%= f.password_field :password_confirmation, {
class: "form-control",
id: "password_confirmation",
placeholder: "Re-enter password",
required: true
} %>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<%= f.submit "Create Account", {
id: "submit_button",
class: "btn btn-primary btn-lg"
} %>
</div>
<hr class="my-4">
<div class="text-center">
<p class="text-muted mb-2">Already have an account?</p>
<%= link_to login_path, class: "btn btn-outline-primary" do %>
<i class="bi bi-box-arrow-in-right"></i> Sign in
<% end %>
</div>
<% end %>
<div class="mt-4 p-3 rounded" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.1), rgba(17, 138, 178, 0.1)); border: 2px solid rgba(6, 214, 160, 0.3);">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2 mt-1" style="font-size: 1.25rem; color: var(--rg-success);"></i>
<div class="small">
<strong class="d-block mb-1">Training Environment</strong>
This application is intentionally vulnerable for security training purposes.
</div>
</div>
</div>
</div>
</div>
<%= javascript_include_tag "validation.js" %>
<style>
/* Override main content styling for signup page */
.rg-main.no-sidebar {
margin: 0;
padding: 0;
}
</style>
+64
View File
@@ -0,0 +1,64 @@
<div class="dashboard-wrapper">
<div class="main-container">
<div class="row-fluid">
<div class="span12">
<div class="widget">
<div class="widget-header">
<div class="title">
<span class="fs1" aria-hidden="true" data-icon="&#xe088;"></span> Employee Information
</div>
</div>
<div class="widget-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:16%">Full Name</th>
<th style="width:16%">Income</th>
<th style="width:16%">Bonus</th>
<th style="width:16%">Years w/ MetaCorp</th>
<th style="width:16%">SSN</th>
<th style="width:16%">DoB</th>
</tr>
</thead>
<tbody>
<tr>
<td><%= "#{@user.first_name} #{@user.last_name}" %></td>
<td><%= @user.work_info.income %></td>
<td><%= @user.work_info.bonuses %></td>
<td><%= @user.work_info.years_worked %></td>
<td class="ssn"><%= @user.work_info.SSN %></td>
<!-- Begin Secure Version>-->
<!--<td class="ssn"><%#= @user.work_info.last_four %></td>-->
<!-- End Secure Version -->
<td><%= @user.work_info.DoB %></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
// This should be an EXCELLENT feature!!!
function maskSSN(){
var fullSSN = $("td.ssn").html().replace(/\d{3}.*?\d{2}/, "*****");
$("td.ssn").html(fullSSN);
}
function makeActive(){
$('li[id="employee_info"]').addClass('active');
};
$(document).ready(function () {
maskSSN(),
makeActive()
});
</script>