Skip to main content

Frontend

The Projects module uses the Orchestrator + Partials pattern for index and show, with an Alpine.js modal for CRUD and asynchronous client search.

File Structure

resources/views/projects/
├── index.blade.php (index orchestrator)
├── show.blade.php (show orchestrator)
├── modals/
│ └── _project-form.blade.php (create/edit modal)
├── index/
│ ├── _header.blade.php
│ ├── _stats-cards.blade.php
│ ├── _filters.blade.php
│ ├── _table.blade.php
│ ├── _empty-state.blade.php
│ ├── stats-cards/
│ │ ├── _total.blade.php
│ │ ├── _in-progress.blade.php
│ │ ├── _completed.blade.php
│ │ └── _archived.blade.php
│ ├── filters/
│ │ ├── _search.blade.php
│ │ ├── _status.blade.php
│ │ ├── _priority.blade.php
│ │ └── _actions.blade.php
│ └── project-table/
│ ├── _header.blade.php
│ ├── _row.blade.php
│ ├── _row-name.blade.php
│ ├── _row-client.blade.php
│ ├── _row-status.blade.php
│ ├── _row-priority.blade.php
│ ├── _row-links.blade.php
│ ├── _row-created-at.blade.php
│ └── _row-actions.blade.php
└── show/
├── _header.blade.php
├── _content-tabs.blade.php
├── _client-info.blade.php
├── _quick-info.blade.php
├── _quick-links.blade.php
├── _tab-overview.blade.php
├── _tab-tasks.blade.php
├── _tab-meetings.blade.php
├── _tab-payments.blade.php
├── _tab-costs.blade.php
├── _tab-profit.blade.php
├── _tab-documents.blade.php
├── _tab-timesheets.blade.php
├── _tab-repository.blade.php
└── _tab-editor.blade.php

resources/js/components/
├── projectModal.js
├── clientSearch.js
└── projectSearch.js

Index

Icon with gradient, title, and "New Project" button that dispatches open-project-modal.

Stats Cards

4-card grid (grid-cols-1 md:grid-cols-2 lg:grid-cols-4):

CardDataGradient
Totalproject count + new this monthblue
In Progressin_progress countyellow
Completedcompleted countgreen
Archivedarchived countgray

Filters

GET form on projects.index:

  • Search - text on project name, client name, VAT number
  • Status - dropdown (all, draft, in_progress, completed, archived)
  • Priority - dropdown (low, medium, high)
  • Actions - Filter / Reset buttons

Table

Columns: name, client, status badge, priority badge, dev links (icons), creation date, actions (edit/delete). Pagination (15 per page).

Empty State

Centered message with icon when there are no projects.

Show

show.blade.php (orchestrator)

show.blade.php is the main orchestrator of the project detail page. It composes the page by including:

  1. projects.show._header - sticky header
  2. Grid grid-cols-1 lg:grid-cols-4:
    • Sidebar (1/4): _client-info, _quick-info, _quick-links
    • Main content (3/4): _content-tabs
  3. projects.modals._project-form - edit project modal (outside the grid)
  4. ai._panel - AI chatbot panel (conditional: only if AI is enabled in AiSettings)

Back arrow, project name, type/status/priority badges, deadline. "Edit" button dispatches edit-project.

  • Client info - x-client-summary component, or "Internal Project" badge if no client
  • Project stats - creation, update dates (diffForHumans), deadline with Google Calendar link
  • Quick links - x-projects.links component with clickable icons (repo, staging, prod, figma, docs)

_content-tabs.blade.php (tab sub-orchestrator)

_content-tabs.blade.php manages tab navigation and content. It is a sub-orchestrator that:

  • Initializes Alpine.js with activeTab from query string (?tab=costs), default overview
  • Renders the navigation bar with tab buttons, each with SVG icon and i18n label
  • The navigation wrapper uses overflow-x-auto + min-w-max to prevent overflow at low zoom levels
  • The active tab shows an emerald border (border-emerald-500), others are gray with hover
  • In the content block, each tab is wrapped in x-show="activeTab === '...'" with x-cloak
  • The Editor tab uses <template x-if> instead of x-show to avoid initializing Trix before the tab is opened
  • Includes the corresponding _tab-*.blade.php partials

