This commit is contained in:
@@ -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> </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>
|
||||
Executable
+70
@@ -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>
|
||||
Executable
+63
@@ -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>
|
||||
Executable
+55
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Executable
+275
@@ -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>
|
||||
|
||||
|
||||
Executable
+445
@@ -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>
|
||||
Executable
+56
@@ -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>
|
||||
© <%= 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>
|
||||
Executable
+115
@@ -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 %>
|
||||
Executable
+38
@@ -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 %>
|
||||
Executable
+90
@@ -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 %>
|
||||
@@ -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>
|
||||
@@ -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=""></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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Executable
+102
@@ -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>
|
||||
@@ -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!
|
||||
Executable
+97
@@ -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=""></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>
|
||||
Executable
+119
@@ -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>
|
||||
@@ -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=""></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>
|
||||
Reference in New Issue
Block a user