- Add Task model with subtasks, priorities, status transitions, dates, hours - Add Comment polymorphic model for tasks/issues/projects - Livewire components: TaskManager (list+filters), TaskForm (modal), TaskDetail, TaskKanban (drag&drop), TaskCalendar (FullCalendar) - TaskPolicy with permissions (view/create/edit/delete/assign/manage all) - Notifications: assigned, status change, overdue, comment added - Daily overdue notification job scheduled - Dashboard widget unifies IssueTask + Task - i18n: en/es/fr/ru (393 keys each) - Routes, navigation, offline sync support - 101 tests passing, Pint compliant on new files
249 lines
16 KiB
PHP
249 lines
16 KiB
PHP
<div>
|
|
<x-slot name="header">
|
|
<div class="flex items-center gap-3">
|
|
<a href="{{ route('companies.manage') }}" class="btn btn-ghost btn-sm px-2" wire:navigate>
|
|
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
|
</a>
|
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
{{ $company ? __('Edit company: :name', ['name' => $company->name]) : __('New company') }}
|
|
</h2>
|
|
</div>
|
|
</x-slot>
|
|
|
|
<div class="py-8">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
|
|
@if(session('notify'))
|
|
<div class="alert alert-success mb-4">{{ session('notify') }}</div>
|
|
@endif
|
|
|
|
<div class="card bg-base-100 shadow">
|
|
<div class="card-body p-8">
|
|
|
|
<form wire:submit.prevent="save" class="space-y-0">
|
|
|
|
@if($errors->any())
|
|
<div class="alert alert-error text-sm mb-6">
|
|
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
|
|
<ul class="list-disc pl-3 space-y-0.5">
|
|
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
|
|
</ul>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ── Macro para fila de formulario horizontal ──────────── --}}
|
|
{{-- Patrón: flex row, label w-48 shrink-0, campo flex-1 --}}
|
|
|
|
{{-- ── Sección: Identificación ──────────────────────────── --}}
|
|
<div class="pb-6 mb-6 border-b border-base-200">
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
|
{{ __('Identification') }}
|
|
</h3>
|
|
<div class="space-y-4">
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Registered name') }} <span class="text-error">*</span>
|
|
</label>
|
|
<div class="flex-1">
|
|
<input type="text" wire:model="name"
|
|
class="input input-bordered w-full"
|
|
placeholder="{{ __('E.g.: Acme Construct, S.L.') }}" />
|
|
@error('name') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Nickname') }}
|
|
</label>
|
|
<div class="flex-1">
|
|
<input type="text" wire:model="nickname"
|
|
class="input input-bordered w-full"
|
|
placeholder="{{ __('E.g.: Acme Construct') }}" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Tax ID / NIF / CIF') }}
|
|
</label>
|
|
<div class="flex-1">
|
|
<input type="text" wire:model="tax_id"
|
|
class="input input-bordered w-full"
|
|
placeholder="{{ __('E.g.: B12345678') }}" />
|
|
@error('tax_id') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Company type') }} <span class="text-error">*</span>
|
|
</label>
|
|
<div class="flex-1">
|
|
<select wire:model="type" class="select select-bordered w-full">
|
|
<option value="">{{ __('Select a type') }}</option>
|
|
<option value="owner">{{ __('Owner / Developer') }}</option>
|
|
<option value="constructor">{{ __('Constructor') }}</option>
|
|
<option value="subcontractor">{{ __('Subcontractor') }}</option>
|
|
<option value="supplier">{{ __('Supplier') }}</option>
|
|
<option value="consultant">{{ __('Consultant / Engineering') }}</option>
|
|
</select>
|
|
@error('type') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Status') }} <span class="text-error">*</span>
|
|
</label>
|
|
<div class="flex-1">
|
|
<select wire:model="status" class="select select-bordered w-full">
|
|
<option value="">{{ __('Select a status') }}</option>
|
|
<option value="active">{{ __('Active') }}</option>
|
|
<option value="inactive">{{ __('Inactive') }}</option>
|
|
<option value="suspended">{{ __('Suspended') }}</option>
|
|
</select>
|
|
@error('status') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ── Sección: Contacto ─────────────────────────────────── --}}
|
|
<div class="pb-6 mb-6 border-b border-base-200">
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
|
{{ __('Contact') }}
|
|
</h3>
|
|
<div class="space-y-4">
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Address') }}
|
|
</label>
|
|
<div class="flex-1">
|
|
<textarea wire:model="address" rows="2"
|
|
class="textarea textarea-bordered w-full"
|
|
placeholder="{{ __('Street, number, city, ZIP, country') }}"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Phone') }}
|
|
</label>
|
|
<div class="flex-1">
|
|
<label class="input input-bordered flex items-center gap-2">
|
|
<x-heroicon-o-phone class="w-4 h-4 opacity-40 shrink-0" />
|
|
<input type="tel" wire:model="phone" class="grow"
|
|
placeholder="+34 600 123 456" />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Email') }}
|
|
</label>
|
|
<div class="flex-1">
|
|
<label class="input input-bordered flex items-center gap-2">
|
|
<x-heroicon-o-envelope class="w-4 h-4 opacity-40 shrink-0" />
|
|
<input type="email" wire:model="email" class="grow"
|
|
placeholder="contacto@empresa.com" />
|
|
</label>
|
|
@error('email') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Website') }}
|
|
</label>
|
|
<div class="flex-1">
|
|
<label class="input input-bordered flex items-center gap-2">
|
|
<x-heroicon-o-globe-alt class="w-4 h-4 opacity-40 shrink-0" />
|
|
<input type="url" wire:model="website" class="grow"
|
|
placeholder="https://www.empresa.com" />
|
|
</label>
|
|
@error('website') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ── Sección: Logo ─────────────────────────────────────── --}}
|
|
<div class="pb-6 mb-6 border-b border-base-200">
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
|
{{ __('Logo') }}
|
|
</h3>
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ $company?->logo_path ? __('Replace logo') : __('Upload logo') }}
|
|
<p class="text-xs text-gray-400 font-normal mt-0.5">{{ __('PNG / JPG, max. 2 MB') }}</p>
|
|
</label>
|
|
<div class="flex-1 flex items-start gap-4">
|
|
{{-- Preview --}}
|
|
@if($logo)
|
|
<img src="{{ $logo->temporaryUrl() }}" alt="Preview"
|
|
class="w-16 h-16 object-contain border border-base-300 rounded-lg shrink-0" />
|
|
@elseif($company?->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($company->logo_path))
|
|
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($company->logo_path) }}"
|
|
alt="{{ __('Current logo') }}"
|
|
class="w-16 h-16 object-contain border border-base-300 rounded-lg shrink-0" />
|
|
@else
|
|
<div class="w-16 h-16 bg-base-200 rounded-lg flex items-center justify-center shrink-0">
|
|
<x-heroicon-o-building-office class="w-7 h-7 opacity-30" />
|
|
</div>
|
|
@endif
|
|
<div class="flex-1">
|
|
<input type="file" wire:model="logo" accept="image/*"
|
|
class="file-input file-input-bordered w-full" />
|
|
<div wire:loading wire:target="logo" class="text-xs text-info mt-1">{{ __('Uploading…') }}</div>
|
|
@error('logo') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ── Sección: Notas ────────────────────────────────────── --}}
|
|
<div class="pb-6 mb-6 border-b border-base-200">
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
|
{{ __('Internal notes') }}
|
|
</h3>
|
|
<div class="flex items-start gap-4">
|
|
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
|
{{ __('Observations') }}
|
|
<p class="text-xs text-gray-400 font-normal mt-0.5">{{ __('Internal information') }}</p>
|
|
</label>
|
|
<div class="flex-1">
|
|
<textarea wire:model="notes" rows="3"
|
|
class="textarea textarea-bordered w-full"
|
|
placeholder="{{ __('Special conditions, observations…') }}"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ── Botones ───────────────────────────────────────────── --}}
|
|
<div class="flex items-center justify-between pt-2">
|
|
<a href="{{ route('companies.manage') }}" class="btn btn-outline gap-1" wire:navigate>
|
|
<x-heroicon-o-x-mark class="w-4 h-4" />
|
|
{{ __('Cancel') }}
|
|
</a>
|
|
<button type="submit" class="btn btn-primary gap-2"
|
|
wire:loading.attr="disabled" wire:target="save">
|
|
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
|
<x-heroicon-o-check class="w-4 h-4" />
|
|
{{ $company ? __('Save changes') : __('Create company') }}
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div> |