Compare commits
No commits in common. "main" and "Project-KPIs" have entirely different histories.
main
...
Project-KP
11
README.md
11
README.md
|
|
@ -92,13 +92,12 @@ items first. (Items grandfathered from before teams existed appear under "No Tea
|
|||
"Uncategorized".)
|
||||
|
||||
The grid shows the trailing 12 months; each cell is the **most recent entry in that month**
|
||||
(color + value for KPIs, color for projects, tooltip with date and notes). KPI and project months
|
||||
without their own entry **carry the most recent earlier entry forward** (marked "Carried forward from …"
|
||||
in the tooltip, and in a project's detail row), so the latest value or status stays visible through the
|
||||
current month — months before the first entry remain blank. Rows expand in place:
|
||||
(color + value for KPIs, color for projects, tooltip with date and notes). Project months without
|
||||
their own entry **carry the most recent earlier entry forward** (marked "Carried forward from …" in the
|
||||
tooltip and detail row), so a project's latest status stays visible through the current month —
|
||||
months before its first entry remain blank. Rows expand in place:
|
||||
**click a KPI row** for an inline SVG trend chart with its threshold zones shaded (line + status-colored
|
||||
points for numeric KPIs, a met/missed timeline for milestones; carried-forward months show as hollow
|
||||
points joined by dashed legs); **click a project's month cell** for that
|
||||
points for numeric KPIs, a met/missed timeline for milestones); **click a project's month cell** for that
|
||||
month's notes/headwinds/tailwinds/highlights/blockers. Clicking anywhere else collapses the row.
|
||||
Group members can click through to any charted item's entries read-only; only the owner (or an
|
||||
admin) can edit. Everyone also has a personal chart at `/my-bowler` covering all of their own items.
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function editPassword(): View
|
||||
{
|
||||
return view('profile.password');
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'string', 'confirmed', Password::min(8)],
|
||||
]);
|
||||
|
||||
$request->user()->update(['password' => $data['password']]);
|
||||
|
||||
return redirect()
|
||||
->route('profile.password.edit')
|
||||
->with('status', 'Your password has been updated.');
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ namespace App\Support;
|
|||
|
||||
use App\Enums\KpiEvaluation;
|
||||
use App\Models\Kpi;
|
||||
use App\Models\KpiEntry;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectEntry;
|
||||
use Illuminate\Support\Collection;
|
||||
|
|
@ -13,9 +12,9 @@ use Illuminate\Support\Collection;
|
|||
* Builds the rolling 12-month bowler grid. Rows are grouped
|
||||
* Team -> Category (by name, case-insensitively, so same-named teams
|
||||
* from different owners merge on shared charts); within each month the
|
||||
* most recent entry wins (entry_date desc, id desc). Project and KPI
|
||||
* months without their own entry carry the most recent earlier entry
|
||||
* forward, so the latest status stays visible through the current month. Items
|
||||
* most recent entry wins (entry_date desc, id desc). Project months
|
||||
* without their own entry carry the most recent earlier entry forward,
|
||||
* so the latest status stays visible through the current month. Items
|
||||
* without a team/category fall into "No Team" / "Uncategorized", sorted last.
|
||||
*/
|
||||
class BowlerGrid
|
||||
|
|
@ -59,14 +58,14 @@ class BowlerGrid
|
|||
|
||||
$months = self::months();
|
||||
|
||||
$rows = $kpis->map(function (Kpi $kpi) use ($months, $start) {
|
||||
$monthEntries = self::kpiMonthEntries($kpi, $months, $start);
|
||||
$rows = $kpis->map(function (Kpi $kpi) use ($months) {
|
||||
$cells = self::kpiCells($kpi);
|
||||
|
||||
return [
|
||||
'type' => 'kpi',
|
||||
'item' => $kpi,
|
||||
'cells' => self::kpiCells($kpi, $monthEntries),
|
||||
'chart' => KpiChart::build($kpi, $monthEntries, $months),
|
||||
'cells' => $cells,
|
||||
'chart' => KpiChart::build($kpi, $cells, $months),
|
||||
];
|
||||
})
|
||||
->concat($projects->map(fn (Project $p) => ['type' => 'project', 'item' => $p, 'cells' => self::projectCells($p, $months, $start)]));
|
||||
|
|
@ -104,66 +103,22 @@ class BowlerGrid
|
|||
}
|
||||
|
||||
/**
|
||||
* The entry that represents each month for a KPI: its own latest entry,
|
||||
* or (for months without one) the most recent earlier entry carried
|
||||
* forward, including the current month. Months before the first entry
|
||||
* ever stay absent. The fill is seeded with the latest entry that
|
||||
* predates the window, so an old value carries across the gap up to the
|
||||
* first recent entry. Shared by the grid cells and the expanded chart.
|
||||
*
|
||||
* @param array<int, array{key: string, label: string}> $months
|
||||
* @param string $start first day of the 12-month window (Y-m-d)
|
||||
* @return array<string, array{entry: KpiEntry, carriedForward: bool}>
|
||||
* @return array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string}>
|
||||
*/
|
||||
private static function kpiMonthEntries(Kpi $kpi, array $months, string $start): array
|
||||
{
|
||||
$byMonth = [];
|
||||
|
||||
// Entries are ordered ascending, so the last write per month wins.
|
||||
foreach ($kpi->entries as $entry) {
|
||||
$byMonth[$entry->entry_date->format('Y-m')] = $entry;
|
||||
}
|
||||
|
||||
$filler = $kpi->entries()
|
||||
->where('entry_date', '<', $start)
|
||||
->orderByDesc('entry_date')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$result = [];
|
||||
foreach ($months as $month) {
|
||||
$key = $month['key'];
|
||||
if (isset($byMonth[$key])) {
|
||||
$filler = $byMonth[$key];
|
||||
$result[$key] = ['entry' => $filler, 'carriedForward' => false];
|
||||
} elseif ($filler !== null) {
|
||||
$result[$key] = ['entry' => $filler, 'carriedForward' => true];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{entry: KpiEntry, carriedForward: bool}> $monthEntries
|
||||
* @return array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string, carriedForward: bool}>
|
||||
*/
|
||||
private static function kpiCells(Kpi $kpi, array $monthEntries): array
|
||||
private static function kpiCells(Kpi $kpi): array
|
||||
{
|
||||
$cells = [];
|
||||
|
||||
foreach ($monthEntries as $key => ['entry' => $entry, 'carriedForward' => $carriedForward]) {
|
||||
// Entries are ordered ascending, so the last write per month wins.
|
||||
foreach ($kpi->entries as $entry) {
|
||||
$display = $kpi->evaluation === KpiEvaluation::Milestone
|
||||
? ($entry->value >= 1 ? 'Met' : 'Miss')
|
||||
: $kpi->value_type->format($entry->value, $kpi->unit_label);
|
||||
|
||||
$tooltip = trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? " — {$entry->notes}" : ''));
|
||||
|
||||
$cells[$key] = [
|
||||
$cells[$entry->entry_date->format('Y-m')] = [
|
||||
'status' => $kpi->evaluate($entry->value),
|
||||
'display' => $display,
|
||||
'tooltip' => $carriedForward ? 'Carried forward from ' . $tooltip : $tooltip,
|
||||
'carriedForward' => $carriedForward,
|
||||
'tooltip' => trim($entry->entry_date->format('Y-m-d') . ($entry->notes ? " — {$entry->notes}" : '')),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ use App\Models\Kpi;
|
|||
* View-model builder for the click-to-expand KPI chart on bowler charts.
|
||||
* Produces SVG-ready geometry: threshold zone bands, gridlines, the value
|
||||
* line (split on missing months), status-colored points, and axis labels.
|
||||
* Months without their own entry show the most recent earlier entry carried
|
||||
* forward: hollow points joined by dashed legs (or hollow milestone markers).
|
||||
*
|
||||
* Colors follow the dataviz status palette (state, not series identity):
|
||||
* good/warning/critical washes for the zones, points colored by each
|
||||
|
|
@ -38,25 +36,31 @@ class KpiChart
|
|||
private const PAD_BOTTOM = 30;
|
||||
|
||||
/**
|
||||
* @param array<string, array{entry: \App\Models\KpiEntry, carriedForward: bool}> $monthEntries per-month entry, with earlier entries carried forward into empty months
|
||||
* @param array<string, array{status: \App\Enums\RagStatus, display: string, tooltip: string}> $cells
|
||||
* @param array<int, array{key: string, label: string}> $months
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function build(Kpi $kpi, array $monthEntries, array $months): array
|
||||
public static function build(Kpi $kpi, array $cells, array $months): array
|
||||
{
|
||||
if ($kpi->evaluation === KpiEvaluation::Milestone) {
|
||||
return self::milestone($kpi, $monthEntries, $months);
|
||||
return self::milestone($kpi, $cells, $months);
|
||||
}
|
||||
|
||||
return self::line($kpi, $monthEntries, $months);
|
||||
return self::line($kpi, $cells, $months);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function line(Kpi $kpi, array $monthEntries, array $months): array
|
||||
private static function line(Kpi $kpi, array $cells, array $months): array
|
||||
{
|
||||
$numbers = array_map(fn ($m) => $m['entry']->value, $monthEntries);
|
||||
// Values come from the loaded entries window (latest per month, ascending order).
|
||||
$byMonth = [];
|
||||
foreach ($kpi->entries as $entry) {
|
||||
$byMonth[$entry->entry_date->format('Y-m')] = $entry;
|
||||
}
|
||||
|
||||
$numbers = array_map(fn ($e) => $e->value, $byMonth);
|
||||
$thresholds = match ($kpi->evaluation) {
|
||||
KpiEvaluation::TargetBand => [
|
||||
$kpi->target - $kpi->yellow_tolerance,
|
||||
|
|
@ -92,66 +96,41 @@ class KpiChart
|
|||
$gridlines[] = ['y' => $y($tick), 'label' => self::fmtTick($tick, $kpi)];
|
||||
}
|
||||
|
||||
// Points + line segments. Legs between two real entries are solid (a
|
||||
// gap splits the path); any leg touching a carried-forward point is
|
||||
// dashed so brought-forward values read as "not recorded this month".
|
||||
// Points + line segments (gaps split the path).
|
||||
$points = [];
|
||||
$solidRuns = [];
|
||||
$dashedLegs = [];
|
||||
$run = [];
|
||||
$prev = null;
|
||||
$lastText = null;
|
||||
$segments = [];
|
||||
$current = [];
|
||||
foreach ($months as $i => $month) {
|
||||
$item = $monthEntries[$month['key']] ?? null;
|
||||
if (! $item) {
|
||||
if (count($run) > 1) {
|
||||
$solidRuns[] = $run;
|
||||
$entry = $byMonth[$month['key']] ?? null;
|
||||
if (! $entry) {
|
||||
if (count($current) > 1) {
|
||||
$segments[] = $current;
|
||||
}
|
||||
$run = [];
|
||||
$prev = null;
|
||||
$current = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = $item['entry'];
|
||||
$carried = $item['carriedForward'];
|
||||
$px = round(self::PAD_LEFT + $slot * ($i + 0.5), 1);
|
||||
$py = $y($entry->value);
|
||||
$status = $kpi->evaluate($entry->value);
|
||||
$valueText = $kpi->value_type->format($entry->value, $kpi->unit_label);
|
||||
$title = $entry->entry_date->format('M Y') . ': ' . $valueText . ' — ' . $status->label();
|
||||
$points[] = [
|
||||
'x' => $px,
|
||||
'y' => $py,
|
||||
'color' => self::statusColor($status->value),
|
||||
'title' => $carried ? 'Carried forward from ' . $title : $title,
|
||||
'title' => $entry->entry_date->format('M Y') . ': '
|
||||
. $kpi->value_type->format($entry->value, $kpi->unit_label)
|
||||
. ' — ' . $status->label(),
|
||||
'note' => $entry->notes,
|
||||
'carriedForward' => $carried,
|
||||
];
|
||||
$lastText = $valueText;
|
||||
|
||||
if ($prev !== null) {
|
||||
if (! $prev['carried'] && ! $carried) {
|
||||
if ($run === []) {
|
||||
$run[] = $prev['xy'];
|
||||
}
|
||||
$run[] = [$px, $py];
|
||||
} else {
|
||||
if (count($run) > 1) {
|
||||
$solidRuns[] = $run;
|
||||
}
|
||||
$run = [];
|
||||
$dashedLegs[] = [$prev['xy'], [$px, $py]];
|
||||
}
|
||||
}
|
||||
$prev = ['xy' => [$px, $py], 'carried' => $carried];
|
||||
$current[] = [$px, $py];
|
||||
}
|
||||
if (count($run) > 1) {
|
||||
$solidRuns[] = $run;
|
||||
if (count($current) > 1) {
|
||||
$segments[] = $current;
|
||||
}
|
||||
|
||||
$toPath = fn ($seg) => 'M' . implode(' L', array_map(fn ($p) => "{$p[0]},{$p[1]}", $seg));
|
||||
$paths = array_map($toPath, $solidRuns);
|
||||
$carriedPaths = array_map($toPath, $dashedLegs);
|
||||
$paths = array_map(
|
||||
fn ($seg) => 'M' . implode(' L', array_map(fn ($p) => "{$p[0]},{$p[1]}", $seg)),
|
||||
$segments
|
||||
);
|
||||
|
||||
// Selective labeling: the value at the line's end only.
|
||||
$endLabel = null;
|
||||
|
|
@ -160,7 +139,7 @@ class KpiChart
|
|||
$endLabel = [
|
||||
'x' => $last['x'] + 9,
|
||||
'y' => $last['y'] + 4,
|
||||
'text' => $lastText,
|
||||
'text' => $kpi->value_type->format($byMonth[array_key_last($byMonth)]->value, $kpi->unit_label),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +152,6 @@ class KpiChart
|
|||
'refLines' => $refLines,
|
||||
'gridlines' => $gridlines,
|
||||
'paths' => $paths,
|
||||
'carriedPaths' => $carriedPaths,
|
||||
'lineColor' => self::LINE,
|
||||
'points' => $points,
|
||||
'endLabel' => $endLabel,
|
||||
|
|
@ -237,8 +215,13 @@ class KpiChart
|
|||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function milestone(Kpi $kpi, array $monthEntries, array $months): array
|
||||
private static function milestone(Kpi $kpi, array $cells, array $months): array
|
||||
{
|
||||
$byMonth = [];
|
||||
foreach ($kpi->entries as $entry) {
|
||||
$byMonth[$entry->entry_date->format('Y-m')] = $entry;
|
||||
}
|
||||
|
||||
$height = 110;
|
||||
$plotW = self::W - self::PAD_LEFT - self::PAD_RIGHT;
|
||||
$slot = $plotW / count($months);
|
||||
|
|
@ -246,21 +229,18 @@ class KpiChart
|
|||
|
||||
$markers = [];
|
||||
foreach ($months as $i => $month) {
|
||||
$item = $monthEntries[$month['key']] ?? null;
|
||||
if (! $item) {
|
||||
$entry = $byMonth[$month['key']] ?? null;
|
||||
if (! $entry) {
|
||||
continue;
|
||||
}
|
||||
$entry = $item['entry'];
|
||||
$met = $entry->value >= 1;
|
||||
$title = $entry->entry_date->format('M Y') . ': ' . ($met ? 'Met' : 'Not met');
|
||||
$markers[] = [
|
||||
'x' => round(self::PAD_LEFT + $slot * ($i + 0.5), 1),
|
||||
'y' => $baseline,
|
||||
'color' => $met ? self::GOOD : self::CRIT,
|
||||
'glyph' => $met ? '✓' : '✕',
|
||||
'title' => $item['carriedForward'] ? 'Carried forward from ' . $title : $title,
|
||||
'title' => $entry->entry_date->format('M Y') . ': ' . ($met ? 'Met' : 'Not met'),
|
||||
'note' => $entry->notes,
|
||||
'carriedForward' => $item['carriedForward'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@
|
|||
</table>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
Each cell shows the most recent entry in that month; months without an entry carry the most recent
|
||||
KPI value or project status forward (the tooltip marks it). The current month is highlighted.
|
||||
Each cell shows the most recent entry in that month; project months without an entry carry the most recent
|
||||
status forward (the tooltip marks it). The current month is highlighted.
|
||||
Click a KPI row to expand its trend chart; click a project row for its latest details, or a specific cell for that month.
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,32 +40,20 @@
|
|||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
@endforeach
|
||||
|
||||
{{-- Carried-forward legs (any leg touching a brought-forward month): dashed, no draw-in --}}
|
||||
@foreach ($chart['carriedPaths'] as $path)
|
||||
<path d="{{ $path }}" fill="none" style="stroke: var(--theme-chart-line)" stroke-width="2"
|
||||
stroke-dasharray="4 3" stroke-linecap="round" stroke-opacity="0.7" />
|
||||
@endforeach
|
||||
|
||||
{{-- Points, status-colored with a 2px surface ring (carried-forward points are hollow:
|
||||
surface fill, status-colored ring). Noted points get a pulsing halo and a custom
|
||||
hover tooltip instead of the native <title>. --}}
|
||||
{{-- Points, status-colored with a 2px surface ring. Noted points get a
|
||||
pulsing halo and a custom hover tooltip instead of the native <title>. --}}
|
||||
@foreach ($chart['points'] as $point)
|
||||
@php
|
||||
$hollow = !empty($point['carriedForward']);
|
||||
$pointFill = $hollow ? '#ffffff' : $point['color'];
|
||||
$pointStroke = $hollow ? $point['color'] : '#ffffff';
|
||||
@endphp
|
||||
@if (!empty($point['note']))
|
||||
<g class="kpi-pop js-note-point" style="--i: {{ $loop->index }}"
|
||||
data-note="{{ $point['note'] }}" data-title="{{ $point['title'] }}">
|
||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="8" class="kpi-note-ring" />
|
||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="4.5"
|
||||
fill="{{ $pointFill }}" stroke="{{ $pointStroke }}" stroke-width="2" />
|
||||
fill="{{ $point['color'] }}" stroke="#ffffff" stroke-width="2" />
|
||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="12" fill="transparent" class="js-note-hit" />
|
||||
</g>
|
||||
@else
|
||||
<circle cx="{{ $point['x'] }}" cy="{{ $point['y'] }}" r="4.5" class="kpi-pop" style="--i: {{ $loop->index }}"
|
||||
fill="{{ $pointFill }}" stroke="{{ $pointStroke }}" stroke-width="2">
|
||||
fill="{{ $point['color'] }}" stroke="#ffffff" stroke-width="2">
|
||||
<title>{{ $point['title'] }}</title>
|
||||
</circle>
|
||||
@endif
|
||||
|
|
@ -90,29 +78,22 @@
|
|||
<line x1="{{ $chart['plot']['x'] }}" y1="{{ $chart['baseline'] }}" x2="{{ $chart['plot']['x'] + $chart['plot']['w'] }}" y2="{{ $chart['baseline'] }}"
|
||||
stroke="{{ \App\Support\KpiChart::GRIDLINE }}" stroke-width="1" />
|
||||
@foreach ($chart['markers'] as $marker)
|
||||
{{-- Carried-forward markers are hollow: surface fill, status-colored ring and glyph --}}
|
||||
@php
|
||||
$hollow = !empty($marker['carriedForward']);
|
||||
$markerFill = $hollow ? '#ffffff' : $marker['color'];
|
||||
$markerStroke = $hollow ? $marker['color'] : '#ffffff';
|
||||
$glyphFill = $hollow ? $marker['color'] : '#ffffff';
|
||||
@endphp
|
||||
@if (!empty($marker['note']))
|
||||
<g class="kpi-pop js-note-point" style="--i: {{ $loop->index }}"
|
||||
data-note="{{ $marker['note'] }}" data-title="{{ $marker['title'] }}">
|
||||
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="13" class="kpi-note-ring" />
|
||||
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="9"
|
||||
fill="{{ $markerFill }}" stroke="{{ $markerStroke }}" stroke-width="2" />
|
||||
fill="{{ $marker['color'] }}" stroke="#ffffff" stroke-width="2" />
|
||||
<text x="{{ $marker['x'] }}" y="{{ $marker['y'] + 3.5 }}" text-anchor="middle"
|
||||
font-size="10" font-weight="700" fill="{{ $glyphFill }}">{{ $marker['glyph'] }}</text>
|
||||
font-size="10" font-weight="700" fill="#ffffff">{{ $marker['glyph'] }}</text>
|
||||
</g>
|
||||
@else
|
||||
<g class="kpi-pop" style="--i: {{ $loop->index }}">
|
||||
<title>{{ $marker['title'] }}</title>
|
||||
<circle cx="{{ $marker['x'] }}" cy="{{ $marker['y'] }}" r="9"
|
||||
fill="{{ $markerFill }}" stroke="{{ $markerStroke }}" stroke-width="2" />
|
||||
fill="{{ $marker['color'] }}" stroke="#ffffff" stroke-width="2" />
|
||||
<text x="{{ $marker['x'] }}" y="{{ $marker['y'] + 3.5 }}" text-anchor="middle"
|
||||
font-size="10" font-weight="700" fill="{{ $glyphFill }}">{{ $marker['glyph'] }}</text>
|
||||
font-size="10" font-weight="700" fill="#ffffff">{{ $marker['glyph'] }}</text>
|
||||
</g>
|
||||
@endif
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@
|
|||
<a href="{{ route('teams.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Teams</a>
|
||||
<a href="{{ route('categories.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Categories</a>
|
||||
<a href="{{ route('api-keys.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">API Keys</a>
|
||||
<a href="{{ route('profile.password.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Change Password</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Change Password — ' . config('app.name'))
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto max-w-md rounded-lg border border-gray-200 bg-white p-8">
|
||||
<h1 class="mb-2 text-2xl font-bold">Change password</h1>
|
||||
<p class="mb-6 text-gray-600">Update the password for your account.</p>
|
||||
|
||||
<form method="POST" action="{{ route('profile.password.update') }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="current_password" class="mb-1 block font-semibold">Current password</label>
|
||||
<input id="current_password" type="password" name="current_password" required autofocus autocomplete="current-password"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
|
||||
@error('current_password')
|
||||
<p class="mt-1 text-sm text-red-700">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 block font-semibold">New password</label>
|
||||
<input id="password" type="password" name="password" required autocomplete="new-password"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
|
||||
@error('password')
|
||||
<p class="mt-1 text-sm text-red-700">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="password_confirmation" class="mb-1 block font-semibold">Confirm new password</label>
|
||||
<input id="password_confirmation" type="password" name="password_confirmation" required autocomplete="new-password"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Update password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -7,7 +7,6 @@ use App\Http\Controllers\CategoryController;
|
|||
use App\Http\Controllers\GroupAdminController;
|
||||
use App\Http\Controllers\KpiController;
|
||||
use App\Http\Controllers\KpiEntryController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\ProjectController;
|
||||
use App\Http\Controllers\ProjectEntryController;
|
||||
use App\Http\Controllers\TeamController;
|
||||
|
|
@ -82,10 +81,6 @@ Route::middleware('auth')->group(function () {
|
|||
Route::put('/categories/{category}', [CategoryController::class, 'update'])->name('categories.update');
|
||||
Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->name('categories.destroy');
|
||||
|
||||
// Profile
|
||||
Route::get('/profile/password', [ProfileController::class, 'editPassword'])->name('profile.password.edit');
|
||||
Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password.update');
|
||||
|
||||
// API keys
|
||||
Route::get('/api-keys', [ApiKeyController::class, 'index'])->name('api-keys.index');
|
||||
Route::post('/api-keys', [ApiKeyController::class, 'store'])->name('api-keys.store');
|
||||
|
|
|
|||
Loading…
Reference in New Issue