Tabs

9 total tabs (Repository and Editor are always visible).

Each show tab includes partials from other modules to reuse existing tables and modals. The pattern is always the same: header with title + "Add" button, module partial table, "View all" link if > 50 records, empty state, and module form modal.

Tab Overview

Project's own content: description + notes, no external partials.

Both fields support inline editing without opening the modal:

  • Clicking "Edit" enters edit mode (textarea via inlineField Alpine.js component)
  • Changes are saved via PATCH /projects/{project}/field with { field, value }
  • Display uses whitespace-pre-wrap to preserve newlines
  • Translations (ui.saved, ui.error_saving) are passed from Blade to the component as parameters — no hardcoded strings in JS

Tab Tasks

Includes partials from the Tasks module:

  • tasks.partials._task-table - task table with data from $showData['tasks']
  • tasks.partials._modal-form - create/edit task modal
  • tasks.index._empty-state - empty state

Open event: open-task-modal. "View all" link points to tasks.index filtered by project_id.

Tab Meetings

Includes partials from the Meetings module:

  • meetings.partials._meeting-table - meetings table from $showData['meetings']
  • meetings.partials._modal-form - create/edit meeting modal

Open event: open-meeting-modal. "View all" link points to meetings.index filtered by project_id.

Tab Payments

Includes partials from the Payments module:

  • payments.partials._payment-table - payments table from $showData['payments']
  • payments.partials._modal-form - create/edit payment modal
  • payments.partials._upload-invoice-modal - invoice upload modal

Open event: open-payment-modal. "View all" link points to payments.index filtered by project_id.

Tab Costs

Includes partials from the Costs module:

  • costs.partials._cost-table - costs table from $showData['costs']
  • costs.partials._modal-form - create/edit cost modal
  • costs.partials._upload-receipt-modal - receipt upload modal

Open event: open-cost-modal. "View all" link points to costs.index filtered by project_id.

Tab Profit

The only tab without external partials - uses the x-profit.stat-card Blade component for the 4 KPI cards.

Currency shown via $currencySymbol (from BusinessSettings via AppServiceProvider):

CardDataGradient
Total Profitamount + margin %emerald (green if positive, red if negative)
Total Paymentsamount + countblue
Total Costsamount + countred
ROI(profit/costs)*100%, shows infinity if costs are zeropurple

Two action buttons that switch tabs (@click="activeTab = '...'")::

  • "View Payments" (blue) with count badge
  • "View Costs" (red) with count badge

Tab Documents

Includes partials from the Documents module:

  • documents.partials._document-table - documents table from $showData['documents']
  • documents.partials._modal - document upload modal (also receives $labels from Label::ordered())

Open event: open-document-modal. "View all" link points to documents.index filtered by project_id.

Tab Timesheets

Includes partials from the Timesheets module for monthly hour tracking. No cross-tab data from $showData — timesheets load independently.

Tab Repository

Conditional tab (only shown when $project->repo_url contains github.com). Powered by the repositoryTab Alpine.js component — fetches data lazily from GET /projects/{project}/repository and renders a commit activity heatmap + recent commits list.

Tab Editor

Rich text editor powered by Trix. Self-contained tab with no external module partials.

  • Form PUT /projects/{project}/editor submits the hidden input editor_notes
  • <trix-editor> is bound to the hidden input via the input attribute
  • data-upload-url points to POST /projects/{project}/editor/images for inline image upload
  • data-error-upload and data-error-invalid-type carry localized error strings for the toast
  • Tab content uses <template x-if> (not x-show) so Trix is only initialized when the tab is first opened, avoiding unnecessary DOM work on page load
  • Supported image formats: PNG, JPG, GIF — max 20MB
  • Save button is disabled while an upload is in progress (re-enabled in the finally block)

