-
Notifications
You must be signed in to change notification settings - Fork 14
UI/Ux improvement #766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/751-ui-ux-improvements
Are you sure you want to change the base?
UI/Ux improvement #766
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,9 +24,10 @@ LOG_LEVEL=debug | |
| DB_CONNECTION=mysql | ||
| DB_HOST=127.0.0.1 | ||
| DB_PORT=3306 | ||
| DB_SOCKET=/var/lib/mysql/mysql.sock | ||
| DB_DATABASE=invoiceplane_test | ||
| DB_USERNAME=root | ||
| DB_PASSWORD=root | ||
| DB_PASSWORD=password | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg --hidden -n 'DB_(PASSWORD|DATABASE|USERNAME|SOCKET)' .env.testing phpunit.xml .Repository: InvoicePlane/InvoicePlane-v2 Length of output: 6718 🤖 get_repo_knowledge executed:
Length of output: 45512 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- composer scripts and test entry points ---'
rg -n -C 3 '"(test|phpunit)|phpunit|APP_ENV|--env' composer.json Makefile .github/workflows/phpunit.yml .github/DOCKER.md .junie/architecture.md 2>/dev/null || true
printf '%s\n' '--- test environment files ---'
cat -n .env.testing
printf '%s\n' '--- PHPUnit configuration ---'
cat -n phpunit.xml
printf '%s\n' '--- application bootstrap and environment loading ---'
rg -n -C 5 'LoadEnvironmentVariables|Dotenv|APP_ENV|env\(' bootstrap config vendor 2>/dev/null | head -240 || trueRepository: InvoicePlane/InvoicePlane-v2 Length of output: 33748 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Laravel bootstrap ---'
fd -t f -a 'bootstrap|Application.php|phpunit' . | head -80
rg -n -C 8 'LoadEnvironmentVariables|useEnvironmentPath|environmentFile|createDotenv|Dotenv' bootstrap app tests 2>/dev/null || true
printf '%s\n' '--- PHPUnit workflow environment and commands ---'
cat -n .github/workflows/phpunit.yml | sed -n '20,80p'
printf '%s\n' '--- test environment example ---'
cat -n .env.testing.example | sed -n '20,35p'
printf '%s\n' '--- relevant dependency versions ---'
rg -n -C 2 '"laravel/framework"|"phpunit/phpunit"|"vlucas/phpdotenv"' composer.json composer.lock | head -80Repository: InvoicePlane/InvoicePlane-v2 Length of output: 7338 Use one effective test database password.
🧰 Tools🪛 Betterleaks (1.8.1)[high] 30-30: Detected a potential hardcoded password literal, which may expose account credentials. (generic-password) 🪛 dotenv-linter (4.0.0)[warning] 30-30: [UnorderedKey] The DB_PASSWORD key should go before the DB_PORT key (UnorderedKey) 🤖 Prompt for AI Agents |
||
|
|
||
| SESSION_DRIVER=array | ||
| SESSION_LIFETIME=120 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| <?php | ||
|
|
||
| namespace Modules\Core\Enums; | ||
|
|
||
| /** | ||
| * The Filament panel themes a company can choose between, one case per | ||
| * stylesheet in `resources/css/filament/company/`. | ||
| * | ||
| * This enum is the whitelist that stands between the `panel_theme` setting | ||
| * row and `Panel::viteTheme()`: the stored value is a free-form string in the | ||
| * `settings` table, and handing an arbitrary one to Vite would fail manifest | ||
| * lookup at render time -- on every page of the panel, with no way back to | ||
| * the settings form to undo it. Resolve through `fromValue()` so an unknown | ||
| * or removed value degrades to the default instead. | ||
| * | ||
| * Adding a theme means: a stylesheet in `resources/css/filament/company/`, an | ||
| * entry in `vite.config.js` (unbuilt entrypoints are not in the manifest), and | ||
| * a case here. | ||
| */ | ||
| enum PanelTheme: string | ||
| { | ||
| case BASE = 'base'; | ||
| case INVOICEPLANE = 'invoiceplane'; | ||
| case INVOICEPLANE_BLUE = 'invoiceplane-blue'; | ||
| case NORD = 'nord'; | ||
| case ORANGE = 'orange'; | ||
| case REDDIT = 'reddit'; | ||
|
|
||
| /** | ||
| * The theme applied when a company has never chosen one, and the fallback | ||
| * for a stored value this enum no longer knows. | ||
| */ | ||
| public static function default(): self | ||
| { | ||
| return self::BASE; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve a stored setting value, falling back to the default for null, | ||
| * empty and unknown values. | ||
| */ | ||
| public static function fromValue(?string $value): self | ||
| { | ||
| if ($value === null || $value === '') { | ||
| return self::default(); | ||
| } | ||
|
|
||
| return self::tryFrom($value) ?? self::default(); | ||
| } | ||
|
|
||
| /** | ||
| * Options for a Filament `Select`, keyed by stored value. | ||
| * | ||
| * @return array<string, string> | ||
| */ | ||
| public static function options(): array | ||
| { | ||
| $options = []; | ||
|
|
||
| foreach (self::cases() as $case) { | ||
| $options[$case->value] = $case->label(); | ||
| } | ||
|
|
||
| return $options; | ||
| } | ||
|
|
||
| /** | ||
| * Display label. These are theme names rather than UI copy, so they are | ||
| * not translated -- same convention as ReportBand::getLabel(). | ||
| */ | ||
| public function label(): string | ||
| { | ||
| return match ($this) { | ||
| self::BASE => 'Base', | ||
| self::INVOICEPLANE => 'InvoicePlane', | ||
| self::INVOICEPLANE_BLUE => 'InvoicePlane Blue', | ||
| self::NORD => 'Nord', | ||
| self::ORANGE => 'Orange', | ||
| self::REDDIT => 'Reddit', | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * One-line description of what the theme looks like, shown under the | ||
| * option in the settings form. | ||
| */ | ||
| public function description(): string | ||
| { | ||
| return match ($this) { | ||
| self::BASE => trans('ip.panel_theme_base_description'), | ||
| self::INVOICEPLANE => trans('ip.panel_theme_invoiceplane_description'), | ||
| self::INVOICEPLANE_BLUE => trans('ip.panel_theme_invoiceplane_blue_description'), | ||
| self::NORD => trans('ip.panel_theme_nord_description'), | ||
| self::ORANGE => trans('ip.panel_theme_orange_description'), | ||
| self::REDDIT => trans('ip.panel_theme_reddit_description'), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * The Vite entrypoint passed to `Panel::viteTheme()`. | ||
| */ | ||
| public function viteEntrypoint(): string | ||
| { | ||
| return 'resources/css/filament/company/' . $this->value . '.css'; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| <?php | ||
|
|
||
| namespace Modules\Core\Filament\Company\Widgets; | ||
|
|
||
| use Filament\Widgets\StatsOverviewWidget; | ||
| use Filament\Widgets\StatsOverviewWidget\Stat; | ||
| use Modules\Core\Support\NumberFormatter; | ||
| use Modules\Invoices\Enums\InvoiceStatus; | ||
| use Modules\Invoices\Filament\Company\Resources\Invoices\InvoiceResource; | ||
| use Modules\Invoices\Models\Invoice; | ||
| use Modules\Quotes\Enums\QuoteStatus; | ||
| use Modules\Quotes\Filament\Company\Resources\Quotes\QuoteResource; | ||
| use Modules\Quotes\Models\Quote; | ||
|
|
||
| class CompanyStatsOverviewWidget extends StatsOverviewWidget | ||
| { | ||
| protected static ?int $sort = 0; | ||
|
|
||
| protected function getStats(): array | ||
| { | ||
| // 1. Paid revenue | ||
| $paidInvoicesQuery = Invoice::query()->where('invoice_status', InvoiceStatus::PAID); | ||
| $paidCount = $paidInvoicesQuery->count(); | ||
| $paidTotal = (float) $paidInvoicesQuery->sum('invoice_total'); | ||
|
|
||
| // 2. Pending / awaiting payment | ||
| $pendingQuery = Invoice::query()->whereIn('invoice_status', [ | ||
| InvoiceStatus::SENT, | ||
| InvoiceStatus::VIEWED, | ||
| InvoiceStatus::PARTIALLY_PAID, | ||
| ]); | ||
| $pendingCount = $pendingQuery->count(); | ||
| $pendingTotal = (float) $pendingQuery->sum('invoice_total'); | ||
|
|
||
| // 3. Overdue invoices | ||
| $overdueQuery = Invoice::query()->where(function ($query) { | ||
| $query->where('invoice_status', InvoiceStatus::OVERDUE) | ||
| ->orWhere(function ($sub) { | ||
| $sub->whereNotIn('invoice_status', [InvoiceStatus::PAID, InvoiceStatus::DRAFT]) | ||
| ->whereNotNull('invoice_due_at') | ||
| ->where('invoice_due_at', '<', now()->startOfDay()); | ||
| }); | ||
| }); | ||
| $overdueCount = $overdueQuery->count(); | ||
| $overdueTotal = (float) $overdueQuery->sum('invoice_total'); | ||
|
|
||
| // 4. Quotes pipeline | ||
| $quotesQuery = Quote::query()->whereIn('quote_status', [ | ||
| QuoteStatus::DRAFT, | ||
| QuoteStatus::SENT, | ||
| QuoteStatus::VIEWED, | ||
| ]); | ||
| $quotesCount = $quotesQuery->count(); | ||
| $quotesTotal = (float) $quotesQuery->sum('quote_total'); | ||
|
|
||
| $invoicesUrl = InvoiceResource::getUrl('index'); | ||
| $quotesUrl = QuoteResource::getUrl('index'); | ||
|
|
||
| return [ | ||
| Stat::make(trans('ip.invoice_status_paid'), NumberFormatter::formatCurrency($paidTotal)) | ||
| ->description(trans('ip.paid_invoices_count', ['count' => $paidCount])) | ||
| ->descriptionIcon('heroicon-m-check-circle') | ||
| ->color('success') | ||
| ->url($invoicesUrl), | ||
|
|
||
| Stat::make(trans('ip.awaiting_payment'), NumberFormatter::formatCurrency($pendingTotal)) | ||
| ->description(trans('ip.pending_invoices_count', ['count' => $pendingCount])) | ||
| ->descriptionIcon('heroicon-m-clock') | ||
| ->color('warning') | ||
| ->url($invoicesUrl), | ||
|
|
||
| Stat::make(trans('ip.invoice_status_overdue'), NumberFormatter::formatCurrency($overdueTotal)) | ||
| ->description(trans('ip.overdue_invoices_count', ['count' => $overdueCount])) | ||
| ->descriptionIcon('heroicon-m-exclamation-triangle') | ||
| ->color($overdueCount > 0 ? 'danger' : 'gray') | ||
| ->url($invoicesUrl), | ||
|
|
||
| Stat::make(trans('ip.quotes_pipeline'), NumberFormatter::formatCurrency($quotesTotal)) | ||
| ->description(trans('ip.active_quotes_count', ['count' => $quotesCount])) | ||
| ->descriptionIcon('heroicon-m-document-text') | ||
| ->color('primary') | ||
| ->url($quotesUrl), | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| <?php | ||
|
|
||
| namespace Modules\Core\Http\Middleware; | ||
|
|
||
| use Closure; | ||
| use Filament\Facades\Filament; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Auth; | ||
| use Modules\Core\Enums\PanelTheme; | ||
| use Modules\Core\Models\Setting; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| /** | ||
| * Applies the current company's `panel_theme` setting to the panel for this | ||
| * request. | ||
| * | ||
| * `Panel::viteTheme()` is normally called once in the panel provider, at | ||
| * service-provider boot, long before there is a tenant to read a setting | ||
| * from. It can be re-pointed later because the panel only resolves the | ||
| * stylesheet at render time -- `filament()->getTheme()` is evaluated inside | ||
| * the layout's `<head>` (see `filament::components.layout.base`). So mutating | ||
| * the panel from middleware is enough, provided the middleware runs before | ||
| * the response is rendered. | ||
| * | ||
| * It is registered in the company panel's `tenantMiddleware`, which Filament | ||
| * always prefixes with its own `IdentifyTenant`, so the tenant is resolved by | ||
| * the time this runs. | ||
| * | ||
| * The theme is set unconditionally, including when the company has not chosen | ||
| * one: the panel object is a singleton, so under a persistent worker (Octane) | ||
| * an early return would leave the previous request's company theme in place | ||
| * for the next one. | ||
| */ | ||
| class ApplyCompanyTheme | ||
| { | ||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| $panel = Filament::getCurrentPanel(); | ||
|
|
||
| if ($panel === null) { | ||
| return $next($request); | ||
| } | ||
|
|
||
| $companyId = $this->resolveCompanyId(); | ||
|
|
||
| $theme = $companyId === null | ||
| ? PanelTheme::default() | ||
| : PanelTheme::fromValue(Setting::getForCompany($companyId, Setting::KEY_PANEL_THEME)); | ||
|
|
||
| $panel->viteTheme($theme->viteEntrypoint()); | ||
|
|
||
| return $next($request); | ||
| } | ||
|
|
||
| /** | ||
| * Tenant first, then the session, then the user's first company -- the | ||
| * same order the tenant middleware chain resolves in, so a request that | ||
| * arrives before the tenant is on the route still themes correctly. | ||
| */ | ||
| private function resolveCompanyId(): ?int | ||
| { | ||
| $tenant = Filament::getTenant(); | ||
|
|
||
| if ($tenant !== null) { | ||
| return (int) $tenant->getKey(); | ||
| } | ||
|
|
||
| $sessionId = session('current_company_id'); | ||
|
|
||
| if ($sessionId !== null) { | ||
| return (int) $sessionId; | ||
| } | ||
|
|
||
| $company = Auth::user()?->companies()->first(); | ||
|
|
||
| return $company === null ? null : (int) $company->getKey(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: InvoicePlane/InvoicePlane-v2
Length of output: 1923
🤖 get_repo_knowledge executed:
get_repo_knowledge InvoicePlane/InvoicePlane-v2 /tmp/coderabbit-repo-knowledge/invoiceplane-invoiceplane-v2-84f901ed/architecture /tmp/coderabbit-repo-knowledge/invoiceplane-invoiceplane-v2-84f901ed/learnings /tmp/coderabbit-repo-knowledge/invoiceplane-invoiceplane-v2-84f901ed/conventionsLength of output: 47272
🏁 Script executed:
Repository: InvoicePlane/InvoicePlane-v2
Length of output: 8142
🏁 Script executed:
Repository: InvoicePlane/InvoicePlane-v2
Length of output: 9606
🌐 Web query:
Laravel 13 MySqlConnector unix_socket configuration uses socket instead of host💡 Result:
In Laravel 13, the MySqlConnector component is designed to prioritize the unix_socket configuration when determining the connection method [1]. Specifically, the MySqlConnector checks for the presence and validity of the unix_socket key in your database configuration array [1]. The logic implemented in the connector functions as follows: 1. Socket Priority: The connector uses a helper method to determine if a Unix socket is configured (i.e., if 'unix_socket' is set and not empty) [1]. 2. DSN Construction: If a Unix socket is provided, the connector generates a Data Source Name (DSN) string specifically for socket connections, which omits the host and port parameters and uses the unix_socket value instead (e.g., mysql:unix_socket=/path/to/socket;dbname=database) [1]. 3. Host Fallback: Only if the 'unix_socket' configuration is absent or empty does the connector fall back to the standard TCP/IP connection method, which utilizes the 'host' and 'port' configuration values [1]. This behavior is standard in Laravel's database configuration files (config/database.php), where the unix_socket option is available for both mysql and mariadb connections, typically defaulting to an environment variable (env('DB_SOCKET', '')) [2][3]. Because of this conditional logic, you do not need to explicitly unset the host configuration when using a Unix socket; providing the socket path is sufficient to override the TCP/IP connection path [1].
Citations:
Remove the fixed test database socket.
phpunit.xmlsetsDB_SOCKET=/var/lib/mysql/mysql.sock, which Laravel prioritizes overDB_HOSTandDB_PORT. PHPUnit can therefore fail before tests run when the MariaDB service does not provide that socket. RemoveDB_SOCKETfrom.env.testingandphpunit.xml, or set it only in environments that provide the socket.🤖 Prompt for AI Agents