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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26,148 changes: 0 additions & 26,148 deletions package-lock.json

This file was deleted.

63 changes: 63 additions & 0 deletions src/components/WeeklyProgress/Filters.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import styles from './WeeklyProgress.module.css';

const Filters = ({ startDate, endDate, weeks, onDateChange, onPresetRange, rangeError }) => {
const handleStartChange = e => {
onDateChange({ start: e.target.value || '' });
};

const handleEndChange = e => {
onDateChange({ end: e.target.value || '' });
};

const presetOptions = [
{ label: 'Last 4 Weeks', value: 4 },
{ label: 'Last 8 Weeks', value: 8 },
{ label: 'Last 12 Weeks', value: 12 },
];

return (
<div className={styles.filters}>
<div className={styles.dateRow}>
<div className={styles.dateField}>
<label htmlFor="weekly-progress-start">Start date</label>
<input
id="weekly-progress-start"
type="date"
value={startDate || ''}
onChange={handleStartChange}
/>
</div>

<div className={styles.dateField}>
<label htmlFor="weekly-progress-end">End date</label>
<input
id="weekly-progress-end"
type="date"
value={endDate || ''}
onChange={handleEndChange}
/>
</div>
</div>

<div className={styles.presetRow}>
{presetOptions.map(opt => (
<button
key={opt.value}
type="button"
className={`${styles.presetButton} ${
weeks === opt.value ? styles.presetButtonActive : ''
}`}
onClick={() => onPresetRange(opt.value)}
>
{opt.label}
</button>
))}
</div>

{rangeError && <p className={styles.rangeError}>{rangeError}</p>}
</div>
);
};

export default Filters;
53 changes: 53 additions & 0 deletions src/components/WeeklyProgress/KpiTiles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import styles from './WeeklyProgress.module.css';

const TILE_CONFIG = [
{
key: 'totalTasks',
label: 'Total Tasks',
tooltip: 'Count of all tasks created up to the end date (excluding deleted).',
},
{
key: 'completedThisWeek',
label: 'Tasks Completed This Week',
tooltip: 'Tasks completed in the latest week of the selected window.',
},
{
key: 'openTasks',
label: 'Open Tasks',
tooltip: 'Tasks that are not in a terminal status as of the end date.',
},
{
key: 'averageCompletionTimeDays',
label: 'Average Completion Time (days)',
tooltip: 'Average time from task creation to completion within the selected window.',
isFloat: true,
},
];

const formatValue = (value, isFloat) => {
if (value == null) return '–';
if (isFloat) return value.toFixed(1);
return value.toLocaleString();
};

const KpiTiles = ({ summary, loading }) => {
return (
<div className={styles.kpiGrid}>
{TILE_CONFIG.map(tile => {
const value = summary ? summary[tile.key] : null;

return (
<div key={tile.key} className={styles.kpiTile} title={tile.tooltip}>
<div className={styles.kpiLabel}>{tile.label}</div>
<div className={styles.kpiValue}>
{loading && summary == null ? '…' : formatValue(value, tile.isFloat)}
</div>
</div>
);
})}
</div>
);
};

export default KpiTiles;
8 changes: 8 additions & 0 deletions src/components/WeeklyProgress/ProjectStatusBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import styles from './WeeklyProgress.module.css';

const ProjectStatusBar = () => {
return <div className={styles.statusBar}></div>;
};

export default ProjectStatusBar;
238 changes: 238 additions & 0 deletions src/components/WeeklyProgress/WeeklyProgress.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
.container {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}

/* Status bar */

.statusBar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}

.statusTitle {
font-weight: 600;
}

.statusBadge {
padding: 0.1rem 0.5rem;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.6);
font-size: 0.75rem;
}

.statusBadgePrimary {
padding: 0.1rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
background: #1d4ed8;
color: #f9fafb;
}

/* Header */

.header {
text-align: center;
}

.title {
font-size: 1.4rem;
font-weight: 600;
margin: 0;
}

.subtitle {
margin-top: 0.25rem;
font-size: 0.85rem;
color: #9ca3af;
}

/* Top layout: filters + KPIs */

.topRow {
display: flex;
flex-direction: column;
gap: 1rem;
}

.filtersWrap {
flex: 1;
}

.kpiWrap {
flex: 1;
}

/* Bigger screens: side-by-side */

@media (min-width: 900px) {
.topRow {
flex-direction: row;
align-items: stretch;
}
}

/* Filters */

.filters {
border-radius: 0.75rem;
padding: 0.9rem 1rem;
border: 1px solid rgba(148, 163, 184, 0.4);
display: flex;
flex-direction: column;
gap: 0.75rem;
}

.dateRow {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}

.dateField {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 140px;
flex: 1;
}

.dateField label {
font-size: 0.8rem;
color: #9ca3af;
}

.dateField input[type='date'] {
padding: 0.35rem 0.5rem;
border-radius: 0.5rem;
border: 1px solid rgba(148, 163, 184, 0.7);
background: transparent;
color: inherit;
font-size: 0.85rem;
}

.presetRow {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

.presetButton {
border-radius: 999px;
padding: 0.3rem 0.8rem;
border: 1px solid rgba(148, 163, 184, 0.7);
background: transparent;
color: inherit;
font-size: 0.8rem;
cursor: pointer;
}

.presetButtonActive {
background: #1d4ed8;
color: #f9fafb;
border-color: #1d4ed8;
}

.rangeError {
font-size: 0.8rem;
color: #f97373;
}

/* KPI tiles */

.kpiGrid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem;
}

.kpiTile {
border-radius: 0.75rem;
padding: 0.9rem 1rem;
border: 1.5px solid rgba(148, 163, 184, 0.4);
background: rgba(15, 23, 42, 0.4);
display: flex;
flex-direction: column;

min-height: 90px;
}

.kpiLabel {
font-size: 0.8rem;
color: #e5e7eb;
font-weight: 700;
margin-bottom: 0.25rem;
}

.kpiValue {
font-size: 1.15rem;
font-weight: 600;
margin-top: auto;
}

/* Chart */

.chartCard {
margin-top: 0.5rem;
padding: 1rem;
border-radius: 0.75rem;
border: 1px solid rgba(148, 163, 184, 0.4);
background: rgba(15, 23, 42, 0.6);
}

.chartWrapper {
display: flex;
flex-direction: column;
gap: 0.75rem;
}

.chartHeader {
display: flex;
flex-direction: column;
gap: 0.15rem;
}

.chartHeader h3 {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
text-align: center;
color: #e5e7eb;
}

.chartSubheader {
font-size: 0.8rem;
color: #cbd5f5;
text-align: center;
}

.chartInner {
width: 100%;
height: 320px;
}

.chartPlaceholder {
padding: 0.75rem 0;
font-size: 0.85rem;
color: #9ca3af;
text-align: center;
}

/* Error banner */

.errorBanner {
margin-top: 0.75rem;
padding: 0.6rem 0.8rem;
border-radius: 0.5rem;
border: 1px solid #f97373;
background: rgba(248, 113, 113, 0.1);
font-size: 0.8rem;
color: #fecaca;
}
Loading
Loading