Cross-Module Dependency Summary

The project show is a hub that integrates partials from 5 external modules:

TabModuleTable PartialModal Partial
Taskstasks_task-table_modal-form
Meetingsmeetings_meeting-table_modal-form
Paymentspayments_payment-table_modal-form + _upload-invoice-modal
Costscosts_cost-table_modal-form + _upload-receipt-modal
Documentsdocuments_document-table_modal

All data is prepared by the backend in $showData (via ProjectShowQuery) with a limit of 50 records per tab and total counts for the "View all" links.

Project Modal

Component: projectModal.js

State: open, isEdit, projectId, activeTab, formData.

3 internal tabs:

Info:

  • Name (required)
  • Type (dropdown: client_work, product, content, asset)
  • Client (optional, with "Internal project" checkbox)
    • Asynchronous search via clientSearch.js on /api/clients/search?q=
    • 300ms debounce, results dropdown, selected client badge
  • Description, Status, Priority, Dates (start_date, due_date)

Links:

  • repo_url, staging_url, production_url, figma_url, docs_url

Notes:

  • Notes field (textarea)

Open: open-project-modal (create) and edit-project (edit with toFormPayload() payload) events.

Component: clientSearch.js

Asynchronous client search with debounce. Methods: searchClients(), selectClient(), clearClient(), syncFromProject().

Component: projectSearch.js

Global project search for navbar on /api/search/projects?q=. Direct navigation to the selected project.

JS Wiring

Component registration in resources/js/app.js:

  • Alpine.data('projectModal', projectModal)
  • Alpine.data('clientSearch', clientSearch)
  • Alpine.data('projectSearch', projectSearch)
  • Alpine.data('inlineField', inlineField) — inline field editing for description/notes on the overview tab

Trix Integration

Trix is imported globally in app.js (import 'trix') and configured via two document-level event listeners:

trix-initialize — fires when a <trix-editor> mounts:

  • Adds a click handler that opens links in a new tab (_blank), skipping image attachment anchors
  • Sets accept="image/jpeg,image/png,image/gif" on the hidden file input (OS picker filter)

trix-attachment-add — fires when a file is dragged/pasted into the editor:

  • Reads data-upload-url from the editor element
  • Disables the form submit button during upload
  • POSTs the file to the upload endpoint with the CSRF token
  • On success: calls attachment.setAttributes({ url }) to embed the image
  • On failure: removes the attachment and shows an error toast via Alpine.store('toast')
  • Always re-enables the submit button in finally

Internationalization

Strings in lang/*/projects.php (10 supported languages: en, it, fr, es, de, nl, pt, pl, uk, ro, da).

Main keys: page titles, form fields, type/status/priority options, placeholders, statistics, tab labels and links.

Editor-specific keys: editor_tab, editor_saved, editor_image_invalid_type, editor_image_upload_error.

Date labels use Carbon::translatedFormat(...).

Internationalization

Strings in lang/*/projects.php (10 supported languages: en, it, fr, es, de, nl, pt, pl, uk, ro, da).

Main keys: page titles, form fields, type/status/priority options, placeholders, statistics, tab labels and links.

Editor-specific keys: editor_tab, editor_saved, editor_image_invalid_type, editor_image_upload_error.

Date labels use Carbon::translatedFormat(...).

Dark Mode

Full support with Tailwind dark:* classes:

  • Background: bg-white dark:bg-gray-800
  • Text: text-gray-900 dark:text-white
  • Borders: border-gray-200 dark:border-gray-700
  • Card gradients: dark variant with reduced opacity
  • Forms: bg-white dark:bg-gray-700
  • Trix editor: toolbar and editor area overridden in app.css with gray-800/900 palette; toolbar icons inverted via filter: invert(1)