You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/docs/backlog/stories-kanban-shared.html

1453 lines
68 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FloDoc Stories Kanban Board (Shared)</title>
<style>
/* ========================================
FLODOC DESIGN SYSTEM
======================================== */
:root {
--color-primary: #784DC7;
--color-primary-light: #E9DFFC;
--color-primary-dark: #6A3FB8;
--color-text-primary: #292929;
--color-text-secondary: #464646;
--color-text-muted: #9F9F9F;
--color-border: #E6E6E6;
--color-bg: #FFFFFF;
--color-bg-alt: #F8F8F8;
--color-success: #83E281;
--color-success-text: #158212;
--color-error: #FF5964;
--color-warning: #FFE74C;
--color-info: #35A7FF;
--color-backlog: #E6E6E6;
--color-todo: #FFE74C;
--color-progress: #35A7FF;
--color-review: #FF5964;
--color-done: #83E281;
--shadow-sm: 0px 2px 4px rgba(151, 71, 255, 0.08);
--shadow-md: 0px 4px 8px rgba(151, 71, 255, 0.08);
--shadow-lg: 0px 6px 12px rgba(41, 41, 41, 0.12);
--shadow-xl: 0px 10px 25px rgba(41, 41, 41, 0.15);
--radius-sm: 4.75px;
--radius-md: 7.5px;
--radius-lg: 12px;
--border-width: 1px;
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-2xl: 48px;
--font-body: 'Segoe UI', system-ui, -apple-system, sans-serif;
--font-mono: 'Consolas', 'Monaco', monospace;
--header-height: 64px;
--column-width: 320px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body);
color: var(--color-text-primary);
background: var(--color-bg-alt);
overflow: hidden;
height: 100vh;
}
/* Header */
.app-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-height);
background: var(--color-bg);
border-bottom: var(--border-width) solid var(--color-border);
display: flex;
align-items: center;
padding: 0 var(--space-lg);
z-index: 100;
box-shadow: var(--shadow-sm);
}
.logo-container {
display: flex;
align-items: center;
gap: var(--space-md);
}
.logo-icon {
width: 32px;
height: 32px;
background: var(--color-primary);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 18px;
box-shadow: var(--shadow-sm);
}
.logo-text {
font-family: var(--font-body);
font-size: 20px;
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: -0.5px;
}
.header-controls {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-md);
}
.search-box {
padding: var(--space-sm) var(--space-md);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
font-size: 14px;
width: 250px;
transition: all 0.2s ease;
}
.search-box:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(120, 77, 199, 0.1);
}
.btn {
padding: var(--space-sm) var(--space-lg);
border: none;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: var(--space-sm);
white-space: nowrap;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover {
background: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-primary:disabled {
background: var(--color-border);
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: var(--color-bg);
color: var(--color-text-primary);
border: var(--border-width) solid var(--color-border);
}
.btn-secondary:hover {
background: var(--color-bg-alt);
border-color: var(--color-primary);
color: var(--color-primary);
}
.btn-danger {
background: var(--color-error);
color: white;
}
.btn-danger:hover {
background: #E04852;
}
.stats {
display: flex;
gap: var(--space-md);
font-size: 13px;
color: var(--color-text-muted);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--space-xs);
}
.stat-value {
font-weight: 700;
color: var(--color-text-primary);
}
.sync-status {
display: flex;
align-items: center;
gap: var(--space-xs);
font-size: 12px;
padding: 4px 8px;
border-radius: var(--radius-sm);
background: var(--color-bg-alt);
}
.sync-status.syncing {
color: var(--color-info);
}
.sync-status.synced {
color: var(--color-success-text);
}
.sync-status.error {
color: var(--color-error);
}
.spinner-small {
width: 12px;
height: 12px;
border: 2px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Kanban Board */
.kanban-container {
position: fixed;
top: var(--header-height);
left: 0;
right: 0;
bottom: 0;
overflow-x: auto;
overflow-y: hidden;
padding: var(--space-lg);
background: var(--color-bg-alt);
}
.kanban-board {
display: flex;
gap: var(--space-lg);
height: 100%;
min-width: min-content;
}
.kanban-column {
flex-shrink: 0;
width: var(--column-width);
background: var(--color-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
display: flex;
flex-direction: column;
overflow: hidden;
border-top: 4px solid transparent;
}
.kanban-column.backlog { border-top-color: var(--color-backlog); }
.kanban-column.todo { border-top-color: var(--color-todo); }
.kanban-column.progress { border-top-color: var(--color-progress); }
.kanban-column.review { border-top-color: var(--color-review); }
.kanban-column.done { border-top-color: var(--color-done); }
.column-header {
padding: var(--space-lg);
border-bottom: var(--border-width) solid var(--color-border);
background: var(--color-bg-alt);
}
.column-title {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: space-between;
}
.column-count {
background: var(--color-border);
color: var(--color-text-secondary);
padding: 2px 8px;
border-radius: var(--radius-lg);
font-size: 11px;
font-weight: 700;
}
.column-body {
flex: 1;
overflow-y: auto;
padding: var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.column-body.drag-over {
background: var(--color-primary-light);
}
/* Story Card */
.story-card {
background: var(--color-bg);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-md);
cursor: grab;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
.story-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
border-color: var(--color-primary);
}
.story-card.dragging {
opacity: 0.5;
cursor: grabbing;
transform: rotate(2deg);
}
.story-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--space-sm);
}
.story-number {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
color: var(--color-primary);
background: var(--color-primary-light);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.story-title {
font-size: 13px;
font-weight: 700;
color: var(--color-text-primary);
line-height: 1.4;
margin-bottom: var(--space-sm);
}
.story-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin-bottom: var(--space-sm);
}
.badge {
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: var(--radius-sm);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.badge-priority-high {
background: rgba(255, 89, 100, 0.15);
color: var(--color-error);
}
.badge-priority-critical {
background: rgba(255, 89, 100, 0.25);
color: var(--color-error);
font-weight: 800;
}
.badge-priority-medium {
background: rgba(255, 231, 76, 0.2);
color: #B8860B;
}
.badge-priority-low {
background: rgba(131, 226, 129, 0.2);
color: var(--color-success-text);
}
.badge-effort {
background: var(--color-bg-alt);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.story-epic {
font-size: 11px;
color: var(--color-text-muted);
margin-bottom: var(--space-sm);
}
.story-preview {
font-size: 11px;
color: var(--color-text-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.story-actions {
display: flex;
gap: var(--space-xs);
margin-top: var(--space-sm);
opacity: 0;
transition: opacity 0.2s ease;
}
.story-card:hover .story-actions {
opacity: 1;
}
.story-action-btn {
padding: 4px 8px;
font-size: 11px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
background: var(--color-bg-alt);
color: var(--color-text-secondary);
transition: all 0.2s ease;
}
.story-action-btn:hover {
background: var(--color-primary);
color: white;
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(41, 41, 41, 0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
backdrop-filter: blur(4px);
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--color-bg);
border-radius: var(--radius-lg);
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-xl);
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
padding: var(--space-lg);
border-bottom: var(--border-width) solid var(--color-border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--color-bg-alt);
}
.modal-title {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--color-text-muted);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
transition: all 0.2s ease;
}
.modal-close:hover {
background: var(--color-bg-alt);
color: var(--color-text-primary);
}
.modal-body {
padding: var(--space-lg);
}
.modal-section {
margin-bottom: var(--space-xl);
}
.modal-section-title {
font-size: 14px;
font-weight: 700;
color: var(--color-primary);
margin-bottom: var(--space-md);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.modal-content {
background: var(--color-bg-alt);
padding: var(--space-lg);
border-radius: var(--radius-md);
line-height: 1.8;
color: var(--color-text-secondary);
font-size: 14px;
}
.modal-content strong {
color: var(--color-text-primary);
font-weight: 700;
}
.modal-content code {
background: var(--color-bg);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 13px;
color: var(--color-primary);
border: 1px solid var(--color-border);
}
.modal-content ul,
.modal-content ol {
margin-left: var(--space-lg);
margin-top: var(--space-sm);
}
.modal-content li {
margin-bottom: var(--space-sm);
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--color-bg-alt); }
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: var(--radius-sm); }
::-webkit-scrollbar-thumb:hover { background: var(--color-text-muted); }
/* Toast */
.toast {
position: fixed;
bottom: var(--space-lg);
right: var(--space-lg);
background: var(--color-primary);
color: white;
padding: var(--space-md) var(--space-lg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: none;
align-items: center;
gap: var(--space-md);
z-index: 300;
animation: toastSlideIn 0.3s ease;
}
.toast.active {
display: flex;
}
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.toast.success {
background: var(--color-success);
color: var(--color-success-text);
}
.toast.error {
background: var(--color-error);
color: white;
}
.toast.warning {
background: var(--color-warning);
color: #B8860B;
}
/* Empty State */
.empty-column {
text-align: center;
padding: var(--space-xl);
color: var(--color-text-muted);
font-size: 13px;
font-style: italic;
}
/* Loading */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: none;
align-items: center;
justify-content: center;
z-index: 400;
}
.loading-overlay.active {
display: flex;
}
.loading-content {
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto var(--space-lg);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Config Modal */
.config-form {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.form-label {
font-size: 13px;
font-weight: 600;
color: var(--color-text-secondary);
}
.form-input {
padding: var(--space-sm) var(--space-md);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
font-size: 14px;
font-family: var(--font-mono);
}
.form-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(120, 77, 199, 0.1);
}
.form-hint {
font-size: 12px;
color: var(--color-text-muted);
}
/* Responsive */
@media (max-width: 768px) {
:root { --column-width: 280px; }
.header-controls { gap: var(--space-sm); }
.search-box { width: 150px; }
.stats { display: none; }
.btn span { display: none; }
.modal { width: 95%; }
}
</style>
</head>
<body>
<!-- Header -->
<header class="app-header">
<div class="logo-container">
<div class="logo-icon">F</div>
<div class="logo-text">FloDoc Kanban</div>
</div>
<div class="header-controls">
<input
type="text"
class="search-box"
id="searchBox"
placeholder="Search stories..."
autocomplete="off"
>
<div class="stats">
<div class="stat-item">
<span>Total:</span>
<span class="stat-value" id="statTotal">0</span>
</div>
<div class="stat-item">
<span>Done:</span>
<span class="stat-value" id="statDone">0</span>
</div>
</div>
<div class="sync-status" id="syncStatus">
<span>Offline</span>
</div>
<button class="btn btn-secondary" id="configBtn">
<span>⚙️</span>
<span>Config</span>
</button>
<button class="btn btn-secondary" id="refreshBtn">
<span></span>
<span>Refresh</span>
</button>
<button class="btn btn-primary" id="saveBtn">
<span>💾</span>
<span>Save</span>
</button>
</div>
</header>
<!-- Kanban Board -->
<div class="kanban-container">
<div class="kanban-board" id="kanbanBoard">
<!-- Columns will be dynamically generated -->
</div>
</div>
<!-- Modal for Story Details -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modalTitle">Story Details</div>
<button class="modal-close" id="modalClose">×</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Content will be dynamically generated -->
</div>
</div>
</div>
<!-- Config Modal -->
<div class="modal-overlay" id="configModalOverlay">
<div class="modal">
<div class="modal-header">
<div class="modal-title">Configuration</div>
<button class="modal-close" id="configModalClose">×</button>
</div>
<div class="modal-body">
<div class="config-form">
<div class="form-group">
<label class="form-label">JSONBin API Key</label>
<input type="text" class="form-input" id="apiKeyInput" placeholder="Enter your JSONBin API key">
<span class="form-hint">Get your API key from jsonbin.io (free tier available)</span>
</div>
<div class="form-group">
<label class="form-label">JSONBin Bin ID</label>
<input type="text" class="form-input" id="binIdInput" placeholder="Enter your Bin ID">
<span class="form-hint">The ID of your board data bin</span>
</div>
<div style="display: flex; gap: var(--space-md); margin-top: var(--space-lg);">
<button class="btn btn-primary" id="saveConfigBtn">Save Configuration</button>
<button class="btn btn-secondary" id="createBinBtn">Create New Bin</button>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<div id="loadingText">Loading...</div>
</div>
</div>
<!-- Toast Notification -->
<div class="toast" id="toast">
<span id="toastMessage">Action completed</span>
</div>
<script>
// ========================================
// FLODOC STORIES DATA
// ========================================
const storiesData = [
{ number: "1.1", title: "Database Schema Extension", priority: "Critical", epic: "Phase 1 - Foundation", effort: "2-3 days", user_story: "**As a** system architect,\n**I want** to create the database schema for FloDoc's new models,\n**So that** the application has the foundation to support cohort management.", background: "Based on the PRD analysis, we need three new tables to support the 3-portal cohort management system:\n- `institutions` - Single training institution (not multi-tenant)\n- `cohorts` - Training program cohorts\n- `cohort_enrollments` - Student enrollments in cohorts\n\nThese tables must integrate with existing DocuSeal tables without breaking existing functionality.", acceptance: "**Functional:**\n- ✅ All three tables created with correct schema\n- ✅ Foreign key relationships established\n- ✅ All indexes created for performance\n- ✅ Migrations are reversible\n- ✅ No modifications to existing DocuSeal tables" },
{ number: "1.2", title: "Core Models Implementation", priority: "High", epic: "Phase 1 - Foundation", effort: "2 days", user_story: "**As a** developer,\n**I want** to create ActiveRecord models for the new FloDoc tables,\n**So that** the application can interact with cohorts and enrollments programmatically.", background: "Models must follow existing DocuSeal patterns:\n- Inherit from `ApplicationRecord`\n- Use `strip_attributes` for data cleaning\n- Include soft delete functionality\n- Define proper associations and validations\n- Follow naming conventions", acceptance: "**Functional:**\n- ✅ All three models created with correct class structure\n- ✅ All associations defined correctly\n- ✅ All validations implemented" },
{ number: "1.3", title: "Authorization Layer Extension", priority: "High", epic: "Phase 1 - Foundation", effort: "1-2 days", user_story: "**As a** system administrator,\n**I want** the authorization system to support FloDoc roles and permissions,\n**So that** users can only access appropriate cohort management functions.", background: "Extend Cancancan abilities to support:\n- TP Admin: Full cohort management\n- Student: View own enrollments, upload documents\n- Sponsor: Review and sign documents\n- Admin: All access plus system configuration", acceptance: "**Functional:**\n- ✅ Ability rules defined for all FloDoc actions\n- ✅ Role-based access control working" },
{ number: "2.1", title: "Cohort Creation & Management", priority: "High", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to create and manage cohorts with all their configuration details,\n**So that** I can organize students into training programs and prepare them for the signature workflow.", background: "Cohort creation requires:\n- Name and program type selection\n- Sponsor email configuration\n- Required student uploads (ID, matric, tertiary)\n- Cohort metadata for additional info\n- Status tracking (draft/active/completed)", acceptance: "**Functional:**\n- ✅ Cohort creation form with all fields\n- ✅ Cohort update functionality" },
{ number: "2.2", title: "TP Signing Phase Logic", priority: "Critical", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to sign the first student's document and have that signing replicated to all other students in the cohort,\n**So that** I don't need to sign each student's document individually, saving time and eliminating duplicate sponsor emails.", background: "This is the CORE INNOVATION of FloDoc:\n- TP signs ONE document\n- System replicates signature to all students\n- Must work with DocuSeal's existing signing mechanism", acceptance: "**Functional:**\n- ✅ TP signs first document\n- ✅ Signature replicated to all cohort students" },
{ number: "2.3", title: "Student Enrollment Management", priority: "High", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to manage student enrollment in cohorts and bulk-create student submissions,\n**So that** students can access their documents to complete after TP signs.", background: "Student enrollment workflow:\n- Bulk import via CSV or manual entry\n- Create cohort_enrollment records\n- Generate submissions linked to students\n- Track enrollment status (waiting/in_progress/complete)", acceptance: "**Functional:**\n- ✅ Bulk student import (CSV)\n- ✅ Individual student enrollment" },
{ number: "2.4", title: "Sponsor Review Workflow", priority: "Medium", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** to receive and review student documents,\n**So that** I can verify information before signing.", background: "Sponsor workflow:\n- Receive email with cohort overview\n- Access documents via secure token\n- Review student information\n- Verify uploaded documents", acceptance: "**Functional:**\n- ✅ Email notification to sponsor\n- ✅ Token-based access to documents" },
{ number: "2.5", title: "TP Review & Finalization", priority: "Medium", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to review all student submissions and finalize the cohort,\n**So that** I can ensure everything is complete before closing.", background: "Finalization workflow:\n- View all student statuses\n- Check completion rates\n- Review any issues\n- Mark cohort as finalized", acceptance: "**Functional:**\n- ✅ Cohort overview dashboard\n- ✅ Completion status tracking" },
{ number: "2.6", title: "Excel Export for Cohort Data", priority: "Medium", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to export cohort data to Excel,\n**So that** I can analyze enrollment and completion data externally.", background: "Export requirements (FR23):\n- Export all cohort data\n- Include student information\n- Show completion status\n- Include timestamps\n- Professional formatting", acceptance: "**Functional:**\n- ✅ Excel export button\n- ✅ All relevant data included" },
{ number: "2.7", title: "Audit Log & Compliance", priority: "Medium", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** compliance officer,\n**I want** all actions logged in an audit trail,\n**So that** I can track who did what and when.", background: "Audit requirements:\n- Log all cohort actions\n- Track user who performed action\n- Timestamp all events\n- Store before/after values\n- Queryable audit trail", acceptance: "**Functional:**\n- ✅ Audit log table created\n- ✅ All actions logged" },
{ number: "2.8", title: "Cohort State Machine", priority: "High", epic: "Phase 2 - Backend Logic", effort: "4 hours", user_story: "**As a** system,\n**I want** to manage cohort state transitions automatically,\n**So that** the workflow progresses smoothly without manual intervention.", background: "State machine for cohorts:\n- draft → active (when TP signs)\n- active → students_complete (when all students finish)\n- students_complete → sponsor_complete (when sponsor signs)\n- sponsor_complete → finalized (when TP reviews)", acceptance: "**Functional:**\n- ✅ State transitions defined\n- ✅ Automatic progression works" },
{ number: "3.1", title: "RESTful Cohort Management API", priority: "High", epic: "Phase 3 - API Layer", effort: "4 hours", user_story: "**As a** developer,\n**I want** RESTful APIs for cohort management,\n**So that** I can integrate FloDoc with other systems.", background: "API endpoints needed:\n- GET /api/cohorts - List cohorts\n- POST /api/cohorts - Create cohort\n- GET /api/cohorts/:id - Show cohort\n- PUT /api/cohorts/:id - Update cohort\n- DELETE /api/cohorts/:id - Delete cohort", acceptance: "**Functional:**\n- ✅ All CRUD endpoints implemented\n- ✅ Proper HTTP status codes" },
{ number: "3.2", title: "Webhook Events", priority: "Medium", epic: "Phase 3 - API Layer", effort: "4 hours", user_story: "**As a** system integrator,\n**I want** webhook notifications for state changes,\n**So that** external systems can react to FloDoc events.", background: "Webhook events:\n- cohort.created\n- cohort.state_changed\n- student.enrolled\n- student.completed\n- sponsor.signed\n- cohort.finalized", acceptance: "**Functional:**\n- ✅ Webhook event system\n- ✅ All required events implemented" },
{ number: "3.3", title: "Student API (Ad-hoc)", priority: "High", epic: "Phase 3 - API Layer", effort: "4 hours", user_story: "**As a** student,\n**I want** to access my documents via token-based API,\n**So that** I can complete forms without creating an account.", background: "Ad-hoc access pattern:\n- Student receives email with token\n- Token grants access to specific submission\n- No user account required\n- Token expires after completion", acceptance: "**Functional:**\n- ✅ Token generation endpoint\n- ✅ Token validation" },
{ number: "3.4", title: "API Documentation", priority: "Low", epic: "Phase 3 - API Layer", effort: "4 hours", user_story: "**As a** developer,\n**I want** comprehensive API documentation,\n**So that** I can easily integrate with FloDoc.", background: "Documentation requirements:\n- OpenAPI/Swagger specs\n- Endpoint descriptions\n- Request/response examples\n- Authentication details", acceptance: "**Functional:**\n- ✅ OpenAPI specification\n- ✅ Interactive API docs" },
{ number: "4.1", title: "Cohort Management Dashboard", priority: "High", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** a dashboard showing all cohorts,\n**So that** I can monitor all training programs at a glance.", background: "Dashboard features:\n- List of all cohorts\n- Status indicators\n- Quick stats (total, active, completed)\n- Search and filter\n- Navigation to detail views", acceptance: "**Functional:**\n- ✅ Cohort list view\n- ✅ Status badges" },
{ number: "4.2", title: "Cohort Creation & Bulk Import", priority: "High", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to create cohorts and bulk import students,\n**So that** I can quickly set up new training programs.", background: "Creation interface:\n- Cohort form (name, program type, sponsor email)\n- Required uploads configuration\n- Bulk student import (CSV)\n- Validation and preview", acceptance: "**Functional:**\n- ✅ Cohort creation form\n- ✅ CSV upload with validation" },
{ number: "4.3", title: "Cohort Detail Overview", priority: "Medium", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to view detailed cohort information,\n**So that** I can monitor progress and manage individual students.", background: "Detail view features:\n- Cohort metadata\n- Student list with statuses\n- Progress indicators\n- Action buttons (enroll, finalize, export)", acceptance: "**Functional:**\n- ✅ Cohort information display\n- ✅ Student list with filtering" },
{ number: "4.4", title: "TP Signing Interface", priority: "High", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** a signing interface for the first student,\n**So that** I can sign once and replicate to all students.", background: "Signing interface:\n- Preview first student's document\n- Digital signature capture\n- Replication confirmation\n- Progress indicator\n- Success notification", acceptance: "**Functional:**\n- ✅ Document preview\n- ✅ Signature capture" },
{ number: "4.5", title: "Student Management View", priority: "Medium", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** to manage individual student enrollments,\n**So that** I can add/remove students and track their progress.", background: "Student management:\n- Add individual students\n- Remove students\n- View student details\n- Track document uploads\n- Resend invitations", acceptance: "**Functional:**\n- ✅ Add/remove students\n- ✅ Student detail view" },
{ number: "4.6", title: "Sponsor Portal Dashboard", priority: "High", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** a dashboard showing cohorts awaiting my signature,\n**So that** I can quickly see what needs my review.", background: "Sponsor dashboard:\n- List of cohorts needing signature\n- Student count per cohort\n- Progress indicators\n- Quick access to review\n- Token-based authentication", acceptance: "**Functional:**\n- ✅ Cohort list for sponsor\n- ✅ Student counts" },
{ number: "4.7", title: "Sponsor Bulk Signing", priority: "High", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** to sign multiple student documents at once,\n**So that** I can complete my review efficiently.", background: "Bulk signing:\n- Select multiple students\n- Single signature application\n- Confirmation before signing\n- Progress indicator\n- Completion notification", acceptance: "**Functional:**\n- ✅ Multi-select students\n- ✅ Single signature application" },
{ number: "4.8", title: "Sponsor Progress Tracking", priority: "Medium", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** to track progress of all students in a cohort,\n**So that** I can see who has completed and who hasn't.", background: "Progress tracking:\n- Visual progress bars\n- Student status list\n- Completion percentages\n- Filter by status\n- Timeline view", acceptance: "**Functional:**\n- ✅ Progress visualization\n- ✅ Student status list" },
{ number: "4.9", title: "Sponsor Token Renewal", priority: "Medium", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** to renew my access token and manage my session,\n**So that** I can continue working without interruption.", background: "Session management:\n- Token refresh mechanism\n- Session timeout warnings\n- Logout functionality\n- Token renewal UI", acceptance: "**Functional:**\n- ✅ Token refresh\n- ✅ Session timeout warnings" },
{ number: "4.10", title: "TP Portal Analytics", priority: "Medium", epic: "Phase 4 - Admin Portal", effort: "4 hours", user_story: "**As a** TP administrator,\n**I want** analytics and monitoring for all cohorts,\n**So that** I can identify bottlenecks and optimize workflows.", background: "Analytics features:\n- Cohort completion rates\n- Average completion time\n- Bottleneck identification\n- Export analytics\n- Visual dashboards", acceptance: "**Functional:**\n- ✅ Analytics dashboard\n- ✅ Completion metrics" },
{ number: "5.1", title: "Student Document Upload", priority: "High", epic: "Phase 5 - Student Portal", effort: "4 hours", user_story: "**As a** student,\n**I want** to upload required documents,\n**So that** I can complete my enrollment requirements.", background: "Document upload:\n- List of required documents\n- Drag-and-drop upload\n- File type validation\n- Upload progress\n- Success/error feedback", acceptance: "**Functional:**\n- ✅ Required document list\n- ✅ File upload interface" },
{ number: "5.2", title: "Student Form Filling", priority: "High", epic: "Phase 5 - Student Portal", effort: "4 hours", user_story: "**As a** student,\n**I want** to fill out form fields,\n**So that** I can complete my document submission.", background: "Form filling:\n- All required fields displayed\n- Field validation\n- Save progress\n- Field completion tracking\n- Signature capture", acceptance: "**Functional:**\n- ✅ All form fields present\n- ✅ Field validation" },
{ number: "5.3", title: "Student Progress & Save Draft", priority: "Medium", epic: "Phase 5 - Student Portal", effort: "4 hours", user_story: "**As a** student,\n**I want** to save my progress and resume later,\n**So that** I can complete the form at my own pace.", background: "Progress saving:\n- Auto-save functionality\n- Manual save button\n- Draft status\n- Resume from last position\n- Completion percentage", acceptance: "**Functional:**\n- ✅ Auto-save every 30 seconds\n- ✅ Manual save button" },
{ number: "5.4", title: "Student Submission Confirmation", priority: "Medium", epic: "Phase 5 - Student Portal", effort: "4 hours", user_story: "**As a** student,\n**I want** to confirm my submission and see its status,\n**So that** I know my documents are received and what happens next.", background: "Submission flow:\n- Final review before submit\n- Confirmation dialog\n- Success message\n- Status tracking\n- Next steps communication", acceptance: "**Functional:**\n- ✅ Final review screen\n- ✅ Confirmation dialog" },
{ number: "5.5", title: "Student Email Notifications", priority: "Low", epic: "Phase 5 - Student Portal", effort: "4 hours", user_story: "**As a** student,\n**I want** to receive email notifications and reminders,\n**So that** I stay informed about my document status.", background: "Email notifications:\n- Enrollment invitation\n- Reminder emails\n- Status updates\n- Completion confirmation\n- Unsubscribe option", acceptance: "**Functional:**\n- ✅ Enrollment email\n- ✅ Reminder emails" },
{ number: "6.1", title: "Sponsor Dashboard & Bulk Signing", priority: "High", epic: "Phase 6 - Sponsor Portal", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** a dashboard to view cohorts and sign documents in bulk,\n**So that** I can efficiently complete my review responsibilities.", background: "Sponsor dashboard:\n- Cohort list with student counts\n- Progress indicators\n- Bulk selection\n- Single signing action\n- Completion tracking", acceptance: "**Functional:**\n- ✅ Cohort overview\n- ✅ Bulk selection" },
{ number: "6.2", title: "Sponsor Email Notifications", priority: "Medium", epic: "Phase 6 - Sponsor Portal", effort: "4 hours", user_story: "**As a** sponsor,\n**I want** email notifications for cohorts needing my signature,\n**So that** I don't miss review deadlines.", background: "Sponsor emails:\n- New cohort notification\n- Reminder emails\n- Completion confirmation\n- Deadline alerts\n- Token-based access links", acceptance: "**Functional:**\n- ✅ New cohort email\n- ✅ Reminder emails" },
{ number: "7.1", title: "End-to-End Testing", priority: "High", epic: "Phase 7 - Testing & QA", effort: "4 hours", user_story: "**As a** QA engineer,\n**I want** comprehensive end-to-end tests,\n**So that** I can verify the complete workflow functions correctly.", background: "E2E testing:\n- Complete cohort lifecycle\n- All three portals\n- All user roles\n- Edge cases\n- Integration points", acceptance: "**Functional:**\n- ✅ Complete workflow tests\n- ✅ All portals covered" },
{ number: "7.2", title: "Mobile Responsiveness", priority: "Medium", epic: "Phase 7 - Testing & QA", effort: "4 hours", user_story: "**As a** QA engineer,\n**I want** to test all interfaces on mobile devices,\n**So that** students and sponsors can work on any device.", background: "Mobile testing:\n- All three portals\n- All screen sizes\n- Touch interactions\n- Performance on mobile\n- Accessibility", acceptance: "**Functional:**\n- ✅ All portals responsive\n- ✅ Touch-friendly UI" },
{ number: "7.3", title: "Performance Testing (50+ Students)", priority: "High", epic: "Phase 7 - Testing & QA", effort: "4 hours", user_story: "**As a** QA engineer,\n**I want** to test performance with 50+ students,\n**So that** the system handles real-world loads.", background: "Performance testing:\n- Load testing with 50 students\n- Bulk operations\n- Database query performance\n- API response times\n- Memory usage", acceptance: "**Functional:**\n- ✅ 50+ students handled\n- ✅ Bulk operations < 5s" },
{ number: "7.4", title: "Security Audit", priority: "High", epic: "Phase 7 - Testing & QA", effort: "4 hours", user_story: "**As a** security auditor,\n**I want** to perform security testing,\n**So that** I can identify and fix vulnerabilities.", background: "Security testing:\n- Authentication bypass attempts\n- SQL injection tests\n- XSS vulnerability checks\n- Token security\n- Data exposure", acceptance: "**Functional:**\n- ✅ No authentication bypass\n- ✅ No SQL injection" },
{ number: "7.5", title: "User Acceptance Testing", priority: "Medium", epic: "Phase 7 - Testing & QA", effort: "4 hours", user_story: "**As a** product owner,\n**I want** user acceptance testing with real stakeholders,\n**So that** I can validate the solution meets requirements.", background: "UAT process:\n- Real TP administrators\n- Real students\n- Real sponsors\n- Complete workflows\n- Feedback collection", acceptance: "**Functional:**\n- ✅ All workflows validated\n- ✅ User feedback collected" },
{ number: "8.0", title: "Development Infrastructure", priority: "High", epic: "Phase 8 - Infrastructure", effort: "4 hours", user_story: "**As a** developer,\n**I want** local Docker infrastructure setup,\n**So that** I can develop and test FloDoc locally.", background: "Local infrastructure:\n- Docker Compose setup\n- Database containers\n- Redis for Sidekiq\n- Local storage\n- Development configuration", acceptance: "**Functional:**\n- ✅ Docker Compose file\n- ✅ All services running" },
{ number: "8.0.1", title: "Management Demo Readiness", priority: "Critical", epic: "Phase 8 - Infrastructure", effort: "4 hours", user_story: "**As a** product owner,\n**I want** management demo scripts and validation,\n**So that** I can demonstrate the FloDoc system to stakeholders.", background: "Demo preparation:\n- Demo scripts\n- Sample data\n- Validation checklist\n- Presentation materials\n- Success metrics", acceptance: "**Functional:**\n- ✅ Demo scripts written\n- ✅ Sample data created" },
{ number: "8.5", title: "User Communication & Training", priority: "Low", epic: "Phase 8 - Infrastructure", effort: "4 hours", user_story: "**As a** training coordinator,\n**I want** user communication and training materials,\n**So that** users can successfully use FloDoc.", background: "Training materials:\n- User guides\n- Video tutorials\n- FAQ documents\n- Email templates\n- Support contact info", acceptance: "**Functional:**\n- ✅ User guides created\n- ✅ Video tutorials recorded" },
{ number: "8.6", title: "In-App User Documentation", priority: "Low", epic: "Phase 8 - Infrastructure", effort: "4 hours", user_story: "**As a** user,\n**I want** in-app help and documentation,\n**So that** I can get assistance without leaving the application.", background: "In-app help:\n- Contextual help icons\n- Tooltips\n- Help sidebar\n- Searchable documentation\n- Contact support", acceptance: "**Functional:**\n- ✅ Help icons on all screens\n- ✅ Tooltips for complex UI" },
{ number: "8.7", title: "Knowledge Transfer & Ops Docs", priority: "Low", epic: "Phase 8 - Infrastructure", effort: "4 hours", user_story: "**As a** operations team,\n**I want** comprehensive operations documentation,\n**So that** I can maintain and support FloDoc.", background: "Operations docs:\n- Deployment procedures\n- Backup/restore\n- Monitoring\n- Troubleshooting\n- Incident response", acceptance: "**Functional:**\n- ✅ Deployment guide\n- ✅ Backup procedures" }
];
// ========================================
// KANBAN COLUMNS
// ========================================
const columns = [
{ id: 'backlog', name: 'Backlog', color: '#E6E6E6' },
{ id: 'todo', name: 'To Do', color: '#FFE74C' },
{ id: 'progress', name: 'In Progress', color: '#35A7FF' },
{ id: 'review', name: 'Review', color: '#FF5964' },
{ id: 'done', name: 'Done', color: '#83E281' }
];
// ========================================
// CONFIG
// ========================================
let config = {
apiKey: '$2a$10$zZz2g5orQv6Zvfq8PoJOyuugC.HipHW6L2UMkT3g1VWZIl4iTMxfe',
binId: '6968da80ae596e708fde1d84'
};
// ========================================
// STATE MANAGEMENT
// ========================================
let storyStates = {};
let filteredStories = [...storiesData];
let draggedStory = null;
let isSyncing = false;
// ========================================
// DOM ELEMENTS
// ========================================
const kanbanBoard = document.getElementById('kanbanBoard');
const searchBox = document.getElementById('searchBox');
const saveBtn = document.getElementById('saveBtn');
const refreshBtn = document.getElementById('refreshBtn');
const configBtn = document.getElementById('configBtn');
const modalOverlay = document.getElementById('modalOverlay');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalClose = document.getElementById('modalClose');
const configModalOverlay = document.getElementById('configModalOverlay');
const configModalClose = document.getElementById('configModalClose');
const saveConfigBtn = document.getElementById('saveConfigBtn');
const createBinBtn = document.getElementById('createBinBtn');
const apiKeyInput = document.getElementById('apiKeyInput');
const binIdInput = document.getElementById('binIdInput');
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
const statTotal = document.getElementById('statTotal');
const statDone = document.getElementById('statDone');
const syncStatus = document.getElementById('syncStatus');
const loadingOverlay = document.getElementById('loadingOverlay');
const loadingText = document.getElementById('loadingText');
// ========================================
// INITIALIZATION
// ========================================
async function init() {
loadConfig();
renderBoard();
updateStats();
attachEventListeners();
// Try to load from cloud
if (config.apiKey && config.binId) {
await loadFromCloud();
} else {
// Try to load from localStorage
loadFromLocal();
}
}
// ========================================
// CONFIG MANAGEMENT
// ========================================
function loadConfig() {
const saved = localStorage.getItem('flodoc-kanban-config');
if (saved) {
config = JSON.parse(saved);
apiKeyInput.value = config.apiKey;
binIdInput.value = config.binId;
}
}
function saveConfig() {
config.apiKey = apiKeyInput.value.trim();
config.binId = binIdInput.value.trim();
if (!config.apiKey || !config.binId) {
showToast('Please enter both API Key and Bin ID', 'error');
return;
}
localStorage.setItem('flodoc-kanban-config', JSON.stringify(config));
configModalOverlay.classList.remove('active');
showToast('Configuration saved! Loading from cloud...', 'success');
loadFromCloud();
}
async function createNewBin() {
if (!config.apiKey) {
showToast('Please enter your API Key first', 'error');
return;
}
showLoading('Creating new bin...');
try {
// Initialize all stories to backlog
const initialState = {};
storiesData.forEach(story => {
initialState[story.number] = 'backlog';
});
const response = await fetch('https://api.jsonbin.io/v3/b', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Master-Key': config.apiKey
},
body: JSON.stringify(initialState)
});
if (!response.ok) {
throw new Error('Failed to create bin');
}
const data = await response.json();
config.binId = data.metadata.id;
binIdInput.value = config.binId;
localStorage.setItem('flodoc-kanban-config', JSON.stringify(config));
hideLoading();
showToast('Bin created successfully!', 'success');
} catch (error) {
hideLoading();
showToast('Error creating bin: ' + error.message, 'error');
}
}
// ========================================
// CLOUD SYNC
// ========================================
async function loadFromCloud() {
if (!config.apiKey || !config.binId) {
updateSyncStatus('error', 'No config');
return;
}
isSyncing = true;
updateSyncStatus('syncing', 'Loading...');
try {
const response = await fetch(`https://api.jsonbin.io/v3/b/${config.binId}/latest`, {
method: 'GET',
headers: {
'X-Master-Key': config.apiKey
}
});
if (!response.ok) {
throw new Error('Failed to load from cloud');
}
const data = await response.json();
storyStates = data.record;
renderBoard();
updateStats();
updateSyncStatus('synced', 'Synced');
showToast('Loaded from cloud!', 'success');
} catch (error) {
updateSyncStatus('error', 'Error');
showToast('Error loading from cloud: ' + error.message, 'error');
// Fallback to local
loadFromLocal();
} finally {
isSyncing = false;
}
}
async function saveToCloud() {
if (!config.apiKey || !config.binId) {
showToast('Please configure JSONBin first', 'error');
configModalOverlay.classList.add('active');
return;
}
isSyncing = true;
updateSyncStatus('syncing', 'Saving...');
try {
const response = await fetch(`https://api.jsonbin.io/v3/b/${config.binId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Master-Key': config.apiKey
},
body: JSON.stringify(storyStates)
});
if (!response.ok) {
throw new Error('Failed to save to cloud');
}
updateSyncStatus('synced', 'Synced');
showToast('Saved to cloud! All users can see changes.', 'success');
} catch (error) {
updateSyncStatus('error', 'Error');
showToast('Error saving to cloud: ' + error.message, 'error');
} finally {
isSyncing = false;
}
}
// ========================================
// LOCAL STORAGE
// ========================================
function loadFromLocal() {
const saved = localStorage.getItem('flodoc-kanban-state');
if (saved) {
storyStates = JSON.parse(saved);
renderBoard();
updateStats();
updateSyncStatus('synced', 'Local');
} else {
// Initialize with default states
storiesData.forEach(story => {
storyStates[story.number] = 'backlog';
});
renderBoard();
updateStats();
}
}
function saveToLocal() {
localStorage.setItem('flodoc-kanban-state', JSON.stringify(storyStates));
}
// ========================================
// RENDERING
// ========================================
function renderBoard() {
kanbanBoard.innerHTML = '';
columns.forEach(column => {
const columnEl = createColumn(column);
kanbanBoard.appendChild(columnEl);
});
}
function createColumn(column) {
const columnEl = document.createElement('div');
columnEl.className = `kanban-column ${column.id}`;
columnEl.dataset.column = column.id;
const storiesInColumn = filteredStories.filter(
story => storyStates[story.number] === column.id
);
columnEl.innerHTML = `
<div class="column-header">
<div class="column-title">
<span>${column.name}</span>
<span class="column-count">${storiesInColumn.length}</span>
</div>
</div>
<div class="column-body" data-column="${column.id}">
${storiesInColumn.length === 0
? '<div class="empty-column">No stories here</div>'
: storiesInColumn.map(story => createStoryCard(story)).join('')
}
</div>
`;
const columnBody = columnEl.querySelector('.column-body');
columnBody.addEventListener('dragover', handleDragOver);
columnBody.addEventListener('dragleave', handleDragLeave);
columnBody.addEventListener('drop', handleDrop);
return columnEl;
}
function createStoryCard(story) {
const priorityClass = `badge-priority-${story.priority.toLowerCase()}`;
return `
<div class="story-card" draggable="true" data-number="${story.number}">
<div class="story-card-header">
<span class="story-number">${story.number}</span>
</div>
<div class="story-title">${story.title}</div>
<div class="story-meta">
<span class="badge ${priorityClass}">${story.priority}</span>
<span class="badge badge-effort">${story.effort}</span>
</div>
<div class="story-epic">${story.epic}</div>
<div class="story-preview">${story.user_story.substring(0, 100)}...</div>
<div class="story-actions">
<button class="story-action-btn" onclick="viewStory('${story.number}')">View</button>
<button class="story-action-btn" onclick="moveToNext('${story.number}')">→</button>
</div>
</div>
`;
}
// ========================================
// DRAG AND DROP
// ========================================
function handleDragStart(e) {
draggedStory = e.target.dataset.number;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
draggedStory = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
e.currentTarget.classList.add('drag-over');
}
function handleDragLeave(e) {
e.currentTarget.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('drag-over');
const targetColumn = e.currentTarget.dataset.column;
if (draggedStory && targetColumn) {
storyStates[draggedStory] = targetColumn;
saveToLocal();
renderBoard();
updateStats();
showToast(`Moved story ${draggedStory} to ${targetColumn}`, 'success');
}
}
// ========================================
// STORY ACTIONS
// ========================================
function viewStory(number) {
const story = storiesData.find(s => s.number === number);
if (!story) return;
modalTitle.textContent = `Story ${story.number}: ${story.title}`;
modalBody.innerHTML = `
<div class="modal-section">
<div class="modal-section-title">User Story</div>
<div class="modal-content">${formatContent(story.user_story)}</div>
</div>
<div class="modal-section">
<div class="modal-section-title">Background</div>
<div class="modal-content">${formatContent(story.background)}</div>
</div>
<div class="modal-section">
<div class="modal-section-title">Acceptance Criteria</div>
<div class="modal-content">${formatContent(story.acceptance)}</div>
</div>
`;
modalOverlay.classList.add('active');
}
function moveToNext(number) {
const currentIndex = columns.findIndex(c => c.id === storyStates[number]);
if (currentIndex < columns.length - 1) {
const nextColumn = columns[currentIndex + 1].id;
storyStates[number] = nextColumn;
saveToLocal();
renderBoard();
updateStats();
showToast(`Moved to ${nextColumn}`, 'success');
}
}
// ========================================
// SEARCH
// ========================================
function searchStories(query) {
const searchTerm = query.toLowerCase().trim();
if (!searchTerm) {
filteredStories = [...storiesData];
} else {
filteredStories = storiesData.filter(story =>
story.number.toLowerCase().includes(searchTerm) ||
story.title.toLowerCase().includes(searchTerm) ||
story.epic.toLowerCase().includes(searchTerm) ||
story.user_story.toLowerCase().includes(searchTerm)
);
}
renderBoard();
updateStats();
}
// ========================================
// STATS
// ========================================
function updateStats() {
const total = storiesData.length;
const done = Object.values(storyStates).filter(state => state === 'done').length;
statTotal.textContent = total;
statDone.textContent = done;
}
// ========================================
// SYNC STATUS
// ========================================
function updateSyncStatus(status, text) {
syncStatus.className = `sync-status ${status}`;
if (status === 'syncing') {
syncStatus.innerHTML = `<div class="spinner-small"></div><span>${text}</span>`;
} else {
syncStatus.innerHTML = `<span>${text}</span>`;
}
}
// ========================================
// LOADING
// ========================================
function showLoading(text) {
loadingText.textContent = text;
loadingOverlay.classList.add('active');
}
function hideLoading() {
loadingOverlay.classList.remove('active');
}
// ========================================
// UTILITIES
// ========================================
function formatContent(text) {
if (!text) return '';
let html = text
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>');
const lines = text.split('\n');
let inList = false;
let listType = 'ul';
let result = '';
lines.forEach(line => {
if (line.match(/^[\*\-]\s+/)) {
if (!inList) {
inList = true;
result += '<ul>';
}
result += `<li>${line.replace(/^[\*\-]\s+/, '')}</li>`;
} else if (line.match(/^\d+\.\s+/)) {
if (!inList) {
inList = true;
listType = 'ol';
result += '<ol>';
}
result += `<li>${line.replace(/^\d+\.\s+/, '')}</li>`;
} else {
if (inList) {
result += `</${listType}>`;
inList = false;
listType = 'ul';
}
if (line.trim()) {
result += `<p>${line}</p>`;
}
}
});
if (inList) {
result += `</${listType}>`;
}
return result || text;
}
function showToast(message, type = 'success') {
toastMessage.textContent = message;
toast.className = `toast ${type} active`;
setTimeout(() => {
toast.classList.remove('active');
}, 3000);
}
// ========================================
// EVENT LISTENERS
// ========================================
function attachEventListeners() {
// Search
searchBox.addEventListener('input', (e) => {
searchStories(e.target.value);
});
// Save button
saveBtn.addEventListener('click', () => {
if (config.apiKey && config.binId) {
saveToCloud();
} else {
saveToLocal();
showToast('Saved locally. Configure JSONBin for cloud sync.', 'warning');
configModalOverlay.classList.add('active');
}
});
// Refresh button
refreshBtn.addEventListener('click', () => {
if (config.apiKey && config.binId) {
loadFromCloud();
} else {
loadFromLocal();
}
});
// Config button
configBtn.addEventListener('click', () => {
configModalOverlay.classList.add('active');
});
// Config modal
configModalClose.addEventListener('click', () => {
configModalOverlay.classList.remove('active');
});
saveConfigBtn.addEventListener('click', saveConfig);
createBinBtn.addEventListener('click', createNewBin);
// Story modal
modalClose.addEventListener('click', () => {
modalOverlay.classList.remove('active');
});
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) {
modalOverlay.classList.remove('active');
}
});
configModalOverlay.addEventListener('click', (e) => {
if (e.target === configModalOverlay) {
configModalOverlay.classList.remove('active');
}
});
// Drag events
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('story-card')) {
handleDragStart(e);
}
});
document.addEventListener('dragend', (e) => {
if (e.target.classList.contains('story-card')) {
handleDragEnd(e);
}
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
modalOverlay.classList.remove('active');
configModalOverlay.classList.remove('active');
}
});
}
// ========================================
// START APPLICATION
// ========================================
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>