refactor: improve UI responsiveness, add currency formatting to invoice inputs, update address formatting, and refine Docker setup instructions.
This commit is contained in:
12
How-To.txt
12
How-To.txt
@@ -1,6 +1,14 @@
|
|||||||
git clone this repository
|
# On local machine
|
||||||
|
#git clone this repository
|
||||||
composer install
|
composer install
|
||||||
#php artisan key:generate
|
php artisan key:generate
|
||||||
touch database/database.sqlite && php artisan migrate
|
touch database/database.sqlite && php artisan migrate
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
|
# On Docker
|
||||||
|
cp .env.example .env.docker
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,14 +57,19 @@ class Client extends Model
|
|||||||
|
|
||||||
public function getFullAddressAttribute(): string
|
public function getFullAddressAttribute(): string
|
||||||
{
|
{
|
||||||
|
$line3_parts = array_filter([$this->city, $this->state]);
|
||||||
|
$country_part = array_filter([$this->postal_code, $this->country]);
|
||||||
|
$country_str = implode(' - ', $country_part);
|
||||||
|
if ($country_str) {
|
||||||
|
$line3_parts[] = $country_str;
|
||||||
|
}
|
||||||
|
$line3 = implode(', ', $line3_parts);
|
||||||
|
|
||||||
return collect([
|
return collect([
|
||||||
$this->address_line1,
|
$this->address_line1,
|
||||||
$this->address_line2,
|
$this->address_line2,
|
||||||
$this->city,
|
$line3
|
||||||
$this->state,
|
])->filter()->implode("\n");
|
||||||
$this->postal_code,
|
|
||||||
$this->country,
|
|
||||||
])->filter()->implode(', ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInitialsAttribute(): string
|
public function getInitialsAttribute(): string
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
@if($client->full_address)
|
@if($client->full_address)
|
||||||
<div class="flex gap-2 items-start">
|
<div class="flex gap-2 items-start">
|
||||||
<span class="text-gray-400 shrink-0">📍</span>
|
<span class="text-gray-400 shrink-0">📍</span>
|
||||||
<span class="text-gray-600">{{ $client->full_address }}</span>
|
<span class="text-gray-600">{!! nl2br(e($client->full_address)) !!}</span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if($client->notes)
|
@if($client->notes)
|
||||||
|
|||||||
@@ -8,19 +8,6 @@
|
|||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<a href="{{ route('content.create') }}" class="btn-sakura text-sm shrink-0">+ New Brief</a>
|
<a href="{{ route('content.create') }}" class="btn-sakura text-sm shrink-0">+ New Brief</a>
|
||||||
@foreach(['instagram', 'tiktok', 'youtube'] as $p)
|
|
||||||
<a href="{{ request()->fullUrlWithQuery(['platform' => $p]) }}"
|
|
||||||
class="px-3 py-1.5 rounded-full text-xs font-semibold transition-colors
|
|
||||||
{{ request('platform') === $p ? 'text-white' : 'bg-white text-gray-500 hover:bg-pink-50 border border-pink-100' }}"
|
|
||||||
style="{{ request('platform') === $p ? 'background:#FFB7C5' : '' }}">
|
|
||||||
{{ ['instagram' => '📸', 'tiktok' => '🎵', 'youtube' => '▶️'][$p] }} {{ ucfirst($p) }}
|
|
||||||
</a>
|
|
||||||
@endforeach
|
|
||||||
@if(request('platform'))
|
|
||||||
<a href="{{ route('content.index') }}"
|
|
||||||
class="px-3 py-1.5 rounded-full text-xs text-gray-400 hover:text-gray-600 border border-gray-200 bg-white">✕
|
|
||||||
Clear</a>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -36,8 +23,19 @@
|
|||||||
@php $status = $col['status']; @endphp
|
@php $status = $col['status']; @endphp
|
||||||
|
|
||||||
{{-- Column shell — layout only, NOT the sort target --}}
|
{{-- Column shell — layout only, NOT the sort target --}}
|
||||||
|
@php
|
||||||
|
$bgColors = [
|
||||||
|
'drafting' => '#f2f2f2',
|
||||||
|
'filming' => 'rgba(251, 189, 35, 0.2)',
|
||||||
|
'editing' => 'rgba(58, 191, 248, 0.2)',
|
||||||
|
'review' => 'rgba(240, 0, 184, 0.2)',
|
||||||
|
'posted' => 'rgba(54, 211, 153, 0.2)',
|
||||||
|
'archived' => 'rgba(42, 50, 60, 0.2)',
|
||||||
|
];
|
||||||
|
$bgStyle = isset($bgColors[$status->value]) ? 'background-color: ' . $bgColors[$status->value] . ';' : '';
|
||||||
|
@endphp
|
||||||
<div class="{{ $status->colorClass() }} flex flex-col w-full lg:flex-1 lg:min-w-36 rounded-2xl p-3"
|
<div class="{{ $status->colorClass() }} flex flex-col w-full lg:flex-1 lg:min-w-36 rounded-2xl p-3"
|
||||||
data-status="{{ $status->value }}">
|
style="{{ $bgStyle }}" data-status="{{ $status->value }}">
|
||||||
|
|
||||||
{{-- Column header --}}
|
{{-- Column header --}}
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
@@ -54,7 +52,7 @@
|
|||||||
|
|
||||||
@foreach($col['items'] as $brief)
|
@foreach($col['items'] as $brief)
|
||||||
<div class="bg-white rounded-2xl p-3 shadow-sm border border-pink-50 select-none
|
<div class="bg-white rounded-2xl p-3 shadow-sm border border-pink-50 select-none
|
||||||
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow kanban-item"
|
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow kanban-item"
|
||||||
data-id="{{ $brief->id }}">
|
data-id="{{ $brief->id }}">
|
||||||
<div class="flex items-start gap-2 mb-1.5">
|
<div class="flex items-start gap-2 mb-1.5">
|
||||||
<span class="text-sm shrink-0">{{ $brief->platform_icon }}</span>
|
<span class="text-sm shrink-0">{{ $brief->platform_icon }}</span>
|
||||||
@@ -93,7 +91,7 @@
|
|||||||
{{-- Add button --}}
|
{{-- Add button --}}
|
||||||
<a href="{{ route('content.create') }}?status={{ $status->value }}"
|
<a href="{{ route('content.create') }}?status={{ $status->value }}"
|
||||||
class="mt-3 text-xs text-gray-400 hover:text-pink-500 text-center py-2
|
class="mt-3 text-xs text-gray-400 hover:text-pink-500 text-center py-2
|
||||||
border-2 border-dashed border-pink-100 rounded-2xl hover:border-pink-300 transition-colors block">
|
border-2 border-dashed border-pink-100 rounded-2xl hover:border-pink-300 transition-colors block">
|
||||||
+ Add
|
+ Add
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,9 +78,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
||||||
<input type="number" :name="`items[${index}][unit_price]`"
|
<input type="hidden" :name="`items[${index}][unit_price]`" :value="item.unit_price">
|
||||||
x-model.number="item.unit_price" @input="recalc()"
|
<input type="text"
|
||||||
class="input-soft text-sm bg-white" min="0" step="1000" placeholder="0">
|
:value="item.unit_price ? formatNum(item.unit_price) : ''"
|
||||||
|
@input="
|
||||||
|
let raw = $event.target.value.replace(/\D/g, '');
|
||||||
|
item.unit_price = raw ? parseInt(raw, 10) : 0;
|
||||||
|
$event.target.value = item.unit_price ? formatNum(item.unit_price) : '';
|
||||||
|
recalc();
|
||||||
|
"
|
||||||
|
class="input-soft text-sm bg-white" placeholder="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24 shrink-0 text-right">
|
<div class="w-24 shrink-0 text-right">
|
||||||
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
||||||
|
|||||||
@@ -75,9 +75,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
||||||
<input type="number" :name="`items[${index}][unit_price]`"
|
<input type="hidden" :name="`items[${index}][unit_price]`" :value="item.unit_price">
|
||||||
x-model.number="item.unit_price" @input="recalc()"
|
<input type="text"
|
||||||
class="input-soft text-sm bg-white" min="0" step="1000">
|
:value="item.unit_price ? formatNum(item.unit_price) : ''"
|
||||||
|
@input="
|
||||||
|
let raw = $event.target.value.replace(/\D/g, '');
|
||||||
|
item.unit_price = raw ? parseInt(raw, 10) : 0;
|
||||||
|
$event.target.value = item.unit_price ? formatNum(item.unit_price) : '';
|
||||||
|
recalc();
|
||||||
|
"
|
||||||
|
class="input-soft text-sm bg-white" placeholder="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24 shrink-0 text-right">
|
<div class="w-24 shrink-0 text-right">
|
||||||
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
||||||
|
|||||||
@@ -271,7 +271,7 @@
|
|||||||
@if($invoice->client->email)
|
@if($invoice->client->email)
|
||||||
<div class="billing-detail">{{ $invoice->client->email }}</div> @endif
|
<div class="billing-detail">{{ $invoice->client->email }}</div> @endif
|
||||||
@if($invoice->client->full_address)
|
@if($invoice->client->full_address)
|
||||||
<div class="billing-detail">{{ $invoice->client->full_address }}</div> @endif
|
<div class="billing-detail">{!! nl2br(e($invoice->client->full_address)) !!}</div> @endif
|
||||||
@else
|
@else
|
||||||
<div class="billing-detail">—</div>
|
<div class="billing-detail">—</div>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
@if($invoice->client->email)
|
@if($invoice->client->email)
|
||||||
<div class="text-sm text-gray-500">{{ $invoice->client->email }}</div> @endif
|
<div class="text-sm text-gray-500">{{ $invoice->client->email }}</div> @endif
|
||||||
@if($invoice->client->full_address)
|
@if($invoice->client->full_address)
|
||||||
<div class="text-sm text-gray-400">{{ $invoice->client->full_address }}</div> @endif
|
<div class="text-sm text-gray-400">{!! nl2br(e($invoice->client->full_address)) !!}</div> @endif
|
||||||
@else
|
@else
|
||||||
<div class="text-gray-500 text-sm">No client</div>
|
<div class="text-gray-500 text-sm">No client</div>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -6,18 +6,18 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-5">
|
<div class="flex flex-wrap items-center justify-between gap-3 mb-5">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2 w-full">
|
||||||
<a href="{{ route('rate-cards.create') }}" class="btn-sakura text-sm shrink-0">+ Add Service</a>
|
<a href="{{ route('rate-cards.create') }}" class="btn-sakura text-xs sm:text-sm shrink-0 px-3 py-1.5 sm:px-4 sm:py-2 flex items-center gap-1">+ Add Service</a>
|
||||||
<a href="{{ route('rate-cards.public') }}" target="_blank"
|
<a href="{{ route('rate-cards.public') }}" target="_blank"
|
||||||
class="btn-sakura text-sm px-3 py-2 rounded-full border border-pink-200 text-pink-500 hover:bg-pink-50 flex items-center gap-1">
|
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-pink-200 text-pink-600 hover:bg-pink-50 flex items-center gap-1 shadow-sm shrink-0">
|
||||||
🔗 Public Page
|
🔗 Public Page
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('rate-cards.pdf') }}"
|
<a href="{{ route('rate-cards.pdf') }}"
|
||||||
class="btn-sakura text-sm px-3 py-2 rounded-full border border-blue-200 text-blue-500 hover:bg-blue-50 flex items-center gap-1">
|
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-blue-200 text-blue-600 hover:bg-blue-50 flex items-center gap-1 shadow-sm shrink-0">
|
||||||
📄 PDF
|
📄 PDF
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('rate-cards.profile') }}"
|
<a href="{{ route('rate-cards.profile') }}"
|
||||||
class="btn-sakura text-sm px-3 py-2 rounded-full border border-purple-200 text-purple-500 hover:bg-purple-50 flex items-center gap-1">
|
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-purple-200 text-purple-600 hover:bg-purple-50 flex items-center gap-1 shadow-sm shrink-0">
|
||||||
🎨 Profile
|
🎨 Profile
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -282,8 +282,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Past Brands --}}
|
{{-- Past Brands --}}
|
||||||
<div class="cat-card overflow-hidden">
|
<div class="cat-card overflow-hidden">
|
||||||
<div class="flex items-center gap-3 px-5 py-4 border-b border-pink-50">
|
<div class="flex items-center gap-3 px-5 py-4 border-b border-pink-50">
|
||||||
@@ -356,18 +354,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Actions --}}
|
{{-- Actions --}}
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-2 w-full">
|
||||||
<button type="submit" class="btn-sakura px-6 py-2.5">💾 Save Profile</button>
|
<button type="submit" class="btn-sakura text-xs sm:text-sm shrink-0 px-4 py-2 sm:px-6 sm:py-2.5">💾 Save Profile</button>
|
||||||
<a href="{{ route('rate-cards.public') }}" target="_blank"
|
<a href="{{ route('rate-cards.public') }}" target="_blank"
|
||||||
class="btn-sakura text-sm px-4 py-2.5 rounded-full border border-pink-200 text-pink-500 hover:bg-pink-50">
|
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-pink-200 text-pink-600 hover:bg-pink-50 shadow-sm shrink-0">
|
||||||
👁️ Preview
|
👁️ Preview
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('rate-cards.pdf') }}"
|
<a href="{{ route('rate-cards.pdf') }}"
|
||||||
class="btn-sakura text-sm px-4 py-2.5 rounded-full border border-blue-200 text-blue-500 hover:bg-blue-50">
|
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-blue-200 text-blue-600 hover:bg-blue-50 shadow-sm shrink-0">
|
||||||
📄 Download PDF
|
📄 Download PDF
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('rate-cards.index') }}"
|
<a href="{{ route('rate-cards.index') }}"
|
||||||
class="btn-sakura text-sm px-4 py-2.5 rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50">
|
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-gray-200 text-gray-600 hover:bg-gray-50 shadow-sm shrink-0">
|
||||||
← Back
|
← Back
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
left: 'prev,next today',
|
left: 'prev,next today',
|
||||||
center: 'title',
|
center: 'title',
|
||||||
right: isMobile ? 'listWeek,timeGridDay' : 'dayGridMonth,timeGridWeek,listWeek',
|
right: isMobile ? 'dayGridMonth,listWeek,timeGridDay' : 'dayGridMonth,timeGridWeek,listWeek',
|
||||||
},
|
},
|
||||||
editable: !isMobile,
|
editable: !isMobile,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@@ -99,7 +99,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
height: 'auto',
|
height: 'auto',
|
||||||
windowResize(view) {
|
windowResize(view) {
|
||||||
const mob = window.innerWidth < 640;
|
const mob = window.innerWidth < 640;
|
||||||
if (mob && calendar.view.type !== 'listWeek' && calendar.view.type !== 'timeGridDay') {
|
if (mob && calendar.view.type !== 'listWeek' && calendar.view.type !== 'timeGridDay' && calendar.view.type !== 'dayGridMonth') {
|
||||||
calendar.changeView('listWeek');
|
calendar.changeView('listWeek');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user